Adds async blob URL serialization for DOM snapshots
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
awaitwhen calling the now-asyncserializeDOMfunction, 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.
- How is backwards compatibility maintained here ?
- How does it work with non async supporting SDKs
@ninadbstack I checked the flow again:
- For JS SDKs like @percy/selenium-webdriver, @percy/playwright, @percy/puppeteer:
- Already uses await everywhere
- driver.executeScript() with async function automatically handles Promise
- For legacy JavaScript SDKs (Deprecated) like @percy/seleniumjs:
- async functions always return Promises and promises work with .then() (ES6 feature since 2015)
- 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
Non-JS SDKs never call PercyDOM.serialize() directly, percy CLI handles all browser interaction this is not true - please check the sdks[ ex