Screenshot a single element

👍

Covered in this doc

  • How to scope a screenshot to a single element

📘

Requires @percy/cli v1.3.0+

Sometimes capturing a full-page screenshot isn't necessary. For example, if there are dynamic parts of the page that you don't need to test or are only interested in a very specific region to test. For these cases, you can pass a scope snapshot option and Percy will only capture the scoped element on the given widths. This can be passed as a global snapshot option or as a per-snapshot option.

The scope selector accepts any valid selector you would be able to pass to document.querySelector.

🚧

If there are multiple matching selectors on the page, Percy will select the first matching element.

Global example:

version: 2
snapshot:
	scope: '.selector'

The specific syntax used for this will vary based on your SDK, but the same concept applies. Per-snapshot example:

percySnapshot('name', { scope: '.selector' });
percySnapshot(driver, 'name', { scope: '.selector' });
Percy.snapshot(driver, 'name', { scope: '.selector' })
percy_snapshot(driver=driver, name='name', scope='.selector');
Map<String, Object> options = new HashMap<String, Object>();
options.put("scope", ".selector");
percy.snapshot("Site with options", options);

Multiple elements with the same selector


If you would like to scope a screenshot to a specific element that has the same matching selector as other elements on the page you'll have to get more specific with your selector. This can be done by either adding another unique selector to that element or by using standard CSS selectors to get more specific. This is the same way you would write CSS -- Percy doesn't add anything to this process.

For example, given the below DOM:

<html>
  <head>
    <title>Example</title>
  </head>
  <body>
    <h1 class="underline">My example</h1>
    <p class="underline">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas tristique convallis sem, vitae sodales risus accumsan in</p>
    <p class="underline">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas tristique convallis sem, vitae sodales risus accumsan in</p>
  </body>
</html>

Instead of using just .underline to select the element, you would want to either specify the element type (h1 / p) or by using CSS tree-structural pseudo-classes like :last-of-type or :nth-child.

percySnapshot(driver, 'name', { scope: 'h1.underline' }); // selects the h1
percySnapshot(driver, 'name', { scope: 'p.underline' }); // selects the first paragraph
percySnapshot(driver, 'name', { scope: 'p.underline:last-of-type' }); // selects last paragraph

Selector for elements serialized by Percy


It may happen that your single-element screenshots are not working correctly with canvas/video elements.

During DOM serialization <canvas>, <video> elements are converted to <img> [ know more how Percy works - ref] This can cause the selector to not match, and an incorrect screenshot.

To work around this, we can use CSS tree structural pseudo-classes as suggested in the previous section.

<!-- Skipping most of the stuff for brevity -->

<!-- Original DOM -->
<div class="selector">
  <canvas> </canvas>
  <canvas> </canvas>
  <video> </video>
</div>

<!-- Post DOM Serialization -->
<div class="selector">
  <img> </img>
  <img> </img>
  <img> </img>
</div>
  • Instead of canvas or video you'll need to change it to img
  • If there is only a single canvas/video tag inside div we can directly use .selector as the scope.
percySnapshot(driver, 'name', { scope: '.selector img:nth-of-type(1)' }); // selects first canvas
percySnapshot(driver, 'name', { scope: '.selector img:nth-of-type(2)' }); // selects second canvas
percySnapshot(driver, 'name', { scope: '.selector img:last-of-type' }); // selects last video

Usage when having Selenium's WebElement


When dealing with Web elements, you may use the below utility function that returns CSS selector i.e scope in our case, that could be easily passed to the percySnapshot function.

🚧

Make sure script eval is allowed in the test browser, Please check ref on how to verify.

async function percyScope(driver, webElement) {
  // # util for getting css selector using the DOM element
  script = `
        if (typeof UTILS === 'undefined') {
            return fetch('https://gist.githubusercontent.com/itsjwala/a7ccb4d0ae4ceb5a9fc33de2823f0bf1/raw/9c5a1044794b5f8aa9dfba17884e01bf71019d67/dom_css_path.js')
                .then(res => res.text())
                .then(eval)
                .then(_ => UTILS.cssPath(arguments[0]));
        } else {
            return UTILS.cssPath(arguments[0]);
        }
        `
  return await driver.executeScript(script, webElement)
}

// percySnapshot(driver, 'name', { scope: await percyScope(driver, webElement) });
def percy_scope(driver, web_element):
    # util for getting css selector using the DOM element
    script = """
            if(typeof UTILS === 'undefined') {
                return fetch('https://gist.githubusercontent.com/itsjwala/a7ccb4d0ae4ceb5a9fc33de2823f0bf1/raw/9c5a1044794b5f8aa9dfba17884e01bf71019d67/dom_css_path.js')
                        .then(res => res.text())
                        .then(eval)
                        .then( _ => UTILS.cssPath(arguments[0]));
            } else {
                return UTILS.cssPath(arguments[0]);
            }
            """
    return driver.execute_script(script, web_element)

# percy_snapshot(driver=driver, name='name', scope=percy_scope(driver,web_element));
public String percyScope(WebDriver driver, WebElement webElement) {
    //  util for getting css selector using the DOM element
    String script = String.join(System.getProperty("line.separator"),
        "if(typeof UTILS === 'undefined') {",
        "return fetch('https://gist.githubusercontent.com/itsjwala/a7ccb4d0ae4ceb5a9fc33de2823f0bf1/raw/9c5a1044794b5f8aa9dfba17884e01bf71019d67/dom_css_path.js')",
        ".then(res => res.text())",
        ".then(eval)",
        ".then( _ => UTILS.cssPath(arguments[0]));",
        "} else {",
        "return UTILS.cssPath(arguments[0]);",
        "}"
    );
    JavascriptExecutor jse = (JavascriptExecutor) driver;
    return (String) jse.executeScript(script, webElement);
}