diff --git a/.changeset/implement-web-ignore-regions.md b/.changeset/implement-web-ignore-regions.md new file mode 100644 index 000000000..3bb033cf9 --- /dev/null +++ b/.changeset/implement-web-ignore-regions.md @@ -0,0 +1,22 @@ +--- +'@wdio/image-comparison-core': minor +'@wdio/visual-service': minor +--- + +## #857 Support ignore regions for web screenshots + +Add `ignore` support to all web screenshot methods (`saveScreen`/`checkScreen`, `saveElement`/`checkElement`, `saveFullPageScreen`/`checkFullPageScreen`) so that specified elements can be blocked out during visual comparison. This brings web parity with the native-app ignore-region support that already existed. + +### Changes + +- **Ignore regions for full-page screenshots:** new `determineWebFullPageIgnoreRegions` function that calculates ignore-region rectangles for full-page screenshots, including a `fullPageCropTopPaddingCSS` correction for mobile scroll-and-stitch scenarios where the address-bar shadow padding shifts element positions +- **Consolidated `ignoreRegionPadding`:** moved `ignoreRegionPadding` into `BaseWebScreenshotOptions` so it is inherited by all web methods instead of being duplicated per method +- **Fix `isAndroidNativeWebScreenshot` type:** ensure `nativeWebScreenshot` is always a boolean (was accidentally an object for LambdaTest capabilities), preventing ignore-region DPR scaling failures +- **Fix viewport rounding for mobile:** restore `Math.round()` in `injectWebviewOverlay` and remove `Math.min` clamping in `getMobileViewPortPosition` to prevent 1-pixel crop shifts during full-page stitching +- **Fix `scrollElementIntoView` for scrolled pages:** account for `currentPosition` (existing scroll offset) when computing the target scroll position, so elements are scrolled into view correctly when the page is already scrolled +- **Dismiss Chrome Start Surface on Android:** when Chrome's tab-overview UI blocks the webview overlay, automatically press the Android Back button (up to 4 retries) to restore the active tab before measuring the viewport +- **Add hybrid status bar blockout:** on hybrid apps the statusbar was not blocked out which could result in flaky tests regarding battery and reception + +# Committers: 1 + +- Wim Selles ([@wswebcreation](https://github.com/wswebcreation)) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a5309af80..2a04a0177 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -23,15 +23,14 @@ jobs: steps: - name: 🧐 Determine Job to Run id: set-vars + env: + PR_TITLE: ${{ github.event.pull_request.title }} run: | - PR_TITLE="${{ github.event.pull_request.title }}" - UNIT_TEST_REGEX='(@typescript-eslint|vitest|eslint|@types|@tsconfig|ts-node|jsdom|typescript)' - WEB_TEST_REGEX='(@wdio(?!\/appium-service)|copyfiles|rimraf|saucelabs|lambdatest|webdriverio)' - if [[ $PR_TITLE =~ $UNIT_TEST_REGEX ]]; then - echo "::set-output name=run_unit_tests::true" - fi - if [[ $PR_TITLE =~ $WEB_TEST_REGEX ]]; then - echo "::set-output name=run_web_tests::true" + echo "$PR_TITLE" | grep -qE '@typescript-eslint|vitest|eslint|@types|@tsconfig|ts-node|jsdom|typescript' && echo "run_unit_tests=true" >> "$GITHUB_OUTPUT" + if echo "$PR_TITLE" | grep -qE 'copyfiles|rimraf|saucelabs|lambdatest|webdriverio'; then + echo "run_web_tests=true" >> "$GITHUB_OUTPUT" + elif echo "$PR_TITLE" | grep -q '@wdio/' && ! echo "$PR_TITLE" | grep -q '@wdio/appium-service'; then + echo "run_web_tests=true" >> "$GITHUB_OUTPUT" fi ci-dependency-unit-test: diff --git a/package.json b/package.json index 628e9c924..ace10df4c 100644 --- a/package.json +++ b/package.json @@ -52,43 +52,43 @@ "watch": "pnpm run -r --parallel watch" }, "devDependencies": { - "@changesets/cli": "^2.29.8", - "@tsconfig/node20": "^20.1.8", + "@changesets/cli": "^2.30.0", + "@tsconfig/node20": "^20.1.9", "@types/eslint": "^9.6.1", "@types/inquirer": "^9.0.9", "@types/jsdom": "~21.1.7", "@types/node": "^24", "@types/xml2js": "~0.4.14", - "@typescript-eslint/eslint-plugin": "^8.53.0", + "@typescript-eslint/eslint-plugin": "^8.57.0", "@wdio/globals": "^9.23.0", - "@wdio/mocha-framework": "^9.23.0", - "@typescript-eslint/parser": "^8.53.0", - "@typescript-eslint/utils": "^8.53.0", + "@wdio/mocha-framework": "^9.25.0", + "@typescript-eslint/parser": "^8.57.0", + "@typescript-eslint/utils": "^8.57.0", "@vitest/coverage-v8": "^3.2.4", "@vitest/ui": "^3.2.4", - "@wdio/appium-service": "^9.23.0", - "@wdio/browserstack-service": "^9.23.0", - "@wdio/cli": "^9.23.0", - "@wdio/local-runner": "^9.23.0", - "@wdio/sauce-service": "^9.23.0", - "@wdio/shared-store-service": "^9.23.0", - "@wdio/spec-reporter": "^9.20.0", - "@wdio/types": "^9.20.0", + "@wdio/appium-service": "^9.25.0", + "@wdio/browserstack-service": "^9.25.0", + "@wdio/cli": "^9.25.0", + "@wdio/local-runner": "^9.25.0", + "@wdio/sauce-service": "^9.25.0", + "@wdio/shared-store-service": "^9.25.0", + "@wdio/spec-reporter": "^9.25.0", + "@wdio/types": "^9.25.0", "cross-env": "^7.0.3", - "eslint": "^9.39.2", + "eslint": "^9.39.4", "eslint-plugin-import": "^2.32.0", "eslint-plugin-unicorn": "^56.0.1", - "eslint-plugin-wdio": "^9.23.0", + "eslint-plugin-wdio": "^9.25.0", "husky": "^9.1.7", "jsdom": "^26.1.0", "npm-run-all2": "^8.0.4", - "release-it": "^19.2.3", - "rimraf": "^6.1.2", + "release-it": "^19.2.4", + "rimraf": "^6.1.3", "saucelabs": "^9.0.2", "ts-node": "^10.9.2", "typescript": "^5.9.3", "vitest": "^3.2.4", - "webdriverio": "^9.23.0", + "webdriverio": "^9.25.0", "wdio-lambdatest-service": "^4.0.1" }, "packageManager": "pnpm@9.15.9+sha256.cf86a7ad764406395d4286a6d09d730711720acc6d93e9dce9ac7ac4dc4a28a7" diff --git a/packages/image-comparison-core/package.json b/packages/image-comparison-core/package.json index c44cab60c..b5a488f17 100644 --- a/packages/image-comparison-core/package.json +++ b/packages/image-comparison-core/package.json @@ -36,10 +36,10 @@ "dependencies": { "jimp": "^1.6.0", "@wdio/logger": "^9.18.0", - "@wdio/types": "^9.20.0" + "@wdio/types": "^9.25.0" }, "devDependencies": { - "webdriverio": "^9.23.0" + "webdriverio": "^9.25.0" }, "publishConfig": { "access": "public" diff --git a/packages/image-comparison-core/src/__snapshots__/base.test.ts.snap b/packages/image-comparison-core/src/__snapshots__/base.test.ts.snap index 300037063..9c059396c 100644 --- a/packages/image-comparison-core/src/__snapshots__/base.test.ts.snap +++ b/packages/image-comparison-core/src/__snapshots__/base.test.ts.snap @@ -30,6 +30,7 @@ exports[`BaseClass > initializes default options correctly 1`] = ` "formatImageName": "{tag}-{browserName}-{width}x{height}-dpr-{dpr}", "fullPageScrollTimeout": 1500, "hideScrollBars": true, + "ignoreRegionPadding": 1, "isHybridApp": false, "savePerInstance": false, "tabbableOptions": { diff --git a/packages/image-comparison-core/src/base.interfaces.ts b/packages/image-comparison-core/src/base.interfaces.ts index 17094f7d4..c39894cf4 100644 --- a/packages/image-comparison-core/src/base.interfaces.ts +++ b/packages/image-comparison-core/src/base.interfaces.ts @@ -51,6 +51,13 @@ export interface BaseWebScreenshotOptions { * @default true */ hideScrollBars?: boolean; + /** + * Padding in device pixels added to each side of ignore regions (makes each region 2× this value wider and higher). + * Helps avoid 1px boundary differences on high-DPR / BiDi. Set to 0 to disable. + * Applies to screen, element, and full-page web methods. + * @default 1 + */ + ignoreRegionPadding?: number; /** * Elements to hide before taking screenshot * @default [] diff --git a/packages/image-comparison-core/src/clientSideScripts/injectWebviewOverlay.test.ts b/packages/image-comparison-core/src/clientSideScripts/injectWebviewOverlay.test.ts index 73defb4e5..13f18f4af 100644 --- a/packages/image-comparison-core/src/clientSideScripts/injectWebviewOverlay.test.ts +++ b/packages/image-comparison-core/src/clientSideScripts/injectWebviewOverlay.test.ts @@ -67,6 +67,40 @@ describe('injectWebviewOverlay', () => { }) }) + it('should round values to integers with non-integer DPR (Android)', () => { + Object.defineProperty(window, 'devicePixelRatio', { + value: 2.625, + configurable: true, + }) + Object.defineProperty(window, 'innerWidth', { + value: 412, + configurable: true, + }) + Object.defineProperty(document.documentElement, 'clientHeight', { + value: 363, + configurable: true, + }) + + injectWebviewOverlay(true) + + const overlay = document.querySelector('[data-test="ics-overlay"]') as HTMLDivElement + const event = new window.MouseEvent('click', { + clientX: 206, + clientY: 181, + bubbles: true, + }) + overlay.dispatchEvent(event) + + const parsedData = JSON.parse(overlay.dataset.icsWebviewData!) + + expect(parsedData).toEqual({ + x: Math.round(206 * 2.625), + y: Math.round(181 * 2.625), + width: Math.round(412 * 2.625), + height: Math.round(363 * 2.625), + }) + }) + it('should use DPR = 1 for iOS (isAndroid = false)', () => { injectWebviewOverlay(false) diff --git a/packages/image-comparison-core/src/clientSideScripts/scrollElementIntoView.test.ts b/packages/image-comparison-core/src/clientSideScripts/scrollElementIntoView.test.ts index a12addd57..ad415fddf 100644 --- a/packages/image-comparison-core/src/clientSideScripts/scrollElementIntoView.test.ts +++ b/packages/image-comparison-core/src/clientSideScripts/scrollElementIntoView.test.ts @@ -69,7 +69,8 @@ describe('scrollElementIntoView', () => { const result = scrollElementIntoView(mockElement, addressBarShadowPadding) expect(result).toBe(50) - expect(mockHtmlNode.scrollTop).toBe(90) + // currentPosition (50) + BCR.top (100) - padding (10) = 140 + expect(mockHtmlNode.scrollTop).toBe(140) }) it('should return current scroll position when body node has scroll', () => { @@ -80,7 +81,8 @@ describe('scrollElementIntoView', () => { const result = scrollElementIntoView(mockElement, addressBarShadowPadding) expect(result).toBe(50) - expect(mockBodyNode.scrollTop).toBe(90) + // currentPosition (50) + BCR.top (100) - padding (10) = 140 + expect(mockBodyNode.scrollTop).toBe(140) }) it('should not scroll when neither html nor body is scrollable', () => { diff --git a/packages/image-comparison-core/src/clientSideScripts/scrollElementIntoView.ts b/packages/image-comparison-core/src/clientSideScripts/scrollElementIntoView.ts index 6e7a8e836..1e8f94149 100644 --- a/packages/image-comparison-core/src/clientSideScripts/scrollElementIntoView.ts +++ b/packages/image-comparison-core/src/clientSideScripts/scrollElementIntoView.ts @@ -19,7 +19,10 @@ export default function scrollElementIntoView(element: HTMLElement, addressBarSh } const { top } = element.getBoundingClientRect() - const yPosition = top - addressBarShadowPadding + // BCR.top is viewport-relative, so the element's document position is + // currentPosition + top. Scroll there (minus padding) to place the + // element at the viewport top. + const yPosition = currentPosition + top - addressBarShadowPadding // Scroll to the position if (htmlNode.scrollHeight > htmlNode.clientHeight) { diff --git a/packages/image-comparison-core/src/commands/__snapshots__/checkFullPageScreen.test.ts.snap b/packages/image-comparison-core/src/commands/__snapshots__/checkFullPageScreen.test.ts.snap index 02f107cfc..4e26155dd 100644 --- a/packages/image-comparison-core/src/commands/__snapshots__/checkFullPageScreen.test.ts.snap +++ b/packages/image-comparison-core/src/commands/__snapshots__/checkFullPageScreen.test.ts.snap @@ -96,6 +96,8 @@ exports[`checkFullPageScreen > should execute checkFullPageScreen with basic opt "hideAfterFirstScroll": [], "hideElements": [], "hideScrollBars": true, + "ignore": undefined, + "ignoreRegionPadding": undefined, "removeElements": [], "waitForFontsLoaded": true, }, @@ -328,6 +330,8 @@ exports[`checkFullPageScreen > should handle all full page specific options 1`] "hideAfterFirstScroll": [], "hideElements": [], "hideScrollBars": false, + "ignore": undefined, + "ignoreRegionPadding": undefined, "removeElements": [], "waitForFontsLoaded": false, }, @@ -479,6 +483,8 @@ exports[`checkFullPageScreen > should handle hideAfterFirstScroll correctly 1`] ], "hideElements": [], "hideScrollBars": true, + "ignore": undefined, + "ignoreRegionPadding": undefined, "removeElements": [], "waitForFontsLoaded": true, }, @@ -630,6 +636,8 @@ exports[`checkFullPageScreen > should handle hideElements and removeElements cor }, ], "hideScrollBars": true, + "ignore": undefined, + "ignoreRegionPadding": undefined, "removeElements": [ { "elementId": "remove-element", @@ -867,6 +875,8 @@ exports[`checkFullPageScreen > should handle undefined method options with fallb "hideAfterFirstScroll": [], "hideElements": [], "hideScrollBars": true, + "ignore": undefined, + "ignoreRegionPadding": undefined, "removeElements": [], "waitForFontsLoaded": true, }, diff --git a/packages/image-comparison-core/src/commands/__snapshots__/checkWebElement.test.ts.snap b/packages/image-comparison-core/src/commands/__snapshots__/checkWebElement.test.ts.snap index c913238dd..921e51d4c 100644 --- a/packages/image-comparison-core/src/commands/__snapshots__/checkWebElement.test.ts.snap +++ b/packages/image-comparison-core/src/commands/__snapshots__/checkWebElement.test.ts.snap @@ -25,6 +25,7 @@ exports[`checkWebElement > should execute checkWebElement with basic options 2`] "baselineFolder": "/test/baseline", "diffFolder": "/test/diff", }, + "ignore": undefined, "instanceData": { "appName": "TestApp", "browserName": "Chrome", @@ -97,6 +98,7 @@ exports[`checkWebElement > should execute checkWebElement with basic options 2`] "enableLegacyScreenshotMethod": false, "hideElements": [], "hideScrollBars": true, + "ignoreRegionPadding": undefined, "removeElements": [], "resizeDimensions": undefined, "waitForFontsLoaded": true, @@ -425,6 +427,7 @@ exports[`checkWebElement > should handle custom element options 1`] = ` "baselineFolder": "/test/baseline", "diffFolder": "/test/diff", }, + "ignore": undefined, "instanceData": { "appName": "TestApp", "browserName": "Chrome", @@ -505,6 +508,7 @@ exports[`checkWebElement > should handle custom element options 1`] = ` }, ], "hideScrollBars": false, + "ignoreRegionPadding": undefined, "removeElements": [ { "elementId": "remove-element", @@ -764,6 +768,7 @@ exports[`checkWebElement > should handle undefined method options with fallbacks "baselineFolder": "/test/baseline", "diffFolder": "/test/diff", }, + "ignore": undefined, "instanceData": { "appName": "TestApp", "browserName": "Chrome", @@ -836,6 +841,7 @@ exports[`checkWebElement > should handle undefined method options with fallbacks "enableLegacyScreenshotMethod": false, "hideElements": [], "hideScrollBars": true, + "ignoreRegionPadding": undefined, "removeElements": [], "resizeDimensions": undefined, "waitForFontsLoaded": true, diff --git a/packages/image-comparison-core/src/commands/__snapshots__/checkWebScreen.test.ts.snap b/packages/image-comparison-core/src/commands/__snapshots__/checkWebScreen.test.ts.snap index 584d6d181..09ecabecc 100644 --- a/packages/image-comparison-core/src/commands/__snapshots__/checkWebScreen.test.ts.snap +++ b/packages/image-comparison-core/src/commands/__snapshots__/checkWebScreen.test.ts.snap @@ -22,6 +22,7 @@ exports[`checkWebScreen > should execute checkWebScreen with basic options 2`] = "baselineFolder": "/test/baseline", "diffFolder": "/test/diff", }, + "ignore": undefined, "instanceData": { "appName": "TestApp", "browserName": "Chrome", @@ -95,6 +96,7 @@ exports[`checkWebScreen > should execute checkWebScreen with basic options 2`] = "enableLegacyScreenshotMethod": false, "hideElements": [], "hideScrollBars": true, + "ignoreRegionPadding": undefined, "removeElements": [], "waitForFontsLoaded": true, }, @@ -333,6 +335,7 @@ exports[`checkWebScreen > should handle all method options correctly 1`] = ` "baselineFolder": "/test/baseline", "diffFolder": "/test/diff", }, + "ignore": undefined, "instanceData": { "appName": "TestApp", "browserName": "Chrome", @@ -414,6 +417,7 @@ exports[`checkWebScreen > should handle all method options correctly 1`] = ` }, ], "hideScrollBars": false, + "ignoreRegionPadding": undefined, "removeElements": [ { "elementId": "remove-element", @@ -577,6 +581,7 @@ exports[`checkWebScreen > should handle hideElements and removeElements correctl "baselineFolder": "/test/baseline", "diffFolder": "/test/diff", }, + "ignore": undefined, "instanceData": { "appName": "TestApp", "browserName": "Chrome", @@ -658,6 +663,7 @@ exports[`checkWebScreen > should handle hideElements and removeElements correctl }, ], "hideScrollBars": true, + "ignoreRegionPadding": undefined, "removeElements": [ { "elementId": "test-element", @@ -821,6 +827,7 @@ exports[`checkWebScreen > should handle native context correctly 1`] = ` "baselineFolder": "/test/baseline", "diffFolder": "/test/diff", }, + "ignore": undefined, "instanceData": { "appName": "TestApp", "browserName": "Chrome", @@ -894,6 +901,7 @@ exports[`checkWebScreen > should handle native context correctly 1`] = ` "enableLegacyScreenshotMethod": false, "hideElements": [], "hideScrollBars": true, + "ignoreRegionPadding": undefined, "removeElements": [], "waitForFontsLoaded": true, }, @@ -1049,6 +1057,7 @@ exports[`checkWebScreen > should merge compare options correctly 1`] = ` "baselineFolder": "/test/baseline", "diffFolder": "/test/diff", }, + "ignore": undefined, "instanceData": { "appName": "TestApp", "browserName": "Chrome", @@ -1122,6 +1131,7 @@ exports[`checkWebScreen > should merge compare options correctly 1`] = ` "enableLegacyScreenshotMethod": false, "hideElements": [], "hideScrollBars": true, + "ignoreRegionPadding": undefined, "removeElements": [], "waitForFontsLoaded": true, }, diff --git a/packages/image-comparison-core/src/commands/__snapshots__/saveElement.test.ts.snap b/packages/image-comparison-core/src/commands/__snapshots__/saveElement.test.ts.snap index db139e1ee..772c3956e 100644 --- a/packages/image-comparison-core/src/commands/__snapshots__/saveElement.test.ts.snap +++ b/packages/image-comparison-core/src/commands/__snapshots__/saveElement.test.ts.snap @@ -185,6 +185,7 @@ exports[`saveElement > should execute saveWebElement when isNativeContext is fal "baselineFolder": "/test/baseline", "diffFolder": "/test/diff", }, + "ignore": undefined, "instanceData": { "appName": "TestApp", "browserName": "Chrome", diff --git a/packages/image-comparison-core/src/commands/check.interfaces.ts b/packages/image-comparison-core/src/commands/check.interfaces.ts index 42baa0390..9de9a2c6d 100644 --- a/packages/image-comparison-core/src/commands/check.interfaces.ts +++ b/packages/image-comparison-core/src/commands/check.interfaces.ts @@ -15,7 +15,7 @@ export interface CheckMethodOptions extends BaseImageCompareOptions, BaseMobileB /** * Ignore elements and or areas */ - ignore?: ElementIgnore[]; + ignore?: (ElementIgnore | ElementIgnore[])[]; } export interface InternalCheckMethodOptions extends InternalSaveMethodOptions { diff --git a/packages/image-comparison-core/src/commands/checkFullPageScreen.ts b/packages/image-comparison-core/src/commands/checkFullPageScreen.ts index 8e94d1596..91b7aeaa8 100644 --- a/packages/image-comparison-core/src/commands/checkFullPageScreen.ts +++ b/packages/image-comparison-core/src/commands/checkFullPageScreen.ts @@ -31,6 +31,7 @@ export default async function checkFullPageScreen( hideAfterFirstScroll = [], hideScrollBars, hideElements = [], + ignoreRegionPadding, removeElements = [], waitForFontsLoaded, } = checkFullPageOptions.method @@ -52,11 +53,13 @@ export default async function checkFullPageScreen( hideAfterFirstScroll, hideScrollBars, hideElements, + ignore: checkFullPageOptions.method.ignore, + ignoreRegionPadding, removeElements, waitForFontsLoaded, }, } - const { devicePixelRatio, fileName, base64Image } = await saveFullPageScreen({ + const { devicePixelRatio, fileName, base64Image, ignoreRegions } = await saveFullPageScreen({ browserInstance, folders, instanceData, @@ -73,6 +76,9 @@ export default async function checkFullPageScreen( methodCompareOptions: compareOptions, devicePixelRatio, fileName, + additionalProperties: { + ignoreRegions: ignoreRegions || [], + }, }) // 5. Now execute the compare and return the data diff --git a/packages/image-comparison-core/src/commands/checkWebElement.ts b/packages/image-comparison-core/src/commands/checkWebElement.ts index 957ec9a8a..e0d1ebb14 100644 --- a/packages/image-comparison-core/src/commands/checkWebElement.ts +++ b/packages/image-comparison-core/src/commands/checkWebElement.ts @@ -30,6 +30,7 @@ export default async function checkWebElement( enableLegacyScreenshotMethod, hideScrollBars, resizeDimensions, + ignoreRegionPadding, hideElements = [], removeElements = [], waitForFontsLoaded = false, @@ -45,17 +46,19 @@ export default async function checkWebElement( enableLegacyScreenshotMethod, hideScrollBars, resizeDimensions, + ignoreRegionPadding, hideElements, removeElements, waitForFontsLoaded, }, } - const { devicePixelRatio, fileName, base64Image } = await saveWebElement({ + const { devicePixelRatio, fileName, base64Image, ignoreRegions } = await saveWebElement({ browserInstance, instanceData, folders, element, tag, + ignore: checkElementOptions.method.ignore, saveElementOptions, }) @@ -68,6 +71,9 @@ export default async function checkWebElement( devicePixelRatio, fileName, isElementScreenshot: true, + additionalProperties: { + ignoreRegions: ignoreRegions || [], + }, }) // 4. Now execute the compare and return the data diff --git a/packages/image-comparison-core/src/commands/checkWebScreen.test.ts b/packages/image-comparison-core/src/commands/checkWebScreen.test.ts index 964181bce..d79ee5387 100644 --- a/packages/image-comparison-core/src/commands/checkWebScreen.test.ts +++ b/packages/image-comparison-core/src/commands/checkWebScreen.test.ts @@ -211,6 +211,64 @@ describe('checkWebScreen', () => { expect(executeImageCompareSpy.mock.calls[0]).toMatchSnapshot() }) + it('should pass ignore elements to saveWebScreen and forward resolved regions', async () => { + const { buildBaseExecuteCompareOptions } = await import('../helpers/utils.js') + const buildBaseExecuteCompareOptionsSpy = vi.mocked(buildBaseExecuteCompareOptions) + + const mockIgnoreElement = { elementId: 'ignore-el', selector: '.navbar' } as any + const mockIgnoreRegion = { x: 10, y: 20, width: 100, height: 50 } + const resolvedRegions = [ + { x: 50, y: 60, width: 200, height: 100 }, + { x: 10, y: 20, width: 100, height: 50 }, + ] + + // saveWebScreen returns ignoreRegions resolved during screenshot + saveWebScreenSpy.mockResolvedValueOnce({ + devicePixelRatio: 2, + fileName: 'test-screen.png', + ignoreRegions: resolvedRegions, + }) + + const options = { + ...baseOptions, + checkScreenOptions: { + ...baseOptions.checkScreenOptions, + method: { + ...baseOptions.checkScreenOptions.method, + ignore: [mockIgnoreElement, mockIgnoreRegion], + } + } + } + + await checkWebScreen(options) + + // ignore is passed through to saveWebScreen + expect(saveWebScreenSpy).toHaveBeenCalledWith( + expect.objectContaining({ + ignore: [mockIgnoreElement, mockIgnoreRegion], + }) + ) + // resolved regions are forwarded to the compare options + expect(buildBaseExecuteCompareOptionsSpy).toHaveBeenCalledWith( + expect.objectContaining({ + additionalProperties: { ignoreRegions: resolvedRegions }, + }) + ) + }) + + it('should pass empty array when no ignore regions are returned', async () => { + const { buildBaseExecuteCompareOptions } = await import('../helpers/utils.js') + const buildBaseExecuteCompareOptionsSpy = vi.mocked(buildBaseExecuteCompareOptions) + + await checkWebScreen(baseOptions) + + expect(buildBaseExecuteCompareOptionsSpy).toHaveBeenCalledWith( + expect.objectContaining({ + additionalProperties: { ignoreRegions: [] }, + }) + ) + }) + it('should handle all method options correctly', async () => { const mockHideElement = { elementId: 'hide-element', diff --git a/packages/image-comparison-core/src/commands/checkWebScreen.ts b/packages/image-comparison-core/src/commands/checkWebScreen.ts index 9e749ce37..28e785918 100644 --- a/packages/image-comparison-core/src/commands/checkWebScreen.ts +++ b/packages/image-comparison-core/src/commands/checkWebScreen.ts @@ -28,12 +28,15 @@ export default async function checkWebScreen( enableLayoutTesting, enableLegacyScreenshotMethod, hideScrollBars, + ignoreRegionPadding, hideElements = [], removeElements = [], waitForFontsLoaded, } = checkScreenOptions.method - // 2. Take the actual screenshot and retrieve the needed data + // 2. Take the actual screenshot and resolve ignore regions in one go. + // Ignore regions are resolved while the DOM is still in screenshot state + // (scrollbar hidden, elements hidden/removed) so positions match the image. const saveScreenOptions: SaveScreenOptions = { wic: checkScreenOptions.wic, method: { @@ -42,16 +45,18 @@ export default async function checkWebScreen( enableLayoutTesting, enableLegacyScreenshotMethod, hideScrollBars, + ignoreRegionPadding, hideElements, removeElements, waitForFontsLoaded, }, } - const { devicePixelRatio, fileName, base64Image } = await saveWebScreen({ + const { devicePixelRatio, fileName, base64Image, ignoreRegions } = await saveWebScreen({ browserInstance, instanceData, folders, tag, + ignore: checkScreenOptions.method.ignore, saveScreenOptions, isNativeContext, }) @@ -64,6 +69,9 @@ export default async function checkWebScreen( methodCompareOptions, devicePixelRatio, fileName, + additionalProperties: { + ignoreRegions: ignoreRegions || [], + }, }) // 4. Now execute the compare and return the data diff --git a/packages/image-comparison-core/src/commands/fullPage.interfaces.ts b/packages/image-comparison-core/src/commands/fullPage.interfaces.ts index f3b6cdd15..c00f80cc9 100644 --- a/packages/image-comparison-core/src/commands/fullPage.interfaces.ts +++ b/packages/image-comparison-core/src/commands/fullPage.interfaces.ts @@ -2,6 +2,7 @@ import type { BaseMobileWebScreenshotOptions, BaseWebScreenshotOptions, Folders import type { DefaultOptions } from '../helpers/options.interfaces.js' import type { ResizeDimensions } from '../methods/images.interfaces.js' import type { CheckMethodOptions } from './check.interfaces.js' +import type { ElementIgnore } from './element.interfaces.js' export interface SaveFullPageOptions { wic: DefaultOptions; @@ -9,6 +10,11 @@ export interface SaveFullPageOptions { } export interface SaveFullPageMethodOptions extends Partial, BaseWebScreenshotOptions, BaseMobileWebScreenshotOptions { + /** + * Elements or regions to ignore when saving/comparing full-page (desktop and mobile web). + * Same format as saveScreen / checkScreen (selectors or { x, y, width, height } in document CSS pixels). + */ + ignore?: (ElementIgnore | ElementIgnore[])[]; /** * The amount of milliseconds to wait for a new scroll. This will be used for the legacy * fullpage screenshot method. diff --git a/packages/image-comparison-core/src/commands/save.interfaces.ts b/packages/image-comparison-core/src/commands/save.interfaces.ts index f687a5a3a..860d10d54 100644 --- a/packages/image-comparison-core/src/commands/save.interfaces.ts +++ b/packages/image-comparison-core/src/commands/save.interfaces.ts @@ -1,7 +1,7 @@ import type { InstanceData } from '../methods/instanceData.interfaces.js' import type { SaveFullPageOptions } from './fullPage.interfaces.js' import type { SaveScreenOptions } from './screen.interfaces.js' -import type { SaveElementOptions, WicElement } from './element.interfaces.js' +import type { SaveElementOptions, WicElement, ElementIgnore } from './element.interfaces.js' import type { SaveTabbableOptions } from './tabbable.interfaces.js' import type { Folders } from '../base.interfaces.js' @@ -14,11 +14,13 @@ export interface InternalSaveMethodOptions { } export interface InternalSaveScreenMethodOptions extends InternalSaveMethodOptions { + ignore?: (ElementIgnore | ElementIgnore[])[]; saveScreenOptions: SaveScreenOptions, } export interface InternalSaveElementMethodOptions extends InternalSaveMethodOptions { element: HTMLElement | WicElement; + ignore?: (ElementIgnore | ElementIgnore[])[]; saveElementOptions: SaveElementOptions, } diff --git a/packages/image-comparison-core/src/commands/saveElement.ts b/packages/image-comparison-core/src/commands/saveElement.ts index 1b34a3756..da8dce7a3 100644 --- a/packages/image-comparison-core/src/commands/saveElement.ts +++ b/packages/image-comparison-core/src/commands/saveElement.ts @@ -13,11 +13,12 @@ export default async function saveElement( folders, instanceData, isNativeContext, + ignore, saveElementOptions, tag, }: InternalSaveElementMethodOptions ): Promise { return isNativeContext ? saveAppElement({ browserInstance, element, folders, instanceData, saveElementOptions, isNativeContext, tag }) - : saveWebElement({ browserInstance, element, folders, instanceData, saveElementOptions, tag }) + : saveWebElement({ browserInstance, element, folders, instanceData, saveElementOptions, tag, ignore }) } diff --git a/packages/image-comparison-core/src/commands/saveFullPageScreen.ts b/packages/image-comparison-core/src/commands/saveFullPageScreen.ts index f4fe8b395..1a1a94a53 100644 --- a/packages/image-comparison-core/src/commands/saveFullPageScreen.ts +++ b/packages/image-comparison-core/src/commands/saveFullPageScreen.ts @@ -8,6 +8,7 @@ import type { FullPageScreenshotDataOptions } from '../methods/screenshots.inter import type { InternalSaveFullPageMethodOptions } from './save.interfaces.js' import { getMethodOrWicOption, canUseBidiScreenshot } from '../helpers/utils.js' import { createBeforeScreenshotOptions, buildAfterScreenshotOptions } from '../helpers/options.js' +import { determineWebFullPageIgnoreRegions } from '../methods/rectangles.js' /** * Saves an image of the full page @@ -51,6 +52,7 @@ export default async function saveFullPageScreen( isAndroidChromeDriverScreenshot, isAndroidNativeWebScreenshot, isIOS, + isMobile, } = enrichedInstanceData // 4. Take the screenshot @@ -78,7 +80,26 @@ export default async function saveFullPageScreen( ? screenshotsData.data[0].screenshot // BiDi screenshot - use directly : await makeFullPageBase64Image(screenshotsData, { devicePixelRatio: devicePixelRatio || NaN, isLandscape }) - // 6. Return the data + // 6. Resolve ignore regions while the DOM is still in screenshot state. + // Full-page image (BiDi or stitched) is in document coordinates; regions are document-relative device pixels. + // On mobile scroll-and-stitch we crop addressBarShadowPadding from the top of each tile, so we pass + // fullPageCropTopPaddingCSS so ignore regions align with the stitched canvas. + const ignore = saveFullPageOptions.method?.ignore + const ignoreRegionPadding = (getMethodOrWicOption(saveFullPageOptions.method, saveFullPageOptions.wic, 'ignoreRegionPadding') as number | undefined) ?? 1 + const usedStitchedMobile = isMobile && !(screenshotsData.fullPageHeight === -1 && screenshotsData.fullPageWidth === -1) + const ignoreRegions = ignore && ignore.length > 0 + ? await determineWebFullPageIgnoreRegions( + { + browserInstance, + devicePixelRatio: devicePixelRatio || 1, + fullPageCropTopPaddingCSS: usedStitchedMobile ? beforeOptions.addressBarShadowPadding : 0, + ignoreRegionPadding, + }, + ignore, + ) + : undefined + + // 7. Return the data const afterOptions = buildAfterScreenshotOptions({ base64Image: fullPageBase64Image, folders, @@ -90,5 +111,10 @@ export default async function saveFullPageScreen( wicOptions: saveFullPageOptions.wic }) - return afterScreenshot(browserInstance, afterOptions!) + const result = await afterScreenshot(browserInstance, afterOptions!) + + return { + ...result, + ...(ignoreRegions ? { ignoreRegions } : {}), + } } diff --git a/packages/image-comparison-core/src/commands/saveWebElement.ts b/packages/image-comparison-core/src/commands/saveWebElement.ts index 150a1473d..61810cb5a 100644 --- a/packages/image-comparison-core/src/commands/saveWebElement.ts +++ b/packages/image-comparison-core/src/commands/saveWebElement.ts @@ -9,6 +9,7 @@ import type { ElementScreenshotDataOptions } from '../methods/screenshots.interf import { canUseBidiScreenshot, getMethodOrWicOption } from '../helpers/utils.js' import { createBeforeScreenshotOptions, buildAfterScreenshotOptions } from '../helpers/options.js' import type { InternalSaveElementMethodOptions } from './save.interfaces.js' +import { determineWebElementIgnoreRegions } from '../methods/rectangles.js' /** * Saves an image of an element @@ -20,6 +21,7 @@ export default async function saveWebElement( folders, element, tag, + ignore, saveElementOptions, }: InternalSaveElementMethodOptions ): Promise { @@ -72,6 +74,28 @@ export default async function saveWebElement( const shouldUseBidi = canUseBidiScreenshot(browserInstance) && !isMobile && !enableLegacyScreenshotMethod const screenshotData = await takeElementScreenshot(browserInstance, elementScreenshotOptions, shouldUseBidi) + // 3b. Resolve ignore regions (element-local) while the DOM is still in screenshot state. + // determineWebElementIgnoreRegions returns device-pixel regions, or CSS-pixel regions when + // the element image is from the native driver on Android native web (see that function). + const ignoreRegionPadding = (getMethodOrWicOption(saveElementOptions.method, saveElementOptions.wic, 'ignoreRegionPadding') as number | undefined) ?? 1 + const ignoreRegions = ignore && ignore.length > 0 + ? await (async () => { + const rootElement = await (element as any as WebdriverIO.Element | Promise) + + return determineWebElementIgnoreRegions( + { + browserInstance, + devicePixelRatio: devicePixelRatio || 1, + rootElement: rootElement as WebdriverIO.Element, + ignoreRegionPadding, + isAndroidNativeWebScreenshot, + isWebDriverElementScreenshot: screenshotData.isWebDriverElementScreenshot, + }, + ignore, + ) + })() + : undefined + // 4. Return the data const afterOptions = buildAfterScreenshotOptions({ base64Image: screenshotData.base64Image, @@ -84,5 +108,10 @@ export default async function saveWebElement( wicOptions: saveElementOptions.wic }) - return afterScreenshot(browserInstance, afterOptions) + const result = await afterScreenshot(browserInstance, afterOptions) + + return { + ...result, + ...(ignoreRegions ? { ignoreRegions } : {}), + } } diff --git a/packages/image-comparison-core/src/commands/saveWebScreen.ts b/packages/image-comparison-core/src/commands/saveWebScreen.ts index eb41ae723..640ec6b18 100644 --- a/packages/image-comparison-core/src/commands/saveWebScreen.ts +++ b/packages/image-comparison-core/src/commands/saveWebScreen.ts @@ -7,6 +7,7 @@ import type { InternalSaveScreenMethodOptions } from './save.interfaces.js' import type { WebScreenshotDataOptions } from '../methods/screenshots.interfaces.js' import { canUseBidiScreenshot, getMethodOrWicOption } from '../helpers/utils.js' import { createBeforeScreenshotOptions, buildAfterScreenshotOptions } from '../helpers/options.js' +import { determineWebScreenIgnoreRegions } from '../methods/rectangles.js' /** * Saves an image of the viewport of the screen @@ -17,6 +18,7 @@ export default async function saveWebScreen( instanceData, folders, tag, + ignore, saveScreenOptions, isNativeContext = false, }: InternalSaveScreenMethodOptions @@ -67,7 +69,26 @@ export default async function saveWebScreen( } const { base64Image } = await takeWebScreenshot(browserInstance, webScreenshotOptions, shouldUseBidi) - // 4. Return the data + // 4. Resolve ignore regions while the DOM is still in screenshot state + // (scrollbar hidden, elements hidden/removed, CSS applied). + // This must happen BEFORE afterScreenshot restores the DOM. + const ignoreRegionPadding = (getMethodOrWicOption(saveScreenOptions.method, saveScreenOptions.wic, 'ignoreRegionPadding') as number | undefined) ?? 1 + const ignoreRegions = ignore && ignore.length > 0 + ? await determineWebScreenIgnoreRegions( + { + browserInstance, + devicePixelRatio: devicePixelRatio || 1, + deviceRectangles: instanceData.deviceRectangles, + isAndroid, + isAndroidNativeWebScreenshot, + isIOS, + ignoreRegionPadding, + }, + ignore, + ) + : undefined + + // 5. Restore the DOM and return the data const afterOptions = buildAfterScreenshotOptions({ base64Image, folders, @@ -79,5 +100,10 @@ export default async function saveWebScreen( wicOptions: saveScreenOptions.wic }) - return afterScreenshot(browserInstance, afterOptions) + const result = await afterScreenshot(browserInstance, afterOptions) + + return { + ...result, + ...(ignoreRegions ? { ignoreRegions } : {}), + } } diff --git a/packages/image-comparison-core/src/helpers/__snapshots__/options.test.ts.snap b/packages/image-comparison-core/src/helpers/__snapshots__/options.test.ts.snap index 9dd979fcb..c23771b92 100644 --- a/packages/image-comparison-core/src/helpers/__snapshots__/options.test.ts.snap +++ b/packages/image-comparison-core/src/helpers/__snapshots__/options.test.ts.snap @@ -106,6 +106,7 @@ exports[`options > defaultOptions > should return the default options when no op "formatImageName": "{tag}-{browserName}-{width}x{height}-dpr-{dpr}", "fullPageScrollTimeout": 1500, "hideScrollBars": true, + "ignoreRegionPadding": 1, "isHybridApp": false, "savePerInstance": false, "tabbableOptions": { @@ -161,6 +162,7 @@ exports[`options > defaultOptions > should return the provided options when opti "formatImageName": "{foo}-{bar}", "fullPageScrollTimeout": 12345, "hideScrollBars": true, + "ignoreRegionPadding": 1, "isHybridApp": false, "savePerInstance": true, "tabbableOptions": { diff --git a/packages/image-comparison-core/src/helpers/afterScreenshot.interfaces.ts b/packages/image-comparison-core/src/helpers/afterScreenshot.interfaces.ts index 9c3a309fb..5c54909a7 100644 --- a/packages/image-comparison-core/src/helpers/afterScreenshot.interfaces.ts +++ b/packages/image-comparison-core/src/helpers/afterScreenshot.interfaces.ts @@ -1,8 +1,12 @@ +import type { RectanglesOutput } from '../methods/rectangles.interfaces.js' + export interface ScreenshotOutput { // The device pixel ratio of the instance devicePixelRatio: number; // The filename fileName: string; + // Resolved ignore regions in device pixels (when ignore elements were provided) + ignoreRegions?: RectanglesOutput[]; // Is Landscape isLandscape: boolean; // The path where the file can be found diff --git a/packages/image-comparison-core/src/helpers/options.interfaces.ts b/packages/image-comparison-core/src/helpers/options.interfaces.ts index 19b1211b8..6d4394001 100644 --- a/packages/image-comparison-core/src/helpers/options.interfaces.ts +++ b/packages/image-comparison-core/src/helpers/options.interfaces.ts @@ -118,6 +118,12 @@ export interface ClassOptions { */ hideScrollBars?: boolean; + /** + * Padding in device pixels added to each side of element ignore regions (default 1). + * Set to 0 to disable. Only applies to element screenshots. + */ + ignoreRegionPadding?: number; + // ================ // Compare options // ================ @@ -356,6 +362,12 @@ export interface DefaultOptions { */ hideScrollBars: boolean; + /** + * Padding in device pixels added to each side of element ignore regions (default 1). + * Set to 0 to disable. Only applies to element screenshots. + */ + ignoreRegionPadding?: number; + /** * Indicates whether the app is a hybrid (native + webview). */ diff --git a/packages/image-comparison-core/src/helpers/options.ts b/packages/image-comparison-core/src/helpers/options.ts index 61dce5289..bbd5209d7 100644 --- a/packages/image-comparison-core/src/helpers/options.ts +++ b/packages/image-comparison-core/src/helpers/options.ts @@ -54,6 +54,7 @@ export function defaultOptions(options: ClassOptions): DefaultOptions { // Default to false for storybook mode as element screenshots use W3C protocol without scrollbars // This also saves an extra webdriver call hideScrollBars: getBooleanOption(options, 'hideScrollBars', !isStorybookMode), + ignoreRegionPadding: options.ignoreRegionPadding ?? 1, waitForFontsLoaded: options.waitForFontsLoaded ?? true, alwaysSaveActualImage: options.alwaysSaveActualImage ?? true, diff --git a/packages/image-comparison-core/src/helpers/utils.interfaces.ts b/packages/image-comparison-core/src/helpers/utils.interfaces.ts index ae39dd648..bab94d1fc 100644 --- a/packages/image-comparison-core/src/helpers/utils.interfaces.ts +++ b/packages/image-comparison-core/src/helpers/utils.interfaces.ts @@ -169,6 +169,7 @@ export interface CommonCheckVariables { /** Optional instance data (not all methods need these) */ platformName?: string; + platformVersion?: string; isIOS?: boolean; /** WIC options */ @@ -235,6 +236,8 @@ export interface BaseExecuteCompareOptions { isAndroidNativeWebScreenshot: boolean; /** Optional: platform name */ platformName?: string; + /** Optional: platform version (e.g. "14.0" for Android API 14, "17.0" for iOS) */ + platformVersion?: string; /** Optional: whether this is iOS */ isIOS?: boolean; /** Optional: whether this is hybrid app */ diff --git a/packages/image-comparison-core/src/helpers/utils.test.ts b/packages/image-comparison-core/src/helpers/utils.test.ts index 3bb9e146d..0f6d74023 100644 --- a/packages/image-comparison-core/src/helpers/utils.test.ts +++ b/packages/image-comparison-core/src/helpers/utils.test.ts @@ -679,6 +679,7 @@ describe('utils', () => { .mockResolvedValueOnce(undefined) // injectWebviewOverlay .mockResolvedValueOnce(undefined) // executeNativeClick .mockResolvedValueOnce({ x: 150, y: 300, width: 100, height: 100 }) // getMobileWebviewClickAndDimensions + .mockResolvedValueOnce({ vs: 'visible', focus: true }) // visibilityState debug check const result = await getMobileViewPortPosition({ browserInstance: mockBrowserInstance, @@ -691,6 +692,149 @@ describe('utils', () => { expect(result).toMatchSnapshot() }) + it('should handle rounded overlay values from non-integer DPR', async () => { + const dpr = 2.625 + const screenW = 1080 + const screenH = 2424 + + const cssClickX = 206 + const cssClickY = 385 + const cssWidth = 412 + const cssHeight = 363 + + const overlayWidth = Math.round(cssWidth * dpr) + const overlayHeight = Math.round(cssHeight * dpr) + + vi.mocked(mockBrowserInstance.execute) + .mockResolvedValueOnce(undefined) // loadBase64Html + .mockResolvedValueOnce(undefined) // checkMetaTag (iOS) + .mockResolvedValueOnce(undefined) // injectWebviewOverlay + .mockResolvedValueOnce(undefined) // executeNativeClick + .mockResolvedValueOnce({ + x: Math.round(cssClickX * dpr), + y: Math.round(cssClickY * dpr), + width: overlayWidth, + height: overlayHeight, + }) // getMobileWebviewClickAndDimensions (rounded integers from overlay) + .mockResolvedValueOnce({ vs: 'visible', focus: true }) // visibilityState debug check + + const result = await getMobileViewPortPosition({ + browserInstance: mockBrowserInstance, + ...baseOptions, + screenHeight: screenH, + screenWidth: screenW, + }) + + const viewportTop = Math.max(0, Math.round(screenH / 2 - Math.round(cssClickY * dpr))) + const viewportLeft = Math.max(0, Math.round(screenW / 2 - Math.round(cssClickX * dpr))) + + expect(result.viewport.y).toBe(viewportTop) + expect(result.viewport.x).toBe(viewportLeft) + expect(result.viewport.width).toBe(overlayWidth) + expect(result.viewport.height).toBe(overlayHeight) + expect(result.statusBarAndAddressBar.height).toBe(Math.max(0, Math.round(viewportTop))) + }) + + it('should retry and succeed on Android when overlay returns zeros (Start Surface blocking)', async () => { + const warnSpy = vi.spyOn(log, 'warn').mockImplementation(() => {}) + + vi.mocked(mockBrowserInstance.execute) + // --- Attempt 1: overlay returns zeros (Start Surface is blocking) --- + .mockResolvedValueOnce(undefined) // loadBase64Html (blob) + .mockResolvedValueOnce(undefined) // injectWebviewOverlay + .mockResolvedValueOnce(undefined) // executeNativeClick (screen center) + .mockResolvedValueOnce({ x: 0, y: 0, width: 0, height: 0 }) // getMobileWebviewClickAndDimensions + // --- Dismiss Start Surface: Back button --- + .mockResolvedValueOnce(undefined) // mobile: pressKey (Back button) + // --- Attempt 2: overlay returns valid data --- + .mockResolvedValueOnce(undefined) // loadBase64Html (blob) + .mockResolvedValueOnce(undefined) // injectWebviewOverlay + .mockResolvedValueOnce(undefined) // executeNativeClick (screen center) + .mockResolvedValueOnce({ x: 150, y: 300, width: 100, height: 100 }) // getMobileWebviewClickAndDimensions + + const result = await getMobileViewPortPosition({ + browserInstance: mockBrowserInstance, + ...baseOptions, + isAndroid: true, + isIOS: false, + }) + + expect(warnSpy).toHaveBeenCalledWith( + expect.stringContaining('overlay did not receive the native click') + ) + expect(mockBrowserInstance.url).toHaveBeenCalledWith('http://example.com') + expect(result.viewport.width).toBe(100) + expect(result.viewport.height).toBe(100) + + warnSpy.mockRestore() + }) + + it('should return initialDeviceRectangles after all retries are exhausted on Android', { timeout: 15000 }, async () => { + const warnSpy = vi.spyOn(log, 'warn').mockImplementation(() => {}) + const errorSpy = vi.spyOn(log, 'error').mockImplementation(() => {}) + + const zeroOverlay = { x: 0, y: 0, width: 0, height: 0 } + const attemptMocks = () => [ + undefined, // loadBase64Html + undefined, // injectWebviewOverlay + undefined, // executeNativeClick (center) + zeroOverlay, // getMobileWebviewClickAndDimensions + ] + const mocked = vi.mocked(mockBrowserInstance.execute) + // 5 attempts: attempts 1-4 have Back button dismissal, last attempt has none + for (let i = 0; i < 5; i++) { + for (const val of attemptMocks()) { mocked.mockResolvedValueOnce(val) } + if (i < 4) { + mocked.mockResolvedValueOnce(undefined) // mobile: pressKey (Back button) + } + } + + const result = await getMobileViewPortPosition({ + browserInstance: mockBrowserInstance, + ...baseOptions, + isAndroid: true, + isIOS: false, + }) + + expect(warnSpy).toHaveBeenCalledTimes(5) + expect(errorSpy).toHaveBeenCalledWith( + expect.stringContaining('Viewport measurement failed after 5 attempts') + ) + expect(mockBrowserInstance.url).toHaveBeenCalledWith('http://example.com') + expect(result).toEqual(DEVICE_RECTANGLES) + + warnSpy.mockRestore() + errorSpy.mockRestore() + }) + + it('should not retry on iOS when overlay returns zeros', async () => { + const warnSpy = vi.spyOn(log, 'warn').mockImplementation(() => {}) + + vi.mocked(mockBrowserInstance.execute) + .mockResolvedValueOnce(undefined) // loadBase64Html (blob) + .mockResolvedValueOnce(undefined) // checkMetaTag (iOS) + .mockResolvedValueOnce(undefined) // injectWebviewOverlay + .mockResolvedValueOnce(undefined) // executeNativeClick (center, 'mobile: tap') + .mockResolvedValueOnce({ x: 0, y: 0, width: 0, height: 0 }) // getMobileWebviewClickAndDimensions + .mockResolvedValueOnce({ vs: 'visible', focus: true }) // visibilityState debug check + + const result = await getMobileViewPortPosition({ + browserInstance: mockBrowserInstance, + ...baseOptions, + isAndroid: false, + isIOS: true, + }) + + // iOS uses a single attempt — no retry, no Start Surface dismissal + expect(warnSpy).not.toHaveBeenCalled() + expect(mockBrowserInstance.url).toHaveBeenCalledWith('http://example.com') + // The zero overlay values produce a viewport at screen center with 0 dimensions + expect(result.viewport.width).toBe(0) + expect(result.viewport.height).toBe(0) + + warnSpy.mockRestore() + }) + it('should return initialDeviceRectangles if not WebView (native context)', async () => { const result = await getMobileViewPortPosition({ browserInstance: mockBrowserInstance, diff --git a/packages/image-comparison-core/src/helpers/utils.ts b/packages/image-comparison-core/src/helpers/utils.ts index 393989518..cf6edca09 100644 --- a/packages/image-comparison-core/src/helpers/utils.ts +++ b/packages/image-comparison-core/src/helpers/utils.ts @@ -125,7 +125,7 @@ export function checkTestInMobileBrowser(isMobile: boolean, browserName: string) * Checks if this is a native webscreenshot on android */ export function checkAndroidNativeWebScreenshot(isAndroid: boolean, nativeWebscreenshot: boolean): boolean { - return (isAndroid && nativeWebscreenshot) || false + return (isAndroid && !!nativeWebscreenshot) || false } /** @@ -491,6 +491,17 @@ export async function executeNativeClick({ browserInstance, isIOS, x, y }: Execu } } +/** + * The maximum number of times we attempt to measure the viewport position on Android. + * Chrome may start in the "Start Surface" (tab thumbnail overview) which blocks + * native clicks from reaching the webview overlay. Each retry dismisses the Start + * Surface via a combination of native taps and WebDriver URL navigation, then + * re-attempts the measurement. + * + * iOS does not suffer from this issue, so only a single attempt is made there. + */ +const MAX_ANDROID_VIEWPORT_MEASUREMENT_RETRIES = 5 + /** * Get the mobile viewport position, we determine this by: * 1. Loading a base64 HTML page @@ -499,6 +510,12 @@ export async function executeNativeClick({ browserInstance, isIOS, x, y }: Execu * 4. Getting the data from the overlay and removing it * 5. Calculating the position of the viewport based on the click position of the native click vs the overlay * 6. Returning the calculated values + * + * On Android only: when the overlay reports zero dimensions (width=0, height=0) + * the native click did not reach the webview. This typically means Chrome's Start + * Surface or tab overview is blocking it. The function will retry up to + * MAX_ANDROID_VIEWPORT_MEASUREMENT_RETRIES times, tapping the tab thumbnail area + * between attempts to dismiss the blocking UI. */ export async function getMobileViewPortPosition({ browserInstance, @@ -512,44 +529,106 @@ export async function getMobileViewPortPosition({ }: GetMobileViewPortPositionOptions): Promise { if (!isNativeContext && (isIOS || (isAndroid && nativeWebScreenshot))) { const currentUrl = await browserInstance.getUrl() - // 1. Load a base64 HTML page - await loadBase64Html({ browserInstance, isIOS }) - // 2. Inject an overlay on top of the webview with an event listener that stores the click position in the webview - await browserInstance.execute(injectWebviewOverlay, isAndroid) - // 3. Click on the overlay in the center of the screen with a native click const nativeClickX = screenWidth / 2 const nativeClickY = screenHeight / 2 - await executeNativeClick({ browserInstance, isIOS, x: nativeClickX, y: nativeClickY }) - // We need to wait a bit here, otherwise the click is not registered - await waitFor(100) - // 4a. Get the data from the overlay and remove it - const { y, x, width, height } = await browserInstance.execute(getMobileWebviewClickAndDimensions, '[data-test="ics-overlay"]') - // 4.b reset the url - await browserInstance.url(currentUrl) - // 5. Calculate the position of the viewport based on the click position of the native click vs the overlay - const viewportTop = Math.max(0, Math.round(nativeClickY - y)) - const viewportLeft = Math.max(0, Math.round(nativeClickX - x)) - const statusBarAndAddressBarHeight = Math.max(0, Math.round(viewportTop)) - const bottomBarHeight = Math.max(0, Math.round(screenHeight - (viewportTop + height))) - const leftSidePaddingWidth = Math.max(0, Math.round(viewportLeft)) - const rightSidePaddingWidth = Math.max(0, Math.round(screenWidth - (viewportLeft + width))) - const deviceRectangles = { - ...initialDeviceRectangles, - bottomBar: { y: viewportTop + height, x: 0, width: screenWidth, height: bottomBarHeight }, - leftSidePadding: { y: viewportTop, x: 0, width: leftSidePaddingWidth, height: height }, - rightSidePadding: { y: viewportTop, x: viewportLeft + width, width: rightSidePaddingWidth, height: height }, - screenSize: { height: screenHeight, width: screenWidth }, - statusBarAndAddressBar: { y: 0, x: 0, width: screenWidth, height: statusBarAndAddressBarHeight }, - viewport: { y: viewportTop, x: viewportLeft, width: width, height: height }, + const maxAttempts = isAndroid ? MAX_ANDROID_VIEWPORT_MEASUREMENT_RETRIES : 1 + + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + // 1. Load a base64 HTML page + await loadBase64Html({ browserInstance, isIOS }) + // 2. Inject an overlay on top of the webview with an event listener that stores the click position in the webview + await browserInstance.execute(injectWebviewOverlay, isAndroid) + // 3. Click on the overlay in the center of the screen with a native click + await executeNativeClick({ browserInstance, isIOS, x: nativeClickX, y: nativeClickY }) + // We need to wait a bit here, otherwise the click is not registered + await waitFor(100) + // 4a. Get the data from the overlay and remove it + const { y, x, width, height } = await browserInstance.execute(getMobileWebviewClickAndDimensions, '[data-test="ics-overlay"]') + + // 4b. On Android, validate the overlay data. + // NOTE: for future detection of Chrome's Start Surface, `document.visibilityState` + // and `document.hasFocus()` can be used as a more direct signal. When the Start + // Surface is active the webview reports visibility: "hidden" and hasFocus: false. + // When width and height are both 0 the native click never reached the overlay, + // which typically means Chrome's Start Surface or tab overview is blocking the webview. + if (isAndroid && width === 0 && height === 0) { + log.warn( + `Viewport measurement attempt ${attempt}/${maxAttempts}: ` + + 'overlay did not receive the native click. ' + + 'Chrome may be showing its Start Surface or tab overview.' + ) + + if (attempt < maxAttempts) { + await dismissAndroidStartSurface({ browserInstance }) + } + + continue + } + + // 4c. Reset the url + await browserInstance.url(currentUrl) + // 5. Calculate the position of the viewport based on the click position of the native click vs the overlay + const viewportTop = Math.max(0, Math.round(nativeClickY - y)) + const viewportLeft = Math.max(0, Math.round(nativeClickX - x)) + const statusBarAndAddressBarHeight = Math.max(0, Math.round(viewportTop)) + const bottomBarHeight = Math.max(0, Math.round(screenHeight - (viewportTop + height))) + const leftSidePaddingWidth = Math.max(0, Math.round(viewportLeft)) + const rightSidePaddingWidth = Math.max(0, Math.round(screenWidth - (viewportLeft + width))) + const deviceRectangles = { + ...initialDeviceRectangles, + bottomBar: { y: viewportTop + height, x: 0, width: screenWidth, height: bottomBarHeight }, + leftSidePadding: { y: viewportTop, x: 0, width: leftSidePaddingWidth, height: height }, + rightSidePadding: { y: viewportTop, x: viewportLeft + width, width: rightSidePaddingWidth, height: height }, + screenSize: { height: screenHeight, width: screenWidth }, + statusBarAndAddressBar: { y: 0, x: 0, width: screenWidth, height: statusBarAndAddressBarHeight }, + viewport: { y: viewportTop, x: viewportLeft, width: width, height: height }, + } + + return deviceRectangles } - return deviceRectangles + // All Android retries exhausted — reset the URL and fall through to initialDeviceRectangles + log.error( + `Viewport measurement failed after ${maxAttempts} attempts. ` + + 'Chrome appears stuck in Start Surface or tab overview mode. ' + + 'Returning initial device rectangles; screenshots may have incorrect dimensions.' + ) + await browserInstance.url(currentUrl) } // No WebView detected, return empty values return initialDeviceRectangles } +/** + * Attempt to dismiss Chrome's "Start Surface" (tab thumbnail overview) on Android + * by pressing the Android Back button (KEYCODE_BACK = 4). + * + * The Back button is preferred over tapping a tab thumbnail because: + * - It reliably exits the tab overview regardless of orientation or device + * - It doesn't risk accidentally closing a tab by hitting the "X" button + * + * // --- Commented-out tap approach kept for reference --- + * // const isLandscape = screenWidth > screenHeight + * // const pct = isLandscape ? 0.30 : 0.15 + * // const tapX = Math.round(screenWidth * pct) + * // const tapY = Math.round(screenHeight * pct) + * // console.log(`[VIEWPORT-DEBUG] dismissStartSurface: tapping (${tapX}, ${tapY}) on ${screenWidth}x${screenHeight} (${isLandscape ? 'landscape' : 'portrait'})`) + * // await executeNativeClick({ browserInstance, isIOS: false, x: tapX, y: tapY }) + */ +async function dismissAndroidStartSurface({ + browserInstance, +}: { + browserInstance: WebdriverIO.Browser +}): Promise { + try { + await browserInstance.execute('mobile: pressKey', { keycode: 4 }) + await waitFor(1500) + } catch (error) { + log.warn('Failed to dismiss Chrome Start Surface via Back button', error) + } +} + /** * Get the value of a method or the default value */ @@ -617,6 +696,7 @@ export function extractCommonCheckVariables( // Optional instance data ...(instanceData.platformName && { platformName: instanceData.platformName }), + ...(instanceData.platformVersion && { platformVersion: instanceData.platformVersion }), ...(instanceData.isIOS !== undefined && { isIOS: instanceData.isIOS }), // WIC options @@ -687,6 +767,7 @@ export function buildBaseExecuteCompareOptions( isAndroidNativeWebScreenshot: commonCheckVariables.isAndroidNativeWebScreenshot, // Add optional properties from commonCheckVariables if they exist ...(commonCheckVariables.platformName && { platformName: commonCheckVariables.platformName }), + ...(commonCheckVariables.platformVersion && { platformVersion: commonCheckVariables.platformVersion }), ...(commonCheckVariables.isIOS !== undefined && { isIOS: commonCheckVariables.isIOS }), ...(commonCheckVariables.isHybridApp !== undefined && { isHybridApp: commonCheckVariables.isHybridApp }), } diff --git a/packages/image-comparison-core/src/index.ts b/packages/image-comparison-core/src/index.ts index a328810c3..67b9f8e7e 100644 --- a/packages/image-comparison-core/src/index.ts +++ b/packages/image-comparison-core/src/index.ts @@ -20,6 +20,7 @@ export type { SaveScreenMethodOptions, } from './commands/screen.interfaces.js' export type { + ElementIgnore, WicElement, CheckElementMethodOptions, SaveElementMethodOptions, diff --git a/packages/image-comparison-core/src/methods/__snapshots__/rectangles.test.ts.snap b/packages/image-comparison-core/src/methods/__snapshots__/rectangles.test.ts.snap index 296e6d8b9..021efe1fc 100644 --- a/packages/image-comparison-core/src/methods/__snapshots__/rectangles.test.ts.snap +++ b/packages/image-comparison-core/src/methods/__snapshots__/rectangles.test.ts.snap @@ -273,18 +273,18 @@ exports[`rectangles > prepareIgnoreRectangles > should combine all rectangle sou "right": 220, "top": 40, }, - { - "bottom": 750, - "left": 400, - "right": 700, - "top": 600, - }, { "bottom": 640, "left": 0, "right": 2688, "top": 0, }, + { + "bottom": 375, + "left": 200, + "right": 350, + "top": 300, + }, ] `; @@ -325,10 +325,10 @@ exports[`rectangles > prepareIgnoreRectangles > should handle blockOut and ignor "top": 40, }, { - "bottom": 750, - "left": 400, - "right": 700, - "top": 600, + "bottom": 375, + "left": 200, + "right": 350, + "top": 300, }, ] `; diff --git a/packages/image-comparison-core/src/methods/__snapshots__/takeElementScreenshots.test.ts.snap b/packages/image-comparison-core/src/methods/__snapshots__/takeElementScreenshots.test.ts.snap index 2f1110f0f..9d98cabdb 100644 --- a/packages/image-comparison-core/src/methods/__snapshots__/takeElementScreenshots.test.ts.snap +++ b/packages/image-comparison-core/src/methods/__snapshots__/takeElementScreenshots.test.ts.snap @@ -1,5 +1,22 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html +exports[`takeElementScreenshot > BiDi screenshots > should scroll element into view when autoElementScroll is enabled 1`] = ` +[ + [MockFunction spy], + { + "elementId": "test-element", + }, + 6, +] +`; + +exports[`takeElementScreenshot > BiDi screenshots > should scroll element into view when autoElementScroll is enabled 2`] = ` +[ + [MockFunction spy], + 100, +] +`; + exports[`takeElementScreenshot > Edge cases > should handle devicePixelRatio values and fallback to NaN when falsy 1`] = ` { "base64Image": "cropped-screenshot-data", diff --git a/packages/image-comparison-core/src/methods/images.interfaces.ts b/packages/image-comparison-core/src/methods/images.interfaces.ts index b8156e278..2e6e6f35c 100644 --- a/packages/image-comparison-core/src/methods/images.interfaces.ts +++ b/packages/image-comparison-core/src/methods/images.interfaces.ts @@ -47,6 +47,10 @@ export interface ImageCompareOptions { isAndroid: boolean; /** If this is a native web screenshot */ isAndroidNativeWebScreenshot: boolean; + /** If this is a hybrid (native + webview) app; enables status bar fallback in webview when overlay reports zero */ + isHybridApp?: boolean; + /** Platform version from the device (e.g. "14.0" for Android API 14); used for Android status bar fallback lookup */ + platformVersion?: string; } export interface WicImageCompareOptions extends BaseImageCompareOptions, BaseMobileBlockOutOptions { diff --git a/packages/image-comparison-core/src/methods/images.ts b/packages/image-comparison-core/src/methods/images.ts index d5635ed78..34ca9b16d 100644 --- a/packages/image-comparison-core/src/methods/images.ts +++ b/packages/image-comparison-core/src/methods/images.ts @@ -345,9 +345,19 @@ export async function executeImageCompare( isAndroidNativeWebScreenshot, isAndroid, fileName, + folderOptions, } = options - const { actualFolder, autoSaveBaseline, alwaysSaveActualImage, baselineFolder, browserName, deviceName, diffFolder, isMobile, savePerInstance } = - options.folderOptions + const { + actualFolder, + autoSaveBaseline, + alwaysSaveActualImage, + baselineFolder, + browserName, + deviceName, + diffFolder, + isMobile, + savePerInstance, + } = folderOptions const imageCompareOptions = { ...options.compareOptions.wic, ...options.compareOptions.method } // 1a. Disable JSON reports if alwaysSaveActualImage is false (JSON reports need the actual file to exist) @@ -424,6 +434,8 @@ export async function executeImageCompare( blockOutStatusBar: imageCompareOptions.blockOutStatusBar, blockOutToolBar: imageCompareOptions.blockOutToolBar, }, + isHybridApp: options.isHybridApp, + platformVersion: options.platformVersion, actualFilePath: isViewPortScreenshot ? undefined : actualFilePath, }) diff --git a/packages/image-comparison-core/src/methods/rectangles.interfaces.ts b/packages/image-comparison-core/src/methods/rectangles.interfaces.ts index bdd7a3199..0c755be49 100644 --- a/packages/image-comparison-core/src/methods/rectangles.interfaces.ts +++ b/packages/image-comparison-core/src/methods/rectangles.interfaces.ts @@ -128,6 +128,10 @@ export interface PrepareIgnoreRectanglesOptions { blockOutStatusBar?: boolean; blockOutToolBar?: boolean; }; + /** Whether this is a hybrid (native + webview) app; enables status bar fallback when overlay reports zero */ + isHybridApp?: boolean; + /** Platform version from the device (e.g. "14.0"); used for Android API level lookup in fallback */ + platformVersion?: string; actualFilePath?: string; } @@ -138,6 +142,63 @@ export interface PreparedIgnoreRectangles { hasIgnoreRectangles: boolean; } +export interface DetermineWebScreenIgnoreRegionsOptions { + /** The browser instance */ + browserInstance: WebdriverIO.Browser; + /** The device pixel ratio */ + devicePixelRatio: number; + /** The device rectangles (contains viewport offset for mobile) */ + deviceRectangles: DeviceRectangles; + /** Whether this is an Android device */ + isAndroid: boolean; + /** Whether this is an Android native web screenshot */ + isAndroidNativeWebScreenshot: boolean; + /** Whether this is an iOS device */ + isIOS: boolean; + /** Padding in device pixels added to each side of computed ignore regions (caller defaults to 1). */ + ignoreRegionPadding: number; +} + +/** + * Options for full-page web ignore regions (desktop and mobile). + * Full-page image is in document coordinates; on mobile scroll-and-stitch the canvas + * crops off the top addressBarShadowPadding (CSS px), so we subtract that from y. + */ +export interface DetermineWebFullPageIgnoreRegionsOptions { + /** The browser instance */ + browserInstance: WebdriverIO.Browser; + /** The device pixel ratio */ + devicePixelRatio: number; + /** Padding in device pixels added to each side of computed ignore regions (caller defaults to 1). */ + ignoreRegionPadding: number; + /** + * Top crop offset in CSS pixels (e.g. addressBarShadowPadding on mobile full-page). + * When set, canvas y = (documentY - fullPageCropTopPaddingCSS) × DPR so ignore regions + * align with the stitched image which crops this much from the top of each tile. + * @default 0 + */ + fullPageCropTopPaddingCSS?: number; +} + +export interface DetermineWebElementIgnoreRegionsOptions { + /** The browser instance */ + browserInstance: WebdriverIO.Browser; + /** The device pixel ratio */ + devicePixelRatio: number; + /** The root element being captured in the element screenshot */ + rootElement: WebdriverIO.Element; + /** Padding in device pixels added to each side of computed ignore regions (caller defaults to 1). */ + ignoreRegionPadding: number; + /** + * When both this and isWebDriverElementScreenshot are true, the element image is at CSS pixel size + * (native driver returns a downscaled image). We then output ignore regions in CSS pixel coordinates + * by dividing by DPR; otherwise regions are in device pixels. + */ + isAndroidNativeWebScreenshot?: boolean; + /** When true, the element screenshot came from the native driver (no fallback crop). */ + isWebDriverElementScreenshot?: boolean; +} + export interface BoundingBox extends BaseBoundingBox { } export interface IgnoreBoxes extends BoundingBox { } diff --git a/packages/image-comparison-core/src/methods/rectangles.test.ts b/packages/image-comparison-core/src/methods/rectangles.test.ts index 24864da9f..0d19300b5 100644 --- a/packages/image-comparison-core/src/methods/rectangles.test.ts +++ b/packages/image-comparison-core/src/methods/rectangles.test.ts @@ -5,6 +5,9 @@ import { determineScreenRectangles, determineStatusAddressToolBarRectangles, determineIgnoreRegions, + determineWebFullPageIgnoreRegions, + determineWebScreenIgnoreRegions, + determineWebElementIgnoreRegions, splitIgnores, determineDeviceBlockOuts, prepareIgnoreRectangles @@ -31,7 +34,9 @@ describe('rectangles', () => { mockBrowserInstance = { execute: mockExecute, - getElementRect: mockGetElementRect + getElementRect: mockGetElementRect, + $: vi.fn(), + $$: vi.fn(), } as unknown as WebdriverIO.Browser }) @@ -757,6 +762,438 @@ describe('rectangles', () => { }) }) + describe('determineWebScreenIgnoreRegions', () => { + const desktopOptions = { + browserInstance: null as unknown as WebdriverIO.Browser, + devicePixelRatio: 2, + deviceRectangles: baseDeviceRectangles, + isAndroid: false, + isAndroidNativeWebScreenshot: false, + isIOS: false, + ignoreRegionPadding: 0, + } + + beforeEach(() => { + desktopOptions.browserInstance = mockBrowserInstance + }) + + it('should resolve elements via raw BCR on desktop and apply DPR', async () => { + const mockElement = { elementId: 'el1', selector: '.nav' } as WebdriverIO.Element + const freshElement = { elementId: 'el1-fresh', selector: '.nav' } as unknown as WebdriverIO.Element + vi.mocked(mockBrowserInstance.$$).mockResolvedValueOnce([freshElement] as any) + mockExecute.mockResolvedValueOnce({ x: 10, y: 20, width: 200, height: 50 }) + + const result = await determineWebScreenIgnoreRegions(desktopOptions, [mockElement]) + + expect(mockBrowserInstance.$$).toHaveBeenCalledWith('.nav') + expect(mockExecute).toHaveBeenCalledOnce() + expect(result).toEqual([ + { x: 20, y: 40, width: 400, height: 100 }, + ]) + }) + + it('should add DPR-scaled viewport offset on iOS and re-query elements via $$', async () => { + const iosDeviceRectangles = { + ...baseDeviceRectangles, + viewport: { y: 94, x: 0, width: 390, height: 650 }, + } + const iosOptions = { + ...desktopOptions, + devicePixelRatio: 3, + deviceRectangles: iosDeviceRectangles, + isIOS: true, + } + const mockElement = { elementId: 'el1', selector: '.hero' } as WebdriverIO.Element + const freshElement = { elementId: 'el1-fresh', selector: '.hero' } as unknown as WebdriverIO.Element + vi.mocked(mockBrowserInstance.$$).mockResolvedValueOnce([freshElement] as any) + mockExecute.mockResolvedValueOnce({ x: 0, y: 100, width: 390, height: 200 }) + + const result = await determineWebScreenIgnoreRegions(iosOptions, [mockElement]) + + expect(mockBrowserInstance.$$).toHaveBeenCalledWith('.hero') + expect(mockBrowserInstance.$).not.toHaveBeenCalled() + expect(result).toEqual([ + { x: 0, y: 582, width: 1170, height: 600 }, + ]) + }) + + it('should correctly resolve multiple elements sharing the same selector on iOS', async () => { + const iosDeviceRectangles = { + ...baseDeviceRectangles, + viewport: { y: 94, x: 0, width: 390, height: 650 }, + } + const iosOptions = { + ...desktopOptions, + devicePixelRatio: 1, + deviceRectangles: iosDeviceRectangles, + isIOS: true, + } + const el1 = { elementId: 'a', selector: '.card' } as WebdriverIO.Element + const el2 = { elementId: 'b', selector: '.card' } as WebdriverIO.Element + const el3 = { elementId: 'c', selector: '.card' } as WebdriverIO.Element + + const fresh1 = { elementId: 'f1', selector: '.card' } as unknown as WebdriverIO.Element + const fresh2 = { elementId: 'f2', selector: '.card' } as unknown as WebdriverIO.Element + const fresh3 = { elementId: 'f3', selector: '.card' } as unknown as WebdriverIO.Element + vi.mocked(mockBrowserInstance.$$).mockResolvedValueOnce([fresh1, fresh2, fresh3] as any) + + mockExecute + .mockResolvedValueOnce({ x: 0, y: 100, width: 390, height: 50 }) + .mockResolvedValueOnce({ x: 0, y: 200, width: 390, height: 50 }) + .mockResolvedValueOnce({ x: 0, y: 300, width: 390, height: 50 }) + + const result = await determineWebScreenIgnoreRegions(iosOptions, [[el1, el2, el3]]) + + // $$ called once for the shared selector, not $ three times + expect(mockBrowserInstance.$$).toHaveBeenCalledTimes(1) + expect(mockBrowserInstance.$$).toHaveBeenCalledWith('.card') + // execute called with each fresh element + expect(mockExecute).toHaveBeenCalledTimes(3) + // Each region has different y (viewport offset 94 added) + expect(result).toEqual([ + { x: 0, y: 194, width: 390, height: 50 }, + { x: 0, y: 294, width: 390, height: 50 }, + { x: 0, y: 394, width: 390, height: 50 }, + ]) + }) + + it('should add device-pixel viewport offset on Android native web screenshot', async () => { + const androidDeviceRectangles = { + ...baseDeviceRectangles, + // On Android, viewport offset is already in device pixels + // (injectWebviewOverlay pre-scales by DPR) + viewport: { y: 240, x: 0, width: 1236, height: 1956 }, + } + const androidOptions = { + ...desktopOptions, + devicePixelRatio: 3, + deviceRectangles: androidDeviceRectangles, + isAndroid: true, + isAndroidNativeWebScreenshot: true, + } + const mockElement = { elementId: 'el1', selector: '#header' } as WebdriverIO.Element + vi.mocked(mockBrowserInstance.$$).mockResolvedValueOnce([mockElement] as any) + mockExecute.mockResolvedValueOnce({ x: 0, y: 0, width: 412, height: 64 }) + + const result = await determineWebScreenIgnoreRegions(androidOptions, [mockElement]) + + // BCR × DPR + viewport (already device px): + // x: 0*3 + 0 = 0, y: 0*3 + 240 = 240, w: 412*3 = 1236, h: 64*3 = 192 + expect(result).toEqual([ + { x: 0, y: 240, width: 1236, height: 192 }, + ]) + }) + + it('should NOT add viewport offset on Android ChromeDriver screenshot', async () => { + const androidChromeOptions = { + ...desktopOptions, + devicePixelRatio: 3, + isAndroid: true, + isAndroidNativeWebScreenshot: false, + } + const mockElement = { elementId: 'el1', selector: '#header' } as WebdriverIO.Element + vi.mocked(mockBrowserInstance.$$).mockResolvedValueOnce([mockElement] as any) + mockExecute.mockResolvedValueOnce({ x: 0, y: 0, width: 412, height: 64 }) + + const result = await determineWebScreenIgnoreRegions(androidChromeOptions, [mockElement]) + + // BCR × DPR only, no viewport offset + expect(result).toEqual([ + { x: 0, y: 0, width: 1236, height: 192 }, + ]) + }) + + it('should apply DPR to coordinate regions as well', async () => { + const region = { x: 10, y: 20, width: 100, height: 150 } + + const result = await determineWebScreenIgnoreRegions(desktopOptions, [region]) + + expect(mockExecute).not.toHaveBeenCalled() + expect(result).toEqual([ + { x: 20, y: 40, width: 200, height: 300 }, + ]) + }) + + it('should handle mixed elements and regions with DPR applied to both', async () => { + const mockElement = { elementId: 'el1', selector: '.ad' } as WebdriverIO.Element + vi.mocked(mockBrowserInstance.$$).mockResolvedValueOnce([mockElement] as any) + const region = { x: 500, y: 0, width: 200, height: 90 } + mockExecute.mockResolvedValueOnce({ x: 10, y: 20, width: 300, height: 80 }) + + const result = await determineWebScreenIgnoreRegions(desktopOptions, [mockElement, region]) + + expect(result).toEqual([ + { x: 1000, y: 0, width: 400, height: 180 }, + { x: 20, y: 40, width: 600, height: 160 }, + ]) + }) + + it('should handle empty array', async () => { + const result = await determineWebScreenIgnoreRegions(desktopOptions, []) + + expect(result).toEqual([]) + expect(mockExecute).not.toHaveBeenCalled() + }) + + it('should handle chainable promise elements', async () => { + const chainableElement = Promise.resolve({ elementId: 'el1', selector: '.footer' } as WebdriverIO.Element) + const freshElement = { elementId: 'el1-fresh', selector: '.footer' } as unknown as WebdriverIO.Element + vi.mocked(mockBrowserInstance.$$).mockResolvedValueOnce([freshElement] as any) + mockExecute.mockResolvedValueOnce({ x: 0, y: 900, width: 1200, height: 100 }) + + const result = await determineWebScreenIgnoreRegions(desktopOptions, [chainableElement as any]) + + expect(result).toEqual([ + { x: 0, y: 1800, width: 2400, height: 200 }, + ]) + }) + + it('should use floor/ceil rounding on sub-pixel BCR values to fully cover elements', async () => { + const mockElement = { elementId: 'el1', selector: '.banner' } as WebdriverIO.Element + vi.mocked(mockBrowserInstance.$$).mockResolvedValueOnce([mockElement] as any) + // Sub-pixel BCR values that would lose precision if rounded independently + mockExecute.mockResolvedValueOnce({ x: 0.33, y: 50.67, width: 412.5, height: 64.33 }) + + const opts = { ...desktopOptions, devicePixelRatio: 3 } + const result = await determineWebScreenIgnoreRegions(opts, [mockElement]) + + // Position uses floor, far-edge uses ceil: + // x: floor(0.33*3) = floor(0.99) = 0 + // y: floor(50.67*3) = floor(152.01) = 152 + // right: ceil((0.33+412.5)*3) = ceil(1238.49) = 1239 → w = 1239-0 = 1239 + // bottom: ceil((50.67+64.33)*3) = ceil(345.0) = 345 → h = 345-152 = 193 + expect(result).toEqual([ + { x: 0, y: 152, width: 1239, height: 193 }, + ]) + }) + + it('should throw on invalid ignore items', async () => { + await expect( + determineWebScreenIgnoreRegions(desktopOptions, ['invalid' as any]) + ).rejects.toThrow('Invalid elements or regions') + }) + + it('should expand regions by ignoreRegionPadding (default 1) on each side', async () => { + const region = { x: 10, y: 20, width: 100, height: 50 } + const optionsWithDefaultPadding = { + ...desktopOptions, + ignoreRegionPadding: 1, + } + + const result = await determineWebScreenIgnoreRegions(optionsWithDefaultPadding, [region]) + + expect(mockExecute).not.toHaveBeenCalled() + // (10,20,100,50) × DPR 2 → (20,40,200,100); + padding 1 each side → (19,39,202,102) + expect(result).toEqual([ + { x: 19, y: 39, width: 202, height: 102 }, + ]) + }) + + it('should use custom ignoreRegionPadding when provided for screen', async () => { + const region = { x: 0, y: 0, width: 50, height: 20 } + const optionsWithPadding2 = { + ...desktopOptions, + ignoreRegionPadding: 2, + } + + const result = await determineWebScreenIgnoreRegions(optionsWithPadding2, [region]) + + // (0,0,50,20) × 2 → (0,0,100,40); + padding 2 → (0,0,104,44) + expect(result).toEqual([ + { x: 0, y: 0, width: 104, height: 44 }, + ]) + }) + }) + + describe('determineWebFullPageIgnoreRegions', () => { + const fullPageOptions = { + browserInstance: null as unknown as WebdriverIO.Browser, + devicePixelRatio: 2, + ignoreRegionPadding: 0, + } + + beforeEach(() => { + fullPageOptions.browserInstance = mockBrowserInstance + }) + + it('should resolve elements via document BCR (BCR + scroll) and apply DPR', async () => { + const mockElement = { elementId: 'el1', selector: '.nav' } as WebdriverIO.Element + const freshElement = { elementId: 'el1-fresh', selector: '.nav' } as unknown as WebdriverIO.Element + vi.mocked(mockBrowserInstance.$$).mockResolvedValueOnce([freshElement] as any) + // rawDocumentBcr returns getBoundingClientRect() + (scrollX, scrollY) = document-relative CSS pixels + mockExecute.mockResolvedValueOnce({ x: 10, y: 1200, width: 200, height: 50 }) + + const result = await determineWebFullPageIgnoreRegions(fullPageOptions, [mockElement]) + + expect(mockBrowserInstance.$$).toHaveBeenCalledWith('.nav') + expect(mockExecute).toHaveBeenCalledOnce() + // Document CSS (10, 1200, 200, 50) × DPR 2 → device pixels (20, 2400, 400, 100) + expect(result).toEqual([ + { x: 20, y: 2400, width: 400, height: 100 }, + ]) + }) + + it('should treat raw regions as document-relative CSS pixels and apply DPR', async () => { + const region = { x: 0, y: 500, width: 300, height: 80 } + + const result = await determineWebFullPageIgnoreRegions(fullPageOptions, [region]) + + expect(mockExecute).not.toHaveBeenCalled() + expect(result).toEqual([ + { x: 0, y: 1000, width: 600, height: 160 }, + ]) + }) + + it('should expand regions by ignoreRegionPadding', async () => { + const region = { x: 10, y: 20, width: 100, height: 50 } + const optionsWithPadding = { + ...fullPageOptions, + ignoreRegionPadding: 1, + } + + const result = await determineWebFullPageIgnoreRegions(optionsWithPadding, [region]) + + // (10,20,100,50) × 2 → (20,40,200,100); + padding 1 → (19,39,202,102) + expect(result).toEqual([ + { x: 19, y: 39, width: 202, height: 102 }, + ]) + }) + + it('should return empty array when ignores is empty', async () => { + const result = await determineWebFullPageIgnoreRegions(fullPageOptions, []) + + expect(result).toEqual([]) + }) + + it('should subtract fullPageCropTopPaddingCSS from y for mobile scroll-and-stitch alignment', async () => { + const region = { x: 0, y: 100, width: 300, height: 80 } + const optionsWithCropTop = { + ...fullPageOptions, + devicePixelRatio: 3, + fullPageCropTopPaddingCSS: 6, + } + + const result = await determineWebFullPageIgnoreRegions(optionsWithCropTop, [region]) + + // document (0, 100, 300, 80) with cropTop 6 → canvas y = (100-6)*3 = 282, height = 80*3 = 240 + expect(mockExecute).not.toHaveBeenCalled() + expect(result).toEqual([ + { x: 0, y: 282, width: 900, height: 240 }, + ]) + }) + }) + + describe('determineWebElementIgnoreRegions', () => { + it('should resolve element-local regions and apply DPR', async () => { + const rootElement = { elementId: 'root', selector: '.root' } as WebdriverIO.Element + const childElement = { elementId: 'child', selector: '.child' } as WebdriverIO.Element + const freshChild = { elementId: 'child-fresh', selector: '.child' } as unknown as WebdriverIO.Element + + vi.mocked(mockBrowserInstance.$$).mockResolvedValueOnce([freshChild] as any) + // Simulate already-relative BCR from execute: (20,30,100,40) + mockExecute.mockResolvedValueOnce({ x: 20, y: 30, width: 100, height: 40 }) + + const result = await determineWebElementIgnoreRegions({ + browserInstance: mockBrowserInstance as unknown as WebdriverIO.Browser, + devicePixelRatio: 2, + rootElement, + ignoreRegionPadding: 0, + }, [childElement]) + + // CSS: (20,30,100,40) × DPR(2) → (40,60,200,80) + expect(result).toEqual([ + { x: 40, y: 60, width: 200, height: 80 }, + ]) + }) + + it('should pass through literal regions (CSS relative to element) with DPR applied', async () => { + const rootElement = { elementId: 'root', selector: '.root' } as WebdriverIO.Element + const region = { x: 5, y: 10, width: 50, height: 20 } + + const result = await determineWebElementIgnoreRegions({ + browserInstance: mockBrowserInstance as unknown as WebdriverIO.Browser, + devicePixelRatio: 2, + rootElement, + ignoreRegionPadding: 0, + }, [region]) + + expect(mockExecute).not.toHaveBeenCalled() + // (5,10,50,20) × 2 → (10,20,100,40) + expect(result).toEqual([ + { x: 10, y: 20, width: 100, height: 40 }, + ]) + }) + + it('should expand regions by ignoreRegionPadding (default 1) on each side', async () => { + const rootElement = { elementId: 'root', selector: '.root' } as WebdriverIO.Element + const region = { x: 10, y: 20, width: 100, height: 40 } + + const result = await determineWebElementIgnoreRegions({ + browserInstance: mockBrowserInstance as unknown as WebdriverIO.Browser, + devicePixelRatio: 2, + rootElement, + ignoreRegionPadding: 1, + }, [region]) + + expect(mockExecute).not.toHaveBeenCalled() + // (10,20,100,40) × 2 → (20,40,200,80); + padding 1 each side → (19,39,202,82) + expect(result).toEqual([ + { x: 19, y: 39, width: 202, height: 82 }, + ]) + }) + + it('should use custom ignoreRegionPadding when provided', async () => { + const rootElement = { elementId: 'root', selector: '.root' } as WebdriverIO.Element + const region = { x: 0, y: 0, width: 50, height: 20 } + + const result = await determineWebElementIgnoreRegions({ + browserInstance: mockBrowserInstance as unknown as WebdriverIO.Browser, + devicePixelRatio: 1, + rootElement, + ignoreRegionPadding: 2, + }, [region]) + + // (0,0,50,20) + padding 2 each side → (0,0,54,24) — x,y clamped to 0 + expect(result).toEqual([ + { x: 0, y: 0, width: 54, height: 24 }, + ]) + }) + + it('should handle empty ignores', async () => { + const rootElement = { elementId: 'root', selector: '.root' } as WebdriverIO.Element + + const result = await determineWebElementIgnoreRegions({ + browserInstance: mockBrowserInstance as unknown as WebdriverIO.Browser, + devicePixelRatio: 2, + rootElement, + ignoreRegionPadding: 0, + }, []) + + expect(result).toEqual([]) + expect(mockExecute).not.toHaveBeenCalled() + }) + + it('should output CSS-pixel regions when isAndroidNativeWebScreenshot and isWebDriverElementScreenshot (native driver image at CSS size)', async () => { + const rootElement = { elementId: 'root', selector: '.root' } as WebdriverIO.Element + const region = { x: 10, y: 20, width: 100, height: 40 } + + const result = await determineWebElementIgnoreRegions({ + browserInstance: mockBrowserInstance as unknown as WebdriverIO.Browser, + devicePixelRatio: 2, + rootElement, + ignoreRegionPadding: 0, + isAndroidNativeWebScreenshot: true, + isWebDriverElementScreenshot: true, + }, [region]) + + expect(mockExecute).not.toHaveBeenCalled() + // Device px: (10,20,100,40) × 2 → (20,40,200,80); then downscale to CSS for native driver image → (10,20,100,40) + expect(result).toEqual([ + { x: 10, y: 20, width: 100, height: 40 }, + ]) + }) + }) + describe('determineDeviceBlockOuts', () => { it('should return empty array when no blockouts are enabled', async () => { const options = createDeviceBlockOutsOptions() @@ -1018,5 +1455,241 @@ describe('rectangles', () => { expect(result.hasIgnoreRectangles).toBe(true) expect(result.ignoredBoxes).toMatchSnapshot() }) + + it('should scale ignoreRegions by DPR for native iOS app (logical → device pixels)', async () => { + const options = createPrepareIgnoreRectanglesOptions({ + ignoreRegions: [ + { x: 10, y: 20, width: 100, height: 50 }, + ], + devicePixelRatio: 3, + isMobile: true, + isNativeContext: true, + isAndroid: false, + }) + + const result = await prepareIgnoreRectangles(options) + + expect(result.hasIgnoreRectangles).toBe(true) + expect(result.ignoredBoxes).toHaveLength(1) + expect(result.ignoredBoxes[0]).toEqual({ + left: 30, + top: 60, + right: 330, + bottom: 210, + }) + }) + + it('should scale multiple ignoreRegions by DPR for native iOS app', async () => { + const options = createPrepareIgnoreRectanglesOptions({ + ignoreRegions: [ + { x: 0, y: 0, width: 390, height: 47 }, + { x: 50, y: 100, width: 200, height: 80 }, + ], + devicePixelRatio: 3, + isMobile: true, + isNativeContext: true, + isAndroid: false, + }) + + const result = await prepareIgnoreRectangles(options) + + expect(result.hasIgnoreRectangles).toBe(true) + expect(result.ignoredBoxes).toHaveLength(2) + expect(result.ignoredBoxes[0]).toEqual({ + left: 0, + top: 0, + right: 1170, + bottom: 141, + }) + expect(result.ignoredBoxes[1]).toEqual({ + left: 150, + top: 300, + right: 750, + bottom: 540, + }) + }) + + it('should not scale ignoreRegions for native Android app', async () => { + const options = createPrepareIgnoreRectanglesOptions({ + ignoreRegions: [{ x: 100, y: 200, width: 150, height: 75 }], + devicePixelRatio: 3, + isMobile: true, + isNativeContext: true, + isAndroid: true, + }) + + const result = await prepareIgnoreRectangles(options) + + expect(result.hasIgnoreRectangles).toBe(true) + expect(result.ignoredBoxes).toHaveLength(1) + expect(result.ignoredBoxes[0]).toEqual({ + left: 100, + top: 200, + right: 250, + bottom: 275, + }) + }) + + it('should not scale ignoreRegions when not native context (e.g. web)', async () => { + const options = createPrepareIgnoreRectanglesOptions({ + ignoreRegions: [{ x: 10, y: 20, width: 100, height: 50 }], + devicePixelRatio: 3, + isMobile: true, + isNativeContext: false, + isAndroid: false, + }) + + const result = await prepareIgnoreRectangles(options) + + expect(result.hasIgnoreRectangles).toBe(true) + expect(result.ignoredBoxes).toHaveLength(1) + expect(result.ignoredBoxes[0]).toEqual({ + left: 10, + top: 20, + right: 110, + bottom: 70, + }) + }) + + it('should add hybrid-app status bar fallback when statusBarAndAddressBar height is 0 (iOS)', async () => { + const deviceRectangles = createDeviceRectanglesWithData({ + screenSize: { width: 375, height: 812 }, + statusBarAndAddressBar: { x: 0, y: 0, width: 375, height: 0 }, + bottomBar: { y: 0, x: 0, width: 0, height: 0 }, + }) + const options = createPrepareIgnoreRectanglesOptions({ + deviceRectangles, + isMobile: true, + isNativeContext: false, + isAndroid: false, + isAndroidNativeWebScreenshot: true, + isViewPortScreenshot: true, + devicePixelRatio: 3, + imageCompareOptions: { + blockOutStatusBar: true, + }, + }) + + const result = await prepareIgnoreRectangles(options) + + expect(result.hasIgnoreRectangles).toBe(true) + const statusBarBox = result.ignoredBoxes.find( + (b: { top: number }) => b.top === 0 + ) as { left: number; top: number; right: number; bottom: number } + expect(statusBarBox).toBeDefined() + expect(statusBarBox.left).toBe(0) + expect(statusBarBox.top).toBe(0) + expect(statusBarBox.right).toBe(1125) + expect(statusBarBox.bottom).toBe(132) + }) + + it('should add hybrid-app status bar fallback when isHybridApp is true (iOS)', async () => { + const deviceRectangles = createDeviceRectanglesWithData({ + screenSize: { width: 390, height: 844 }, + statusBarAndAddressBar: { x: 0, y: 0, width: 390, height: 47 }, + }) + const options = createPrepareIgnoreRectanglesOptions({ + deviceRectangles, + isMobile: true, + isNativeContext: false, + isAndroid: false, + isAndroidNativeWebScreenshot: true, + isViewPortScreenshot: true, + devicePixelRatio: 2, + isHybridApp: true, + imageCompareOptions: { + blockOutStatusBar: true, + }, + }) + + const result = await prepareIgnoreRectangles(options) + + expect(result.hasIgnoreRectangles).toBe(true) + const statusBarBoxes = result.ignoredBoxes.filter( + (b: { top: number }) => b.top === 0 + ) as Array<{ left: number; top: number; right: number; bottom: number }> + expect(statusBarBoxes.length).toBeGreaterThanOrEqual(1) + }) + + it('should add hybrid-app status bar fallback for Android when overlay reports zero', async () => { + const deviceRectangles = createDeviceRectanglesWithData({ + screenSize: { width: 412, height: 869 }, + statusBarAndAddressBar: { x: 0, y: 0, width: 412, height: 0 }, + bottomBar: { y: 0, x: 0, width: 0, height: 0 }, + }) + const options = createPrepareIgnoreRectanglesOptions({ + deviceRectangles, + isMobile: true, + isNativeContext: false, + isAndroid: true, + isAndroidNativeWebScreenshot: true, + isViewPortScreenshot: true, + devicePixelRatio: 2, + imageCompareOptions: { + blockOutStatusBar: true, + }, + }) + + const result = await prepareIgnoreRectangles(options) + + expect(result.hasIgnoreRectangles).toBe(true) + const statusBarBox = result.ignoredBoxes.find( + (b: { top: number }) => b.top === 0 + ) as { left: number; top: number; right: number; bottom: number } + expect(statusBarBox).toBeDefined() + expect(statusBarBox.bottom).toBe(24) + }) + + it('should use device platformVersion for Android hybrid status bar fallback when in ANDROID_OFFSETS list', async () => { + const deviceRectangles = createDeviceRectanglesWithData({ + screenSize: { width: 412, height: 869 }, + statusBarAndAddressBar: { x: 0, y: 0, width: 412, height: 0 }, + }) + const options = createPrepareIgnoreRectanglesOptions({ + deviceRectangles, + isMobile: true, + isNativeContext: false, + isAndroid: true, + isViewPortScreenshot: true, + devicePixelRatio: 2, + imageCompareOptions: { blockOutStatusBar: true }, + platformVersion: '12', + }) + + const result = await prepareIgnoreRectangles(options) + + expect(result.hasIgnoreRectangles).toBe(true) + const statusBarBox = result.ignoredBoxes.find( + (b: { top: number }) => b.top === 0 + ) as { left: number; top: number; right: number; bottom: number } + expect(statusBarBox).toBeDefined() + expect(statusBarBox.bottom).toBe(24) + }) + + it('should fall back to latest API level for Android when platformVersion not in ANDROID_OFFSETS', async () => { + const deviceRectangles = createDeviceRectanglesWithData({ + screenSize: { width: 412, height: 869 }, + statusBarAndAddressBar: { x: 0, y: 0, width: 412, height: 0 }, + }) + const options = createPrepareIgnoreRectanglesOptions({ + deviceRectangles, + isMobile: true, + isNativeContext: false, + isAndroid: true, + isViewPortScreenshot: true, + devicePixelRatio: 2, + imageCompareOptions: { blockOutStatusBar: true }, + platformVersion: '99', + }) + + const result = await prepareIgnoreRectangles(options) + + expect(result.hasIgnoreRectangles).toBe(true) + const statusBarBox = result.ignoredBoxes.find( + (b: { top: number }) => b.top === 0 + ) as { left: number; top: number; right: number; bottom: number } + expect(statusBarBox).toBeDefined() + expect(statusBarBox.bottom).toBe(24) + }) }) }) diff --git a/packages/image-comparison-core/src/methods/rectangles.ts b/packages/image-comparison-core/src/methods/rectangles.ts index a88ca7187..8d98a258b 100644 --- a/packages/image-comparison-core/src/methods/rectangles.ts +++ b/packages/image-comparison-core/src/methods/rectangles.ts @@ -1,8 +1,12 @@ import { Jimp } from 'jimp' +import { ANDROID_OFFSETS, IOS_OFFSETS } from '../helpers/constants.js' import { calculateDprData, getBase64ScreenshotSize, isObject } from '../helpers/utils.js' import { getElementPositionAndroid, getElementPositionDesktop, getElementWebviewPosition } from './elementPosition.js' import type { DetermineDeviceBlockOutsOptions, + DetermineWebFullPageIgnoreRegionsOptions, + DetermineWebScreenIgnoreRegionsOptions, + DetermineWebElementIgnoreRegionsOptions, DeviceRectangles, ElementRectangles, PrepareIgnoreRectanglesOptions, @@ -254,7 +258,7 @@ export async function getRegionsFromElements(browserInstance: WebdriverIO.Browse */ export async function determineIgnoreRegions( browserInstance: WebdriverIO.Browser, - ignores: ElementIgnore[], + ignores: (ElementIgnore | ElementIgnore[])[], ): Promise{ const awaitedIgnores = await Promise.all(ignores) const { elements, regions } = splitIgnores(awaitedIgnores) @@ -269,6 +273,266 @@ export async function determineIgnoreRegions( })) } +/** + * Translate ignores to regions for web screen (viewport) screenshots. + * Uses getBoundingClientRect (CSS pixels) and converts to device pixels, + * accounting for the viewport offset on native web screenshot devices. + * + * Coordinate systems per platform: + * - Desktop / Android ChromeDriver: screenshot is viewport-only, BCR × DPR + * - iOS: full-device screenshot, viewport offset is in CSS points → (BCR + offset) × DPR + * - Android native web: full-device screenshot, viewport offset is already in + * device pixels (injectWebviewOverlay pre-scales by DPR) → BCR × DPR + offset + */ +export async function determineWebScreenIgnoreRegions( + options: DetermineWebScreenIgnoreRegionsOptions, + ignores: (ElementIgnore | ElementIgnore[])[], +): Promise { + const awaitedIgnores = await Promise.all(ignores) + const { elements, regions } = splitIgnores(awaitedIgnores) + const { browserInstance, devicePixelRatio, deviceRectangles, isAndroid, isAndroidNativeWebScreenshot, isIOS, ignoreRegionPadding: padding } = options + + // Get raw (unrounded) BCR values so we can multiply by DPR before + // rounding. The shared getBoundingClientRect script pre-rounds to CSS + // integers which loses sub-pixel precision that matters at higher DPRs. + const rawBcr = (el: HTMLElement) => { + const rect = el.getBoundingClientRect() + return { x: rect.x, y: rect.y, width: rect.width, height: rect.height } + } + // Browsers can invalidate element references when the DOM is mutated + // (e.g. by beforeScreenshot CSS/style injection). Re-query via $$ to + // get fresh refs. Use $$ per unique selector so multiple elements + // sharing the same selector (e.g. from a $$ call) each resolve to + // the correct match by index. + const regionsFromElements: RectanglesOutput[] = [] + const selectorCache = new Map() + const selectorIndex = new Map() + + for (const element of elements) { + const selector = element.selector as string + + if (!selectorCache.has(selector)) { + const fresh = await browserInstance.$$(selector) + selectorCache.set(selector, fresh as unknown as WebdriverIO.Element[]) + selectorIndex.set(selector, 0) + } + + const idx = selectorIndex.get(selector)! + const cached = selectorCache.get(selector)! + const el = idx < cached.length ? cached[idx] : element + selectorIndex.set(selector, idx + 1) + + const bcr = await browserInstance.execute(rawBcr, el as any) as RectanglesOutput + regionsFromElements.push(bcr) + } + + return [...regions, ...regionsFromElements] + .map((region: RectanglesOutput) => { + // Use floor for top-left and ceil for bottom-right so the + // device-pixel rectangle fully covers the CSS-pixel element. + // Rounding position and size independently can miss edge pixels. + let cssX = region.x + let cssY = region.y + + if (isIOS) { + cssX += deviceRectangles.viewport.x + cssY += deviceRectangles.viewport.y + } + + const left = Math.floor(cssX * devicePixelRatio) + const top = Math.floor(cssY * devicePixelRatio) + const right = Math.ceil((cssX + region.width) * devicePixelRatio) + const bottom = Math.ceil((cssY + region.height) * devicePixelRatio) + + let x = left + let y = top + + if (isAndroid && isAndroidNativeWebScreenshot) { + // Android native web viewport offset is already in device pixels + x += deviceRectangles.viewport.x + y += deviceRectangles.viewport.y + } + + let width = right - left + let height = bottom - top + if (padding > 0) { + x = Math.max(0, x - padding) + y = Math.max(0, y - padding) + width += 2 * padding + height += 2 * padding + } + return { x, y, width, height } + }) +} + +/** + * Translate ignores to regions for web full-page screenshots (desktop and mobile). + * Full-page image (BiDi or scroll-and-stitch) is in document coordinates: (0,0) = top-left + * of document, device pixels. Uses getBoundingClientRect + (scrollX, scrollY) for elements, + * then converts to device pixels. Same logic for all platforms; no viewport offset needed + * because the stitched canvas is built in document space. + */ +export async function determineWebFullPageIgnoreRegions( + options: DetermineWebFullPageIgnoreRegionsOptions, + ignores: (ElementIgnore | ElementIgnore[])[], +): Promise { + const awaitedIgnores = await Promise.all(ignores) + const { elements, regions } = splitIgnores(awaitedIgnores) + const { browserInstance, devicePixelRatio, ignoreRegionPadding: padding, fullPageCropTopPaddingCSS: cropTop = 0 } = options + + const rawDocumentBcr = (el: HTMLElement) => { + const rect = el.getBoundingClientRect() + return { + x: rect.x + window.scrollX, + y: rect.y + window.scrollY, + width: rect.width, + height: rect.height, + } + } + + const regionsFromElements: RectanglesOutput[] = [] + const selectorCache = new Map() + const selectorIndex = new Map() + + for (const element of elements) { + const selector = element.selector as string + + if (!selectorCache.has(selector)) { + const fresh = await browserInstance.$$(selector) + selectorCache.set(selector, fresh as unknown as WebdriverIO.Element[]) + selectorIndex.set(selector, 0) + } + + const idx = selectorIndex.get(selector)! + const cached = selectorCache.get(selector)! + const el = idx < cached.length ? cached[idx] : element + selectorIndex.set(selector, idx + 1) + + const bcr = await browserInstance.execute(rawDocumentBcr, el as any) as RectanglesOutput + regionsFromElements.push(bcr) + } + + return [...regions, ...regionsFromElements] + .map((region: RectanglesOutput) => { + const left = Math.floor(region.x * devicePixelRatio) + const right = Math.ceil((region.x + region.width) * devicePixelRatio) + // On mobile full-page scroll-and-stitch, the canvas crops cropTop (e.g. 6px) from the top + // of each tile, so canvas y = (documentY - cropTop) × DPR + const topDevice = Math.floor((region.y - cropTop) * devicePixelRatio) + const bottomDevice = Math.ceil((region.y + region.height - cropTop) * devicePixelRatio) + const top = Math.max(0, topDevice) + const bottom = Math.max(top, bottomDevice) + + let x = left + let y = top + let width = right - left + let height = bottom - top + if (padding > 0) { + x = Math.max(0, x - padding) + y = Math.max(0, y - padding) + width += 2 * padding + height += 2 * padding + } + return { x, y, width, height } + }) +} + +/** + * Translate ignores to regions for web element screenshots. + * By default regions are in *element-local* device pixels so they match the cropped element image + * (BiDi clip or fallback full-screenshot crop, both at device pixel size). + * Exception: when the element screenshot is from the native driver on Android native web + * (isWebDriverElementScreenshot && isAndroidNativeWebScreenshot), the driver returns an image at + * CSS pixel size (downscaled). We then output regions in CSS pixel coordinates (divide by DPR) + * so they align with that image. Fallback (full screenshot + crop) is at device size, so we do + * not downscale when fallback was used. + */ +export async function determineWebElementIgnoreRegions( + options: DetermineWebElementIgnoreRegionsOptions, + ignores: (ElementIgnore | ElementIgnore[])[], +): Promise { + const awaitedIgnores = await Promise.all(ignores) + const { elements, regions } = splitIgnores(awaitedIgnores) + const { + browserInstance, + devicePixelRatio, + rootElement, + ignoreRegionPadding: padding, + isAndroidNativeWebScreenshot, + isWebDriverElementScreenshot, + } = options + + // Compute bounding boxes relative to the root element: (childBCR - rootBCR) + const rawRelativeBcr = (el: HTMLElement, root: HTMLElement) => { + const elRect = el.getBoundingClientRect() + const rootRect = root.getBoundingClientRect() + + return { + x: elRect.x - rootRect.x, + y: elRect.y - rootRect.y, + width: elRect.width, + height: elRect.height, + } + } + + const regionsFromElements: RectanglesOutput[] = [] + const selectorCache = new Map() + const selectorIndex = new Map() + + for (const element of elements) { + const selector = element.selector as string + + if (!selectorCache.has(selector)) { + const fresh = await browserInstance.$$(selector) + selectorCache.set(selector, fresh as unknown as WebdriverIO.Element[]) + selectorIndex.set(selector, 0) + } + + const idx = selectorIndex.get(selector)! + const cached = selectorCache.get(selector)! + const el = idx < cached.length ? cached[idx] : element + selectorIndex.set(selector, idx + 1) + + const bcr = await browserInstance.execute(rawRelativeBcr, el as any, rootElement as any) as RectanglesOutput + regionsFromElements.push(bcr) + } + + // Both literal regions and element-derived regions are currently expected + // to be in CSS pixels relative to the element. Scale everything by DPR and + // express as device-pixel rectangles using the same floor-based rounding + // strategy as the BiDi element clip (x/y/width/height all floored). + // Then expand each region by ignoreRegionPadding on each side (configurable, default 1) + // to reduce 1px boundary differences on high-DPR / BiDi. + let result = [...regions, ...regionsFromElements] + .map((region: RectanglesOutput) => { + let x = Math.floor(region.x * devicePixelRatio) + let y = Math.floor(region.y * devicePixelRatio) + let width = Math.floor(region.width * devicePixelRatio) + let height = Math.floor(region.height * devicePixelRatio) + if (padding > 0) { + x = Math.max(0, x - padding) + y = Math.max(0, y - padding) + width += 2 * padding + height += 2 * padding + } + return { x, y, width, height } + }) + + // Only downscale when the element image is at CSS pixel size: native driver element screenshot + // on Android native web (fallback false). Fallback uses a device-pixel crop, so no downscale. + if (isAndroidNativeWebScreenshot === true && isWebDriverElementScreenshot === true && devicePixelRatio > 0) { + const dpr = devicePixelRatio + result = result.map((r) => ({ + x: Math.round(r.x / dpr), + y: Math.round(r.y / dpr), + width: Math.round(r.width / dpr), + height: Math.round(r.height / dpr), + })) + } + + return result +} + /** * Determine the device block outs */ @@ -298,6 +562,59 @@ export async function determineDeviceBlockOuts({ isAndroid, screenCompareOptions return rectangles } +/** + * Return a status bar rectangle for hybrid-app fallback when the overlay reports zero height. + * Uses IOS_OFFSETS / ANDROID_OFFSETS so the system status bar is blocked out in webview context. + * Android: uses device platformVersion (e.g. "14.0") as API level when present and in list; otherwise latest. + * iOS: keyed by screen size only (no OS version in data). When device not in list, uses latest entry. + */ +function getHybridAppStatusBarFallback( + deviceRectangles: DeviceRectangles, + isAndroid: boolean, + platformVersion?: string, +): RectanglesOutput | null { + const { width: screenWidth, height: screenHeight } = deviceRectangles.screenSize + if (screenWidth === 0 || screenHeight === 0) { + return null + } + + if (isAndroid) { + const apiLevels = Object.keys(ANDROID_OFFSETS).map(Number) + const latestApiLevel = apiLevels.length > 0 ? Math.max(...apiLevels) : 14 + const deviceApiLevel = platformVersion !== undefined ? parseInt(platformVersion, 10) : NaN + const useApiLevel = + Number.isInteger(deviceApiLevel) && apiLevels.includes(deviceApiLevel) + ? deviceApiLevel + : latestApiLevel + const statusBarHeight = ANDROID_OFFSETS[useApiLevel as keyof typeof ANDROID_OFFSETS]?.STATUS_BAR ?? 24 + return { + x: 0, + y: 0, + width: screenWidth, + height: statusBarHeight, + } + } + + const isIphone = screenWidth < 1024 && screenHeight < 1024 + const deviceType = isIphone ? 'IPHONE' : 'IPAD' + const portraitHeight = screenWidth > screenHeight ? screenWidth : screenHeight + const keys = Object.keys(IOS_OFFSETS[deviceType]).map(Number) + const exactMatch = keys.includes(portraitHeight) + const offsetPortraitHeight = exactMatch + ? portraitHeight + : (keys.length > 0 ? Math.max(...keys) : (isIphone ? 667 : 1024)) + const orientation = screenWidth > screenHeight ? 'LANDSCAPE' : 'PORTRAIT' + const currentOffsets = IOS_OFFSETS[deviceType][offsetPortraitHeight]?.[orientation] + const statusBarHeight = currentOffsets?.STATUS_BAR ?? (isIphone ? 44 : 20) + + return { + x: 0, + y: 0, + width: screenWidth, + height: statusBarHeight, + } +} + /** * Prepare all ignore rectangles for image comparison */ @@ -313,6 +630,8 @@ export async function prepareIgnoreRectangles(options: PrepareIgnoreRectanglesOp isAndroidNativeWebScreenshot, isViewPortScreenshot, imageCompareOptions, + isHybridApp, + platformVersion, actualFilePath } = options @@ -335,6 +654,18 @@ export async function prepareIgnoreRectangles(options: PrepareIgnoreRectanglesOp ...(determineStatusAddressToolBarRectangles({ deviceRectangles, options: statusAddressToolBarOptions })) || [] ) + // Hybrid-app fallback: in webview the overlay often reports statusBarAndAddressBar height 0. + // Use native offsets (IOS_OFFSETS / ANDROID_OFFSETS) so the system status bar is still blocked out. + const needStatusBarFallback = + imageCompareOptions.blockOutStatusBar !== false && + (isHybridApp === true || deviceRectangles.statusBarAndAddressBar.height === 0) + if (needStatusBarFallback) { + const fallback = getHybridAppStatusBarFallback(deviceRectangles, isAndroid, platformVersion) + if (fallback && fallback.height > 0) { + webStatusAddressToolBarOptions.push(fallback) + } + } + if (webStatusAddressToolBarOptions.length > 0) { // There's an issue with the resemble lib when all the rectangles are 0,0,0,0, it will see this as a full // blockout of the image and the comparison will succeed with 0 % difference. @@ -375,33 +706,44 @@ export async function prepareIgnoreRectangles(options: PrepareIgnoreRectanglesOp } } - // Combine all ignore regions - const ignoredBoxes = [ - // These come from the method + // blockOut and device bar rectangles are in CSS pixels, scale by DPR + const dprScaledBoxes = [ ...blockOut, - // @TODO: I'm defaulting ignore regions for devices - // Need to check if this is the right thing to do for web and mobile browser tests - ...ignoreRegions, - // Only get info about the status bars when we are in the web context ...webStatusAddressToolBarOptions ] .map( - // Make sure all the rectangles are equal to the dpr for the screenshot (rectangles) => { return calculateDprData( { - // Adjust for the ResembleJS API bottom: rectangles.y + rectangles.height, right: rectangles.x + rectangles.width, left: rectangles.x, top: rectangles.y, }, - // For Android we don't need to do it times the pixel ratio, for all others we need to isAndroid ? 1 : devicePixelRatio, ) }, ) + // ignoreRegions: for web they are already in device pixels (pre-scaled by the caller). + // For native iOS app they are in logical pixels (getElementRect / statusBar/homeBar), + // so we scale by DPR here to match the device-pixel screenshot. + const isNativeIos = isNativeContext && isMobile && !isAndroid + const preScaledIgnoreBoxes = ignoreRegions.map((rectangles) => { + const box = { + left: rectangles.x, + top: rectangles.y, + right: rectangles.x + rectangles.width, + bottom: rectangles.y + rectangles.height, + } + if (isNativeIos) { + return calculateDprData({ ...box }, devicePixelRatio) + } + return box + }) + + const ignoredBoxes = [...dprScaledBoxes, ...preScaledIgnoreBoxes] + return { ignoredBoxes, hasIgnoreRectangles: ignoredBoxes.length > 0 diff --git a/packages/image-comparison-core/src/methods/screenshots.ts b/packages/image-comparison-core/src/methods/screenshots.ts index 258af8ac9..1bab8ec44 100644 --- a/packages/image-comparison-core/src/methods/screenshots.ts +++ b/packages/image-comparison-core/src/methods/screenshots.ts @@ -41,7 +41,7 @@ export async function getMobileFullPageNativeWebScreenshotsData(browserInstance: const effectiveViewportHeight = hasNoBottomBar && hasHomeBar ? viewportHeight - Math.round(homeBar.height / (isAndroid ? devicePixelRatio : 1)) : viewportHeight - const viewportWidth= Math.round(viewport.width / (isAndroid ? devicePixelRatio : 1)) + const viewportWidth = Math.round(viewport.width / (isAndroid ? devicePixelRatio : 1)) const viewportX = Math.round(viewport.x / (isAndroid ? devicePixelRatio : 1)) const viewportY = Math.round(viewport.y / (isAndroid ? devicePixelRatio : 1)) // Start with an empty array, during the scroll it will be filled because a page could also have a lazy loading diff --git a/packages/image-comparison-core/src/methods/takeElementScreenshots.test.ts b/packages/image-comparison-core/src/methods/takeElementScreenshots.test.ts index 820c5738c..36f6711b9 100644 --- a/packages/image-comparison-core/src/methods/takeElementScreenshots.test.ts +++ b/packages/image-comparison-core/src/methods/takeElementScreenshots.test.ts @@ -83,7 +83,7 @@ describe('takeElementScreenshot', () => { }) describe('BiDi screenshots', () => { - it('should take BiDi screenshot from viewport when shouldUseBidi is true', async () => { + it('should take BiDi screenshot from document when shouldUseBidi is true', async () => { const result = await takeElementScreenshot(browserInstance, baseOptions, true) expect(result).toEqual({ @@ -93,44 +93,43 @@ describe('takeElementScreenshot', () => { expect(getElementRectMock).toHaveBeenCalledWith('test-element') expect(takeBase64BiDiScreenshotSpy).toHaveBeenCalledWith({ browserInstance, - origin: 'viewport', + origin: 'document', clip: { x: 10, y: 20, width: 100, height: 200 } }) expect(takeWebElementScreenshotSpy).not.toHaveBeenCalled() expect(makeCroppedBase64ImageSpy).not.toHaveBeenCalled() }) + // + // We intentionally rely on BiDi with origin: 'document' only. If that + // ever fails, we surface the underlying error instead of silently + // falling back to a different origin with mismatched coordinates. - it('should fallback to document screenshot when viewport fails with zero dimensions error', async () => { - takeBase64BiDiScreenshotSpy.mockRejectedValueOnce( - new Error('WebDriver Bidi command "browsingContext.captureScreenshot" failed with error: unable to capture screen - Unable to capture screenshot with zero dimensions') - ) + it('should scroll element into view when autoElementScroll is enabled', async () => { + const optionsWithScroll = { ...baseOptions, autoElementScroll: true } + executeMock.mockResolvedValueOnce(100) // previous scroll position - const result = await takeElementScreenshot(browserInstance, baseOptions, true) + const result = await takeElementScreenshot(browserInstance, optionsWithScroll, true) expect(result).toEqual({ base64Image: 'bidi-screenshot-data', isWebDriverElementScreenshot: false }) - expect(takeBase64BiDiScreenshotSpy).toHaveBeenCalledTimes(2) - expect(takeBase64BiDiScreenshotSpy.mock.calls[0][0]).toEqual({ - browserInstance, - origin: 'viewport', - clip: { x: 10, y: 20, width: 100, height: 200 } - }) - expect(takeBase64BiDiScreenshotSpy.mock.calls[1][0]).toEqual({ - browserInstance, - origin: 'document', - clip: { x: 10, y: 20, width: 100, height: 200 } - }) + // First call: scrollElementIntoView, second call: scrollToPosition (restore) + expect(executeMock).toHaveBeenCalledTimes(2) + expect(executeMock.mock.calls[0]).toMatchSnapshot() + expect(executeMock.mock.calls[1]).toMatchSnapshot() + expect(waitForSpy).toHaveBeenCalledWith(100) }) - it('should throw error when BiDi screenshot fails with non-zero dimension error', async () => { - const error = new Error('Some other BiDi error') - takeBase64BiDiScreenshotSpy.mockRejectedValueOnce(error) + it('should not restore scroll when autoElementScroll is enabled but no previous position', async () => { + const optionsWithScroll = { ...baseOptions, autoElementScroll: true } + executeMock.mockResolvedValueOnce(undefined) // no previous position - await expect(takeElementScreenshot(browserInstance, baseOptions, true)).rejects.toThrow(error) - expect(takeBase64BiDiScreenshotSpy).toHaveBeenCalledTimes(1) - expect(takeWebElementScreenshotSpy).not.toHaveBeenCalled() + await takeElementScreenshot(browserInstance, optionsWithScroll, true) + + // Only the scrollElementIntoView call, no restore + expect(executeMock).toHaveBeenCalledTimes(1) + expect(waitForSpy).toHaveBeenCalledWith(100) }) }) diff --git a/packages/image-comparison-core/src/methods/takeElementScreenshots.ts b/packages/image-comparison-core/src/methods/takeElementScreenshots.ts index fd7fb7650..0e4de5e8a 100644 --- a/packages/image-comparison-core/src/methods/takeElementScreenshots.ts +++ b/packages/image-comparison-core/src/methods/takeElementScreenshots.ts @@ -23,30 +23,31 @@ async function takeBiDiElementScreenshot( browserInstance: WebdriverIO.Browser, options: ElementScreenshotDataOptions ): Promise { - let base64Image: string const isWebDriverElementScreenshot = false - // We also need to clip the image to the element size, taking into account the DPR - // and also clip it from the document, not the viewport + // Scroll the element into the viewport so any lazy‑load / intersection + // observers are triggered. We always capture from the *document* origin, + // so the clip coordinates are document‑relative and independent of scroll. + let currentPosition: number | undefined + if (options.autoElementScroll) { + currentPosition = await browserInstance.execute(scrollElementIntoView as any, options.element, options.addressBarShadowPadding) + await waitFor(100) + } + + // Get the element rect and clip the screenshot. WebDriver getElementRect + // returns coordinates relative to the document origin, which matches the + // BiDi `origin: 'document'` coordinate system. const rect = await browserInstance.getElementRect!((await options.element as WebdriverIO.Element).elementId) const clip = { x: Math.floor(rect.x), y: Math.floor(rect.y), width: Math.floor(rect.width), height: Math.floor(rect.height) } - const takeBiDiElementScreenshot = (origin: 'document' | 'viewport') => takeBase64BiDiScreenshot({ browserInstance, origin, clip }) - - try { - // By default we take the screenshot from the viewport - base64Image = await takeBiDiElementScreenshot('viewport') - } catch (err: any) { - // But when we get a zero dimension error (meaning the element might be bigger than the - // viewport or it might not be in the viewport), we need to take the screenshot from the document. - const isZeroDimensionError = typeof err?.message === 'string' && err.message.includes( - 'WebDriver Bidi command "browsingContext.captureScreenshot" failed with error: unable to capture screen - Unable to capture screenshot with zero dimensions' - ) - - if (!isZeroDimensionError) { - throw err - } - - base64Image = await takeBiDiElementScreenshot('document') + const base64Image = await takeBase64BiDiScreenshot({ + browserInstance, + origin: 'document', + clip, + }) + + // Restore scroll position + if (options.autoElementScroll && currentPosition) { + await browserInstance.execute(scrollToPosition, currentPosition) } return { diff --git a/packages/ocr-service/package.json b/packages/ocr-service/package.json index 67215dfef..0acec6259 100644 --- a/packages/ocr-service/package.json +++ b/packages/ocr-service/package.json @@ -30,7 +30,7 @@ "dependencies": { "@wdio/globals": "^9.23.0", "@wdio/logger": "^9.18.0", - "@wdio/types": "^9.20.0", + "@wdio/types": "^9.25.0", "fuse.js": "^7.1.0", "@inquirer/prompts": "7.10.1", "jimp": "^1.6.0", diff --git a/packages/visual-reporter/package.json b/packages/visual-reporter/package.json index d2b45b311..ee066b725 100644 --- a/packages/visual-reporter/package.json +++ b/packages/visual-reporter/package.json @@ -39,22 +39,22 @@ "@remix-run/react": "^2.17.4", "@remix-run/serve": "^2.17.4", "@remix-run/dev": "^2.17.4", - "@types/react": "^18.3.27", + "@types/react": "^18.3.28", "@types/react-dom": "^18.3.7", - "@typescript-eslint/eslint-plugin": "^8.53.0", - "@typescript-eslint/parser": "^8.53.0", - "autoprefixer": "^10.4.23", - "eslint": "^9.39.2", + "@typescript-eslint/eslint-plugin": "^8.57.0", + "@typescript-eslint/parser": "^8.57.0", + "autoprefixer": "^10.4.27", + "eslint": "^9.39.4", "eslint-import-resolver-typescript": "^3.10.1", "eslint-plugin-import": "^2.32.0", "eslint-plugin-jsx-a11y": "^6.10.2", "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^5.2.0", - "isbot": "^5.1.32", - "postcss": "^8.5.6", + "isbot": "^5.1.36", + "postcss": "^8.5.8", "react": "^18.3.1", "react-dom": "^18.3.1", - "react-icons": "^5.5.0", + "react-icons": "^5.6.0", "react-select": "^5.10.2", "tailwindcss": "^3.4.19", "typescript": "^5.9.3", diff --git a/packages/visual-service/package.json b/packages/visual-service/package.json index f1e02da67..6808d5832 100644 --- a/packages/visual-service/package.json +++ b/packages/visual-service/package.json @@ -28,8 +28,8 @@ "dependencies": { "@wdio/globals": "^9.23.0", "@wdio/logger": "^9.18.0", - "@wdio/types": "^9.20.0", - "expect-webdriverio": "^5.6.1", + "@wdio/types": "^9.25.0", + "expect-webdriverio": "^5.6.4", "@wdio/image-comparison-core": "workspace:*" } } \ No newline at end of file diff --git a/packages/visual-service/src/types.ts b/packages/visual-service/src/types.ts index 6a61544a7..29088027e 100644 --- a/packages/visual-service/src/types.ts +++ b/packages/visual-service/src/types.ts @@ -19,8 +19,9 @@ import type { InternalCheckScreenMethodOptions, InternalCheckElementMethodOptions, InternalCheckFullPageMethodOptions, + ElementIgnore, } from '@wdio/image-comparison-core' -import type { ChainablePromiseElement } from 'webdriverio' +import type { ChainablePromiseElement, ChainablePromiseArray } from 'webdriverio' import type { ContextManager } from './contextManager.js' import type { WaitForStorybookComponentToBeLoaded } from './storybook/Types.js' @@ -82,15 +83,19 @@ export interface WdioIcsScrollOptions extends WdioIcsCommonOptions { hideAfterFirstScroll?: (WebdriverIO.Element | ChainablePromiseElement)[]; } +export interface WdioIcsIgnoreOptions { + ignore?: (ElementIgnore | ElementIgnore[] | WebdriverIO.ElementArray | ChainablePromiseArray)[]; +} + // Save methods export interface WdioSaveScreenMethodOptions extends Omit, WdioIcsCommonOptions {} export interface WdioSaveElementMethodOptions extends Omit, WdioIcsCommonOptions {} export interface WdioSaveFullPageMethodOptions extends Omit, WdioIcsScrollOptions { } // Check methods -export interface WdioCheckScreenMethodOptions extends Omit, WdioIcsCommonOptions {} -export interface WdioCheckElementMethodOptions extends Omit, WdioIcsCommonOptions {} -export interface WdioCheckFullPageMethodOptions extends Omit, WdioIcsScrollOptions {} +export interface WdioCheckScreenMethodOptions extends Omit, WdioIcsCommonOptions, WdioIcsIgnoreOptions {} +export interface WdioCheckElementMethodOptions extends Omit, WdioIcsCommonOptions, WdioIcsIgnoreOptions {} +export interface WdioCheckFullPageMethodOptions extends Omit, WdioIcsScrollOptions, WdioIcsIgnoreOptions {} export interface VisualServiceOptions extends ClassOptions { } diff --git a/packages/visual-service/src/utils.ts b/packages/visual-service/src/utils.ts index 1daddc295..8f73c7acc 100644 --- a/packages/visual-service/src/utils.ts +++ b/packages/visual-service/src/utils.ts @@ -244,7 +244,7 @@ export async function getInstanceData({ const ltOptions = getLtOptions(requestedCapabilities) // @TODO: Figure this one out in the future when we know more about the Appium capabilities from LT // 20241216: LT doesn't have the option to take a ChromeDriver screenshot, so if it's Android it's always native - const nativeWebScreenshot = isAndroid && ltOptions || !!getRequestedAppiumCapability(requestedCapabilities, 'nativeWebScreenshot') + const nativeWebScreenshot = !!(isAndroid && ltOptions) || !!getRequestedAppiumCapability(requestedCapabilities, 'nativeWebScreenshot') const platformVersion = (rawPlatformVersion === undefined || rawPlatformVersion === '') ? NOT_KNOWN : rawPlatformVersion.toLowerCase() const { devicePixelRatio: mobileDevicePixelRatio, diff --git a/packages/visual-service/tests/__snapshots__/utils.test.ts.snap b/packages/visual-service/tests/__snapshots__/utils.test.ts.snap index bb00c2d5a..98513aa6b 100644 --- a/packages/visual-service/tests/__snapshots__/utils.test.ts.snap +++ b/packages/visual-service/tests/__snapshots__/utils.test.ts.snap @@ -571,10 +571,7 @@ exports[`utils > getInstanceData > should return instance data when the lambdate "isMobile": true, "logName": "", "name": "", - "nativeWebScreenshot": { - "deviceName": "Samsung Galaxy S22 LT", - "platformVersion": "11", - }, + "nativeWebScreenshot": true, "platformName": "osx", "platformVersion": "11", } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2bd669cc1..c7ed098b4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,11 +9,11 @@ importers: .: devDependencies: '@changesets/cli': - specifier: ^2.29.8 - version: 2.29.8(@types/node@24.0.14) + specifier: ^2.30.0 + version: 2.30.0(@types/node@24.0.14) '@tsconfig/node20': - specifier: ^20.1.8 - version: 20.1.8 + specifier: ^20.1.9 + version: 20.1.9 '@types/eslint': specifier: ^9.6.1 version: 9.6.1 @@ -30,14 +30,14 @@ importers: specifier: ~0.4.14 version: 0.4.14 '@typescript-eslint/eslint-plugin': - specifier: ^8.53.0 - version: 8.53.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + specifier: ^8.57.0 + version: 8.57.0(@typescript-eslint/parser@8.57.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/parser': - specifier: ^8.53.0 - version: 8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + specifier: ^8.57.0 + version: 8.57.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/utils': - specifier: ^8.53.0 - version: 8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + specifier: ^8.57.0 + version: 8.57.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) '@vitest/coverage-v8': specifier: ^3.2.4 version: 3.2.4(vitest@3.2.4) @@ -45,50 +45,50 @@ importers: specifier: ^3.2.4 version: 3.2.4(vitest@3.2.4) '@wdio/appium-service': - specifier: ^9.23.0 - version: 9.23.0 + specifier: ^9.25.0 + version: 9.25.0 '@wdio/browserstack-service': - specifier: ^9.23.0 - version: 9.23.0(@wdio/cli@9.23.0(@types/node@24.0.14)(expect-webdriverio@5.6.1)) + specifier: ^9.25.0 + version: 9.25.0(@wdio/cli@9.25.0(@types/node@24.0.14)(expect-webdriverio@5.6.4)) '@wdio/cli': - specifier: ^9.23.0 - version: 9.23.0(@types/node@24.0.14)(expect-webdriverio@5.6.1) + specifier: ^9.25.0 + version: 9.25.0(@types/node@24.0.14)(expect-webdriverio@5.6.4) '@wdio/globals': specifier: ^9.23.0 - version: 9.23.0(expect-webdriverio@5.6.1)(webdriverio@9.23.0) + version: 9.23.0(expect-webdriverio@5.6.4)(webdriverio@9.25.0) '@wdio/local-runner': - specifier: ^9.23.0 - version: 9.23.0(@wdio/globals@9.23.0)(webdriverio@9.23.0) + specifier: ^9.25.0 + version: 9.25.0(@wdio/globals@9.23.0)(webdriverio@9.25.0) '@wdio/mocha-framework': - specifier: ^9.23.0 - version: 9.23.0 + specifier: ^9.25.0 + version: 9.25.0 '@wdio/sauce-service': - specifier: ^9.23.0 - version: 9.23.0 + specifier: ^9.25.0 + version: 9.25.0 '@wdio/shared-store-service': - specifier: ^9.23.0 - version: 9.23.0 + specifier: ^9.25.0 + version: 9.25.0 '@wdio/spec-reporter': - specifier: ^9.20.0 - version: 9.20.0 + specifier: ^9.25.0 + version: 9.25.0 '@wdio/types': - specifier: ^9.20.0 - version: 9.20.0 + specifier: ^9.25.0 + version: 9.25.0 cross-env: specifier: ^7.0.3 version: 7.0.3 eslint: - specifier: ^9.39.2 - version: 9.39.2(jiti@2.6.1) + specifier: ^9.39.4 + version: 9.39.4(jiti@2.6.1) eslint-plugin-import: specifier: ^2.32.0 - version: 2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)) + version: 2.32.0(@typescript-eslint/parser@8.57.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)) eslint-plugin-unicorn: specifier: ^56.0.1 - version: 56.0.1(eslint@9.39.2(jiti@2.6.1)) + version: 56.0.1(eslint@9.39.4(jiti@2.6.1)) eslint-plugin-wdio: - specifier: ^9.23.0 - version: 9.23.0 + specifier: ^9.25.0 + version: 9.25.0(eslint@9.39.4(jiti@2.6.1))(globals@15.15.0) husky: specifier: ^9.1.7 version: 9.1.7 @@ -99,11 +99,11 @@ importers: specifier: ^8.0.4 version: 8.0.4 release-it: - specifier: ^19.2.3 - version: 19.2.3(@types/node@24.0.14)(magicast@0.3.5) + specifier: ^19.2.4 + version: 19.2.4(@types/node@24.0.14)(magicast@0.3.5) rimraf: - specifier: ^6.1.2 - version: 6.1.2 + specifier: ^6.1.3 + version: 6.1.3 saucelabs: specifier: ^9.0.2 version: 9.0.2 @@ -118,10 +118,10 @@ importers: version: 3.2.4(@types/debug@4.1.12)(@types/node@24.0.14)(@vitest/ui@3.2.4)(jsdom@26.1.0) wdio-lambdatest-service: specifier: ^4.0.1 - version: 4.0.1(@wdio/cli@9.23.0(@types/node@24.0.14)(expect-webdriverio@5.6.1))(@wdio/types@9.20.0)(webdriverio@9.23.0) + version: 4.0.1(@wdio/cli@9.25.0(@types/node@24.0.14)(expect-webdriverio@5.6.4))(@wdio/types@9.25.0)(webdriverio@9.25.0) webdriverio: - specifier: ^9.23.0 - version: 9.23.0 + specifier: ^9.25.0 + version: 9.25.0 packages/image-comparison-core: dependencies: @@ -129,15 +129,15 @@ importers: specifier: ^9.18.0 version: 9.18.0 '@wdio/types': - specifier: ^9.20.0 - version: 9.20.0 + specifier: ^9.25.0 + version: 9.25.0 jimp: specifier: ^1.6.0 version: 1.6.0 devDependencies: webdriverio: - specifier: ^9.23.0 - version: 9.23.0 + specifier: ^9.25.0 + version: 9.25.0 packages/ocr-service: dependencies: @@ -146,13 +146,13 @@ importers: version: 7.10.1(@types/node@24.10.1) '@wdio/globals': specifier: ^9.23.0 - version: 9.23.0(expect-webdriverio@5.6.1)(webdriverio@9.23.0) + version: 9.23.0(expect-webdriverio@5.6.4)(webdriverio@9.25.0) '@wdio/logger': specifier: ^9.18.0 version: 9.18.0 '@wdio/types': - specifier: ^9.20.0 - version: 9.20.0 + specifier: ^9.25.0 + version: 9.25.0 fuse.js: specifier: ^7.1.0 version: 7.1.0 @@ -204,44 +204,44 @@ importers: specifier: ^2.17.4 version: 2.17.4(typescript@5.9.3) '@types/react': - specifier: ^18.3.27 - version: 18.3.27 + specifier: ^18.3.28 + version: 18.3.28 '@types/react-dom': specifier: ^18.3.7 - version: 18.3.7(@types/react@18.3.27) + version: 18.3.7(@types/react@18.3.28) '@typescript-eslint/eslint-plugin': - specifier: ^8.53.0 - version: 8.53.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + specifier: ^8.57.0 + version: 8.57.0(@typescript-eslint/parser@8.57.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/parser': - specifier: ^8.53.0 - version: 8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + specifier: ^8.57.0 + version: 8.57.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) autoprefixer: - specifier: ^10.4.23 - version: 10.4.23(postcss@8.5.6) + specifier: ^10.4.27 + version: 10.4.27(postcss@8.5.8) eslint: - specifier: ^9.39.2 - version: 9.39.2(jiti@2.6.1) + specifier: ^9.39.4 + version: 9.39.4(jiti@2.6.1) eslint-import-resolver-typescript: specifier: ^3.10.1 - version: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1)) + version: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.6.1)) eslint-plugin-import: specifier: ^2.32.0 - version: 2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)) + version: 2.32.0(@typescript-eslint/parser@8.57.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)) eslint-plugin-jsx-a11y: specifier: ^6.10.2 - version: 6.10.2(eslint@9.39.2(jiti@2.6.1)) + version: 6.10.2(eslint@9.39.4(jiti@2.6.1)) eslint-plugin-react: specifier: ^7.37.5 - version: 7.37.5(eslint@9.39.2(jiti@2.6.1)) + version: 7.37.5(eslint@9.39.4(jiti@2.6.1)) eslint-plugin-react-hooks: specifier: ^5.2.0 - version: 5.2.0(eslint@9.39.2(jiti@2.6.1)) + version: 5.2.0(eslint@9.39.4(jiti@2.6.1)) isbot: - specifier: ^5.1.32 - version: 5.1.32 + specifier: ^5.1.36 + version: 5.1.36 postcss: - specifier: ^8.5.6 - version: 8.5.6 + specifier: ^8.5.8 + version: 8.5.8 react: specifier: ^18.3.1 version: 18.3.1 @@ -249,11 +249,11 @@ importers: specifier: ^18.3.1 version: 18.3.1(react@18.3.1) react-icons: - specifier: ^5.5.0 - version: 5.5.0(react@18.3.1) + specifier: ^5.6.0 + version: 5.6.0(react@18.3.1) react-select: specifier: ^5.10.2 - version: 5.10.2(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 5.10.2(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) tailwindcss: specifier: ^3.4.19 version: 3.4.19(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) @@ -271,7 +271,7 @@ importers: dependencies: '@wdio/globals': specifier: ^9.23.0 - version: 9.23.0(expect-webdriverio@5.6.1)(webdriverio@9.23.0) + version: 9.23.0(expect-webdriverio@5.6.4)(webdriverio@9.25.0) '@wdio/image-comparison-core': specifier: workspace:* version: link:../image-comparison-core @@ -279,11 +279,11 @@ importers: specifier: ^9.18.0 version: 9.18.0 '@wdio/types': - specifier: ^9.20.0 - version: 9.20.0 + specifier: ^9.25.0 + version: 9.25.0 expect-webdriverio: - specifier: ^5.6.1 - version: 5.6.1(@wdio/globals@9.23.0)(@wdio/logger@9.18.0)(webdriverio@9.23.0) + specifier: ^5.6.4 + version: 5.6.4(@wdio/globals@9.23.0)(@wdio/logger@9.18.0)(webdriverio@9.25.0) packages: @@ -459,8 +459,8 @@ packages: '@bufbuild/protobuf@2.10.2': resolution: {integrity: sha512-uFsRXwIGyu+r6AMdz+XijIIZJYpoWeYzILt5yZ2d3mCjQrWUTVpVD9WL/jZAbvp+Ed04rOhrsk7FiTcEDseB5A==} - '@changesets/apply-release-plan@7.0.14': - resolution: {integrity: sha512-ddBvf9PHdy2YY0OUiEl3TV78mH9sckndJR14QAt87KLEbIov81XO0q0QAmvooBxXlqRRP8I9B7XOzZwQG7JkWA==} + '@changesets/apply-release-plan@7.1.0': + resolution: {integrity: sha512-yq8ML3YS7koKQ/9bk1PqO0HMzApIFNwjlwCnwFEXMzNe8NpzeeYYKCmnhWJGkN8g7E51MnWaSbqRcTcdIxUgnQ==} '@changesets/assemble-release-plan@6.0.9': resolution: {integrity: sha512-tPgeeqCHIwNo8sypKlS3gOPmsS3wP0zHt67JDuL20P4QcXiw/O4Hl7oXiuLnP9yg+rXLQ2sScdV1Kkzde61iSQ==} @@ -468,12 +468,12 @@ packages: '@changesets/changelog-git@0.2.1': resolution: {integrity: sha512-x/xEleCFLH28c3bQeQIyeZf8lFXyDFVn1SgcBiR2Tw/r4IAWlk1fzxCEZ6NxQAjF2Nwtczoen3OA2qR+UawQ8Q==} - '@changesets/cli@2.29.8': - resolution: {integrity: sha512-1weuGZpP63YWUYjay/E84qqwcnt5yJMM0tep10Up7Q5cS/DGe2IZ0Uj3HNMxGhCINZuR7aO9WBMdKnPit5ZDPA==} + '@changesets/cli@2.30.0': + resolution: {integrity: sha512-5D3Nk2JPqMI1wK25pEymeWRSlSMdo5QOGlyfrKg0AOufrUcjEE3RQgaCpHoBiM31CSNrtSgdJ0U6zL1rLDDfBA==} hasBin: true - '@changesets/config@3.1.2': - resolution: {integrity: sha512-CYiRhA4bWKemdYi/uwImjPxqWNpqGPNbEBdX1BdONALFIDK7MCUj6FPkzD+z9gJcvDFUQJn9aDVf4UG7OT6Kog==} + '@changesets/config@3.1.3': + resolution: {integrity: sha512-vnXjcey8YgBn2L1OPWd3ORs0bGC4LoYcK/ubpgvzNVr53JXV5GiTVj7fWdMRsoKUH7hhhMAQnsJUqLr21EncNw==} '@changesets/errors@0.2.0': resolution: {integrity: sha512-6BLOQUscTpZeGljvyQXlWOItQyU71kCdGz7Pi8H8zdw6BI0g3m43iL4xKUVPWtG+qrrL9DTjpdn8eYuCQSRpow==} @@ -481,8 +481,8 @@ packages: '@changesets/get-dependents-graph@2.1.3': resolution: {integrity: sha512-gphr+v0mv2I3Oxt19VdWRRUxq3sseyUpX9DaHpTUmLj92Y10AGy+XOtV+kbM6L/fDcpx7/ISDFK6T8A/P3lOdQ==} - '@changesets/get-release-plan@4.0.14': - resolution: {integrity: sha512-yjZMHpUHgl4Xl5gRlolVuxDkm4HgSJqT93Ri1Uz8kGrQb+5iJ8dkXJ20M2j/Y4iV5QzS2c5SeTxVSKX+2eMI0g==} + '@changesets/get-release-plan@4.0.15': + resolution: {integrity: sha512-Q04ZaRPuEVZtA+auOYgFaVQQSA98dXiVe/yFaZfY7hoSmQICHGvP0TF4u3EDNHWmmCS4ekA/XSpKlSM2PyTS2g==} '@changesets/get-version-range-type@0.4.0': resolution: {integrity: sha512-hwawtob9DryoGTpixy1D3ZXbGgJu1Rhr+ySH2PvTLHvkZuQ7sRT4oQwMh0hbqZH1weAooedEjRsbrWcGLCeyVQ==} @@ -493,14 +493,14 @@ packages: '@changesets/logger@0.1.1': resolution: {integrity: sha512-OQtR36ZlnuTxKqoW4Sv6x5YIhOmClRd5pWsjZsddYxpWs517R0HkyiefQPIytCVh4ZcC5x9XaG8KTdd5iRQUfg==} - '@changesets/parse@0.4.2': - resolution: {integrity: sha512-Uo5MC5mfg4OM0jU3up66fmSn6/NE9INK+8/Vn/7sMVcdWg46zfbvvUSjD9EMonVqPi9fbrJH9SXHn48Tr1f2yA==} + '@changesets/parse@0.4.3': + resolution: {integrity: sha512-ZDmNc53+dXdWEv7fqIUSgRQOLYoUom5Z40gmLgmATmYR9NbL6FJJHwakcCpzaeCy+1D0m0n7mT4jj2B/MQPl7A==} '@changesets/pre@2.0.2': resolution: {integrity: sha512-HaL/gEyFVvkf9KFg6484wR9s0qjAXlZ8qWPDkTyKF6+zqjBe/I2mygg3MbpZ++hdi0ToqNUF8cjj7fBy0dg8Ug==} - '@changesets/read@0.6.6': - resolution: {integrity: sha512-P5QaN9hJSQQKJShzzpBT13FzOSPyHbqdoIBUd2DJdgvnECCyO6LmAOWSV+O8se2TaZJVwSXjL+v9yhb+a9JeJg==} + '@changesets/read@0.6.7': + resolution: {integrity: sha512-D1G4AUYGrBEk8vj8MGwf75k9GpN6XL3wg8i42P2jZZwFLXnlr2Pn7r9yuQNbaMCarP7ZQWNJbV6XLeysAIMhTA==} '@changesets/should-skip-package@0.1.2': resolution: {integrity: sha512-qAK/WrqWLNCP22UDdBTMPH5f41elVDlsNyat180A33dWxuUDyNpg6fPi/FyTZwRriVjg0L8gnjJn2F9XAoF0qw==} @@ -1038,28 +1038,18 @@ packages: peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - '@eslint-community/eslint-utils@4.9.0': - resolution: {integrity: sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - '@eslint-community/eslint-utils@4.9.1': resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - '@eslint-community/regexpp@4.12.1': - resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} - engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - '@eslint-community/regexpp@4.12.2': resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - '@eslint/config-array@0.21.1': - resolution: {integrity: sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==} + '@eslint/config-array@0.21.2': + resolution: {integrity: sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/config-helpers@0.4.2': @@ -1070,12 +1060,12 @@ packages: resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/eslintrc@3.3.1': - resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==} + '@eslint/eslintrc@3.3.5': + resolution: {integrity: sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/js@9.39.2': - resolution: {integrity: sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==} + '@eslint/js@9.39.4': + resolution: {integrity: sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/object-schema@2.1.7': @@ -1403,6 +1393,14 @@ packages: resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} + '@isaacs/cliui@9.0.0': + resolution: {integrity: sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==} + engines: {node: '>=18'} + + '@isaacs/fs-minipass@4.0.1': + resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} + engines: {node: '>=18.0.0'} + '@istanbuljs/schema@0.1.3': resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} engines: {node: '>=8'} @@ -1987,8 +1985,8 @@ packages: '@tsconfig/node16@1.0.4': resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} - '@tsconfig/node20@20.1.8': - resolution: {integrity: sha512-Em+IdPfByIzWRRpqWL4Z7ArLHZGxmc36BxE3jCz9nBFSm+5aLaPMZyjwu4yetvyKXeogWcxik4L1jB5JTWfw7A==} + '@tsconfig/node20@20.1.9': + resolution: {integrity: sha512-IjlTv1RsvnPtUcjTqtVsZExKVq+KQx4g5pCP5tI7rAs6Xesl2qFwSz/tPDBC4JajkL/MlezBu3gPUwqRHl+RIg==} '@tybys/wasm-util@0.9.0': resolution: {integrity: sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==} @@ -2071,9 +2069,6 @@ packages: '@types/node@16.9.1': resolution: {integrity: sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g==} - '@types/node@20.19.17': - resolution: {integrity: sha512-gfehUI8N1z92kygssiuWvLiwcbOB3IRktR6hTDgJlXMYh5OvkPSRmgfoBUmfZt+vhwJtX7v1Yw4KvvAf7c5QKQ==} - '@types/node@20.19.25': resolution: {integrity: sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ==} @@ -2109,8 +2104,8 @@ packages: peerDependencies: '@types/react': '*' - '@types/react@18.3.27': - resolution: {integrity: sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==} + '@types/react@18.3.28': + resolution: {integrity: sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==} '@types/responselike@1.0.3': resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==} @@ -2151,63 +2146,63 @@ packages: '@types/yauzl@2.10.3': resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} - '@typescript-eslint/eslint-plugin@8.53.0': - resolution: {integrity: sha512-eEXsVvLPu8Z4PkFibtuFJLJOTAV/nPdgtSjkGoPpddpFk3/ym2oy97jynY6ic2m6+nc5M8SE1e9v/mHKsulcJg==} + '@typescript-eslint/eslint-plugin@8.57.0': + resolution: {integrity: sha512-qeu4rTHR3/IaFORbD16gmjq9+rEs9fGKdX0kF6BKSfi+gCuG3RCKLlSBYzn/bGsY9Tj7KE/DAQStbp8AHJGHEQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.53.0 - eslint: ^8.57.0 || ^9.0.0 + '@typescript-eslint/parser': ^8.57.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/parser@8.53.0': - resolution: {integrity: sha512-npiaib8XzbjtzS2N4HlqPvlpxpmZ14FjSJrteZpPxGUaYPlvhzlzUZ4mZyABo0EFrOWnvyd0Xxroq//hKhtAWg==} + '@typescript-eslint/parser@8.57.0': + resolution: {integrity: sha512-XZzOmihLIr8AD1b9hL9ccNMzEMWt/dE2u7NyTY9jJG6YNiNthaD5XtUHVF2uCXZ15ng+z2hT3MVuxnUYhq6k1g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - eslint: ^8.57.0 || ^9.0.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/project-service@8.53.0': - resolution: {integrity: sha512-Bl6Gdr7NqkqIP5yP9z1JU///Nmes4Eose6L1HwpuVHwScgDPPuEWbUVhvlZmb8hy0vX9syLk5EGNL700WcBlbg==} + '@typescript-eslint/project-service@8.57.0': + resolution: {integrity: sha512-pR+dK0BlxCLxtWfaKQWtYr7MhKmzqZxuii+ZjuFlZlIGRZm22HnXFqa2eY+90MUz8/i80YJmzFGDUsi8dMOV5w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/scope-manager@8.53.0': - resolution: {integrity: sha512-kWNj3l01eOGSdVBnfAF2K1BTh06WS0Yet6JUgb9Cmkqaz3Jlu0fdVUjj9UI8gPidBWSMqDIglmEXifSgDT/D0g==} + '@typescript-eslint/scope-manager@8.57.0': + resolution: {integrity: sha512-nvExQqAHF01lUM66MskSaZulpPL5pgy5hI5RfrxviLgzZVffB5yYzw27uK/ft8QnKXI2X0LBrHJFr1TaZtAibw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.53.0': - resolution: {integrity: sha512-K6Sc0R5GIG6dNoPdOooQ+KtvT5KCKAvTcY8h2rIuul19vxH5OTQk7ArKkd4yTzkw66WnNY0kPPzzcmWA+XRmiA==} + '@typescript-eslint/tsconfig-utils@8.57.0': + resolution: {integrity: sha512-LtXRihc5ytjJIQEH+xqjB0+YgsV4/tW35XKX3GTZHpWtcC8SPkT/d4tqdf1cKtesryHm2bgp6l555NYcT2NLvA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/type-utils@8.53.0': - resolution: {integrity: sha512-BBAUhlx7g4SmcLhn8cnbxoxtmS7hcq39xKCgiutL3oNx1TaIp+cny51s8ewnKMpVUKQUGb41RAUWZ9kxYdovuw==} + '@typescript-eslint/type-utils@8.57.0': + resolution: {integrity: sha512-yjgh7gmDcJ1+TcEg8x3uWQmn8ifvSupnPfjP21twPKrDP/pTHlEQgmKcitzF/rzPSmv7QjJ90vRpN4U+zoUjwQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - eslint: ^8.57.0 || ^9.0.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/types@8.53.0': - resolution: {integrity: sha512-Bmh9KX31Vlxa13+PqPvt4RzKRN1XORYSLlAE+sO1i28NkisGbTtSLFVB3l7PWdHtR3E0mVMuC7JilWJ99m2HxQ==} + '@typescript-eslint/types@8.57.0': + resolution: {integrity: sha512-dTLI8PEXhjUC7B9Kre+u0XznO696BhXcTlOn0/6kf1fHaQW8+VjJAVHJ3eTI14ZapTxdkOmc80HblPQLaEeJdg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.53.0': - resolution: {integrity: sha512-pw0c0Gdo7Z4xOG987u3nJ8akL9093yEEKv8QTJ+Bhkghj1xyj8cgPaavlr9rq8h7+s6plUJ4QJYw2gCZodqmGw==} + '@typescript-eslint/typescript-estree@8.57.0': + resolution: {integrity: sha512-m7faHcyVg0BT3VdYTlX8GdJEM7COexXxS6KqGopxdtkQRvBanK377QDHr4W/vIPAR+ah9+B/RclSW5ldVniO1Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/utils@8.53.0': - resolution: {integrity: sha512-XDY4mXTez3Z1iRDI5mbRhH4DFSt46oaIFsLg+Zn97+sYrXACziXSQcSelMybnVZ5pa1P6xYkPr5cMJyunM1ZDA==} + '@typescript-eslint/utils@8.57.0': + resolution: {integrity: sha512-5iIHvpD3CZe06riAsbNxxreP+MuYgVUsV0n4bwLH//VJmgtt54sQeY2GszntJ4BjYCpMzrfVh2SBnUQTtys2lQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - eslint: ^8.57.0 || ^9.0.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/visitor-keys@8.53.0': - resolution: {integrity: sha512-LZ2NqIHFhvFwxG0qZeLL9DvdNAHPGCY5dIRwBhyYeU+LfLhcStE1ImjsuTG/WaVh3XysGaeLW8Rqq7cGkPCFvw==} + '@typescript-eslint/visitor-keys@8.57.0': + resolution: {integrity: sha512-zm6xx8UT/Xy2oSr2ZXD0pZo7Jx2XsCoID2IUh9YSTFRu7z+WdwYTRk6LhUftm1crwqbuoF6I8zAFeCMw0YjwDg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@unrs/resolver-binding-darwin-arm64@1.7.11': @@ -2362,28 +2357,28 @@ packages: '@vitest/utils@3.2.4': resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} - '@wdio/appium-service@9.23.0': - resolution: {integrity: sha512-Enfg/oBwYBS3M6ytFoCcMThLAYnH5+CDoL4xlxoryD1MQJlZytag11DjRNXmqyM4xLpp3JQ0NIx8F5xe/KNZBA==} + '@wdio/appium-service@9.25.0': + resolution: {integrity: sha512-9i3PvBAENPDIzQxAYi7WovkN71fkwntVGp0zykek5/3lXybadzFzFwW0pgysp2F85iXkzoiLOsDw7QP9XnpWCA==} engines: {node: '>=18.20.0'} hasBin: true - '@wdio/browserstack-service@9.23.0': - resolution: {integrity: sha512-V/NUVDm28hGjL59ub5nNtPElM3hhN+WWdVn42eTvq9VCPYwGXbGfsBt2atYMGk8sH6xnMucpVoXZbuLkhN5J+A==} + '@wdio/browserstack-service@9.25.0': + resolution: {integrity: sha512-5M0m5FeUZM9ILbH5wmr+5aCpiE1jBRTzyLvS7LQBPWf+uQJm6moa2PlL2cJ8G680DRvLFmTiiRjhW2tvP14VLQ==} engines: {node: '>=18.20.0'} peerDependencies: '@wdio/cli': ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 - '@wdio/cli@9.23.0': - resolution: {integrity: sha512-jVuyZ84Ino6akBlmf38/bc1Ji+DI3NoGvWATQXhKaDmmF7tAhSdlUXK3VV970GfVKvGe1ARPaGtTf8L2lbRDSw==} + '@wdio/cli@9.25.0': + resolution: {integrity: sha512-7m6Au8U0Fz+/n6vlchxHd5ogYESjz2XEYVnmcqnDapCAUo570Ilb86Muz69AFrBiZ6D+xkesQTwlWOdNRjIY1w==} engines: {node: '>=18.20.0'} hasBin: true - '@wdio/config@9.23.0': - resolution: {integrity: sha512-hhtngUG2uCxYmScSEor+k22EVlsTW3ARXgke8NPVeQA4p1+GC2CvRZi4P7nmhRTZubgLrENYYsveFcYR+1UXhQ==} + '@wdio/config@9.25.0': + resolution: {integrity: sha512-EWa7l1rrbSNthCRDpdBw7ESAa1/jAjSsWCGkaVAO0HMOGlQjzvYI6gNi4KUeymnurDZ2IPr0jr+f9We6AWi6QA==} engines: {node: '>=18.20.0'} - '@wdio/dot-reporter@9.20.0': - resolution: {integrity: sha512-lRhihDQ56dApJcKOIEkVHThl8t2e5h7f3FW3JVmMLcGgbbkkLgXqVWPpbEGJcLld3wL4CipAPojVE/YEWp80hw==} + '@wdio/dot-reporter@9.25.0': + resolution: {integrity: sha512-yFlyHfCJOERWIgiFzbyliCr6YxEYZDM3rykCHmpIlbAOwATxjIh26fvkKI8/23LDqMybym8Pn/Yjj/W78/KgIg==} engines: {node: '>=18.20.0'} '@wdio/globals@9.23.0': @@ -2393,8 +2388,8 @@ packages: expect-webdriverio: ^5.3.4 webdriverio: ^9.0.0 - '@wdio/local-runner@9.23.0': - resolution: {integrity: sha512-kBWIqBDbCAJuxENl4t1qiCf8mivHN++cNdgsmlkP8nG7KJ8ebCseqsBHTrvx/YAqRPZIBD50cN6xsB6MZTmUfg==} + '@wdio/local-runner@9.25.0': + resolution: {integrity: sha512-E6pEeQouVLle19Gk55Y/JxPGMzT2jhmHS80yjT3g481Q1EML9y8m3T6UMemAUnz70FGneP0aCcuFKJdmkF9dKw==} engines: {node: '>=18.20.0'} '@wdio/logger@7.26.0': @@ -2405,63 +2400,63 @@ packages: resolution: {integrity: sha512-HdzDrRs+ywAqbXGKqe1i/bLtCv47plz4TvsHFH3j729OooT5VH38ctFn5aLXgECmiAKDkmH/A6kOq2Zh5DIxww==} engines: {node: '>=18.20.0'} - '@wdio/mocha-framework@9.23.0': - resolution: {integrity: sha512-1Lg8MCLNvs4a1pwz6WzWDPS44mxdAJQCw19DqWuEI8b406HtdIcPoc6sBsqkXVW8aNxMkqvTf87aMeLBFFbaYA==} + '@wdio/mocha-framework@9.25.0': + resolution: {integrity: sha512-30qPP7w0UGG49LB3nAMxVf9Q9GA8q/NeDPHjBsXCjpSnO0AswtjFktb91hkqQqssrlGMQV+QyQuBD+kMxkhrDQ==} engines: {node: '>=18.20.0'} - '@wdio/protocols@9.16.2': - resolution: {integrity: sha512-h3k97/lzmyw5MowqceAuY3HX/wGJojXHkiPXA3WlhGPCaa2h4+GovV2nJtRvknCKsE7UHA1xB5SWeI8MzloBew==} + '@wdio/protocols@9.25.0': + resolution: {integrity: sha512-PErbZqdpFmE69bRuku3OR34Ro2xuZNNLXYFOcJnjXJVzf5+ApDyGHYrMlvhtrrSy9/55LUybk851ppjS+3RoDA==} '@wdio/repl@9.16.2': resolution: {integrity: sha512-FLTF0VL6+o5BSTCO7yLSXocm3kUnu31zYwzdsz4n9s5YWt83sCtzGZlZpt7TaTzb3jVUfxuHNQDTb8UMkCu0lQ==} engines: {node: '>=18.20.0'} - '@wdio/reporter@9.20.0': - resolution: {integrity: sha512-HjKJzm8o0MCcnwGVGprzaCAyau0OB8mWHwH1ZI/ka+z1nmVBr2tsr7H53SdHsGIhAg/XuZObobqdzeVF63ApeA==} + '@wdio/reporter@9.25.0': + resolution: {integrity: sha512-zmyjr7/EoGwlmrICNzhRL3k0dlJoqdQShzHd5l8V1axYsaC3UHGy2oNDXwKD/OjhEThJsGHxwjyUDkKYhbZdCw==} engines: {node: '>=18.20.0'} - '@wdio/runner@9.23.0': - resolution: {integrity: sha512-a2afdICcEzzMjSPCwY3g9Hl2kWXXjBFyWv5DxvjaJOmQygnKzz9olFOrpVotgLKXE9ZLuJ4EP98or69sFIeLBg==} + '@wdio/runner@9.25.0': + resolution: {integrity: sha512-Oe7NnFWJICF5g+LGLVWi3x41aL2ZRto3bOQZlBjNnGFRkx+BPz7qHdENueflfh7VCX9o3qRns6cQ3CEuurLaNg==} engines: {node: '>=18.20.0'} peerDependencies: expect-webdriverio: ^5.3.4 webdriverio: ^9.0.0 - '@wdio/sauce-service@9.23.0': - resolution: {integrity: sha512-gN1QVKG7vKKNmXAxKW3mqXbSajY2CSevRdocJbt4ca4P+sQymshNtHWd6JRwko4WAXeMW/KwFgm4CXqCPgbvIQ==} + '@wdio/sauce-service@9.25.0': + resolution: {integrity: sha512-ExrHdpSnQlbchcvhgcT1G6fvzxpDqSAB+P2WnvMdlpw5AY3+HC6eQqW0kllN/Uo90P51eirgHDhIpWg6dy9KMQ==} engines: {node: '>=18.20.0'} - '@wdio/shared-store-service@9.23.0': - resolution: {integrity: sha512-MmYY5p54fxJFuZP08P5v+Md3oewD3zQ0ExLj1TzReZY89nHsHvIjOGP7tOMYmGwtnhW5bg8pvxTAqEZ0uPw87g==} + '@wdio/shared-store-service@9.25.0': + resolution: {integrity: sha512-LuSxqxJKMbFf5+BiOEX//7CpajRIu68zQrgwtavIfxKfcaTCfaNCuVyE3PDhKix5LJv6L750RWrkOKBOuAMPdA==} engines: {node: '>=18.20.0'} - '@wdio/spec-reporter@9.20.0': - resolution: {integrity: sha512-YHj3kF86RoOVVR+k3eb+e/Fki6Mq1FIrJQ380Cz5SSWbIc9gL8HXG3ydReldY6/80KLFOuHn9ZHvDHrCIXRjiw==} + '@wdio/spec-reporter@9.25.0': + resolution: {integrity: sha512-15+YnhnXDW7dAJ4PP+qZ2imAbVcMMSwewtjVrKRWK0OsMASXRXka/zV3jViRp1Rf+WCGp76HMxhv7YOygfE68A==} engines: {node: '>=18.20.0'} - '@wdio/types@9.20.0': - resolution: {integrity: sha512-zMmAtse2UMCSOW76mvK3OejauAdcFGuKopNRH7crI0gwKTZtvV89yXWRziz9cVXpFgfmJCjf9edxKFWdhuF5yw==} + '@wdio/types@9.25.0': + resolution: {integrity: sha512-ovSEcUBLz6gVDIsBZYKQXz8EGU37jS8sqbmlOe5+jB4XbsTBCyTLjQK/rO7LWQAKJcs0vBq+Pd+VrlsFtA7tTQ==} engines: {node: '>=18.20.0'} - '@wdio/utils@9.23.0': - resolution: {integrity: sha512-WhXuVSxEvPw/i34bL1aCHAOi+4g29kRkIMyBShNSxH+Shxh2G91RJYsXm4IAiPMGcC4H6G8T2VcbZ32qnGPm5Q==} + '@wdio/utils@9.25.0': + resolution: {integrity: sha512-w/ej8gZkc2tZr8L91ATyA1AWrbPDYDOvblQ7r+zt1uPRobuA4H98GME7Zm7i3FIP695BvV4G35Gcs5NssZW1pw==} engines: {node: '>=18.20.0'} - '@wdio/xvfb@9.20.0': - resolution: {integrity: sha512-shllZH9CsLiZqTXkqBTJrwi6k/ajBE7/78fQgvafMUIQU1Hpb2RdsmydKfPFZ5NDoA+LNm67PD2cPkvkXy4pSw==} + '@wdio/xvfb@9.25.0': + resolution: {integrity: sha512-qbsdWm1sP5CGikz3n3dwoVGqbRyBsERGzckDMsQeQ9QVTG6OsNOm4KiVejdiwdPXqDjLUnBv8xGtfuFrftFwcA==} engines: {node: '>=18'} '@web3-storage/multipart-parser@1.0.0': resolution: {integrity: sha512-BEO6al7BYqcnfX15W2cnGR+Q566ACXAT9UQykORCWW80lmkpWsnEob6zJS1ZVBKsSJC8+7vJkHwlp+lXG1UCdw==} + '@xmldom/xmldom@0.9.8': + resolution: {integrity: sha512-p96FSY54r+WJ50FIOsCOjyj/wavs8921hG5+kVMmZgKcvIKxMXHTrjNJvRgWa/zuX3B6t2lijLNFaOyuxUH+2A==} + engines: {node: '>=14.6'} + '@zip.js/zip.js@2.8.15': resolution: {integrity: sha512-HZKJLFe4eGVgCe9J87PnijY7T1Zn638bEHS+Fm/ygHZozRpefzWcOYfPaP52S8pqk9g4xN3+LzMDl3Lv9dLglA==} engines: {bun: '>=0.7.0', deno: '>=1.0.0', node: '>=18.0.0'} - '@zip.js/zip.js@2.8.7': - resolution: {integrity: sha512-8daf29EMM3gUpH/vSBSCYo2bY/wbamgRPxPpE2b+cDnbOLBHAcZikWad79R4Guemth/qtipzEHrZMq1lFXxWIA==} - engines: {bun: '>=0.7.0', deno: '>=1.0.0', node: '>=18.0.0'} - '@zxing/text-encoding@0.9.0': resolution: {integrity: sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==} @@ -2512,8 +2507,8 @@ packages: resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} engines: {node: '>=8'} - ajv@6.12.6: - resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + ajv@6.14.0: + resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==} ansi-colors@4.1.3: resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} @@ -2655,8 +2650,8 @@ packages: asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} - autoprefixer@10.4.23: - resolution: {integrity: sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA==} + autoprefixer@10.4.27: + resolution: {integrity: sha512-NP9APE+tO+LuJGn7/9+cohklunJsXWiaWEfV3si4Gi/XHDwVNgkwr1J3RQYFIvPy76GmJ9/bW8vyoU1LcxwKHA==} engines: {node: ^10 || ^12 || >=14} hasBin: true peerDependencies: @@ -2699,6 +2694,10 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + bare-events@2.7.0: resolution: {integrity: sha512-b3N5eTW1g7vXkw+0CXh/HazGTcO5KYuu/RCNaJbDMPI6LHDi+7qe8EmxKUVe1sUbY2KZOVZFyj62x0OEz9qyAA==} @@ -2735,10 +2734,6 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - baseline-browser-mapping@2.8.30: - resolution: {integrity: sha512-aTUKW4ptQhS64+v2d6IkPzymEzzhw+G0bA1g3uBRV3+ntkH+svttKseW5IOR4Ed6NUVKqnY7qT3dKvzQ7io4AA==} - hasBin: true - baseline-browser-mapping@2.9.14: resolution: {integrity: sha512-B0xUquLkiGLgHhpPBqvl7GWegWBUNuujQ6kXd/r1U38ElPT6Ok8KZ8e+FpUGEc2ZoRQUzq/aUnaKFc/svWUGSg==} hasBin: true @@ -2750,6 +2745,7 @@ packages: basic-ftp@5.0.5: resolution: {integrity: sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==} engines: {node: '>=10.0.0'} + deprecated: Security vulnerability fixed in 5.2.0, please upgrade before-after-hook@4.0.0: resolution: {integrity: sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==} @@ -2787,6 +2783,10 @@ packages: brace-expansion@2.0.2: resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + brace-expansion@5.0.4: + resolution: {integrity: sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==} + engines: {node: 18 || 20 || >=22} + braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} @@ -2802,11 +2802,6 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true - browserslist@4.28.0: - resolution: {integrity: sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==} - engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} - hasBin: true - browserslist@4.28.1: resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} @@ -2906,11 +2901,8 @@ packages: caniuse-lite@1.0.30001721: resolution: {integrity: sha512-cOuvmUVtKrtEaoKiO0rSc29jcjwMwX5tOHDy4MgVFEWiUXj4uBMJkwI8MDySkgXidpMiHUcviogAvFi4pA2hDQ==} - caniuse-lite@1.0.30001756: - resolution: {integrity: sha512-4HnCNKbMLkLdhJz3TToeVWHSnfJvPaq6vu/eRP0Ahub/07n484XHhBF5AJoSGHdVrS8tKFauUQz8Bp9P7LVx7A==} - - caniuse-lite@1.0.30001764: - resolution: {integrity: sha512-9JGuzl2M+vPL+pz70gtMF9sHdMFbY9FJaQBi186cHKH3pSzDvzoUJUPV6fqiKIMyXbud9ZLg4F3Yza1vJ1+93g==} + caniuse-lite@1.0.30001778: + resolution: {integrity: sha512-PN7uxFL+ExFJO61aVmP1aIEG4i9whQd4eoSCebav62UwDyp5OHh06zN4jqKSMePVgxHifCw1QJxdRkA1Pisekg==} capital-case@1.0.4: resolution: {integrity: sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==} @@ -2985,9 +2977,9 @@ packages: resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} engines: {node: '>=10'} - ci-info@3.9.0: - resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} - engines: {node: '>=8'} + chownr@3.0.0: + resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} + engines: {node: '>=18'} ci-info@4.2.0: resolution: {integrity: sha512-cYY9mypksY8NRqgDB1XD1RiJL338v/551niynFTGkZOO2LHuB2OmOYxDIe/ttN9AHwrqdum1360G3ald0W9kCg==} @@ -3174,8 +3166,8 @@ packages: create-require@1.1.1: resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} - create-wdio@9.21.0: - resolution: {integrity: sha512-L6gsQLArY3AH5uTGpf3VfUezIsmZKufkF3ixSWqCuA/m458YVKeGghu1bBOWBdDIzqa6GX4e29dv0uVam0CTpw==} + create-wdio@9.25.0: + resolution: {integrity: sha512-ZnkJVDx3HB7UNH2t1kxK1VoNWQAQhLYyTjSEdpcFTHoj/QUrzS7K9nW99Q6WT0mgsedbtl86VMq4IXfy6f8Jrw==} engines: {node: '>=12.0.0'} hasBin: true @@ -3482,9 +3474,6 @@ packages: electron-to-chromium@1.5.165: resolution: {integrity: sha512-naiMx1Z6Nb2TxPU6fiFrUrDTjyPMLdTtaOd2oLmG8zVSg2hCWGkhPyxwk+qRmZ1ytwVqUv0u7ZcDA5+ALhaUtw==} - electron-to-chromium@1.5.259: - resolution: {integrity: sha512-I+oLXgpEJzD6Cwuwt1gYjxsDmu/S/Kd41mmLA3O+/uH2pFRO/DvOjUyGozL8j3KeLV6WyZ7ssPwELMsXCcsJAQ==} - electron-to-chromium@1.5.267: resolution: {integrity: sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==} @@ -3693,9 +3682,16 @@ packages: peerDependencies: eslint: '>=8.56.0' - eslint-plugin-wdio@9.23.0: - resolution: {integrity: sha512-8tcpupzp2Qmv+uSfhzeHi42LVA9PyjkpMBPclSIkPxBfXpj4fMrejwAHu1PROh1OmJN1VQcGQUTWvSzyRcV2vA==} + eslint-plugin-wdio@9.25.0: + resolution: {integrity: sha512-OuaJ1gtaLY6Z1roNMds+AFI9sbJiMJre95UCca5qLuHB/gUSiAojbLfF2ISB+5Dl+ntC3+j1PBfPrhCl5LMnEQ==} engines: {node: '>=18.20.0'} + peerDependencies: + eslint: ^9.39.2 + globals: ^16.5.0 + typescript-eslint: ^8.54.0 + peerDependenciesMeta: + typescript-eslint: + optional: true eslint-scope@8.4.0: resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} @@ -3709,8 +3705,12 @@ packages: resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - eslint@9.39.2: - resolution: {integrity: sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==} + eslint-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint@9.39.4: + resolution: {integrity: sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} hasBin: true peerDependencies: @@ -3822,8 +3822,8 @@ packages: resolution: {integrity: sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw==} engines: {node: '>=12.0.0'} - expect-webdriverio@5.6.1: - resolution: {integrity: sha512-gQHqfI6SmtYBIkTeMizpHThdpXh6ej2Hk68oKZneFM6iu99ZGXvOPnmhd8VDus3xOWhVDDdf4sLsMV2/o+X6Yg==} + expect-webdriverio@5.6.4: + resolution: {integrity: sha512-Bkoqs+39fHwjos51qab7ZWmvZrYNBbzgSAIykH2CrgLOLhHJXzC30DP9lZq2MsmaUsbBnN5c5m8VqAhOHTrCRw==} engines: {node: '>=20'} peerDependencies: '@wdio/globals': ^9.0.0 @@ -4183,18 +4183,24 @@ packages: resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} hasBin: true - glob@13.0.0: - resolution: {integrity: sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA==} + glob@11.1.0: + resolution: {integrity: sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==} engines: {node: 20 || >=22} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + hasBin: true + + glob@13.0.6: + resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} + engines: {node: 18 || 20 || >=22} glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} - deprecated: Glob versions prior to v9 are no longer supported + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me glob@8.1.0: resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} engines: {node: '>=12'} - deprecated: Glob versions prior to v9 are no longer supported + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me globals@14.0.0: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} @@ -4683,8 +4689,8 @@ packages: isarray@2.0.5: resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} - isbot@5.1.32: - resolution: {integrity: sha512-VNfjM73zz2IBZmdShMfAUg10prm6t7HFUQmNAEOAVS4YH92ZrZcvkMcGX6cIgBJAzWDzPent/EeAtYEHNPNPBQ==} + isbot@5.1.36: + resolution: {integrity: sha512-C/ZtXyJqDPZ7G7JPr06ApWyYoHjYexQbS6hPYD4WYCzpv2Qes6Z+CCEfTX4Owzf+1EJ933PoI2p+B9v7wpGZBQ==} engines: {node: '>=18'} isexe@2.0.0: @@ -4721,6 +4727,10 @@ packages: jackspeak@3.4.3: resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + jackspeak@4.2.3: + resolution: {integrity: sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==} + engines: {node: 20 || >=22} + jake@10.9.4: resolution: {integrity: sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==} engines: {node: '>=10'} @@ -5269,9 +5279,16 @@ packages: resolution: {integrity: sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==} engines: {node: 20 || >=22} + minimatch@10.2.4: + resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==} + engines: {node: 18 || 20 || >=22} + minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + minimatch@3.1.5: + resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + minimatch@5.1.6: resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} engines: {node: '>=10'} @@ -5307,10 +5324,18 @@ packages: resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} engines: {node: '>=16 || 14 >=14.17'} + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + minizlib@2.1.2: resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} engines: {node: '>= 8'} + minizlib@3.1.0: + resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==} + engines: {node: '>= 18'} + mitt@3.0.1: resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} @@ -5773,6 +5798,10 @@ packages: resolution: {integrity: sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==} engines: {node: 20 || >=22} + path-scurry@2.0.2: + resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} + engines: {node: 18 || 20 || >=22} + path-to-regexp@0.1.12: resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==} @@ -5946,8 +5975,8 @@ packages: postcss-value-parser@4.2.0: resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} - postcss@8.5.6: - resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + postcss@8.5.8: + resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} engines: {node: ^10 || ^12 || >=14} prelude-ls@1.2.1: @@ -6083,8 +6112,8 @@ packages: peerDependencies: react: ^18.3.1 - react-icons@5.5.0: - resolution: {integrity: sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==} + react-icons@5.6.0: + resolution: {integrity: sha512-RH93p5ki6LfOiIt0UtDyNg/cee+HLVR6cHHtW3wALfo+eOHTp8RnU2kRkI6E+H19zMIs03DyxUG/GfZMOGvmiA==} peerDependencies: react: '*' @@ -6207,8 +6236,8 @@ packages: resolution: {integrity: sha512-qx+xQGZVsy55CH0a1hiVwHmqjLryfh7wQyF5HO07XJ9f7dQMY/gPQHhlyDkIzJKC+x2fUCpCcUODUUUFrm7SHA==} hasBin: true - release-it@19.2.3: - resolution: {integrity: sha512-GTSfrKb0cPmssEFliX0+WQBHC6NiDsMR/M8Y2tkvct/TO36BjB5XV6LO5BUTYV12C3+f78Y8aroku7jCyNiDeA==} + release-it@19.2.4: + resolution: {integrity: sha512-BwaJwQYUIIAKuDYvpqQTSoy0U7zIy6cHyEjih/aNaFICphGahia4cjDANuFXb7gVZ51hIK9W0io6fjNQWXqICg==} engines: {node: ^20.12.0 || >=22.0.0} hasBin: true @@ -6300,8 +6329,8 @@ packages: deprecated: Rimraf versions prior to v4 are no longer supported hasBin: true - rimraf@6.1.2: - resolution: {integrity: sha512-cFCkPslJv7BAXJsYlK1dZsbP8/ZNLkCAQ0bi1hf5EKX2QHegmDFEFA6QhuYJlk7UDdc+02JjO80YSOrWPpw06g==} + rimraf@6.1.3: + resolution: {integrity: sha512-LKg+Cr2ZF61fkcaK1UdkH2yEBBKnYjTyWzTJT6KNPcSPaiT7HSdhtMXQuN5wkTX0Xu72KQ1l8S42rlmexS2hSA==} engines: {node: 20 || >=22} hasBin: true @@ -6779,6 +6808,11 @@ packages: tar@6.2.1: resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} engines: {node: '>=10'} + deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + + tar@7.5.11: + resolution: {integrity: sha512-ChjMH33/KetonMTAtpYdgUFr0tbz69Fp2v7zWxQfYZX4g5ZN2nOBXm1R2xyA+lMIKrLKIoKAwFj93jE/avX9cQ==} + engines: {node: '>=18'} temp-fs@0.9.9: resolution: {integrity: sha512-WfecDCR1xC9b0nsrzSaxPf3ZuWeWLUWblW4vlDQAa1biQaKHiImHnJfeQocQe/hXKMcolRzgkcVX/7kK4zoWbw==} @@ -7055,6 +7089,10 @@ packages: resolution: {integrity: sha512-hU/10obOIu62MGYjdskASR3CUAiYaFTtC9Pa6vHyf//mAipSvSQg6od2CnJswq7fvzNS3zJhxoRkgNVaHurWKw==} engines: {node: '>=18.17'} + undici@6.23.0: + resolution: {integrity: sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==} + engines: {node: '>=18.17'} + undici@7.16.0: resolution: {integrity: sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g==} engines: {node: '>=20.18.1'} @@ -7122,12 +7160,6 @@ packages: peerDependencies: browserslist: '>= 4.21.0' - update-browserslist-db@1.1.4: - resolution: {integrity: sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==} - hasBin: true - peerDependencies: - browserslist: '>= 4.21.0' - update-browserslist-db@1.2.3: resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} hasBin: true @@ -7328,12 +7360,12 @@ packages: resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==} engines: {node: '>= 14'} - webdriver@9.23.0: - resolution: {integrity: sha512-XkZOhjoBOY7maKI3BhDF2rNiDne4wBD6Gw6VUnt4X9b7j9NtfzcCrThBlT0hnA8W77bWNtMRCSpw9Ajy08HqKg==} + webdriver@9.25.0: + resolution: {integrity: sha512-XnABKdrp83zX3xVltmX0OcFzn8zOzWGtZQxIUKY0+INB0g9Nnnfu7G75W0G+0y4nyb3zH8mavGzDBiXctdEd3Q==} engines: {node: '>=18.20.0'} - webdriverio@9.23.0: - resolution: {integrity: sha512-Y5y4jpwHvuduUfup+gXTuCU6AROn/k6qOba3st0laFluKHY+q5SHOpQAJdS8acYLwE8caDQ2dXJhmXyxuJrm0Q==} + webdriverio@9.25.0: + resolution: {integrity: sha512-ualC/LtWGjL5rwGAbUUzURKqKoHJG2/qecEppcS9k4n1IX3MlbzGXuL/qpXiRbs/h4981HpRbZAKBxRYqwUe3g==} engines: {node: '>=18.20.0'} peerDependencies: puppeteer-core: '>=22.x || <=24.x' @@ -7498,6 +7530,10 @@ packages: xmlchars@2.2.0: resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + xpath@0.0.34: + resolution: {integrity: sha512-FxF6+rkr1rNSQrhUNYrAFJpRXNzlDoMxeXN5qI84939ylEv3qqPFKa85Oxr6tDaJKqwW6KKyo2v26TSv3k6LeA==} + engines: {node: '>=0.6.0'} + xtend@4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} @@ -7512,6 +7548,10 @@ packages: yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + yallist@5.0.0: + resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} + engines: {node: '>=18'} + yaml@1.10.2: resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} engines: {node: '>= 6'} @@ -7647,7 +7687,7 @@ snapshots: dependencies: '@babel/compat-data': 7.28.4 '@babel/helper-validator-option': 7.27.1 - browserslist: 4.28.0 + browserslist: 4.28.1 lru-cache: 5.1.1 semver: 6.3.1 @@ -7814,9 +7854,9 @@ snapshots: '@bufbuild/protobuf@2.10.2': {} - '@changesets/apply-release-plan@7.0.14': + '@changesets/apply-release-plan@7.1.0': dependencies: - '@changesets/config': 3.1.2 + '@changesets/config': 3.1.3 '@changesets/get-version-range-type': 0.4.0 '@changesets/git': 3.0.4 '@changesets/should-skip-package': 0.1.2 @@ -7843,30 +7883,28 @@ snapshots: dependencies: '@changesets/types': 6.1.0 - '@changesets/cli@2.29.8(@types/node@24.0.14)': + '@changesets/cli@2.30.0(@types/node@24.0.14)': dependencies: - '@changesets/apply-release-plan': 7.0.14 + '@changesets/apply-release-plan': 7.1.0 '@changesets/assemble-release-plan': 6.0.9 '@changesets/changelog-git': 0.2.1 - '@changesets/config': 3.1.2 + '@changesets/config': 3.1.3 '@changesets/errors': 0.2.0 '@changesets/get-dependents-graph': 2.1.3 - '@changesets/get-release-plan': 4.0.14 + '@changesets/get-release-plan': 4.0.15 '@changesets/git': 3.0.4 '@changesets/logger': 0.1.1 '@changesets/pre': 2.0.2 - '@changesets/read': 0.6.6 + '@changesets/read': 0.6.7 '@changesets/should-skip-package': 0.1.2 '@changesets/types': 6.1.0 '@changesets/write': 0.4.0 '@inquirer/external-editor': 1.0.3(@types/node@24.0.14) '@manypkg/get-packages': 1.1.3 ansi-colors: 4.1.3 - ci-info: 3.9.0 enquirer: 2.4.1 fs-extra: 7.0.1 mri: 1.2.0 - p-limit: 2.3.0 package-manager-detector: 0.2.11 picocolors: 1.1.1 resolve-from: 5.0.0 @@ -7876,11 +7914,12 @@ snapshots: transitivePeerDependencies: - '@types/node' - '@changesets/config@3.1.2': + '@changesets/config@3.1.3': dependencies: '@changesets/errors': 0.2.0 '@changesets/get-dependents-graph': 2.1.3 '@changesets/logger': 0.1.1 + '@changesets/should-skip-package': 0.1.2 '@changesets/types': 6.1.0 '@manypkg/get-packages': 1.1.3 fs-extra: 7.0.1 @@ -7897,12 +7936,12 @@ snapshots: picocolors: 1.1.1 semver: 7.7.3 - '@changesets/get-release-plan@4.0.14': + '@changesets/get-release-plan@4.0.15': dependencies: '@changesets/assemble-release-plan': 6.0.9 - '@changesets/config': 3.1.2 + '@changesets/config': 3.1.3 '@changesets/pre': 2.0.2 - '@changesets/read': 0.6.6 + '@changesets/read': 0.6.7 '@changesets/types': 6.1.0 '@manypkg/get-packages': 1.1.3 @@ -7920,7 +7959,7 @@ snapshots: dependencies: picocolors: 1.1.1 - '@changesets/parse@0.4.2': + '@changesets/parse@0.4.3': dependencies: '@changesets/types': 6.1.0 js-yaml: 4.1.1 @@ -7932,11 +7971,11 @@ snapshots: '@manypkg/get-packages': 1.1.3 fs-extra: 7.0.1 - '@changesets/read@0.6.6': + '@changesets/read@0.6.7': dependencies: '@changesets/git': 3.0.4 '@changesets/logger': 0.1.1 - '@changesets/parse': 0.4.2 + '@changesets/parse': 0.4.3 '@changesets/types': 6.1.0 fs-extra: 7.0.1 p-filter: 2.1.0 @@ -8039,7 +8078,7 @@ snapshots: '@emotion/memoize@0.9.0': {} - '@emotion/react@11.14.0(@types/react@18.3.27)(react@18.3.1)': + '@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1)': dependencies: '@babel/runtime': 7.27.6 '@emotion/babel-plugin': 11.13.5 @@ -8051,7 +8090,7 @@ snapshots: hoist-non-react-statics: 3.3.2 react: 18.3.1 optionalDependencies: - '@types/react': 18.3.27 + '@types/react': 18.3.28 transitivePeerDependencies: - supports-color @@ -8288,30 +8327,23 @@ snapshots: '@esbuild/win32-x64@0.25.11': optional: true - '@eslint-community/eslint-utils@4.7.0(eslint@9.39.2(jiti@2.6.1))': - dependencies: - eslint: 9.39.2(jiti@2.6.1) - eslint-visitor-keys: 3.4.3 - - '@eslint-community/eslint-utils@4.9.0(eslint@9.39.2(jiti@2.6.1))': + '@eslint-community/eslint-utils@4.7.0(eslint@9.39.4(jiti@2.6.1))': dependencies: - eslint: 9.39.2(jiti@2.6.1) + eslint: 9.39.4(jiti@2.6.1) eslint-visitor-keys: 3.4.3 - '@eslint-community/eslint-utils@4.9.1(eslint@9.39.2(jiti@2.6.1))': + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.4(jiti@2.6.1))': dependencies: - eslint: 9.39.2(jiti@2.6.1) + eslint: 9.39.4(jiti@2.6.1) eslint-visitor-keys: 3.4.3 - '@eslint-community/regexpp@4.12.1': {} - '@eslint-community/regexpp@4.12.2': {} - '@eslint/config-array@0.21.1': + '@eslint/config-array@0.21.2': dependencies: '@eslint/object-schema': 2.1.7 debug: 4.4.3(supports-color@8.1.1) - minimatch: 3.1.2 + minimatch: 3.1.5 transitivePeerDependencies: - supports-color @@ -8323,21 +8355,21 @@ snapshots: dependencies: '@types/json-schema': 7.0.15 - '@eslint/eslintrc@3.3.1': + '@eslint/eslintrc@3.3.5': dependencies: - ajv: 6.12.6 + ajv: 6.14.0 debug: 4.4.3(supports-color@8.1.1) espree: 10.4.0 globals: 14.0.0 ignore: 5.3.2 import-fresh: 3.3.1 - js-yaml: 4.1.0 - minimatch: 3.1.2 + js-yaml: 4.1.1 + minimatch: 3.1.5 strip-json-comments: 3.1.1 transitivePeerDependencies: - supports-color - '@eslint/js@9.39.2': {} + '@eslint/js@9.39.4': {} '@eslint/object-schema@2.1.7': {} @@ -8737,6 +8769,12 @@ snapshots: wrap-ansi: 8.1.0 wrap-ansi-cjs: wrap-ansi@7.0.0 + '@isaacs/cliui@9.0.0': {} + + '@isaacs/fs-minipass@4.0.1': + dependencies: + minipass: 7.1.2 + '@istanbuljs/schema@0.1.3': {} '@jest/diff-sequences@30.0.1': {} @@ -9284,10 +9322,10 @@ snapshots: picocolors: 1.1.1 picomatch: 2.3.1 pidtree: 0.6.0 - postcss: 8.5.6 - postcss-discard-duplicates: 5.1.0(postcss@8.5.6) - postcss-load-config: 4.0.2(postcss@8.5.6)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) - postcss-modules: 6.0.1(postcss@8.5.6) + postcss: 8.5.8 + postcss-discard-duplicates: 5.1.0(postcss@8.5.8) + postcss-load-config: 4.0.2(postcss@8.5.8)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) + postcss-modules: 6.0.1(postcss@8.5.8) prettier: 2.8.8 pretty-ms: 7.0.1 react-refresh: 0.14.2 @@ -9499,7 +9537,7 @@ snapshots: '@tsconfig/node16@1.0.4': {} - '@tsconfig/node20@20.1.8': {} + '@tsconfig/node20@20.1.9': {} '@tybys/wasm-util@0.9.0': dependencies: @@ -9591,10 +9629,6 @@ snapshots: '@types/node@16.9.1': {} - '@types/node@20.19.17': - dependencies: - undici-types: 6.21.0 - '@types/node@20.19.25': dependencies: undici-types: 6.21.0 @@ -9621,15 +9655,15 @@ snapshots: '@types/prop-types@15.7.14': {} - '@types/react-dom@18.3.7(@types/react@18.3.27)': + '@types/react-dom@18.3.7(@types/react@18.3.28)': dependencies: - '@types/react': 18.3.27 + '@types/react': 18.3.28 - '@types/react-transition-group@4.4.12(@types/react@18.3.27)': + '@types/react-transition-group@4.4.12(@types/react@18.3.28)': dependencies: - '@types/react': 18.3.27 + '@types/react': 18.3.28 - '@types/react@18.3.27': + '@types/react@18.3.28': dependencies: '@types/prop-types': 15.7.14 csstype: 3.2.3 @@ -9673,15 +9707,15 @@ snapshots: '@types/node': 24.10.1 optional: true - '@typescript-eslint/eslint-plugin@8.53.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.57.0(@typescript-eslint/parser@8.57.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.53.0 - '@typescript-eslint/type-utils': 8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/utils': 8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.53.0 - eslint: 9.39.2(jiti@2.6.1) + '@typescript-eslint/parser': 8.57.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.57.0 + '@typescript-eslint/type-utils': 8.57.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.57.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.57.0 + eslint: 9.39.4(jiti@2.6.1) ignore: 7.0.5 natural-compare: 1.4.0 ts-api-utils: 2.4.0(typescript@5.9.3) @@ -9689,58 +9723,58 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/parser@8.57.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@typescript-eslint/scope-manager': 8.53.0 - '@typescript-eslint/types': 8.53.0 - '@typescript-eslint/typescript-estree': 8.53.0(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.53.0 + '@typescript-eslint/scope-manager': 8.57.0 + '@typescript-eslint/types': 8.57.0 + '@typescript-eslint/typescript-estree': 8.57.0(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.57.0 debug: 4.4.3(supports-color@8.1.1) - eslint: 9.39.2(jiti@2.6.1) + eslint: 9.39.4(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.53.0(typescript@5.9.3)': + '@typescript-eslint/project-service@8.57.0(typescript@5.9.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.53.0(typescript@5.9.3) - '@typescript-eslint/types': 8.53.0 + '@typescript-eslint/tsconfig-utils': 8.57.0(typescript@5.9.3) + '@typescript-eslint/types': 8.57.0 debug: 4.4.3(supports-color@8.1.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.53.0': + '@typescript-eslint/scope-manager@8.57.0': dependencies: - '@typescript-eslint/types': 8.53.0 - '@typescript-eslint/visitor-keys': 8.53.0 + '@typescript-eslint/types': 8.57.0 + '@typescript-eslint/visitor-keys': 8.57.0 - '@typescript-eslint/tsconfig-utils@8.53.0(typescript@5.9.3)': + '@typescript-eslint/tsconfig-utils@8.57.0(typescript@5.9.3)': dependencies: typescript: 5.9.3 - '@typescript-eslint/type-utils@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.57.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@typescript-eslint/types': 8.53.0 - '@typescript-eslint/typescript-estree': 8.53.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/types': 8.57.0 + '@typescript-eslint/typescript-estree': 8.57.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.57.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) debug: 4.4.3(supports-color@8.1.1) - eslint: 9.39.2(jiti@2.6.1) + eslint: 9.39.4(jiti@2.6.1) ts-api-utils: 2.4.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.53.0': {} + '@typescript-eslint/types@8.57.0': {} - '@typescript-eslint/typescript-estree@8.53.0(typescript@5.9.3)': + '@typescript-eslint/typescript-estree@8.57.0(typescript@5.9.3)': dependencies: - '@typescript-eslint/project-service': 8.53.0(typescript@5.9.3) - '@typescript-eslint/tsconfig-utils': 8.53.0(typescript@5.9.3) - '@typescript-eslint/types': 8.53.0 - '@typescript-eslint/visitor-keys': 8.53.0 + '@typescript-eslint/project-service': 8.57.0(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.57.0(typescript@5.9.3) + '@typescript-eslint/types': 8.57.0 + '@typescript-eslint/visitor-keys': 8.57.0 debug: 4.4.3(supports-color@8.1.1) - minimatch: 9.0.5 + minimatch: 10.2.4 semver: 7.7.3 tinyglobby: 0.2.15 ts-api-utils: 2.4.0(typescript@5.9.3) @@ -9748,21 +9782,21 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/utils@8.57.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1)) - '@typescript-eslint/scope-manager': 8.53.0 - '@typescript-eslint/types': 8.53.0 - '@typescript-eslint/typescript-estree': 8.53.0(typescript@5.9.3) - eslint: 9.39.2(jiti@2.6.1) + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.6.1)) + '@typescript-eslint/scope-manager': 8.57.0 + '@typescript-eslint/types': 8.57.0 + '@typescript-eslint/typescript-estree': 8.57.0(typescript@5.9.3) + eslint: 9.39.4(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.53.0': + '@typescript-eslint/visitor-keys@8.57.0': dependencies: - '@typescript-eslint/types': 8.53.0 - eslint-visitor-keys: 4.2.1 + '@typescript-eslint/types': 8.57.0 + eslint-visitor-keys: 5.0.1 '@unrs/resolver-binding-darwin-arm64@1.7.11': optional: true @@ -9961,17 +9995,20 @@ snapshots: loupe: 3.1.4 tinyrainbow: 2.0.0 - '@wdio/appium-service@9.23.0': + '@wdio/appium-service@9.25.0': dependencies: - '@wdio/config': 9.23.0 + '@wdio/config': 9.25.0 '@wdio/logger': 9.18.0 - '@wdio/types': 9.20.0 - '@wdio/utils': 9.23.0 + '@wdio/reporter': 9.25.0 + '@wdio/types': 9.25.0 + '@wdio/utils': 9.25.0 + '@xmldom/xmldom': 0.9.8 change-case: 5.4.4 get-port: 7.1.0 import-meta-resolve: 4.2.0 tree-kill: 1.2.2 - webdriverio: 9.23.0 + webdriverio: 9.25.0 + xpath: 0.0.34 transitivePeerDependencies: - bare-buffer - bufferutil @@ -9980,27 +10017,28 @@ snapshots: - supports-color - utf-8-validate - '@wdio/browserstack-service@9.23.0(@wdio/cli@9.23.0(@types/node@24.0.14)(expect-webdriverio@5.6.1))': + '@wdio/browserstack-service@9.25.0(@wdio/cli@9.25.0(@types/node@24.0.14)(expect-webdriverio@5.6.4))': dependencies: '@browserstack/ai-sdk-node': 1.5.17 '@browserstack/wdio-browserstack-service': 2.0.2 '@percy/appium-app': 2.1.0 '@percy/selenium-webdriver': 2.2.4 '@types/gitconfiglocal': 2.0.3 - '@wdio/cli': 9.23.0(@types/node@24.0.14)(expect-webdriverio@5.6.1) + '@wdio/cli': 9.25.0(@types/node@24.0.14)(expect-webdriverio@5.6.4) '@wdio/logger': 9.18.0 - '@wdio/reporter': 9.20.0 - '@wdio/types': 9.20.0 + '@wdio/reporter': 9.25.0 + '@wdio/types': 9.25.0 browserstack-local: 1.5.8 chalk: 5.6.2 csv-writer: 1.6.0 formdata-node: 5.0.1 git-repo-info: 2.1.1 gitconfiglocal: 2.1.0 - tar: 6.2.1 + glob: 11.1.0 + tar: 7.5.11 undici: 6.22.0 uuid: 11.1.0 - webdriverio: 9.23.0 + webdriverio: 9.25.0 winston-transport: 4.9.0 yauzl: 3.2.0 transitivePeerDependencies: @@ -10012,19 +10050,19 @@ snapshots: - supports-color - utf-8-validate - '@wdio/cli@9.23.0(@types/node@24.0.14)(expect-webdriverio@5.6.1)': + '@wdio/cli@9.25.0(@types/node@24.0.14)(expect-webdriverio@5.6.4)': dependencies: '@vitest/snapshot': 2.1.9 - '@wdio/config': 9.23.0 - '@wdio/globals': 9.23.0(expect-webdriverio@5.6.1)(webdriverio@9.23.0) + '@wdio/config': 9.25.0 + '@wdio/globals': 9.23.0(expect-webdriverio@5.6.4)(webdriverio@9.25.0) '@wdio/logger': 9.18.0 - '@wdio/protocols': 9.16.2 - '@wdio/types': 9.20.0 - '@wdio/utils': 9.23.0 + '@wdio/protocols': 9.25.0 + '@wdio/types': 9.25.0 + '@wdio/utils': 9.25.0 async-exit-hook: 2.0.1 chalk: 5.6.2 chokidar: 4.0.3 - create-wdio: 9.21.0(@types/node@24.0.14) + create-wdio: 9.25.0(@types/node@24.0.14) dotenv: 17.2.3 import-meta-resolve: 4.2.0 lodash.flattendeep: 4.4.0 @@ -10032,7 +10070,7 @@ snapshots: lodash.union: 4.6.0 read-pkg-up: 10.1.0 tsx: 4.20.5 - webdriverio: 9.23.0 + webdriverio: 9.25.0 yargs: 17.7.2 transitivePeerDependencies: - '@types/node' @@ -10044,40 +10082,41 @@ snapshots: - supports-color - utf-8-validate - '@wdio/config@9.23.0': + '@wdio/config@9.25.0': dependencies: '@wdio/logger': 9.18.0 - '@wdio/types': 9.20.0 - '@wdio/utils': 9.23.0 + '@wdio/types': 9.25.0 + '@wdio/utils': 9.25.0 deepmerge-ts: 7.1.5 glob: 10.4.5 import-meta-resolve: 4.2.0 + jiti: 2.6.1 transitivePeerDependencies: - bare-buffer - react-native-b4a - supports-color - '@wdio/dot-reporter@9.20.0': + '@wdio/dot-reporter@9.25.0': dependencies: - '@wdio/reporter': 9.20.0 - '@wdio/types': 9.20.0 + '@wdio/reporter': 9.25.0 + '@wdio/types': 9.25.0 chalk: 5.6.2 - '@wdio/globals@9.23.0(expect-webdriverio@5.6.1)(webdriverio@9.23.0)': + '@wdio/globals@9.23.0(expect-webdriverio@5.6.4)(webdriverio@9.25.0)': dependencies: - expect-webdriverio: 5.6.1(@wdio/globals@9.23.0)(@wdio/logger@9.18.0)(webdriverio@9.23.0) - webdriverio: 9.23.0 + expect-webdriverio: 5.6.4(@wdio/globals@9.23.0)(@wdio/logger@9.18.0)(webdriverio@9.25.0) + webdriverio: 9.25.0 - '@wdio/local-runner@9.23.0(@wdio/globals@9.23.0)(webdriverio@9.23.0)': + '@wdio/local-runner@9.25.0(@wdio/globals@9.23.0)(webdriverio@9.25.0)': dependencies: '@types/node': 20.19.25 '@wdio/logger': 9.18.0 '@wdio/repl': 9.16.2 - '@wdio/runner': 9.23.0(expect-webdriverio@5.6.1)(webdriverio@9.23.0) - '@wdio/types': 9.20.0 - '@wdio/xvfb': 9.20.0 + '@wdio/runner': 9.25.0(expect-webdriverio@5.6.4)(webdriverio@9.25.0) + '@wdio/types': 9.25.0 + '@wdio/xvfb': 9.25.0 exit-hook: 4.0.0 - expect-webdriverio: 5.6.1(@wdio/globals@9.23.0)(@wdio/logger@9.18.0)(webdriverio@9.23.0) + expect-webdriverio: 5.6.4(@wdio/globals@9.23.0)(@wdio/logger@9.18.0)(webdriverio@9.25.0) split2: 4.2.0 stream-buffers: 3.0.3 transitivePeerDependencies: @@ -10104,46 +10143,46 @@ snapshots: safe-regex2: 5.0.0 strip-ansi: 7.1.0 - '@wdio/mocha-framework@9.23.0': + '@wdio/mocha-framework@9.25.0': dependencies: '@types/mocha': 10.0.10 '@types/node': 20.19.25 '@wdio/logger': 9.18.0 - '@wdio/types': 9.20.0 - '@wdio/utils': 9.23.0 + '@wdio/types': 9.25.0 + '@wdio/utils': 9.25.0 mocha: 10.8.2 transitivePeerDependencies: - bare-buffer - react-native-b4a - supports-color - '@wdio/protocols@9.16.2': {} + '@wdio/protocols@9.25.0': {} '@wdio/repl@9.16.2': dependencies: '@types/node': 20.19.25 - '@wdio/reporter@9.20.0': + '@wdio/reporter@9.25.0': dependencies: '@types/node': 20.19.25 '@wdio/logger': 9.18.0 - '@wdio/types': 9.20.0 + '@wdio/types': 9.25.0 diff: 8.0.2 object-inspect: 1.13.4 - '@wdio/runner@9.23.0(expect-webdriverio@5.6.1)(webdriverio@9.23.0)': + '@wdio/runner@9.25.0(expect-webdriverio@5.6.4)(webdriverio@9.25.0)': dependencies: '@types/node': 20.19.25 - '@wdio/config': 9.23.0 - '@wdio/dot-reporter': 9.20.0 - '@wdio/globals': 9.23.0(expect-webdriverio@5.6.1)(webdriverio@9.23.0) + '@wdio/config': 9.25.0 + '@wdio/dot-reporter': 9.25.0 + '@wdio/globals': 9.23.0(expect-webdriverio@5.6.4)(webdriverio@9.25.0) '@wdio/logger': 9.18.0 - '@wdio/types': 9.20.0 - '@wdio/utils': 9.23.0 + '@wdio/types': 9.25.0 + '@wdio/utils': 9.25.0 deepmerge-ts: 7.1.5 - expect-webdriverio: 5.6.1(@wdio/globals@9.23.0)(@wdio/logger@9.18.0)(webdriverio@9.23.0) - webdriver: 9.23.0 - webdriverio: 9.23.0 + expect-webdriverio: 5.6.4(@wdio/globals@9.23.0)(@wdio/logger@9.18.0)(webdriverio@9.25.0) + webdriver: 9.25.0 + webdriverio: 9.25.0 transitivePeerDependencies: - bare-buffer - bufferutil @@ -10151,13 +10190,13 @@ snapshots: - supports-color - utf-8-validate - '@wdio/sauce-service@9.23.0': + '@wdio/sauce-service@9.25.0': dependencies: '@wdio/logger': 9.18.0 - '@wdio/types': 9.20.0 - '@wdio/utils': 9.23.0 + '@wdio/types': 9.25.0 + '@wdio/utils': 9.25.0 saucelabs: 9.0.2 - webdriverio: 9.23.0 + webdriverio: 9.25.0 transitivePeerDependencies: - bare-buffer - bufferutil @@ -10166,13 +10205,13 @@ snapshots: - supports-color - utf-8-validate - '@wdio/shared-store-service@9.23.0': + '@wdio/shared-store-service@9.25.0': dependencies: '@polka/parse': 1.0.0-next.0 '@wdio/logger': 9.18.0 - '@wdio/types': 9.20.0 + '@wdio/types': 9.25.0 polka: 0.5.2 - webdriverio: 9.23.0 + webdriverio: 9.25.0 transitivePeerDependencies: - bare-buffer - bufferutil @@ -10181,23 +10220,23 @@ snapshots: - supports-color - utf-8-validate - '@wdio/spec-reporter@9.20.0': + '@wdio/spec-reporter@9.25.0': dependencies: - '@wdio/reporter': 9.20.0 - '@wdio/types': 9.20.0 + '@wdio/reporter': 9.25.0 + '@wdio/types': 9.25.0 chalk: 5.6.2 easy-table: 1.2.0 pretty-ms: 9.3.0 - '@wdio/types@9.20.0': + '@wdio/types@9.25.0': dependencies: - '@types/node': 20.19.17 + '@types/node': 20.19.25 - '@wdio/utils@9.23.0': + '@wdio/utils@9.25.0': dependencies: '@puppeteer/browsers': 2.10.10 '@wdio/logger': 9.18.0 - '@wdio/types': 9.20.0 + '@wdio/types': 9.25.0 decamelize: 6.0.1 deepmerge-ts: 7.1.5 edgedriver: 6.1.2 @@ -10214,15 +10253,15 @@ snapshots: - react-native-b4a - supports-color - '@wdio/xvfb@9.20.0': + '@wdio/xvfb@9.25.0': dependencies: '@wdio/logger': 9.18.0 '@web3-storage/multipart-parser@1.0.0': {} - '@zip.js/zip.js@2.8.15': {} + '@xmldom/xmldom@0.9.8': {} - '@zip.js/zip.js@2.8.7': {} + '@zip.js/zip.js@2.8.15': {} '@zxing/text-encoding@0.9.0': optional: true @@ -10265,7 +10304,7 @@ snapshots: clean-stack: 2.2.0 indent-string: 4.0.0 - ajv@6.12.6: + ajv@6.14.0: dependencies: fast-deep-equal: 3.1.3 fast-json-stable-stringify: 2.1.0 @@ -10432,13 +10471,13 @@ snapshots: asynckit@0.4.0: {} - autoprefixer@10.4.23(postcss@8.5.6): + autoprefixer@10.4.27(postcss@8.5.8): dependencies: browserslist: 4.28.1 - caniuse-lite: 1.0.30001764 + caniuse-lite: 1.0.30001778 fraction.js: 5.3.4 picocolors: 1.1.1 - postcss: 8.5.6 + postcss: 8.5.8 postcss-value-parser: 4.2.0 available-typed-arrays@1.0.7: @@ -10471,6 +10510,8 @@ snapshots: balanced-match@1.0.2: {} + balanced-match@4.0.4: {} + bare-events@2.7.0: {} bare-fs@4.4.5: @@ -10508,8 +10549,6 @@ snapshots: base64-js@1.5.1: {} - baseline-browser-mapping@2.8.30: {} - baseline-browser-mapping@2.9.14: {} basic-auth@2.0.1: @@ -10569,6 +10608,10 @@ snapshots: dependencies: balanced-match: 1.0.2 + brace-expansion@5.0.4: + dependencies: + balanced-match: 4.0.4 + braces@3.0.3: dependencies: fill-range: 7.1.1 @@ -10586,18 +10629,10 @@ snapshots: node-releases: 2.0.19 update-browserslist-db: 1.1.3(browserslist@4.25.0) - browserslist@4.28.0: - dependencies: - baseline-browser-mapping: 2.8.30 - caniuse-lite: 1.0.30001756 - electron-to-chromium: 1.5.259 - node-releases: 2.0.27 - update-browserslist-db: 1.1.4(browserslist@4.28.0) - browserslist@4.28.1: dependencies: baseline-browser-mapping: 2.9.14 - caniuse-lite: 1.0.30001764 + caniuse-lite: 1.0.30001778 electron-to-chromium: 1.5.267 node-releases: 2.0.27 update-browserslist-db: 1.2.3(browserslist@4.28.1) @@ -10721,9 +10756,7 @@ snapshots: caniuse-lite@1.0.30001721: {} - caniuse-lite@1.0.30001756: {} - - caniuse-lite@1.0.30001764: {} + caniuse-lite@1.0.30001778: {} capital-case@1.0.4: dependencies: @@ -10826,7 +10859,7 @@ snapshots: chownr@2.0.0: {} - ci-info@3.9.0: {} + chownr@3.0.0: {} ci-info@4.2.0: {} @@ -11008,7 +11041,7 @@ snapshots: create-require@1.1.1: {} - create-wdio@9.21.0(@types/node@24.0.14): + create-wdio@9.25.0(@types/node@24.0.14): dependencies: chalk: 5.6.2 commander: 14.0.1 @@ -11272,7 +11305,7 @@ snapshots: edgedriver@6.1.2: dependencies: '@wdio/logger': 9.18.0 - '@zip.js/zip.js': 2.8.7 + '@zip.js/zip.js': 2.8.15 decamelize: 6.0.1 edge-paths: 3.0.5 fast-xml-parser: 5.3.0 @@ -11291,8 +11324,6 @@ snapshots: electron-to-chromium@1.5.165: {} - electron-to-chromium@1.5.259: {} - electron-to-chromium@1.5.267: {} emoji-regex@10.4.0: {} @@ -11557,33 +11588,33 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1)): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.6.1)): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.1 - eslint: 9.39.2(jiti@2.6.1) + eslint: 9.39.4(jiti@2.6.1) get-tsconfig: 4.10.1 is-bun-module: 2.0.0 stable-hash: 0.0.5 tinyglobby: 0.2.14 unrs-resolver: 1.7.11 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.57.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.57.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)): dependencies: debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) - eslint: 9.39.2(jiti@2.6.1) + '@typescript-eslint/parser': 8.57.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + eslint: 9.39.4(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.6.1)) transitivePeerDependencies: - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.57.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -11592,9 +11623,9 @@ snapshots: array.prototype.flatmap: 1.3.3 debug: 3.2.7 doctrine: 2.1.0 - eslint: 9.39.2(jiti@2.6.1) + eslint: 9.39.4(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.57.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -11606,13 +11637,13 @@ snapshots: string.prototype.trimend: 1.0.9 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.57.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack - supports-color - eslint-plugin-jsx-a11y@6.10.2(eslint@9.39.2(jiti@2.6.1)): + eslint-plugin-jsx-a11y@6.10.2(eslint@9.39.4(jiti@2.6.1)): dependencies: aria-query: 5.3.2 array-includes: 3.1.9 @@ -11622,7 +11653,7 @@ snapshots: axobject-query: 4.1.0 damerau-levenshtein: 1.0.8 emoji-regex: 9.2.2 - eslint: 9.39.2(jiti@2.6.1) + eslint: 9.39.4(jiti@2.6.1) hasown: 2.0.2 jsx-ast-utils: 3.3.5 language-tags: 1.0.9 @@ -11631,11 +11662,11 @@ snapshots: safe-regex-test: 1.1.0 string.prototype.includes: 2.0.1 - eslint-plugin-react-hooks@5.2.0(eslint@9.39.2(jiti@2.6.1)): + eslint-plugin-react-hooks@5.2.0(eslint@9.39.4(jiti@2.6.1)): dependencies: - eslint: 9.39.2(jiti@2.6.1) + eslint: 9.39.4(jiti@2.6.1) - eslint-plugin-react@7.37.5(eslint@9.39.2(jiti@2.6.1)): + eslint-plugin-react@7.37.5(eslint@9.39.4(jiti@2.6.1)): dependencies: array-includes: 3.1.9 array.prototype.findlast: 1.2.5 @@ -11643,7 +11674,7 @@ snapshots: array.prototype.tosorted: 1.1.4 doctrine: 2.1.0 es-iterator-helpers: 1.2.1 - eslint: 9.39.2(jiti@2.6.1) + eslint: 9.39.4(jiti@2.6.1) estraverse: 5.3.0 hasown: 2.0.2 jsx-ast-utils: 3.3.5 @@ -11657,14 +11688,14 @@ snapshots: string.prototype.matchall: 4.0.12 string.prototype.repeat: 1.0.0 - eslint-plugin-unicorn@56.0.1(eslint@9.39.2(jiti@2.6.1)): + eslint-plugin-unicorn@56.0.1(eslint@9.39.4(jiti@2.6.1)): dependencies: '@babel/helper-validator-identifier': 7.27.1 - '@eslint-community/eslint-utils': 4.7.0(eslint@9.39.2(jiti@2.6.1)) + '@eslint-community/eslint-utils': 4.7.0(eslint@9.39.4(jiti@2.6.1)) ci-info: 4.2.0 clean-regexp: 1.0.0 core-js-compat: 3.42.0 - eslint: 9.39.2(jiti@2.6.1) + eslint: 9.39.4(jiti@2.6.1) esquery: 1.6.0 globals: 15.15.0 indent-string: 4.0.0 @@ -11677,7 +11708,10 @@ snapshots: semver: 7.7.2 strip-indent: 3.0.0 - eslint-plugin-wdio@9.23.0: {} + eslint-plugin-wdio@9.25.0(eslint@9.39.4(jiti@2.6.1))(globals@15.15.0): + dependencies: + eslint: 9.39.4(jiti@2.6.1) + globals: 15.15.0 eslint-scope@8.4.0: dependencies: @@ -11688,21 +11722,23 @@ snapshots: eslint-visitor-keys@4.2.1: {} - eslint@9.39.2(jiti@2.6.1): + eslint-visitor-keys@5.0.1: {} + + eslint@9.39.4(jiti@2.6.1): dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.2(jiti@2.6.1)) - '@eslint-community/regexpp': 4.12.1 - '@eslint/config-array': 0.21.1 + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.6.1)) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.21.2 '@eslint/config-helpers': 0.4.2 '@eslint/core': 0.17.0 - '@eslint/eslintrc': 3.3.1 - '@eslint/js': 9.39.2 + '@eslint/eslintrc': 3.3.5 + '@eslint/js': 9.39.4 '@eslint/plugin-kit': 0.4.1 '@humanfs/node': 0.16.7 '@humanwhocodes/module-importer': 1.0.1 '@humanwhocodes/retry': 0.4.3 '@types/estree': 1.0.8 - ajv: 6.12.6 + ajv: 6.14.0 chalk: 4.1.2 cross-spawn: 7.0.6 debug: 4.4.3(supports-color@8.1.1) @@ -11721,7 +11757,7 @@ snapshots: is-glob: 4.0.3 json-stable-stringify-without-jsonify: 1.0.1 lodash.merge: 4.6.2 - minimatch: 3.1.2 + minimatch: 3.1.5 natural-compare: 1.4.0 optionator: 0.9.4 optionalDependencies: @@ -11856,15 +11892,15 @@ snapshots: expect-type@1.2.1: {} - expect-webdriverio@5.6.1(@wdio/globals@9.23.0)(@wdio/logger@9.18.0)(webdriverio@9.23.0): + expect-webdriverio@5.6.4(@wdio/globals@9.23.0)(@wdio/logger@9.18.0)(webdriverio@9.25.0): dependencies: '@vitest/snapshot': 4.0.17 - '@wdio/globals': 9.23.0(expect-webdriverio@5.6.1)(webdriverio@9.23.0) + '@wdio/globals': 9.23.0(expect-webdriverio@5.6.4)(webdriverio@9.25.0) '@wdio/logger': 9.18.0 deep-eql: 5.0.2 expect: 30.2.0 jest-matcher-utils: 30.2.0 - webdriverio: 9.23.0 + webdriverio: 9.25.0 expect@30.2.0: dependencies: @@ -12272,12 +12308,21 @@ snapshots: package-json-from-dist: 1.0.1 path-scurry: 1.11.1 - glob@13.0.0: + glob@11.1.0: dependencies: + foreground-child: 3.3.1 + jackspeak: 4.2.3 minimatch: 10.1.1 minipass: 7.1.2 + package-json-from-dist: 1.0.1 path-scurry: 2.0.0 + glob@13.0.6: + dependencies: + minimatch: 10.2.4 + minipass: 7.1.3 + path-scurry: 2.0.2 + glob@7.2.3: dependencies: fs.realpath: 1.0.0 @@ -12497,9 +12542,9 @@ snapshots: dependencies: safer-buffer: 2.1.2 - icss-utils@5.1.0(postcss@8.5.6): + icss-utils@5.1.0(postcss@8.5.8): dependencies: - postcss: 8.5.6 + postcss: 8.5.8 idb-keyval@6.2.2: {} @@ -12765,7 +12810,7 @@ snapshots: isarray@2.0.5: {} - isbot@5.1.32: {} + isbot@5.1.36: {} isexe@2.0.0: {} @@ -12815,6 +12860,10 @@ snapshots: optionalDependencies: '@pkgjs/parseargs': 0.11.0 + jackspeak@4.2.3: + dependencies: + '@isaacs/cliui': 9.0.0 + jake@10.9.4: dependencies: async: 3.2.6 @@ -13554,10 +13603,18 @@ snapshots: dependencies: '@isaacs/brace-expansion': 5.0.0 + minimatch@10.2.4: + dependencies: + brace-expansion: 5.0.4 + minimatch@3.1.2: dependencies: brace-expansion: 1.1.12 + minimatch@3.1.5: + dependencies: + brace-expansion: 1.1.12 + minimatch@5.1.6: dependencies: brace-expansion: 2.0.2 @@ -13588,11 +13645,17 @@ snapshots: minipass@7.1.2: {} + minipass@7.1.3: {} + minizlib@2.1.2: dependencies: minipass: 3.3.6 yallist: 4.0.0 + minizlib@3.1.0: + dependencies: + minipass: 7.1.2 + mitt@3.0.1: {} mkdirp-classic@0.5.3: {} @@ -13621,7 +13684,7 @@ snapshots: find-up: 5.0.0 glob: 8.1.0 he: 1.2.0 - js-yaml: 4.1.0 + js-yaml: 4.1.1 log-symbols: 4.1.0 minimatch: 5.1.6 ms: 2.1.3 @@ -14121,6 +14184,11 @@ snapshots: lru-cache: 11.2.2 minipass: 7.1.2 + path-scurry@2.0.2: + dependencies: + lru-cache: 11.2.2 + minipass: 7.1.3 + path-to-regexp@0.1.12: {} path-type@4.0.0: {} @@ -14198,66 +14266,66 @@ snapshots: possible-typed-array-names@1.1.0: {} - postcss-discard-duplicates@5.1.0(postcss@8.5.6): + postcss-discard-duplicates@5.1.0(postcss@8.5.8): dependencies: - postcss: 8.5.6 + postcss: 8.5.8 - postcss-import@15.1.0(postcss@8.5.6): + postcss-import@15.1.0(postcss@8.5.8): dependencies: - postcss: 8.5.6 + postcss: 8.5.8 postcss-value-parser: 4.2.0 read-cache: 1.0.0 resolve: 1.22.10 - postcss-js@4.0.1(postcss@8.5.6): + postcss-js@4.0.1(postcss@8.5.8): dependencies: camelcase-css: 2.0.1 - postcss: 8.5.6 + postcss: 8.5.8 - postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)): + postcss-load-config@4.0.2(postcss@8.5.8)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)): dependencies: lilconfig: 3.1.3 yaml: 2.8.1 optionalDependencies: - postcss: 8.5.6 + postcss: 8.5.8 ts-node: 10.9.2(@types/node@24.10.1)(typescript@5.9.3) - postcss-modules-extract-imports@3.1.0(postcss@8.5.6): + postcss-modules-extract-imports@3.1.0(postcss@8.5.8): dependencies: - postcss: 8.5.6 + postcss: 8.5.8 - postcss-modules-local-by-default@4.2.0(postcss@8.5.6): + postcss-modules-local-by-default@4.2.0(postcss@8.5.8): dependencies: - icss-utils: 5.1.0(postcss@8.5.6) - postcss: 8.5.6 + icss-utils: 5.1.0(postcss@8.5.8) + postcss: 8.5.8 postcss-selector-parser: 7.1.0 postcss-value-parser: 4.2.0 - postcss-modules-scope@3.2.1(postcss@8.5.6): + postcss-modules-scope@3.2.1(postcss@8.5.8): dependencies: - postcss: 8.5.6 + postcss: 8.5.8 postcss-selector-parser: 7.1.0 - postcss-modules-values@4.0.0(postcss@8.5.6): + postcss-modules-values@4.0.0(postcss@8.5.8): dependencies: - icss-utils: 5.1.0(postcss@8.5.6) - postcss: 8.5.6 + icss-utils: 5.1.0(postcss@8.5.8) + postcss: 8.5.8 - postcss-modules@6.0.1(postcss@8.5.6): + postcss-modules@6.0.1(postcss@8.5.8): dependencies: generic-names: 4.0.0 - icss-utils: 5.1.0(postcss@8.5.6) + icss-utils: 5.1.0(postcss@8.5.8) lodash.camelcase: 4.3.0 - postcss: 8.5.6 - postcss-modules-extract-imports: 3.1.0(postcss@8.5.6) - postcss-modules-local-by-default: 4.2.0(postcss@8.5.6) - postcss-modules-scope: 3.2.1(postcss@8.5.6) - postcss-modules-values: 4.0.0(postcss@8.5.6) + postcss: 8.5.8 + postcss-modules-extract-imports: 3.1.0(postcss@8.5.8) + postcss-modules-local-by-default: 4.2.0(postcss@8.5.8) + postcss-modules-scope: 3.2.1(postcss@8.5.8) + postcss-modules-values: 4.0.0(postcss@8.5.8) string-hash: 1.1.3 - postcss-nested@6.2.0(postcss@8.5.6): + postcss-nested@6.2.0(postcss@8.5.8): dependencies: - postcss: 8.5.6 + postcss: 8.5.8 postcss-selector-parser: 6.1.2 postcss-selector-parser@6.1.2: @@ -14272,7 +14340,7 @@ snapshots: postcss-value-parser@4.2.0: {} - postcss@8.5.6: + postcss@8.5.8: dependencies: nanoid: 3.3.11 picocolors: 1.1.1 @@ -14426,7 +14494,7 @@ snapshots: react: 18.3.1 scheduler: 0.23.2 - react-icons@5.5.0(react@18.3.1): + react-icons@5.6.0(react@18.3.1): dependencies: react: 18.3.1 @@ -14448,19 +14516,19 @@ snapshots: '@remix-run/router': 1.23.2 react: 18.3.1 - react-select@5.10.2(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + react-select@5.10.2(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@babel/runtime': 7.27.6 '@emotion/cache': 11.14.0 - '@emotion/react': 11.14.0(@types/react@18.3.27)(react@18.3.1) + '@emotion/react': 11.14.0(@types/react@18.3.28)(react@18.3.1) '@floating-ui/dom': 1.7.1 - '@types/react-transition-group': 4.4.12(@types/react@18.3.27) + '@types/react-transition-group': 4.4.12(@types/react@18.3.28) memoize-one: 6.0.0 prop-types: 15.8.1 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) react-transition-group: 4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - use-isomorphic-layout-effect: 1.2.1(@types/react@18.3.27)(react@18.3.1) + use-isomorphic-layout-effect: 1.2.1(@types/react@18.3.28)(react@18.3.1) transitivePeerDependencies: - '@types/react' - supports-color @@ -14592,7 +14660,7 @@ snapshots: dependencies: jsesc: 0.5.0 - release-it@19.2.3(@types/node@24.0.14)(magicast@0.3.5): + release-it@19.2.4(@types/node@24.0.14)(magicast@0.3.5): dependencies: '@nodeutils/defaults-deep': 1.1.0 '@octokit/rest': 22.0.1 @@ -14613,7 +14681,7 @@ snapshots: proxy-agent: 6.5.0 semver: 7.7.3 tinyglobby: 0.2.15 - undici: 6.22.0 + undici: 6.23.0 url-join: 5.0.0 wildcard-match: 5.1.4 yargs-parser: 21.1.1 @@ -14716,9 +14784,9 @@ snapshots: dependencies: glob: 7.2.3 - rimraf@6.1.2: + rimraf@6.1.3: dependencies: - glob: 13.0.0 + glob: 13.0.6 package-json-from-dist: 1.0.1 rollup@4.52.5: @@ -15286,11 +15354,11 @@ snapshots: normalize-path: 3.0.0 object-hash: 3.0.0 picocolors: 1.1.1 - postcss: 8.5.6 - postcss-import: 15.1.0(postcss@8.5.6) - postcss-js: 4.0.1(postcss@8.5.6) - postcss-load-config: 4.0.2(postcss@8.5.6)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) - postcss-nested: 6.2.0(postcss@8.5.6) + postcss: 8.5.8 + postcss-import: 15.1.0(postcss@8.5.8) + postcss-js: 4.0.1(postcss@8.5.8) + postcss-load-config: 4.0.2(postcss@8.5.8)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) + postcss-nested: 6.2.0(postcss@8.5.8) postcss-selector-parser: 6.1.2 resolve: 1.22.10 sucrase: 3.35.0 @@ -15350,6 +15418,14 @@ snapshots: mkdirp: 1.0.4 yallist: 4.0.0 + tar@7.5.11: + dependencies: + '@isaacs/fs-minipass': 4.0.1 + chownr: 3.0.0 + minipass: 7.1.2 + minizlib: 3.1.0 + yallist: 5.0.0 + temp-fs@0.9.9: dependencies: rimraf: 2.5.4 @@ -15627,6 +15703,8 @@ snapshots: undici@6.22.0: {} + undici@6.23.0: {} + undici@7.16.0: {} unicorn-magic@0.3.0: {} @@ -15719,12 +15797,6 @@ snapshots: escalade: 3.2.0 picocolors: 1.1.1 - update-browserslist-db@1.1.4(browserslist@4.28.0): - dependencies: - browserslist: 4.28.0 - escalade: 3.2.0 - picocolors: 1.1.1 - update-browserslist-db@1.2.3(browserslist@4.28.1): dependencies: browserslist: 4.28.1 @@ -15747,11 +15819,11 @@ snapshots: urlpattern-polyfill@10.1.0: {} - use-isomorphic-layout-effect@1.2.1(@types/react@18.3.27)(react@18.3.1): + use-isomorphic-layout-effect@1.2.1(@types/react@18.3.28)(react@18.3.1): dependencies: react: 18.3.1 optionalDependencies: - '@types/react': 18.3.27 + '@types/react': 18.3.28 userhome@1.0.1: {} @@ -15877,7 +15949,7 @@ snapshots: vite@5.4.21(@types/node@24.0.14): dependencies: esbuild: 0.21.5 - postcss: 8.5.6 + postcss: 8.5.8 rollup: 4.52.5 optionalDependencies: '@types/node': 24.0.14 @@ -15886,7 +15958,7 @@ snapshots: vite@5.4.21(@types/node@24.10.1): dependencies: esbuild: 0.21.5 - postcss: 8.5.6 + postcss: 8.5.8 rollup: 4.52.5 optionalDependencies: '@types/node': 24.10.1 @@ -15951,17 +16023,17 @@ snapshots: dependencies: defaults: 1.0.4 - wdio-lambdatest-service@4.0.1(@wdio/cli@9.23.0(@types/node@24.0.14)(expect-webdriverio@5.6.1))(@wdio/types@9.20.0)(webdriverio@9.23.0): + wdio-lambdatest-service@4.0.1(@wdio/cli@9.25.0(@types/node@24.0.14)(expect-webdriverio@5.6.4))(@wdio/types@9.25.0)(webdriverio@9.25.0): dependencies: '@lambdatest/node-tunnel': 4.0.9 - '@wdio/cli': 9.23.0(@types/node@24.0.14)(expect-webdriverio@5.6.1) + '@wdio/cli': 9.25.0(@types/node@24.0.14)(expect-webdriverio@5.6.4) '@wdio/logger': 7.26.0 - '@wdio/types': 9.20.0 + '@wdio/types': 9.25.0 axios: 1.13.2 colors: 1.4.0 form-data: 4.0.5 source-map-support: 0.5.21 - webdriverio: 9.23.0 + webdriverio: 9.25.0 winston: 3.17.0 transitivePeerDependencies: - debug @@ -15977,15 +16049,15 @@ snapshots: web-streams-polyfill@4.0.0-beta.3: {} - webdriver@9.23.0: + webdriver@9.25.0: dependencies: '@types/node': 20.19.25 '@types/ws': 8.18.1 - '@wdio/config': 9.23.0 + '@wdio/config': 9.25.0 '@wdio/logger': 9.18.0 - '@wdio/protocols': 9.16.2 - '@wdio/types': 9.20.0 - '@wdio/utils': 9.23.0 + '@wdio/protocols': 9.25.0 + '@wdio/types': 9.25.0 + '@wdio/utils': 9.25.0 deepmerge-ts: 7.1.5 https-proxy-agent: 7.0.6 undici: 6.22.0 @@ -15997,16 +16069,16 @@ snapshots: - supports-color - utf-8-validate - webdriverio@9.23.0: + webdriverio@9.25.0: dependencies: '@types/node': 20.19.25 '@types/sinonjs__fake-timers': 8.1.5 - '@wdio/config': 9.23.0 + '@wdio/config': 9.25.0 '@wdio/logger': 9.18.0 - '@wdio/protocols': 9.16.2 + '@wdio/protocols': 9.25.0 '@wdio/repl': 9.16.2 - '@wdio/types': 9.20.0 - '@wdio/utils': 9.23.0 + '@wdio/types': 9.25.0 + '@wdio/utils': 9.25.0 archiver: 7.0.1 aria-query: 5.3.2 cheerio: 1.1.2 @@ -16023,7 +16095,7 @@ snapshots: rgb2hex: 0.2.5 serialize-error: 12.0.0 urlpattern-polyfill: 10.1.0 - webdriver: 9.23.0 + webdriver: 9.25.0 transitivePeerDependencies: - bare-buffer - bufferutil @@ -16187,6 +16259,8 @@ snapshots: xmlchars@2.2.0: {} + xpath@0.0.34: {} + xtend@4.0.2: {} y18n@5.0.8: {} @@ -16195,6 +16269,8 @@ snapshots: yallist@4.0.0: {} + yallist@5.0.0: {} + yaml@1.10.2: {} yaml@2.8.1: {} diff --git a/tests/configs/lambdatest.android.emus.web.ts b/tests/configs/lambdatest.android.emus.web.ts index 3f3922874..03721b11b 100644 --- a/tests/configs/lambdatest.android.emus.web.ts +++ b/tests/configs/lambdatest.android.emus.web.ts @@ -70,7 +70,6 @@ function createCaps({ mobileSpecs: string; build: string; deviceOrientation: DeviceOrientation; - }): { 'lt:options': { deviceName: string, @@ -80,12 +79,6 @@ function createCaps({ build: string, w3c: boolean, queueTimeout: number, - /** - * There are issues with the Chrome version on LT - * the installed versions sometimes give tabs as the first page. - * This should be fixed in the future and below is a workaround - */ - chromeVersion: 126, }, specs: string[]; 'wdio-ics:options': { @@ -93,6 +86,9 @@ function createCaps({ commands: string[]; }; 'wdio:enforceWebDriverClassic': boolean; + 'appium:chromeOptions': { + args: string[]; + }; } { const driverScreenshotType = 'NativeWebScreenshot' const adjustedDeviceName = deviceName !== '' ? @@ -119,6 +115,13 @@ function createCaps({ .toLowerCase()}${driverScreenshotType}${platformVersion}`, commands: wdioIcsCommands, }, - 'wdio:enforceWebDriverClassic': true + 'wdio:enforceWebDriverClassic': true, + 'appium:chromeOptions': { + args: [ + '--disable-features=StartSurfaceAndroid,GridTabSwitcherForTablets,TabGridLayout', + '--no-first-run', + '--disable-fre' + ] + } } } diff --git a/tests/configs/wdio.local.android.emus.web.conf.ts b/tests/configs/wdio.local.android.emus.web.conf.ts index cad43f783..68c4a3a57 100644 --- a/tests/configs/wdio.local.android.emus.web.conf.ts +++ b/tests/configs/wdio.local.android.emus.web.conf.ts @@ -12,8 +12,8 @@ export const config: WebdriverIO.Config = { // Capabilities // ============ capabilities: [ - // androidCaps('Pixel_8_Pro_Android_15_API_35', 'PORTRAIT', '15.0', true), - androidCaps('Pixel_8_Pro_Android_15_API_35', 'LANDSCAPE', '15.0', true), + androidCaps('Pixel_8_Pro_Android_15_API_35', 'PORTRAIT', '15.0', true), + // androidCaps('Pixel_8_Pro_Android_15_API_35', 'LANDSCAPE', '15.0', true), ], } @@ -36,6 +36,7 @@ function androidCaps( 'appium:orientation': orientation, 'appium:newCommandTimeout': 240, ...(nativeWebScreenshot ? { 'appium:nativeWebScreenshot': true } : {}), + 'wdio:enforceWebDriverClassic': true, 'wdio-ics:options': { logName: `${deviceName .split(' ') diff --git a/tests/configs/wdio.local.appium.shared.conf.ts b/tests/configs/wdio.local.appium.shared.conf.ts index 2d5d382a2..b75f3d7e3 100644 --- a/tests/configs/wdio.local.appium.shared.conf.ts +++ b/tests/configs/wdio.local.appium.shared.conf.ts @@ -7,22 +7,23 @@ export const config: Omit = { // =================== // Image compare setup // =================== + port: 4723, services: [ ...sharedConfig.services || [], - [ - 'appium', - { - // This will use the globally installed version of Appium - command: 'appium', - args: { - // This is needed to tell Appium that we can execute local ADB commands - // and to automatically download the latest version of ChromeDriver - relaxedSecurity: true, - // Write the Appium logs to a file in the root of the directory - log: './logs/appium.log', - }, - }, - ], + // [ + // 'appium', + // { + // // This will use the globally installed version of Appium + // command: 'appium', + // args: { + // // This is needed to tell Appium that we can execute local ADB commands + // // and to automatically download the latest version of ChromeDriver + // relaxedSecurity: true, + // // Write the Appium logs to a file in the root of the directory + // log: './logs/appium.log', + // }, + // }, + // ], [ 'visual', { diff --git a/tests/lambdaTestBaseline/desktop_chrome/bidiIgnoredElementsElementScreenshot-chrome-latest-320x658.png b/tests/lambdaTestBaseline/desktop_chrome/bidiIgnoredElementsElementScreenshot-chrome-latest-320x658.png new file mode 100644 index 000000000..839cb9bb5 Binary files /dev/null and b/tests/lambdaTestBaseline/desktop_chrome/bidiIgnoredElementsElementScreenshot-chrome-latest-320x658.png differ diff --git a/tests/lambdaTestBaseline/desktop_chrome/ignoredElementsElementScreenshot-chrome-latest-1366x768.png b/tests/lambdaTestBaseline/desktop_chrome/ignoredElementsElementScreenshot-chrome-latest-1366x768.png new file mode 100644 index 000000000..e450c0e7a Binary files /dev/null and b/tests/lambdaTestBaseline/desktop_chrome/ignoredElementsElementScreenshot-chrome-latest-1366x768.png differ diff --git a/tests/lambdaTestBaseline/desktop_chrome/ignoredElementsFullPageScreenshot-chrome-latest-1366x768.png b/tests/lambdaTestBaseline/desktop_chrome/ignoredElementsFullPageScreenshot-chrome-latest-1366x768.png new file mode 100644 index 000000000..d22b44489 Binary files /dev/null and b/tests/lambdaTestBaseline/desktop_chrome/ignoredElementsFullPageScreenshot-chrome-latest-1366x768.png differ diff --git a/tests/lambdaTestBaseline/desktop_chrome/ignoredElementsViewportScreenshot-chrome-latest-1366x768.png b/tests/lambdaTestBaseline/desktop_chrome/ignoredElementsViewportScreenshot-chrome-latest-1366x768.png new file mode 100644 index 000000000..14cd80de0 Binary files /dev/null and b/tests/lambdaTestBaseline/desktop_chrome/ignoredElementsViewportScreenshot-chrome-latest-1366x768.png differ diff --git a/tests/lambdaTestBaseline/desktop_chrome/ignoredElementsViewportScreenshot-chrome-latest-320x658.png b/tests/lambdaTestBaseline/desktop_chrome/ignoredElementsViewportScreenshot-chrome-latest-320x658.png new file mode 100644 index 000000000..84c82a271 Binary files /dev/null and b/tests/lambdaTestBaseline/desktop_chrome/ignoredElementsViewportScreenshot-chrome-latest-320x658.png differ diff --git a/tests/lambdaTestBaseline/desktop_chrome/legacyIgnoredElementsElementScreenshot-chrome-latest-320x658.png b/tests/lambdaTestBaseline/desktop_chrome/legacyIgnoredElementsElementScreenshot-chrome-latest-320x658.png new file mode 100644 index 000000000..fb910775b Binary files /dev/null and b/tests/lambdaTestBaseline/desktop_chrome/legacyIgnoredElementsElementScreenshot-chrome-latest-320x658.png differ diff --git a/tests/lambdaTestBaseline/desktop_firefox/ignoredElementsElementScreenshot-Firefox_latest-1366x768.png b/tests/lambdaTestBaseline/desktop_firefox/ignoredElementsElementScreenshot-Firefox_latest-1366x768.png new file mode 100644 index 000000000..53c01c60c Binary files /dev/null and b/tests/lambdaTestBaseline/desktop_firefox/ignoredElementsElementScreenshot-Firefox_latest-1366x768.png differ diff --git a/tests/lambdaTestBaseline/desktop_firefox/ignoredElementsFullPageScreenshot-Firefox_latest-1366x768.png b/tests/lambdaTestBaseline/desktop_firefox/ignoredElementsFullPageScreenshot-Firefox_latest-1366x768.png new file mode 100644 index 000000000..24cc8ad18 Binary files /dev/null and b/tests/lambdaTestBaseline/desktop_firefox/ignoredElementsFullPageScreenshot-Firefox_latest-1366x768.png differ diff --git a/tests/lambdaTestBaseline/desktop_firefox/ignoredElementsViewportScreenshot-Firefox_latest-1366x768.png b/tests/lambdaTestBaseline/desktop_firefox/ignoredElementsViewportScreenshot-Firefox_latest-1366x768.png new file mode 100644 index 000000000..ea5bf6fa1 Binary files /dev/null and b/tests/lambdaTestBaseline/desktop_firefox/ignoredElementsViewportScreenshot-Firefox_latest-1366x768.png differ diff --git a/tests/lambdaTestBaseline/desktop_microsoftedge/ignoredElementsElementScreenshot-Microsoft_Edge_latest-1366x768.png b/tests/lambdaTestBaseline/desktop_microsoftedge/ignoredElementsElementScreenshot-Microsoft_Edge_latest-1366x768.png new file mode 100644 index 000000000..6966c0f13 Binary files /dev/null and b/tests/lambdaTestBaseline/desktop_microsoftedge/ignoredElementsElementScreenshot-Microsoft_Edge_latest-1366x768.png differ diff --git a/tests/lambdaTestBaseline/desktop_microsoftedge/ignoredElementsFullPageScreenshot-Microsoft_Edge_latest-1366x768.png b/tests/lambdaTestBaseline/desktop_microsoftedge/ignoredElementsFullPageScreenshot-Microsoft_Edge_latest-1366x768.png new file mode 100644 index 000000000..6d6b2864d Binary files /dev/null and b/tests/lambdaTestBaseline/desktop_microsoftedge/ignoredElementsFullPageScreenshot-Microsoft_Edge_latest-1366x768.png differ diff --git a/tests/lambdaTestBaseline/desktop_microsoftedge/ignoredElementsViewportScreenshot-Microsoft_Edge_latest-1366x768.png b/tests/lambdaTestBaseline/desktop_microsoftedge/ignoredElementsViewportScreenshot-Microsoft_Edge_latest-1366x768.png new file mode 100644 index 000000000..d83506c5a Binary files /dev/null and b/tests/lambdaTestBaseline/desktop_microsoftedge/ignoredElementsViewportScreenshot-Microsoft_Edge_latest-1366x768.png differ diff --git a/tests/lambdaTestBaseline/desktop_safari/ignoredElementsElementScreenshot-SafariLatest-1366x768.png b/tests/lambdaTestBaseline/desktop_safari/ignoredElementsElementScreenshot-SafariLatest-1366x768.png new file mode 100644 index 000000000..ea7f6c9d4 Binary files /dev/null and b/tests/lambdaTestBaseline/desktop_safari/ignoredElementsElementScreenshot-SafariLatest-1366x768.png differ diff --git a/tests/lambdaTestBaseline/desktop_safari/ignoredElementsFullPageScreenshot-SafariLatest-1366x768.png b/tests/lambdaTestBaseline/desktop_safari/ignoredElementsFullPageScreenshot-SafariLatest-1366x768.png new file mode 100644 index 000000000..a13e756df Binary files /dev/null and b/tests/lambdaTestBaseline/desktop_safari/ignoredElementsFullPageScreenshot-SafariLatest-1366x768.png differ diff --git a/tests/lambdaTestBaseline/desktop_safari/ignoredElementsViewportScreenshot-SafariLatest-1366x768.png b/tests/lambdaTestBaseline/desktop_safari/ignoredElementsViewportScreenshot-SafariLatest-1366x768.png new file mode 100644 index 000000000..83c01b534 Binary files /dev/null and b/tests/lambdaTestBaseline/desktop_safari/ignoredElementsViewportScreenshot-SafariLatest-1366x768.png differ diff --git a/tests/lambdaTestBaseline/galaxy_tab_s8/ignoredElementsElementScreenshot-EmulatorGalaxyTabS8LandscapeNativeWebScreenshot13-1707x1067.png b/tests/lambdaTestBaseline/galaxy_tab_s8/ignoredElementsElementScreenshot-EmulatorGalaxyTabS8LandscapeNativeWebScreenshot13-1707x1067.png new file mode 100644 index 000000000..7e30adbfa Binary files /dev/null and b/tests/lambdaTestBaseline/galaxy_tab_s8/ignoredElementsElementScreenshot-EmulatorGalaxyTabS8LandscapeNativeWebScreenshot13-1707x1067.png differ diff --git a/tests/lambdaTestBaseline/galaxy_tab_s8/ignoredElementsElementScreenshot-EmulatorGalaxyTabS8LandscapeNativeWebScreenshot14-1707x1067.png b/tests/lambdaTestBaseline/galaxy_tab_s8/ignoredElementsElementScreenshot-EmulatorGalaxyTabS8LandscapeNativeWebScreenshot14-1707x1067.png new file mode 100644 index 000000000..7e30adbfa Binary files /dev/null and b/tests/lambdaTestBaseline/galaxy_tab_s8/ignoredElementsElementScreenshot-EmulatorGalaxyTabS8LandscapeNativeWebScreenshot14-1707x1067.png differ diff --git a/tests/lambdaTestBaseline/galaxy_tab_s8/ignoredElementsElementScreenshot-EmulatorGalaxyTabS8PortraitNativeWebScreenshot13-1067x1707.png b/tests/lambdaTestBaseline/galaxy_tab_s8/ignoredElementsElementScreenshot-EmulatorGalaxyTabS8PortraitNativeWebScreenshot13-1067x1707.png new file mode 100644 index 000000000..2637a388f Binary files /dev/null and b/tests/lambdaTestBaseline/galaxy_tab_s8/ignoredElementsElementScreenshot-EmulatorGalaxyTabS8PortraitNativeWebScreenshot13-1067x1707.png differ diff --git a/tests/lambdaTestBaseline/galaxy_tab_s8/ignoredElementsElementScreenshot-EmulatorGalaxyTabS8PortraitNativeWebScreenshot14-1067x1707.png b/tests/lambdaTestBaseline/galaxy_tab_s8/ignoredElementsElementScreenshot-EmulatorGalaxyTabS8PortraitNativeWebScreenshot14-1067x1707.png new file mode 100644 index 000000000..2637a388f Binary files /dev/null and b/tests/lambdaTestBaseline/galaxy_tab_s8/ignoredElementsElementScreenshot-EmulatorGalaxyTabS8PortraitNativeWebScreenshot14-1067x1707.png differ diff --git a/tests/lambdaTestBaseline/galaxy_tab_s8/ignoredElementsFullPageScreenshot-EmulatorGalaxyTabS8LandscapeNativeWebScreenshot13-1707x1067.png b/tests/lambdaTestBaseline/galaxy_tab_s8/ignoredElementsFullPageScreenshot-EmulatorGalaxyTabS8LandscapeNativeWebScreenshot13-1707x1067.png new file mode 100644 index 000000000..af59dd512 Binary files /dev/null and b/tests/lambdaTestBaseline/galaxy_tab_s8/ignoredElementsFullPageScreenshot-EmulatorGalaxyTabS8LandscapeNativeWebScreenshot13-1707x1067.png differ diff --git a/tests/lambdaTestBaseline/galaxy_tab_s8/ignoredElementsFullPageScreenshot-EmulatorGalaxyTabS8LandscapeNativeWebScreenshot14-1707x1067.png b/tests/lambdaTestBaseline/galaxy_tab_s8/ignoredElementsFullPageScreenshot-EmulatorGalaxyTabS8LandscapeNativeWebScreenshot14-1707x1067.png new file mode 100644 index 000000000..a726c3126 Binary files /dev/null and b/tests/lambdaTestBaseline/galaxy_tab_s8/ignoredElementsFullPageScreenshot-EmulatorGalaxyTabS8LandscapeNativeWebScreenshot14-1707x1067.png differ diff --git a/tests/lambdaTestBaseline/galaxy_tab_s8/ignoredElementsFullPageScreenshot-EmulatorGalaxyTabS8PortraitNativeWebScreenshot13-1067x1707.png b/tests/lambdaTestBaseline/galaxy_tab_s8/ignoredElementsFullPageScreenshot-EmulatorGalaxyTabS8PortraitNativeWebScreenshot13-1067x1707.png new file mode 100644 index 000000000..6d5375fd6 Binary files /dev/null and b/tests/lambdaTestBaseline/galaxy_tab_s8/ignoredElementsFullPageScreenshot-EmulatorGalaxyTabS8PortraitNativeWebScreenshot13-1067x1707.png differ diff --git a/tests/lambdaTestBaseline/galaxy_tab_s8/ignoredElementsFullPageScreenshot-EmulatorGalaxyTabS8PortraitNativeWebScreenshot14-1067x1707.png b/tests/lambdaTestBaseline/galaxy_tab_s8/ignoredElementsFullPageScreenshot-EmulatorGalaxyTabS8PortraitNativeWebScreenshot14-1067x1707.png new file mode 100644 index 000000000..6d5375fd6 Binary files /dev/null and b/tests/lambdaTestBaseline/galaxy_tab_s8/ignoredElementsFullPageScreenshot-EmulatorGalaxyTabS8PortraitNativeWebScreenshot14-1067x1707.png differ diff --git a/tests/lambdaTestBaseline/galaxy_tab_s8/ignoredElementsScreenshot-EmulatorGalaxyTabS8LandscapeNativeWebScreenshot13-1707x1067.png b/tests/lambdaTestBaseline/galaxy_tab_s8/ignoredElementsScreenshot-EmulatorGalaxyTabS8LandscapeNativeWebScreenshot13-1707x1067.png new file mode 100644 index 000000000..9207de149 Binary files /dev/null and b/tests/lambdaTestBaseline/galaxy_tab_s8/ignoredElementsScreenshot-EmulatorGalaxyTabS8LandscapeNativeWebScreenshot13-1707x1067.png differ diff --git a/tests/lambdaTestBaseline/galaxy_tab_s8/ignoredElementsScreenshot-EmulatorGalaxyTabS8LandscapeNativeWebScreenshot14-1707x1067.png b/tests/lambdaTestBaseline/galaxy_tab_s8/ignoredElementsScreenshot-EmulatorGalaxyTabS8LandscapeNativeWebScreenshot14-1707x1067.png new file mode 100644 index 000000000..45ffbf8c8 Binary files /dev/null and b/tests/lambdaTestBaseline/galaxy_tab_s8/ignoredElementsScreenshot-EmulatorGalaxyTabS8LandscapeNativeWebScreenshot14-1707x1067.png differ diff --git a/tests/lambdaTestBaseline/galaxy_tab_s8/ignoredElementsScreenshot-EmulatorGalaxyTabS8PortraitNativeWebScreenshot13-1067x1707.png b/tests/lambdaTestBaseline/galaxy_tab_s8/ignoredElementsScreenshot-EmulatorGalaxyTabS8PortraitNativeWebScreenshot13-1067x1707.png new file mode 100644 index 000000000..5572a6797 Binary files /dev/null and b/tests/lambdaTestBaseline/galaxy_tab_s8/ignoredElementsScreenshot-EmulatorGalaxyTabS8PortraitNativeWebScreenshot13-1067x1707.png differ diff --git a/tests/lambdaTestBaseline/galaxy_tab_s8/ignoredElementsScreenshot-EmulatorGalaxyTabS8PortraitNativeWebScreenshot14-1067x1707.png b/tests/lambdaTestBaseline/galaxy_tab_s8/ignoredElementsScreenshot-EmulatorGalaxyTabS8PortraitNativeWebScreenshot14-1067x1707.png new file mode 100644 index 000000000..45c5369cd Binary files /dev/null and b/tests/lambdaTestBaseline/galaxy_tab_s8/ignoredElementsScreenshot-EmulatorGalaxyTabS8PortraitNativeWebScreenshot14-1067x1707.png differ diff --git a/tests/lambdaTestBaseline/galaxy_tab_s8/screenshot-EmulatorGalaxyTabS8LandscapeNativeWebScreenshot13-1067x1707.png b/tests/lambdaTestBaseline/galaxy_tab_s8/screenshot-EmulatorGalaxyTabS8LandscapeNativeWebScreenshot13-1067x1707.png new file mode 100644 index 000000000..9b9b7dffa Binary files /dev/null and b/tests/lambdaTestBaseline/galaxy_tab_s8/screenshot-EmulatorGalaxyTabS8LandscapeNativeWebScreenshot13-1067x1707.png differ diff --git a/tests/lambdaTestBaseline/galaxy_tab_s8/screenshot-EmulatorGalaxyTabS8LandscapeNativeWebScreenshot14-1067x1707.png b/tests/lambdaTestBaseline/galaxy_tab_s8/screenshot-EmulatorGalaxyTabS8LandscapeNativeWebScreenshot14-1067x1707.png new file mode 100644 index 000000000..da7ff378c Binary files /dev/null and b/tests/lambdaTestBaseline/galaxy_tab_s8/screenshot-EmulatorGalaxyTabS8LandscapeNativeWebScreenshot14-1067x1707.png differ diff --git a/tests/lambdaTestBaseline/galaxy_tab_s8/screenshot-EmulatorGalaxyTabS8LandscapeNativeWebScreenshot14-1707x1067.png b/tests/lambdaTestBaseline/galaxy_tab_s8/screenshot-EmulatorGalaxyTabS8LandscapeNativeWebScreenshot14-1707x1067.png index 715652cdf..d419eae42 100644 Binary files a/tests/lambdaTestBaseline/galaxy_tab_s8/screenshot-EmulatorGalaxyTabS8LandscapeNativeWebScreenshot14-1707x1067.png and b/tests/lambdaTestBaseline/galaxy_tab_s8/screenshot-EmulatorGalaxyTabS8LandscapeNativeWebScreenshot14-1707x1067.png differ diff --git a/tests/lambdaTestBaseline/galaxy_tab_s8/screenshot-EmulatorGalaxyTabS8PortraitNativeWebScreenshot13-1707x1067.png b/tests/lambdaTestBaseline/galaxy_tab_s8/screenshot-EmulatorGalaxyTabS8PortraitNativeWebScreenshot13-1707x1067.png new file mode 100644 index 000000000..424d34a4a Binary files /dev/null and b/tests/lambdaTestBaseline/galaxy_tab_s8/screenshot-EmulatorGalaxyTabS8PortraitNativeWebScreenshot13-1707x1067.png differ diff --git a/tests/lambdaTestBaseline/galaxy_tab_s8/screenshot-EmulatorGalaxyTabS8PortraitNativeWebScreenshot14-1067x1707.png b/tests/lambdaTestBaseline/galaxy_tab_s8/screenshot-EmulatorGalaxyTabS8PortraitNativeWebScreenshot14-1067x1707.png index 46e57cc1c..5aa39e3bb 100644 Binary files a/tests/lambdaTestBaseline/galaxy_tab_s8/screenshot-EmulatorGalaxyTabS8PortraitNativeWebScreenshot14-1067x1707.png and b/tests/lambdaTestBaseline/galaxy_tab_s8/screenshot-EmulatorGalaxyTabS8PortraitNativeWebScreenshot14-1067x1707.png differ diff --git a/tests/lambdaTestBaseline/iphone_13_mini/ignoredElementsElementScreenshot-Iphone13MiniLandscape17-375x812.png b/tests/lambdaTestBaseline/iphone_13_mini/ignoredElementsElementScreenshot-Iphone13MiniLandscape17-375x812.png new file mode 100644 index 000000000..a0bd1e81a Binary files /dev/null and b/tests/lambdaTestBaseline/iphone_13_mini/ignoredElementsElementScreenshot-Iphone13MiniLandscape17-375x812.png differ diff --git a/tests/lambdaTestBaseline/iphone_13_mini/ignoredElementsElementScreenshot-Iphone13MiniPortrait17-375x812.png b/tests/lambdaTestBaseline/iphone_13_mini/ignoredElementsElementScreenshot-Iphone13MiniPortrait17-375x812.png new file mode 100644 index 000000000..adeba0008 Binary files /dev/null and b/tests/lambdaTestBaseline/iphone_13_mini/ignoredElementsElementScreenshot-Iphone13MiniPortrait17-375x812.png differ diff --git a/tests/lambdaTestBaseline/iphone_13_mini/ignoredElementsFullPageScreenshot-Iphone13MiniLandscape17-375x812.png b/tests/lambdaTestBaseline/iphone_13_mini/ignoredElementsFullPageScreenshot-Iphone13MiniLandscape17-375x812.png new file mode 100644 index 000000000..183d604c9 Binary files /dev/null and b/tests/lambdaTestBaseline/iphone_13_mini/ignoredElementsFullPageScreenshot-Iphone13MiniLandscape17-375x812.png differ diff --git a/tests/lambdaTestBaseline/iphone_13_mini/ignoredElementsFullPageScreenshot-Iphone13MiniPortrait17-375x812.png b/tests/lambdaTestBaseline/iphone_13_mini/ignoredElementsFullPageScreenshot-Iphone13MiniPortrait17-375x812.png new file mode 100644 index 000000000..235df92c8 Binary files /dev/null and b/tests/lambdaTestBaseline/iphone_13_mini/ignoredElementsFullPageScreenshot-Iphone13MiniPortrait17-375x812.png differ diff --git a/tests/lambdaTestBaseline/iphone_13_mini/ignoredElementsScreenshot-Iphone13MiniPortrait17-375x812.png b/tests/lambdaTestBaseline/iphone_13_mini/ignoredElementsScreenshot-Iphone13MiniPortrait17-375x812.png new file mode 100644 index 000000000..865335b5a Binary files /dev/null and b/tests/lambdaTestBaseline/iphone_13_mini/ignoredElementsScreenshot-Iphone13MiniPortrait17-375x812.png differ diff --git a/tests/lambdaTestBaseline/iphone_13_pro/ignoredElementsElementScreenshot-Iphone13ProLandscape16-390x844.png b/tests/lambdaTestBaseline/iphone_13_pro/ignoredElementsElementScreenshot-Iphone13ProLandscape16-390x844.png new file mode 100644 index 000000000..e79b4bbd0 Binary files /dev/null and b/tests/lambdaTestBaseline/iphone_13_pro/ignoredElementsElementScreenshot-Iphone13ProLandscape16-390x844.png differ diff --git a/tests/lambdaTestBaseline/iphone_13_pro/ignoredElementsElementScreenshot-Iphone13ProPortrait16-390x844.png b/tests/lambdaTestBaseline/iphone_13_pro/ignoredElementsElementScreenshot-Iphone13ProPortrait16-390x844.png new file mode 100644 index 000000000..df439fa90 Binary files /dev/null and b/tests/lambdaTestBaseline/iphone_13_pro/ignoredElementsElementScreenshot-Iphone13ProPortrait16-390x844.png differ diff --git a/tests/lambdaTestBaseline/iphone_13_pro/ignoredElementsFullPageScreenshot-Iphone13ProLandscape16-390x844.png b/tests/lambdaTestBaseline/iphone_13_pro/ignoredElementsFullPageScreenshot-Iphone13ProLandscape16-390x844.png new file mode 100644 index 000000000..212b84256 Binary files /dev/null and b/tests/lambdaTestBaseline/iphone_13_pro/ignoredElementsFullPageScreenshot-Iphone13ProLandscape16-390x844.png differ diff --git a/tests/lambdaTestBaseline/iphone_13_pro/ignoredElementsFullPageScreenshot-Iphone13ProPortrait16-390x844.png b/tests/lambdaTestBaseline/iphone_13_pro/ignoredElementsFullPageScreenshot-Iphone13ProPortrait16-390x844.png new file mode 100644 index 000000000..62a379d7b Binary files /dev/null and b/tests/lambdaTestBaseline/iphone_13_pro/ignoredElementsFullPageScreenshot-Iphone13ProPortrait16-390x844.png differ diff --git a/tests/lambdaTestBaseline/iphone_13_pro/ignoredElementsScreenshot-Iphone13ProPortrait16-390x844.png b/tests/lambdaTestBaseline/iphone_13_pro/ignoredElementsScreenshot-Iphone13ProPortrait16-390x844.png new file mode 100644 index 000000000..b30f942fd Binary files /dev/null and b/tests/lambdaTestBaseline/iphone_13_pro/ignoredElementsScreenshot-Iphone13ProPortrait16-390x844.png differ diff --git a/tests/lambdaTestBaseline/iphone_14_pro/ignoredElementsElementScreenshot-Iphone14ProLandscape17-393x852.png b/tests/lambdaTestBaseline/iphone_14_pro/ignoredElementsElementScreenshot-Iphone14ProLandscape17-393x852.png new file mode 100644 index 000000000..f27bd6d81 Binary files /dev/null and b/tests/lambdaTestBaseline/iphone_14_pro/ignoredElementsElementScreenshot-Iphone14ProLandscape17-393x852.png differ diff --git a/tests/lambdaTestBaseline/iphone_14_pro/ignoredElementsElementScreenshot-Iphone14ProPortrait17-393x852.png b/tests/lambdaTestBaseline/iphone_14_pro/ignoredElementsElementScreenshot-Iphone14ProPortrait17-393x852.png new file mode 100644 index 000000000..3b544822c Binary files /dev/null and b/tests/lambdaTestBaseline/iphone_14_pro/ignoredElementsElementScreenshot-Iphone14ProPortrait17-393x852.png differ diff --git a/tests/lambdaTestBaseline/iphone_14_pro/ignoredElementsFullPageScreenshot-Iphone14ProLandscape17-393x852.png b/tests/lambdaTestBaseline/iphone_14_pro/ignoredElementsFullPageScreenshot-Iphone14ProLandscape17-393x852.png new file mode 100644 index 000000000..6bb25ab7b Binary files /dev/null and b/tests/lambdaTestBaseline/iphone_14_pro/ignoredElementsFullPageScreenshot-Iphone14ProLandscape17-393x852.png differ diff --git a/tests/lambdaTestBaseline/iphone_14_pro/ignoredElementsFullPageScreenshot-Iphone14ProPortrait17-393x852.png b/tests/lambdaTestBaseline/iphone_14_pro/ignoredElementsFullPageScreenshot-Iphone14ProPortrait17-393x852.png new file mode 100644 index 000000000..f6ad62ebc Binary files /dev/null and b/tests/lambdaTestBaseline/iphone_14_pro/ignoredElementsFullPageScreenshot-Iphone14ProPortrait17-393x852.png differ diff --git a/tests/lambdaTestBaseline/iphone_14_pro/ignoredElementsScreenshot-Iphone14ProPortrait17-393x852.png b/tests/lambdaTestBaseline/iphone_14_pro/ignoredElementsScreenshot-Iphone14ProPortrait17-393x852.png new file mode 100644 index 000000000..ddabb8a9e Binary files /dev/null and b/tests/lambdaTestBaseline/iphone_14_pro/ignoredElementsScreenshot-Iphone14ProPortrait17-393x852.png differ diff --git a/tests/lambdaTestBaseline/iphone_15_pro_max/ignoredElementsElementScreenshot-Iphone15ProMaxLandscape18-430x932.png b/tests/lambdaTestBaseline/iphone_15_pro_max/ignoredElementsElementScreenshot-Iphone15ProMaxLandscape18-430x932.png new file mode 100644 index 000000000..1c5603cdb Binary files /dev/null and b/tests/lambdaTestBaseline/iphone_15_pro_max/ignoredElementsElementScreenshot-Iphone15ProMaxLandscape18-430x932.png differ diff --git a/tests/lambdaTestBaseline/iphone_15_pro_max/ignoredElementsElementScreenshot-Iphone15ProMaxPortrait18-430x932.png b/tests/lambdaTestBaseline/iphone_15_pro_max/ignoredElementsElementScreenshot-Iphone15ProMaxPortrait18-430x932.png new file mode 100644 index 000000000..c38539ed0 Binary files /dev/null and b/tests/lambdaTestBaseline/iphone_15_pro_max/ignoredElementsElementScreenshot-Iphone15ProMaxPortrait18-430x932.png differ diff --git a/tests/lambdaTestBaseline/iphone_15_pro_max/ignoredElementsFullPageScreenshot-Iphone15ProMaxLandscape18-430x932.png b/tests/lambdaTestBaseline/iphone_15_pro_max/ignoredElementsFullPageScreenshot-Iphone15ProMaxLandscape18-430x932.png new file mode 100644 index 000000000..d7d4cd5f3 Binary files /dev/null and b/tests/lambdaTestBaseline/iphone_15_pro_max/ignoredElementsFullPageScreenshot-Iphone15ProMaxLandscape18-430x932.png differ diff --git a/tests/lambdaTestBaseline/iphone_15_pro_max/ignoredElementsFullPageScreenshot-Iphone15ProMaxPortrait18-430x932.png b/tests/lambdaTestBaseline/iphone_15_pro_max/ignoredElementsFullPageScreenshot-Iphone15ProMaxPortrait18-430x932.png new file mode 100644 index 000000000..56df7dafa Binary files /dev/null and b/tests/lambdaTestBaseline/iphone_15_pro_max/ignoredElementsFullPageScreenshot-Iphone15ProMaxPortrait18-430x932.png differ diff --git a/tests/lambdaTestBaseline/iphone_15_pro_max/ignoredElementsScreenshot-Iphone15ProMaxPortrait18-430x932.png b/tests/lambdaTestBaseline/iphone_15_pro_max/ignoredElementsScreenshot-Iphone15ProMaxPortrait18-430x932.png new file mode 100644 index 000000000..7d0388dd9 Binary files /dev/null and b/tests/lambdaTestBaseline/iphone_15_pro_max/ignoredElementsScreenshot-Iphone15ProMaxPortrait18-430x932.png differ diff --git a/tests/lambdaTestBaseline/pixel_4/ignoredElementsElementScreenshot-EmulatorPixel4LandscapeNativeWebScreenshot11-652x309.png b/tests/lambdaTestBaseline/pixel_4/ignoredElementsElementScreenshot-EmulatorPixel4LandscapeNativeWebScreenshot11-652x309.png new file mode 100644 index 000000000..76c980326 Binary files /dev/null and b/tests/lambdaTestBaseline/pixel_4/ignoredElementsElementScreenshot-EmulatorPixel4LandscapeNativeWebScreenshot11-652x309.png differ diff --git a/tests/lambdaTestBaseline/pixel_4/ignoredElementsElementScreenshot-EmulatorPixel4LandscapeNativeWebScreenshot12-760x360.png b/tests/lambdaTestBaseline/pixel_4/ignoredElementsElementScreenshot-EmulatorPixel4LandscapeNativeWebScreenshot12-760x360.png new file mode 100644 index 000000000..b15af3df1 Binary files /dev/null and b/tests/lambdaTestBaseline/pixel_4/ignoredElementsElementScreenshot-EmulatorPixel4LandscapeNativeWebScreenshot12-760x360.png differ diff --git a/tests/lambdaTestBaseline/pixel_4/ignoredElementsElementScreenshot-EmulatorPixel4LandscapeNativeWebScreenshot13-760x360.png b/tests/lambdaTestBaseline/pixel_4/ignoredElementsElementScreenshot-EmulatorPixel4LandscapeNativeWebScreenshot13-760x360.png new file mode 100644 index 000000000..f4f36a6a8 Binary files /dev/null and b/tests/lambdaTestBaseline/pixel_4/ignoredElementsElementScreenshot-EmulatorPixel4LandscapeNativeWebScreenshot13-760x360.png differ diff --git a/tests/lambdaTestBaseline/pixel_4/ignoredElementsElementScreenshot-EmulatorPixel4PortraitNativeWebScreenshot11-309x652.png b/tests/lambdaTestBaseline/pixel_4/ignoredElementsElementScreenshot-EmulatorPixel4PortraitNativeWebScreenshot11-309x652.png new file mode 100644 index 000000000..08dfa68de Binary files /dev/null and b/tests/lambdaTestBaseline/pixel_4/ignoredElementsElementScreenshot-EmulatorPixel4PortraitNativeWebScreenshot11-309x652.png differ diff --git a/tests/lambdaTestBaseline/pixel_4/ignoredElementsElementScreenshot-EmulatorPixel4PortraitNativeWebScreenshot12-360x760.png b/tests/lambdaTestBaseline/pixel_4/ignoredElementsElementScreenshot-EmulatorPixel4PortraitNativeWebScreenshot12-360x760.png new file mode 100644 index 000000000..6ba4e6bf6 Binary files /dev/null and b/tests/lambdaTestBaseline/pixel_4/ignoredElementsElementScreenshot-EmulatorPixel4PortraitNativeWebScreenshot12-360x760.png differ diff --git a/tests/lambdaTestBaseline/pixel_4/ignoredElementsElementScreenshot-EmulatorPixel4PortraitNativeWebScreenshot13-360x760.png b/tests/lambdaTestBaseline/pixel_4/ignoredElementsElementScreenshot-EmulatorPixel4PortraitNativeWebScreenshot13-360x760.png new file mode 100644 index 000000000..0bb88860f Binary files /dev/null and b/tests/lambdaTestBaseline/pixel_4/ignoredElementsElementScreenshot-EmulatorPixel4PortraitNativeWebScreenshot13-360x760.png differ diff --git a/tests/lambdaTestBaseline/pixel_4/ignoredElementsFullPageScreenshot-EmulatorPixel4LandscapeNativeWebScreenshot11-652x309.png b/tests/lambdaTestBaseline/pixel_4/ignoredElementsFullPageScreenshot-EmulatorPixel4LandscapeNativeWebScreenshot11-652x309.png new file mode 100644 index 000000000..293a7c0ee Binary files /dev/null and b/tests/lambdaTestBaseline/pixel_4/ignoredElementsFullPageScreenshot-EmulatorPixel4LandscapeNativeWebScreenshot11-652x309.png differ diff --git a/tests/lambdaTestBaseline/pixel_4/ignoredElementsFullPageScreenshot-EmulatorPixel4LandscapeNativeWebScreenshot12-760x360.png b/tests/lambdaTestBaseline/pixel_4/ignoredElementsFullPageScreenshot-EmulatorPixel4LandscapeNativeWebScreenshot12-760x360.png new file mode 100644 index 000000000..daae30736 Binary files /dev/null and b/tests/lambdaTestBaseline/pixel_4/ignoredElementsFullPageScreenshot-EmulatorPixel4LandscapeNativeWebScreenshot12-760x360.png differ diff --git a/tests/lambdaTestBaseline/pixel_4/ignoredElementsFullPageScreenshot-EmulatorPixel4LandscapeNativeWebScreenshot13-760x360.png b/tests/lambdaTestBaseline/pixel_4/ignoredElementsFullPageScreenshot-EmulatorPixel4LandscapeNativeWebScreenshot13-760x360.png new file mode 100644 index 000000000..9e85b325c Binary files /dev/null and b/tests/lambdaTestBaseline/pixel_4/ignoredElementsFullPageScreenshot-EmulatorPixel4LandscapeNativeWebScreenshot13-760x360.png differ diff --git a/tests/lambdaTestBaseline/pixel_4/ignoredElementsFullPageScreenshot-EmulatorPixel4PortraitNativeWebScreenshot11-309x652.png b/tests/lambdaTestBaseline/pixel_4/ignoredElementsFullPageScreenshot-EmulatorPixel4PortraitNativeWebScreenshot11-309x652.png new file mode 100644 index 000000000..9cbe3e426 Binary files /dev/null and b/tests/lambdaTestBaseline/pixel_4/ignoredElementsFullPageScreenshot-EmulatorPixel4PortraitNativeWebScreenshot11-309x652.png differ diff --git a/tests/lambdaTestBaseline/pixel_4/ignoredElementsFullPageScreenshot-EmulatorPixel4PortraitNativeWebScreenshot12-360x760.png b/tests/lambdaTestBaseline/pixel_4/ignoredElementsFullPageScreenshot-EmulatorPixel4PortraitNativeWebScreenshot12-360x760.png new file mode 100644 index 000000000..05c6e1bd6 Binary files /dev/null and b/tests/lambdaTestBaseline/pixel_4/ignoredElementsFullPageScreenshot-EmulatorPixel4PortraitNativeWebScreenshot12-360x760.png differ diff --git a/tests/lambdaTestBaseline/pixel_4/ignoredElementsFullPageScreenshot-EmulatorPixel4PortraitNativeWebScreenshot13-360x760.png b/tests/lambdaTestBaseline/pixel_4/ignoredElementsFullPageScreenshot-EmulatorPixel4PortraitNativeWebScreenshot13-360x760.png new file mode 100644 index 000000000..8bb3174ed Binary files /dev/null and b/tests/lambdaTestBaseline/pixel_4/ignoredElementsFullPageScreenshot-EmulatorPixel4PortraitNativeWebScreenshot13-360x760.png differ diff --git a/tests/lambdaTestBaseline/pixel_4/ignoredElementsScreenshot-EmulatorPixel4PortraitNativeWebScreenshot12-360x760.png b/tests/lambdaTestBaseline/pixel_4/ignoredElementsScreenshot-EmulatorPixel4PortraitNativeWebScreenshot12-360x760.png new file mode 100644 index 000000000..b4e1cb9fc Binary files /dev/null and b/tests/lambdaTestBaseline/pixel_4/ignoredElementsScreenshot-EmulatorPixel4PortraitNativeWebScreenshot12-360x760.png differ diff --git a/tests/lambdaTestBaseline/pixel_4/ignoredElementsScreenshot-EmulatorPixel4PortraitNativeWebScreenshot13-360x760.png b/tests/lambdaTestBaseline/pixel_4/ignoredElementsScreenshot-EmulatorPixel4PortraitNativeWebScreenshot13-360x760.png new file mode 100644 index 000000000..61b3f9849 Binary files /dev/null and b/tests/lambdaTestBaseline/pixel_4/ignoredElementsScreenshot-EmulatorPixel4PortraitNativeWebScreenshot13-360x760.png differ diff --git a/tests/lambdaTestBaseline/pixel_4/screenshot-EmulatorPixel4LandscapeNativeWebScreenshot13-360x760.png b/tests/lambdaTestBaseline/pixel_4/screenshot-EmulatorPixel4LandscapeNativeWebScreenshot13-360x760.png new file mode 100644 index 000000000..43ea18722 Binary files /dev/null and b/tests/lambdaTestBaseline/pixel_4/screenshot-EmulatorPixel4LandscapeNativeWebScreenshot13-360x760.png differ diff --git a/tests/lambdaTestBaseline/pixel_4/screenshot-EmulatorPixel4PortraitNativeWebScreenshot13-760x360.png b/tests/lambdaTestBaseline/pixel_4/screenshot-EmulatorPixel4PortraitNativeWebScreenshot13-760x360.png new file mode 100644 index 000000000..c26e1a6d2 Binary files /dev/null and b/tests/lambdaTestBaseline/pixel_4/screenshot-EmulatorPixel4PortraitNativeWebScreenshot13-760x360.png differ diff --git a/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsElementScreenshot-EmulatorPixel9ProLandscapeNativeWebScreenshot14-952x427.png b/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsElementScreenshot-EmulatorPixel9ProLandscapeNativeWebScreenshot14-952x427.png new file mode 100644 index 000000000..71e675979 Binary files /dev/null and b/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsElementScreenshot-EmulatorPixel9ProLandscapeNativeWebScreenshot14-952x427.png differ diff --git a/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsElementScreenshot-EmulatorPixel9ProLandscapeNativeWebScreenshot15-952x427.png b/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsElementScreenshot-EmulatorPixel9ProLandscapeNativeWebScreenshot15-952x427.png new file mode 100644 index 000000000..e4a352287 Binary files /dev/null and b/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsElementScreenshot-EmulatorPixel9ProLandscapeNativeWebScreenshot15-952x427.png differ diff --git a/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsElementScreenshot-EmulatorPixel9ProPortraitNativeWebScreenshot14-427x952.png b/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsElementScreenshot-EmulatorPixel9ProPortraitNativeWebScreenshot14-427x952.png new file mode 100644 index 000000000..ffe58ac19 Binary files /dev/null and b/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsElementScreenshot-EmulatorPixel9ProPortraitNativeWebScreenshot14-427x952.png differ diff --git a/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsElementScreenshot-EmulatorPixel9ProPortraitNativeWebScreenshot15-427x952.png b/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsElementScreenshot-EmulatorPixel9ProPortraitNativeWebScreenshot15-427x952.png new file mode 100644 index 000000000..60d958986 Binary files /dev/null and b/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsElementScreenshot-EmulatorPixel9ProPortraitNativeWebScreenshot15-427x952.png differ diff --git a/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsFullPageScreenshot-EmulatorPixel9ProLandscapeNativeWebScreenshot14-952x427.png b/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsFullPageScreenshot-EmulatorPixel9ProLandscapeNativeWebScreenshot14-952x427.png new file mode 100644 index 000000000..f12b1e8dd Binary files /dev/null and b/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsFullPageScreenshot-EmulatorPixel9ProLandscapeNativeWebScreenshot14-952x427.png differ diff --git a/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsFullPageScreenshot-EmulatorPixel9ProLandscapeNativeWebScreenshot15-952x427.png b/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsFullPageScreenshot-EmulatorPixel9ProLandscapeNativeWebScreenshot15-952x427.png new file mode 100644 index 000000000..a9ab875cb Binary files /dev/null and b/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsFullPageScreenshot-EmulatorPixel9ProLandscapeNativeWebScreenshot15-952x427.png differ diff --git a/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsFullPageScreenshot-EmulatorPixel9ProPortraitNativeWebScreenshot14-427x952.png b/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsFullPageScreenshot-EmulatorPixel9ProPortraitNativeWebScreenshot14-427x952.png new file mode 100644 index 000000000..2eae258b2 Binary files /dev/null and b/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsFullPageScreenshot-EmulatorPixel9ProPortraitNativeWebScreenshot14-427x952.png differ diff --git a/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsFullPageScreenshot-EmulatorPixel9ProPortraitNativeWebScreenshot15-427x952.png b/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsFullPageScreenshot-EmulatorPixel9ProPortraitNativeWebScreenshot15-427x952.png new file mode 100644 index 000000000..f23f22575 Binary files /dev/null and b/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsFullPageScreenshot-EmulatorPixel9ProPortraitNativeWebScreenshot15-427x952.png differ diff --git a/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsScreenshot-EmulatorPixel9ProPortraitNativeWebScreenshot14-427x952.png b/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsScreenshot-EmulatorPixel9ProPortraitNativeWebScreenshot14-427x952.png new file mode 100644 index 000000000..bd1ce2a29 Binary files /dev/null and b/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsScreenshot-EmulatorPixel9ProPortraitNativeWebScreenshot14-427x952.png differ diff --git a/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsScreenshot-EmulatorPixel9ProPortraitNativeWebScreenshot15-427x952.png b/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsScreenshot-EmulatorPixel9ProPortraitNativeWebScreenshot15-427x952.png new file mode 100644 index 000000000..2ef491303 Binary files /dev/null and b/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsScreenshot-EmulatorPixel9ProPortraitNativeWebScreenshot15-427x952.png differ diff --git a/tests/specs/desktop.bidi.emulated.spec.ts b/tests/specs/desktop.bidi.emulated.spec.ts index 5734b726d..51aaa6d07 100644 --- a/tests/specs/desktop.bidi.emulated.spec.ts +++ b/tests/specs/desktop.bidi.emulated.spec.ts @@ -11,7 +11,40 @@ describe('@wdio/visual-service desktop bidi emulated', () => { }) it(`should compare an element successful with a baseline for '${browserName}'`, async function() { - await expect($('.hero__title-logo')).toMatchElementSnapshot('bidiEmulatedWdioLogo') + await expect($('.hero__title-logo')).toMatchElementSnapshot( + 'bidiEmulatedWdioLogo', { + hideElements: [await $('nav.navbar')] + } + ) + }) + + it(`should compare an element screenshot with ignore elements successful with a baseline for '${browserName}'`, async function () { + await $('.features_vqN4').scrollIntoView() + + // When running a new set of images then first comment out block 1 and 2. Then run the test. + // Then uncomment block 1, check if they fail with `--store-diffs` as an extra argument. + // If so, then uncomment block 2 and check if pass with the same arguments. + // Block 1 + await browser.execute(() => { + document.querySelectorAll('.feature_G9wp h3').forEach(heading => { + (heading as HTMLElement).style.backgroundColor = 'var(--ifm-color-primary)' + }) + }) + + await expect($('.features_vqN4')).toMatchElementSnapshot( + 'bidiIgnoredElementsElementScreenshot', + { + // Block 2 + ignore: [ + await $$('.feature_G9wp h3'), + ], + // Some padding to make sure that we cover the element, + // with BiDi we sometimes miss the element due to internal calculations + ignoreRegionPadding: 2, + // Don't comment this out, it's needed to hide the navbar + hideElements: [await $('nav.navbar')] + } + ) }) it(`should compare a viewport screenshot successful with a baseline for '${browserName}'`, async function () { @@ -32,6 +65,33 @@ describe('@wdio/visual-service desktop bidi emulated', () => { }) }) + it(`should compare an element screenshot with ignore elements successful with a baseline for '${browserName}' with the legacy API`, async function () { + await $('.features_vqN4').scrollIntoView() + + // When running a new set of images then first comment out block 1 and 2. Then run the test. + // Then uncomment block 1, check if they fail with `--store-diffs` as an extra argument. + // If so, then uncomment block 2 and check if pass with the same arguments. + // Block 1 + await browser.execute(() => { + document.querySelectorAll('.feature_G9wp h3').forEach(heading => { + (heading as HTMLElement).style.backgroundColor = 'var(--ifm-color-primary)' + }) + }) + + await expect($('.features_vqN4')).toMatchElementSnapshot( + 'legacyIgnoredElementsElementScreenshot', + { + enableLegacyScreenshotMethod: true, + // Block 2 + ignore: [ + await $$('.feature_G9wp h3'), + ], + // Don't comment this out, it's needed to hide the navbar + hideElements: [await $('nav.navbar')] + } + ) + }) + it(`should compare a viewport screenshot successful with a baseline for '${browserName}' with the legacy API`, async function () { await expect(browser).toMatchScreenSnapshot('legacyEmulatedViewportScreenshot', { enableLegacyScreenshotMethod: true }) }) diff --git a/tests/specs/desktop.spec.ts b/tests/specs/desktop.spec.ts index bf276dc50..54208a4f8 100644 --- a/tests/specs/desktop.spec.ts +++ b/tests/specs/desktop.spec.ts @@ -21,10 +21,59 @@ describe('@wdio/visual-service desktop', () => { }) }) + it(`should compare an element screenshot with ignore elements successful with a baseline for '${browserName}'`, async function () { + await $('.features_vqN4').scrollIntoView() + + // When running a new set of images then first comment out block 1 and 2. Then run the test. + // Then uncomment block 1, check if they fail with `--store-diffs` as an extra argument. + // If so, then uncomment block 2 and check if pass with the same arguments. + // Block 1 + await browser.execute(() => { + document.querySelectorAll('.feature_G9wp h3').forEach(heading => { + (heading as HTMLElement).style.backgroundColor = 'var(--ifm-color-primary)' + }) + }) + + await expect($('.features_vqN4')).toMatchElementSnapshot( + 'ignoredElementsElementScreenshot', + { + // Block 2 + ignore: [ + await $$('.feature_G9wp h3'), + ], + // Don't comment this out, it's needed to hide the navbar + hideElements: [await $('nav.navbar')] + } + ) + }) + it(`should compare a viewport screenshot successful with a baseline for '${browserName}'`, async function() { await expect(browser).toMatchScreenSnapshot('viewportScreenshot') }) + it(`should compare a viewport screenshot with ignore elements successful with a baseline for '${browserName}'`, async function () { + // When running a new set of images then first comment out block 1 and 2. Then run the test. + // Then uncomment block 1, check if they fail with `--store-diffs` as an extra argument. + // If so, then uncomment block 2 and check if pass with the same arguments. + // Block 1 + await browser.execute(() => { + document.querySelectorAll('.navbar__items--right a.navbar__item, .feature_G9wp').forEach(link => { + (link as HTMLElement).style.backgroundColor = 'var(--ifm-color-primary)' + }) + }) + + await expect(browser).toMatchScreenSnapshot( + 'ignoredElementsViewportScreenshot', + { + // Block 2 + ignore: [ + await $$('.navbar__items--right a.navbar__item'), + await $$('.feature_G9wp'), + ], + } + ) + }) + it(`should compare a full page screenshot successful with a baseline for '${browserName}'`, async function () { await expect(browser).toMatchFullPageSnapshot('fullPage', { fullPageScrollTimeout: 1500, @@ -34,6 +83,29 @@ describe('@wdio/visual-service desktop', () => { }) }) + it(`should compare a full page screenshot with ignore elements successful with a baseline for '${browserName}'`, async function () { + // When running a new set of images then first comment out block 1 and 2. Then run the test. + // Then uncomment block 1, check if they fail with `--store-diffs` as an extra argument. + // If so, then uncomment block 2 and check if pass with the same arguments. + // Block 1 + await browser.execute(() => { + document.querySelectorAll('.feature_G9wp h3').forEach(heading => { + (heading as HTMLElement).style.backgroundColor = 'var(--ifm-color-primary)' + }) + }) + + await expect(browser).toMatchFullPageSnapshot('ignoredElementsFullPageScreenshot', { + fullPageScrollTimeout: 1500, + hideAfterFirstScroll: [ + await $('nav.navbar'), + ], + // // Block 2 + ignore: [ + await $$('.feature_G9wp h3'), + ], + }) + }) + it(`should compare a tabbable screenshot successful with a baseline for '${browserName}'`, async function() { await expect(browser).toMatchTabbablePageSnapshot('tabbable', { hideAfterFirstScroll: [ diff --git a/tests/specs/mobile.web.spec.ts b/tests/specs/mobile.web.spec.ts index 0c2c7fa86..96dac93c0 100644 --- a/tests/specs/mobile.web.spec.ts +++ b/tests/specs/mobile.web.spec.ts @@ -6,27 +6,15 @@ import { browser, expect } from '@wdio/globals' describe('@wdio/visual-service mobile web', () => { // Get the commands that need to be executed // 0 means all, otherwise it will only execute the commands that are specified - const wdioIcsCommands = driver.requestedCapabilities['wdio-ics:options'].commands - const deviceName = driver.requestedCapabilities['lt:options']?.deviceName || - driver.requestedCapabilities['bstack:options']?.deviceName || - driver.requestedCapabilities['appium:options']?.deviceName || - driver.requestedCapabilities.deviceName - const platformName = ( - driver.requestedCapabilities['lt:options']?.platformName || - driver.requestedCapabilities['appium:options']?.platformName || - driver.requestedCapabilities.platformName - ).toLowerCase() === 'android' ? 'Android' : 'iOS' - const platformVersion = - driver.requestedCapabilities['lt:options']?.platformVersion || - driver.requestedCapabilities['bstack:options']?.osVersion || - driver.requestedCapabilities['appium:options']?.platformVersion || - driver.requestedCapabilities.platformVersion - const orientation = ( - driver.requestedCapabilities['lt:options']?.deviceOrientation || - driver.requestedCapabilities['bstack:options']?.deviceOrientation || - driver.requestedCapabilities['appium:options']?.orientation || - driver.requestedCapabilities.orientation || 'PORTRAIT' - ).toLowerCase() + const caps = driver.requestedCapabilities + const lt = caps['lt:options'] + const bs = caps['bstack:options'] + const appium = caps['appium:options'] + const wdioIcsCommands = caps['wdio-ics:options'].commands + const deviceName = lt?.deviceName || bs?.deviceName || appium?.deviceName || caps.deviceName + const platformName = (lt?.platformName || appium?.platformName || caps.platformName).toLowerCase() === 'android' ? 'Android' : 'iOS' + const platformVersion = lt?.platformVersion || bs?.osVersion || appium?.platformVersion || caps.platformVersion + const orientation = (lt?.deviceOrientation || bs?.deviceOrientation || appium?.orientation || caps.orientation || 'PORTRAIT').toLowerCase() beforeEach(async () => { await browser.url('') @@ -34,17 +22,17 @@ describe('@wdio/visual-service mobile web', () => { await browser.pause(3000) }) - // Chrome remembers the last position when the url is loaded again, this will reset it. - afterEach( - async () => await browser.executeScript('window.scrollTo(0, 0);', []) - ) + // Refresh after each test to reset DOM modifications and scroll position. + // Chrome can restore pages from bfcache when navigating to the same URL, + // which would preserve inline style changes from "ignore elements" tests. + afterEach(async () => await browser.refresh()) if ( wdioIcsCommands.length === 0 || wdioIcsCommands.includes('checkScreen') ) { it(`should compare a screen successful for '${deviceName}' with ${platformName}:${platformVersion} in ${orientation}-mode`, async function () { - // @ts-ignore + skipTest({ test: this, deviceName, platformName, platformVersion, orientation }) this.retries(2) // This is normally a bad practice, but a mobile screenshot is normally around 1M pixels @@ -68,6 +56,34 @@ describe('@wdio/visual-service mobile web', () => { await browser.setOrientation(orientation) await expect(newResult < 0.05 ? 0 : newResult).toEqual(0) }) + + it(`should compare a screen with ignore elements successful for '${deviceName}' with ${platformName}:${platformVersion} in ${orientation}-mode`, async function () { + skipTest({ test: this, deviceName, platformName, platformVersion, orientation }) + + // When running a new set of images then first comment out block 1 and 2. Then run the test. + // Then uncomment block 1, check if they fail with `--store-diffs` as an extra argument. + // If so, then uncomment block 2 and check if pass with the same arguments. + // Block 1 + await browser.execute(() => { + document.querySelectorAll('.getStarted_Sjon').forEach(link => { + (link as HTMLElement).style.backgroundColor = 'var(--ifm-font-color-base)' + }) + }) + + // This is normally a bad practice, but a mobile screenshot is normally around 1M pixels + // We're accepting 0.05%, which is 500 pixels, to be a max difference + const result = await browser.checkScreen( + 'ignoredElementsScreenshot', { + // Block 2 + ignore: [ + await $$('.getStarted_Sjon'), + ], + }) as number + if (result > 0 && result < 0.05) { + console.log(`\n\n\n'Screenshot for ${deviceName}' with ${platformName}:${platformVersion} in ${orientation}-mode has a difference of ${result}%\n\n\n`) + } + await expect(result < 0.05 ? 0 : result).toEqual(0) + }) } if ( @@ -75,8 +91,9 @@ describe('@wdio/visual-service mobile web', () => { wdioIcsCommands.includes('checkElement') ) { it(`should compare an element successful for '${deviceName}' with ${platformName}:${platformVersion} in ${orientation}-mode`, async function() { - // @ts-ignore + skipTest({ test: this, deviceName, platformName, platformVersion, orientation }) this.retries(2) + await expect( await browser.checkElement( await $('.hero__title-logo'), @@ -87,6 +104,34 @@ describe('@wdio/visual-service mobile web', () => { ) ).toEqual(0) }) + + it(`should compare an element with ignore elements successful for '${deviceName}' with ${platformName}:${platformVersion} in ${orientation}-mode`, async function() { + skipTest({ test: this, deviceName, platformName, platformVersion, orientation }) + + await $('.features_vqN4').scrollIntoView() + + // When running a new set of images then first comment out block 1 and 2. Then run the test. + // Then uncomment block 1, check if they fail with `--store-diffs` as an extra argument. + // If so, then uncomment block 2 and check if pass with the same arguments. + // Block 1 + await browser.execute(() => { + document.querySelectorAll('.feature_G9wp h3').forEach(heading => { + (heading as HTMLElement).style.backgroundColor = 'var(--ifm-color-primary)' + }) + }) + + await expect($('.features_vqN4')).toMatchElementSnapshot( + 'ignoredElementsElementScreenshot', + { + // Block 2 + ignore: [ + await $$('.feature_G9wp h3'), + ], + // Don't comment this out, it's needed to hide the navbar + hideElements: [await $('nav.navbar')] + } + ) + }) } if ( @@ -94,8 +139,9 @@ describe('@wdio/visual-service mobile web', () => { wdioIcsCommands.includes('checkFullPageScreen') ) { it(`should compare a full page screenshot successful for '${deviceName}' with ${platformName}:${platformVersion} in ${orientation}-mode`, async function() { - // @ts-ignore + skipTest({ test: this, deviceName, platformName, platformVersion, orientation }) this.retries(2) + // This is normally a bad practice, but a mobile full page screenshot is normally around 4M pixels // We're accepting 0.05%, which is 2000 pixels, to be a max difference const result = await browser.checkFullPageScreen('fullPage', { @@ -109,5 +155,135 @@ describe('@wdio/visual-service mobile web', () => { } await expect(result < 0.05 ? 0 : result).toEqual(0) }) + + it(`should compare a full page screenshot with ignore elements successful for '${deviceName}' with ${platformName}:${platformVersion} in ${orientation}-mode`, async function() { + skipTest({ test: this, deviceName, platformName, platformVersion, orientation }) + + // When running a new set of images then first comment out block 1 and 2. Then run the test. + // Then uncomment block 1, check if they fail with `--store-diffs` as an extra argument. + // If so, then uncomment block 2 and check if pass with the same arguments. + // Block 1 + await browser.execute(() => { + document.querySelectorAll('.feature_G9wp h3').forEach(heading => { + (heading as HTMLElement).style.backgroundColor = 'var(--ifm-color-primary)' + }) + }) + + await expect(browser).toMatchFullPageSnapshot( + 'ignoredElementsFullPageScreenshot', + { + // Block 2 + ignore: [ + await $$('.feature_G9wp h3'), + ], + // We need to add some padding to the ignore regions + // to make sure that we cover the element that is being ignored. + ignoreRegionPadding: 5, + fullPageScrollTimeout: 1500, + hideAfterFirstScroll: [ + await $('nav.navbar'), + ], + } + ) + }) } }) + +/****************************************************************************************** + * SKIP RULES + * These are most likely TODO's that we have to fix but are not a blocker for the release. + * The reason is added to help us remember why we skipped the test. + ******************************************************************************************/ + +interface SkipRule { + titleIncludes: string | string[] + deviceName: string + platformName: 'Android' | 'iOS' + platformVersions: string[] + orientations: ('landscape' | 'portrait')[] + reason: string +} + +const skipRules: SkipRule[] = [ + // Android devices + { + titleIncludes: 'compare a screen with ignore elements', + deviceName: 'Pixel 4', + platformName: 'Android', + platformVersions: ['11'], + orientations: ['landscape', 'portrait'], + reason: 'Elements not visible in the screenshot, no value in testing', + }, + { + titleIncludes: 'compare a screen with ignore elements', + deviceName: 'Pixel 4', + platformName: 'Android', + platformVersions: ['12', '13'], + orientations: ['landscape'], + reason: 'Elements not visible in the screenshot, no value in testing', + }, + { + titleIncludes: 'compare a screen with ignore elements', + deviceName: 'Pixel 9 Pro', + platformName: 'Android', + platformVersions: ['14', '15'], + orientations: ['landscape'], + reason: 'Elements not visible in the screenshot, no value in testing', + }, + // iOS devices + { + titleIncludes: 'compare a screen with ignore elements', + deviceName: 'iPhone 13 mini', + platformName: 'iOS', + platformVersions: ['17.5'], + orientations: ['landscape'], + reason: 'Elements not visible in the screenshot, no value in testing', + }, + { + titleIncludes: 'compare a screen with ignore elements', + deviceName: 'iPhone 13 Pro', + platformName: 'iOS', + platformVersions: ['16.0'], + orientations: ['landscape'], + reason: 'Elements not visible in the screenshot, no value in testing', + }, + { + titleIncludes: 'compare a screen with ignore elements', + deviceName: 'iPhone 14 Pro', + platformName: 'iOS', + platformVersions: ['17.5'], + orientations: ['landscape'], + reason: 'Elements not visible in the screenshot, no value in testing', + }, + { + titleIncludes: 'compare a screen with ignore elements', + deviceName: 'iPhone 15 Pro Max', + platformName: 'iOS', + platformVersions: ['18.0'], + orientations: ['landscape'], + reason: 'Elements not visible in the screenshot, no value in testing', + }, +] +function skipTest({ test, deviceName, platformName, platformVersion, orientation }: { + test: Mocha.Context + deviceName: string + platformName: string + platformVersion: string + orientation: string +}) { + const { title } = test.test! + + const matchedRule = skipRules.find(rule => { + const patterns = Array.isArray(rule.titleIncludes) ? rule.titleIncludes : [rule.titleIncludes] + + return patterns.some(p => title.includes(p)) + && rule.deviceName === deviceName + && rule.platformName === platformName + && rule.platformVersions.includes(platformVersion) + && rule.orientations.includes(orientation as 'landscape' | 'portrait') + }) + + if (matchedRule) { + test.skip() + } +}