Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions packages/core/src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,23 @@ export const configSchema = {
sync: {
type: 'boolean'
},
readiness: {
type: 'object',
additionalProperties: false,
properties: {
preset: { type: 'string', enum: ['balanced', 'strict', 'fast', 'disabled'] },
stabilityWindowMs: { type: 'integer', minimum: 50, maximum: 30000 },
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Naming convention inconsistency with the rest of the config schema

The readiness properties use camelCase (stabilityWindowMs, networkIdleWindowMs, timeoutMs) but the internal readiness implementation uses snake_case, and other Percy config options like enable_javascript, dom_transformation, reshuffle_invalid_tags use snake_case in the schema.

Consider aligning to snake_case (stability_window_ms, network_idle_window_ms, timeout_ms) for consistency with the rest of the Percy config, or add explicit camelCase to snake_case mapping in readiness.js.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I verified this and I think the premise here might be inverted. Grepping the existing schema in packages/core/src/config.js:

enableJavaScript, cliEnableJavaScript, disableShadowDOM,
forceShadowAsLightDOM, enableLayout, domTransformation, reshuffleInvalidTags,
deferUploads, useSystemProxy, skipBaseBuild, browserName, browserVersion,
osVersion, deviceName, percyBrowserCustomName, diffSensitivity,
imageIgnoreThreshold, carouselsEnabled, bannersEnabled, adsEnabled,
minHeight, percyCSS, scopeOptions, ignoreRegionSelectors, freezeAnimation,
freezeAnimatedImage, responsiveSnapshotCapture, testCase, thTestCaseExecutionId,
fullPage ...

Every existing snapshot option in the schema is already camelCase — there are no enable_javascript / dom_transformation / reshuffle_invalid_tags entries in config.js. So the readiness schema (stabilityWindowMs, timeoutMs, etc.) is actually consistent with the rest of the file.

The snake_case you're seeing in readiness.js is the internal naming used after normalization (matches the diagnostic output keys like stability_window_ms, network_idle_window_ms, which are the payload contract with the backend). The normalizeOptions() helper bridges the two, and it accepts both forms so direct snake_case overrides still work if anyone is using them.

Happy to rename if you'd still prefer snake_case for the user-facing schema, but the current naming was chosen specifically to match the surrounding camelCase conventions.

jsIdleWindowMs: { type: 'integer', minimum: 50, maximum: 30000 },
networkIdleWindowMs: { type: 'integer', minimum: 50, maximum: 10000 },
timeoutMs: { type: 'integer', minimum: 1000, maximum: 60000 },
imageReady: { type: 'boolean' },
fontReady: { type: 'boolean' },
jsIdle: { type: 'boolean' },
readySelectors: { type: 'array', items: { type: 'string' } },
notPresentSelectors: { type: 'array', items: { type: 'string' } },
maxTimeoutMs: { type: 'integer', minimum: 1000, maximum: 60000 }
}
},
responsiveSnapshotCapture: {
type: 'boolean',
default: false
Expand Down Expand Up @@ -489,6 +506,7 @@ export const snapshotSchema = {
domTransformation: { $ref: '/config/snapshot#/properties/domTransformation' },
enableLayout: { $ref: '/config/snapshot#/properties/enableLayout' },
sync: { $ref: '/config/snapshot#/properties/sync' },
readiness: { $ref: '/config/snapshot#/properties/readiness' },
responsiveSnapshotCapture: { $ref: '/config/snapshot#/properties/responsiveSnapshotCapture' },
testCase: { $ref: '/config/snapshot#/properties/testCase' },
labels: { $ref: '/config/snapshot#/properties/labels' },
Expand Down Expand Up @@ -682,6 +700,10 @@ export const snapshotSchema = {
type: 'array',
items: { type: 'string' }
},
readiness_diagnostics: {
type: 'object',
description: 'Diagnostics from readiness checks run before serialization'
},
corsIframes: {
type: 'array',
items: {
Expand Down
17 changes: 13 additions & 4 deletions packages/core/src/page.js
Original file line number Diff line number Diff line change
Expand Up @@ -214,14 +214,23 @@ export class Page {
await this.insertPercyDom();

// serialize and capture a DOM snapshot
// Readiness config is passed to PercyDOM.serialize() so that readiness
// checks run BEFORE serialization — in both URL-based and SDK paths.
// The readiness config comes from per-snapshot options or the global
// Percy config (injected as window.__PERCY__.config.snapshot.readiness).
let readiness = snapshot.readiness || this.browser?.percy?.config?.snapshot?.readiness;
this.log.debug('Serialize DOM', this.meta);

// Use serializeDOMWithReadiness so readiness runs BEFORE serialize in the
// URL-capture path. Existing SDKs continue calling the sync serializeDOM.
// page.eval uses CDP awaitPromise: true, which auto-awaits the returned Promise.
/* istanbul ignore next: no instrumenting injected code */
let capture = await this.eval((_, options) => ({
let capture = await this.eval(async (_, options) => {
/* eslint-disable-next-line no-undef */
domSnapshot: PercyDOM.serialize(options),
url: document.URL
}), { enableJavaScript, disableShadowDOM, forceShadowAsLightDOM, domTransformation, reshuffleInvalidTags, ignoreCanvasSerializationErrors, ignoreStyleSheetSerializationErrors, pseudoClassEnabledElements });
let fn = (PercyDOM.serializeDOMWithReadiness || PercyDOM.serialize);
let domSnapshot = await fn(options);
return { domSnapshot, url: document.URL };
}, { enableJavaScript, disableShadowDOM, forceShadowAsLightDOM, domTransformation, reshuffleInvalidTags, ignoreCanvasSerializationErrors, ignoreStyleSheetSerializationErrors, pseudoClassEnabledElements, readiness });

return { ...snapshot, ...capture };
}
Expand Down
11 changes: 11 additions & 0 deletions packages/core/src/snapshot.js
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,17 @@ export function validateSnapshotOptions(options) {
log.warn('Encountered snapshot serialization warnings:');
for (let w of domWarnings) log.warn(`- ${w}`);
}

// log readiness diagnostics when present (SDK-submitted snapshots with readiness enabled)
let readinessDiag = migrated.domSnapshot?.readiness_diagnostics;
if (readinessDiag) {
if (readinessDiag.timed_out) {
log.warn(`Readiness timed out after ${readinessDiag.total_duration_ms}ms (preset: ${readinessDiag.preset || 'custom'})`);
} else {
log.debug(`Readiness passed in ${readinessDiag.total_duration_ms}ms (preset: ${readinessDiag.preset || 'custom'})`);
}
}

// warn on validation errors
let errors = PercyConfig.validate(migrated, schema);
if (errors?.length > 0) {
Expand Down
2 changes: 1 addition & 1 deletion packages/core/test/percy.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ describe('Percy', () => {
});

// expect required arguments are passed to PercyDOM.serialize
expect(evalSpy.calls.allArgs()[3]).toEqual(jasmine.arrayContaining([jasmine.anything(), { enableJavaScript: undefined, disableShadowDOM: true, domTransformation: undefined, reshuffleInvalidTags: undefined, ignoreCanvasSerializationErrors: undefined, ignoreStyleSheetSerializationErrors: undefined, forceShadowAsLightDOM: undefined, pseudoClassEnabledElements: undefined }]));
expect(evalSpy.calls.allArgs()[3]).toEqual(jasmine.arrayContaining([jasmine.anything(), { enableJavaScript: undefined, disableShadowDOM: true, domTransformation: undefined, reshuffleInvalidTags: undefined, ignoreCanvasSerializationErrors: undefined, ignoreStyleSheetSerializationErrors: undefined, forceShadowAsLightDOM: undefined, pseudoClassEnabledElements: undefined, readiness: undefined }]));

expect(snapshot.url).toEqual('http://localhost:8000/');
expect(snapshot.domSnapshot).toEqual(jasmine.objectContaining({
Expand Down
3 changes: 3 additions & 0 deletions packages/dom/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ export {
serializeDOM,
// namespace alias
serializeDOM as serialize,
serializeDOMWithReadiness,
waitForResize
} from './serialize-dom';

export { loadAllSrcsetLinks } from './serialize-image-srcset';

export { waitForReady } from './readiness';
Loading
Loading