From 9e5a20555518f75fabf254cb712994368335cc59 Mon Sep 17 00:00:00 2001 From: rishigupta1599 Date: Tue, 18 Aug 2026 15:08:49 +0530 Subject: [PATCH 1/9] feat(webdriver-utils): add scaleToFit for full-page Automate captures (PER-10530) Full-page POA captures are capped at a 50,000px stitched-image budget, which mobile-common enforces by truncating the DOM walk at MAX_PIXEL_HEIGHT_LIMIT/dpr -- 16,666 CSS px at DPR 3. Pages taller than that lose their tail silently. scaleToFit opts a capture into downscaling every tile by a fixed 1/dpr instead, so a page of up to 50,000 CSS px fits the same budget. The factor is fixed at construction and deliberately NOT derived from the measured page height: Percy diffs pixel to pixel and a uniform resize moves both dimensions, so a height-derived factor would make output WIDTH a function of page height. A page drifting across a threshold would then change width, leaving snapshot and baseline with different dimensions -- unalignable, not merely different. Two pieces here: - Schema entry on /config/snapshot. The section sets additionalProperties: false, so without a declaration the key is rejected before it ever reaches the Automate session. Gated onlyAutomate, like fullPage. - PERCY_SCALE_TO_FIT in addDefaultOptions(), so a whole run can opt in without editing per-snapshot config -- how support enables this for a customer hitting the truncation. Coerced to a real boolean, because mobile-common compares with `== true` and a truthy string would silently no-op. No provider changes: automateProvider already forwards `options` wholesale, and SeleniumHub passes it through verbatim, so the camelCase spelling is the contract with mobile-common. Requires browserstack/mobile-common#1255. Verified: webdriver-utils 240/240, core unit/config 21/21, eslint clean. Co-Authored-By: Claude Opus 5 (1M context) --- packages/core/src/config.js | 8 +++++ packages/core/test/unit/config.test.js | 30 +++++++++++++++++++ .../src/providers/genericProvider.js | 6 ++++ .../test/providers/genericProvider.test.js | 30 +++++++++++++++++++ 4 files changed, 74 insertions(+) diff --git a/packages/core/src/config.js b/packages/core/src/config.js index 0fcf5abf9..2db93173e 100644 --- a/packages/core/src/config.js +++ b/packages/core/src/config.js @@ -202,6 +202,14 @@ export const configSchema = { type: 'boolean', onlyAutomate: true }, + // Opts a full-page Automate capture into downscaling each tile by 1/dpr, so the + // stitched image stays inside the 50,000px comparison ceiling. Without it the + // walkable DOM height is capped at 50000/dpr CSS px and taller pages are truncated. + // Only honoured on the fullPage path -- mobile-common ignores it for singlepage. + scaleToFit: { + type: 'boolean', + onlyAutomate: true + }, freezeAnimation: { // for backward compatibility type: 'boolean', onlyAutomate: true diff --git a/packages/core/test/unit/config.test.js b/packages/core/test/unit/config.test.js index 539184bd1..dfb94c84d 100644 --- a/packages/core/test/unit/config.test.js +++ b/packages/core/test/unit/config.test.js @@ -116,6 +116,36 @@ describe('SnapshotSchema', () => { expect(errors[0].path).toBe('scope'); expect(errors[0].message).toBe('must have property scope when property scopeOptions is present'); }); + + // Automate-only options live on /config/snapshot (the .percy.yml `snapshot:` section), + // NOT on /snapshot -- which $refs only a hand-picked subset and so reports fullPage as + // an unknown property too. /config/snapshot sets additionalProperties: false, so an + // undeclared key is rejected there: the reason scaleToFit needs a schema entry and not + // just provider plumbing. + // Structural assertion rather than a PercyConfig.validate() round-trip: the + // onlyAutomate keyword is evaluated when AJV COMPILES the schema, so a validate() call + // reflects whatever PERCY_TOKEN was set when the schema was first added in this process + // -- flipping the env var inside a spec cannot change the outcome. Asserting the + // declaration directly is what actually pins the change. + it('declares scaleToFit as an automate-only boolean', () => { + expect(CoreConfig.schemas[0].snapshot.properties.scaleToFit) + .toEqual({ type: 'boolean', onlyAutomate: true }); + }); + + // ...and this proves it is really wired into validation with the same gating as + // fullPage, not just present as an inert key. + it('flags scaleToFit on a non-automate token, exactly like fullPage', () => { + PercyConfig.addSchema(CoreConfig.schemas); + const errors = PercyConfig.validate({ fullPage: true, scaleToFit: true }, '/config/snapshot'); + const paths = errors.map(e => e.path); + const messages = new Set(errors.map(e => e.message)); + + expect(paths).toContain('scaleToFit'); + expect(paths).toContain('fullPage'); + // Same message as fullPage, and crucially NOT 'unknown property' -- an unknown-property + // error would mean the schema entry is missing and this spec passes vacuously. + expect([...messages]).toEqual(['property only valid with Automate integration.']); + }); }); describe('ComparisonSchema - elementSelectorsData', () => { diff --git a/packages/webdriver-utils/src/providers/genericProvider.js b/packages/webdriver-utils/src/providers/genericProvider.js index 01a1a5dc1..158b9b570 100644 --- a/packages/webdriver-utils/src/providers/genericProvider.js +++ b/packages/webdriver-utils/src/providers/genericProvider.js @@ -48,6 +48,12 @@ export default class GenericProvider { addDefaultOptions() { this.options.freezeAnimation = this.options.freezeAnimatedImage || this.options.freezeAnimation || false; + // PERCY_SCALE_TO_FIT lets a whole run opt in without touching per-snapshot config, + // which is how support enables it for a customer hitting the 50,000px truncation. + // Coerced to a real boolean: mobile-common compares with `== true`, so a truthy + // string would silently fail to enable scaling. + this.options.scaleToFit = this.options.scaleToFit === true || + process.env.PERCY_SCALE_TO_FIT === 'true'; } async createDriver() { diff --git a/packages/webdriver-utils/test/providers/genericProvider.test.js b/packages/webdriver-utils/test/providers/genericProvider.test.js index 8e283cf8f..dce254659 100644 --- a/packages/webdriver-utils/test/providers/genericProvider.test.js +++ b/packages/webdriver-utils/test/providers/genericProvider.test.js @@ -51,6 +51,36 @@ describe('GenericProvider', () => { provider.addDefaultOptions(); expect(provider.options.freezeAnimation).toBeFalse(); }); + + it('enables scaleToFit from the option or PERCY_SCALE_TO_FIT', () => { + let provider = new GenericProvider({ options: { scaleToFit: true } }); + provider.addDefaultOptions(); + expect(provider.options.scaleToFit).toBeTrue(); + + process.env.PERCY_SCALE_TO_FIT = 'true'; + provider = new GenericProvider({ options: {} }); + provider.addDefaultOptions(); + expect(provider.options.scaleToFit).toBeTrue(); + + delete process.env.PERCY_SCALE_TO_FIT; + provider = new GenericProvider({ options: {} }); + provider.addDefaultOptions(); + expect(provider.options.scaleToFit).toBeFalse(); + }); + + // mobile-common compares the forwarded option with `== true`, so anything that is + // merely truthy would arrive as a no-op and silently truncate the page instead. + it('coerces scaleToFit to a real boolean', () => { + const provider = new GenericProvider({ options: { scaleToFit: 'true' } }); + provider.addDefaultOptions(); + expect(provider.options.scaleToFit).toBeFalse(); + + process.env.PERCY_SCALE_TO_FIT = '1'; + const other = new GenericProvider({ options: {} }); + other.addDefaultOptions(); + expect(other.options.scaleToFit).toBeFalse(); + delete process.env.PERCY_SCALE_TO_FIT; + }); }); describe('supports', () => { From baf5f5dec814ca1ae065494ba1b31a548a89fb6b Mon Sep 17 00:00:00 2001 From: rishigupta1599 Date: Tue, 18 Aug 2026 15:16:25 +0530 Subject: [PATCH 2/9] feat(webdriver-utils): forward the scaleToFit factor into comparison metadata The previous commit plumbs the option INTO the Automate session. This carries the result back OUT: mobile-common reports `scale_to_fit` and `applied_scale_factor` in its response, but getTiles() built metadata from `screenshotType` alone, so neither ever reached percy-api. percy-api needs the factor to relax calculate_max_tiles_limit. A scaleToFit capture walks a taller page and therefore returns roughly 1/factor times the usual tile count -- so without this the extended walk is pointless: the page is rejected at the tile-count gate instead of being truncated upstream. Sending only a boolean would leave the API guessing a worst-case DPR, so send the magnitude that was actually applied. Both keys are added only when mobile-common reports it really scaled. They land in comparison_details.metadata -- a 1:1 child of comparisons and the largest table on the platform -- so the default path must not grow every row with constants. Verified: webdriver-utils 242/242, eslint clean. Co-Authored-By: Claude Opus 5 (1M context) --- .../src/providers/automateProvider.js | 10 +++++ .../test/providers/automateProvider.test.js | 44 +++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/packages/webdriver-utils/src/providers/automateProvider.js b/packages/webdriver-utils/src/providers/automateProvider.js index eab956812..969dadeb8 100644 --- a/packages/webdriver-utils/src/providers/automateProvider.js +++ b/packages/webdriver-utils/src/providers/automateProvider.js @@ -96,6 +96,16 @@ export default class AutomateProvider extends GenericProvider { const metadata = { screenshotType: screenshotType }; + // Forwarded so percy-api can relax its tile-count limit by exactly the amount each + // tile shrank: a scaleToFit capture walks a taller page and so returns ~1/factor times + // the usual tile count, which the unrelaxed limit would reject. + // Added only when mobile-common reports it actually scaled -- these land in + // comparison_details.metadata, the largest table on the platform, so the default path + // must not add constants to every row. + if (tileResponse.scale_to_fit === true) { + metadata.scaleToFit = true; + metadata.appliedScaleFactor = tileResponse.applied_scale_factor; + } return { tiles: tiles, domInfoSha: tileResponse.dom_sha, diff --git a/packages/webdriver-utils/test/providers/automateProvider.test.js b/packages/webdriver-utils/test/providers/automateProvider.test.js index ad7573d74..f8fb11a25 100644 --- a/packages/webdriver-utils/test/providers/automateProvider.test.js +++ b/packages/webdriver-utils/test/providers/automateProvider.test.js @@ -331,6 +331,50 @@ describe('AutomateProvider', () => { expect(res).toEqual(expectedOutput); }); + // percy-api relaxes its tile-count limit by this factor, so it has to reach the + // comparison payload -- a scaleToFit capture walks a taller page and returns ~1/factor + // times the usual tile count, which the unrelaxed limit would reject. + it('forwards scaleToFit and the applied factor into metadata when scaling happened', async () => { + const response = { + success: true, + result: JSON.stringify({ + tiles: [{ sha: 'abc', index: 0 }], + dom_sha: 'def', + scale_to_fit: true, + applied_scale_factor: 0.38095238095 + }) + }; + spyOn(AutomateProvider.prototype, 'browserstackExecutor') + .and.returnValue(Promise.resolve({ value: JSON.stringify(response) })); + await automateProvider.createDriver(); + const res = await automateProvider.getTiles(false); + + expect(res.metadata).toEqual({ + screenshotType: 'fullpage', + scaleToFit: true, + appliedScaleFactor: 0.38095238095 + }); + }); + + // These land in comparison_details.metadata, the largest table on the platform, so + // the default path must not add constants to every row. + it('omits the scaleToFit metadata keys when mobile-common did not scale', async () => { + const response = { + success: true, + result: JSON.stringify({ + tiles: [{ sha: 'abc', index: 0 }], + dom_sha: 'def', + scale_to_fit: false + }) + }; + spyOn(AutomateProvider.prototype, 'browserstackExecutor') + .and.returnValue(Promise.resolve({ value: JSON.stringify(response) })); + await automateProvider.createDriver(); + const res = await automateProvider.getTiles(false); + + expect(res.metadata).toEqual({ screenshotType: 'fullpage' }); + }); + it('should return default values of header and footer if not in response', async () => { const response = { success: true, From 2b974e1156dba9f77dcfd738eebe915c678dd205 Mon Sep 17 00:00:00 2001 From: rishigupta1599 Date: Tue, 18 Aug 2026 18:29:26 +0530 Subject: [PATCH 3/9] chore(webdriver-utils): trim the new comments to 1-2 lines Comment-only change, no behaviour difference. Keeps the load-bearing "why" on each -- why the schema entry is required, why the boolean is coerced, why the metadata keys are conditional -- and drops the surrounding exposition. Verified unchanged: webdriver-utils 242/242, core unit/config 21/21, eslint clean. Co-Authored-By: Claude Opus 5 (1M context) --- packages/core/src/config.js | 6 ++---- packages/core/test/unit/config.test.js | 18 ++++-------------- .../src/providers/automateProvider.js | 9 +++------ .../src/providers/genericProvider.js | 6 ++---- .../test/providers/automateProvider.test.js | 7 ++----- .../test/providers/genericProvider.test.js | 3 +-- 6 files changed, 14 insertions(+), 35 deletions(-) diff --git a/packages/core/src/config.js b/packages/core/src/config.js index 2db93173e..4743542d2 100644 --- a/packages/core/src/config.js +++ b/packages/core/src/config.js @@ -202,10 +202,8 @@ export const configSchema = { type: 'boolean', onlyAutomate: true }, - // Opts a full-page Automate capture into downscaling each tile by 1/dpr, so the - // stitched image stays inside the 50,000px comparison ceiling. Without it the - // walkable DOM height is capped at 50000/dpr CSS px and taller pages are truncated. - // Only honoured on the fullPage path -- mobile-common ignores it for singlepage. + // Downscales each tile by 1/dpr so the stitched image fits the 50,000px ceiling; + // without it the DOM walk is capped at 50000/dpr CSS px. fullPage path only. scaleToFit: { type: 'boolean', onlyAutomate: true diff --git a/packages/core/test/unit/config.test.js b/packages/core/test/unit/config.test.js index dfb94c84d..cd008b270 100644 --- a/packages/core/test/unit/config.test.js +++ b/packages/core/test/unit/config.test.js @@ -117,23 +117,14 @@ describe('SnapshotSchema', () => { expect(errors[0].message).toBe('must have property scope when property scopeOptions is present'); }); - // Automate-only options live on /config/snapshot (the .percy.yml `snapshot:` section), - // NOT on /snapshot -- which $refs only a hand-picked subset and so reports fullPage as - // an unknown property too. /config/snapshot sets additionalProperties: false, so an - // undeclared key is rejected there: the reason scaleToFit needs a schema entry and not - // just provider plumbing. - // Structural assertion rather than a PercyConfig.validate() round-trip: the - // onlyAutomate keyword is evaluated when AJV COMPILES the schema, so a validate() call - // reflects whatever PERCY_TOKEN was set when the schema was first added in this process - // -- flipping the env var inside a spec cannot change the outcome. Asserting the - // declaration directly is what actually pins the change. + // Structural, not a validate() round-trip: onlyAutomate is evaluated when AJV COMPILES + // the schema, so flipping PERCY_TOKEN inside a spec cannot change the outcome. it('declares scaleToFit as an automate-only boolean', () => { expect(CoreConfig.schemas[0].snapshot.properties.scaleToFit) .toEqual({ type: 'boolean', onlyAutomate: true }); }); - // ...and this proves it is really wired into validation with the same gating as - // fullPage, not just present as an inert key. + // ...and this proves it is wired into validation with fullPage's gating, not inert. it('flags scaleToFit on a non-automate token, exactly like fullPage', () => { PercyConfig.addSchema(CoreConfig.schemas); const errors = PercyConfig.validate({ fullPage: true, scaleToFit: true }, '/config/snapshot'); @@ -142,8 +133,7 @@ describe('SnapshotSchema', () => { expect(paths).toContain('scaleToFit'); expect(paths).toContain('fullPage'); - // Same message as fullPage, and crucially NOT 'unknown property' -- an unknown-property - // error would mean the schema entry is missing and this spec passes vacuously. + // NOT 'unknown property', which would mean the entry is missing and this passes vacuously. expect([...messages]).toEqual(['property only valid with Automate integration.']); }); }); diff --git a/packages/webdriver-utils/src/providers/automateProvider.js b/packages/webdriver-utils/src/providers/automateProvider.js index 969dadeb8..66670ed13 100644 --- a/packages/webdriver-utils/src/providers/automateProvider.js +++ b/packages/webdriver-utils/src/providers/automateProvider.js @@ -96,12 +96,9 @@ export default class AutomateProvider extends GenericProvider { const metadata = { screenshotType: screenshotType }; - // Forwarded so percy-api can relax its tile-count limit by exactly the amount each - // tile shrank: a scaleToFit capture walks a taller page and so returns ~1/factor times - // the usual tile count, which the unrelaxed limit would reject. - // Added only when mobile-common reports it actually scaled -- these land in - // comparison_details.metadata, the largest table on the platform, so the default path - // must not add constants to every row. + // percy-api needs the factor to relax its tile-count limit, since a scaleToFit capture + // returns ~1/factor times the usual tiles. Added only when it actually scaled: these + // land in the largest table on the platform, so the default path must not grow rows. if (tileResponse.scale_to_fit === true) { metadata.scaleToFit = true; metadata.appliedScaleFactor = tileResponse.applied_scale_factor; diff --git a/packages/webdriver-utils/src/providers/genericProvider.js b/packages/webdriver-utils/src/providers/genericProvider.js index 158b9b570..5bd951ba8 100644 --- a/packages/webdriver-utils/src/providers/genericProvider.js +++ b/packages/webdriver-utils/src/providers/genericProvider.js @@ -48,10 +48,8 @@ export default class GenericProvider { addDefaultOptions() { this.options.freezeAnimation = this.options.freezeAnimatedImage || this.options.freezeAnimation || false; - // PERCY_SCALE_TO_FIT lets a whole run opt in without touching per-snapshot config, - // which is how support enables it for a customer hitting the 50,000px truncation. - // Coerced to a real boolean: mobile-common compares with `== true`, so a truthy - // string would silently fail to enable scaling. + // PERCY_SCALE_TO_FIT opts a whole run in without per-snapshot config. Coerced to a + // real boolean: mobile-common compares with `== true`, so a truthy string would no-op. this.options.scaleToFit = this.options.scaleToFit === true || process.env.PERCY_SCALE_TO_FIT === 'true'; } diff --git a/packages/webdriver-utils/test/providers/automateProvider.test.js b/packages/webdriver-utils/test/providers/automateProvider.test.js index f8fb11a25..ba25977d3 100644 --- a/packages/webdriver-utils/test/providers/automateProvider.test.js +++ b/packages/webdriver-utils/test/providers/automateProvider.test.js @@ -331,9 +331,7 @@ describe('AutomateProvider', () => { expect(res).toEqual(expectedOutput); }); - // percy-api relaxes its tile-count limit by this factor, so it has to reach the - // comparison payload -- a scaleToFit capture walks a taller page and returns ~1/factor - // times the usual tile count, which the unrelaxed limit would reject. + // percy-api relaxes its tile-count limit by this factor, so it must reach the payload. it('forwards scaleToFit and the applied factor into metadata when scaling happened', async () => { const response = { success: true, @@ -356,8 +354,7 @@ describe('AutomateProvider', () => { }); }); - // These land in comparison_details.metadata, the largest table on the platform, so - // the default path must not add constants to every row. + // These land in the largest table on the platform; don't grow every row. it('omits the scaleToFit metadata keys when mobile-common did not scale', async () => { const response = { success: true, diff --git a/packages/webdriver-utils/test/providers/genericProvider.test.js b/packages/webdriver-utils/test/providers/genericProvider.test.js index dce254659..2d30e1a4e 100644 --- a/packages/webdriver-utils/test/providers/genericProvider.test.js +++ b/packages/webdriver-utils/test/providers/genericProvider.test.js @@ -68,8 +68,7 @@ describe('GenericProvider', () => { expect(provider.options.scaleToFit).toBeFalse(); }); - // mobile-common compares the forwarded option with `== true`, so anything that is - // merely truthy would arrive as a no-op and silently truncate the page instead. + // mobile-common compares with `== true`, so a merely-truthy value would silently no-op. it('coerces scaleToFit to a real boolean', () => { const provider = new GenericProvider({ options: { scaleToFit: 'true' } }); provider.addDefaultOptions(); From 4a5c92f6533e14c563089e2fbf39720e083e8f90 Mon Sep 17 00:00:00 2001 From: rishigupta1599 Date: Tue, 18 Aug 2026 18:34:31 +0530 Subject: [PATCH 4/9] chore(webdriver-utils): cap the remaining comment at 2 lines One block was still 3 lines after the previous pass. 242/242, eslint clean. Co-Authored-By: Claude Opus 5 (1M context) --- packages/webdriver-utils/src/providers/automateProvider.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/webdriver-utils/src/providers/automateProvider.js b/packages/webdriver-utils/src/providers/automateProvider.js index 66670ed13..ab054c6fb 100644 --- a/packages/webdriver-utils/src/providers/automateProvider.js +++ b/packages/webdriver-utils/src/providers/automateProvider.js @@ -96,9 +96,8 @@ export default class AutomateProvider extends GenericProvider { const metadata = { screenshotType: screenshotType }; - // percy-api needs the factor to relax its tile-count limit, since a scaleToFit capture - // returns ~1/factor times the usual tiles. Added only when it actually scaled: these - // land in the largest table on the platform, so the default path must not grow rows. + // percy-api needs the factor to relax its tile-count limit. Added only when it really + // scaled -- these land in the largest table on the platform. if (tileResponse.scale_to_fit === true) { metadata.scaleToFit = true; metadata.appliedScaleFactor = tileResponse.applied_scale_factor; From a2a4204bf4d6c8ee84e8e07a98215e9d08909122 Mon Sep 17 00:00:00 2001 From: rishigupta1599 Date: Tue, 18 Aug 2026 19:05:19 +0530 Subject: [PATCH 5/9] fix(core): make the scaleToFit gating spec environment-independent CI failed with "Cannot read property 'map' of undefined": PercyConfig.validate returns undefined when there are no errors, and onlyAutomate only errors when PERCY_TOKEN is set to a non-automate token. My local run had one set, CI does not -- so the spec asserted an absolute outcome that only held locally. Assert scaleToFit is treated the SAME as fullPage instead. Both are onlyAutomate, so the comparison holds whether the token is absent, web, or automate, and an undeclared key would still draw 'unknown property' where fullPage drew none. Verified passing with PERCY_TOKEN unset, web_*, and auto_*; and verified BOTH schema specs fail in all three states when the scaleToFit entry is removed, so neither can pass vacuously. Co-Authored-By: Claude Opus 5 (1M context) --- packages/core/test/unit/config.test.js | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/packages/core/test/unit/config.test.js b/packages/core/test/unit/config.test.js index cd008b270..35e1c3beb 100644 --- a/packages/core/test/unit/config.test.js +++ b/packages/core/test/unit/config.test.js @@ -124,17 +124,18 @@ describe('SnapshotSchema', () => { .toEqual({ type: 'boolean', onlyAutomate: true }); }); - // ...and this proves it is wired into validation with fullPage's gating, not inert. - it('flags scaleToFit on a non-automate token, exactly like fullPage', () => { + // ...and this proves it is wired into validation, not inert. Asserted RELATIVE to + // fullPage: onlyAutomate is compiled in from PERCY_TOKEN, which differs between a local + // run and CI, so an absolute expectation here passes locally and breaks in CI. + it('gates scaleToFit exactly like fullPage', () => { PercyConfig.addSchema(CoreConfig.schemas); - const errors = PercyConfig.validate({ fullPage: true, scaleToFit: true }, '/config/snapshot'); - const paths = errors.map(e => e.path); - const messages = new Set(errors.map(e => e.message)); - - expect(paths).toContain('scaleToFit'); - expect(paths).toContain('fullPage'); - // NOT 'unknown property', which would mean the entry is missing and this passes vacuously. - expect([...messages]).toEqual(['property only valid with Automate integration.']); + const errors = PercyConfig.validate({ fullPage: true, scaleToFit: true }, '/config/snapshot') || []; + const messagesFor = (path) => errors.filter(e => e.path === path).map(e => e.message); + + expect(messagesFor('scaleToFit')).toEqual(messagesFor('fullPage')); + // An undeclared key would draw 'unknown property' here while fullPage drew none, so + // this still fails if the schema entry goes missing. + expect(messagesFor('scaleToFit')).not.toContain('unknown property'); }); }); From 823addb412c6808a6ddae0e5211e2bf90baff047 Mon Sep 17 00:00:00 2001 From: rishigupta1599 Date: Tue, 18 Aug 2026 23:54:12 +0530 Subject: [PATCH 6/9] fix(core): carry scaleToFit from global .percy.yml config into automate options percyAutomateRequestHandler builds the provider options by ENUMERATING each global snapshot config key, then merging per-screenshot options on top. Adding scaleToFit to the schema made `.percy.yml` accept it, but because it was not in that list it was silently dropped on the way to the provider -- the page truncated exactly as before while validation and the build stayed green. So of the two levels only per-screenshot actually worked. Both work now. The failure mode is the reason for the test: it asserts global-only, a per-screenshot override of a global true, and per-screenshot opt-in with global unset. Verified it fails when the merge line is removed, so it cannot rot. Verified: core utils 140/140, core unit/config 21/21, eslint clean. Co-Authored-By: Claude Opus 5 (1M context) --- packages/core/src/utils.js | 3 +++ packages/core/test/utils.test.js | 25 +++++++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/packages/core/src/utils.js b/packages/core/src/utils.js index a28ea02ca..740ece3c4 100644 --- a/packages/core/src/utils.js +++ b/packages/core/src/utils.js @@ -412,6 +412,9 @@ export function percyAutomateRequestHandler(req, percy) { req.body.options = merge([{ fullPage: percy.config.snapshot.fullPage, + // Must be listed here or a .percy.yml `snapshot.scaleToFit` validates and is then + // silently dropped -- the page truncates as before while the build stays green. + scaleToFit: percy.config.snapshot.scaleToFit, percyCSS: percy.config.snapshot.percyCSS, freezeAnimatedImage: percy.config.snapshot.freezeAnimatedImage || percy.config.snapshot.freezeAnimation, freezeImageBySelectors: percy.config.snapshot.freezeAnimatedImageOptions?.freezeImageBySelectors, diff --git a/packages/core/test/utils.test.js b/packages/core/test/utils.test.js index e2e33340e..45b32c151 100644 --- a/packages/core/test/utils.test.js +++ b/packages/core/test/utils.test.js @@ -81,6 +81,31 @@ describe('utils', () => { expect(req.body.buildInfo).toEqual({ id: 'b1' }); }); + // scaleToFit is settable at BOTH levels: globally via .percy.yml `snapshot:` (which + // only reaches the provider because it is enumerated in the merge above) and + // per-screenshot (which overrides it). Both paths are load-bearing. + it('carries scaleToFit from global config and lets a per-screenshot value override it', () => { + const base = () => ({ + build: { id: 'b1' }, + config: { percy: { platforms: [] }, snapshot: { percyCSS: '', scaleToFit: true } } + }); + + // global only + let req = { body: { options: {} } }; + percyAutomateRequestHandler(req, base()); + expect(req.body.options.scaleToFit).toBeTrue(); + + // per-screenshot snake_case wins over global + req = { body: { options: { scale_to_fit: false } } }; + percyAutomateRequestHandler(req, base()); + expect(req.body.options.scaleToFit).toBeFalse(); + + // per-screenshot can opt in when global is unset + req = { body: { options: { scaleToFit: true } } }; + percyAutomateRequestHandler(req, { build: {}, config: { percy: {}, snapshot: { percyCSS: '' } } }); + expect(req.body.options.scaleToFit).toBeTrue(); + }); + it('handles missing client/environment and empty options', () => { const req = { body: { options: { } } }; const percy = { build: { id: 'b' }, config: { percy: { platforms: [] }, snapshot: { percyCSS: '', freezeAnimation: false } } }; From d34d8585dc293b5517d4fe7a5b354f64746a53c8 Mon Sep 17 00:00:00 2001 From: rishigupta1599 Date: Tue, 25 Aug 2026 20:08:17 +0530 Subject: [PATCH 7/9] fix(core): declare scaleToFit metadata on the comparison schema Review findings on the scaleToFit option. BLOCKER: comparisonSchema.metadata sets additionalProperties:false and PercyConfig.validate DELETES unknown keys from the object it validates, so both new metadata keys were stripped before upload. percy-api therefore never received appliedScaleFactor and never relaxed its tile-count limit -- the same fail-open class as the global-config path, one layer further down, and equally invisible in a green build. Declares both keys (with a 0 < x <= 1 bound on the factor) and adds a round-trip test asserting the values survive validate, which a structural assertion would not catch. Also: - gate scaleToFit on fullPage. The host shrinks tiles only in its full-page loop, so outside it the flag reported a factor for unscaled tiles. - let an explicit `scaleToFit: false` beat PERCY_SCALE_TO_FIT, so the env var is a run-wide default rather than an unopt-outable override. - require a finite applied_scale_factor before reporting the pair, so a half-pair cannot ask the API to relax by nothing. - mirror the metadata block in playwrightProvider, which inherits the option from GenericProvider but never read the factor back. - add scaleToFit to percy doctor's automateOnlyKeys, the third enumeration of this option, so a web-token user gets the warning. - look the config schema up by absence of $id instead of index [0]. Co-Authored-By: Claude Opus 5 (1M context) --- packages/cli-doctor/src/checks/config.js | 2 +- packages/core/src/config.js | 6 +- packages/core/test/unit/config.test.js | 39 +++++++++++- .../src/providers/automateProvider.js | 2 +- .../src/providers/genericProvider.js | 7 ++- .../src/providers/playwrightProvider.js | 6 ++ .../test/providers/genericProvider.test.js | 63 ++++++++++++------- 7 files changed, 96 insertions(+), 29 deletions(-) diff --git a/packages/cli-doctor/src/checks/config.js b/packages/cli-doctor/src/checks/config.js index 91a69b0ab..a8c73f45a 100644 --- a/packages/cli-doctor/src/checks/config.js +++ b/packages/cli-doctor/src/checks/config.js @@ -80,7 +80,7 @@ export async function checkConfig(options = {}) { // Keys that only work with automate tokens const automateOnlyKeys = ['fullPage', 'freezeAnimation', 'freezeAnimatedImage', - 'freezeAnimatedImageOptions', 'ignoreRegions', 'considerRegions']; + 'freezeAnimatedImageOptions', 'ignoreRegions', 'considerRegions', 'scaleToFit']; // Keys that only work with web tokens (not automate, not app) const webOnlyKeys = ['waitForTimeout', 'waitForSelector']; diff --git a/packages/core/src/config.js b/packages/core/src/config.js index 4743542d2..dfd94ed68 100644 --- a/packages/core/src/config.js +++ b/packages/core/src/config.js @@ -933,7 +933,11 @@ export const comparisonSchema = { }, cliScreenshotStartTime: { type: 'integer', default: 0 }, cliScreenshotEndTime: { type: 'integer', default: 0 }, - screenshotType: { type: 'string', default: 'singlepage' } + screenshotType: { type: 'string', default: 'singlepage' }, + // additionalProperties is false here and PercyConfig.validate DELETES unknown keys + // from the object it validates, so an undeclared key is silently dropped pre-upload. + scaleToFit: { type: 'boolean' }, + appliedScaleFactor: { type: 'number', exclusiveMinimum: 0, maximum: 1 } } }, tag: { diff --git a/packages/core/test/unit/config.test.js b/packages/core/test/unit/config.test.js index 35e1c3beb..52cefa20c 100644 --- a/packages/core/test/unit/config.test.js +++ b/packages/core/test/unit/config.test.js @@ -120,7 +120,9 @@ describe('SnapshotSchema', () => { // Structural, not a validate() round-trip: onlyAutomate is evaluated when AJV COMPILES // the schema, so flipping PERCY_TOKEN inside a spec cannot change the outcome. it('declares scaleToFit as an automate-only boolean', () => { - expect(CoreConfig.schemas[0].snapshot.properties.scaleToFit) + // The config schema is the one entry with no $id; index into that rather than [0]. + const configSchema = CoreConfig.schemas.find(s => !s.$id); + expect(configSchema.snapshot.properties.scaleToFit) .toEqual({ type: 'boolean', onlyAutomate: true }); }); @@ -139,6 +141,41 @@ describe('SnapshotSchema', () => { }); }); +describe('ComparisonSchema - scaleToFit metadata', () => { + beforeEach(() => { + PercyConfig.addSchema(CoreConfig.schemas); + }); + + // metadata sets additionalProperties:false and PercyConfig.validate DELETES unknown keys + // from the object it is handed, so an undeclared key is dropped before upload -- silently, + // with the build still green. A structural assertion would not catch that; this asserts + // the value survives the round trip. + it('keeps scaleToFit and appliedScaleFactor on the validated object', () => { + const options = { + name: 'snap', + tag: { name: 'Pixel 10' }, + tiles: [], + metadata: { screenshotType: 'fullpage', scaleToFit: true, appliedScaleFactor: 0.380952 } + }; + + expect(PercyConfig.validate(options, '/comparison')).toBe(undefined); + expect(options.metadata).toEqual({ + screenshotType: 'fullpage', scaleToFit: true, appliedScaleFactor: 0.380952 + }); + }); + + it('rejects a factor outside (0, 1]', () => { + const build = (appliedScaleFactor) => PercyConfig.validate({ + name: 'snap', tag: { name: 'Pixel 10' }, tiles: [], + metadata: { scaleToFit: true, appliedScaleFactor } + }, '/comparison') || []; + + expect(build(2).map(e => e.path)).toContain('metadata.appliedScaleFactor'); + expect(build(0).map(e => e.path)).toContain('metadata.appliedScaleFactor'); + expect(build(0.380952)).toEqual([]); + }); +}); + describe('ComparisonSchema - elementSelectorsData', () => { beforeEach(() => { PercyConfig.addSchema(CoreConfig.schemas); diff --git a/packages/webdriver-utils/src/providers/automateProvider.js b/packages/webdriver-utils/src/providers/automateProvider.js index ab054c6fb..f70456ed5 100644 --- a/packages/webdriver-utils/src/providers/automateProvider.js +++ b/packages/webdriver-utils/src/providers/automateProvider.js @@ -98,7 +98,7 @@ export default class AutomateProvider extends GenericProvider { }; // percy-api needs the factor to relax its tile-count limit. Added only when it really // scaled -- these land in the largest table on the platform. - if (tileResponse.scale_to_fit === true) { + if (tileResponse.scale_to_fit === true && Number.isFinite(tileResponse.applied_scale_factor)) { metadata.scaleToFit = true; metadata.appliedScaleFactor = tileResponse.applied_scale_factor; } diff --git a/packages/webdriver-utils/src/providers/genericProvider.js b/packages/webdriver-utils/src/providers/genericProvider.js index 5bd951ba8..e82662ff5 100644 --- a/packages/webdriver-utils/src/providers/genericProvider.js +++ b/packages/webdriver-utils/src/providers/genericProvider.js @@ -50,8 +50,11 @@ export default class GenericProvider { this.options.freezeAnimation = this.options.freezeAnimatedImage || this.options.freezeAnimation || false; // PERCY_SCALE_TO_FIT opts a whole run in without per-snapshot config. Coerced to a // real boolean: mobile-common compares with `== true`, so a truthy string would no-op. - this.options.scaleToFit = this.options.scaleToFit === true || - process.env.PERCY_SCALE_TO_FIT === 'true'; + // fullPage-only: the host shrinks tiles solely in its full-page loop, so outside it the + // flag would report a scale factor for tiles that were never scaled. + this.options.scaleToFit = this.options.fullPage === true && + (this.options.scaleToFit === true || + (this.options.scaleToFit !== false && process.env.PERCY_SCALE_TO_FIT === 'true')); } async createDriver() { diff --git a/packages/webdriver-utils/src/providers/playwrightProvider.js b/packages/webdriver-utils/src/providers/playwrightProvider.js index 5c5b50149..157f16870 100644 --- a/packages/webdriver-utils/src/providers/playwrightProvider.js +++ b/packages/webdriver-utils/src/providers/playwrightProvider.js @@ -134,6 +134,12 @@ export default class PlaywrightProvider extends GenericProvider { const metadata = { screenshotType: screenshotType }; + // Same pair as automateProvider: the host shrinks tiles, so percy-api needs the factor + // to relax its tile-count limit. Without this a playwright capture reports none. + if (tileResponse.scale_to_fit === true && Number.isFinite(tileResponse.applied_scale_factor)) { + metadata.scaleToFit = true; + metadata.appliedScaleFactor = tileResponse.applied_scale_factor; + } return { tiles: tiles, domInfoSha: tileResponse.dom_sha, diff --git a/packages/webdriver-utils/test/providers/genericProvider.test.js b/packages/webdriver-utils/test/providers/genericProvider.test.js index 2d30e1a4e..882ccd048 100644 --- a/packages/webdriver-utils/test/providers/genericProvider.test.js +++ b/packages/webdriver-utils/test/providers/genericProvider.test.js @@ -52,33 +52,50 @@ describe('GenericProvider', () => { expect(provider.options.freezeAnimation).toBeFalse(); }); - it('enables scaleToFit from the option or PERCY_SCALE_TO_FIT', () => { - let provider = new GenericProvider({ options: { scaleToFit: true } }); - provider.addDefaultOptions(); - expect(provider.options.scaleToFit).toBeTrue(); + describe('scaleToFit', () => { + const build = (options) => { + const provider = new GenericProvider({ options }); + provider.addDefaultOptions(); + return provider.options.scaleToFit; + }; - process.env.PERCY_SCALE_TO_FIT = 'true'; - provider = new GenericProvider({ options: {} }); - provider.addDefaultOptions(); - expect(provider.options.scaleToFit).toBeTrue(); + beforeEach(() => { delete process.env.PERCY_SCALE_TO_FIT; }); + afterEach(() => { delete process.env.PERCY_SCALE_TO_FIT; }); - delete process.env.PERCY_SCALE_TO_FIT; - provider = new GenericProvider({ options: {} }); - provider.addDefaultOptions(); - expect(provider.options.scaleToFit).toBeFalse(); - }); + it('enables from the option or PERCY_SCALE_TO_FIT', () => { + expect(build({ fullPage: true, scaleToFit: true })).toBeTrue(); - // mobile-common compares with `== true`, so a merely-truthy value would silently no-op. - it('coerces scaleToFit to a real boolean', () => { - const provider = new GenericProvider({ options: { scaleToFit: 'true' } }); - provider.addDefaultOptions(); - expect(provider.options.scaleToFit).toBeFalse(); + process.env.PERCY_SCALE_TO_FIT = 'true'; + expect(build({ fullPage: true })).toBeTrue(); - process.env.PERCY_SCALE_TO_FIT = '1'; - const other = new GenericProvider({ options: {} }); - other.addDefaultOptions(); - expect(other.options.scaleToFit).toBeFalse(); - delete process.env.PERCY_SCALE_TO_FIT; + delete process.env.PERCY_SCALE_TO_FIT; + expect(build({ fullPage: true })).toBeFalse(); + }); + + // The host only shrinks tiles in its full-page loop, so outside it the flag would + // report a scale factor for tiles that were never scaled. + it('is ignored without fullPage', () => { + expect(build({ scaleToFit: true })).toBeFalse(); + expect(build({ fullPage: false, scaleToFit: true })).toBeFalse(); + + process.env.PERCY_SCALE_TO_FIT = 'true'; + expect(build({})).toBeFalse(); + }); + + // The env var is a run-wide default, not an override -- otherwise a single snapshot + // could never opt out once it is set. + it('lets an explicit false beat the env var', () => { + process.env.PERCY_SCALE_TO_FIT = 'true'; + expect(build({ fullPage: true, scaleToFit: false })).toBeFalse(); + }); + + // mobile-common compares with `== true`, so a merely-truthy value would silently no-op. + it('coerces to a real boolean', () => { + expect(build({ fullPage: true, scaleToFit: 'true' })).toBeFalse(); + + process.env.PERCY_SCALE_TO_FIT = '1'; + expect(build({ fullPage: true })).toBeFalse(); + }); }); }); From f22dd4b858271b86ebe5a41058cf27e4674b8aca Mon Sep 17 00:00:00 2001 From: rishigupta1599 Date: Tue, 25 Aug 2026 22:24:16 +0530 Subject: [PATCH 8/9] test(webdriver-utils): cover the playwright scaleToFit branch CI enforces 100% global coverage and the new playwrightProvider metadata block was untested, so lines 140-141 dropped branches to 99.34% and failed all four retries. Local `test:coverage` reports zeros for this package, so the threshold is only observable in CI. Covers both arms: scale_to_fit with a finite factor reports the pair, and scale_to_fit without a factor omits it. Also fixes an object-property-newline lint error in the config round-trip test. Co-Authored-By: Claude Opus 5 (1M context) --- packages/core/test/unit/config.test.js | 4 +- .../test/providers/playwrightProvider.test.js | 52 +++++++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/packages/core/test/unit/config.test.js b/packages/core/test/unit/config.test.js index 52cefa20c..4e5d169cb 100644 --- a/packages/core/test/unit/config.test.js +++ b/packages/core/test/unit/config.test.js @@ -166,7 +166,9 @@ describe('ComparisonSchema - scaleToFit metadata', () => { it('rejects a factor outside (0, 1]', () => { const build = (appliedScaleFactor) => PercyConfig.validate({ - name: 'snap', tag: { name: 'Pixel 10' }, tiles: [], + name: 'snap', + tag: { name: 'Pixel 10' }, + tiles: [], metadata: { scaleToFit: true, appliedScaleFactor } }, '/comparison') || []; diff --git a/packages/webdriver-utils/test/providers/playwrightProvider.test.js b/packages/webdriver-utils/test/providers/playwrightProvider.test.js index 78328c38d..79c254f1e 100644 --- a/packages/webdriver-utils/test/providers/playwrightProvider.test.js +++ b/packages/webdriver-utils/test/providers/playwrightProvider.test.js @@ -368,6 +368,58 @@ describe('PlaywrightProvider', () => { }); }); + it('reports the scale factor when the host shrank the tiles', async () => { + const provider = new PlaywrightProvider( + 'sessionId', 'frameGuid', 'pageGuid', 'clientInfo', 'environmentInfo', + { fullPage: true, scaleToFit: true }, { id: 1 } + ); + provider.browserstackExecutor = jasmine + .createSpy('browserstackExecutor') + .and.resolveTo({ + value: JSON.stringify({ + success: true, + result: JSON.stringify({ + tiles: [{ status_bar: 0, nav_bar: 0, header_height: 0, footer_height: 0, sha: 't1' }], + comparison_tag_data: { width: 411, height: 858, resolution: '1080x2251' }, + dom_sha: 'domSHA', + scale_to_fit: true, + applied_scale_factor: 0.380952 + }) + }) + }); + + const response = await provider.getTiles(true); + + expect(response.metadata).toEqual({ + screenshotType: 'fullpage', scaleToFit: true, appliedScaleFactor: 0.380952 + }); + }); + + // A half-pair would ask percy-api to relax its tile limit by nothing. + it('omits the pair when the factor is missing', async () => { + const provider = new PlaywrightProvider( + 'sessionId', 'frameGuid', 'pageGuid', 'clientInfo', 'environmentInfo', + { fullPage: true, scaleToFit: true }, { id: 1 } + ); + provider.browserstackExecutor = jasmine + .createSpy('browserstackExecutor') + .and.resolveTo({ + value: JSON.stringify({ + success: true, + result: JSON.stringify({ + tiles: [{ status_bar: 0, nav_bar: 0, header_height: 0, footer_height: 0, sha: 't1' }], + comparison_tag_data: { width: 411, height: 858, resolution: '1080x2251' }, + dom_sha: 'domSHA', + scale_to_fit: true + }) + }) + }); + + const response = await provider.getTiles(true); + + expect(response.metadata).toEqual({ screenshotType: 'fullpage' }); + }); + it('should handle errors during tile capture', async () => { const error = new Error('Failed to capture tiles'); provider.browserstackExecutor = jasmine From c3286cff64de8a3fbca7775b74a65e7dd34aec42 Mon Sep 17 00:00:00 2001 From: rishigupta1599 Date: Tue, 25 Aug 2026 23:50:18 +0530 Subject: [PATCH 9/9] fix(webdriver-utils): run the scaleToFit gate on the playwright path Re-review findings. PlaywrightProvider.screenshot fully overrides GenericProvider.screenshot and never called super, so addDefaultOptions was skipped on that path entirely -- the fullPage gate, the boolean coercion and PERCY_SCALE_TO_FIT were all inert for playwright captures, and raw options reached the host. Calling it also restores the freezeAnimation default and the percyCSS newline strip, both of which were missing there for the same reason. Also tighten the reported factor to (0, 1]: Number.isFinite(0) and Number.isFinite(-1) are both true, so a host reporting 0 produced exactly the half-pair the guard exists to prevent -- the schema drops appliedScaleFactor while leaving scaleToFit true, plus a user-visible warning. The shared playwright test fixture passed the literal string 'options', which addDefaultOptions cannot write to; it now passes a real object. 246 specs pass. Co-Authored-By: Claude Opus 5 (1M context) --- .../webdriver-utils/src/providers/automateProvider.js | 6 ++++-- .../src/providers/playwrightProvider.js | 10 ++++++++-- .../test/providers/playwrightProvider.test.js | 8 ++++---- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/packages/webdriver-utils/src/providers/automateProvider.js b/packages/webdriver-utils/src/providers/automateProvider.js index f70456ed5..10f980624 100644 --- a/packages/webdriver-utils/src/providers/automateProvider.js +++ b/packages/webdriver-utils/src/providers/automateProvider.js @@ -98,9 +98,11 @@ export default class AutomateProvider extends GenericProvider { }; // percy-api needs the factor to relax its tile-count limit. Added only when it really // scaled -- these land in the largest table on the platform. - if (tileResponse.scale_to_fit === true && Number.isFinite(tileResponse.applied_scale_factor)) { + const scaleFactor = tileResponse.applied_scale_factor; + if (tileResponse.scale_to_fit === true && + Number.isFinite(scaleFactor) && scaleFactor > 0 && scaleFactor <= 1) { metadata.scaleToFit = true; - metadata.appliedScaleFactor = tileResponse.applied_scale_factor; + metadata.appliedScaleFactor = scaleFactor; } return { tiles: tiles, diff --git a/packages/webdriver-utils/src/providers/playwrightProvider.js b/packages/webdriver-utils/src/providers/playwrightProvider.js index 157f16870..7384b2a9c 100644 --- a/packages/webdriver-utils/src/providers/playwrightProvider.js +++ b/packages/webdriver-utils/src/providers/playwrightProvider.js @@ -42,6 +42,10 @@ export default class PlaywrightProvider extends GenericProvider { async screenshot(name, options) { let response = null; let error; + // This override does not call super.screenshot(), which is where GenericProvider applies + // addDefaultOptions -- so without this the scaleToFit fullPage gate, its boolean + // coercion and PERCY_SCALE_TO_FIT were all skipped on the playwright path. + this.addDefaultOptions(); log.debug(`[${name}] : Preparing to capture screenshots on playwright with automate ...`); try { log.debug(`[${name}] : Marking automate session as percy ...`); @@ -136,9 +140,11 @@ export default class PlaywrightProvider extends GenericProvider { }; // Same pair as automateProvider: the host shrinks tiles, so percy-api needs the factor // to relax its tile-count limit. Without this a playwright capture reports none. - if (tileResponse.scale_to_fit === true && Number.isFinite(tileResponse.applied_scale_factor)) { + const scaleFactor = tileResponse.applied_scale_factor; + if (tileResponse.scale_to_fit === true && + Number.isFinite(scaleFactor) && scaleFactor > 0 && scaleFactor <= 1) { metadata.scaleToFit = true; - metadata.appliedScaleFactor = tileResponse.applied_scale_factor; + metadata.appliedScaleFactor = scaleFactor; } return { tiles: tiles, diff --git a/packages/webdriver-utils/test/providers/playwrightProvider.test.js b/packages/webdriver-utils/test/providers/playwrightProvider.test.js index 79c254f1e..f5390ebb5 100644 --- a/packages/webdriver-utils/test/providers/playwrightProvider.test.js +++ b/packages/webdriver-utils/test/providers/playwrightProvider.test.js @@ -13,7 +13,7 @@ describe('PlaywrightProvider', () => { 'pageGuid', 'clientInfo', 'environmentInfo', - 'options', + {}, { id: 1 } ); }); @@ -25,7 +25,7 @@ describe('PlaywrightProvider', () => { expect(provider.pageGuid).toBe('pageGuid'); expect(provider.clientInfo).toBe('clientInfo'); expect(provider.environmentInfo).toBe('environmentInfo'); - expect(provider.options).toBe('options'); + expect(provider.options).toEqual({}); expect(provider.buildInfo).toEqual({ id: 1 }); }); }); @@ -206,7 +206,7 @@ describe('PlaywrightProvider', () => { percyBuildId: 1, screenshotType: 'singlepage', scaleFactor: 1, - options: 'options', + options: {}, frameworkData: { frameGuid: 'frameGuid', pageGuid: 'pageGuid' }, framework: 'playwright' } @@ -260,7 +260,7 @@ describe('PlaywrightProvider', () => { percyBuildId: 1, screenshotType: 'singlepage', scaleFactor: 1, - options: 'options', + options: {}, frameworkData: { frameGuid: 'frameGuid', pageGuid: 'pageGuid' }, framework: 'playwright' }