diff --git a/src/client/index.js b/src/client/index.js index 652b5544..e1a54f60 100644 --- a/src/client/index.js +++ b/src/client/index.js @@ -365,6 +365,9 @@ function createSimpleClient(serverUrl, clientOptions = {}) { * @param {number} [options.threshold] - CIEDE2000 Delta E threshold for this screenshot * @param {number} [options.minClusterSize] - Minimum changed cluster size for this screenshot * @param {boolean} [options.fullPage] - Whether this is a full page screenshot + * @param {string} [options.captureMode] - Capture coordinate mode + * @param {number} [options.deviceScaleFactor] - Bitmap pixels per CSS pixel + * @param {string} [options.selector] - Selector used for an element capture * * @returns {Promise} Screenshot result from the server, or null when capture is skipped * diff --git a/src/types/client.d.ts b/src/types/client.d.ts index f46d9cb6..2dacce71 100644 --- a/src/types/client.d.ts +++ b/src/types/client.d.ts @@ -86,6 +86,12 @@ export function vizzlyScreenshot( threshold?: number; minClusterSize?: number; fullPage?: boolean; + /** Coordinate frame used by the harness that captured the image. */ + captureMode?: 'viewport' | 'full_page' | 'element' | 'component'; + /** Bitmap pixels per CSS pixel reported by the capture harness. */ + deviceScaleFactor?: number; + /** Exact selector used when captureMode is element or component. */ + selector?: string; /** Transport-only build ID used to route the request. */ buildId?: string; /** Client-side HTTP timeout in milliseconds; not stored as metadata. */ diff --git a/src/types/index.d.ts b/src/types/index.d.ts index db1e343d..e214026e 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -78,6 +78,12 @@ export interface ScreenshotOptions { threshold?: number; minClusterSize?: number; fullPage?: boolean; + /** Coordinate frame used by the harness that captured the image. */ + captureMode?: 'viewport' | 'full_page' | 'element' | 'component'; + /** Bitmap pixels per CSS pixel reported by the capture harness. */ + deviceScaleFactor?: number; + /** Exact selector used when captureMode is element or component. */ + selector?: string; /** Transport-only build ID used to route the request. */ buildId?: string; /** Client-side HTTP timeout in milliseconds; not stored as metadata. */ diff --git a/src/utils/screenshot-options.js b/src/utils/screenshot-options.js index 11e11f14..1a060097 100644 --- a/src/utils/screenshot-options.js +++ b/src/utils/screenshot-options.js @@ -15,6 +15,18 @@ export let RESERVED_PROPERTY_OPTIONS = Object.freeze({ message: 'Move "fullPage" out of properties; properties is only for user metadata.', }, + captureMode: { + message: + 'Move "captureMode" out of properties; properties is only for user metadata.', + }, + deviceScaleFactor: { + message: + 'Move "deviceScaleFactor" out of properties; properties is only for user metadata.', + }, + selector: { + message: + 'Move "selector" out of properties; properties is only for user metadata.', + }, buildId: { message: 'Move "buildId" out of properties; properties is only for user metadata.', @@ -36,6 +48,10 @@ function createReservedPropertyWarning(option) { /** * Normalize screenshot SDK options into the properties payload consumed by the * local TDD server and cloud-compatible comparison path. + * + * Capture geometry is reserved because the cloud must receive the facts from the + * harness that took the image. Dropping these values would force the API to guess + * a coordinate space from bitmap dimensions after the capture already happened. */ export function normalizeScreenshotOptions(options = {}) { let { @@ -45,6 +61,9 @@ export function normalizeScreenshotOptions(options = {}) { threshold, minClusterSize, fullPage, + captureMode, + deviceScaleFactor, + selector, } = options; let warnings = []; @@ -59,6 +78,13 @@ export function normalizeScreenshotOptions(options = {}) { minClusterSize = value; } if (key === 'fullPage' && fullPage === undefined) fullPage = value; + if (key === 'captureMode' && captureMode === undefined) { + captureMode = value; + } + if (key === 'deviceScaleFactor' && deviceScaleFactor === undefined) { + deviceScaleFactor = value; + } + if (key === 'selector' && selector === undefined) selector = value; if (key === 'buildId' && buildId === undefined) buildId = value; if (key === 'requestTimeout' && requestTimeout === undefined) { requestTimeout = value; @@ -82,6 +108,18 @@ export function normalizeScreenshotOptions(options = {}) { normalizedProperties.fullPage = fullPage; } + if (captureMode !== undefined) { + normalizedProperties.captureMode = captureMode; + } + + if (deviceScaleFactor !== undefined) { + normalizedProperties.deviceScaleFactor = deviceScaleFactor; + } + + if (selector !== undefined) { + normalizedProperties.selector = selector; + } + return { buildId, requestTimeout, diff --git a/tests/sdk/client.test.js b/tests/sdk/client.test.js index e6ce2604..6ae38353 100644 --- a/tests/sdk/client.test.js +++ b/tests/sdk/client.test.js @@ -495,6 +495,27 @@ describe('client/index httpPost integration tests', () => { }); }); + it('preserves capture geometry for cloud identity and region evidence', async () => { + await vizzlyScreenshot('test', Buffer.from('data'), { + captureMode: 'element', + deviceScaleFactor: 2, + selector: '[data-testid="account-card"]', + properties: { + browser: 'chromium', + viewport: { width: 1440, height: 900 }, + }, + }); + + assert.strictEqual(requests.length, 1); + assert.deepStrictEqual(requests[0].body.properties, { + browser: 'chromium', + viewport: { width: 1440, height: 900 }, + captureMode: 'element', + deviceScaleFactor: 2, + selector: '[data-testid="account-card"]', + }); + }); + it('lets explicit comparison options override nested properties', async () => { await vizzlyScreenshot('test', Buffer.from('data'), { threshold: 0,