From 6ef20f24f21543bd847bf6a345baeadbce325962 Mon Sep 17 00:00:00 2001 From: Joan Perals Tresserra Date: Tue, 12 May 2026 09:42:27 +0200 Subject: [PATCH 01/10] feat: Add capturePermutations method to ScreenshotPageObject --- src/page-objects/index.ts | 2 +- src/page-objects/screenshot.ts | 72 +++++++++++++++++++++++++++++++++- 2 files changed, 72 insertions(+), 2 deletions(-) diff --git a/src/page-objects/index.ts b/src/page-objects/index.ts index 5c6b173..43be3d8 100644 --- a/src/page-objects/index.ts +++ b/src/page-objects/index.ts @@ -1,6 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 export { default as BasePageObject } from './base'; -export { default as ScreenshotPageObject } from './screenshot'; +export { default as ScreenshotPageObject, PermutationScreenshot } from './screenshot'; export { default as EventsSpy } from './events-spy'; export { ScreenshotWithOffset, ElementSize, ElementRect, ElementOffset } from './types'; diff --git a/src/page-objects/screenshot.ts b/src/page-objects/screenshot.ts index b9f7a53..28600e2 100644 --- a/src/page-objects/screenshot.ts +++ b/src/page-objects/screenshot.ts @@ -3,9 +3,36 @@ import { ScrollAction, scrollAction } from '../browser-scripts'; import { parsePng } from '../image-utils'; import BasePageObject from './base'; -import { ElementOffset, ScreenshotCapturingOptions, ScreenshotWithOffset } from './types'; +import { ElementOffset, ElementSize, ScreenshotCapturingOptions, ScreenshotWithOffset } from './types'; import fullPageScreenshot from './full-page-screenshot'; +interface PermutationInfo extends ElementSize { + id: string; + offset: ElementOffset; +} + +export interface PermutationScreenshot extends ScreenshotWithOffset { + id: string; +} + +function getPermutationSizes(): PermutationInfo[] { + const pixelRatio = window.devicePixelRatio || 1; + return Array.prototype.slice + .call(document.querySelectorAll('[data-permutation]')) + .map(function (element: HTMLElement) { + const rect = element.getBoundingClientRect(); + return { + id: element.getAttribute('data-permutation')!, + width: rect.width * pixelRatio, + height: rect.height * pixelRatio, + offset: { + top: rect.top * pixelRatio, + left: rect.left * pixelRatio, + }, + }; + }); +} + export default class ScreenshotPageObject extends BasePageObject { constructor(browser: WebdriverIO.Browser, public readonly forceScrollAndMerge: boolean = false) { super(browser); @@ -65,4 +92,47 @@ export default class ScreenshotPageObject extends BasePageObject { const image = await parsePng(screenshot); return { image, offset, height, width }; } + + async capturePermutations(): Promise { + await this.windowScrollTo({ top: 0, left: 0 }); + + // Adapt viewport height to fit all elements before taking a screenshot + const originalWindowSize = await this.fitWindowHeightToContent(); + + const screenshot = await this.fullPageScreenshot(); + const image = await parsePng(screenshot); + const permutations = await this.browser.execute(getPermutationSizes); + + // Restore window size after taking the screenshot + await this.safeSetWindowSize(originalWindowSize.width, originalWindowSize.height); + + if (permutations.length === 0) { + throw new Error('No permutations found on current page.'); + } + + return permutations.map(permutation => ({ ...permutation, image })); + } + + private async fitWindowHeightToContent(): Promise<{ width: number; height: number }> { + const originalWindowSize = await this.browser.getWindowSize(); + const { viewportHeight, pageHeight } = await this.browser.execute(() => ({ + pageHeight: document.documentElement.scrollHeight, + viewportHeight: window.innerHeight, + })); + const windowUIHeight = originalWindowSize.height - viewportHeight; + await this.safeSetWindowSize(originalWindowSize.width, pageHeight + windowUIHeight); + return originalWindowSize; + } + + private async safeSetWindowSize(width: number, height: number): Promise { + try { + await this.browser.setWindowSize(width, height); + } catch (error) { + if (error instanceof Error && error.message.includes('Method has not yet been implemented')) { + console.log('setWindowSize is not supported on this device'); + } else { + throw error; + } + } + } } From 67121d9ff1b2b7b129a6bb352613c01c6dd8260e Mon Sep 17 00:00:00 2001 From: Joan Perals Tresserra Date: Tue, 12 May 2026 11:11:46 +0200 Subject: [PATCH 02/10] Add test coverage --- test/fixtures/test-permutations.html | 26 +++++++++++++++++++++ test/page-object.test.ts | 35 ++++++++++++++++++++++++++-- 2 files changed, 59 insertions(+), 2 deletions(-) create mode 100644 test/fixtures/test-permutations.html diff --git a/test/fixtures/test-permutations.html b/test/fixtures/test-permutations.html new file mode 100644 index 0000000..24c8659 --- /dev/null +++ b/test/fixtures/test-permutations.html @@ -0,0 +1,26 @@ + + + + + + Permutations test page + + + +
Permutation 1
+
Permutation 2
+
Permutation 3
+ + diff --git a/test/page-object.test.ts b/test/page-object.test.ts index a6a1450..204a4e2 100644 --- a/test/page-object.test.ts +++ b/test/page-object.test.ts @@ -6,9 +6,9 @@ import useBrowser from '../src/use-browser'; import './utils/setup-local-driver'; type TestFn = (page: ScreenshotPageObject) => Promise; -function setupTest(testFn: TestFn) { +function setupTest(testFn: TestFn, url = './test-page-object.html') { return useBrowser(async browser => { - await browser.url('./test-page-object.html'); + await browser.url(url); await testFn(new ScreenshotPageObject(browser)); }); } @@ -362,3 +362,34 @@ test( }); }) ); + +describe('capturePermutations', () => { + test( + 'captures all permutations with correct ids and dimensions', + setupTest(async page => { + const permutations = await page.capturePermutations(); + + expect(permutations).toHaveLength(3); + expect(permutations.map(p => p.id)).toEqual(['perm-1', 'perm-2', 'perm-3']); + + permutations.forEach(perm => { + expect(perm.image).toBeDefined(); + expect(perm.width).toBeGreaterThan(0); + expect(perm.height).toBeGreaterThan(0); + expect(perm.offset.top).toBeGreaterThanOrEqual(0); + expect(perm.offset.left).toBeGreaterThanOrEqual(0); + }); + + // Verify permutations are in order (top to bottom) + expect(permutations[0].offset.top).toBeLessThan(permutations[1].offset.top); + expect(permutations[1].offset.top).toBeLessThan(permutations[2].offset.top); + }, './test-permutations.html') + ); + + test( + 'throws error when no permutations found', + setupTest(async page => { + await expect(page.capturePermutations()).rejects.toThrowError('No permutations found on current page.'); + }) + ); +}); From 96c7a6e697446790bdd890f32557e5cdc8e1bac4 Mon Sep 17 00:00:00 2001 From: Joan Perals Tresserra Date: Tue, 12 May 2026 11:52:55 +0200 Subject: [PATCH 03/10] Define permutations script outside of browser.execute call --- src/browser-scripts/index.ts | 1 + src/browser-scripts/permutations.ts | 38 +++++++++++++++++++++++++++ src/page-objects/screenshot.ts | 40 ++++++++--------------------- 3 files changed, 49 insertions(+), 30 deletions(-) create mode 100644 src/browser-scripts/permutations.ts diff --git a/src/browser-scripts/index.ts b/src/browser-scripts/index.ts index 97436f0..d15058a 100644 --- a/src/browser-scripts/index.ts +++ b/src/browser-scripts/index.ts @@ -3,3 +3,4 @@ export * from './scroll'; export * from './sizes'; export * from './events-spy'; +export * from './permutations'; diff --git a/src/browser-scripts/permutations.ts b/src/browser-scripts/permutations.ts new file mode 100644 index 0000000..dfa3b55 --- /dev/null +++ b/src/browser-scripts/permutations.ts @@ -0,0 +1,38 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { ElementOffset, ElementSize } from '../page-objects/types'; + +export interface PermutationInfo extends ElementSize { + id: string; + offset: ElementOffset; +} + +export interface PageDimensions { + pageHeight: number; + viewportHeight: number; +} + +export function getPermutationSizes(): PermutationInfo[] { + var pixelRatio = window.devicePixelRatio || 1; + return Array.prototype.slice + .call(document.querySelectorAll('[data-permutation]')) + .map(function (element: HTMLElement) { + var rect = element.getBoundingClientRect(); + return { + id: element.getAttribute('data-permutation') || '', + width: rect.width * pixelRatio, + height: rect.height * pixelRatio, + offset: { + top: rect.top * pixelRatio, + left: rect.left * pixelRatio, + }, + }; + }); +} + +export function getPageDimensions(): PageDimensions { + return { + pageHeight: document.documentElement.scrollHeight, + viewportHeight: window.innerHeight, + }; +} diff --git a/src/page-objects/screenshot.ts b/src/page-objects/screenshot.ts index 28600e2..158b782 100644 --- a/src/page-objects/screenshot.ts +++ b/src/page-objects/screenshot.ts @@ -1,38 +1,21 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { ScrollAction, scrollAction } from '../browser-scripts'; +import { + ScrollAction, + scrollAction, + getPermutationSizes, + getPageDimensions, + PermutationInfo, +} from '../browser-scripts'; import { parsePng } from '../image-utils'; import BasePageObject from './base'; -import { ElementOffset, ElementSize, ScreenshotCapturingOptions, ScreenshotWithOffset } from './types'; +import { ElementOffset, ScreenshotCapturingOptions, ScreenshotWithOffset } from './types'; import fullPageScreenshot from './full-page-screenshot'; -interface PermutationInfo extends ElementSize { - id: string; - offset: ElementOffset; -} - export interface PermutationScreenshot extends ScreenshotWithOffset { id: string; } -function getPermutationSizes(): PermutationInfo[] { - const pixelRatio = window.devicePixelRatio || 1; - return Array.prototype.slice - .call(document.querySelectorAll('[data-permutation]')) - .map(function (element: HTMLElement) { - const rect = element.getBoundingClientRect(); - return { - id: element.getAttribute('data-permutation')!, - width: rect.width * pixelRatio, - height: rect.height * pixelRatio, - offset: { - top: rect.top * pixelRatio, - left: rect.left * pixelRatio, - }, - }; - }); -} - export default class ScreenshotPageObject extends BasePageObject { constructor(browser: WebdriverIO.Browser, public readonly forceScrollAndMerge: boolean = false) { super(browser); @@ -110,15 +93,12 @@ export default class ScreenshotPageObject extends BasePageObject { throw new Error('No permutations found on current page.'); } - return permutations.map(permutation => ({ ...permutation, image })); + return permutations.map((permutation: PermutationInfo) => ({ ...permutation, image })); } private async fitWindowHeightToContent(): Promise<{ width: number; height: number }> { const originalWindowSize = await this.browser.getWindowSize(); - const { viewportHeight, pageHeight } = await this.browser.execute(() => ({ - pageHeight: document.documentElement.scrollHeight, - viewportHeight: window.innerHeight, - })); + const { viewportHeight, pageHeight } = await this.browser.execute(getPageDimensions); const windowUIHeight = originalWindowSize.height - viewportHeight; await this.safeSetWindowSize(originalWindowSize.width, pageHeight + windowUIHeight); return originalWindowSize; From 46f769216bc62126ba9c61f5748e38c04ea9d53c Mon Sep 17 00:00:00 2001 From: Joan Perals Tresserra Date: Tue, 12 May 2026 12:12:46 +0200 Subject: [PATCH 04/10] Exclude mobile-specific error branch from coverage --- src/page-objects/screenshot.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/page-objects/screenshot.ts b/src/page-objects/screenshot.ts index 158b782..e977652 100644 --- a/src/page-objects/screenshot.ts +++ b/src/page-objects/screenshot.ts @@ -108,6 +108,7 @@ export default class ScreenshotPageObject extends BasePageObject { try { await this.browser.setWindowSize(width, height); } catch (error) { + /* istanbul ignore next -- setWindowSize is unsupported on some mobile browsers, not testable in CI */ if (error instanceof Error && error.message.includes('Method has not yet been implemented')) { console.log('setWindowSize is not supported on this device'); } else { From b1377c80faf56a6264e88edc81929eb09b1bffb5 Mon Sep 17 00:00:00 2001 From: Joan Perals Tresserra Date: Tue, 12 May 2026 12:34:09 +0200 Subject: [PATCH 05/10] Fix istanbul comment --- src/page-objects/screenshot.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/page-objects/screenshot.ts b/src/page-objects/screenshot.ts index e977652..607e6ec 100644 --- a/src/page-objects/screenshot.ts +++ b/src/page-objects/screenshot.ts @@ -107,8 +107,7 @@ export default class ScreenshotPageObject extends BasePageObject { private async safeSetWindowSize(width: number, height: number): Promise { try { await this.browser.setWindowSize(width, height); - } catch (error) { - /* istanbul ignore next -- setWindowSize is unsupported on some mobile browsers, not testable in CI */ + } catch (error) /* istanbul ignore next -- setWindowSize is unsupported on some mobile browsers, not testable in CI */ { if (error instanceof Error && error.message.includes('Method has not yet been implemented')) { console.log('setWindowSize is not supported on this device'); } else { From 9dec76012838aa812806e2cb39434c355b2682bb Mon Sep 17 00:00:00 2001 From: Joan Perals Tresserra Date: Tue, 12 May 2026 13:19:37 +0200 Subject: [PATCH 06/10] Again --- src/page-objects/screenshot.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/page-objects/screenshot.ts b/src/page-objects/screenshot.ts index 607e6ec..d4b65e6 100644 --- a/src/page-objects/screenshot.ts +++ b/src/page-objects/screenshot.ts @@ -107,10 +107,9 @@ export default class ScreenshotPageObject extends BasePageObject { private async safeSetWindowSize(width: number, height: number): Promise { try { await this.browser.setWindowSize(width, height); - } catch (error) /* istanbul ignore next -- setWindowSize is unsupported on some mobile browsers, not testable in CI */ { - if (error instanceof Error && error.message.includes('Method has not yet been implemented')) { - console.log('setWindowSize is not supported on this device'); - } else { + } catch (error) { + /* istanbul ignore next -- setWindowSize is unsupported on some mobile browsers, not testable in CI */ + if (!(error instanceof Error && error.message.includes('Method has not yet been implemented'))) { throw error; } } From 0a3dd6f90234db1126ff7df3eb0f2edbf17f16c0 Mon Sep 17 00:00:00 2001 From: Joan Perals Tresserra Date: Tue, 12 May 2026 13:30:00 +0200 Subject: [PATCH 07/10] Again --- src/page-objects/screenshot.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/page-objects/screenshot.ts b/src/page-objects/screenshot.ts index d4b65e6..8ca8b08 100644 --- a/src/page-objects/screenshot.ts +++ b/src/page-objects/screenshot.ts @@ -108,8 +108,10 @@ export default class ScreenshotPageObject extends BasePageObject { try { await this.browser.setWindowSize(width, height); } catch (error) { - /* istanbul ignore next -- setWindowSize is unsupported on some mobile browsers, not testable in CI */ - if (!(error instanceof Error && error.message.includes('Method has not yet been implemented'))) { + // setWindowSize is unsupported on some mobile browsers, not testable in CI + /* istanbul ignore next */ if ( + !(error instanceof Error && error.message.includes('Method has not yet been implemented')) + ) { throw error; } } From 9c35ee61530832d8cd3b6dd353c221db6f9a92fe Mon Sep 17 00:00:00 2001 From: Joan Perals Tresserra Date: Tue, 12 May 2026 14:40:38 +0200 Subject: [PATCH 08/10] Again --- src/page-objects/screenshot.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/page-objects/screenshot.ts b/src/page-objects/screenshot.ts index 8ca8b08..e6cfe3d 100644 --- a/src/page-objects/screenshot.ts +++ b/src/page-objects/screenshot.ts @@ -107,11 +107,8 @@ export default class ScreenshotPageObject extends BasePageObject { private async safeSetWindowSize(width: number, height: number): Promise { try { await this.browser.setWindowSize(width, height); - } catch (error) { - // setWindowSize is unsupported on some mobile browsers, not testable in CI - /* istanbul ignore next */ if ( - !(error instanceof Error && error.message.includes('Method has not yet been implemented')) - ) { + } catch (error) /* istanbul ignore next -- setWindowSize is unsupported on some mobile browsers, not testable in CI */ { + if (!(error instanceof Error && error.message.includes('Method has not yet been implemented'))) { throw error; } } From cc1037073669c5a8c4280df6e14eeb6cde85ceb6 Mon Sep 17 00:00:00 2001 From: Joan Perals Tresserra Date: Tue, 12 May 2026 14:44:53 +0200 Subject: [PATCH 09/10] Again --- src/page-objects/screenshot.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/page-objects/screenshot.ts b/src/page-objects/screenshot.ts index e6cfe3d..31c4691 100644 --- a/src/page-objects/screenshot.ts +++ b/src/page-objects/screenshot.ts @@ -104,11 +104,14 @@ export default class ScreenshotPageObject extends BasePageObject { return originalWindowSize; } + /* istanbul ignore next -- setWindowSize is unsupported on some mobile browsers, not testable in CI */ private async safeSetWindowSize(width: number, height: number): Promise { try { await this.browser.setWindowSize(width, height); - } catch (error) /* istanbul ignore next -- setWindowSize is unsupported on some mobile browsers, not testable in CI */ { - if (!(error instanceof Error && error.message.includes('Method has not yet been implemented'))) { + } catch (error) { + if (error instanceof Error && error.message.includes('Method has not yet been implemented')) { + console.log('setWindowSize is not supported on this device'); + } else { throw error; } } From e91752db2f3c6f6178c1c32a069e01e09c271027 Mon Sep 17 00:00:00 2001 From: Joan Perals Tresserra Date: Wed, 13 May 2026 14:33:07 +0200 Subject: [PATCH 10/10] Check for existance of permutations before taking screenshot --- src/page-objects/screenshot.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/page-objects/screenshot.ts b/src/page-objects/screenshot.ts index 31c4691..f2965cb 100644 --- a/src/page-objects/screenshot.ts +++ b/src/page-objects/screenshot.ts @@ -82,8 +82,6 @@ export default class ScreenshotPageObject extends BasePageObject { // Adapt viewport height to fit all elements before taking a screenshot const originalWindowSize = await this.fitWindowHeightToContent(); - const screenshot = await this.fullPageScreenshot(); - const image = await parsePng(screenshot); const permutations = await this.browser.execute(getPermutationSizes); // Restore window size after taking the screenshot @@ -93,6 +91,9 @@ export default class ScreenshotPageObject extends BasePageObject { throw new Error('No permutations found on current page.'); } + const screenshot = await this.fullPageScreenshot(); + const image = await parsePng(screenshot); + return permutations.map((permutation: PermutationInfo) => ({ ...permutation, image })); }