TESTEROPS

A pragmatic approach to QA and OPS

Playing with SVG – Playwright

In the last blog post, I posted about the ways Playwright can be used to handle elements inside shadow root – both open and closed shadow root.

I got responses on the poll to post something new every week about Playwright. So this week we’re going to see how we can handle a chart , made with svg elements.

svg or scalable vector graphics is defined by MDN as

The svg element is a container that defines a new coordinate system and viewport. It is used as the outermost element of SVG documents, but it can also be used to embed an SVG fragment inside an SVG or HTML document.

SVG is most popularly used in making graphics and animations just like HTMLCanvas element.  It is a type of vector graphic that may be scaled up or down. Elements are the core things that is required to work with SVGs.

Below is an example of a svg element block.

<svg
  viewBox="0 0 300 100"
  xmlns="http://www.w3.org/2000/svg"
  stroke="red"
  fill="grey">
  <circle cx="50" cy="50" r="40" />
  <circle cx="150" cy="50" r="4" />

  <svg viewBox="0 0 10 10" x="200" width="100">
    <circle cx="5" cy="5" r="4" />
  </svg>
</svg>

HANDLING SVG

svg elements are one of those elements which are bit tricky to automate. In Selenium, folks generally use the xpaths and no other locators to get to the svg elements. Since svg elements generally have height and width properties attached to the, most folks find it hard to automate them. You can still find many good articles on how to automate using Selenium like this, and Naveen’s video on this topic is also very good.

Since Playwright has a very good mechanism of allowing to use various locators, so in this blog, we’ll try to avoid using the xpath and see if we can get the elements and get the values.

We’ll use this website -> https://frappe.io/charts and see if we can automate and fetch values from the chart below.

PROBLEM STATEMENT

When you go to the website mentioned above, and scroll down to see the charts, you’ll see that there are two charts – a line chart and a bar chart.

The line chart shows the value per year of the Fireball events, and if you click on each line in the line chart, then you’ll see corresponding monthly value in the bar chart for that year.

So what we’re trying to do here, is click on each year in the line chart and get the corresponding values for each month from the bar chart.

For e.g, if you click on the line chart for the year 2007, then you will see corresponding values for each month in the bar chart below – see the image

The total events for the year 2007 is 152, and ordered by each month is shown in the bar chart below.

So what we will do is –

  • Get all the year values and all event values from chart 1.
  • Click on each event from line chart 1 and get corresponding monthly value from bar chart 2.

FETCHING VALUES FROM LINE CHART

Each of the charts is housed inside a figure tag. For the line chart, it has an id line-composite-1 , where as for the bar chart it is bar-composite-1.

We’ll start with the line chart first and get the values. So we’ve a starting point for the line chart which is the figure tag with id line-composite-1.

The svg components are housed in the g tag – which is

The <g> element is used to group SVG shapes together. Once grouped you can transform the whole group of shapes as if it was a single shape. This is an advantage compared to a nested <svg> element which cannot be the target of transformation by itself.

The g tag has class of line-chart chart-draw-area , which we can utilise.

Now, in Playwright, if you want to filter an element, which is nested inside another element you can use this syntax

page.locator('article', { has: page.locator('button.subscribe') })

So to get the parent g tag, we can use

const parentelement = await page.locator('#line-composite-1', { has: page.locator('g.line-chart.chart-draw-area') });

Now inside that g tag, there are multiple other g tags, which get the x-axis and the y-axis of the chart and other details. Open each g tag and see that the tag with the class dataset-units dataset-line dataset-0 houses other nested g tags, inside which there is text tag, which contains the text for the value for the year.

By the way, I always use SelectorsHub extension on Chrome to get the xpath/css selectors for my Selenium/Playwright test cases/scenarios.

To get all the text content inside the nested g tags, we can use something like this

const letcount = await parentelement.locator('g.dataset-units.dataset-line.dataset-0').locator('g');
const texts = await letcount.allTextContents();

So, we tried the above and see that this works. We’re able to get the values of the yearly events from the line chart.

Now, we’ve to get the value of the years. Carefully look at the source code of the charts using the Google console.

Similar to how we got the text inside the g tag, we can utilise the elements which are housed inside the figure tag with the class bar-composite-1., and can use the filtering technique mentioned above to get the elements.

const secondbarparent = await page.locator('#bar-composite-1',{has: page.locator('g.bar-chart.chart-draw-area')});

Inside this element is another set of elements with the class g.dataset-units.dataset-bars.dataset-2, which again has a g tag, which contains the text that is being shown on the bar chart

See the image below to understand

Now we will create a function that gets this text contents from all these elements

const gettext = async () => {
    const secondbarparent = await page.locator('#bar-composite-1',{has: page.locator('g.bar-chart.chart-draw-area')});
    const totalbarcount = await secondbarparent.locator('g.dataset-units.dataset-bars.dataset-2').locator('g');
    const bartext = await totalbarcount.allTextContents();
    await console.log(`${bartext}`);
   }

So what now? In order to get the text of all monthly elements in the bar charts, that correspond to all the yearly elements in the line chart, we will have to iterate the gettext() method for each element for the line chart

So let’s do that and reach at a final code which is given below

import { chromium,test } from "@playwright/test";
test.use({ viewport: { width: 1400, height: 1000 } });
test('Launch the charts page and get values',async()=>{
const browser = await chromium.launch({
headless: false
});
const context = await browser.newContext();
const page = await context.newPage();
await page.goto("https://frappe.io/charts&quot;);
await page.waitForSelector('#line-composite-1',{
state: "visible"
});
const parentelement = await page.locator('#line-composite-1', { has: page.locator('g.line-chart.chart-draw-area') });
const letcount = await parentelement.locator('g.dataset-units.dataset-line.dataset-0').locator('g');
const circletoclick = await parentelement.locator('g.dataset-units.dataset-line.dataset-0').locator('g >> circle');
const x_axis = await parentelement.locator("g.x.axis g");
const texts = await letcount.allTextContents();
const x_axis_text = await x_axis.allTextContents();
await console.log(`${texts}`);
await console.log(`${x_axis_text}`);
const gettext = async () => {
const secondbarparent = await page.locator('#bar-composite-1',{has: page.locator('g.bar-chart.chart-draw-area')});
const totalbarcount = await secondbarparent.locator('g.dataset-units.dataset-bars.dataset-2').locator('g');
const bartext = await totalbarcount.allTextContents();
await console.log(`${bartext}`);
}
const count2 = await circletoclick.count();
for (let i =0;i< count2;i++){
await circletoclick.nth(i).click();
// await console.log(`${bartext}`);
await page.waitForTimeout(1000);
await gettext();
await page.waitForTimeout(1000);
}
});

Once we run the code we can see that it iterates over to each element in line chart and then gets value of the bar chart elements

So in this way we can

  • Filter one element or a list of elements from inside of another element.
  • You can interact with the g elements of the svg.
  • Iterate over a list of element and get text of inside elements.

While the iteration logic has been written in the tradition for loop approach, we can also use other approaches for this. If you think we can optimise this, feel free to comment on this post and I’ll test and post an update on this.

%d bloggers like this: