diff --git a/packages/client/src/client.js b/packages/client/src/client.js index b8cddb620..c91c20be1 100644 --- a/packages/client/src/client.js +++ b/packages/client/src/client.js @@ -366,7 +366,7 @@ export class PercyClient { return snapshot; } - async createComparison(snapshotId, { tag, tiles = [], externalDebugUrl, ignoredElementsData, domInfoSha, consideredElementsData } = {}) { + async createComparison(snapshotId, { tag, tiles = [], externalDebugUrl, ignoredElementsData, domInfoSha, consideredElementsData, metadata } = {}) { validateId('snapshot', snapshotId); // Remove post percy api deploy this.log.debug(`Creating comparision: ${tag.name}...`); @@ -388,7 +388,8 @@ export class PercyClient { 'external-debug-url': externalDebugUrl || null, 'ignore-elements-data': ignoredElementsData || null, 'consider-elements-data': consideredElementsData || null, - 'dom-info-sha': domInfoSha || null + 'dom-info-sha': domInfoSha || null, + metadata: metadata || null }, relationships: { tag: { diff --git a/packages/client/test/client.test.js b/packages/client/test/client.test.js index fc4b33e72..86b95d4d3 100644 --- a/packages/client/test/client.test.js +++ b/packages/client/test/client.test.js @@ -4,6 +4,8 @@ import { mockgit } from '@percy/env/test/helpers'; import { sha256hash, base64encode } from '@percy/client/utils'; import PercyClient from '@percy/client'; import api from './helpers.js'; +import * as CoreConfig from '@percy/core/config'; +import PercyConfig from '@percy/config'; describe('PercyClient', () => { let client; @@ -853,7 +855,10 @@ describe('PercyClient', () => { externalDebugUrl: 'http://debug.localhost', ignoredElementsData: ignoredElementsData, consideredElementsData: consideredElementsData, - domInfoSha: 'abcd=' + domInfoSha: 'abcd=', + metadata: { + windowHeight: 1947 + } })).toBeResolved(); expect(api.requests['/snapshots/4567/comparisons'][0].body).toEqual({ @@ -863,7 +868,10 @@ describe('PercyClient', () => { 'external-debug-url': 'http://debug.localhost', 'ignore-elements-data': ignoredElementsData, 'consider-elements-data': consideredElementsData, - 'dom-info-sha': 'abcd=' + 'dom-info-sha': 'abcd=', + metadata: { + windowHeight: 1947 + } }, relationships: { tag: { @@ -932,7 +940,8 @@ describe('PercyClient', () => { 'external-debug-url': null, 'ignore-elements-data': null, 'consider-elements-data': null, - 'dom-info-sha': null + 'dom-info-sha': null, + metadata: null }, relationships: { tag: { @@ -967,6 +976,47 @@ describe('PercyClient', () => { } }); }); + + it('throws unknown property in invalid comparison json', () => { + spyOn(fs.promises, 'readFile') + .withArgs('foo/bar').and.resolveTo('bar'); + + const comparison = { + name: 'test', + tag: { + name: 'Samsung Galaxy S22', + osName: 'Android', + osVersion: '12', + width: 1080, + height: 2115, + orientation: 'portrait', + browserName: 'chrome', + browserVersion: 'Samsung Galaxy S22', + resolution: '1080 x 2340' + }, + tiles: [ + { + statusBarHeight: 0, + navBarHeight: 0, + headerHeight: 0, + footerHeight: 168, + fullscreen: false, + sha: 'abcd' + }], + externalDebugUrl: 'https://automate.browserstack.com/builds/acs', + metadata: { + windowHeight: 1947, + abc: 123 + } + }; + + PercyConfig.addSchema(CoreConfig.schemas); + const errors = PercyConfig.validate(comparison, '/comparison'); + expect(errors).not.toBe(null); + expect(errors.length).toBe(1); + expect(errors[0].path).toBe('metadata.abc'); + expect(errors[0].message).toBe('unknown property'); + }); }); describe('#uploadComparisonTile()', () => { @@ -1186,7 +1236,8 @@ describe('PercyClient', () => { 'external-debug-url': null, 'ignore-elements-data': null, 'consider-elements-data': null, - 'dom-info-sha': null + 'dom-info-sha': null, + metadata: null }, relationships: { tag: { diff --git a/packages/core/src/config.js b/packages/core/src/config.js index db2020389..b95a4bcd7 100644 --- a/packages/core/src/config.js +++ b/packages/core/src/config.js @@ -438,6 +438,16 @@ export const comparisonSchema = { name: { type: 'string' }, externalDebugUrl: { type: 'string' }, domInfoSha: { type: 'string' }, + metadata: { + type: 'object', + additionalProperties: false, + properties: { + windowHeight: { + type: 'integer', + minimum: 0 + } + } + }, tag: { type: 'object', additionalProperties: false, diff --git a/packages/webdriver-utils/src/providers/automateProvider.js b/packages/webdriver-utils/src/providers/automateProvider.js index 0a3fa036c..bc6202c90 100644 --- a/packages/webdriver-utils/src/providers/automateProvider.js +++ b/packages/webdriver-utils/src/providers/automateProvider.js @@ -113,13 +113,13 @@ export default class AutomateProvider extends GenericProvider { async getTiles(headerHeight, footerHeight, fullscreen) { if (!this.driver) throw new Error('Driver is null, please initialize driver with createDriver().'); log.debug('Starting actual screenshotting phase'); - + const dpr = await this.metaData.devicePixelRatio(); const response = await TimeIt.run('percyScreenshot:screenshot', async () => { return await this.browserstackExecutor('percyScreenshot', { state: 'screenshot', percyBuildId: this.buildInfo.id, screenshotType: 'singlepage', - scaleFactor: await this.metaData.devicePixelRatio(), + scaleFactor: dpr, options: this.options }); }); @@ -133,18 +133,22 @@ export default class AutomateProvider extends GenericProvider { const tiles = []; const tileResponse = JSON.parse(responseValue.result); log.debug('Tiles captured successfully'); - + const windowHeight = responseValue?.metadata?.window_height || 0; for (let tileData of tileResponse.sha) { tiles.push(new Tile({ - statusBarHeight: 0, - navBarHeight: 0, + statusBarHeight: tileResponse.header_height || 0, + navBarHeight: tileResponse.footer_height || 0, headerHeight, footerHeight, fullscreen, sha: tileData.split('-')[0] // drop build id })); } - return { tiles: tiles, domInfoSha: tileResponse.dom_sha }; + + const metadata = { + windowHeight: Math.ceil(windowHeight * dpr) + }; + return { tiles: tiles, domInfoSha: tileResponse.dom_sha, metadata: metadata }; } async browserstackExecutor(action, args) { diff --git a/packages/webdriver-utils/src/providers/genericProvider.js b/packages/webdriver-utils/src/providers/genericProvider.js index 1ebdaa83c..714b2a365 100644 --- a/packages/webdriver-utils/src/providers/genericProvider.js +++ b/packages/webdriver-utils/src/providers/genericProvider.js @@ -155,7 +155,8 @@ export default class GenericProvider { }, environmentInfo: [...this.environmentInfo].join('; '), clientInfo: [...this.clientInfo].join(' '), - domInfoSha: tiles.domInfoSha + domInfoSha: tiles.domInfoSha, + metadata: tiles.metadata || null }; } @@ -165,6 +166,11 @@ export default class GenericProvider { return 'dummyValue'; } + async getWindowHeight() { + // execute script and return window height + return await this.driver.executeScript({ script: 'return window.innerHeight', args: [] }); ; + } + async getTiles(headerHeight, footerHeight, fullscreen) { if (!this.driver) throw new Error('Driver is null, please initialize driver with createDriver().'); const base64content = await this.driver.takeScreenshot(); @@ -181,7 +187,10 @@ export default class GenericProvider { }) ], // TODO: Add Generic support sha for contextual diff for non-automate - domInfoSha: await this.getDomContent() + domInfoSha: await this.getDomContent(), + metadata: { + windowHeight: await this.getWindowHeight() + } }; } diff --git a/packages/webdriver-utils/test/providers/automateProvider.test.js b/packages/webdriver-utils/test/providers/automateProvider.test.js index 4b9f23829..5dc205913 100644 --- a/packages/webdriver-utils/test/providers/automateProvider.test.js +++ b/packages/webdriver-utils/test/providers/automateProvider.test.js @@ -213,7 +213,7 @@ describe('AutomateProvider', () => { const res = await automateProvider.getTiles(123, 456, false); expect(browserstackExecutorSpy).toHaveBeenCalledTimes(1); expect(executeScriptSpy).toHaveBeenCalledTimes(1); - expect(Object.keys(res).length).toEqual(2); + expect(Object.keys(res).length).toEqual(3); expect(res.domInfoSha).toBe('abc'); expect(res.tiles.length).toEqual(2); expect(res.tiles[0]).toBeInstanceOf(Tile); diff --git a/packages/webdriver-utils/test/providers/genericProvider.test.js b/packages/webdriver-utils/test/providers/genericProvider.test.js index b822481e2..3f3b9e0fb 100644 --- a/packages/webdriver-utils/test/providers/genericProvider.test.js +++ b/packages/webdriver-utils/test/providers/genericProvider.test.js @@ -37,6 +37,7 @@ describe('GenericProvider', () => { beforeEach(() => { spyOn(Driver.prototype, 'takeScreenshot').and.returnValue(Promise.resolve('123b=')); spyOn(GenericProvider.prototype, 'getHeaderFooter').and.returnValue(Promise.resolve([123, 456])); + spyOn(GenericProvider.prototype, 'getWindowHeight').and.returnValue(Promise.resolve(1947)); }); it('creates tiles from screenshot', async () => { @@ -168,7 +169,8 @@ describe('GenericProvider', () => { ignoredElementsData: { ignoreElementsData: [] }, consideredElementsData: { considerElementsData: [] }, clientInfo: 'local-poc-poa', - domInfoSha: 'mock-dom-sha' + domInfoSha: 'mock-dom-sha', + metadata: null }); }); }); @@ -224,6 +226,20 @@ describe('GenericProvider', () => { }); }); + describe('getWindowHeight', () => { + beforeEach(() => { + spyOn(Driver.prototype, 'executeScript').and.returnValue(Promise.resolve(true)); + }); + + it('should call executeScript to get windowHeight', async () => { + genericProvider = new GenericProvider('123', 'http:executorUrl', { platform: 'win' }, {}, 'local-poc-poa', 'staging-poc-poa', {}); + await genericProvider.createDriver(); + await genericProvider.getWindowHeight(); + expect(genericProvider.driver.executeScript).toHaveBeenCalledTimes(1); + expect(genericProvider.driver.executeScript).toHaveBeenCalledWith({ script: 'return window.innerHeight', args: [] }); + }); + }); + describe('removePercyCSS', () => { beforeEach(() => { spyOn(Driver.prototype, 'executeScript').and.returnValue(Promise.resolve(true));