cli icon indicating copy to clipboard operation
cli copied to clipboard

Adds async blob URL serialization for DOM snapshots

Open bs-shobhitkumar opened this issue 2 months ago • 3 comments

Ticket: https://browserstack.atlassian.net/browse/PER-5498

This pull request introduces asynchronous handling of dynamic resources—especially blob URLs—during DOM serialization in the Percy core and DOM packages. The main changes include making the DOM serialization pipeline fully async, adding a new module to preprocess and convert blob URLs (and handle lazy-loaded images), and updating related code and tests to support these asynchronous operations.

Async DOM Serialization & Resource Handling

  • Made the entire DOM serialization process asynchronous to support dynamic resources, such as blob URLs and lazy-loaded images, ensuring all resources are captured and converted before snapshotting. (packages/dom/src/serialize-dom.js, packages/dom/src/serialize-frames.js, packages/core/src/page.js, packages/core/src/api.js) [1] [2] [3] [4] [5] [6] [7] [8]

  • Added a new module, serialize-blob-urls.js, which finds all blob URLs in the DOM, converts them into Percy resources asynchronously, and rewrites DOM references to use the new resource URLs. It also processes lazy-loaded images by resolving their sources. (packages/dom/src/serialize-blob-urls.js)

Test Updates for Async Serialization

  • Updated all affected tests to use await when calling the now-async serializeDOM function, ensuring correct test execution and coverage for the new async flow. (packages/dom/test/serialize-base64.test.js, packages/dom/test/serialize-canvas.test.js, packages/core/test/api.test.js, packages/core/test/discovery.test.js) [1] [2] [3] [4] [5] [6] [7] [8] [9] [10] [11] [12] [13] [14] [15]

API and Percy Agent Updates

  • Updated the Percy Agent wrapper and API server to support async snapshotting, ensuring the new async serialization is correctly exposed and used in Percy’s runtime environment. (packages/core/src/api.js) [1] [2]

These enhancements ensure Percy can accurately capture and serialize complex, dynamic web pages—including those using blob URLs and lazy-loading techniques—by waiting for all resources to be processed before completing a snapshot.

bs-shobhitkumar avatar Nov 03 '25 21:11 bs-shobhitkumar

  1. How is backwards compatibility maintained here ?
  2. How does it work with non async supporting SDKs

ninadbstack avatar Nov 06 '25 06:11 ninadbstack

@ninadbstack I checked the flow again:

  1. For JS SDKs like @percy/selenium-webdriver, @percy/playwright, @percy/puppeteer:
  • Already uses await everywhere
  • driver.executeScript() with async function automatically handles Promise
  1. For legacy JavaScript SDKs (Deprecated) like @percy/seleniumjs:
  • async functions always return Promises and promises work with .then() (ES6 feature since 2015)
  1. For non-JavaScript SDKs like percy-selenium-java, percy-selenium-python, percy-selenium-ruby, percy-selenium-dotnet:
  • Non-JS SDKs never call PercyDOM.serialize() directly, percy CLI handles all browser interaction

Internal flow:

# 1. Python SDK sends session details to Percy CLI
requests.post('http://localhost:5338/percy/automateScreenshot', json={
    'sessionId': driver.session_id,
    'commandExecutorUrl': 'http://localhost:4444/wd/hub',
    'capabilities': driver.capabilities,
    'snapshotName': 'Homepage',
    'options': {}
})

// 2. Percy CLI receives request
// packages/core/src/api.js
.route('post', '/percy/automateScreenshot', async (req, res) => {
  // Percy CLI takes over completely!
  const result = await WebdriverUtils.captureScreenshot(req.body);
  return res.json(200, result);
})

// 3. Percy CLI uses its OWN browser via webdriver-utils
// packages/webdriver-utils/src/index.js
static async captureScreenshot({ sessionId, commandExecutorUrl, ... }) {
  // Create driver using SDK's session
  await provider.createDriver();
  
  // Percy CLI navigates and captures
  const comparisonData = await provider.screenshot(snapshotName, options);
  
  return comparisonData;
}

// 4. Percy CLI uses Page class for DOM serialization
// packages/core/src/page.js
async snapshot(options) {
  await this.insertPercyDom();  // Inject @percy/dom
  
  // Percy CLI executes ASYNC serialization
  let capture = await this.eval(async (_, options) => {
    return {
      domSnapshot: await PercyDOM.serialize(options),  // ← ASYNC!
      url: document.URL
    };
  }, options);
  
  return capture;
}

So the flow is:

SDK Layer:        await percySnapshot(driver, name)
                           ↓
Browser Layer:    await driver.executeScript(async () => ...)
                           ↓
Percy DOM Layer:  await PercyDOM.serialize()
                           ↓
Blob Processing:  await preprocessDynamicResources()

Also, as mentioned eariler i've verified it by running builds for percy-selenium-java, percy-selenium-python, percy-selenium-dotnet, cypress and it's not breaking while running E2E

Please let me know if I'm missing something on this or there's anything specific that I should test for this

bs-shobhitkumar avatar Nov 06 '25 10:11 bs-shobhitkumar

Non-JS SDKs never call PercyDOM.serialize() directly, percy CLI handles all browser interaction this is not true - please check the sdks[ ex

ninadbstack avatar Nov 06 '25 10:11 ninadbstack