diff --git a/.changeset/many-olives-deny.md b/.changeset/many-olives-deny.md new file mode 100644 index 000000000..94c52d131 --- /dev/null +++ b/.changeset/many-olives-deny.md @@ -0,0 +1,130 @@ +--- +"webdriver-image-comparison": major +"@wdio/visual-service": major +"@wdio/visual-reporter": patch +"@wdio/ocr-service": patch +--- + +## πŸ’₯ BREAKING CHANGES + +### πŸ” Viewport Screenshot Logic Reworked for Mobile Web & Hybrid Apps + +#### What was the problem? + +Screenshots for mobile devices were inconsistent due to platform differences. iOS captures the entire device screen (including status and address bars), while Android (using ChromeDriver) only captures the webview, unless the capability `"appium:nativeWebScreenshot": true` is used. + +#### What changed? + +We’ve reimplemented the logic to correctly handle both platforms by default. +This fix addresses [[#747](https://github.com/webdriverio/visual-testing/pull/747)](https://github.com/webdriverio/visual-testing/pull/747). + +πŸ’‘ Credit to [Benjamin Karran (@ebekebe)](https://github.com/ebekebe) for pointing us in the right direction to improve this logic! + +#### What’s the advantage? + +βœ… More **accurate full-page and element screenshots** on both Android and iOS. +⚠️ But this change may **break your current baselines**, especially on Android and iOS. + +--- + +### 🍏 iOS Element Screenshot Strategy Changed + +#### What was the problem? + +iOS element screenshots were previously cut from full-device screenshots, which could lead to misalignment or off-by-a-few-pixels issues. + +#### What changed? + +We now use the element screenshot endpoint directly. + +#### What’s the advantage? + +βœ… More accurate iOS element screenshots. +⚠️ But again, this may affect your existing baselines. + +--- + +### πŸ–₯️ New Full-Page Screenshot Strategy for **Desktop Web** + +#### What was the problem? + +The "previous" scroll-and-stitch method simulated user interaction by scrolling the page, waiting, taking a screenshot, and repeating until the entire page was captured. +This works well for **lazy-loaded content**, but it is **slow and unstable** on other pages. + +#### What changed? + +We now use WebDriver BiDi’s [`[browsingContext.captureScreenshot](https://webdriver.io/docs/api/webdriverBidi#browsingcontextcapturescreenshot)`] to capture **full-page screenshots in one go**. This is the new **default strategy for desktop web browsers**. + +πŸ“Œ **Mobile platforms (iOS/Android)** still use the scroll-and-stitch approach for now. + +#### What’s the advantage? +βœ… Execution time reduced by **50%+** +βœ… Logic is greatly simplified +βœ… More consistent and stable results on static or non-lazy pages +πŸ“Έ ![Example](https://github.com/user-attachments/assets/394ad1d6-bbc7-42dd-b93b-ff7eb5a80429) + +**Still want the old scroll-and-stitch behavior or need fullpage screenshots for pages who have lazy-loading?** + +Use the `userBasedFullPageScreenshot` option to simulate user-like scrolling. This remains the **better choice for pages with lazy-loading**: + +```ts +// wdio.conf.ts +services: [ + ["visual", { + userBasedFullPageScreenshot: true + }] +] +``` + +Or per test: + +```ts +await expect(browser).toMatchFullPageSnapshot('homepage', { + userBasedFullPageScreenshot: true, +}) +``` + +--- + +## πŸ’… Polish + +### ⚠️ Deprecated Root-Level Compare Options + +#### What was the problem? + +Compare options were allowed at the root level of the service config, making them harder to group or discover. + +#### What changed? + +You now get a warning if you still use root-level keys. Please move them under the `compareOptions` property instead. + +**Example warning:** + +```log +WARN The following root-level compare options are deprecated and should be moved under 'compareOptions': + - blockOutStatusBar + - ignoreColors +In the next major version, these options will be removed from the root level. +``` + +πŸ“˜ See: [[compareOptions docs](https://webdriver.io/docs/visual-testing/service-options#compare-options)](https://webdriver.io/docs/visual-testing/service-options#compare-options) + +--- + +## πŸ› Bug Fixes + +- βœ… [[#747](https://github.com/your-repo/issues/747)](https://github.com/your-repo/issues/747): Fixed incorrect mobile webview context data. + +--- + +## πŸ”§ Other + +- πŸ†™ Updated dependencies +- πŸ§ͺ Improved test coverage +- πŸ“Έ Refreshed image baselines + +--- + +## Committers: 1 + +- Wim Selles ([@wswebcreation](https://github.com/wswebcreation)) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 56a679c2f..4ab928246 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -176,7 +176,7 @@ jobs: LAMBDATEST_USERNAME: ${{ secrets.LAMBDATEST_USERNAME }} LAMBDATEST_ACCESS_KEY: ${{ secrets.LAMBDATEST_ACCESS_KEY }} BUILD_PREFIX: true - run: pnpm test.lambdatest.desktop --maxConcurrency=3 + run: pnpm test.lambdatest.desktop --maxConcurrency=4 - name: πŸ“€ Upload artifacts uses: actions/upload-artifact@v4 @@ -239,7 +239,7 @@ jobs: LAMBDATEST_USERNAME: ${{ secrets.LAMBDATEST_USERNAME }} LAMBDATEST_ACCESS_KEY: ${{ secrets.LAMBDATEST_ACCESS_KEY }} BUILD_PREFIX: true - run: pnpm test.lambdatest.emu.web --maxConcurrency=4 + run: pnpm test.lambdatest.emu.web --maxConcurrency=6 - name: πŸ“€ Upload artifacts uses: actions/upload-artifact@v4 @@ -302,7 +302,7 @@ jobs: LAMBDATEST_USERNAME: ${{ secrets.LAMBDATEST_USERNAME }} LAMBDATEST_ACCESS_KEY: ${{ secrets.LAMBDATEST_ACCESS_KEY }} BUILD_PREFIX: true - run: pnpm test.lambdatest.sims.web --maxConcurrency=4 + run: pnpm test.lambdatest.sims.web --maxConcurrency=6 - name: πŸ“€ Upload artifacts uses: actions/upload-artifact@v4 diff --git a/package.json b/package.json index a1bec2208..172a13519 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "scripts": { "build": "pnpm run -r build", "build:reporter": "node ./packages/visual-reporter/dist/cli.js --jsonOutput=.tmp/actual/output.json --reportFolder=.tmp/ --logLevel=debug", - "clean": "rimraf coverage dist .tmp '**/dist'", + "clean": "rimraf coverage dist .tmp **/dist", "release": "run-s build && changeset publish", "test": "run-s test:*", "test:lint": "rimraf .eslintcache && eslint packages tests", @@ -56,43 +56,43 @@ "webdriver-image-comparison": "workspace:*" }, "devDependencies": { - "@changesets/cli": "^2.29.0", + "@changesets/cli": "^2.29.2", "@tsconfig/node20": "^20.1.5", "@types/eslint": "^9.6.1", "@types/inquirer": "^9.0.7", "@types/jsdom": "~21.1.7", "@types/node": "^22", "@types/xml2js": "~0.4.14", - "@typescript-eslint/eslint-plugin": "^8.29.1", - "@typescript-eslint/parser": "^8.29.1", - "@typescript-eslint/utils": "^8.29.1", - "@vitest/coverage-v8": "^3.0.8", - "@vitest/ui": "^3.0.8", - "@wdio/appium-service": "^9.12.4", - "@wdio/cli": "^9.12.5", - "@wdio/globals": "^9.9.1", - "@wdio/local-runner": "^9.12.5", - "@wdio/mocha-framework": "^9.12.5", - "@wdio/sauce-service": "^9.12.5", - "@wdio/shared-store-service": "^9.12.5", - "@wdio/spec-reporter": "^9.12.3", - "@wdio/types": "^9.12.2", + "@typescript-eslint/eslint-plugin": "^8.30.1", + "@wdio/globals": "^9.12.6", + "@wdio/mocha-framework": "^9.12.6", + "@typescript-eslint/parser": "^8.30.1", + "@typescript-eslint/utils": "^8.30.1", + "@vitest/coverage-v8": "^3.1.1", + "@vitest/ui": "^3.1.1", + "@wdio/appium-service": "^9.12.6", + "@wdio/cli": "^9.12.6", + "@wdio/local-runner": "^9.12.6", + "@wdio/sauce-service": "^9.12.6", + "@wdio/shared-store-service": "^9.12.6", + "@wdio/spec-reporter": "^9.12.6", + "@wdio/types": "^9.12.6", "cross-env": "^7.0.3", - "eslint": "^9.23.0", + "eslint": "^9.24.0", "eslint-plugin-import": "^2.31.0", "eslint-plugin-unicorn": "^56.0.1", "eslint-plugin-wdio": "^9.9.1", "husky": "^9.1.7", - "jsdom": "^25.0.1", + "jsdom": "^26.1.0", "npm-run-all2": "^7.0.2", - "release-it": "^17.11.0", + "release-it": "^18.1.2", "rimraf": "^6.0.1", - "saucelabs": "^8.0.0", + "saucelabs": "^9.0.2", "ts-node": "^10.9.2", - "typescript": "^5.7.3", - "vitest": "^3.0.8", - "wdio-lambdatest-service": "^4.0.0", - "webdriverio": "^9.12.4" + "typescript": "^5.8.3", + "vitest": "^3.1.1", + "webdriverio": "^9.12.6", + "wdio-lambdatest-service": "^4.0.0" }, "packageManager": "pnpm@9.15.9+sha256.cf86a7ad764406395d4286a6d09d730711720acc6d93e9dce9ac7ac4dc4a28a7" } diff --git a/packages/ocr-service/package.json b/packages/ocr-service/package.json index f387c733d..ccac42fb0 100644 --- a/packages/ocr-service/package.json +++ b/packages/ocr-service/package.json @@ -28,11 +28,11 @@ "watch": "pnpm run build:tsc -w" }, "dependencies": { - "@wdio/globals": "^9.9.1", + "@wdio/globals": "^9.12.6", "@wdio/logger": "^9.4.4", - "@wdio/types": "^9.12.2", + "@wdio/types": "^9.12.6", "fuse.js": "^7.1.0", - "@inquirer/prompts": "7.3.2", + "@inquirer/prompts": "7.4.1", "jimp": "^1.6.0", "node-tesseract-ocr": "^2.2.1", "tesseract.js": "^5.1.1", @@ -42,4 +42,4 @@ "@types/inquirer": "~9.0.7", "@types/xml2js": "~0.4.14" } -} \ No newline at end of file +} diff --git a/packages/ocr-service/src/utils/getData.ts b/packages/ocr-service/src/utils/getData.ts index 27ddcca16..07a883900 100644 --- a/packages/ocr-service/src/utils/getData.ts +++ b/packages/ocr-service/src/utils/getData.ts @@ -1,7 +1,7 @@ import logger from '@wdio/logger' import { getNodeOcrData, getSystemOcrData } from './tesseract.js' import type { GetOcrData, Line, GetData, GetDataOptions, RectReturn, Words } from '../types.js' -import { adjustElementBbox, getScreenshotSize, isRectanglesObject } from './index.js' +import { adjustElementBbox, getBase64ScreenshotSize, isRectanglesObject } from './index.js' import { drawHighlightedWords, processImage } from './imageProcessing.js' const log = logger('@wdio/ocr-service:getData') @@ -22,7 +22,7 @@ export default async function getData(browser: WebdriverIO.Browser, options: Get if (!cliFile) { const screenSize = await browser.getWindowSize() screenshot = await browser.takeScreenshot() - const { width } = getScreenshotSize(screenshot) + const { width } = getBase64ScreenshotSize(screenshot) dpr = width / screenSize.width } else { screenshot = cliFile diff --git a/packages/ocr-service/src/utils/index.ts b/packages/ocr-service/src/utils/index.ts index c8183bf96..1ba35dae6 100644 --- a/packages/ocr-service/src/utils/index.ts +++ b/packages/ocr-service/src/utils/index.ts @@ -3,7 +3,7 @@ import { mkdirSync } from 'node:fs' import type { ChainablePromiseElement } from 'webdriverio' import type { ClickPoint, DetermineClickPointOptions, Rectangles, RectReturn, ScreenshotSize } from '../types.js' -export function getScreenshotSize(screenshot: string): ScreenshotSize { +export function getBase64ScreenshotSize(screenshot: string): ScreenshotSize { return { height: Math.round(Buffer.from(screenshot, 'base64').readUInt32BE(20)), width: Math.round(Buffer.from(screenshot, 'base64').readUInt32BE(16)), diff --git a/packages/ocr-service/tests/utils/__snapshots__/ocrGetData.test.ts.snap b/packages/ocr-service/tests/utils/__snapshots__/ocrGetData.test.ts.snap index 7bb7b2b84..56fdfa8a1 100644 --- a/packages/ocr-service/tests/utils/__snapshots__/ocrGetData.test.ts.snap +++ b/packages/ocr-service/tests/utils/__snapshots__/ocrGetData.test.ts.snap @@ -37,7 +37,7 @@ exports[`getData > processes OCR data correctly using a haystack 1`] = ` "Using system installed version of Tesseract", ], [ - "It took '0.000s' to process the image.", + "It took 'X.XXXs' to process the image.", ], [ "The following text was found through OCR: diff --git a/packages/ocr-service/tests/utils/index.test.ts b/packages/ocr-service/tests/utils/index.test.ts index 5c153b308..2cdecd84c 100644 --- a/packages/ocr-service/tests/utils/index.test.ts +++ b/packages/ocr-service/tests/utils/index.test.ts @@ -1,6 +1,6 @@ import { mkdirSync } from 'node:fs' import { describe, it, expect, vi } from 'vitest' -import { adjustElementBbox, createOcrDir, determineClickPoint, getDprPositions, getScreenshotSize, isRectanglesObject } from '../../src/utils/index.js' +import { adjustElementBbox, createOcrDir, determineClickPoint, getDprPositions, getBase64ScreenshotSize, isRectanglesObject } from '../../src/utils/index.js' import type { RectReturn } from '../../src/types.js' vi.mock('node:fs', () => ({ @@ -15,26 +15,26 @@ function createMockScreenshot(width: number, height: number): string { return buffer.toString('base64') } -describe('getScreenshotSize', () => { +describe('getBase64ScreenshotSize', () => { it('should correctly extract dimensions from a valid screenshot', () => { const width = 800 const height = 600 const base64 = createMockScreenshot(width, height) - const result = getScreenshotSize(base64) + const result = getBase64ScreenshotSize(base64) expect(result).toEqual({ width: width, height: height }) }) it('should handle invalid base64 strings gracefully', () => { const invalidBase64 = 'not-a-real-base64-string' - const action = () => getScreenshotSize(invalidBase64) + const action = () => getBase64ScreenshotSize(invalidBase64) expect(action).toThrowError() }) it('should handle unexpected data layout', () => { const malformedBase64 = Buffer.from([1, 2, 3, 4, 5]).toString('base64') - const action = () => getScreenshotSize(malformedBase64) + const action = () => getBase64ScreenshotSize(malformedBase64) expect(action).toThrowError() }) diff --git a/packages/ocr-service/tests/utils/ocrGetData.test.ts b/packages/ocr-service/tests/utils/ocrGetData.test.ts index 21d16d024..54c51919c 100644 --- a/packages/ocr-service/tests/utils/ocrGetData.test.ts +++ b/packages/ocr-service/tests/utils/ocrGetData.test.ts @@ -5,7 +5,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest' import getData from '../../src/utils/getData.js' import * as Tesseract from '../../src/utils/tesseract.js' import * as ImageProcessing from '../../src/utils/imageProcessing.js' -import { adjustElementBbox, getScreenshotSize, isRectanglesObject } from '../../src/utils/index.js' +import { adjustElementBbox, getBase64ScreenshotSize, isRectanglesObject } from '../../src/utils/index.js' const browser = wdioBrowser const log = logger('test') @@ -15,7 +15,7 @@ vi.mock('../../src/utils/tesseract.js') vi.mock('../../src/utils/imageProcessing.js') vi.mock('../../src/utils/index.js') vi.mock('webdriver-image-comparison/dist/helpers/utils', () => ({ - getScreenshotSize: vi.fn(() => ({ width: 1200 })), + getBase64ScreenshotSize: vi.fn(() => ({ width: 1200 })), })) vi.mock('../../src/utils/tesseract.js', () => ({ getNodeOcrData: vi.fn().mockResolvedValue({ @@ -42,7 +42,7 @@ vi.mock('@wdio/globals', () => ({ isAndroid: false, isIOS: false, getElementRect:vi.fn().mockResolvedValue({ x: 10, y: 20, width: 300, height: 400 }), - getScreenshotSize:vi.fn().mockReturnValue({ width: 1200 }), + getBase64ScreenshotSize:vi.fn().mockReturnValue({ width: 1200 }), getWindowSize: vi.fn().mockResolvedValue({ width: 1200, height: 800 }), takeScreenshot:vi.fn().mockResolvedValue('screenshotData'), } @@ -61,13 +61,13 @@ describe('getData', () => { language: 'ENG', ocrImagesPath: '/fake/ocr/path' } - vi.mocked(getScreenshotSize).mockReturnValue({ height: 1200, width: 1200 }) + vi.mocked(getBase64ScreenshotSize).mockReturnValue({ height: 1200, width: 1200 }) const result = await getData(browser, options) expect(browser.getWindowSize).toHaveBeenCalled() expect(browser.takeScreenshot).toHaveBeenCalled() - expect(getScreenshotSize).toHaveBeenCalledWith('screenshotData') + expect(getBase64ScreenshotSize).toHaveBeenCalledWith('screenshotData') expect(isRectanglesObject).not.toHaveBeenCalled() expect(ImageProcessing.processImage).toHaveBeenCalledTimes(1) expect(Tesseract.getSystemOcrData).toHaveBeenCalledWith({ @@ -87,13 +87,13 @@ describe('getData', () => { language: 'ENG', ocrImagesPath: '/fake/ocr/path' } - vi.mocked(getScreenshotSize).mockReturnValue({ height: 1200, width: 1200 }) + vi.mocked(getBase64ScreenshotSize).mockReturnValue({ height: 1200, width: 1200 }) const result = await getData(browser, options) expect(browser.getWindowSize).toHaveBeenCalled() expect(browser.takeScreenshot).toHaveBeenCalled() - expect(getScreenshotSize).toHaveBeenCalledWith('screenshotData') + expect(getBase64ScreenshotSize).toHaveBeenCalledWith('screenshotData') expect(isRectanglesObject).not.toHaveBeenCalled() expect(ImageProcessing.processImage).toHaveBeenCalledTimes(1) expect(Tesseract.getNodeOcrData).toHaveBeenCalledWith({ @@ -114,13 +114,20 @@ describe('getData', () => { language: 'ENG', ocrImagesPath: '/fake/ocr/path' } - vi.mocked(getScreenshotSize).mockReturnValue({ height: 1200, width: 1200 }) + vi.mocked(getBase64ScreenshotSize).mockReturnValue({ height: 1200, width: 1200 }) const result = await getData(browser, options) + const sanitizedCalls = logInfoMock.mock.calls.map((args) => + args.map((msg) => + typeof msg === 'string' + ? msg.replace(/'[\d.]+s'/g, "'X.XXXs'") + : msg + ) + ) expect(browser.getWindowSize).toHaveBeenCalled() expect(browser.takeScreenshot).toHaveBeenCalled() - expect(getScreenshotSize).toHaveBeenCalledWith('screenshotData') + expect(getBase64ScreenshotSize).toHaveBeenCalledWith('screenshotData') expect(isRectanglesObject).toHaveBeenCalledTimes(1) expect(ImageProcessing.processImage).toHaveBeenCalledTimes(2) expect(Tesseract.getSystemOcrData).toHaveBeenCalledWith({ @@ -128,7 +135,7 @@ describe('getData', () => { language: 'ENG' }) expect(adjustElementBbox).toHaveBeenCalledTimes(4) - expect(logInfoMock.mock.calls).toMatchSnapshot() + expect(sanitizedCalls).toMatchSnapshot() expect(result).toMatchSnapshot() }) diff --git a/packages/visual-reporter/package.json b/packages/visual-reporter/package.json index 61c84cf09..1b1e5dc76 100644 --- a/packages/visual-reporter/package.json +++ b/packages/visual-reporter/package.json @@ -29,23 +29,23 @@ "watch:scripts": "npm run build:scripts --watch" }, "dependencies": { - "@inquirer/prompts": "^7.3.2", + "@inquirer/prompts": "^7.4.1", "ora": "^8.2.0", - "sirv-cli": "^3.0.0", + "sirv-cli": "^3.0.1", "sharp": "^0.34.1" }, "devDependencies": { "@remix-run/node": "^2.16.5", "@remix-run/react": "^2.16.5", - "@remix-run/serve": "^2.16.4", - "@remix-run/dev": "^2.15.3", - "@types/react": "^18.3.18", - "@types/react-dom": "^18.3.5", - "@typescript-eslint/eslint-plugin": "^8.29.1", - "@typescript-eslint/parser": "^8.29.1", + "@remix-run/serve": "^2.16.5", + "@remix-run/dev": "^2.16.5", + "@types/react": "^18.3.20", + "@types/react-dom": "^18.3.6", + "@typescript-eslint/eslint-plugin": "^8.30.1", + "@typescript-eslint/parser": "^8.30.1", "autoprefixer": "^10.4.21", - "eslint": "^9.23.0", - "eslint-import-resolver-typescript": "^3.8.0", + "eslint": "^9.24.0", + "eslint-import-resolver-typescript": "^3.10.0", "eslint-plugin-import": "^2.31.0", "eslint-plugin-jsx-a11y": "^6.10.2", "eslint-plugin-react": "^7.37.5", @@ -57,11 +57,11 @@ "react-icons": "^5.5.0", "react-select": "^5.10.1", "tailwindcss": "^3.4.17", - "typescript": "^5.7.3", + "typescript": "^5.8.3", "vite": "^5.4.18", "vite-tsconfig-paths": "^5.1.4" }, "engines": { "node": ">=20.0.0" } -} \ No newline at end of file +} diff --git a/packages/visual-service/package.json b/packages/visual-service/package.json index 9788dc14b..3e8bb1d91 100644 --- a/packages/visual-service/package.json +++ b/packages/visual-service/package.json @@ -26,9 +26,9 @@ "watch": "pnpm run build:tsc -w" }, "dependencies": { - "@wdio/globals": "^9.9.1", + "@wdio/globals": "^9.12.6", "@wdio/logger": "^9.4.4", - "@wdio/types": "^9.12.2", + "@wdio/types": "^9.12.6", "expect-webdriverio": "^5.1.0", "webdriver-image-comparison": "workspace:*" } diff --git a/packages/visual-service/src/contextManager.ts b/packages/visual-service/src/contextManager.ts new file mode 100644 index 000000000..fde871b1c --- /dev/null +++ b/packages/visual-service/src/contextManager.ts @@ -0,0 +1,82 @@ +import logger from '@wdio/logger' +import type { DeviceRectangles } from 'webdriver-image-comparison' +import { DEVICE_RECTANGLES } from 'webdriver-image-comparison' +import { getNativeContext } from './utils.js' + +const log = logger('@wdio/visual-service:ContextManager') + +export class ContextManager { + #browser: WebdriverIO.Browser + #needsUpdate = false + #currentContext?: string + #isNativeContext: boolean + private cachedViewport: DeviceRectangles = DEVICE_RECTANGLES + + constructor(browser: WebdriverIO.Browser) { + this.#browser = browser + const capabilities = this.#browser.requestedCapabilities + this.#isNativeContext = getNativeContext({ capabilities, isMobile: this.#browser.isMobile }) + this.#browser.on('result', this.#onCommandResult.bind(this)) + } + + getViewportContext(): DeviceRectangles { + return this.cachedViewport + } + + setViewPortContext(viewport: DeviceRectangles): void { + this.cachedViewport = viewport + this.#needsUpdate = false + } + + get browser(): WebdriverIO.Browser { + return this.#browser + } + + get needsUpdate(): boolean { + return this.#needsUpdate + } + + markForUpdate(): void { + this.#needsUpdate = true + } + + setCurrentContext(context: string) { + this.#currentContext = context + if (this.#browser.isMobile) { + this.#isNativeContext = context ? context === 'NATIVE_APP' : this.#isNativeContext + } + } + + async getCurrentContext () { + return this.#currentContext + } + + get isNativeContext() { + return this.#isNativeContext + } + + async #onCommandResult(event: { command: string, body: unknown, result: unknown }) { + const commands = ['switchAppiumContext', 'switchContext', 'setOrientation'] + if (commands.includes(event.command)) { + const { body: { name, orientation }, result } = event as { body: { name?: string; orientation?: string }, result: unknown } + // Check if result exists and is not an error object + // @ts-expect-error + const isSuccess = result && !result.error && typeof result === 'object' + if (isSuccess) { + if (event.command === 'setOrientation') { + log.info(`Device rotation to "${orientation}" detected, context will need recalculation.`) + this.markForUpdate() + } else { + if (name && name !== 'NATIVE_APP') { + log.info(`Context changed to "${name}", context will need recalculation.`) + this.markForUpdate() + } + // Set the context to the current context + this.setCurrentContext(name as string) + } + } else { + log.warn('Orientation set failed: \n', result, '\nWe could not recalibrate the context') + } + } + } +} diff --git a/packages/visual-service/src/service.ts b/packages/visual-service/src/service.ts index 6bcd6e9df..88dc69552 100644 --- a/packages/visual-service/src/service.ts +++ b/packages/visual-service/src/service.ts @@ -15,9 +15,15 @@ import { FOLDERS, DEFAULT_TEST_CONTEXT, } from 'webdriver-image-comparison' -import type { TestContext } from 'webdriver-image-comparison' +import type { InstanceData, TestContext } from 'webdriver-image-comparison' import { SevereServiceError } from 'webdriverio' -import { determineNativeContext, enrichTestContext, getFolders, getInstanceData, getNativeContext } from './utils.js' +import { + enrichTestContext, + getFolders, + getInstanceData, + getNativeContext, + isBiDiScreenshotSupported, +} from './utils.js' import { toMatchScreenSnapshot, toMatchFullPageSnapshot, @@ -26,8 +32,10 @@ import { } from './matcher.js' import { waitForStorybookComponentToBeLoaded } from './storybook/utils.js' import type { WaitForStorybookComponentToBeLoaded } from './storybook/Types.js' -import type { MultiremoteCommandResult, NativeContextType, VisualServiceOptions } from './types.js' +import type { VisualServiceOptions } from './types.js' import { PAGE_OPTIONS_MAP } from './constants.js' +import { ContextManager } from './contextManager.js' +import { wrapWithContext } from './wrapWithContext.js' const log = logger('@wdio/visual-service') const elementCommands = { saveElement, checkElement } @@ -47,12 +55,12 @@ export default class WdioImageComparisonService extends BaseClass { #currentFilePath?: string #testContext: TestContext #browser?: WebdriverIO.Browser | WebdriverIO.MultiRemoteBrowser - private _isNativeContext: NativeContextType | undefined + private _contextManager?: ContextManager + private _contextManagers?: Map = new Map() constructor(options: VisualServiceOptions, _: WebdriverIO.Capabilities, config: WebdriverIO.Config) { super(options) this.#config = config - this._isNativeContext = undefined this.#testContext = DEFAULT_TEST_CONTEXT } @@ -69,7 +77,6 @@ export default class WdioImageComparisonService extends BaseClass { browser: WebdriverIO.Browser | WebdriverIO.MultiRemoteBrowser ) { this.#browser = browser - this._isNativeContext = determineNativeContext(this.#browser) if (!this.#browser.isMultiremote) { log.info('Adding commands to global browser') @@ -78,10 +85,6 @@ export default class WdioImageComparisonService extends BaseClass { await this.#extendMultiremoteBrowser(capabilities as Capabilities.RequestedMultiremoteCapabilities) } - if (browser.isMultiremote) { - this.#setupMultiremoteContextListener() - } - /** * add custom matcher for visual comparison when expect has been added. * this is not the case in standalone mode @@ -109,14 +112,6 @@ export default class WdioImageComparisonService extends BaseClass { this.#testContext = this.#getTestContext(world) } - afterCommand(commandName: string, _args: string[], result: number | string, error: any) { - // This is for the cases where in the E2E tests we switch to a WEBVIEW or back to NATIVE_APP context - if (commandName === 'getContext' && error === undefined && typeof result === 'string') { - // Multiremote logic is handled in the `before` method during an event listener - this._isNativeContext = this.#browser?.isMultiremote ? this._isNativeContext : result.includes('NATIVE') - } - } - #getBaselineFolder() { const isDefaultBaselineFolder = normalize(FOLDERS.DEFAULT.BASE) === this.folders.baselineFolder const baselineFolder =(isDefaultBaselineFolder && this.#currentFilePath ? this.#currentFilePath : this.folders.baselineFolder) as string @@ -146,7 +141,12 @@ export default class WdioImageComparisonService extends BaseClass { */ for (const browserName of browserNames) { log.info(`Adding commands to Multi Browser: ${browserName}`) + const browserInstance = browser.getInstance(browserName) + const contextManager = new ContextManager(browserInstance) + + this._contextManagers?.set(browserName, contextManager) + await this.#addCommandsToBrowser(browserInstance) } @@ -172,119 +172,168 @@ export default class WdioImageComparisonService extends BaseClass { * Add commands to the "normal" browser object */ async #addCommandsToBrowser(currentBrowser: WebdriverIO.Browser) { - const instanceData = await getInstanceData(currentBrowser) - const isNativeContext = getNativeContext( - this.#browser as WebdriverIO.Browser | WebdriverIO.MultiRemoteBrowser, + this._contextManager = new ContextManager(currentBrowser); + (currentBrowser as any).visualService = this + const instanceData = await getInstanceData({ currentBrowser, - this._isNativeContext as NativeContextType - ) + initialDeviceRectangles: this._contextManager.getViewportContext(), + isNativeContext: this._contextManager.isNativeContext, + }) + + // Update the context manager with the current viewport + this._contextManager.setViewPortContext(instanceData.deviceRectangles) for (const [commandName, command] of Object.entries(elementCommands)) { - this.#addElementCommand(currentBrowser, commandName, command, instanceData, isNativeContext) + this.#addElementCommand(currentBrowser, commandName, command, instanceData) } for (const [commandName, command] of Object.entries(pageCommands)) { - this.#addPageCommand(currentBrowser, commandName, command, instanceData, isNativeContext) + this.#addPageCommand(currentBrowser, commandName, command, instanceData) } } /** * Add new element commands to the browser object */ - #addElementCommand(browser: WebdriverIO.Browser, commandName: string, command: any, instanceData: any, isNativeContext: boolean) { + #addElementCommand( + browser: WebdriverIO.Browser, + commandName: string, + command: any, + initialInstanceData: InstanceData, + ) { log.info(`Adding element command "${commandName}" to browser object`) + const elementOptionsKey = commandName === 'saveElement' ? 'saveElementOptions' : 'checkElementOptions' const self = this + browser.addCommand( commandName, function ( - this: typeof browser, - element, - tag, - elementOptions = {}, + this: WebdriverIO.Browser, + element: WebdriverIO.Element, + tag: string, + elementOptions = {} ) { - const elementOptionsKey = commandName === 'saveElement' ? 'saveElementOptions' : 'checkElementOptions' + const wrapped = wrapWithContext({ + browser, + command, + contextManager: self.contextManager, + getArgs: () => { + const updatedInstanceData = { + ...initialInstanceData, + deviceRectangles: self.contextManager.getViewportContext(), + } + const isCurrentContextNative = self.contextManager.isNativeContext - return command( - { - methods: { - executor: ( - fn: string | ((...args: InnerArguments) => ReturnValue), - ...args: InnerArguments): Promise => { - return this.execute.bind(browser)(fn, ...args) as Promise + return [{ + methods: { + executor: ( + fn: string | ((...args: InnerArguments) => ReturnValue), + ...args: InnerArguments + ): Promise => { + return this.execute(fn, ...args) as Promise + }, + getElementRect: this.getElementRect.bind(this), + screenShot: this.takeScreenshot.bind(this), + takeElementScreenshot: this.takeElementScreenshot.bind(this), }, - getElementRect: this.getElementRect.bind(browser), - screenShot: this.takeScreenshot.bind(browser), - takeElementScreenshot: this.takeElementScreenshot.bind(browser), - }, - instanceData, - folders: getFolders(elementOptions, self.folders, self.#getBaselineFolder()), - element, - tag, - [elementOptionsKey]: { - wic: self.defaultOptions, - method: elementOptions, - }, - isNativeContext, - testContext: enrichTestContext({ - commandName, - currentTestContext: self.#testContext, - instanceData, + instanceData: updatedInstanceData, + folders: getFolders(elementOptions, self.folders, self.#getBaselineFolder()), + element, tag, - }) + [elementOptionsKey]: { + wic: self.defaultOptions, + method: elementOptions, + }, + isNativeContext: isCurrentContextNative, + testContext: enrichTestContext({ + commandName, + currentTestContext: self.#testContext, + instanceData: updatedInstanceData, + tag, + }), + }] } - ) - }) + }) + + return wrapped.call(this) + } + ) } /** * Add new page commands to the browser object */ - #addPageCommand(browser: WebdriverIO.Browser, commandName: string, command: any, instanceData: any, isNativeContext: boolean) { + #addPageCommand( + browser: WebdriverIO.Browser, + commandName: string, + command: any, + initialInstanceData: InstanceData, + ) { log.info(`Adding browser command "${commandName}" to browser object`) const self = this const pageOptionsKey = PAGE_OPTIONS_MAP[commandName] if (commandName === 'waitForStorybookComponentToBeLoaded') { - browser.addCommand(commandName, (options: WaitForStorybookComponentToBeLoaded) => waitForStorybookComponentToBeLoaded(options)) - } else { - browser.addCommand( - commandName, - function ( - this: typeof browser, - tag, - pageOptions = {} - ) { - return command( - { + browser.addCommand(commandName, (options: WaitForStorybookComponentToBeLoaded) => + waitForStorybookComponentToBeLoaded(options) + ) + return + } + + browser.addCommand( + commandName, + function ( + this: WebdriverIO.Browser, + tag: string, + pageOptions = {} + ) { + const wrapped = wrapWithContext({ + browser, + command, + contextManager: self.contextManager, + getArgs: () => { + const updatedInstanceData = { + ...initialInstanceData, + deviceRectangles: self.contextManager.getViewportContext() + } + const isCurrentContextNative = self.contextManager.isNativeContext + + return [{ methods: { + bidiScreenshot: isBiDiScreenshotSupported(browser) ? this.browsingContextCaptureScreenshot.bind(browser) : undefined, executor: ( fn: string | ((...args: InnerArguments) => ReturnValue), - ...args: InnerArguments): Promise => { - return this.execute.bind(browser)(fn, ...args) as Promise + ...args: InnerArguments + ): Promise => { + return this.execute(fn, ...args) as Promise }, getElementRect: this.getElementRect.bind(browser), + getWindowHandle: this.getWindowHandle.bind(browser), screenShot: this.takeScreenshot.bind(browser), }, - instanceData, + instanceData: updatedInstanceData, folders: getFolders(pageOptions, self.folders, self.#getBaselineFolder()), tag, - [pageOptionsKey]:{ + [pageOptionsKey]: { wic: self.defaultOptions, method: pageOptions, }, - isNativeContext, + isNativeContext: isCurrentContextNative, testContext: enrichTestContext({ commandName, currentTestContext: self.#testContext, - instanceData, + instanceData: updatedInstanceData, tag, - }) - } - ) + }), + }] + } }) - } + + return wrapped.call(this) + } + ) } #addMultiremoteElementCommand(browser: WebdriverIO.MultiRemoteBrowser, browserNames: string[], commandName: string, command: any) { @@ -297,129 +346,164 @@ export default class WdioImageComparisonService extends BaseClass { this: WebdriverIO.MultiRemoteBrowser, element, tag, - pageOptions = {} + elementOptions = {} ) { const returnData: Record = {} const elementOptionsKey = commandName === 'saveElement' ? 'saveElementOptions' : 'checkElementOptions' for (const browserName of browserNames) { const browserInstance = browser.getInstance(browserName) - const isNativeContext = getNativeContext( - self.#browser as WebdriverIO.Browser | WebdriverIO.MultiRemoteBrowser, - browserInstance, - self._isNativeContext as NativeContextType - ) - const instanceData = await getInstanceData(browserInstance) - - returnData[browserName] = await command( - { - methods: { - executor: ( - fn: string | ((...args: InnerArguments) => ReturnValue), - ...args: InnerArguments): Promise => { - return this.execute.bind(browser)(fn, ...args) as Promise + const contextManager = self._contextManagers?.get(browserName) + + if (!contextManager) { + throw new Error(`No ContextManager found for browser instance: ${browserName}`) + } + + const isNativeContext = contextManager.isNativeContext + const initialInstanceData = await getInstanceData({ + currentBrowser: browserInstance, + initialDeviceRectangles: contextManager.getViewportContext(), + isNativeContext + }) + + const wrapped = wrapWithContext({ + browser: browserInstance, + command, + contextManager, + getArgs: () => { + const updatedInstanceData = { + ...initialInstanceData, + deviceRectangles: contextManager.getViewportContext(), + } + + return [{ + methods: { + bidiScreenshot: isBiDiScreenshotSupported(browserInstance) ? browserInstance.browsingContextCaptureScreenshot.bind(browserInstance) : undefined, + executor: ( + fn: string | ((...args: InnerArguments) => ReturnValue), + ...args: InnerArguments + ): Promise => { + return browserInstance.execute(fn, ...args) as Promise + }, + getElementRect: browserInstance.getElementRect.bind(browserInstance), + getWindowHandle: browserInstance.getWindowHandle.bind(browserInstance), + screenShot: browserInstance.takeScreenshot.bind(browserInstance), + takeElementScreenshot: browserInstance.takeElementScreenshot.bind(browserInstance), }, - getElementRect: browserInstance.getElementRect.bind(browserInstance), - screenShot: browserInstance.takeScreenshot.bind(browserInstance), - takeElementScreenshot: browserInstance.takeElementScreenshot.bind(browserInstance), - }, - instanceData, - folders:getFolders(pageOptions, self.folders, self.#getBaselineFolder()), - tag, - element, - [elementOptionsKey]:{ - wic: self.defaultOptions, - method: pageOptions, - }, - isNativeContext, - testContext: enrichTestContext({ - commandName, - currentTestContext: self.#testContext, - instanceData, + instanceData: updatedInstanceData, + folders: getFolders(elementOptions, self.folders, self.#getBaselineFolder()), tag, - }) + element, + [elementOptionsKey]: { + wic: self.defaultOptions, + method: elementOptions, + }, + isNativeContext, + testContext: enrichTestContext({ + commandName, + currentTestContext: self.#testContext, + instanceData: updatedInstanceData, + tag, + }), + }] } - ) + }) + + returnData[browserName] = await wrapped.call(browserInstance) } + return returnData }) } - #addMultiremoteCommand(browser: WebdriverIO.MultiRemoteBrowser, browserNames: string[], commandName: string, command: any) { + #addMultiremoteCommand( + browser: WebdriverIO.MultiRemoteBrowser, + browserNames: string[], + commandName: string, + command: any + ) { log.info(`Adding browser command "${commandName}" to Multi browser object`) const self = this if (commandName === 'waitForStorybookComponentToBeLoaded') { browser.addCommand(commandName, waitForStorybookComponentToBeLoaded) - } else { - browser.addCommand( - commandName, - async function ( - this: WebdriverIO.MultiRemoteBrowser, - tag, - pageOptions = {} - ) { - const returnData: Record = {} - const pageOptionsKey = PAGE_OPTIONS_MAP[commandName] - - for (const browserName of browserNames) { - const browserInstance = browser.getInstance(browserName) - const isNativeContext = getNativeContext( - self.#browser as WebdriverIO.Browser | WebdriverIO.MultiRemoteBrowser, - browserInstance, - self._isNativeContext as NativeContextType - ) - const instanceData = await getInstanceData(browserInstance) - - returnData[browserName] = await command( - { + return + } + + browser.addCommand( + commandName, + async function ( + this: WebdriverIO.MultiRemoteBrowser, + tag, + pageOptions = {} + ) { + const returnData: Record = {} + const pageOptionsKey = PAGE_OPTIONS_MAP[commandName] + + for (const browserName of browserNames) { + const browserInstance = browser.getInstance(browserName) + const contextManager = self._contextManagers?.get(browserName) + + if (!contextManager) { + throw new Error(`No ContextManager found for browser instance: ${browserName}`) + } + + const isNativeContext = getNativeContext({ + capabilities: browserInstance.requestedCapabilities, + isMobile: browserInstance.isMobile, + }) + const initialInstanceData = await getInstanceData({ + currentBrowser: browserInstance, + initialDeviceRectangles: contextManager.getViewportContext(), + isNativeContext + }) + + const wrapped = wrapWithContext({ + browser: browserInstance, + command, + contextManager, + getArgs: () => { + const updatedInstanceData = { + ...initialInstanceData, + deviceRectangles: contextManager.getViewportContext() + } + const isCurrentContextNative = contextManager.isNativeContext + + return [{ methods: { executor: ( fn: string | ((...args: InnerArguments) => ReturnValue), - ...args: InnerArguments): Promise => { - return this.execute.bind(browser)(fn, ...args) as Promise + ...args: InnerArguments + ): Promise => { + return browserInstance.execute(fn, ...args) as Promise }, getElementRect: browserInstance.getElementRect.bind(browserInstance), screenShot: browserInstance.takeScreenshot.bind(browserInstance), }, - instanceData, - folders:getFolders(pageOptions, self.folders, self.#getBaselineFolder()), + instanceData: updatedInstanceData, + folders: getFolders(pageOptions, self.folders, self.#getBaselineFolder()), tag, - [pageOptionsKey]:{ + [pageOptionsKey]: { wic: self.defaultOptions, method: pageOptions, }, - isNativeContext, + isNativeContext: isCurrentContextNative, testContext: enrichTestContext({ commandName, currentTestContext: self.#testContext, - instanceData, + instanceData: updatedInstanceData, tag, - }) - }) - } - return returnData - }) - } - } + }), + }] + } + }) - #setupMultiremoteContextListener() { - const multiremoteBrowser = this.#browser as WebdriverIO.MultiRemoteBrowser - const browserInstances = multiremoteBrowser.instances - - for (const instanceName of browserInstances) { - const instance = (multiremoteBrowser as any)[instanceName] - instance.on('result', (result:MultiremoteCommandResult) => { - if (result.command === 'getContext') { - const value = result.result.value - const sessionId = instance.sessionId - if (typeof this._isNativeContext !== 'object' || this._isNativeContext === null) { - this._isNativeContext = {} - } - this._isNativeContext[sessionId] = value.includes('NATIVE') + returnData[browserName] = await wrapped.call(browserInstance) } - }) - } + + return returnData + } + ) } #getTestContext(test: Frameworks.Test | Frameworks.World): TestContext { @@ -472,4 +556,11 @@ export default class WdioImageComparisonService extends BaseClass { throw new SevereServiceError(`Framework ${framework} is not supported by the Visual Service and should be either "mocha", "jasmine" or "cucumber".`) } + + get contextManager(): ContextManager { + if (!this._contextManager) { + throw new Error('ContextManager has not been initialized') + } + return this._contextManager + } } diff --git a/packages/visual-service/src/types.ts b/packages/visual-service/src/types.ts index 8256934ed..ea87fce11 100644 --- a/packages/visual-service/src/types.ts +++ b/packages/visual-service/src/types.ts @@ -8,8 +8,12 @@ import type { CheckFullPageMethodOptions, SaveFullPageMethodOptions, ClassOptions, + DeviceRectangles, + TestContext, + InstanceData, } from 'webdriver-image-comparison' import type { ChainablePromiseElement } from 'webdriverio' +import type { ContextManager } from './contextManager.js' type MultiOutput = { [browserName: string]: ScreenshotOutput; @@ -19,16 +23,45 @@ type MultiResult = { [browserName: string]: ImageCompareResult | number; }; export type Result = MultiResult | (ImageCompareResult | number); -export type NativeContextType = boolean | Record -export type MultiremoteCommandResult = { - command: string, - method: string, - endpoint: string, - body: Record, - result: { value: string }, - sessionId: string | undefined, - cid: string, - type: string, +export type MobileInstanceData = { + devicePixelRatio: number; + deviceRectangles: DeviceRectangles; +} +export type getFolderMethodOptions = + | CheckElementMethodOptions + | CheckFullPageMethodOptions + | CheckScreenMethodOptions + | SaveElementMethodOptions + | SaveFullPageMethodOptions + | SaveScreenMethodOptions; +export type GetInstanceDataOptions = { + currentBrowser: WebdriverIO.Browser, + initialDeviceRectangles: DeviceRectangles, + isNativeContext: boolean +} +export type EnrichTestContextOptions = { + commandName: string; + currentTestContext: TestContext; + instanceData: InstanceData; + tag: string; +} +export type GetMobileInstanceDataOptions = { + currentBrowser: WebdriverIO.Browser; + initialDeviceRectangles: DeviceRectangles; + isNativeContext:boolean; + nativeWebScreenshot:boolean; +} + +export interface WrapWithContextOptions any> { + browser: WebdriverIO.Browser + command: T + contextManager: ContextManager + getArgs: () => Parameters +} + +export interface WdioIcsOptions { + logName?: string; + name?: string; } export interface WdioIcsCommonOptions { diff --git a/packages/visual-service/src/utils.ts b/packages/visual-service/src/utils.ts index 6b951a9a1..9164f1415 100644 --- a/packages/visual-service/src/utils.ts +++ b/packages/visual-service/src/utils.ts @@ -1,24 +1,15 @@ import type { Capabilities } from '@wdio/types' import type { AppiumCapabilities } from 'node_modules/@wdio/types/build/Capabilities.js' -import { IOS_OFFSETS } from 'webdriver-image-comparison' +import { getMobileScreenSize, getMobileViewPortPosition, IOS_OFFSETS, NOT_KNOWN } from 'webdriver-image-comparison' +import type { Folders, InstanceData, TestContext } from 'webdriver-image-comparison' import type { - Folders, - InstanceData, - CheckScreenMethodOptions, - SaveScreenMethodOptions, - CheckFullPageMethodOptions, - SaveFullPageMethodOptions, - CheckElementMethodOptions, - SaveElementMethodOptions, - TestContext, -} from 'webdriver-image-comparison' -import { NOT_KNOWN } from 'webdriver-image-comparison/dist/helpers/constants.js' -import type { NativeContextType } from './types.js' - -interface WdioIcsOptions { - logName?: string; - name?: string; -} + EnrichTestContextOptions, + getFolderMethodOptions, + GetInstanceDataOptions, + GetMobileInstanceDataOptions, + MobileInstanceData, + WdioIcsOptions, +} from './types.js' /** * Get the folders data @@ -26,13 +17,6 @@ interface WdioIcsOptions { * If folder options are passed in use those values * Otherwise, use the values set during instantiation */ -type getFolderMethodOptions = - | CheckElementMethodOptions - | CheckFullPageMethodOptions - | CheckScreenMethodOptions - | SaveElementMethodOptions - | SaveFullPageMethodOptions - | SaveScreenMethodOptions; export function getFolders( methodOptions: getFolderMethodOptions, @@ -47,9 +31,9 @@ export function getFolders( } /** - * Get the size of a screenshot in pixels without the device pixel ratio + * Get the size of a base64 screenshot in pixels without the device pixel ratio */ -export function getScreenshotSize(screenshot: string, devicePixelRation = 1): { +export function getBase64ScreenshotSize(screenshot: string, devicePixelRation = 1): { height: number; width: number; } { @@ -63,7 +47,7 @@ export function getScreenshotSize(screenshot: string, devicePixelRation = 1): { * Get the device pixel ratio */ export function getDevicePixelRatio(screenshot: string, deviceScreenSize: {height:number, width: number}): number { - const screenshotSize = getScreenshotSize(screenshot) + const screenshotSize = getBase64ScreenshotSize(screenshot) const devicePixelRatio = Math.round(screenshotSize.width / deviceScreenSize.width) === Math.round(screenshotSize.height / deviceScreenSize.height) ? Math.round(screenshotSize.width / deviceScreenSize.width) : Math.round(screenshotSize.height / deviceScreenSize.width) @@ -76,36 +60,49 @@ export function getDevicePixelRatio(screenshot: string, deviceScreenSize: {heigh */ async function getMobileInstanceData({ currentBrowser, - isAndroid, - isMobile -}: { - currentBrowser: WebdriverIO.Browser; - isAndroid:boolean; - isMobile: boolean -}): Promise<{ - devicePixelRatio: number; - devicePlatformRect: { - statusBar: { height: number; x: number; width: number; y: number }; - homeBar: { height: number; x: number; width: number; y: number }; - }; - deviceScreenSize: { height: number; width: number }; -}>{ - const deviceScreenSize = { - height: 0, - width: 0, - } - const devicePlatformRect = { - statusBar: { height: 0, x: 0, width: 0, y: 0 }, - homeBar: { height: 0, x: 0, width: 0, y: 0 }, - } + initialDeviceRectangles, + isNativeContext, + nativeWebScreenshot, +}: GetMobileInstanceDataOptions): Promise{ + const { isAndroid, isIOS, isMobile } = currentBrowser let devicePixelRatio = 1 - - if (isMobile){ + let deviceRectangles = initialDeviceRectangles + + if (isMobile) { + const executor = ( + fn: string | ((...args: InnerArguments) => ReturnValue), + ...args: InnerArguments) => currentBrowser.execute(fn, ...args) as Promise + const getUrl = () => currentBrowser.getUrl() + const url = (arg:string) => currentBrowser.url(arg) const currentDriverCapabilities = currentBrowser.capabilities - const { height, width } = await currentBrowser.getWindowSize() - deviceScreenSize.height = height - deviceScreenSize.width = width + const { height: screenHeight, width: screenWidth } = await getMobileScreenSize({ + currentBrowser, + executor, + isIOS, + isNativeContext, + }) + // Update the width for the device rectangles for bottomBar, screenSize, statusBar, statusBarAndAddressBar + deviceRectangles.screenSize.height = screenHeight + deviceRectangles.screenSize.width = screenWidth + deviceRectangles.bottomBar.width = screenWidth + deviceRectangles.statusBarAndAddressBar.width = screenWidth + deviceRectangles.statusBar.width = screenWidth + deviceRectangles = await getMobileViewPortPosition({ + initialDeviceRectangles, + isAndroid, + isIOS, + isNativeContext, + methods: { + executor, + getUrl, + url, + }, + nativeWebScreenshot, + screenHeight, + screenWidth, + }) + // @TODO: 20250317: When we have all things tested with the above, we can simplify the below part to only use the iOS part // @TODO: This is al based on PORTRAIT mode if (isAndroid && currentDriverCapabilities) { // We use a few `@ts-ignore` here because `pixelRatio` and `statBarHeight` @@ -118,36 +115,38 @@ async function getMobileInstanceData({ // @ts-ignore if (currentDriverCapabilities?.statBarHeight !== undefined){ // @ts-ignore - devicePlatformRect.statusBar.height = currentDriverCapabilities?.statBarHeight - devicePlatformRect.statusBar.width = width + deviceRectangles.statusBar.height = currentDriverCapabilities?.statBarHeight + deviceRectangles.statusBar.width = deviceRectangles.screenSize.width } } else { // This is to already determine the device pixel ratio if it's not set in the capabilities const base64Image = await currentBrowser.takeScreenshot() - devicePixelRatio = getDevicePixelRatio(base64Image, deviceScreenSize) - const isIphone = width < 1024 && height < 1024 + devicePixelRatio = getDevicePixelRatio(base64Image, deviceRectangles.screenSize) + const isIphone = deviceRectangles.screenSize.width < 1024 && deviceRectangles.screenSize.height < 1024 const deviceType = isIphone ? 'IPHONE' : 'IPAD' const defaultPortraitHeight = isIphone ? 667 : 1024 - const portraitHeight = width > height ? width : height - const offsetPortraitHeight = - Object.keys(IOS_OFFSETS[deviceType]).indexOf(portraitHeight.toString()) > -1 ? portraitHeight : defaultPortraitHeight + const portraitHeight = deviceRectangles.screenSize.width > deviceRectangles.screenSize.height ? + deviceRectangles.screenSize.width : + deviceRectangles.screenSize.height + const offsetPortraitHeight = Object.keys(IOS_OFFSETS[deviceType]).indexOf(portraitHeight.toString()) > -1 ? + portraitHeight : + defaultPortraitHeight const currentOffsets = IOS_OFFSETS[deviceType][offsetPortraitHeight].PORTRAIT // NOTE: The values for iOS are based on CSS pixels, so we need to multiply them with the devicePixelRatio, // This will NOT be done here but in a central place - devicePlatformRect.statusBar = { - y: 0, + deviceRectangles.statusBar = { x: 0, - width, + y: 0, + width: deviceRectangles.screenSize.width, height: currentOffsets.STATUS_BAR, } - devicePlatformRect.homeBar = currentOffsets.HOME_BAR + deviceRectangles.homeBar = currentOffsets.HOME_BAR } } return { devicePixelRatio, - devicePlatformRect, - deviceScreenSize, + deviceRectangles, } } @@ -198,8 +197,11 @@ function getDeviceName(currentBrowser: WebdriverIO.Browser): string { /** * Get the instance data */ -export async function getInstanceData(currentBrowser: WebdriverIO.Browser): Promise { - const NOT_KNOWN = 'not-known' +export async function getInstanceData({ + currentBrowser, + initialDeviceRectangles, + isNativeContext +}: GetInstanceDataOptions): Promise { const { capabilities: currentCapabilities, requestedCapabilities } = currentBrowser const { browserName: rawBrowserName = NOT_KNOWN, @@ -240,8 +242,11 @@ export async function getInstanceData(currentBrowser: WebdriverIO.Browser): Prom // 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 || !!((requestedCapabilities as Capabilities.AppiumAndroidCapabilities)['appium:nativeWebScreenshot']) const platformVersion = (rawPlatformVersion === undefined || rawPlatformVersion === '') ? NOT_KNOWN : rawPlatformVersion.toLowerCase() + const { + devicePixelRatio: mobileDevicePixelRatio, + deviceRectangles, + } = await getMobileInstanceData({ currentBrowser, initialDeviceRectangles, isNativeContext, nativeWebScreenshot }) - const { devicePixelRatio: mobileDevicePixelRatio, devicePlatformRect, deviceScreenSize, } = await getMobileInstanceData({ currentBrowser, isAndroid, isMobile }) devicePixelRatio = isMobile ? mobileDevicePixelRatio : devicePixelRatio return { @@ -250,8 +255,7 @@ export async function getInstanceData(currentBrowser: WebdriverIO.Browser): Prom browserVersion, deviceName, devicePixelRatio, - devicePlatformRect, - deviceScreenSize, + deviceRectangles, isAndroid, isIOS, isMobile, @@ -271,79 +275,36 @@ export function getBrowserObject (elem: WebdriverIO.Element | WebdriverIO.Browse return (elemObject as WebdriverIO.Element).parent ? getBrowserObject(elemObject.parent) : elem as WebdriverIO.Browser } -/** - * We can't say it's native context if the autoWebview is provided and set to true, for all other cases we can say it's native - */ -export function determineNativeContext( - driver: WebdriverIO.Browser | WebdriverIO.MultiRemoteBrowser -): NativeContextType { - // First check if it's multi remote - if (driver.isMultiremote) { - return Object.keys(driver).reduce((acc, instanceName) => { - const instance = (driver as any)[instanceName] as WebdriverIO.Browser - - if (instance.sessionId) { - acc[instance.sessionId] = determineNativeContext(instance) as boolean - } - return acc - }, {} as Record) - } - - // If not check if it's a mobile - if (driver.isMobile) { - const isAppiumAppCapPresent = (capabilities: AppiumCapabilities) => { - const appiumKeys = [ - 'appium:app', - 'appium:bundleId', - 'appium:appPackage', - 'appium:appActivity', - 'appium:appWaitActivity', - 'appium:appWaitPackage', - 'appium:autoWebview', - ] - const optionsKeys = appiumKeys.map(key => key.replace('appium:', '')) - const isInRoot = appiumKeys.some(key => capabilities[key as keyof AppiumCapabilities] !== undefined) - // @ts-expect-error - const isInAppiumOptions = capabilities['appium:options'] && - // @ts-expect-error - optionsKeys.some(key => capabilities['appium:options']?.[key as keyof AppiumCapabilities['appium:options']] !== undefined) - // @ts-expect-error - const isInLtOptions = capabilities['lt:options'] && - // @ts-expect-error - optionsKeys.some(key => capabilities['lt:options']?.[key as keyof AppiumCapabilities['lt:options']] !== undefined) - - return !!(isInRoot || isInAppiumOptions || isInLtOptions) - } - const capabilities = driver.requestedCapabilities as WebdriverIO.Capabilities & AppiumCapabilities - const isBrowserNameFalse = !!capabilities.browserName === false - const isAutoWebviewFalse = !( - capabilities['appium:autoWebview'] === true || - capabilities['appium:options']?.autoWebview === true || - capabilities['lt:options']?.autoWebview === true - ) - - return isBrowserNameFalse && isAppiumAppCapPresent(capabilities) && isAutoWebviewFalse - } - - // If not, it's webcontext - return false -} - /** * Get the native context for the current browser */ -export function getNativeContext( - browser: WebdriverIO.Browser | WebdriverIO.MultiRemoteBrowser, - currentBrowser: WebdriverIO.Browser, - nativeContext: NativeContextType +const appiumKeys = ['app', 'bundleId', 'appPackage', 'appActivity', 'appWaitActivity', 'appWaitPackage'] as const +type AppiumKeysType = typeof appiumKeys[number] +export function getNativeContext({ capabilities, isMobile }: + { capabilities: WebdriverIO.Capabilities, isMobile: boolean } ): boolean { - if (browser.isMultiremote) { - return (nativeContext as any)[currentBrowser.sessionId] - } else if (typeof nativeContext === 'boolean') { - return nativeContext + if (!capabilities || typeof capabilities !== 'object' || !isMobile) { + return false + } + + const isAppiumAppCapPresent = (capabilities: Capabilities.RequestedStandaloneCapabilities) => { + return appiumKeys.some((key) => ( + (capabilities as Capabilities.AppiumCapabilities)[key as keyof Capabilities.AppiumCapabilities] !== undefined || + (capabilities as Capabilities.AppiumCapabilities)[`appium:${key}`as keyof Capabilities.AppiumCapabilities] !== undefined || + (capabilities as WebdriverIO.Capabilities)['appium:options']?.[key as AppiumKeysType] !== undefined || + (capabilities as WebdriverIO.Capabilities)['lt:options']?.[key as AppiumKeysType] !== undefined + )) } + const isBrowserNameFalse = !!capabilities?.browserName === false + const isAutoWebviewFalse = !( + // @ts-expect-error + capabilities?.autoWebview === true || + capabilities['appium:autoWebview'] === true || + capabilities['appium:options']?.autoWebview === true || + capabilities['lt:options']?.autoWebview === true + ) - return false + return isBrowserNameFalse && isAppiumAppCapPresent(capabilities) && isAutoWebviewFalse } /** @@ -369,14 +330,7 @@ export function enrichTestContext( platformVersion, }, tag, - }: - { - commandName: string; - currentTestContext: TestContext; - instanceData: InstanceData; - tag: string; - } -): TestContext { + }: EnrichTestContextOptions): TestContext { return { commandName, instanceData: { @@ -401,3 +355,13 @@ export function enrichTestContext( } } +/** + * Check if the current browser supports isBidi screenshots + */ +export function isBiDiScreenshotSupported(driver: WebdriverIO.Browser): boolean { + const { isBidi } = driver + const isBiDiSupported = typeof driver.browsingContextCaptureScreenshot === 'function' + + return isBidi && isBiDiSupported +} + diff --git a/packages/visual-service/src/wrapWithContext.ts b/packages/visual-service/src/wrapWithContext.ts new file mode 100644 index 000000000..726ef6414 --- /dev/null +++ b/packages/visual-service/src/wrapWithContext.ts @@ -0,0 +1,29 @@ +import type { InstanceData } from 'webdriver-image-comparison' +import type { WrapWithContextOptions } from './types.js' +import { getInstanceData } from './utils.js' + +/** + * Wrap the command with the context manager + * This will make sure that the context manager is updated when needed + * and that the command is executed in the correct context + */ + +export function wrapWithContext any>(opts: WrapWithContextOptions): () => Promise> { + const { browser, command, contextManager, getArgs } = opts + + return async function (this: WebdriverIO.Browser): Promise> { + if (contextManager.needsUpdate) { + const instanceData: InstanceData = await getInstanceData({ + currentBrowser: browser, + initialDeviceRectangles: contextManager.getViewportContext(), + isNativeContext: contextManager.isNativeContext, + }) + + contextManager.setViewPortContext(instanceData.deviceRectangles) + } + + const finalArgs = getArgs() + + return command.apply(this, finalArgs) + } +} diff --git a/packages/visual-service/tests/ContextManager.test.ts b/packages/visual-service/tests/ContextManager.test.ts new file mode 100644 index 000000000..00601f0c2 --- /dev/null +++ b/packages/visual-service/tests/ContextManager.test.ts @@ -0,0 +1,111 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { DEVICE_RECTANGLES } from 'webdriver-image-comparison' +import { ContextManager } from '../src/contextManager.js' + +vi.mock('../src/utils', async () => { + return { + getNativeContext: vi.fn().mockReturnValue(true), + } +}) + +describe('ContextManager', () => { + let browserMock: WebdriverIO.Browser + let eventHandlers: Record + + beforeEach(() => { + eventHandlers = {} + browserMock = { + capabilities: {}, + isMobile: true, + on: vi.fn((event: string, handler: Function) => { + eventHandlers[event] = handler + }) + } as unknown as WebdriverIO.Browser + }) + + it('should initialize with default values and attach event listener', () => { + const cm = new ContextManager(browserMock) + expect(cm.getViewportContext()).toEqual(DEVICE_RECTANGLES) + expect(cm.needsUpdate).toBe(false) + expect(browserMock.on).toHaveBeenCalledWith('result', expect.any(Function)) + }) + + it('should update the viewport context and reset the needsUpdate flag', () => { + const cm = new ContextManager(browserMock) + const newRects = { + ...DEVICE_RECTANGLES, + screenSize: { height: 100, width: 200 }, + } + cm.setViewPortContext(newRects) + expect(cm.getViewportContext()).toEqual(newRects) + expect(cm.needsUpdate).toBe(false) + }) + + it('should mark for update', () => { + const cm = new ContextManager(browserMock) + cm.markForUpdate() + expect(cm.needsUpdate).toBe(true) + }) + + it('should handle orientation change command', async () => { + const cm = new ContextManager(browserMock) + const event = { + command: 'setOrientation', + body: { orientation: 'LANDSCAPE' }, + result: {}, + } + // @ts-ignore + await eventHandlers.result(event) + expect(cm.needsUpdate).toBe(true) + }) + + it('should not mark for update if orientation fails', async () => { + const cm = new ContextManager(browserMock) + const event = { + command: 'setOrientation', + body: { orientation: 'PORTRAIT' }, + result: { error: 'some error' } + } + // @ts-ignore + await eventHandlers.result(event) + expect(cm.needsUpdate).toBe(false) + }) + + it('should not mark for update on invalid command', async () => { + const cm = new ContextManager(browserMock) + const event = { + command: 'unknownCommand', + body: {}, + result: {}, + } + // @ts-ignore + await eventHandlers.result(event) + expect(cm.needsUpdate).toBe(false) + }) + + it('should handle context change command (switchAppiumContext)', async () => { + const cm = new ContextManager(browserMock) + const event = { + command: 'switchAppiumContext', + body: { name: 'WEBVIEW_1' }, + result: {}, + } + // @ts-ignore + await eventHandlers.result(event) + expect(await cm.getCurrentContext()).toBe('WEBVIEW_1') + expect(cm.needsUpdate).toBe(true) + }) + + it('should handle context change command (switchContext)', async () => { + const cm = new ContextManager(browserMock) + const event = { + command: 'switchContext', + body: { name: 'NATIVE_APP' }, + result: {}, + } + // @ts-ignore + await eventHandlers.result(event) + expect(await cm.getCurrentContext()).toBe('NATIVE_APP') + expect(cm.needsUpdate).toBe(false) + }) +}) diff --git a/packages/visual-service/tests/__snapshots__/utils.test.ts.snap b/packages/visual-service/tests/__snapshots__/utils.test.ts.snap index f1b6bea65..a0074cdb6 100644 --- a/packages/visual-service/tests/__snapshots__/utils.test.ts.snap +++ b/packages/visual-service/tests/__snapshots__/utils.test.ts.snap @@ -1,5 +1,30 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html +exports[`utils > enrichTestContext > should generate the expected TestContext structure with values 1`] = ` +{ + "commandName": "checkScreen", + "framework": "mocha", + "instanceData": { + "app": "myApp.apk", + "browser": { + "name": "chrome", + "version": "114", + }, + "deviceName": "Pixel_5", + "isAndroid": true, + "isIOS": false, + "isMobile": true, + "platform": { + "name": "android", + "version": "13.0", + }, + }, + "parent": "Login tests", + "tag": "login-screen", + "title": "should show login screen", +} +`; + exports[`utils > getFolders > should be able to return the correct folders when method options are provided 1`] = ` { "actualFolder": "methodActual", @@ -19,27 +44,57 @@ exports[`utils > getFolders > should be able to return the correct folders when exports[`utils > getInstanceData > should return instance data for a mobile app with incomplete capability data 1`] = ` { "appName": "", - "browserName": "not-known", - "browserVersion": "not-known", + "browserName": "not_known", + "browserVersion": "not_known", "deviceName": "not_known", "devicePixelRatio": 3.5, - "devicePlatformRect": { + "deviceRectangles": { + "bottomBar": { + "height": 0, + "width": 100, + "x": 0, + "y": 0, + }, "homeBar": { "height": 0, "width": 0, "x": 0, "y": 0, }, + "leftSidePadding": { + "height": 0, + "width": 0, + "x": 0, + "y": 0, + }, + "rightSidePadding": { + "height": 0, + "width": 0, + "x": 0, + "y": 0, + }, + "screenSize": { + "height": 200, + "width": 100, + }, "statusBar": { "height": 144, "width": 100, "x": 0, "y": 0, }, - }, - "deviceScreenSize": { - "height": 200, - "width": 100, + "statusBarAndAddressBar": { + "height": 0, + "width": 100, + "x": 0, + "y": 0, + }, + "viewport": { + "height": 0, + "width": 0, + "x": 0, + "y": 0, + }, }, "isAndroid": true, "isIOS": false, @@ -47,35 +102,65 @@ exports[`utils > getInstanceData > should return instance data for a mobile app "logName": "", "name": "", "nativeWebScreenshot": false, - "platformName": "not-known", - "platformVersion": "not-known", + "platformName": "not_known", + "platformVersion": "not_known", } `; exports[`utils > getInstanceData > should return instance data for an Android mobile app 1`] = ` { "appName": "android.apk", - "browserName": "not-known", - "browserVersion": "not-known", + "browserName": "not_known", + "browserVersion": "not_known", "deviceName": "android emulator", "devicePixelRatio": 3.5, - "devicePlatformRect": { + "deviceRectangles": { + "bottomBar": { + "height": 0, + "width": 100, + "x": 0, + "y": 0, + }, "homeBar": { "height": 0, "width": 0, "x": 0, "y": 0, }, + "leftSidePadding": { + "height": 0, + "width": 0, + "x": 0, + "y": 0, + }, + "rightSidePadding": { + "height": 0, + "width": 0, + "x": 0, + "y": 0, + }, + "screenSize": { + "height": 200, + "width": 100, + }, "statusBar": { "height": 144, "width": 100, "x": 0, "y": 0, }, - }, - "deviceScreenSize": { - "height": 200, - "width": 100, + "statusBarAndAddressBar": { + "height": 0, + "width": 100, + "x": 0, + "y": 0, + }, + "viewport": { + "height": 0, + "width": 0, + "x": 0, + "y": 0, + }, }, "isAndroid": true, "isIOS": false, @@ -91,30 +176,60 @@ exports[`utils > getInstanceData > should return instance data for an Android mo exports[`utils > getInstanceData > should return instance data for an iOS iPad mobile app 1`] = ` { "appName": "ios.zip", - "browserName": "not-known", - "browserVersion": "not-known", + "browserName": "not_known", + "browserVersion": "not_known", "deviceName": "ipad", "devicePixelRatio": 0, - "devicePlatformRect": { + "deviceRectangles": { + "bottomBar": { + "height": 0, + "width": 834, + "x": 0, + "y": 0, + }, "homeBar": { "height": 9, "width": 276, "x": 279, "y": 1179, }, + "leftSidePadding": { + "height": 0, + "width": 0, + "x": 0, + "y": 0, + }, + "rightSidePadding": { + "height": 0, + "width": 0, + "x": 0, + "y": 0, + }, + "screenSize": { + "height": 1194, + "width": 834, + }, "statusBar": { "height": 24, "width": 834, "x": 0, "y": 0, }, - }, - "deviceScreenSize": { - "height": 1194, - "width": 834, + "statusBarAndAddressBar": { + "height": 0, + "width": 834, + "x": 0, + "y": 0, + }, + "viewport": { + "height": 0, + "width": 0, + "x": 0, + "y": 0, + }, }, "isAndroid": false, - "isIOS": false, + "isIOS": true, "isMobile": true, "logName": "", "name": "", @@ -127,30 +242,60 @@ exports[`utils > getInstanceData > should return instance data for an iOS iPad m exports[`utils > getInstanceData > should return instance data for an iOS iPad mobile app for a non matching screensize 1`] = ` { "appName": "ios.zip", - "browserName": "not-known", - "browserVersion": "not-known", + "browserName": "not_known", + "browserVersion": "not_known", "deviceName": "ipad", "devicePixelRatio": 0, - "devicePlatformRect": { + "deviceRectangles": { + "bottomBar": { + "height": 0, + "width": 1234, + "x": 0, + "y": 0, + }, "homeBar": { "height": 0, "width": 0, "x": 0, "y": 0, }, + "leftSidePadding": { + "height": 0, + "width": 0, + "x": 0, + "y": 0, + }, + "rightSidePadding": { + "height": 0, + "width": 0, + "x": 0, + "y": 0, + }, + "screenSize": { + "height": 888, + "width": 1234, + }, "statusBar": { "height": 20, "width": 1234, "x": 0, "y": 0, }, - }, - "deviceScreenSize": { - "height": 888, - "width": 1234, + "statusBarAndAddressBar": { + "height": 0, + "width": 1234, + "x": 0, + "y": 0, + }, + "viewport": { + "height": 0, + "width": 0, + "x": 0, + "y": 0, + }, }, "isAndroid": false, - "isIOS": false, + "isIOS": true, "isMobile": true, "logName": "", "name": "", @@ -163,30 +308,60 @@ exports[`utils > getInstanceData > should return instance data for an iOS iPad m exports[`utils > getInstanceData > should return instance data for an iOS iPad mobile app in landscape mode 1`] = ` { "appName": "ios.zip", - "browserName": "not-known", - "browserVersion": "not-known", + "browserName": "not_known", + "browserVersion": "not_known", "deviceName": "ipad", "devicePixelRatio": 0, - "devicePlatformRect": { + "deviceRectangles": { + "bottomBar": { + "height": 0, + "width": 1194, + "x": 0, + "y": 0, + }, "homeBar": { "height": 9, "width": 276, "x": 279, "y": 1179, }, + "leftSidePadding": { + "height": 0, + "width": 0, + "x": 0, + "y": 0, + }, + "rightSidePadding": { + "height": 0, + "width": 0, + "x": 0, + "y": 0, + }, + "screenSize": { + "height": 834, + "width": 1194, + }, "statusBar": { "height": 24, "width": 1194, "x": 0, "y": 0, }, - }, - "deviceScreenSize": { - "height": 834, - "width": 1194, + "statusBarAndAddressBar": { + "height": 0, + "width": 1194, + "x": 0, + "y": 0, + }, + "viewport": { + "height": 0, + "width": 0, + "x": 0, + "y": 0, + }, }, "isAndroid": false, - "isIOS": false, + "isIOS": true, "isMobile": true, "logName": "", "name": "", @@ -199,30 +374,60 @@ exports[`utils > getInstanceData > should return instance data for an iOS iPad m exports[`utils > getInstanceData > should return instance data for an iOS iPhone mobile app 1`] = ` { "appName": "ios.zip", - "browserName": "not-known", - "browserVersion": "not-known", + "browserName": "not_known", + "browserVersion": "not_known", "deviceName": "iphone 15 pro", "devicePixelRatio": 0, - "devicePlatformRect": { + "deviceRectangles": { + "bottomBar": { + "height": 0, + "width": 393, + "x": 0, + "y": 0, + }, "homeBar": { "height": 9, "width": 143, "x": 125, "y": 837, }, + "leftSidePadding": { + "height": 0, + "width": 0, + "x": 0, + "y": 0, + }, + "rightSidePadding": { + "height": 0, + "width": 0, + "x": 0, + "y": 0, + }, + "screenSize": { + "height": 852, + "width": 393, + }, "statusBar": { "height": 59, "width": 393, "x": 0, "y": 0, }, - }, - "deviceScreenSize": { - "height": 852, - "width": 393, + "statusBarAndAddressBar": { + "height": 0, + "width": 393, + "x": 0, + "y": 0, + }, + "viewport": { + "height": 0, + "width": 0, + "x": 0, + "y": 0, + }, }, "isAndroid": false, - "isIOS": false, + "isIOS": true, "isMobile": true, "logName": "", "name": "", @@ -234,29 +439,59 @@ exports[`utils > getInstanceData > should return instance data for an iOS iPhone exports[`utils > getInstanceData > should return instance data when the browserstack capabilities are provided 1`] = ` { - "appName": "not-known", + "appName": "not_known", "browserName": "chrome", "browserVersion": "75.123", "deviceName": "samsung galaxy s22", - "devicePixelRatio": 1, - "devicePlatformRect": { + "devicePixelRatio": 3.5, + "deviceRectangles": { + "bottomBar": { + "height": 0, + "width": 100, + "x": 0, + "y": 0, + }, "homeBar": { "height": 0, "width": 0, "x": 0, "y": 0, }, + "leftSidePadding": { + "height": 0, + "width": 0, + "x": 0, + "y": 0, + }, + "rightSidePadding": { + "height": 0, + "width": 0, + "x": 0, + "y": 0, + }, + "screenSize": { + "height": 200, + "width": 100, + }, "statusBar": { + "height": 50, + "width": 100, + "x": 0, + "y": 0, + }, + "statusBarAndAddressBar": { + "height": 0, + "width": 100, + "x": 0, + "y": 0, + }, + "viewport": { "height": 0, "width": 0, "x": 0, "y": 0, }, }, - "deviceScreenSize": { - "height": 200, - "width": 100, - }, "isAndroid": true, "isIOS": false, "isMobile": true, @@ -264,35 +499,65 @@ exports[`utils > getInstanceData > should return instance data when the browsers "name": "", "nativeWebScreenshot": false, "platformName": "osx", - "platformVersion": "not-known", + "platformVersion": "not_known", } `; exports[`utils > getInstanceData > should return instance data when the lambdatest capabilities are provided 1`] = ` { - "appName": "not-known", + "appName": "not_known", "browserName": "chrome", "browserVersion": "75.123", "deviceName": "samsung galaxy s22 lt", - "devicePixelRatio": 1, - "devicePlatformRect": { + "devicePixelRatio": 3.5, + "deviceRectangles": { + "bottomBar": { + "height": 0, + "width": 100, + "x": 0, + "y": 0, + }, "homeBar": { "height": 0, "width": 0, "x": 0, "y": 0, }, + "leftSidePadding": { + "height": 0, + "width": 0, + "x": 0, + "y": 0, + }, + "rightSidePadding": { + "height": 0, + "width": 0, + "x": 0, + "y": 0, + }, + "screenSize": { + "height": 200, + "width": 100, + }, "statusBar": { + "height": 50, + "width": 100, + "x": 0, + "y": 0, + }, + "statusBarAndAddressBar": { + "height": 0, + "width": 100, + "x": 0, + "y": 0, + }, + "viewport": { "height": 0, "width": 0, "x": 0, "y": 0, }, }, - "deviceScreenSize": { - "height": 200, - "width": 100, - }, "isAndroid": true, "isIOS": false, "isMobile": true, @@ -309,28 +574,58 @@ exports[`utils > getInstanceData > should return instance data when the lambdate exports[`utils > getInstanceData > should return instance data when the minimum of capabilities is provided 1`] = ` { - "appName": "not-known", + "appName": "not_known", "browserName": "chrome", "browserVersion": "75.123", "deviceName": "not_known", "devicePixelRatio": 1, - "devicePlatformRect": { + "deviceRectangles": { + "bottomBar": { + "height": 0, + "width": 0, + "x": 0, + "y": 0, + }, "homeBar": { "height": 0, "width": 0, "x": 0, "y": 0, }, + "leftSidePadding": { + "height": 0, + "width": 0, + "x": 0, + "y": 0, + }, + "rightSidePadding": { + "height": 0, + "width": 0, + "x": 0, + "y": 0, + }, + "screenSize": { + "height": 0, + "width": 0, + }, "statusBar": { "height": 0, "width": 0, "x": 0, "y": 0, }, - }, - "deviceScreenSize": { - "height": 0, - "width": 0, + "statusBarAndAddressBar": { + "height": 0, + "width": 0, + "x": 0, + "y": 0, + }, + "viewport": { + "height": 0, + "width": 0, + "x": 0, + "y": 0, + }, }, "isAndroid": false, "isIOS": false, @@ -339,34 +634,64 @@ exports[`utils > getInstanceData > should return instance data when the minimum "name": "", "nativeWebScreenshot": false, "platformName": "osx", - "platformVersion": "not-known", + "platformVersion": "not_known", } `; exports[`utils > getInstanceData > should return instance data when wdio-ics option log name is provided 1`] = ` { - "appName": "not-known", + "appName": "not_known", "browserName": "chrome", "browserVersion": "75.123", "deviceName": "not_known", "devicePixelRatio": 1, - "devicePlatformRect": { + "deviceRectangles": { + "bottomBar": { + "height": 0, + "width": 0, + "x": 0, + "y": 0, + }, "homeBar": { "height": 0, "width": 0, "x": 0, "y": 0, }, + "leftSidePadding": { + "height": 0, + "width": 0, + "x": 0, + "y": 0, + }, + "rightSidePadding": { + "height": 0, + "width": 0, + "x": 0, + "y": 0, + }, + "screenSize": { + "height": 0, + "width": 0, + }, "statusBar": { "height": 0, "width": 0, "x": 0, "y": 0, }, - }, - "deviceScreenSize": { - "height": 0, - "width": 0, + "statusBarAndAddressBar": { + "height": 0, + "width": 0, + "x": 0, + "y": 0, + }, + "viewport": { + "height": 0, + "width": 0, + "x": 0, + "y": 0, + }, }, "isAndroid": false, "isIOS": false, @@ -375,34 +700,64 @@ exports[`utils > getInstanceData > should return instance data when wdio-ics opt "name": "", "nativeWebScreenshot": false, "platformName": "osx", - "platformVersion": "not-known", + "platformVersion": "not_known", } `; exports[`utils > getInstanceData > should return instance data when wdio-ics option name is provided 1`] = ` { - "appName": "not-known", + "appName": "not_known", "browserName": "chrome", "browserVersion": "75.123", "deviceName": "not_known", "devicePixelRatio": 1, - "devicePlatformRect": { + "deviceRectangles": { + "bottomBar": { + "height": 0, + "width": 0, + "x": 0, + "y": 0, + }, "homeBar": { "height": 0, "width": 0, "x": 0, "y": 0, }, + "leftSidePadding": { + "height": 0, + "width": 0, + "x": 0, + "y": 0, + }, + "rightSidePadding": { + "height": 0, + "width": 0, + "x": 0, + "y": 0, + }, + "screenSize": { + "height": 0, + "width": 0, + }, "statusBar": { "height": 0, "width": 0, "x": 0, "y": 0, }, - }, - "deviceScreenSize": { - "height": 0, - "width": 0, + "statusBarAndAddressBar": { + "height": 0, + "width": 0, + "x": 0, + "y": 0, + }, + "viewport": { + "height": 0, + "width": 0, + "x": 0, + "y": 0, + }, }, "isAndroid": false, "isIOS": false, @@ -411,6 +766,20 @@ exports[`utils > getInstanceData > should return instance data when wdio-ics opt "name": "wdio-ics-name", "nativeWebScreenshot": false, "platformName": "osx", - "platformVersion": "not-known", + "platformVersion": "not_known", +} +`; + +exports[`utils > getLtOptions > should return the lt:options when it exists (correct casing) 1`] = ` +{ + "project": "testProject", + "user": "wim", +} +`; + +exports[`utils > getLtOptions > should return the lt:options when it exists (different casing) 1`] = ` +{ + "project": "testUpper", + "user": "upperCase", } `; diff --git a/packages/visual-service/tests/service.expect.test.ts b/packages/visual-service/tests/service.expect.test.ts index bf521c1f1..1d053fa08 100644 --- a/packages/visual-service/tests/service.expect.test.ts +++ b/packages/visual-service/tests/service.expect.test.ts @@ -14,6 +14,17 @@ vi.mock('webdriver-image-comparison', () => ({ saveTabbablePage: vi.fn(), checkTabbablePage: vi.fn(), DEFAULT_TEST_CONTEXT: {}, + NOT_KNOWN: 'not_known', + DEVICE_RECTANGLES: { + bottomBar: { y: 0, x: 0, width: 0, height: 0 }, + homeBar: { y: 0, x: 0, width: 0, height: 0 }, + leftSidePadding: { y: 0, x: 0, width: 0, height: 0 }, + rightSidePadding: { y: 0, x: 0, width: 0, height: 0 }, + statusBar: { y: 0, x: 0, width: 0, height: 0 }, + statusBarAndAddressBar: { y: 0, x: 0, width: 0, height: 0 }, + screenSize: { width: 0, height: 0 }, + viewport: { y: 0, x: 0, width: 0, height: 0 }, + } })) const log = logger('test') @@ -28,7 +39,8 @@ describe('@wdio/visual-service', () => { isMultiremote: false, addCommand: vi.fn(), capabilities: {}, - requestedCapabilities: {} + requestedCapabilities: {}, + on: vi.fn(), } as any as WebdriverIO.Browser await service.before({}, [], browser) // expect(log.warn).toBeCalledTimes(1) diff --git a/packages/visual-service/tests/service.test.ts b/packages/visual-service/tests/service.test.ts index 5954302be..94a781d5d 100644 --- a/packages/visual-service/tests/service.test.ts +++ b/packages/visual-service/tests/service.test.ts @@ -17,6 +17,17 @@ vi.mock('webdriver-image-comparison', () => ({ saveTabbablePage: vi.fn(), checkTabbablePage: vi.fn(), DEFAULT_TEST_CONTEXT: {}, + NOT_KNOWN: 'not_known', + DEVICE_RECTANGLES: { + bottomBar: { y: 0, x: 0, width: 0, height: 0 }, + homeBar: { y: 0, x: 0, width: 0, height: 0 }, + leftSidePadding: { y: 0, x: 0, width: 0, height: 0 }, + rightSidePadding: { y: 0, x: 0, width: 0, height: 0 }, + statusBar: { y: 0, x: 0, width: 0, height: 0 }, + statusBarAndAddressBar: { y: 0, x: 0, width: 0, height: 0 }, + screenSize: { width: 0, height: 0 }, + viewport: { y: 0, x: 0, width: 0, height: 0 }, + }, })) vi.mock('@wdio/globals', async () => ({ expect: { @@ -32,7 +43,12 @@ describe('@wdio/visual-service', () => { describe('remoteSetup', () => { it('should call the before hook when using the remoteSetup method', async () => { const service = new VisualService({}, {}, {} as unknown as WebdriverIO.Config) - const browser = { addCommand: vi.fn(), capabilities: {}, requestedCapabilities: {} } as any as WebdriverIO.Browser + const browser = { + addCommand: vi.fn(), + capabilities: {}, + requestedCapabilities: {}, + on: vi.fn(), + } as any as WebdriverIO.Browser const spy = vi.spyOn(service, 'before') await service.remoteSetup(browser as any) @@ -44,9 +60,9 @@ describe('@wdio/visual-service', () => { }) describe('before', () => { - let service - let browser - let browserInstance + let service: VisualService + let browser: WebdriverIO.Browser | WebdriverIO.MultiRemoteBrowser + let browserInstance: WebdriverIO.Browser let chromeInstance let firefoxInstance const commands = ['saveElement', 'checkElement', 'saveScreen', 'saveFullPageScreen', 'saveTabbablePage', 'checkScreen', 'checkFullPageScreen', 'checkTabbablePage', 'waitForStorybookComponentToBeLoaded'] @@ -68,6 +84,7 @@ describe('@wdio/visual-service', () => { browser = { isMultiremote: false, addCommand: vi.fn((name, fn) => { + // @ts-expect-error browser[name] = fn }), capabilities: {}, @@ -76,9 +93,12 @@ describe('@wdio/visual-service', () => { getInstance: vi.fn().mockReturnValue(browserInstance), chrome: chromeInstance, firefox: firefoxInstance, + on: vi.fn(), } as any as WebdriverIO.Browser + // @ts-expect-error browserInstance = { addCommand: vi.fn((name, fn) => { + // @ts-expect-error browserInstance[name] = fn }), capabilities: {}, @@ -97,7 +117,9 @@ describe('@wdio/visual-service', () => { it('adds command to multiremote browser in before hook', async () => { browser.isMultiremote = true + // @ts-expect-error browser.getInstances = vi.fn().mockReturnValue(['chrome', 'firefox']) + // @ts-expect-error browser.getInstance = vi.fn().mockReturnValue(browserInstance) await service.before({ @@ -119,7 +141,8 @@ describe('@wdio/visual-service', () => { isMultiremote: false, addCommand: vi.fn(), capabilities: {}, - requestedCapabilities: {} + requestedCapabilities: {}, + on: vi.fn(), } as any as WebdriverIO.Browser await service.before({}, [], browser) @@ -137,7 +160,8 @@ describe('@wdio/visual-service', () => { isMultiremote: false, addCommand: vi.fn(), capabilities: {}, - requestedCapabilities: {} + requestedCapabilities: {}, + on: vi.fn(), } as any as WebdriverIO.Browser await service.before({}, [], browser) @@ -145,22 +169,4 @@ describe('@wdio/visual-service', () => { expect(log.warn).toMatchSnapshot() }) }) - - describe('afterCommand', () => { - it('should set _isNativeContext to true when dealing with a native app', () => { - const service = new VisualService({}, {}, {} as unknown as WebdriverIO.Config) - service.afterCommand('getContext', [], 'NATIVE_APP', undefined) - - // @ts-ignore - expect(service._isNativeContext).toBe(true) - }) - - it('should set _isNativeContext to false when not dealing with a native app', () => { - const service = new VisualService({}, {}, {} as unknown as WebdriverIO.Config) - service.afterCommand('getContext', [], 'WEBVIEW', undefined) - - // @ts-ignore - expect(service._isNativeContext).toBe(false) - }) - }) }) diff --git a/packages/visual-service/tests/utils.test.ts b/packages/visual-service/tests/utils.test.ts index c1851d1ff..55095083f 100644 --- a/packages/visual-service/tests/utils.test.ts +++ b/packages/visual-service/tests/utils.test.ts @@ -1,6 +1,24 @@ -import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest' -import { determineNativeContext, getBrowserObject, getDevicePixelRatio, getFolders, getInstanceData, getScreenshotSize } from '../src/utils.js' -import type { AppiumCapabilities } from '@wdio/types/build/Capabilities.js' +import { describe, it, expect, afterEach, vi } from 'vitest' +import { + getBrowserObject, + getDevicePixelRatio, getFolders, + getInstanceData, + getBase64ScreenshotSize, + getNativeContext, + enrichTestContext, + getLtOptions, +} from '../src/utils.js' + +const DEVICE_RECTANGLES = { + bottomBar: { y: 0, x: 0, width: 0, height: 0 }, + homeBar: { y: 0, x: 0, width: 0, height: 0 }, + leftSidePadding: { y: 0, x: 0, width: 0, height: 0 }, + rightSidePadding: { y: 0, x: 0, width: 0, height: 0 }, + screenSize: { height: 0, width: 0 }, + statusBar: { y: 0, x: 0, width: 0, height: 0 }, + statusBarAndAddressBar: { y: 0, x: 0, width: 0, height: 0 }, + viewport: { y: 0, x: 0, width: 0, height: 0 }, +} describe('utils', () => { describe('getFolders', () => { @@ -31,21 +49,21 @@ describe('utils', () => { }) }) - describe('getScreenshotSize', () => { + describe('getBase64ScreenshotSize', () => { // Transparent image of 20x40 pixels const mockScreenshot = 'iVBORw0KGgoAAAANSUhEUgAAABQAAAAoCAIAAABxU02MAAAAJElEQVR4nO3LMQEAAAgDILV/59nBV/jpJHU15ynLsizLsvw+L/3pA02VPl1RAAAAAElFTkSuQmCC' const width = 20 const height = 40 it('should correctly calculate size with default device pixel ratio', () => { - const size = getScreenshotSize(mockScreenshot) + const size = getBase64ScreenshotSize(mockScreenshot) expect(size.width).toEqual(width) expect(size.height).toEqual(height) }) it('should correctly calculate size with different device pixel ratios', () => { const dpr = 2 - const size = getScreenshotSize(mockScreenshot, dpr) + const size = getBase64ScreenshotSize(mockScreenshot, dpr) expect(size.width).toEqual(width/dpr) expect(size.height).toEqual(height/dpr) }) @@ -72,6 +90,43 @@ describe('utils', () => { }) }) + describe('getLtOptions', () => { + it('should return the lt:options when it exists (correct casing)', () => { + const caps = { + 'lt:options': { user: 'wim', project: 'testProject' } + } + + expect(getLtOptions(caps)).toMatchSnapshot() + }) + + it('should return the lt:options when it exists (different casing)', () => { + const caps = { + 'LT:OPTIONS': { user: 'upperCase', project: 'testUpper' } + } + + // @ts-expect-error + expect(getLtOptions(caps)).toMatchSnapshot() + }) + + it('should return undefined when lt:options does not exist', () => { + const caps = { + platformName: 'iOS', + deviceName: 'iPhone 14' + } + + expect(getLtOptions(caps)).toBeUndefined() + }) + + it('should return undefined when capabilities is an empty object', () => { + expect(getLtOptions({})).toBeUndefined() + }) + + it('should handle unexpected types gracefully', () => { + const caps = Object.create(null) + expect(getLtOptions(caps)).toBeUndefined() + }) + }) + describe('getInstanceData', () => { const DEFAULT_DESKTOP_BROWSER = { capabilities:{ @@ -98,7 +153,7 @@ describe('utils', () => { it('should return instance data when the minimum of capabilities is provided', async() => { const driver = createDriverMock({}) - expect(await getInstanceData(driver)).toMatchSnapshot() + expect(await getInstanceData({ currentBrowser: driver, initialDeviceRectangles: DEVICE_RECTANGLES, isNativeContext:false })).toMatchSnapshot() }) it('should return instance data when wdio-ics option log name is provided', async() => { @@ -111,7 +166,7 @@ describe('utils', () => { }, }, }) - expect(await getInstanceData(driver)).toMatchSnapshot() + expect(await getInstanceData({ currentBrowser: driver, initialDeviceRectangles: DEVICE_RECTANGLES, isNativeContext:false })).toMatchSnapshot() }) it('should return instance data when wdio-ics option name is provided', async() => { @@ -124,7 +179,7 @@ describe('utils', () => { }, }, }) - expect(await getInstanceData(driver)).toMatchSnapshot() + expect(await getInstanceData({ currentBrowser: driver, initialDeviceRectangles: DEVICE_RECTANGLES, isNativeContext:false })).toMatchSnapshot() }) it('should return instance data for an Android mobile app', async() => { @@ -154,8 +209,10 @@ describe('utils', () => { isAndroid: true, isMobile: true, getWindowSize: vi.fn().mockResolvedValueOnce({ width: 100, height: 200 }), + execute: vi.fn().mockResolvedValueOnce({ realDisplaySize:'100x200' }), + getOrientation: vi.fn().mockResolvedValue('PORTRAIT') }) - expect(await getInstanceData(driver)).toMatchSnapshot() + expect(await getInstanceData({ currentBrowser: driver, initialDeviceRectangles: DEVICE_RECTANGLES, isNativeContext:true })).toMatchSnapshot() }) it('should return instance data for an iOS iPhone mobile app', async() => { @@ -184,12 +241,15 @@ describe('utils', () => { 'appium:app': '/Users/WebdriverIO/visual-testing/apps/ios.zip', }, } as WebdriverIO.Capabilities, + isIOS: true, isAndroid: false, isMobile: true, - getWindowSize: vi.fn().mockResolvedValueOnce({ height: 852, width: 393 }), takeScreenshot: vi.fn().mockResolvedValueOnce(mockScreenshot), + execute: vi.fn().mockResolvedValueOnce({ screenSize: { height: 852, width: 393 } }), + getWindowSize: vi.fn(), + getOrientation: vi.fn().mockResolvedValue('PORTRAIT') }) - expect(await getInstanceData(driver)).toMatchSnapshot() + expect(await getInstanceData({ currentBrowser: driver, initialDeviceRectangles: DEVICE_RECTANGLES, isNativeContext:true })).toMatchSnapshot() }) it('should return instance data for an iOS iPad mobile app', async() => { @@ -217,12 +277,15 @@ describe('utils', () => { 'appium:app': '/Users/WebdriverIO/visual-testing/apps/ios.zip', } as WebdriverIO.Capabilities, + isIOS: true, isAndroid: false, isMobile: true, - getWindowSize: vi.fn().mockResolvedValueOnce({ height: 1194, width: 834 }), takeScreenshot: vi.fn().mockResolvedValueOnce(mockScreenshot), + execute: vi.fn().mockResolvedValueOnce({ screenSize: { height: 1194, width: 834 } }), + getWindowSize: vi.fn(), + getOrientation: vi.fn().mockResolvedValue('PORTRAIT') }) - expect(await getInstanceData(driver)).toMatchSnapshot() + expect(await getInstanceData({ currentBrowser: driver, initialDeviceRectangles: DEVICE_RECTANGLES, isNativeContext:true })).toMatchSnapshot() }) it('should return instance data for an iOS iPad mobile app in landscape mode', async() => { @@ -251,12 +314,15 @@ describe('utils', () => { 'appium:app': '/Users/WebdriverIO/visual-testing/apps/ios.zip', } as WebdriverIO.Capabilities, + isIOS: true, isAndroid: false, isMobile: true, - getWindowSize: vi.fn().mockResolvedValueOnce({ height: 834, width: 1194 }), takeScreenshot: vi.fn().mockResolvedValueOnce(mockScreenshot), + execute: vi.fn().mockResolvedValueOnce({ screenSize: { height: 1194, width: 834 } }), + getWindowSize: vi.fn(), + getOrientation: vi.fn().mockResolvedValue('LANDSCAPE') }) - expect(await getInstanceData(driver)).toMatchSnapshot() + expect(await getInstanceData({ currentBrowser: driver, initialDeviceRectangles: DEVICE_RECTANGLES, isNativeContext:true })).toMatchSnapshot() }) it('should return instance data for an iOS iPad mobile app for a non matching screensize', async() => { @@ -283,12 +349,15 @@ describe('utils', () => { 'appium:platformVersion': '17.0', 'appium:app': '/Users/WebdriverIO/visual-testing/apps/ios.zip', } as WebdriverIO.Capabilities, + isIOS: true, isAndroid: false, isMobile: true, - getWindowSize: vi.fn().mockResolvedValueOnce({ height: 888, width: 1234 }), takeScreenshot: vi.fn().mockResolvedValueOnce(mockScreenshot), + execute: vi.fn().mockResolvedValueOnce({ screenSize: { height: 1234, width: 888 } }), + getWindowSize: vi.fn(), + getOrientation: vi.fn().mockResolvedValue('LANDSCAPE') }) - expect(await getInstanceData(driver)).toMatchSnapshot() + expect(await getInstanceData({ currentBrowser: driver, initialDeviceRectangles: DEVICE_RECTANGLES, isNativeContext:true })).toMatchSnapshot() }) it('should return instance data for a mobile app with incomplete capability data', async() => { @@ -317,14 +386,21 @@ describe('utils', () => { } as WebdriverIO.Capabilities, isAndroid: true, isMobile: true, - getWindowSize: vi.fn().mockResolvedValueOnce({ width: 100, height: 200 }), + execute: vi.fn().mockResolvedValueOnce({ realDisplaySize:'100x200' }), + getWindowSize: vi.fn(), + getOrientation: vi.fn().mockResolvedValue('PORTRAIT') }) - expect(await getInstanceData(driver)).toMatchSnapshot() + expect(await getInstanceData({ currentBrowser: driver, initialDeviceRectangles: DEVICE_RECTANGLES, isNativeContext:true })).toMatchSnapshot() }) it('should return instance data when the browserstack capabilities are provided', async() => { const driver = createDriverMock({ - ...DEFAULT_DESKTOP_BROWSER, + capabilities: { + ...DEFAULT_DESKTOP_BROWSER.capabilities, + // @ts-ignore + pixelRatio: 3.5, + statBarHeight: 50, + }, requestedCapabilities:{ ...DEFAULT_DESKTOP_BROWSER.requestedCapabilities, 'bstack:options': { @@ -334,14 +410,23 @@ describe('utils', () => { }, isAndroid: true, isMobile: true, - getWindowSize: vi.fn().mockResolvedValueOnce({ width: 100, height: 200 }), + execute: vi.fn().mockResolvedValueOnce({ realDisplaySize: '100x200' }), + getWindowSize: vi.fn(), + getOrientation: vi.fn().mockResolvedValue('PORTRAIT') }) - expect(await getInstanceData(driver)).toMatchSnapshot() + expect(await getInstanceData({ currentBrowser: driver, initialDeviceRectangles: DEVICE_RECTANGLES, isNativeContext:true })).toMatchSnapshot() }) it('should return instance data when the lambdatest capabilities are provided', async() => { const driver = createDriverMock({ - ...DEFAULT_DESKTOP_BROWSER, + capabilities: { + ...DEFAULT_DESKTOP_BROWSER.capabilities, + // @ts-ignore + deviceName: 'Samsung Galaxy S22 LT', + platformVersion: '11', + pixelRatio: 3.5, + statBarHeight: 50, + }, requestedCapabilities:{ ...DEFAULT_DESKTOP_BROWSER.requestedCapabilities, 'lt:options': { @@ -349,16 +434,13 @@ describe('utils', () => { platformVersion: '11', }, }, - capabilities: { - ...DEFAULT_DESKTOP_BROWSER.capabilities, - // @ts-expect-error - platformVersion: '11', - }, isAndroid: true, isMobile: true, - getWindowSize: vi.fn().mockResolvedValueOnce({ width: 100, height: 200 }), + execute: vi.fn().mockResolvedValueOnce({ realDisplaySize: '100x200' }), + getWindowSize: vi.fn(), + getOrientation: vi.fn().mockResolvedValue('PORTRAIT') }) - expect(await getInstanceData(driver)).toMatchSnapshot() + expect(await getInstanceData({ currentBrowser: driver, initialDeviceRectangles: DEVICE_RECTANGLES, isNativeContext:true })).toMatchSnapshot() }) }) @@ -401,232 +483,128 @@ describe('utils', () => { }) }) - describe('determineNativeContext', ()=>{ - const DRIVER_DEFAULTS = { - sessionId: 'sessionId', - isMobile: true, - capabilities:{} as WebdriverIO.Capabilities, - requestedCapabilities:{} as WebdriverIO.Capabilities | AppiumCapabilities, - } - let driver = {} as WebdriverIO.Browser - beforeEach(() => { - driver = structuredClone(DRIVER_DEFAULTS) as WebdriverIO.Browser - }) - - it('should return false for desktop browsers', async() => { - driver.isMobile = false - expect(await determineNativeContext(driver)).toBeFalsy() - }) - - it('should return false for mobile browsers when no browser has been set', async() => { - expect(await determineNativeContext(driver)).toBeFalsy() - }) - - it('should return false for mobile browsers', async() => { - (driver.capabilities as WebdriverIO.Capabilities).browserName = 'chrome' - expect(await determineNativeContext(driver)).toBeFalsy() - }) - - it('should return true for when the app is provided and browser name is empty', async() => { - (driver.capabilities as WebdriverIO.Capabilities).browserName = ''; - (driver.requestedCapabilities as AppiumCapabilities)['appium:app'] = 'app' - expect(await determineNativeContext(driver)).toBeTruthy() - }) - - it('should return true for when the app is provided and autoWebview is false', async() => { - (driver.capabilities as WebdriverIO.Capabilities).browserName = ''; - (driver.requestedCapabilities as AppiumCapabilities)['appium:app'] = 'app'; - (driver.requestedCapabilities as AppiumCapabilities)['appium:autoWebview'] = false - - expect(await determineNativeContext(driver)).toBeTruthy() - }) - - it('should return false for when the app is provided and autoWebview is true', async() => { - (driver.capabilities as WebdriverIO.Capabilities).browserName = ''; - (driver.requestedCapabilities as AppiumCapabilities)['appium:app'] = 'app'; - (driver.requestedCapabilities as AppiumCapabilities)['appium:autoWebview'] = true - - expect(await determineNativeContext(driver)).toBeFalsy() - }) - - it('should return true for when appium:appPackage is provided and autoWebview is true', async() => { - (driver.capabilities as WebdriverIO.Capabilities).browserName = ''; - (driver.requestedCapabilities as AppiumCapabilities)['appium:appPackage'] = 'string'; - (driver.requestedCapabilities as AppiumCapabilities)['appium:autoWebview'] = true - - expect(await determineNativeContext(driver)).toBeFalsy() - }) - - // For iOS - it('should return true for when appium:bundleId is provided and autoWebview is true', async() => { - (driver.capabilities as WebdriverIO.Capabilities).browserName = ''; - (driver.requestedCapabilities as AppiumCapabilities)['appium:bundleId'] = 'string'; - (driver.requestedCapabilities as AppiumCapabilities)['appium:autoWebview'] = true - - expect(await determineNativeContext(driver)).toBeFalsy() + describe('getNativeContext', () => { + it('should return false if capabilities is not an object', () => { + expect(getNativeContext({ capabilities: null as any, isMobile: true })).toBe(false) + expect(getNativeContext({ capabilities: undefined as any, isMobile: true })).toBe(false) + expect(getNativeContext({ capabilities: 'not-object' as any, isMobile: true })).toBe(false) }) - // For Android - it('should return true for when appium:appPackage is provided', async() => { - (driver.capabilities as WebdriverIO.Capabilities).browserName = ''; - (driver.requestedCapabilities as AppiumCapabilities)['appium:appPackage'] = 'string' - - expect(await determineNativeContext(driver)).toBeTruthy() - }) - - it('should return true for when appium:appActivity is provided', async() => { - (driver.capabilities as WebdriverIO.Capabilities).browserName = ''; - (driver.requestedCapabilities as AppiumCapabilities)['appium:appActivity'] = 'appActivity' - - expect(await determineNativeContext(driver)).toBeTruthy() - }) - - it('should return true for when appium:appWaitActivity is provided', async() => { - (driver.capabilities as WebdriverIO.Capabilities).browserName = ''; - (driver.requestedCapabilities as AppiumCapabilities)['appium:appWaitActivity'] = 'appWaitActivity' - - expect(await determineNativeContext(driver)).toBeTruthy() - }) - - it('should return true for when appium:appWaitPackage is provided', async() => { - (driver.capabilities as WebdriverIO.Capabilities).browserName = ''; - (driver.requestedCapabilities as AppiumCapabilities)['appium:appWaitPackage'] = 'appWaitPackage' - - expect(await determineNativeContext(driver)).toBeTruthy() - }) - - /** - * For the `appium:options` - */ - it('should return true for when the app is provided and browser name is empty for the `appium:options`', async() => { - (driver.capabilities as WebdriverIO.Capabilities).browserName = ''; - (driver.requestedCapabilities as AppiumCapabilities)['appium:options'] = { 'app': 'app' } - expect(await determineNativeContext(driver)).toBeTruthy() - }) - - it('should return true for when the app is provided and autoWebview is false for the `appium:options`', async() => { - (driver.capabilities as WebdriverIO.Capabilities).browserName = ''; - (driver.requestedCapabilities as AppiumCapabilities)['appium:options'] = { app: 'app', autoWebview: false } - - expect(await determineNativeContext(driver)).toBeTruthy() + it('should return false if isMobile is false', () => { + expect(getNativeContext({ capabilities: {}, isMobile: false })).toBe(false) }) - it('should return false for when the app is provided and autoWebview is true for the `appium:options`', async() => { - (driver.capabilities as WebdriverIO.Capabilities).browserName = ''; - (driver.requestedCapabilities as AppiumCapabilities)['appium:options'] = { app: 'app', autoWebview: true } - - expect(await determineNativeContext(driver)).toBeFalsy() - }) - - it('should return true for when appPackage is provided and autoWebview is true for the `appium:options`', async() => { - (driver.capabilities as WebdriverIO.Capabilities).browserName = ''; - (driver.requestedCapabilities as AppiumCapabilities)['appium:options'] = { appPackage: 'string', autoWebview: true } - - expect(await determineNativeContext(driver)).toBeFalsy() + it('should return false if browserName is present', () => { + const capabilities = { browserName: 'chrome' } + expect(getNativeContext({ capabilities, isMobile: true })).toBe(false) }) - // For iOS - it('should return true for when bundleId is provided and autoWebview is true for the `appium:options`', async() => { - (driver.capabilities as WebdriverIO.Capabilities).browserName = ''; - (driver.requestedCapabilities as AppiumCapabilities)['appium:options'] = { bundleId: 'string', autoWebview: true } - - expect(await determineNativeContext(driver)).toBeFalsy() - }) - - // For Android - it('should return true for when appPackage is provided for the `appium:options`', async() => { - (driver.capabilities as WebdriverIO.Capabilities).browserName = ''; - (driver.requestedCapabilities as AppiumCapabilities)['appium:options'] = { appPackage: 'string' } - - expect(await determineNativeContext(driver)).toBeTruthy() - }) - - it('should return true for when appActivity is provided for the `appium:options`', async() => { - (driver.capabilities as WebdriverIO.Capabilities).browserName = ''; - (driver.requestedCapabilities as AppiumCapabilities)['appium:options'] = { appActivity: 'appActivity' } - - expect(await determineNativeContext(driver)).toBeTruthy() - }) - - it('should return true for when appWaitActivity is provided for the `appium:options`', async() => { - (driver.capabilities as WebdriverIO.Capabilities).browserName = ''; - (driver.requestedCapabilities as AppiumCapabilities)['appium:options'] = { appWaitActivity: 'appWaitActivity' } - - expect(await determineNativeContext(driver)).toBeTruthy() - }) + it('should return false if autoWebview is true in various places', () => { + const variants = [ + { autoWebview: true }, + { 'appium:autoWebview': true }, + { 'appium:options': { autoWebview: true } }, + { 'lt:options': { autoWebview: true } }, + ] - it('should return true for when appWaitPackage is provided for the `appium:options`', async() => { - (driver.capabilities as WebdriverIO.Capabilities).browserName = ''; - (driver.requestedCapabilities as AppiumCapabilities)['appium:options'] = { appWaitPackage: 'appWaitPackage' } - - expect(await determineNativeContext(driver)).toBeTruthy() - }) - - /** - * For the `lt:options` - */ - it('should return true for when the app is provided and browser name is empty for the `lt:options`', async() => { - (driver.capabilities as WebdriverIO.Capabilities).browserName = ''; - (driver.requestedCapabilities as AppiumCapabilities)['lt:options'] = { 'app': 'app' } - expect(await determineNativeContext(driver)).toBeTruthy() - }) - - it('should return true for when the app is provided and autoWebview is false for the `lt:options`', async() => { - (driver.capabilities as WebdriverIO.Capabilities).browserName = ''; - (driver.requestedCapabilities as AppiumCapabilities)['lt:options'] = { app: 'app', autoWebview: false } - - expect(await determineNativeContext(driver)).toBeTruthy() - }) - - it('should return false for when the app is provided and autoWebview is true for the `lt:options`', async() => { - (driver.capabilities as WebdriverIO.Capabilities).browserName = ''; - (driver.requestedCapabilities as AppiumCapabilities)['lt:options'] = { app: 'app', autoWebview: true } - - expect(await determineNativeContext(driver)).toBeFalsy() - }) - - it('should return true for when appPackage is provided and autoWebview is true for the `lt:options`', async() => { - (driver.capabilities as WebdriverIO.Capabilities).browserName = ''; - (driver.requestedCapabilities as AppiumCapabilities)['lt:options'] = { appPackage: 'string', autoWebview: true } - - expect(await determineNativeContext(driver)).toBeFalsy() + for (const caps of variants) { + const capabilities = { browserName: undefined, ...caps } + expect(getNativeContext({ capabilities, isMobile: true })).toBe(false) + } }) - // For iOS - it('should return true for when bundleId is provided and autoWebview is true for the `lt:options`', async() => { - (driver.capabilities as WebdriverIO.Capabilities).browserName = ''; - (driver.requestedCapabilities as AppiumCapabilities)['lt:options'] = { bundleId: 'string', autoWebview: true } - - expect(await determineNativeContext(driver)).toBeFalsy() + it('should return true if browserName is falsy, autoWebview is false, and appium app caps are present in root', () => { + const capabilities = { + browserName: undefined, + app: 'my.app', + } + expect(getNativeContext({ capabilities, isMobile: true })).toBe(true) }) - // For Android - it('should return true for when appPackage is provided for the `lt:options`', async() => { - (driver.capabilities as WebdriverIO.Capabilities).browserName = ''; - (driver.requestedCapabilities as AppiumCapabilities)['lt:options'] = { appPackage: 'string' } - - expect(await determineNativeContext(driver)).toBeTruthy() + it('should return true if appPackage is in appium:options and autoWebview is false', () => { + const capabilities = { + browserName: undefined, + 'appium:options': { + appPackage: 'com.example', + }, + } + expect(getNativeContext({ capabilities, isMobile: true })).toBe(true) }) - it('should return true for when appActivity is provided for the `lt:options`', async() => { - (driver.capabilities as WebdriverIO.Capabilities).browserName = ''; - (driver.requestedCapabilities as AppiumCapabilities)['lt:options'] = { appActivity: 'appActivity' } - - expect(await determineNativeContext(driver)).toBeTruthy() + it('should return true if bundleId is in lt:options and autoWebview is false', () => { + const capabilities = { + browserName: undefined, + 'lt:options': { + bundleId: 'com.example.app', + }, + } + expect(getNativeContext({ capabilities, isMobile: true })).toBe(true) }) - it('should return true for when appWaitActivity is provided for the `lt:options`', async() => { - (driver.capabilities as WebdriverIO.Capabilities).browserName = ''; - (driver.requestedCapabilities as AppiumCapabilities)['lt:options'] = { appWaitActivity: 'appWaitActivity' } - - expect(await determineNativeContext(driver)).toBeTruthy() + it('should return false if no appium-related caps are found', () => { + const capabilities = { + browserName: undefined, + someOtherCap: true + } + expect(getNativeContext({ capabilities, isMobile: true })).toBe(false) }) + }) - it('should return true for when appWaitPackage is provided for the `lt:options`', async() => { - (driver.capabilities as WebdriverIO.Capabilities).browserName = ''; - (driver.requestedCapabilities as AppiumCapabilities)['lt:options'] = { appWaitPackage: 'appWaitPackage' } + describe('enrichTestContext', () => { + it('should generate the expected TestContext structure with values', () => { + const result = enrichTestContext({ + commandName: 'checkScreen', + currentTestContext: { + commandName: 'checkScreen', + framework: 'mocha', + parent: 'Login tests', + tag: 'login-screen', + title: 'should show login screen', + instanceData: { + browser: { + name: 'chrome', + version: '114', + }, + deviceName: 'Pixel_5', + platform: { + name: 'android', + version: '13.0', + }, + app: 'myApp.apk', + isMobile: true, + isAndroid: true, + isIOS: false, + } + }, + instanceData: { + appName: 'myApp.apk', + browserName: 'chrome', + browserVersion: '114', + deviceName: 'Pixel_5', + isMobile: true, + isAndroid: true, + isIOS: false, + platformName: 'android', + platformVersion: '13.0', + devicePixelRatio: 3.5, + deviceRectangles: { + bottomBar: { y: 0, x: 0, width: 0, height: 0 }, + homeBar: { y: 0, x: 0, width: 0, height: 0 }, + leftSidePadding: { y: 0, x: 0, width: 0, height: 0 }, + rightSidePadding: { y: 0, x: 0, width: 0, height: 0 }, + statusBar: { y: 0, x: 0, width: 0, height: 0 }, + statusBarAndAddressBar: { y: 0, x: 0, width: 0, height: 0 }, + screenSize: { width: 0, height: 0 }, + viewport: { y: 0, x: 0, width: 0, height: 0 }, + }, + logName: 'Pixel_5_Chrome', + name: 'Pixel_5', + nativeWebScreenshot: false, + }, + tag: 'login-screen' + }) - expect(await determineNativeContext(driver)).toBeTruthy() + expect(result).toMatchSnapshot() }) }) }) diff --git a/packages/visual-service/tests/wrapWithContext.test.ts b/packages/visual-service/tests/wrapWithContext.test.ts new file mode 100644 index 000000000..4db7024aa --- /dev/null +++ b/packages/visual-service/tests/wrapWithContext.test.ts @@ -0,0 +1,62 @@ +import { DEVICE_RECTANGLES } from 'webdriver-image-comparison' +import { describe, it, expect, vi, beforeEach } from 'vitest' +import * as utilsModule from '../src/utils.js' +import { wrapWithContext } from '../src/wrapWithContext.js' + +describe('wrapWithContext', () => { + let mockCommand: any + let mockContextManager: any + let mockBrowser: WebdriverIO.Browser + + beforeEach(() => { + mockCommand = vi.fn() + mockContextManager = { + needsUpdate: false, + getViewportContext: vi.fn(() => DEVICE_RECTANGLES), + setViewPortContext: vi.fn(), + isNativeContext: false, + } + mockBrowser = {} as WebdriverIO.Browser + }) + + it('should call command directly when no update is needed', async () => { + const wrapper = wrapWithContext({ + browser: mockBrowser, + command: mockCommand, + contextManager: mockContextManager, + getArgs: () => ['arg1'] + }) + + await wrapper.call(mockBrowser) + expect(mockCommand).toHaveBeenCalledWith('arg1') + expect(mockContextManager.setViewPortContext).not.toHaveBeenCalled() + }) + + it('should fetch new instanceData and update context when needed', async () => { + mockContextManager.needsUpdate = true + const updatedRectangles = { + ...DEVICE_RECTANGLES, + screenSize: { height: 900, width: 450 } + } + + const getInstanceDataMock = vi.spyOn(utilsModule, 'getInstanceData').mockResolvedValue({ + deviceRectangles: updatedRectangles, + devicePixelRatio: 3, + } as any) + + const wrapper = wrapWithContext({ + browser: mockBrowser, + command: mockCommand, + contextManager: mockContextManager, + getArgs: () => ['arg2'] + }) + + await wrapper.call(mockBrowser) + + expect(getInstanceDataMock).toHaveBeenCalled() + expect(mockContextManager.setViewPortContext).toHaveBeenCalledWith(updatedRectangles) + expect(mockCommand).toHaveBeenCalledWith('arg2') + + getInstanceDataMock.mockRestore() + }) +}) diff --git a/packages/webdriver-image-comparison/package.json b/packages/webdriver-image-comparison/package.json index f3de72e53..0a8b958be 100644 --- a/packages/webdriver-image-comparison/package.json +++ b/packages/webdriver-image-comparison/package.json @@ -31,6 +31,6 @@ }, "devDependencies": { "@types/fs-extra": "^11.0.4", - "webdriverio": "^9.12.4" + "webdriverio": "^9.12.6" } } diff --git a/packages/webdriver-image-comparison/src/__snapshots__/base.test.ts.snap b/packages/webdriver-image-comparison/src/__snapshots__/base.test.ts.snap index 1be051254..bf6200bd7 100644 --- a/packages/webdriver-image-comparison/src/__snapshots__/base.test.ts.snap +++ b/packages/webdriver-image-comparison/src/__snapshots__/base.test.ts.snap @@ -48,6 +48,7 @@ exports[`BaseClass > initializes default options correctly 1`] = ` }, }, "toolBarShadowPadding": 6, + "userBasedFullPageScreenshot": false, "waitForFontsLoaded": true, } `; diff --git a/packages/webdriver-image-comparison/src/clientSideScripts/__snapshots__/getAndroidStatusAddressToolBarHeight.test.ts.snap b/packages/webdriver-image-comparison/src/clientSideScripts/__snapshots__/getAndroidStatusAddressToolBarHeight.test.ts.snap index f01cc09e5..991a4e5c6 100644 --- a/packages/webdriver-image-comparison/src/clientSideScripts/__snapshots__/getAndroidStatusAddressToolBarHeight.test.ts.snap +++ b/packages/webdriver-image-comparison/src/clientSideScripts/__snapshots__/getAndroidStatusAddressToolBarHeight.test.ts.snap @@ -2,15 +2,21 @@ exports[`getAndroidStatusAddressToolBarOffsets > should get the android status, address and toolbar height with major and minor version in the navigator 1`] = ` { - "safeArea": 0, - "screenHeight": 732, - "screenWidth": 412, - "sideBar": { + "leftSidePadding": { "height": 0, "width": 0, "x": 0, "y": 0, }, + "rightSidePadding": { + "height": 0, + "width": 0, + "x": 0, + "y": 0, + }, + "safeArea": 0, + "screenHeight": 732, + "screenWidth": 412, "statusAddressBar": { "height": 80, "width": 412, @@ -28,15 +34,21 @@ exports[`getAndroidStatusAddressToolBarOffsets > should get the android status, exports[`getAndroidStatusAddressToolBarOffsets > should get the android status, address and toolbar height with major, minor and patch version in the navigator 1`] = ` { - "safeArea": 0, - "screenHeight": 732, - "screenWidth": 412, - "sideBar": { + "leftSidePadding": { "height": 0, "width": 0, "x": 0, "y": 0, }, + "rightSidePadding": { + "height": 0, + "width": 0, + "x": 0, + "y": 0, + }, + "safeArea": 0, + "screenHeight": 732, + "screenWidth": 412, "statusAddressBar": { "height": 80, "width": 412, @@ -54,15 +66,21 @@ exports[`getAndroidStatusAddressToolBarOffsets > should get the android status, exports[`getAndroidStatusAddressToolBarOffsets > should get the android status, address and toolbar height with only a major version in the navigator 1`] = ` { - "safeArea": 0, - "screenHeight": 732, - "screenWidth": 412, - "sideBar": { + "leftSidePadding": { "height": 0, "width": 0, "x": 0, "y": 0, }, + "rightSidePadding": { + "height": 0, + "width": 0, + "x": 0, + "y": 0, + }, + "safeArea": 0, + "screenHeight": 732, + "screenWidth": 412, "statusAddressBar": { "height": 80, "width": 412, @@ -80,15 +98,21 @@ exports[`getAndroidStatusAddressToolBarOffsets > should get the android status, exports[`getAndroidStatusAddressToolBarOffsets > should set the default toolbar height when the toolbar height will become negative 1`] = ` { - "safeArea": 0, - "screenHeight": 732, - "screenWidth": 412, - "sideBar": { + "leftSidePadding": { "height": 0, "width": 0, "x": 0, "y": 0, }, + "rightSidePadding": { + "height": 0, + "width": 0, + "x": 0, + "y": 0, + }, + "safeArea": 0, + "screenHeight": 732, + "screenWidth": 412, "statusAddressBar": { "height": 24, "width": 412, @@ -106,15 +130,21 @@ exports[`getAndroidStatusAddressToolBarOffsets > should set the default toolbar exports[`getAndroidStatusAddressToolBarOffsets > should set the dimensions properly for a device in landscape mode 1`] = ` { - "safeArea": 0, - "screenHeight": 732, - "screenWidth": 732, - "sideBar": { + "leftSidePadding": { "height": 0, "width": 0, "x": 0, "y": 0, }, + "rightSidePadding": { + "height": 0, + "width": 0, + "x": 0, + "y": 0, + }, + "safeArea": 0, + "screenHeight": 732, + "screenWidth": 732, "statusAddressBar": { "height": 24, "width": 412, @@ -132,15 +162,21 @@ exports[`getAndroidStatusAddressToolBarOffsets > should set the dimensions prope exports[`getAndroidStatusAddressToolBarOffsets > should set the dimensions properly for a table in landscape mode 1`] = ` { - "safeArea": 0, - "screenHeight": 1024, - "screenWidth": 1024, - "sideBar": { + "leftSidePadding": { "height": 0, "width": 0, "x": 0, "y": 0, }, + "rightSidePadding": { + "height": 0, + "width": 0, + "x": 0, + "y": 0, + }, + "safeArea": 0, + "screenHeight": 1024, + "screenWidth": 1024, "statusAddressBar": { "height": 24, "width": 1024, diff --git a/packages/webdriver-image-comparison/src/clientSideScripts/__snapshots__/getIosStatusAddressToolBarOffsets.test.ts.snap b/packages/webdriver-image-comparison/src/clientSideScripts/__snapshots__/getIosStatusAddressToolBarOffsets.test.ts.snap index 604d9ba65..7a2578e6f 100644 --- a/packages/webdriver-image-comparison/src/clientSideScripts/__snapshots__/getIosStatusAddressToolBarOffsets.test.ts.snap +++ b/packages/webdriver-image-comparison/src/clientSideScripts/__snapshots__/getIosStatusAddressToolBarOffsets.test.ts.snap @@ -2,15 +2,21 @@ exports[`getIosStatusAddressToolBarOffsets > should get the correct status bar height for an iPhone 11 with iOS 13 to validate the iPhone 11 hack 1`] = ` { - "safeArea": 48, - "screenHeight": 896, - "screenWidth": 375, - "sideBar": { + "leftSidePadding": { "height": 0, "width": 0, "x": 0, "y": 0, }, + "rightSidePadding": { + "height": 0, + "width": 0, + "x": 0, + "y": 0, + }, + "safeArea": 48, + "screenHeight": 896, + "screenWidth": 375, "statusAddressBar": { "height": 94, "width": 375, @@ -28,15 +34,21 @@ exports[`getIosStatusAddressToolBarOffsets > should get the correct status bar h exports[`getIosStatusAddressToolBarOffsets > should get the correct status bar height for an iPhone 11 with iOS 13 to validate the iPhone 11 hack in landscape mode 1`] = ` { - "safeArea": 44, - "screenHeight": 375, - "screenWidth": 896, - "sideBar": { + "leftSidePadding": { "height": 0, "width": 0, "x": 0, "y": 0, }, + "rightSidePadding": { + "height": 0, + "width": 0, + "x": 0, + "y": 0, + }, + "safeArea": 44, + "screenHeight": 375, + "screenWidth": 896, "statusAddressBar": { "height": 50, "width": 896, @@ -54,15 +66,21 @@ exports[`getIosStatusAddressToolBarOffsets > should get the correct status bar h exports[`getIosStatusAddressToolBarOffsets > should get the correct status bar height for an iPhone 11 with iOS 15 1`] = ` { - "safeArea": 48, - "screenHeight": 896, - "screenWidth": 375, - "sideBar": { + "leftSidePadding": { "height": 0, "width": 0, "x": 0, "y": 0, }, + "rightSidePadding": { + "height": 0, + "width": 0, + "x": 0, + "y": 0, + }, + "safeArea": 48, + "screenHeight": 896, + "screenWidth": 375, "statusAddressBar": { "height": 48, "width": 375, @@ -80,15 +98,21 @@ exports[`getIosStatusAddressToolBarOffsets > should get the correct status bar h exports[`getIosStatusAddressToolBarOffsets > should get the correct status, address and toolbar height for a default iPhone with iOS 14 in Portrait mode 1`] = ` { - "safeArea": 0, - "screenHeight": 667, - "screenWidth": 375, - "sideBar": { + "leftSidePadding": { "height": 0, "width": 0, "x": 0, "y": 0, }, + "rightSidePadding": { + "height": 0, + "width": 0, + "x": 0, + "y": 0, + }, + "safeArea": 0, + "screenHeight": 667, + "screenWidth": 375, "statusAddressBar": { "height": 70, "width": 375, @@ -106,15 +130,21 @@ exports[`getIosStatusAddressToolBarOffsets > should get the correct status, addr exports[`getIosStatusAddressToolBarOffsets > should get the correct status, address and toolbar height for a default iPhone with iOS 14 in landscape mode 1`] = ` { - "safeArea": 0, - "screenHeight": 375, - "screenWidth": 667, - "sideBar": { + "leftSidePadding": { "height": 0, "width": 0, "x": 0, "y": 0, }, + "rightSidePadding": { + "height": 0, + "width": 0, + "x": 0, + "y": 0, + }, + "safeArea": 0, + "screenHeight": 375, + "screenWidth": 667, "statusAddressBar": { "height": 50, "width": 667, @@ -132,15 +162,21 @@ exports[`getIosStatusAddressToolBarOffsets > should get the correct status, addr exports[`getIosStatusAddressToolBarOffsets > should get the correct status, address and toolbar height for a default iPhone with iOS 15 in landscape mode 1`] = ` { - "safeArea": 44, - "screenHeight": 375, - "screenWidth": 812, - "sideBar": { + "leftSidePadding": { "height": 0, "width": 0, "x": 0, "y": 0, }, + "rightSidePadding": { + "height": 0, + "width": 0, + "x": 0, + "y": 0, + }, + "safeArea": 44, + "screenHeight": 375, + "screenWidth": 812, "statusAddressBar": { "height": 50, "width": 812, @@ -158,15 +194,21 @@ exports[`getIosStatusAddressToolBarOffsets > should get the correct status, addr exports[`getIosStatusAddressToolBarOffsets > should get the correct status, address and toolbar height for a default iPhone with iOS 15 in portrait mode 1`] = ` { - "safeArea": 44, - "screenHeight": 812, - "screenWidth": 375, - "sideBar": { + "leftSidePadding": { "height": 0, "width": 0, "x": 0, "y": 0, }, + "rightSidePadding": { + "height": 0, + "width": 0, + "x": 0, + "y": 0, + }, + "safeArea": 44, + "screenHeight": 812, + "screenWidth": 375, "statusAddressBar": { "height": 44, "width": 375, @@ -184,15 +226,21 @@ exports[`getIosStatusAddressToolBarOffsets > should get the correct status, addr exports[`getIosStatusAddressToolBarOffsets > should get the correct status, address and toolbar height for an iPad 1`] = ` { - "safeArea": 0, - "screenHeight": 1366, - "screenWidth": 1024, - "sideBar": { + "leftSidePadding": { "height": 0, "width": 0, "x": 0, "y": 0, }, + "rightSidePadding": { + "height": 0, + "width": 0, + "x": 0, + "y": 0, + }, + "safeArea": 0, + "screenHeight": 1366, + "screenWidth": 1024, "statusAddressBar": { "height": 74, "width": 1024, @@ -210,15 +258,21 @@ exports[`getIosStatusAddressToolBarOffsets > should get the correct status, addr exports[`getIosStatusAddressToolBarOffsets > should get the correct status, address and toolbar height for an iPad in landscape mode 1`] = ` { - "safeArea": 0, - "screenHeight": 1024, - "screenWidth": 1366, - "sideBar": { + "leftSidePadding": { + "height": 0, + "width": 0, + "x": 0, + "y": 0, + }, + "rightSidePadding": { "height": 950, "width": 320, "x": 0, "y": 74, }, + "safeArea": 0, + "screenHeight": 1024, + "screenWidth": 1366, "statusAddressBar": { "height": 74, "width": 1366, @@ -236,15 +290,21 @@ exports[`getIosStatusAddressToolBarOffsets > should get the correct status, addr exports[`getIosStatusAddressToolBarOffsets > should get the correct status, address and toolbar height for an iPad with big sizes 1`] = ` { - "safeArea": 0, - "screenHeight": 5432, - "screenWidth": 9876, - "sideBar": { + "leftSidePadding": { "height": 0, "width": 0, "x": 0, "y": 0, }, + "rightSidePadding": { + "height": 0, + "width": 0, + "x": 0, + "y": 0, + }, + "safeArea": 0, + "screenHeight": 5432, + "screenWidth": 9876, "statusAddressBar": { "height": 70, "width": 9876, @@ -262,15 +322,21 @@ exports[`getIosStatusAddressToolBarOffsets > should get the correct status, addr exports[`getIosStatusAddressToolBarOffsets > should get the correct status, address and toolbar height for an iPhone with extreme not known dimensions 1`] = ` { - "safeArea": 0, - "screenHeight": 896, - "screenWidth": 1024, - "sideBar": { + "leftSidePadding": { "height": 0, "width": 0, "x": 0, "y": 0, }, + "rightSidePadding": { + "height": 0, + "width": 0, + "x": 0, + "y": 0, + }, + "safeArea": 0, + "screenHeight": 896, + "screenWidth": 1024, "statusAddressBar": { "height": 70, "width": 1024, @@ -288,15 +354,21 @@ exports[`getIosStatusAddressToolBarOffsets > should get the correct status, addr exports[`getIosStatusAddressToolBarOffsets > should get the correct status, address and toolbar height for the iPad Pro 12.9 2017 hack in landscape mode 1`] = ` { - "safeArea": 0, - "screenHeight": 1024, - "screenWidth": 1366, - "sideBar": { + "leftSidePadding": { + "height": 0, + "width": 0, + "x": 0, + "y": 0, + }, + "rightSidePadding": { "height": 954, "width": 320, "x": 0, "y": 70, }, + "safeArea": 0, + "screenHeight": 1024, + "screenWidth": 1366, "statusAddressBar": { "height": 70, "width": 1366, diff --git a/packages/webdriver-image-comparison/src/clientSideScripts/checkMetaTag.test.ts b/packages/webdriver-image-comparison/src/clientSideScripts/checkMetaTag.test.ts new file mode 100644 index 000000000..d03eb08cc --- /dev/null +++ b/packages/webdriver-image-comparison/src/clientSideScripts/checkMetaTag.test.ts @@ -0,0 +1,33 @@ +// @vitest-environment jsdom + +import { describe, it, expect, beforeEach } from 'vitest' +import { checkMetaTag } from './checkMetaTag.js' + +describe('checkMetaTag', () => { + beforeEach(() => { + document.head.innerHTML = '' + }) + + it('should add a viewport meta tag when not present', () => { + expect(document.querySelector('meta[name="viewport"]')).toBeNull() + + checkMetaTag() + + const meta = document.querySelector('meta[name="viewport"]') as HTMLMetaElement + expect(meta).not.toBeNull() + expect(meta?.content).toBe('width=device-width, initial-scale=1') + }) + + it('should not add a viewport meta tag if one already exists', () => { + const existing = document.createElement('meta') + existing.name = 'viewport' + existing.content = 'custom' + document.head.appendChild(existing) + + checkMetaTag() + + const metas = Array.from(document.querySelectorAll('meta[name="viewport"]')) as HTMLMetaElement[] + expect(metas.length).toBe(1) + expect(metas[0].content).toBe('custom') + }) +}) diff --git a/packages/webdriver-image-comparison/src/clientSideScripts/checkMetaTag.ts b/packages/webdriver-image-comparison/src/clientSideScripts/checkMetaTag.ts new file mode 100644 index 000000000..ca64c0151 --- /dev/null +++ b/packages/webdriver-image-comparison/src/clientSideScripts/checkMetaTag.ts @@ -0,0 +1,9 @@ +export function checkMetaTag() { + const meta = document.querySelector("meta[name='viewport']") + if (!meta) { + const newMeta = document.createElement('meta') + newMeta.name = 'viewport' + newMeta.content = 'width=device-width, initial-scale=1' + document.head.appendChild(newMeta) + } +} diff --git a/packages/webdriver-image-comparison/src/clientSideScripts/getAndroidStatusAddressToolBarHeight.test.ts b/packages/webdriver-image-comparison/src/clientSideScripts/getAndroidStatusAddressToolBarHeight.test.ts deleted file mode 100644 index 027b3783a..000000000 --- a/packages/webdriver-image-comparison/src/clientSideScripts/getAndroidStatusAddressToolBarHeight.test.ts +++ /dev/null @@ -1,60 +0,0 @@ -// @vitest-environment jsdom - -import { describe, it, expect } from 'vitest' -import { ANDROID_OFFSETS } from '../helpers/constants.js' -import { ANDROID_DEVICES, NAVIGATOR_APP_VERSIONS, CONFIGURABLE } from '../mocks/mocks.js' -import getAndroidStatusAddressToolBarOffsets from './getAndroidStatusAddressToolBarOffsets.js' - -describe('getAndroidStatusAddressToolBarOffsets', () => { - it('should get the android status, address and toolbar height with only a major version in the navigator', () => { - setEnvironment('ANDROID', 9, 'NEXUS_5X') - - expect(getAndroidStatusAddressToolBarOffsets(ANDROID_OFFSETS, { isHybridApp: false, isLandscape: false })).toMatchSnapshot() - }) - - it('should get the android status, address and toolbar height with major and minor version in the navigator', () => { - setEnvironment('ANDROID', 8, 'NEXUS_5X') - - expect(getAndroidStatusAddressToolBarOffsets(ANDROID_OFFSETS, { isHybridApp: false, isLandscape: false })).toMatchSnapshot() - }) - - it('should get the android status, address and toolbar height with major, minor and patch version in the navigator', () => { - setEnvironment('ANDROID', 7, 'NEXUS_5X') - - expect(getAndroidStatusAddressToolBarOffsets(ANDROID_OFFSETS, { isHybridApp: false, isLandscape: false })).toMatchSnapshot() - }) - - it('should set the default toolbar height when the toolbar height will become negative', () => { - setEnvironment('ANDROID', 7, 'NEXUS_5X_INNER_HEIGHT') - - expect(getAndroidStatusAddressToolBarOffsets(ANDROID_OFFSETS, { isHybridApp: true, isLandscape: false })).toMatchSnapshot() - }) - - it('should set the dimensions properly for a device in landscape mode', () => { - setEnvironment('ANDROID', 7, 'NEXUS_5X') - - expect(getAndroidStatusAddressToolBarOffsets(ANDROID_OFFSETS, { isHybridApp: true, isLandscape: true })).toMatchSnapshot() - }) - - it('should set the dimensions properly for a table in landscape mode', () => { - setEnvironment('ANDROID', 7, 'TABLET_WIDTH') - - expect(getAndroidStatusAddressToolBarOffsets(ANDROID_OFFSETS, { isHybridApp: true, isLandscape: true })).toMatchSnapshot() - }) -}) - -/** - * Set the environment for the test - */ -function setEnvironment(os: string, version: number, deviceType: string) { - // @ts-ignore - Object.defineProperty(navigator, 'appVersion', { value: NAVIGATOR_APP_VERSIONS[os][version], ...CONFIGURABLE }) - // @ts-ignore - Object.defineProperty(window.screen, 'width', { value: ANDROID_DEVICES[deviceType].width, ...CONFIGURABLE }) - // @ts-ignore - Object.defineProperty(window.screen, 'height', { value: ANDROID_DEVICES[deviceType].height, ...CONFIGURABLE }) - // @ts-ignore - Object.defineProperty(window, 'innerWidth', { value: ANDROID_DEVICES[deviceType].innerWidth, ...CONFIGURABLE }) - // @ts-ignore - Object.defineProperty(window, 'innerHeight', { value: ANDROID_DEVICES[deviceType].innerHeight, ...CONFIGURABLE }) -} diff --git a/packages/webdriver-image-comparison/src/clientSideScripts/getAndroidStatusAddressToolBarOffsets.ts b/packages/webdriver-image-comparison/src/clientSideScripts/getAndroidStatusAddressToolBarOffsets.ts deleted file mode 100644 index 126e81f05..000000000 --- a/packages/webdriver-image-comparison/src/clientSideScripts/getAndroidStatusAddressToolBarOffsets.ts +++ /dev/null @@ -1,48 +0,0 @@ -import type { StatusAddressToolBarOffsets } from './statusAddressToolBarOffsets.interfaces.js' -import type { AndroidOffsets } from '../helpers/constants.interfaces.js' - -/** - * Get the current height of the Android status and address bar - */ -export default function getAndroidStatusAddressToolBarOffsets( - androidOffsets: AndroidOffsets, - { isHybridApp, isLandscape }: { isHybridApp: boolean; isLandscape: boolean }, -): StatusAddressToolBarOffsets { - // Determine version for the right offsets - const { height, width } = window.screen - const { innerHeight } = window - const match = navigator.appVersion.match(/Android (\d+).?(\d+)?.?(\d+)?/)! - const majorVersion = parseInt(match[1], 10) - const versionOffsets = androidOffsets[majorVersion] - // Not sure if it's a bug, but in Landscape mode the height is the width - const deviceHeight = isLandscape && width > height ? width : height - const deviceWidth = isLandscape && height > width ? height : width - const statusAddressBarHeight = versionOffsets.STATUS_BAR + (isHybridApp ? 0 : versionOffsets.ADDRESS_BAR) - let toolBarHeight = height - innerHeight - statusAddressBarHeight - - if (toolBarHeight < 0) { - toolBarHeight = versionOffsets.TOOL_BAR - } - - // Determine status, address and tool bar height - return { - // For now Android doesn't have a safe area - safeArea: 0, - screenHeight: deviceHeight, - screenWidth: deviceWidth, - statusAddressBar: { - height: statusAddressBarHeight, - width, - x: 0, - y: 0, - }, - // For now Android doesn't have a side bar - sideBar: { height: 0, width: 0, x: 0, y: 0 }, - toolBar: { - height: toolBarHeight, - width, - x: 0, - y: height - toolBarHeight, - }, - } -} diff --git a/packages/webdriver-image-comparison/src/clientSideScripts/getBoundingClientRect.test.ts b/packages/webdriver-image-comparison/src/clientSideScripts/getBoundingClientRect.test.ts new file mode 100644 index 000000000..324e73d83 --- /dev/null +++ b/packages/webdriver-image-comparison/src/clientSideScripts/getBoundingClientRect.test.ts @@ -0,0 +1,32 @@ +// @vitest-environment jsdom + +import { describe, it, expect } from 'vitest' +import { getBoundingClientRect } from './getBoundingClientRect.js' + +describe('getBoundingClientRect', () => { + it('should return rounded values of the bounding client rect', () => { + const element = document.createElement('div') + Object.defineProperty(element, 'getBoundingClientRect', { + value: () => ({ + x: 10.7, + y: 20.4, + width: 100.9, + height: 200.3, + top: 20.4, + left: 10.7, + bottom: 220.7, + right: 110.9, + toJSON: () => {}, + }), + }) + + const result = getBoundingClientRect(element) + + expect(result).toEqual({ + x: 11, + y: 20, + width: 101, + height: 200, + }) + }) +}) diff --git a/packages/webdriver-image-comparison/src/clientSideScripts/getBoundingClientRect.ts b/packages/webdriver-image-comparison/src/clientSideScripts/getBoundingClientRect.ts new file mode 100644 index 000000000..cbfe218cc --- /dev/null +++ b/packages/webdriver-image-comparison/src/clientSideScripts/getBoundingClientRect.ts @@ -0,0 +1,17 @@ +import type { ElementPosition } from './elementPosition.interfaces.js' + +/** + * Get the element position relative to the viewport + */ +export function getBoundingClientRect( + element: HTMLElement, +): ElementPosition { + const { height, width, x, y } = element.getBoundingClientRect() + + return { + height: Math.round(height), + width: Math.round(width), + x: Math.round(x), + y: Math.round(y), + } +} diff --git a/packages/webdriver-image-comparison/src/clientSideScripts/getElementPositionTopWindow.test.ts b/packages/webdriver-image-comparison/src/clientSideScripts/getElementPositionTopWindow.test.ts deleted file mode 100644 index 63f655b11..000000000 --- a/packages/webdriver-image-comparison/src/clientSideScripts/getElementPositionTopWindow.test.ts +++ /dev/null @@ -1,22 +0,0 @@ -// @vitest-environment jsdom - -import { describe, it, expect, vi } from 'vitest' -import getElementPositionTopWindow from './getElementPositionTopWindow.js' - -describe('getElementPositionTopWindow', () => { - it('should the the element position to the top of the window', () => { - Element.prototype.getBoundingClientRect = vi.fn(() => { - return { - width: 120, - height: 120, - top: 10, - left: 100, - bottom: 5, - right: 12, - } - }) as any - document.body.innerHTML = '
' + ' Hello' + '
' - - expect(getElementPositionTopWindow(document.querySelector('#username')!)).toMatchSnapshot() - }) -}) diff --git a/packages/webdriver-image-comparison/src/clientSideScripts/getElementPositionTopWindow.ts b/packages/webdriver-image-comparison/src/clientSideScripts/getElementPositionTopWindow.ts deleted file mode 100644 index 9a8194334..000000000 --- a/packages/webdriver-image-comparison/src/clientSideScripts/getElementPositionTopWindow.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { ElementPosition } from './elementPosition.interfaces.js' - -/** - * Get the position of the element to the top of the window - */ -export default function getElementPositionTopWindow(element: HTMLElement): ElementPosition { - const rectangles = element.getBoundingClientRect() - - return { - height: Math.round(rectangles.height), - width: Math.round(rectangles.width), - x: Math.round(rectangles.left), - y: Math.round(rectangles.top), - } -} diff --git a/packages/webdriver-image-comparison/src/clientSideScripts/getIosStatusAddressToolBarOffsets.test.ts b/packages/webdriver-image-comparison/src/clientSideScripts/getIosStatusAddressToolBarOffsets.test.ts deleted file mode 100644 index 1f71dcfbf..000000000 --- a/packages/webdriver-image-comparison/src/clientSideScripts/getIosStatusAddressToolBarOffsets.test.ts +++ /dev/null @@ -1,102 +0,0 @@ -// @vitest-environment jsdom - -import { describe, it, expect } from 'vitest' -import { IOS_DEVICES, NAVIGATOR_APP_VERSIONS, CONFIGURABLE } from '../mocks/mocks.js' -import getIosStatusAddressToolBarOffsets from './getIosStatusAddressToolBarOffsets.js' -import { IOS_OFFSETS } from '../helpers/constants.js' - -describe('getIosStatusAddressToolBarOffsets', () => { - it('should get the correct status, address and toolbar height for a default iPhone with iOS 14 in Portrait mode', () => { - setEnvironment('IOS', 14, 'IPHONE') - - expect(getIosStatusAddressToolBarOffsets(IOS_OFFSETS, false)).toMatchSnapshot() - }) - - it('should get the correct status, address and toolbar height for a default iPhone with iOS 14 in landscape mode', () => { - setEnvironment('IOS', 14, 'IPHONE') - - expect(getIosStatusAddressToolBarOffsets(IOS_OFFSETS, true)).toMatchSnapshot() - }) - - it('should get the correct status, address and toolbar height for a default iPhone with iOS 15 in portrait mode', () => { - setEnvironment('IOS', 15, 'IPHONE_X') - - expect(getIosStatusAddressToolBarOffsets(IOS_OFFSETS, false)).toMatchSnapshot() - }) - - it('should get the correct status, address and toolbar height for a default iPhone with iOS 15 in landscape mode', () => { - setEnvironment('IOS', 15, 'IPHONE_X') - - expect(getIosStatusAddressToolBarOffsets(IOS_OFFSETS, true)).toMatchSnapshot() - }) - - it('should get the correct status bar height for an iPhone 11 with iOS 13 to validate the iPhone 11 hack', () => { - setEnvironment('IOS', 13, 'IPHONE_11') - - expect(getIosStatusAddressToolBarOffsets(IOS_OFFSETS, false)).toMatchSnapshot() - }) - - it('should get the correct status bar height for an iPhone 11 with iOS 13 to validate the iPhone 11 hack in landscape mode', () => { - setEnvironment('IOS', 13, 'IPHONE_11') - - expect(getIosStatusAddressToolBarOffsets(IOS_OFFSETS, true)).toMatchSnapshot() - }) - - it('should get the correct status bar height for an iPhone 11 with iOS 15', () => { - setEnvironment('IOS', 15, 'IPHONE_11') - - expect(getIosStatusAddressToolBarOffsets(IOS_OFFSETS, false)).toMatchSnapshot() - }) - - it('should get the correct status, address and toolbar height for an iPhone with extreme not known dimensions', () => { - setEnvironment('IOS', 15, 'IPHONE_HEIGHT') - - expect(getIosStatusAddressToolBarOffsets(IOS_OFFSETS, false)).toMatchSnapshot() - }) - - it('should get the correct status, address and toolbar height for an iPad', () => { - setEnvironment('IOS', 15, 'IPAD') - - expect(getIosStatusAddressToolBarOffsets(IOS_OFFSETS, false)).toMatchSnapshot() - }) - - it('should get the correct status, address and toolbar height for an iPad with big sizes', () => { - setEnvironment('IOS', 15, 'IPAD_BIG_SIZE') - - expect(getIosStatusAddressToolBarOffsets(IOS_OFFSETS, false)).toMatchSnapshot() - }) - - it('should get the correct status, address and toolbar height for an iPad in landscape mode', () => { - setEnvironment('IOS', 15, 'IPAD_LANDSCAPE') - - expect(getIosStatusAddressToolBarOffsets(IOS_OFFSETS, true)).toMatchSnapshot() - }) - - it('should get the correct status, address and toolbar height for the iPad Pro 12.9 2017 hack in landscape mode', () => { - setEnvironment('IOS', 15, 'IPAD_PRO_LANDSCAPE') - - expect(getIosStatusAddressToolBarOffsets(IOS_OFFSETS, true)).toMatchSnapshot() - }) -}) - -/** - * Set the environment for the test - */ -function setEnvironment(os: string, version: number, deviceType: string) { - // @ts-ignore - Object.defineProperty(navigator, 'appVersion', { value: NAVIGATOR_APP_VERSIONS[os][version], ...CONFIGURABLE }) - // @ts-ignore - Object.defineProperty(window.screen, 'width', { value: IOS_DEVICES[deviceType].width, ...CONFIGURABLE }) - // @ts-ignore - Object.defineProperty(global.document, 'documentElement', { - // @ts-ignore - value: { scrollWidth: IOS_DEVICES[deviceType].scrollWidth }, - ...CONFIGURABLE, - }) - // @ts-ignore - Object.defineProperty(window.screen, 'height', { value: IOS_DEVICES[deviceType].height, ...CONFIGURABLE }) - // @ts-ignore - Object.defineProperty(window, 'innerWidth', { value: IOS_DEVICES[deviceType].innerWidth, ...CONFIGURABLE }) - // @ts-ignore - Object.defineProperty(window, 'innerHeight', { value: IOS_DEVICES[deviceType].innerHeight, ...CONFIGURABLE }) -} diff --git a/packages/webdriver-image-comparison/src/clientSideScripts/getIosStatusAddressToolBarOffsets.ts b/packages/webdriver-image-comparison/src/clientSideScripts/getIosStatusAddressToolBarOffsets.ts deleted file mode 100644 index 2a3313147..000000000 --- a/packages/webdriver-image-comparison/src/clientSideScripts/getIosStatusAddressToolBarOffsets.ts +++ /dev/null @@ -1,103 +0,0 @@ -import type { StatusAddressToolBarOffsets } from './statusAddressToolBarOffsets.interfaces.js' -import type { IosOffsets } from '../helpers/constants.interfaces.js' - -/** - * Get the current height of the iOS status and address bar - */ -export default function getIosStatusAddressToolBarOffsets( - iosOffsets: IosOffsets, - isLandscape: boolean, -): StatusAddressToolBarOffsets { - // 1. Determine screen width/height to determine the current iPhone/iPad offset data - // For data on the screen sizes check the constants.ts-file - const { width, height } = window.screen - const { innerHeight } = window - const isIphone = width < 1024 && height < 1024 - const deviceType = isIphone ? 'IPHONE' : 'IPAD' - const orientationType = isLandscape ? 'LANDSCAPE' : 'PORTRAIT' - const defaultPortraitHeight = isIphone ? 667 : 1024 - const portraitHeight = width > height ? width : height - // Not sure if it's a bug, but in Landscape mode the height is the width - const deviceHeight = isLandscape ? width : height - const deviceWidth = isLandscape ? height : width - const offsetPortraitHeight = - Object.keys(iosOffsets[deviceType]).indexOf(portraitHeight.toString()) > -1 ? portraitHeight : defaultPortraitHeight - const currentOffsets = iosOffsets[deviceType][offsetPortraitHeight][orientationType] - const osVersion = parseInt(navigator.appVersion.match(/(?:OS |Version\/)(\d+)(?:_|\.)(\d+)(?:_|\.)?(\d+)?/)![1], 10) - - // 2. Get the statusbar height - let statusBarHeight = currentOffsets.STATUS_BAR - let isIpadPro129FirstGeneration = false - let safeArea = currentOffsets.SAFE_AREA - /** - * Dirty little hack, but the status bar of the iPad Pro (12.9 inch) (1st generation) - * has a status bar of 20px I wanted the data to be as generic as possible, so I added this hack - */ - if ( - (deviceHeight === 1366 || deviceWidth === 1366) && - deviceType === 'IPAD' && - innerHeight + currentOffsets.ADDRESS_BAR + currentOffsets.STATUS_BAR > deviceHeight - ) { - statusBarHeight = 20 - isIpadPro129FirstGeneration = true - } - /** - * Dirty little hack for iPhone XSMax|XR|11|11ProMax for iOS 13. The status bar is 44 when in - * portrait mode and the safe area is 44 when in landscape mode - */ - if ((deviceHeight === 896 || deviceWidth === 896) && deviceType === 'IPHONE' && osVersion === 13) { - if (isLandscape) { - safeArea = 44 - } else { - statusBarHeight = 44 - } - } - // 3. Determine the address bar height - // Since iOS 15 the address bar for iPhones is at the bottom by default - // This is also what we assume because we can't determine it from the - // web context - const addressBarOnTop = (!isLandscape && isIphone && osVersion < 15) || isLandscape || !isIphone - const statusAddressBarHeight = statusBarHeight + (addressBarOnTop ? currentOffsets.ADDRESS_BAR : 0) - // 4. Determine the toolbar offsets - // In Landscape mode the toolbar is hidden by default and we need to add - // home bar data - const toolBarHeight = height - innerHeight - statusAddressBarHeight - const toolBarOffsets = - isLandscape || toolBarHeight <= 0 - ? // The iPad Pro (12.9 inch) (1st generation) doesn't have a home bar - isIpadPro129FirstGeneration - ? { height: 0, width: 0, x: 0, y: 0 } - : currentOffsets.HOME_BAR - : { - height: toolBarHeight, - width: deviceWidth, - x: 0, - y: height - toolBarHeight, - } - // 5. Return the sidebar offsets - const sideBarOffsets = - isLandscape && !isIphone - ? { - height: deviceHeight - statusAddressBarHeight, - width: deviceWidth - document.documentElement.scrollWidth, - x: 0, - y: statusAddressBarHeight, - } - : { height: 0, width: 0, x: 0, y: 0 } - - // 6. Return the offsets - return { - safeArea, - screenHeight: deviceHeight, - screenWidth: deviceWidth, - // We only have a side bar with iPads and in landscape mode - sideBar: sideBarOffsets, - statusAddressBar: { - height: statusAddressBarHeight, - width: deviceWidth, - x: 0, - y: 0, - }, - toolBar: toolBarOffsets, - } -} diff --git a/packages/webdriver-image-comparison/src/clientSideScripts/getMobileWebviewClickAndDimensions.test.ts b/packages/webdriver-image-comparison/src/clientSideScripts/getMobileWebviewClickAndDimensions.test.ts new file mode 100644 index 000000000..a0e62474e --- /dev/null +++ b/packages/webdriver-image-comparison/src/clientSideScripts/getMobileWebviewClickAndDimensions.test.ts @@ -0,0 +1,58 @@ +import { describe, it, expect, beforeEach } from 'vitest' +import { JSDOM } from 'jsdom' +import { getMobileWebviewClickAndDimensions } from './getMobileWebviewClickAndDimensions.js' + +describe('getMobileWebviewClickAndDimensions', () => { + const selector = '[data-test="ics-overlay"]' + + beforeEach(() => { + const dom = new JSDOM('') + global.document = dom.window.document + global.window = dom.window as any + }) + + it('should return default values when overlay is not found', () => { + expect(getMobileWebviewClickAndDimensions(selector)).toEqual({ + x: 0, + y: 0, + width: 0, + height: 0, + }) + }) + + it('should return default values when data attribute is missing', () => { + const el = document.createElement('div') + el.setAttribute('data-test', 'ics-overlay') + document.body.appendChild(el) + + expect(getMobileWebviewClickAndDimensions(selector)).toEqual({ + x: 0, + y: 0, + width: 0, + height: 0, + }) + }) + + it('should return parsed data from data attribute and remove element', () => { + const el = document.createElement('div') + el.setAttribute('data-test', 'ics-overlay') + el.dataset.icsWebviewData = JSON.stringify({ x: 10, y: 20, width: 100, height: 200 }) + document.body.appendChild(el) + + const result = getMobileWebviewClickAndDimensions(selector) + + expect(result).toEqual({ x: 10, y: 20, width: 100, height: 200 }) + expect(document.querySelector(selector)).toBeNull() + }) + + it('should return default values if JSON parsing fails', () => { + const el = document.createElement('div') + el.setAttribute('data-test', 'ics-overlay') + el.dataset.icsWebviewData = '{not:valid:json' + document.body.appendChild(el) + + const result = getMobileWebviewClickAndDimensions(selector) + + expect(result).toEqual({ x: 0, y: 0, width: 0, height: 0 }) + }) +}) diff --git a/packages/webdriver-image-comparison/src/clientSideScripts/getMobileWebviewClickAndDimensions.ts b/packages/webdriver-image-comparison/src/clientSideScripts/getMobileWebviewClickAndDimensions.ts new file mode 100644 index 000000000..5f5edef01 --- /dev/null +++ b/packages/webdriver-image-comparison/src/clientSideScripts/getMobileWebviewClickAndDimensions.ts @@ -0,0 +1,21 @@ +import type { RectanglesOutput } from '../methods/rectangles.interfaces.js' + +/** + * Get the click and dimensions of the mobile webview and remove the overlay + */ +export function getMobileWebviewClickAndDimensions(overlaySelector: string):RectanglesOutput { + const overlay = document.querySelector(overlaySelector) as HTMLElement | null + const defaultValue = { y: 0, x: 0, width: 0, height: 0 } + + if (!overlay || !overlay.dataset.icsWebviewData) { + return defaultValue + } + + overlay.remove() + + try { + return JSON.parse(overlay.dataset.icsWebviewData) + } catch { + return defaultValue + } +} diff --git a/packages/webdriver-image-comparison/src/clientSideScripts/injectWebviewOverlay.test.ts b/packages/webdriver-image-comparison/src/clientSideScripts/injectWebviewOverlay.test.ts new file mode 100644 index 000000000..64eb09891 --- /dev/null +++ b/packages/webdriver-image-comparison/src/clientSideScripts/injectWebviewOverlay.test.ts @@ -0,0 +1,94 @@ +import { describe, it, expect, beforeEach } from 'vitest' +import { JSDOM } from 'jsdom' +import { injectWebviewOverlay } from './injectWebviewOverlay.js' + +describe('injectWebviewOverlay', () => { + beforeEach(() => { + const dom = new JSDOM('', { + pretendToBeVisual: true, + runScripts: 'dangerously', + }) + + global.window = dom.window as unknown as Window & typeof globalThis + global.document = dom.window.document + + // Mock required layout properties + Object.defineProperty(document.documentElement, 'clientHeight', { + value: 800, + configurable: true, + }) + + Object.defineProperty(window, 'innerWidth', { + value: 400, + configurable: true, + }) + + Object.defineProperty(window, 'devicePixelRatio', { + value: 2, + configurable: true, + }) + }) + + it('should inject an overlay if not already present', () => { + expect(document.querySelector('[data-test="ics-overlay"]')).toBeNull() + + injectWebviewOverlay(true) + + const overlay = document.querySelector('[data-test="ics-overlay"]') as HTMLElement + expect(overlay).toBeTruthy() + expect(overlay?.tagName).toBe('DIV') + expect(overlay?.style.position).toBe('fixed') + expect(overlay?.style.height).toContain('800px') + }) + + it('should not inject a second overlay if one already exists', () => { + injectWebviewOverlay(true) + injectWebviewOverlay(true) + + const overlays = document.querySelectorAll('[data-test="ics-overlay"]') + expect(overlays.length).toBe(1) + }) + + it('should store click position and dimensions in dataset on click (Android DPR)', () => { + injectWebviewOverlay(true) + + const overlay = document.querySelector('[data-test="ics-overlay"]') as HTMLDivElement + + // Simulate click at position (50, 100) + const event = new window.MouseEvent('click', { + clientX: 50, + clientY: 100, + bubbles: true, + }) + overlay.dispatchEvent(event) + + const parsedData = JSON.parse(overlay.dataset.icsWebviewData!) + expect(parsedData).toEqual({ + x: 100, + y: 200, + width: 800, + height: 1600, + }) + }) + + it('should use DPR = 1 for iOS (isAndroid = false)', () => { + injectWebviewOverlay(false) + + const overlay = document.querySelector('[data-test="ics-overlay"]') as HTMLDivElement + + const event = new window.MouseEvent('click', { + clientX: 50, + clientY: 100, + bubbles: true, + }) + overlay.dispatchEvent(event) + + const parsedData = JSON.parse(overlay.dataset.icsWebviewData!) + expect(parsedData).toEqual({ + x: 50, + y: 100, + width: 400, + height: 800, + }) + }) +}) diff --git a/packages/webdriver-image-comparison/src/clientSideScripts/injectWebviewOverlay.ts b/packages/webdriver-image-comparison/src/clientSideScripts/injectWebviewOverlay.ts new file mode 100644 index 000000000..92e64a2ef --- /dev/null +++ b/packages/webdriver-image-comparison/src/clientSideScripts/injectWebviewOverlay.ts @@ -0,0 +1,38 @@ +/** + * Inject an overlay on top of the webview with an event listener that stores the click position in the webview + */ +export function injectWebviewOverlay(isAndroid: boolean): void { + if (document.querySelector('[data-test="ics-overlay"]')) { return } + + const overlay = document.createElement('div') + const dpr = isAndroid ? window.devicePixelRatio : 1 + overlay.style.cssText = ` + position: fixed; top: 0; left: 0; width: 100vw; + height: ${document.documentElement.clientHeight}px; + background: rgba(255, 165, 0, 0.5); z-index: 2147483647; + display: flex; flex-direction: column; align-items: center; justify-content: center; + color: black; font-size: 36px; font-family: Arial, sans-serif; text-align: center;` + overlay.dataset.test = 'ics-overlay' + + const textContainer = document.createElement('div') + textContainer.innerText = 'This overlay is used to determine the position of the webview.' + overlay.appendChild(textContainer) + + overlay.onclick = (event) => { + const { clientX: x, clientY: y } = event + const data = { + x: Math.round(x * dpr), + y: Math.round(y * dpr), + width: Math.round(window.innerWidth * dpr), + height: Math.round(document.documentElement.clientHeight * dpr), + } + + overlay.dataset.icsWebviewData = JSON.stringify(data) + textContainer.innerHTML = ` + This overlay is used to determine the position of the webview.
+ Clicked at: X: ${data.x}, Y: ${data.y}
+ Dimensions: Viewport width: ${data.width}, Viewport height: ${data.height}` + } + + document.body.appendChild(overlay) +} diff --git a/packages/webdriver-image-comparison/src/clientSideScripts/statusAddressToolBarOffsets.interfaces.ts b/packages/webdriver-image-comparison/src/clientSideScripts/statusAddressToolBarOffsets.interfaces.ts index 73f7893bb..f9e052532 100644 --- a/packages/webdriver-image-comparison/src/clientSideScripts/statusAddressToolBarOffsets.interfaces.ts +++ b/packages/webdriver-image-comparison/src/clientSideScripts/statusAddressToolBarOffsets.interfaces.ts @@ -1,10 +1,11 @@ import type { RectanglesOutput } from '../methods/rectangles.interfaces.js' export interface StatusAddressToolBarOffsets { + leftSidePadding: RectanglesOutput; + rightSidePadding: RectanglesOutput; safeArea: number; screenHeight: number; screenWidth: number; - sideBar: RectanglesOutput; statusAddressBar: RectanglesOutput; toolBar: RectanglesOutput; } diff --git a/packages/webdriver-image-comparison/src/commands/checkAppElement.ts b/packages/webdriver-image-comparison/src/commands/checkAppElement.ts index a3841afbe..b39db5943 100644 --- a/packages/webdriver-image-comparison/src/commands/checkAppElement.ts +++ b/packages/webdriver-image-comparison/src/commands/checkAppElement.ts @@ -1,3 +1,4 @@ +import { checkIsAndroid } from '../helpers/utils.js' import { methodCompareOptions } from '../helpers/options.js' import type { ImageCompareResult } from '../methods/images.interfaces.js' import { executeImageCompare } from '../methods/images.js' @@ -22,10 +23,9 @@ export default async function checkAppElement( ): Promise { // 1. Set some vars const { isMobile } = instanceData - const { executor } = methods // 2. Save the element and return the data - const { devicePixelRatio, fileName, isLandscape } = await saveAppElement({ + const { devicePixelRatio, fileName } = await saveAppElement({ methods, instanceData, folders, @@ -40,7 +40,6 @@ export default async function checkAppElement( // 3a. Determine the options const compareOptions = methodCompareOptions(checkElementOptions.method) const executeCompareOptions = { - devicePixelRatio, compareOptions: { wic: { ...checkElementOptions.wic.compareOptions, @@ -51,6 +50,8 @@ export default async function checkAppElement( }, method: compareOptions, }, + devicePixelRatio, + deviceRectangles: instanceData.deviceRectangles, fileName, folderOptions: { autoSaveBaseline: checkElementOptions.wic.autoSaveBaseline, @@ -62,15 +63,14 @@ export default async function checkAppElement( isMobile, savePerInstance: checkElementOptions.wic.savePerInstance, }, + isAndroid: checkIsAndroid(instanceData.platformName), isAndroidNativeWebScreenshot: instanceData.nativeWebScreenshot, isHybridApp: checkElementOptions.wic.isHybridApp, - isLandscape, platformName: instanceData.platformName, } // 3b Now execute the compare and return the data return executeImageCompare({ - executor, options: executeCompareOptions, testContext, isViewPortScreenshot: false, diff --git a/packages/webdriver-image-comparison/src/commands/checkAppScreen.ts b/packages/webdriver-image-comparison/src/commands/checkAppScreen.ts index 8b277eead..fa0897fee 100644 --- a/packages/webdriver-image-comparison/src/commands/checkAppScreen.ts +++ b/packages/webdriver-image-comparison/src/commands/checkAppScreen.ts @@ -1,4 +1,4 @@ -import type { RectanglesOutput } from 'src/methods/rectangles.interfaces.js' +import type { RectanglesOutput } from '../methods/rectangles.interfaces.js' import { screenMethodCompareOptions } from '../helpers/options.js' import type { ImageCompareOptions, ImageCompareResult } from '../methods/images.interfaces.js' import { executeImageCompare } from '../methods/images.js' @@ -41,15 +41,11 @@ export default async function checkAppScreen( ] } - const { executor, getElementRect } = methods + const { getElementRect } = methods const { isAndroid, isMobile } = instanceData // 2. Take the actual screenshot and retrieve the needed data - const { - devicePixelRatio, - fileName, - isLandscape, - } = await saveAppScreen({ + const { devicePixelRatio, fileName } = await saveAppScreen({ methods, instanceData, folders, @@ -70,12 +66,12 @@ export default async function checkAppScreen( const methodCompareOptions = screenMethodCompareOptions(checkScreenOptions.method) const executeCompareOptions: ImageCompareOptions = { - ignoreRegions: [...ignoreRegions, ...deviceIgnoreRegions], compareOptions: { wic: checkScreenOptions.wic.compareOptions, method: methodCompareOptions, }, devicePixelRatio, + deviceRectangles: instanceData.deviceRectangles, fileName, folderOptions: { autoSaveBaseline: checkScreenOptions.wic.autoSaveBaseline, @@ -87,16 +83,13 @@ export default async function checkAppScreen( isMobile, savePerInstance: checkScreenOptions.wic.savePerInstance, }, - isAndroidNativeWebScreenshot: instanceData.nativeWebScreenshot, - isHybridApp: false, + ignoreRegions: [...ignoreRegions, ...deviceIgnoreRegions], isAndroid, - isLandscape, - platformName: instanceData.platformName, + isAndroidNativeWebScreenshot: instanceData.nativeWebScreenshot, } // 4b Now execute the compare and return the data return executeImageCompare({ - executor, isViewPortScreenshot: true, isNativeContext, options: executeCompareOptions, diff --git a/packages/webdriver-image-comparison/src/commands/checkFullPageScreen.ts b/packages/webdriver-image-comparison/src/commands/checkFullPageScreen.ts index d99b8968b..b27d63820 100644 --- a/packages/webdriver-image-comparison/src/commands/checkFullPageScreen.ts +++ b/packages/webdriver-image-comparison/src/commands/checkFullPageScreen.ts @@ -1,5 +1,5 @@ import { executeImageCompare } from '../methods/images.js' -import { checkIsMobile } from '../helpers/utils.js' +import { checkIsAndroid, checkIsMobile } from '../helpers/utils.js' import saveFullPageScreen from './saveFullPageScreen.js' import type { ImageCompareResult } from '../methods/images.interfaces.js' import type { SaveFullPageOptions } from './fullPage.interfaces.js' @@ -40,7 +40,7 @@ export default async function checkFullPageScreen( waitForFontsLoaded: checkFullPageOptions.method.waitForFontsLoaded, }, } - const { devicePixelRatio, fileName, isLandscape } = await saveFullPageScreen({ + const { devicePixelRatio, fileName } = await saveFullPageScreen({ methods, instanceData, folders, @@ -52,11 +52,12 @@ export default async function checkFullPageScreen( // 2a. Determine the options const compareOptions = methodCompareOptions(checkFullPageOptions.method) const executeCompareOptions = { - devicePixelRatio, compareOptions: { wic: checkFullPageOptions.wic.compareOptions, method: compareOptions, }, + devicePixelRatio, + deviceRectangles: instanceData.deviceRectangles, fileName, folderOptions: { autoSaveBaseline: checkFullPageOptions.wic.autoSaveBaseline, @@ -68,15 +69,14 @@ export default async function checkFullPageScreen( isMobile: checkIsMobile(instanceData.platformName), savePerInstance: checkFullPageOptions.wic.savePerInstance, }, + isAndroid: checkIsAndroid(instanceData.platformName), isAndroidNativeWebScreenshot: instanceData.nativeWebScreenshot, isHybridApp: checkFullPageOptions.wic.isHybridApp, - isLandscape, platformName: instanceData.platformName, } // 2b Now execute the compare and return the data return executeImageCompare({ - executor: methods.executor, isViewPortScreenshot: false, isNativeContext, options: executeCompareOptions, diff --git a/packages/webdriver-image-comparison/src/commands/checkWebElement.ts b/packages/webdriver-image-comparison/src/commands/checkWebElement.ts index e8c55d96f..327181cf6 100644 --- a/packages/webdriver-image-comparison/src/commands/checkWebElement.ts +++ b/packages/webdriver-image-comparison/src/commands/checkWebElement.ts @@ -1,5 +1,5 @@ import { executeImageCompare } from '../methods/images.js' -import { checkIsMobile } from '../helpers/utils.js' +import { checkIsAndroid, checkIsMobile } from '../helpers/utils.js' import saveWebElement from './saveWebElement.js' import type { ImageCompareResult } from '../methods/images.interfaces.js' import type { SaveElementOptions } from './element.interfaces.js' @@ -35,7 +35,7 @@ export default async function checkWebElement( waitForFontsLoaded: checkElementOptions.method.waitForFontsLoaded, }, } - const { devicePixelRatio, fileName, isLandscape } = await saveWebElement({ + const { devicePixelRatio, fileName } = await saveWebElement({ methods, instanceData, folders, @@ -47,7 +47,6 @@ export default async function checkWebElement( // 2a. Determine the options const compareOptions = methodCompareOptions(checkElementOptions.method) const executeCompareOptions = { - devicePixelRatio, compareOptions: { wic: { ...checkElementOptions.wic.compareOptions, @@ -58,6 +57,8 @@ export default async function checkWebElement( }, method: compareOptions, }, + devicePixelRatio, + deviceRectangles: instanceData.deviceRectangles, fileName, folderOptions: { autoSaveBaseline: checkElementOptions.wic.autoSaveBaseline, @@ -69,15 +70,13 @@ export default async function checkWebElement( isMobile: checkIsMobile(instanceData.platformName), savePerInstance: checkElementOptions.wic.savePerInstance, }, + isAndroid: checkIsAndroid(instanceData.platformName), isAndroidNativeWebScreenshot: instanceData.nativeWebScreenshot, - isHybridApp: checkElementOptions.wic.isHybridApp, - isLandscape, platformName: instanceData.platformName, } // 2b Now execute the compare and return the data return executeImageCompare({ - executor: methods.executor, isViewPortScreenshot: true, isNativeContext, options: executeCompareOptions, diff --git a/packages/webdriver-image-comparison/src/commands/checkWebScreen.ts b/packages/webdriver-image-comparison/src/commands/checkWebScreen.ts index 1fe146ded..c08b06f82 100644 --- a/packages/webdriver-image-comparison/src/commands/checkWebScreen.ts +++ b/packages/webdriver-image-comparison/src/commands/checkWebScreen.ts @@ -1,6 +1,6 @@ import saveWebScreen from './saveWebScreen.js' import { executeImageCompare } from '../methods/images.js' -import { checkIsMobile } from '../helpers/utils.js' +import { checkIsAndroid, checkIsMobile } from '../helpers/utils.js' import type { ImageCompareOptions, ImageCompareResult } from '../methods/images.interfaces.js' import type { SaveScreenOptions } from './screen.interfaces.js' import { screenMethodCompareOptions } from '../helpers/options.js' @@ -33,7 +33,7 @@ export default async function checkWebScreen( waitForFontsLoaded: checkScreenOptions.method.waitForFontsLoaded, }, } - const { devicePixelRatio, fileName, isLandscape } = await saveWebScreen({ + const { devicePixelRatio, fileName } = await saveWebScreen({ methods, instanceData, folders, @@ -45,11 +45,12 @@ export default async function checkWebScreen( // 2a. Determine the compare options const methodCompareOptions = screenMethodCompareOptions(checkScreenOptions.method) const executeCompareOptions: ImageCompareOptions = { - devicePixelRatio, compareOptions: { wic: checkScreenOptions.wic.compareOptions, method: methodCompareOptions, }, + devicePixelRatio, + deviceRectangles: instanceData.deviceRectangles, fileName, folderOptions: { autoSaveBaseline: checkScreenOptions.wic.autoSaveBaseline, @@ -61,15 +62,12 @@ export default async function checkWebScreen( isMobile: checkIsMobile(instanceData.platformName), savePerInstance: checkScreenOptions.wic.savePerInstance, }, + isAndroid: checkIsAndroid(instanceData.platformName), isAndroidNativeWebScreenshot: instanceData.nativeWebScreenshot, - isHybridApp: checkScreenOptions.wic.isHybridApp, - isLandscape, - platformName: instanceData.platformName, } // 2b Now execute the compare and return the data return executeImageCompare({ - executor: methods.executor, isViewPortScreenshot: true, isNativeContext, options: executeCompareOptions, diff --git a/packages/webdriver-image-comparison/src/commands/fullPage.interfaces.ts b/packages/webdriver-image-comparison/src/commands/fullPage.interfaces.ts index 096d4fc02..02e6729e5 100644 --- a/packages/webdriver-image-comparison/src/commands/fullPage.interfaces.ts +++ b/packages/webdriver-image-comparison/src/commands/fullPage.interfaces.ts @@ -11,6 +11,8 @@ export interface SaveFullPageOptions { export interface SaveFullPageMethodOptions extends Partial { // The padding that needs to be added to the address bar on iOS and Android addressBarShadowPadding?: number; + // Create fullpage screenshots with the bidi protocol + userBasedFullPageScreenshot?: boolean; // Disable the blinking cursor disableBlinkingCursor?: boolean; // Disable all css animations diff --git a/packages/webdriver-image-comparison/src/commands/saveAppElement.ts b/packages/webdriver-image-comparison/src/commands/saveAppElement.ts index e0156dd6b..908f76218 100644 --- a/packages/webdriver-image-comparison/src/commands/saveAppElement.ts +++ b/packages/webdriver-image-comparison/src/commands/saveAppElement.ts @@ -33,7 +33,7 @@ export default async function saveAppElement( browserVersion, deviceName, devicePixelRatio, - deviceScreenSize, + deviceRectangles:{ screenSize }, isIOS, isMobile, logName, @@ -75,8 +75,8 @@ export default async function saveAppElement( name: '', platformName, platformVersion, - screenHeight: deviceScreenSize.height, - screenWidth: deviceScreenSize.width, + screenHeight: screenSize.height, + screenWidth: screenSize.width, tag, }, isNativeContext, diff --git a/packages/webdriver-image-comparison/src/commands/saveAppScreen.ts b/packages/webdriver-image-comparison/src/commands/saveAppScreen.ts index e43288693..efb99fed4 100644 --- a/packages/webdriver-image-comparison/src/commands/saveAppScreen.ts +++ b/packages/webdriver-image-comparison/src/commands/saveAppScreen.ts @@ -28,7 +28,7 @@ export default async function saveAppScreen( browserVersion, deviceName, devicePixelRatio, - deviceScreenSize, + deviceRectangles: { screenSize }, isIOS, isMobile, logName, @@ -51,8 +51,8 @@ export default async function saveAppScreen( isLandscape: false, rectangles :{ // For iOS the screen size is always in css pixels, the screenshot is in device pixels - height: isIOS ? deviceScreenSize.height * devicePixelRatio : deviceScreenSize.height, - width: isIOS ? deviceScreenSize.width * devicePixelRatio : deviceScreenSize.width, + height: isIOS ? screenSize.height * devicePixelRatio : screenSize.height, + width: isIOS ? screenSize.width * devicePixelRatio : screenSize.width, x: 0, y: 0, }, @@ -81,8 +81,8 @@ export default async function saveAppScreen( name: '', platformName, platformVersion, - screenHeight: deviceScreenSize.height, - screenWidth: deviceScreenSize.width, + screenHeight: screenSize.height, + screenWidth: screenSize.width, tag, }, isNativeContext, diff --git a/packages/webdriver-image-comparison/src/commands/saveFullPageScreen.ts b/packages/webdriver-image-comparison/src/commands/saveFullPageScreen.ts index 5762ac29c..959954ec9 100644 --- a/packages/webdriver-image-comparison/src/commands/saveFullPageScreen.ts +++ b/packages/webdriver-image-comparison/src/commands/saveFullPageScreen.ts @@ -6,6 +6,7 @@ import type { ScreenshotOutput, AfterScreenshotOptions } from '../helpers/afterS import type { BeforeScreenshotOptions, BeforeScreenshotResult } from '../helpers/beforeScreenshot.interfaces.js' import type { FullPageScreenshotDataOptions, FullPageScreenshotsData } from '../methods/screenshots.interfaces.js' import type { InternalSaveFullPageMethodOptions } from './save.interfaces.js' +import { getMethodOrWicOption } from '../helpers/utils.js' /** * Saves an image of the full page @@ -29,33 +30,21 @@ export default async function saveFullPageScreen( const { addressBarShadowPadding, formatImageName, - isHybridApp, savePerInstance, toolBarShadowPadding, } = saveFullPageOptions.wic // 1c. Set the method options to the right values - const disableBlinkingCursor: boolean = saveFullPageOptions.method.disableBlinkingCursor !== undefined - ? Boolean(saveFullPageOptions.method.disableBlinkingCursor) - : saveFullPageOptions.wic.disableBlinkingCursor - const disableCSSAnimation: boolean = saveFullPageOptions.method.disableCSSAnimation !== undefined - ? Boolean(saveFullPageOptions.method.disableCSSAnimation) - : saveFullPageOptions.wic.disableCSSAnimation - const enableLayoutTesting: boolean = saveFullPageOptions.method.enableLayoutTesting !== undefined - ? Boolean(saveFullPageOptions.method.enableLayoutTesting) - : saveFullPageOptions.wic.enableLayoutTesting - const hideScrollBars: boolean = saveFullPageOptions.method.hideScrollBars !== undefined - ? Boolean(saveFullPageOptions.method.hideScrollBars) - : saveFullPageOptions.wic.hideScrollBars - const fullPageScrollTimeout: number = saveFullPageOptions.method.fullPageScrollTimeout !== undefined - ? saveFullPageOptions.method.fullPageScrollTimeout! - : saveFullPageOptions.wic.fullPageScrollTimeout + const userBasedFullPageScreenshot = getMethodOrWicOption(saveFullPageOptions.method, saveFullPageOptions.wic, 'userBasedFullPageScreenshot') + const disableBlinkingCursor = getMethodOrWicOption(saveFullPageOptions.method, saveFullPageOptions.wic, 'disableBlinkingCursor') + const disableCSSAnimation = getMethodOrWicOption(saveFullPageOptions.method, saveFullPageOptions.wic, 'disableCSSAnimation') + const enableLayoutTesting = getMethodOrWicOption(saveFullPageOptions.method, saveFullPageOptions.wic, 'enableLayoutTesting') + const fullPageScrollTimeout = getMethodOrWicOption(saveFullPageOptions.method, saveFullPageOptions.wic, 'fullPageScrollTimeout') + const hideAfterFirstScroll: HTMLElement[] = saveFullPageOptions.method.hideAfterFirstScroll || [] const hideElements: HTMLElement[] = saveFullPageOptions.method.hideElements || [] + const hideScrollBars = getMethodOrWicOption(saveFullPageOptions.method, saveFullPageOptions.wic, 'hideScrollBars') const removeElements: HTMLElement[] = saveFullPageOptions.method.removeElements || [] - const hideAfterFirstScroll: HTMLElement[] = saveFullPageOptions.method.hideAfterFirstScroll || [] - const waitForFontsLoaded: boolean = saveFullPageOptions.method.waitForFontsLoaded !== undefined - ? Boolean(saveFullPageOptions.method.waitForFontsLoaded) - : saveFullPageOptions.wic.waitForFontsLoaded + const waitForFontsLoaded = getMethodOrWicOption(saveFullPageOptions.method, saveFullPageOptions.wic, 'waitForFontsLoaded') // 2. Prepare the beforeScreenshot const beforeOptions: BeforeScreenshotOptions = { @@ -73,35 +62,42 @@ export default async function saveFullPageScreen( const enrichedInstanceData: BeforeScreenshotResult = await beforeScreenshot(methods.executor, beforeOptions, true) const devicePixelRatio = enrichedInstanceData.dimensions.window.devicePixelRatio const isLandscape = enrichedInstanceData.dimensions.window.isLandscape + let fullPageBase64Image: string - // 3. Fullpage screenshots are taken per scrolled viewport - const fullPageScreenshotOptions: FullPageScreenshotDataOptions = { - addressBarShadowPadding: enrichedInstanceData.addressBarShadowPadding, - devicePixelRatio: devicePixelRatio || NaN, - fullPageScrollTimeout, - hideAfterFirstScroll, - innerHeight: enrichedInstanceData.dimensions.window.innerHeight || NaN, - isAndroid: enrichedInstanceData.isAndroid, - isAndroidChromeDriverScreenshot: enrichedInstanceData.isAndroidChromeDriverScreenshot, - isAndroidNativeWebScreenshot: enrichedInstanceData.isAndroidNativeWebScreenshot, - isHybridApp, - isIOS: enrichedInstanceData.isIOS, - isLandscape, - screenHeight: enrichedInstanceData.dimensions.window.screenHeight || NaN, - screenWidth: enrichedInstanceData.dimensions.window.screenWidth || NaN, - toolBarShadowPadding: enrichedInstanceData.toolBarShadowPadding, - } - const screenshotsData: FullPageScreenshotsData = await getBase64FullPageScreenshotsData( - methods.screenShot, - methods.executor, - fullPageScreenshotOptions, - ) + if (typeof methods.bidiScreenshot === 'function' && typeof methods.getWindowHandle === 'function' && userBasedFullPageScreenshot) { + // 3a. Fullpage screenshots are taken in one go with the Bidi protocol + const contextID = await methods.getWindowHandle() + fullPageBase64Image =( await methods.bidiScreenshot({ context: contextID, origin: 'document' })).data + } else { + // 3b. Fullpage screenshots are taken per scrolled viewport + const fullPageScreenshotOptions: FullPageScreenshotDataOptions = { + addressBarShadowPadding: enrichedInstanceData.addressBarShadowPadding, + devicePixelRatio: devicePixelRatio || NaN, + deviceRectangles: instanceData.deviceRectangles, + fullPageScrollTimeout, + hideAfterFirstScroll, + innerHeight: enrichedInstanceData.dimensions.window.innerHeight || NaN, + isAndroid: enrichedInstanceData.isAndroid, + isAndroidChromeDriverScreenshot: enrichedInstanceData.isAndroidChromeDriverScreenshot, + isAndroidNativeWebScreenshot: enrichedInstanceData.isAndroidNativeWebScreenshot, + isIOS: enrichedInstanceData.isIOS, + isLandscape, + screenHeight: enrichedInstanceData.dimensions.window.screenHeight || NaN, + screenWidth: enrichedInstanceData.dimensions.window.screenWidth || NaN, + toolBarShadowPadding: enrichedInstanceData.toolBarShadowPadding, + } + const screenshotsData: FullPageScreenshotsData = await getBase64FullPageScreenshotsData( + methods.screenShot, + methods.executor, + fullPageScreenshotOptions, + ) - // 4. Make a fullpage base64 image - const fullPageBase64Image: string = await makeFullPageBase64Image(screenshotsData, { - devicePixelRatio: devicePixelRatio || NaN, - isLandscape, - }) + // 4. Make a fullpage base64 image by scrolling and stitching the images together + fullPageBase64Image = await makeFullPageBase64Image(screenshotsData, { + devicePixelRatio: devicePixelRatio || NaN, + isLandscape, + }) + } // 5. The after the screenshot methods const afterOptions: AfterScreenshotOptions = { @@ -145,3 +141,4 @@ export default async function saveFullPageScreen( // 6. Return the data return afterScreenshot(methods.executor, afterOptions!) } + diff --git a/packages/webdriver-image-comparison/src/commands/saveWebElement.ts b/packages/webdriver-image-comparison/src/commands/saveWebElement.ts index d9812c2bb..e0e49f106 100644 --- a/packages/webdriver-image-comparison/src/commands/saveWebElement.ts +++ b/packages/webdriver-image-comparison/src/commands/saveWebElement.ts @@ -7,7 +7,7 @@ import type { BeforeScreenshotOptions, BeforeScreenshotResult } from '../helpers import { DEFAULT_RESIZE_DIMENSIONS } from '../helpers/constants.js' import type { ResizeDimensions } from '../methods/images.interfaces.js' import scrollElementIntoView from '../clientSideScripts/scrollElementIntoView.js' -import { getScreenshotSize } from '../helpers/utils.js' +import { getBase64ScreenshotSize, getMethodOrWicOption, waitFor } from '../helpers/utils.js' import scrollToPosition from '../clientSideScripts/scrollToPosition.js' import type { InternalSaveElementMethodOptions } from './save.interfaces.js' @@ -29,24 +29,14 @@ export default async function saveWebElement( saveElementOptions.wic const { executor, screenShot, takeElementScreenshot } = methods // 1b. Set the method options to the right values - const disableBlinkingCursor: boolean = saveElementOptions.method.disableBlinkingCursor !== undefined - ? Boolean(saveElementOptions.method.disableBlinkingCursor) - : saveElementOptions.wic.disableBlinkingCursor - const disableCSSAnimation: boolean = saveElementOptions.method.disableCSSAnimation !== undefined - ? Boolean(saveElementOptions.method.disableCSSAnimation) - : saveElementOptions.wic.disableCSSAnimation - const enableLayoutTesting: boolean = saveElementOptions.method.enableLayoutTesting !== undefined - ? Boolean(saveElementOptions.method.enableLayoutTesting) - : saveElementOptions.wic.enableLayoutTesting - const hideScrollBars: boolean = saveElementOptions.method.hideScrollBars !== undefined - ? Boolean(saveElementOptions.method.hideScrollBars) - : saveElementOptions.wic.hideScrollBars - const resizeDimensions: ResizeDimensions | number = saveElementOptions.method.resizeDimensions || DEFAULT_RESIZE_DIMENSIONS + const disableBlinkingCursor = getMethodOrWicOption(saveElementOptions.method, saveElementOptions.wic, 'disableBlinkingCursor') + const disableCSSAnimation = getMethodOrWicOption(saveElementOptions.method, saveElementOptions.wic, 'disableCSSAnimation') + const enableLayoutTesting = getMethodOrWicOption(saveElementOptions.method, saveElementOptions.wic, 'enableLayoutTesting') const hideElements: HTMLElement[] = saveElementOptions.method.hideElements || [] + const hideScrollBars = getMethodOrWicOption(saveElementOptions.method, saveElementOptions.wic, 'hideScrollBars') const removeElements: HTMLElement[] = saveElementOptions.method.removeElements || [] - const waitForFontsLoaded: boolean = saveElementOptions.method.waitForFontsLoaded !== undefined - ? Boolean(saveElementOptions.method.waitForFontsLoaded) - : saveElementOptions.wic.waitForFontsLoaded + const resizeDimensions: ResizeDimensions | number = saveElementOptions.method.resizeDimensions || DEFAULT_RESIZE_DIMENSIONS + const waitForFontsLoaded = getMethodOrWicOption(saveElementOptions.method, saveElementOptions.wic, 'waitForFontsLoaded') // 2. Prepare the beforeScreenshot const beforeOptions: BeforeScreenshotOptions = { @@ -84,11 +74,14 @@ export default async function saveWebElement( let currentPosition: number | undefined if (autoElementScroll) { currentPosition = await executor(scrollElementIntoView, element, addressBarShadowPadding) + // We need to wait for the scroll to finish before taking the screenshot + await waitFor(100) } // 3. Take the screenshot and determine the rectangles const { base64Image, rectangles, isWebDriverElementScreenshot } = await takeWebElementScreenshot({ devicePixelRatio, + deviceRectangles: instanceData.deviceRectangles, element, executor, innerHeight, @@ -111,7 +104,7 @@ export default async function saveWebElement( // When the element has no height or width, we default to the viewport screen size if (rectangles.width === 0 || rectangles.height === 0) { - const { height, width } = getScreenshotSize(base64Image) + const { height, width } = getBase64ScreenshotSize(base64Image) rectangles.width = width rectangles.height = height rectangles.x = 0 diff --git a/packages/webdriver-image-comparison/src/commands/saveWebScreen.ts b/packages/webdriver-image-comparison/src/commands/saveWebScreen.ts index c237f73d9..e0cb977a0 100644 --- a/packages/webdriver-image-comparison/src/commands/saveWebScreen.ts +++ b/packages/webdriver-image-comparison/src/commands/saveWebScreen.ts @@ -7,6 +7,7 @@ import type { BeforeScreenshotOptions, BeforeScreenshotResult } from '../helpers import type { AfterScreenshotOptions, ScreenshotOutput } from '../helpers/afterScreenshot.interfaces.js' import type { RectanglesOutput, ScreenRectanglesOptions } from '../methods/rectangles.interfaces.js' import type { InternalSaveScreenMethodOptions } from './save.interfaces.js' +import { getMethodOrWicOption } from '../helpers/utils.js' /** * Saves an image of the viewport of the screen @@ -26,23 +27,13 @@ export default async function saveWebScreen( saveScreenOptions.wic // 1b. Set the method options to the right values - const disableBlinkingCursor: boolean = saveScreenOptions.method.disableBlinkingCursor !== undefined - ? Boolean(saveScreenOptions.method.disableBlinkingCursor) - : saveScreenOptions.wic.disableBlinkingCursor - const disableCSSAnimation: boolean = saveScreenOptions.method.disableCSSAnimation !== undefined - ? Boolean(saveScreenOptions.method.disableCSSAnimation) - : saveScreenOptions.wic.disableCSSAnimation - const enableLayoutTesting: boolean = saveScreenOptions.method.enableLayoutTesting !== undefined - ? Boolean(saveScreenOptions.method.enableLayoutTesting) - : saveScreenOptions.wic.enableLayoutTesting - const hideScrollBars: boolean = saveScreenOptions.method.hideScrollBars !== undefined - ? Boolean(saveScreenOptions.method.hideScrollBars) - : saveScreenOptions.wic.hideScrollBars + const disableBlinkingCursor = getMethodOrWicOption(saveScreenOptions.method, saveScreenOptions.wic, 'disableBlinkingCursor') + const disableCSSAnimation = getMethodOrWicOption(saveScreenOptions.method, saveScreenOptions.wic, 'disableCSSAnimation') + const enableLayoutTesting = getMethodOrWicOption(saveScreenOptions.method, saveScreenOptions.wic, 'enableLayoutTesting') + const hideScrollBars = getMethodOrWicOption(saveScreenOptions.method, saveScreenOptions.wic, 'hideScrollBars') const hideElements: HTMLElement[] = saveScreenOptions.method.hideElements || [] const removeElements: HTMLElement[] = saveScreenOptions.method.removeElements || [] - const waitForFontsLoaded: boolean = saveScreenOptions.method.waitForFontsLoaded !== undefined - ? Boolean(saveScreenOptions.method.waitForFontsLoaded) - : saveScreenOptions.wic.waitForFontsLoaded + const waitForFontsLoaded = getMethodOrWicOption(saveScreenOptions.method, saveScreenOptions.wic, 'waitForFontsLoaded') // 2. Prepare the beforeScreenshot const beforeOptions: BeforeScreenshotOptions = { diff --git a/packages/webdriver-image-comparison/src/helpers/__snapshots__/beforeScreenshot.test.ts.snap b/packages/webdriver-image-comparison/src/helpers/__snapshots__/beforeScreenshot.test.ts.snap index 8711586cd..5a47f93d9 100644 --- a/packages/webdriver-image-comparison/src/helpers/__snapshots__/beforeScreenshot.test.ts.snap +++ b/packages/webdriver-image-comparison/src/helpers/__snapshots__/beforeScreenshot.test.ts.snap @@ -8,23 +8,53 @@ exports[`beforeScreenshot > should be able to return the enriched instance data "browserVersion": "browserVersion", "deviceName": "deviceName", "devicePixelRatio": 1, - "devicePlatformRect": { + "deviceRectangles": { + "bottomBar": { + "height": 0, + "width": 0, + "x": 0, + "y": 0, + }, "homeBar": { "height": 0, "width": 0, "x": 0, "y": 0, }, + "leftSidePadding": { + "height": 0, + "width": 0, + "x": 0, + "y": 0, + }, + "rightSidePadding": { + "height": 0, + "width": 0, + "x": 0, + "y": 0, + }, + "screenSize": { + "height": 1, + "width": 1, + }, "statusBar": { "height": 0, "width": 0, "x": 0, "y": 0, }, - }, - "deviceScreenSize": { - "height": 1, - "width": 1, + "statusBarAndAddressBar": { + "height": 0, + "width": 0, + "x": 0, + "y": 0, + }, + "viewport": { + "height": 0, + "width": 0, + "x": 0, + "y": 0, + }, }, "isAndroid": false, "isAndroidChromeDriverScreenshot": false, @@ -50,23 +80,53 @@ exports[`beforeScreenshot > should be able to return the enriched instance data "browserVersion": "browserVersion", "deviceName": "deviceName", "devicePixelRatio": 1, - "devicePlatformRect": { + "deviceRectangles": { + "bottomBar": { + "height": 0, + "width": 0, + "x": 0, + "y": 0, + }, "homeBar": { "height": 0, "width": 0, "x": 0, "y": 0, }, + "leftSidePadding": { + "height": 0, + "width": 0, + "x": 0, + "y": 0, + }, + "rightSidePadding": { + "height": 0, + "width": 0, + "x": 0, + "y": 0, + }, + "screenSize": { + "height": 1, + "width": 1, + }, "statusBar": { "height": 0, "width": 0, "x": 0, "y": 0, }, - }, - "deviceScreenSize": { - "height": 1, - "width": 1, + "statusBarAndAddressBar": { + "height": 0, + "width": 0, + "x": 0, + "y": 0, + }, + "viewport": { + "height": 0, + "width": 0, + "x": 0, + "y": 0, + }, }, "isAndroid": false, "isAndroidChromeDriverScreenshot": false, diff --git a/packages/webdriver-image-comparison/src/helpers/__snapshots__/options.test.ts.snap b/packages/webdriver-image-comparison/src/helpers/__snapshots__/options.test.ts.snap index 9bf453a10..1af0ea487 100644 --- a/packages/webdriver-image-comparison/src/helpers/__snapshots__/options.test.ts.snap +++ b/packages/webdriver-image-comparison/src/helpers/__snapshots__/options.test.ts.snap @@ -48,6 +48,7 @@ exports[`options > defaultOptions > should return the default options when no op }, }, "toolBarShadowPadding": 6, + "userBasedFullPageScreenshot": false, "waitForFontsLoaded": true, } `; @@ -100,6 +101,7 @@ exports[`options > defaultOptions > should return the provided options when opti }, }, "toolBarShadowPadding": 1, + "userBasedFullPageScreenshot": false, "waitForFontsLoaded": true, } `; diff --git a/packages/webdriver-image-comparison/src/helpers/__snapshots__/utils.test.ts.snap b/packages/webdriver-image-comparison/src/helpers/__snapshots__/utils.test.ts.snap index b5f514c81..a1164ab7d 100644 --- a/packages/webdriver-image-comparison/src/helpers/__snapshots__/utils.test.ts.snap +++ b/packages/webdriver-image-comparison/src/helpers/__snapshots__/utils.test.ts.snap @@ -89,21 +89,248 @@ exports[`utils > getAndCreatePath > should create the folder and return the fold exports[`utils > getAndCreatePath > should create the folder and return the folder name for a device that needs to have its own folder 2`] = `true`; -exports[`utils > getScreenshotSize > should get the screenshot size of a screenshot string with DRP 2 1`] = ` +exports[`utils > getBase64ScreenshotSize > should get the screenshot size of a screenshot string with DRP 2 1`] = ` { "height": 768, "width": 1366, } `; -exports[`utils > getScreenshotSize > should get the screenshot size of a screenshot string with the default DPR 1`] = ` +exports[`utils > getBase64ScreenshotSize > should get the screenshot size of a screenshot string with the default DPR 1`] = ` { "height": 1536, "width": 2732, } `; -exports[`utils > getToolBarShadowPadding > should return 0 when this is a check for Android browser and adding a shadow padding 1`] = `0`; +exports[`utils > getDevicePixelRatio > should return 1 when the screenshot width equals device screen width 1`] = `85`; + +exports[`utils > getDevicePixelRatio > should return 2 when the screenshot width is double the device screen width 1`] = `171`; + +exports[`utils > getDevicePixelRatio > should round the result to the nearest integer 1`] = `161`; + +exports[`utils > getIosBezelImageNames > should return bezel image names for "ipadair" 1`] = ` +{ + "bottomImageName": "ipadair4th.ipadair5th-bottom", + "topImageName": "ipadair4th.ipadair5th-top", +} +`; + +exports[`utils > getIosBezelImageNames > should return bezel image names for "ipadmini" 1`] = ` +{ + "bottomImageName": "ipadmini6th-bottom", + "topImageName": "ipadmini6th-top", +} +`; + +exports[`utils > getIosBezelImageNames > should return bezel image names for "ipadpro11" 1`] = ` +{ + "bottomImageName": "ipadpro11-bottom", + "topImageName": "ipadpro11-top", +} +`; + +exports[`utils > getIosBezelImageNames > should return bezel image names for "ipadpro129" 1`] = ` +{ + "bottomImageName": "ipadpro129-bottom", + "topImageName": "ipadpro129-top", +} +`; + +exports[`utils > getIosBezelImageNames > should return bezel image names for "iphone11" 1`] = ` +{ + "bottomImageName": "iphonexr.iphone11-bottom", + "topImageName": "iphonexr.iphone11-top", +} +`; + +exports[`utils > getIosBezelImageNames > should return bezel image names for "iphone11pro" 1`] = ` +{ + "bottomImageName": "iphonex.iphonexs.iphone11pro-bottom", + "topImageName": "iphonex.iphonexs.iphone11pro-top", +} +`; + +exports[`utils > getIosBezelImageNames > should return bezel image names for "iphone11promax" 1`] = ` +{ + "bottomImageName": "iphone11promax-bottom", + "topImageName": "iphone11promax-top", +} +`; + +exports[`utils > getIosBezelImageNames > should return bezel image names for "iphone12" 1`] = ` +{ + "bottomImageName": "iphone12.iphone12pro.iphone13.iphone13pro.iphone14-bottom", + "topImageName": "iphone12.iphone12pro-top", +} +`; + +exports[`utils > getIosBezelImageNames > should return bezel image names for "iphone12mini" 1`] = ` +{ + "bottomImageName": "iphone12mini.iphone13mini-bottom", + "topImageName": "iphone12mini-top", +} +`; + +exports[`utils > getIosBezelImageNames > should return bezel image names for "iphone12pro" 1`] = ` +{ + "bottomImageName": "iphone12.iphone12pro.iphone13.iphone13pro.iphone14-bottom", + "topImageName": "iphone12.iphone12pro-top", +} +`; + +exports[`utils > getIosBezelImageNames > should return bezel image names for "iphone12promax" 1`] = ` +{ + "bottomImageName": "iphone12promax.iphone13promax.iphone14plus-bottom", + "topImageName": "iphone12promax-top", +} +`; + +exports[`utils > getIosBezelImageNames > should return bezel image names for "iphone13" 1`] = ` +{ + "bottomImageName": "iphone12.iphone12pro.iphone13.iphone13pro.iphone14-bottom", + "topImageName": "iphone13.iphone13pro.iphone14-top", +} +`; + +exports[`utils > getIosBezelImageNames > should return bezel image names for "iphone13mini" 1`] = ` +{ + "bottomImageName": "iphone12mini.iphone13mini-bottom", + "topImageName": "iphone13mini-top", +} +`; + +exports[`utils > getIosBezelImageNames > should return bezel image names for "iphone13pro" 1`] = ` +{ + "bottomImageName": "iphone12.iphone12pro.iphone13.iphone13pro.iphone14-bottom", + "topImageName": "iphone13.iphone13pro.iphone14-top", +} +`; + +exports[`utils > getIosBezelImageNames > should return bezel image names for "iphone13promax" 1`] = ` +{ + "bottomImageName": "iphone12promax.iphone13promax.iphone14plus-bottom", + "topImageName": "iphone13promax.iphone14plus-top", +} +`; + +exports[`utils > getIosBezelImageNames > should return bezel image names for "iphone14" 1`] = ` +{ + "bottomImageName": "iphone12.iphone12pro.iphone13.iphone13pro.iphone14-bottom", + "topImageName": "iphone13.iphone13pro.iphone14-top", +} +`; + +exports[`utils > getIosBezelImageNames > should return bezel image names for "iphone14plus" 1`] = ` +{ + "bottomImageName": "iphone12promax.iphone13promax.iphone14plus-bottom", + "topImageName": "iphone13promax.iphone14plus-top", +} +`; + +exports[`utils > getIosBezelImageNames > should return bezel image names for "iphone14pro" 1`] = ` +{ + "bottomImageName": "iphone14pro-bottom", + "topImageName": "iphone14pro-top", +} +`; + +exports[`utils > getIosBezelImageNames > should return bezel image names for "iphone14promax" 1`] = ` +{ + "bottomImageName": "iphone14promax-bottom", + "topImageName": "iphone14promax-top", +} +`; + +exports[`utils > getIosBezelImageNames > should return bezel image names for "iphone15" 1`] = ` +{ + "bottomImageName": "iphone15-bottom", + "topImageName": "iphone15-top", +} +`; + +exports[`utils > getIosBezelImageNames > should return bezel image names for "iphonex" 1`] = ` +{ + "bottomImageName": "iphonex.iphonexs.iphone11pro-bottom", + "topImageName": "iphonex.iphonexs.iphone11pro-top", +} +`; + +exports[`utils > getIosBezelImageNames > should return bezel image names for "iphonexr" 1`] = ` +{ + "bottomImageName": "iphonexr.iphone11-bottom", + "topImageName": "iphonexr.iphone11-top", +} +`; + +exports[`utils > getIosBezelImageNames > should return bezel image names for "iphonexs" 1`] = ` +{ + "bottomImageName": "iphonex.iphonexs.iphone11pro-bottom", + "topImageName": "iphonex.iphonexs.iphone11pro-top", +} +`; + +exports[`utils > getIosBezelImageNames > should return bezel image names for "iphonexsmax" 1`] = ` +{ + "bottomImageName": "iphonexsmax-bottom", + "topImageName": "iphonexsmax-top", +} +`; + +exports[`utils > getIosBezelImageNames > should throw an error for unsupported device names 1`] = `[Error: Could not find iOS bezel images for device unsupportedDevice]`; + +exports[`utils > getMobileViewPortPosition > should return correct device rectangles for iOS WebView flow 1`] = ` +{ + "bottomBar": { + "height": 600, + "width": 400, + "x": 0, + "y": 200, + }, + "homeBar": { + "height": 0, + "width": 0, + "x": 0, + "y": 0, + }, + "leftSidePadding": { + "height": 100, + "width": 50, + "x": 0, + "y": 100, + }, + "rightSidePadding": { + "height": 100, + "width": 250, + "x": 150, + "y": 100, + }, + "screenSize": { + "height": 800, + "width": 400, + }, + "statusBar": { + "height": 0, + "width": 0, + "x": 0, + "y": 0, + }, + "statusBarAndAddressBar": { + "height": 100, + "width": 400, + "x": 0, + "y": 0, + }, + "viewport": { + "height": 100, + "width": 100, + "x": 50, + "y": 100, + }, +} +`; + +exports[`utils > getToolBarShadowPadding > should return 0 when this is a check for Android browser and adding a shadow padding 1`] = `6`; exports[`utils > getToolBarShadowPadding > should return 0 when this is a check for a desktop browser 1`] = `0`; @@ -116,3 +343,41 @@ exports[`utils > getToolBarShadowPadding > should return 0 when this is a check exports[`utils > getToolBarShadowPadding > should return 0 when this is a check for an iOS app with adding a shadow padding 1`] = `0`; exports[`utils > getToolBarShadowPadding > should return 15 when this is a check for iOS browser and adding a shadow padding 1`] = `15`; + +exports[`utils > logAllDeprecatedCompareOptions > should log a deprecation warning for each deprecated key 1`] = ` +"The following root-level compare options are deprecated and should be moved under 'compareOptions': + - blockOutSideBar + - blockOutStatusBar + - blockOutToolBar + - createJsonReportFiles + - diffPixelBoundingBoxProximity + - ignoreAlpha + - ignoreAntialiasing + - ignoreColors + - ignoreLess + - ignoreNothing + - rawMisMatchPercentage + - returnAllCompareData + - saveAboveTolerance + - scaleImagesToSameSize +In the next major version, these options will be removed from the root level and only be available under 'compareOptions'" +`; + +exports[`utils > logAllDeprecatedCompareOptions > should return a subset of CompareOptions with deprecated keys only 1`] = ` +{ + "blockOutSideBar": true, + "blockOutStatusBar": true, + "blockOutToolBar": true, + "createJsonReportFiles": true, + "diffPixelBoundingBoxProximity": 5, + "ignoreAlpha": true, + "ignoreAntialiasing": true, + "ignoreColors": true, + "ignoreLess": true, + "ignoreNothing": true, + "rawMisMatchPercentage": true, + "returnAllCompareData": true, + "saveAboveTolerance": 100, + "scaleImagesToSameSize": true, +} +`; diff --git a/packages/webdriver-image-comparison/src/helpers/beforeScreenshot.test.ts b/packages/webdriver-image-comparison/src/helpers/beforeScreenshot.test.ts index 0e2540af2..d96a2d22a 100644 --- a/packages/webdriver-image-comparison/src/helpers/beforeScreenshot.test.ts +++ b/packages/webdriver-image-comparison/src/helpers/beforeScreenshot.test.ts @@ -4,7 +4,6 @@ import beforeScreenshot from './beforeScreenshot.js' describe('beforeScreenshot', () => { it('should be able to return the enriched instance data with default options', async () => { const MOCKED_EXECUTOR = vi.fn().mockReturnValue('') - const options = { instanceData: { appName: 'appName', @@ -12,18 +11,20 @@ describe('beforeScreenshot', () => { browserVersion: 'browserVersion', deviceName: 'deviceName', devicePixelRatio: 1, - deviceScreenSize: { - height:1, - width: 1, - }, - devicePlatformRect: { - statusBar: { x: 0, y:0, width: 0, height: 0 }, - homeBar: { x: 0, y:0, width: 0, height: 0 }, - }, isAndroid: false, isIOS: false, isMobile: false, logName: 'logName', + deviceRectangles: { + bottomBar: { y: 0, x: 0, width: 0, height: 0 }, + homeBar: { x: 0, y:0, width: 0, height: 0 }, + leftSidePadding: { y: 0, x: 0, width: 0, height: 0 }, + rightSidePadding: { y: 0, x: 0, width: 0, height: 0 }, + screenSize: { height: 1, width: 1 }, + statusBar: { x: 0, y:0, width: 0, height: 0 }, + statusBarAndAddressBar: { y: 0, x: 0, width: 0, height: 0 }, + viewport: { y: 0, x: 0, width: 0, height: 0 }, + }, name: 'name', nativeWebScreenshot: false, platformName: 'platformName', @@ -53,15 +54,17 @@ describe('beforeScreenshot', () => { browserVersion: 'browserVersion', deviceName: 'deviceName', devicePixelRatio: 1, - deviceScreenSize: { - height:1, - width: 1, - }, - devicePlatformRect: { - statusBar: { x: 0, y:0, width: 0, height: 0 }, + logName: 'logName', + deviceRectangles: { + bottomBar: { y: 0, x: 0, width: 0, height: 0 }, homeBar: { x: 0, y:0, width: 0, height: 0 }, + leftSidePadding: { y: 0, x: 0, width: 0, height: 0 }, + rightSidePadding: { y: 0, x: 0, width: 0, height: 0 }, + screenSize: { height: 1, width: 1 }, + statusBar: { x: 0, y:0, width: 0, height: 0 }, + statusBarAndAddressBar: { y: 0, x: 0, width: 0, height: 0 }, + viewport: { y: 0, x: 0, width: 0, height: 0 }, }, - logName: 'logName', isAndroid: false, isIOS: false, isMobile: false, diff --git a/packages/webdriver-image-comparison/src/helpers/constants.ts b/packages/webdriver-image-comparison/src/helpers/constants.ts index 216e19f42..91040659b 100644 --- a/packages/webdriver-image-comparison/src/helpers/constants.ts +++ b/packages/webdriver-image-comparison/src/helpers/constants.ts @@ -1,7 +1,24 @@ import type { IosOffsets } from './constants.interfaces.js' import type { ResizeDimensions } from '../methods/images.interfaces.js' -import type { TestContext } from 'src/commands/check.interfaces.js' +import type { TestContext } from '../commands/check.interfaces.js' +import type { DeviceRectangles } from '../methods/rectangles.interfaces.js' +export const DEFAULT_COMPARE_OPTIONS = { + blockOutSideBar: true, + blockOutStatusBar: true, + blockOutToolBar: true, + createJsonReportFiles: false, + diffPixelBoundingBoxProximity: 5, + ignoreAlpha: false, + ignoreAntialiasing: false, + ignoreColors: false, + ignoreLess: false, + ignoreNothing: false, + rawMisMatchPercentage: false, + returnAllCompareData: false, + saveAboveTolerance: 0, + scaleImagesToSameSize: false, +} export const DEFAULT_FORMAT_STRING = '{tag}-{browserName}-{width}x{height}-dpr-{dpr}' export const STORYBOOK_FORMAT_STRING = '{tag}-{logName}-{width}x{height}-dpr-{dpr}' export const PLATFORMS = { @@ -47,6 +64,16 @@ export const DEFAULT_TABBABLE_OPTIONS = { width: 1, }, } +export const DEVICE_RECTANGLES: DeviceRectangles = { + bottomBar: { y: 0, x: 0, width: 0, height: 0 }, + homeBar: { y: 0, x: 0, width: 0, height: 0 }, + leftSidePadding: { y: 0, x: 0, width: 0, height: 0 }, + rightSidePadding: { y: 0, x: 0, width: 0, height: 0 }, + statusBar: { y: 0, x: 0, width: 0, height: 0 }, + statusBarAndAddressBar: { y: 0, x: 0, width: 0, height: 0 }, + screenSize: { width: 0, height: 0 }, + viewport: { y: 0, x: 0, width: 0, height: 0 }, +} /** * Android and iOS offsets data diff --git a/packages/webdriver-image-comparison/src/helpers/options.interfaces.ts b/packages/webdriver-image-comparison/src/helpers/options.interfaces.ts index 71c690df6..47a92978a 100644 --- a/packages/webdriver-image-comparison/src/helpers/options.interfaces.ts +++ b/packages/webdriver-image-comparison/src/helpers/options.interfaces.ts @@ -1,163 +1,447 @@ import type { TabbableOptions } from '../commands/tabbable.interfaces.js' export interface ClassOptions { + // ================== + // Class options + // ================== + /** - * Class options + * The padding added to the address bar on iOS and Android + * to properly cut out the viewport. */ - // The padding that needs to be added to the address bar on iOS and Android to do a proper cutout of the the viewport. addressBarShadowPadding?: number; - // Automatically scroll to an element before taking a screenshot. + + /** + * Automatically scroll to an element before taking a screenshot. + */ autoElementScroll?: boolean; - // Add iOS bezel corners and notch/dynamic island to the screenshot + + /** + * Add iOS bezel corners and notch/dynamic island to the screenshot. + */ addIOSBezelCorners?: boolean; - // Delete runtime folder (actual & diff) on initialization + + /** + * Delete runtime folder (actual & diff) on initialization. + */ clearRuntimeFolder?: boolean; - // The naming of the saved images can be customized by passing the parameter `formatImageName` with a format string + + /** + * Use user based fullpage screenshots, by default the desktop screenshots are taken with the BiDi protocol. + */ + userBasedFullPageScreenshot?: boolean; + + /** + * Customize the naming of saved images using a format string. + */ formatImageName?: string; - // Is it an hybrid app or not + + /** + * Indicates if this is a hybrid app. + */ isHybridApp?: boolean; - // Save the images per instance in a separate folder. + + /** + * Save the images per instance in a separate folder. + */ savePerInstance?: boolean; - // The padding that needs to be added to the toolbar bar on iOS and Android to do a proper cutout of the the viewport. + + /** + * The padding added to the toolbar on iOS and Android + * to properly cut out the viewport. + */ toolBarShadowPadding?: number; - // Wait for the fonts to be loaded before taking a screenshot + + /** + * Wait for fonts to be fully loaded before taking a screenshot. + */ waitForFontsLoaded?: boolean; + // ================== + // Baseline options + // ================== + /** - * Baseline options + * If no baseline image is found during comparison, automatically save the image to the baseline folder. */ - // If no baseline image is found during the comparison the image is automatically copied to the baseline folder when this is set to `true` autoSaveBaseline?: boolean; - // The directory that will hold all the actual / difference screenshots + + /** + * Directory where all actual and diff screenshots will be stored. + */ screenshotPath?: string | ((options: ClassOptions) => string); - // The directory that will hold all the baseline images that are used to during the comparison + + /** + * Directory for storing baseline images used during comparison. + */ baselineFolder?: string | ((options: ClassOptions) => string); + // ========================== + // Class and method options + // ========================== + /** - * Class and method options + * Disable the blinking cursor in screenshots. */ - // Disable the blinking cursor in the screenshots disableBlinkingCursor?: boolean; - // En/Disable all css animations and the input caret in the application. + + /** + * Enable or disable all CSS animations and input carets. + */ disableCSSAnimation?: boolean; - // Make all text on a page transparent to only focus on the layout. + + /** + * Make all text transparent to focus only on layout. + */ enableLayoutTesting?: boolean; - // The timeout in milliseconds to wait after a scroll. This might help identifying pages with lazy loading. + + /** + * Time (ms) to wait after a scroll; useful for lazy loading content. + */ fullPageScrollTimeout?: number; - // Hide scrollbars + + /** + * Hide scrollbars in the screenshots. + */ hideScrollBars?: boolean; + // ================ + // Compare options + // ================ + /** - * Compare options + * @deprecated Use `compareOptions` instead. + * Automatically block out the side bar for iPads in landscape mode during comparisons. + * Prevents failures on the tab/private/bookmark native component. */ - // Automatically block out the side bar for iPads in landscape mode during comparisons. This prevents failures on the tab/private/bookmark native component. blockOutSideBar?: boolean; - // Automatically block out the status and address bar during comparisons. This prevents failures on time, wifi or battery status. - // This is mobile only. + + /** + * Automatically block out the status and address bar during comparisons (mobile only). + * Prevents failures due to dynamic UI elements like time, Wi-Fi, battery, etc. + */ blockOutStatusBar?: boolean; - // Automatically block out the tool bar. This is mobile only. + + /** + * Automatically block out the tool bar during comparisons (mobile only). + */ blockOutToolBar?: boolean; - // Create JSON report files with all comparison data, this can be used to create a custom (HTML) report. + + /** + * Generate JSON report files with comparison data (useful for custom HTML reports). + */ createJsonReportFiles?: boolean; - // The proximity of the diff pixels to determine if a diff pixel is part of a group that is used for the JSON report files. - // The higher the number the more pixels will be grouped, the lower the number the less pixels will be grouped due to accuracy. - // Default is 5 pixels + + /** + * Pixel proximity used to group diff pixels in JSON reports. + * + * @default 5 + */ diffPixelBoundingBoxProximity?: number; - // Compare images and discard alpha. + + /** + * Ignore alpha channel when comparing images. + */ ignoreAlpha?: boolean; - // Compare images and discard anti aliasing. + + /** + * Ignore anti-aliasing when comparing images. + */ ignoreAntialiasing?: boolean; - // Even though the images are in colour, the comparison wil compare 2 black/white images + + /** + * Compare two images in black and white only. + */ ignoreColors?: boolean; - // Compare images and compare with `red = 16, green = 16, blue = 16, alpha = 16, minBrightness=16, maxBrightness=240` + + /** + * Compare images with reduced sensitivity. + * red = 16, green = 16, blue = 16, alpha = 16, minBrightness = 16, maxBrightness = 240 + */ ignoreLess?: boolean; - // Compare images and compare with `red = 0, green = 0, blue = 0, alpha = 0, minBrightness=0, maxBrightness=255` + + /** + * Compare images with full sensitivity. + * red = 0, green = 0, blue = 0, alpha = 0, minBrightness = 0, maxBrightness = 255 + */ ignoreNothing?: boolean; - // If true the return percentage will be like `0.12345678`, default is `0.12` + + /** + * Return raw mismatch percentage (e.g., `0.12345678`) instead of rounded (e.g., `0.12`). + */ rawMisMatchPercentage?: boolean; - // This will return all compare data, not only the mismatch percentage + + /** + * Return all compare data, not just mismatch percentage. + */ returnAllCompareData?: boolean; - // Allowable value of misMatchPercentage that prevents saving image with differences + + /** + * Threshold mismatch percentage above which diff image will be saved. + */ saveAboveTolerance?: number; - //Scale images to same size before comparison + + /** + * Scale images to same dimensions before comparison. + */ scaleImagesToSameSize?: boolean; /** - * Tabbable options + * Options object passed to the underlying image comparison engine. + */ + compareOptions?: Partial; + + // ================== + // Tabbable options + // ================== + + /** + * Configuration for tabbable elements highlighting. */ tabbableOptions?: TabbableOptions; /** - * Storybook options + * Storybook-specific visual testing options. */ - storybook?:{ - // If true, the story will be clipped component preventing extraneous whitespace. Enabled by default + storybook?: { + /** + * If true, clips the story component to remove extra whitespace. + * + * @default true + */ clip?: boolean; - // The selector to clip to when clip = true. Defaults to Storybook's V7 root element + + /** + * Selector to use when `clip = true`. Defaults to Storybook's root element. + */ clipSelector?: string; - // Specify the number of separate shards to create, default is 1 - numShards?: number - // Skip stories that match the given string or regex + + /** + * Number of shards to divide tests across. + * + * @default 1 + */ + numShards?: number; + + /** + * Stories to skip, by string or RegExp. + */ skipStories?: string | string[]; - // The URL of the storybook, default will be 'http://127.0.0.1:6006' + + /** + * Storybook server URL. + * + * @default 'http://127.0.0.1:6006' + */ url?: string; - // Version of the storybook, default is 7 - version?: number + /** - * Additional search parameters to be added to the Storybook URL + * Version of Storybook in use. * - * @example { additionalSearchParams: new URLSearchParams({ 'foo': 'bar' }) } - * This will generate the following Storybook URL for stories test: `http://storybook.url/iframe.html?id=story-id&foo=bar` + * @default 7 + */ + version?: number; + + /** + * Additional search parameters for the Storybook URL. * + * @example new URLSearchParams({ 'foo': 'bar' }) + * Results in URL like: `http://storybook.url/iframe.html?id=story-id&foo=bar` */ additionalSearchParams?: URLSearchParams; + /** - * Path builder for the story baseline + * Path builder for saving story baselines. * - * @param category The component category (e.g. "forms" when the story is "Forms/Input") - * @param component The component name (e.g. "input" when the story is "Forms/Input") - * @returns The path where the baseline will be saved, under the `baselineFolder` folder. + * @param category The component category (e.g. "forms"). + * @param component The component name (e.g. "input"). + * @returns Path to store baselines under the `baselineFolder`. * - * @example (category, component) => `${category}__${component}` - * @default (category, component) => `./${category}/${component}/` + * @example + * ```ts + * (category, component) => `${category}__${component}` + * ``` + * + * @default + * ```ts + * (category, component) => `./${category}/${component}/` + * ``` */ getStoriesBaselinePath?: (category: string, component: string) => string; } } export interface DefaultOptions { + /** + * Padding added to the address bar (iOS/Android) to properly cut out the viewport. + */ addressBarShadowPadding: number; + + /** + * Automatically scroll to an element before capturing a screenshot. + */ autoElementScroll: boolean; + + /** + * Adds iOS bezel corners and notch/dynamic island to the screenshot. + */ addIOSBezelCorners: boolean; + + /** + * Automatically saves the image as a baseline if none is found during comparison. + */ autoSaveBaseline: boolean; + + /** + * Delete the runtime folder (`actual` and `diff`) on initialization. + */ clearFolder: boolean; + + /** + * Options used for image comparison. + */ compareOptions: CompareOptions; + + /** + * Use user based fullpage screenshots, by default the desktop screenshots are taken with the BiDi protocol. + */ + userBasedFullPageScreenshot: boolean; + + /** + * Disable the blinking cursor in the screenshot. + */ disableBlinkingCursor: boolean; + + /** + * Disable all CSS animations and input caret. + */ disableCSSAnimation: boolean; + + /** + * Make all text transparent to focus only on layout during comparison. + */ enableLayoutTesting: boolean; + + /** + * A format string for customizing image file names. + */ formatImageName: string; + + /** + * Timeout in milliseconds to wait after scrolling, useful for lazy-loaded pages. + */ fullPageScrollTimeout: number; + + /** + * Hide scrollbars in the screenshot. + */ hideScrollBars: boolean; + + /** + * Indicates whether the app is a hybrid (native + webview). + */ isHybridApp: boolean; + + /** + * Save screenshots per instance in separate folders. + */ savePerInstance: boolean; + + /** + * Options for visualizing tabbable elements. + */ tabbableOptions: TabbableOptions; + + /** + * Padding added to the toolbar (iOS/Android) to properly cut out the viewport. + */ toolBarShadowPadding: number; + + /** + * Wait for fonts to be fully loaded before taking a screenshot. + */ waitForFontsLoaded: boolean; } -interface CompareOptions { +export interface CompareOptions { + /** + * Automatically block out the side bar for iPads in landscape mode during comparisons. + * Prevents failures caused by the tab/private/bookmark native component. + */ blockOutSideBar: boolean; + + /** + * Automatically block out the status and address bar during comparisons. + * This is mobile only and helps avoid failures caused by dynamic content like time, Wi-Fi, or battery status. + */ blockOutStatusBar: boolean; + + /** + * Automatically block out the tool bar during comparisons. + * This is mobile only. + */ blockOutToolBar: boolean; + + /** + * Create JSON report files with all comparison data. + * This can be used to generate custom (HTML) reports. + */ createJsonReportFiles: boolean; + + /** + * Proximity of diff pixels used to group them in the JSON report. + * + * The higher the value, the more pixels will be grouped together. Lower values improve accuracy. + * + * @default 5 + */ diffPixelBoundingBoxProximity: number; + + /** + * Compare images and discard the alpha channel. + */ ignoreAlpha: boolean; + + /** + * Compare images and ignore anti-aliasing effects. + */ ignoreAntialiasing: boolean; + + /** + * Compare two black-and-white versions of the images, ignoring colors. + */ ignoreColors: boolean; + + /** + * Use a less sensitive comparison setting: + * red = 16, green = 16, blue = 16, alpha = 16, minBrightness = 16, maxBrightness = 240 + */ ignoreLess: boolean; + + /** + * Use the most sensitive comparison setting: + * red = 0, green = 0, blue = 0, alpha = 0, minBrightness = 0, maxBrightness = 255 + */ ignoreNothing: boolean; + + /** + * Return the raw mismatch percentage as a decimal (e.g., `0.12345678`), instead of a rounded value (e.g., `0.12`). + */ rawMisMatchPercentage: boolean; + + /** + * Return all comparison data, not just the mismatch percentage. + */ returnAllCompareData: boolean; + + /** + * Mismatch percentage threshold above which the image with differences will be saved. + */ saveAboveTolerance: number; + + /** + * Scale images to the same size before comparing them. + */ scaleImagesToSameSize: boolean; } + diff --git a/packages/webdriver-image-comparison/src/helpers/options.ts b/packages/webdriver-image-comparison/src/helpers/options.ts index 7c4426400..4cb011c34 100644 --- a/packages/webdriver-image-comparison/src/helpers/options.ts +++ b/packages/webdriver-image-comparison/src/helpers/options.ts @@ -1,7 +1,14 @@ -import { DEFAULT_FORMAT_STRING, DEFAULT_SHADOW, DEFAULT_TABBABLE_OPTIONS, FULL_PAGE_SCROLL_TIMEOUT, STORYBOOK_FORMAT_STRING } from './constants.js' +import { + DEFAULT_COMPARE_OPTIONS, + DEFAULT_FORMAT_STRING, + DEFAULT_SHADOW, + DEFAULT_TABBABLE_OPTIONS, + FULL_PAGE_SCROLL_TIMEOUT, + STORYBOOK_FORMAT_STRING, +} from './constants.js' import type { ClassOptions, DefaultOptions } from './options.interfaces.js' import type { MethodImageCompareCompareOptions, ScreenMethodImageCompareCompareOptions } from '../methods/images.interfaces.js' -import { isStorybook } from './utils.js' +import { logAllDeprecatedCompareOptions, isStorybook } from './utils.js' /** * Determine the default options @@ -18,6 +25,7 @@ export function defaultOptions(options: ClassOptions): DefaultOptions { addIOSBezelCorners: options.addIOSBezelCorners ?? false, autoSaveBaseline: options.autoSaveBaseline ?? true, clearFolder: options.clearRuntimeFolder ?? false, + userBasedFullPageScreenshot: options.userBasedFullPageScreenshot ?? false, // Storybook will have it's own default format string formatImageName: options.formatImageName ?? (isStorybook() ? STORYBOOK_FORMAT_STRING : DEFAULT_FORMAT_STRING), isHybridApp: options.isHybridApp ?? false, @@ -41,23 +49,14 @@ export function defaultOptions(options: ClassOptions): DefaultOptions { waitForFontsLoaded: options.waitForFontsLoaded ?? true, /** - * Compare options + * Defining the compare options by overwriting them sequentially: + * First the default ones (fallback), then the root compareOptions (deprecated), then the ones from + * the `options.compareOptions` */ compareOptions: { - blockOutSideBar: options.blockOutSideBar ?? true, - blockOutStatusBar: options.blockOutStatusBar ?? true, - blockOutToolBar: options.blockOutToolBar ?? true, - createJsonReportFiles: options.createJsonReportFiles ?? false, - diffPixelBoundingBoxProximity: options.diffPixelBoundingBoxProximity ?? 5, - ignoreAlpha: options.ignoreAlpha ?? false, - ignoreAntialiasing: options.ignoreAntialiasing ?? false, - ignoreColors: options.ignoreColors ?? false, - ignoreLess: options.ignoreLess ?? false, - ignoreNothing: options.ignoreNothing ?? false, - rawMisMatchPercentage: options.rawMisMatchPercentage ?? false, - returnAllCompareData: options.returnAllCompareData ?? false, - saveAboveTolerance: options.saveAboveTolerance ?? 0, - scaleImagesToSameSize: options.scaleImagesToSameSize ?? false, + ...DEFAULT_COMPARE_OPTIONS, + ...logAllDeprecatedCompareOptions(options), + ...(options.compareOptions ? options.compareOptions : {}), }, /** diff --git a/packages/webdriver-image-comparison/src/helpers/utils.interfaces.ts b/packages/webdriver-image-comparison/src/helpers/utils.interfaces.ts index db59e9d5d..63ebe0800 100644 --- a/packages/webdriver-image-comparison/src/helpers/utils.interfaces.ts +++ b/packages/webdriver-image-comparison/src/helpers/utils.interfaces.ts @@ -1,3 +1,6 @@ +import type { Executor } from '../methods/methods.interfaces.js' +import type { DeviceRectangles } from '../methods/rectangles.interfaces.js' + export interface GetAndCreatePathOptions { // The name of the browser browserName: string; @@ -99,3 +102,25 @@ export interface ScreenshotSize { height: number; width: number; } + +export interface GetMobileViewPortPositionOptions { + initialDeviceRectangles: DeviceRectangles, + isNativeContext: boolean, + isAndroid: boolean, + isIOS: boolean, + methods: { + executor: Executor, + getUrl: () => Promise, + url: (arg:string) => Promise, + } + nativeWebScreenshot: boolean, + screenHeight: number, + screenWidth: number, +} + +export interface GetMobileScreenSizeOptions { + currentBrowser: WebdriverIO.Browser, + executor: Executor, + isIOS: boolean, + isNativeContext: boolean, +} diff --git a/packages/webdriver-image-comparison/src/helpers/utils.test.ts b/packages/webdriver-image-comparison/src/helpers/utils.test.ts index bbf63fd4c..46d91cecc 100644 --- a/packages/webdriver-image-comparison/src/helpers/utils.test.ts +++ b/packages/webdriver-image-comparison/src/helpers/utils.test.ts @@ -1,6 +1,7 @@ -import { describe, it, expect, afterEach } from 'vitest' +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest' import { existsSync, rmSync } from 'node:fs' import { join } from 'node:path' +import logger from '@wdio/logger' import { calculateDprData, checkAndroidChromeDriverScreenshot, @@ -10,14 +11,44 @@ import { checkIsMobile, checkTestInBrowser, checkTestInMobileBrowser, + executeNativeClick, formatFileName, getAddressBarShadowPadding, getAndCreatePath, - getScreenshotSize, + getBase64ScreenshotSize, + getDevicePixelRatio, + getIosBezelImageNames, + getMobileScreenSize, + getMobileViewPortPosition, getToolBarShadowPadding, + isObject, + isStorybook, + loadBase64Html, + logAllDeprecatedCompareOptions, + updateVisualBaseline, } from './utils.js' import type { FormatFileNameOptions, GetAndCreatePathOptions } from './utils.interfaces.js' import { IMAGE_STRING } from '../mocks/mocks.js' +import { DEVICE_RECTANGLES } from './constants.js' +import { injectWebviewOverlay } from '../clientSideScripts/injectWebviewOverlay.js' +import { getMobileWebviewClickAndDimensions } from '../clientSideScripts/getMobileWebviewClickAndDimensions.js' +import { checkMetaTag } from '../clientSideScripts/checkMetaTag.js' +import type { ClassOptions } from './options.interfaces.js' + +vi.mock('../clientSideScripts/injectWebviewOverlay.js', () => ({ + injectWebviewOverlay: Symbol('injectWebviewOverlay'), +})) + +vi.mock('../clientSideScripts/getMobileWebviewClickAndDimensions.js', () => ({ + getMobileWebviewClickAndDimensions: Symbol('getMobileWebviewClickAndDimensions'), +})) + +vi.mock('../clientSideScripts/checkMetaTag.js', () => ({ + checkMetaTag: Symbol('checkMetaTag'), +})) + +const log = logger('test') +vi.mock('@wdio/logger', () => import(join(process.cwd(), '__mocks__', '@wdio/logger'))) describe('utils', () => { describe('getAndCreatePath', () => { @@ -408,13 +439,434 @@ describe('utils', () => { // }); // }); - describe('getScreenshotSize', () => { + describe('getBase64ScreenshotSize', () => { it('should get the screenshot size of a screenshot string with the default DPR', () => { - expect(getScreenshotSize(IMAGE_STRING)).toMatchSnapshot() + expect(getBase64ScreenshotSize(IMAGE_STRING)).toMatchSnapshot() }) it('should get the screenshot size of a screenshot string with DRP 2', () => { - expect(getScreenshotSize(IMAGE_STRING, 2)).toMatchSnapshot() + expect(getBase64ScreenshotSize(IMAGE_STRING, 2)).toMatchSnapshot() + }) + }) + + describe('getDevicePixelRatio', () => { + it('should return 1 when the screenshot width equals device screen width', () => { + const deviceScreenSize = { width: 32, height: 64 } + expect(getDevicePixelRatio(IMAGE_STRING, deviceScreenSize)).toMatchSnapshot() + }) + + it('should return 2 when the screenshot width is double the device screen width', () => { + const deviceScreenSize = { width: 16, height: 32 } + expect(getDevicePixelRatio(IMAGE_STRING, deviceScreenSize)).toMatchSnapshot() + }) + + it('should round the result to the nearest integer', () => { + const deviceScreenSize = { width: 17, height: 32 } + expect(getDevicePixelRatio(IMAGE_STRING, deviceScreenSize)).toMatchSnapshot() + }) + }) + + describe('getIosBezelImageNames', () => { + const supportedDevices = [ + 'iphonex', + 'iphonexs', + 'iphonexsmax', + 'iphonexr', + 'iphone11', + 'iphone11pro', + 'iphone11promax', + 'iphone12', + 'iphone12mini', + 'iphone12pro', + 'iphone12promax', + 'iphone13', + 'iphone13mini', + 'iphone13pro', + 'iphone13promax', + 'iphone14', + 'iphone14plus', + 'iphone14pro', + 'iphone14promax', + 'iphone15', + 'ipadmini', + 'ipadair', + 'ipadpro11', + 'ipadpro129', + ] + + supportedDevices.forEach((device) => { + it(`should return bezel image names for "${device}"`, () => { + expect(getIosBezelImageNames(device)).toMatchSnapshot() + }) + }) + + it('should throw an error for unsupported device names', () => { + expect(() => getIosBezelImageNames('unsupportedDevice')).toThrowErrorMatchingSnapshot() + }) + }) + + describe('isObject', () => { + it('should return true for a plain object', () => { + expect(isObject({})).toBe(true) + }) + + it('should return true for a function', () => { + expect(isObject(() => {})).toBe(true) + }) + + it('should return false for null', () => { + expect(isObject(null)).toBe(false) + }) + + it('should return false for undefined', () => { + expect(isObject(undefined)).toBe(false) + }) + + it('should return false for a string', () => { + expect(isObject('string')).toBe(false) + }) + + it('should return false for a number', () => { + expect(isObject(123)).toBe(false) + }) + + it('should return false for a boolean', () => { + expect(isObject(true)).toBe(false) + }) + + it('should return true for an array (since typeof array is object)', () => { + expect(isObject([])).toBe(true) + }) + }) + + describe('isStorybook', () => { + const originalArgv = [...process.argv] + + afterEach(() => { + process.argv = [...originalArgv] + }) + + it('should return true when "--storybook" is in process.argv', () => { + process.argv.push('--storybook') + expect(isStorybook()).toBe(true) + }) + + it('should return false when "--storybook" is not in process.argv', () => { + process.argv = originalArgv.filter(arg => arg !== '--storybook') + expect(isStorybook()).toBe(false) + }) + }) + + describe('updateVisualBaseline', () => { + const originalArgv = [...process.argv] + + afterEach(() => { + process.argv = [...originalArgv] + }) + + it('should return true when "--update-visual-baseline" is in process.argv', () => { + process.argv.push('--update-visual-baseline') + expect(updateVisualBaseline()).toBe(true) + }) + + it('should return false when "--update-visual-baseline" is not in process.argv', () => { + process.argv = originalArgv.filter(arg => arg !== '--update-visual-baseline') + expect(updateVisualBaseline()).toBe(false) + }) + }) + + describe('getMobileScreenSize', () => { + afterEach(() => { + vi.restoreAllMocks() + }) + + it('returns iOS screen size in portrait', async () => { + const executor = vi.fn().mockResolvedValue({ + screenSize: { width: 390, height: 844 }, + }) + const browser = { getOrientation: vi.fn().mockResolvedValue('PORTRAIT') } as any + + const result = await getMobileScreenSize({ executor, currentBrowser: browser, isIOS: true, isNativeContext: true }) + + expect(result).toEqual({ width: 390, height: 844 }) + }) + + it('returns iOS screen size in landscape', async () => { + const executor = vi.fn().mockResolvedValue({ + screenSize: { width: 390, height: 844 }, + }) + const browser = { getOrientation: vi.fn().mockResolvedValue('LANDSCAPE') } as any + + const result = await getMobileScreenSize({ executor, currentBrowser: browser, isIOS: true, isNativeContext: true }) + + expect(result).toEqual({ width: 844, height: 390 }) + }) + + it('returns Android screen size in portrait', async () => { + const executor = vi.fn().mockResolvedValue({ realDisplaySize: '1080x2400' }) + const browser = { getOrientation: vi.fn().mockResolvedValue('PORTRAIT') } as any + + const result = await getMobileScreenSize({ executor, currentBrowser: browser, isIOS: false, isNativeContext: true }) + + expect(result).toEqual({ width: 1080, height: 2400 }) + }) + + it('falls back for iOS when screenSize is missing (web context)', async () => { + const executor = vi.fn() + .mockRejectedValueOnce(new Error('Missing screenSize')) + .mockResolvedValueOnce({ width: 800, height: 1200 }) + const warnSpy = vi.spyOn(log, 'warn') + const browser = { + getOrientation: vi.fn().mockResolvedValue('PORTRAIT'), + } as any + + const result = await getMobileScreenSize({ executor, currentBrowser: browser, isIOS: true, isNativeContext: false }) + + expect(warnSpy).toHaveBeenCalled() + expect(result).toEqual({ width: 800, height: 1200 }) + }) + + it('falls back for Android when realDisplaySize is invalid (web context)', async () => { + const executor = vi.fn() + .mockResolvedValueOnce({ realDisplaySize: 'invalid' }) + .mockResolvedValueOnce({ width: 800, height: 1200 }) + const warnSpy = vi.spyOn(log, 'warn') + const browser = { + getOrientation: vi.fn().mockResolvedValue('PORTRAIT'), + } as any + + const result = await getMobileScreenSize({ executor, currentBrowser: browser, isIOS: false, isNativeContext: false }) + + expect(warnSpy).toHaveBeenCalled() + expect(result).toEqual({ width: 800, height: 1200 }) + }) + + it('falls back to getWindowSize in native context', async () => { + const executor = vi.fn().mockRejectedValueOnce(new Error('Boom')) + const warnSpy = vi.spyOn(log, 'warn') + const browser = { + getOrientation: vi.fn().mockResolvedValue('PORTRAIT'), + getWindowSize: vi.fn().mockResolvedValue({ width: 123, height: 456 }) + } as any + + const result = await getMobileScreenSize({ executor, currentBrowser: browser, isIOS: true, isNativeContext: true }) + + expect(result).toEqual({ width: 123, height: 456 }) + expect(warnSpy).toHaveBeenCalled() + }) + + it('returns dimensions in landscape fallback native context', async () => { + const executor = vi.fn().mockRejectedValueOnce(new Error('Boom')) + const warnSpy = vi.spyOn(log, 'warn') + const browser = { + getOrientation: vi.fn().mockResolvedValue('LANDSCAPE'), + getWindowSize: vi.fn().mockResolvedValue({ width: 123, height: 456 }) + } as any + + const result = await getMobileScreenSize({ executor, currentBrowser: browser, isIOS: true, isNativeContext: true }) + + expect(result).toEqual({ width: 456, height: 123 }) + expect(warnSpy).toHaveBeenCalled() + }) + }) + + describe('loadBase64Html', () => { + const mockUrl = vi.fn() + const mockExecutor = vi.fn() + + afterEach(() => { + vi.clearAllMocks() + }) + + it('should call url with base64 html and skip executor for Android', async () => { + await loadBase64Html({ executor: mockExecutor, isIOS: false, url: mockUrl }) + + expect(mockUrl).toHaveBeenCalledTimes(1) + expect(mockUrl.mock.calls[0][0]).toMatch(/^data:text\/html;base64,/) + expect(mockExecutor).not.toHaveBeenCalled() + }) + + it('should call url with base64 html and call executor for iOS', async () => { + await loadBase64Html({ executor: mockExecutor, isIOS: true, url: mockUrl }) + + expect(mockUrl).toHaveBeenCalledTimes(1) + expect(mockUrl.mock.calls[0][0]).toMatch(/^data:text\/html;base64,/) + expect(mockExecutor).toHaveBeenCalledWith(checkMetaTag) + }) + }) + + describe('executeNativeClick', () => { + const coords = { x: 100, y: 200 } + + afterEach(() => { + vi.clearAllMocks() + }) + + it('should call executor with "mobile: tap" on iOS', async () => { + const executor = vi.fn() + await executeNativeClick({ executor, isIOS: true, ...coords }) + + expect(executor).toHaveBeenCalledWith('mobile: tap', coords) + }) + + it('should call executor with "mobile: clickGesture" on Android (Appium 2)', async () => { + const executor = vi.fn() + await executeNativeClick({ executor, isIOS: false, ...coords }) + + expect(executor).toHaveBeenCalledWith('mobile: clickGesture', coords) + }) + + it('should fall back to "doubleClickGesture" when clickGesture fails (Appium 1)', async () => { + const executor = vi.fn() + .mockRejectedValueOnce(new Error('WebDriverError: Unknown mobile command: clickGesture')) + .mockResolvedValueOnce(undefined) + const logWarnMock = vi.spyOn(log, 'warn') + + await executeNativeClick({ executor, isIOS: false, ...coords }) + + expect(executor).toHaveBeenCalledWith('mobile: clickGesture', coords) + expect(executor).toHaveBeenCalledWith('mobile: doubleClickGesture', coords) + expect(logWarnMock).toHaveBeenCalledWith(expect.stringContaining('falling back to `doubleClickGesture`')) + + logWarnMock.mockRestore() + }) + + it('should throw the error if it’s not a known Appium command error', async () => { + const executor = vi.fn().mockRejectedValueOnce(new Error('Some unexpected error')) + + await expect(executeNativeClick({ executor, isIOS: false, ...coords })) + .rejects + .toThrowError('Some unexpected error') + }) + }) + + describe('getMobileViewPortPosition', () => { + const mockExecutor = vi.fn() + const mockUrl = vi.fn() + const mockGetUrl = vi.fn().mockResolvedValue('http://example.com') + + const baseOptions = { + isAndroid: false, + isIOS: true, + isNativeContext: false, + nativeWebScreenshot: true, + screenHeight: 800, + screenWidth: 400, + methods: { + executor: mockExecutor, + url: mockUrl, + getUrl: mockGetUrl, + }, + } + + beforeEach(() => { + vi.clearAllMocks() + }) + + it('should return correct device rectangles for iOS WebView flow', async () => { + mockExecutor + .mockResolvedValueOnce(undefined) // checkMetaTag (loadBase64Html) + .mockResolvedValueOnce(undefined) // injectWebviewOverlay + .mockResolvedValueOnce(undefined) // nativeClick + .mockResolvedValueOnce({ x: 150, y: 300, width: 100, height: 100 }) // getMobileWebviewClickAndDimensions + + const result = await getMobileViewPortPosition({ + ...baseOptions, + initialDeviceRectangles: DEVICE_RECTANGLES, + }) + + expect(mockGetUrl).toHaveBeenCalled() + expect(mockUrl).toHaveBeenCalledTimes(2) + expect(mockExecutor).toHaveBeenCalledWith(injectWebviewOverlay, false) + expect(mockExecutor).toHaveBeenCalledWith(getMobileWebviewClickAndDimensions, '[data-test="ics-overlay"]') + + expect(result).toMatchSnapshot() + }) + + it('should return initialDeviceRectangles if not WebView (native context)', async () => { + const result = await getMobileViewPortPosition({ + ...baseOptions, + isNativeContext: true, + initialDeviceRectangles: DEVICE_RECTANGLES, + }) + + expect(result).toEqual(DEVICE_RECTANGLES) + expect(mockExecutor).not.toHaveBeenCalled() + }) + + it('should return initialDeviceRectangles if Android + not nativeWebScreenshot', async () => { + const result = await getMobileViewPortPosition({ + ...baseOptions, + isAndroid: true, + isIOS: false, + nativeWebScreenshot: false, + initialDeviceRectangles: DEVICE_RECTANGLES, + }) + + expect(result).toEqual(DEVICE_RECTANGLES) + }) + }) + + describe('logAllDeprecatedCompareOptions', () => { + const allDeprecatedOptions = { + blockOutSideBar: true, + blockOutStatusBar: true, + blockOutToolBar: true, + createJsonReportFiles: true, + diffPixelBoundingBoxProximity: 5, + ignoreAlpha: true, + ignoreAntialiasing: true, + ignoreColors: true, + ignoreLess: true, + ignoreNothing: true, + rawMisMatchPercentage: true, + returnAllCompareData: true, + saveAboveTolerance: 100, + scaleImagesToSameSize: true, + } + + it('should log a deprecation warning for each deprecated key', () => { + const warnSpy = vi.spyOn(log, 'warn').mockImplementation(() => {}) + + logAllDeprecatedCompareOptions(allDeprecatedOptions) + + expect(warnSpy).toHaveBeenCalledTimes(1) + expect(warnSpy.mock.calls[0][0]).toMatchSnapshot() + }) + + it('should return a subset of CompareOptions with deprecated keys only', () => { + const result = logAllDeprecatedCompareOptions(allDeprecatedOptions) + expect(result).toMatchSnapshot() + }) + + it('should only return deprecated keys when full config is provided', () => { + const fullOptions: ClassOptions = { + addressBarShadowPadding: 10, + autoElementScroll: true, + addIOSBezelCorners: false, + clearRuntimeFolder: false, + userBasedFullPageScreenshot: false, + formatImageName: 'test', + isHybridApp: false, + savePerInstance: true, + toolBarShadowPadding: 5, + waitForFontsLoaded: true, + autoSaveBaseline: true, + screenshotPath: './screenshots', + baselineFolder: './baseline', + disableBlinkingCursor: false, + disableCSSAnimation: false, + enableLayoutTesting: true, + fullPageScrollTimeout: 500, + hideScrollBars: true, + storybook: { url: 'http://localhost:6006' }, + + // Add deprecated keys mixed in + ...allDeprecatedOptions + } + + const result = logAllDeprecatedCompareOptions(fullOptions) + expect(result).toEqual(allDeprecatedOptions) }) }) }) diff --git a/packages/webdriver-image-comparison/src/helpers/utils.ts b/packages/webdriver-image-comparison/src/helpers/utils.ts index 6be872b1e..2785bd0be 100644 --- a/packages/webdriver-image-comparison/src/helpers/utils.ts +++ b/packages/webdriver-image-comparison/src/helpers/utils.ts @@ -1,3 +1,4 @@ +import logger from '@wdio/logger' import { join } from 'node:path' import { DESKTOP, NOT_KNOWN, PLATFORMS } from './constants.js' import { mkdirSync } from 'node:fs' @@ -6,9 +7,19 @@ import type { FormatFileNameOptions, GetAddressBarShadowPaddingOptions, GetAndCreatePathOptions, + GetMobileScreenSizeOptions, + GetMobileViewPortPositionOptions, GetToolBarShadowPaddingOptions, ScreenshotSize, } from './utils.interfaces.js' +import type { ClassOptions, CompareOptions } from './options.interfaces.js' +import type { Executor } from '../methods/methods.interfaces.js' +import { checkMetaTag } from '../clientSideScripts/checkMetaTag.js' +import { injectWebviewOverlay } from '../clientSideScripts/injectWebviewOverlay.js' +import { getMobileWebviewClickAndDimensions } from '../clientSideScripts/getMobileWebviewClickAndDimensions.js' +import type { DeviceRectangles } from '../methods/rectangles.interfaces.js' + +const log = logger('@wdio/visual-service:webdriver-image-comparison:utils') /** * Get and create a folder @@ -140,12 +151,12 @@ export function getAddressBarShadowPadding(options: GetAddressBarShadowPaddingOp } /** - * Get the tool bar shadow padding. This is only needed for iOS + * Get the tool bar shadow padding. Add some extra padding for iOS when we have a home bar */ export function getToolBarShadowPadding(options: GetToolBarShadowPaddingOptions): number { const { platformName, browserName, toolBarShadowPadding, addShadowPadding } = options - return checkTestInMobileBrowser(platformName, browserName) && checkIsIos(platformName) && addShadowPadding + return checkTestInMobileBrowser(platformName, browserName) && addShadowPadding ? checkIsIos(platformName) ? // The 9 extra are for iOS home bar for iPhones with a notch or iPads with a home bar toolBarShadowPadding + 9 @@ -175,7 +186,7 @@ export async function waitFor(milliseconds: number): Promise { /** * Get the size of a screenshot in pixels without the device pixel ratio */ -export function getScreenshotSize(screenshot: string, devicePixelRation = 1): ScreenshotSize { +export function getBase64ScreenshotSize(screenshot: string, devicePixelRation = 1): ScreenshotSize { return { height: Math.round(Buffer.from(screenshot, 'base64').readUInt32BE(20) / devicePixelRation), width: Math.round(Buffer.from(screenshot, 'base64').readUInt32BE(16) / devicePixelRation), @@ -186,7 +197,7 @@ export function getScreenshotSize(screenshot: string, devicePixelRation = 1): Sc * Get the device pixel ratio */ export function getDevicePixelRatio(screenshot: string, deviceScreenSize: {height:number, width: number}): number { - const screenshotSize = getScreenshotSize(screenshot) + const screenshotSize = getBase64ScreenshotSize(screenshot) const devicePixelRatio = screenshotSize.width / deviceScreenSize.width return Math.round(devicePixelRatio) @@ -325,3 +336,233 @@ export function isStorybook(){ export function updateVisualBaseline(): boolean { return process.argv.includes('--update-visual-baseline') } +/** + * Log the deprecated root compareOptions (at `ClassOptions` level) + * and returns non-undefined ones to be added back to the config + */ +export function logAllDeprecatedCompareOptions(options: ClassOptions) { + const deprecatedKeys: (keyof CompareOptions)[] = [ + 'blockOutSideBar', + 'blockOutStatusBar', + 'blockOutToolBar', + 'createJsonReportFiles', + 'diffPixelBoundingBoxProximity', + 'ignoreAlpha', + 'ignoreAntialiasing', + 'ignoreColors', + 'ignoreLess', + 'ignoreNothing', + 'rawMisMatchPercentage', + 'returnAllCompareData', + 'saveAboveTolerance', + 'scaleImagesToSameSize', + ] + const foundDeprecatedKeys = deprecatedKeys.filter((key) => key in options) + + if (foundDeprecatedKeys.length > 0) { + log.warn( + 'The following root-level compare options are deprecated and should be moved under \'compareOptions\':\n' + + foundDeprecatedKeys.map((k) => ` - ${k}`).join('\n') + '\nIn the next major version, these options will be removed from the root level and only be available under \'compareOptions\'', + ) + } + + return foundDeprecatedKeys.reduce>((acc, key) => { + if (options[key] !== undefined) { + acc[key] = options[key] as any + } + return acc + }, {}) +} + +/** + * Get the mobile screen size, this is different for native and webview + */ +export async function getMobileScreenSize({ + currentBrowser, + executor, + isIOS, + isNativeContext, +}: GetMobileScreenSizeOptions): Promise<{ height: number; width: number }> { + let height = 0, width = 0 + const isLandscapeByOrientation = (await currentBrowser.getOrientation()).toUpperCase() === 'LANDSCAPE' + + try { + if (isIOS) { + ({ screenSize: { height, width } } = (await executor('mobile: deviceScreenInfo')) as { + statusBarSize: { width: number, height: number }, + scale: number, + screenSize: { width: number, height: number }, + }) + // It's Android + } else { + const { realDisplaySize } = (await executor('mobile: deviceInfo')) as { realDisplaySize: string } + + if (!realDisplaySize || !/^\d+x\d+$/.test(realDisplaySize)) { + throw new Error(`Invalid realDisplaySize format. Expected 'widthxheight', got "${realDisplaySize}"`) + } + [width, height] = realDisplaySize.split('x').map(Number) + } + } catch (error: unknown) { + log.warn('Error getting mobile screen size:\n', error, `\nFalling back to ${isNativeContext ? + '`getWindowSize()` which might not be as accurate' : + 'window.screen.height and window.screen.width'}` + ) + + if (isNativeContext) { + ({ height, width } = await currentBrowser.getWindowSize()) + } else { + // This is a fallback and not 100% accurate, but we need to have something =) + ({ height, width } = await executor(() => { + const { height, width } = window.screen + return { height, width } + })) + } + } + + // There are issues where the landscape mode by orientation is not the same as the landscape mode by value + // So we need to check and fix this + const isLandscapeByValue = width > height + if (isLandscapeByOrientation !== isLandscapeByValue) { + [height, width] = [width, height] + } + + return { height, width } +} + +/** + * Load a base64 HTML page in the browser + */ +export async function loadBase64Html({ executor, isIOS, url }: {executor:Executor, isIOS:boolean, url:any}): Promise { + const htmlContent = ` + + + Base64 Page + + + + +

Hello from Base64!

+

This page was loaded without visiting a URL.

+ + ` + + const base64Html = Buffer.from(htmlContent).toString('base64') + + await url(`data:text/html;base64,${base64Html}`) + + if (isIOS) { + await executor(checkMetaTag) + } +} + +/** + * Execute a native click + */ +export async function executeNativeClick({ executor, isIOS, x, y }:{executor: Executor, isIOS:boolean, x: number, y: number}): Promise { + if (isIOS) { + return executor('mobile: tap', { x, y }) + } + + try { + // The `clickGesture` is not working on Appium 1, only on Appium 2 + await executor('mobile: clickGesture', { x, y }) + } catch (error: unknown) { + if ( + error instanceof Error && + /WebDriverError: Unknown mobile command.*?(clickGesture|tap)/i.test(error.message) + ) { + log.warn( + 'Error executing `clickGesture`, falling back to `doubleClickGesture`. This likely means you are using Appium 1. Is this intentional?' + ) + await executor('mobile: doubleClickGesture', { x, y }) + } else { + throw error + } + } +} + +/** + * Get the mobile viewport position, we determine this by: + * 1. Loading a base64 HTML page + * 2. Injecting an overlay on top of the webview with an event listener that stores the click position in the webview + * 3. Clicking on the overlay in the center of the screen with a native click + * 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 + */ +export async function getMobileViewPortPosition({ + initialDeviceRectangles, + isAndroid, + isIOS, + isNativeContext, + methods: { + executor, + getUrl, + url, + }, + nativeWebScreenshot, + screenHeight, + screenWidth, +}: GetMobileViewPortPositionOptions): Promise { + + if (!isNativeContext && (isIOS || (isAndroid && nativeWebScreenshot))) { + const currentUrl = await getUrl() + // 1. Load a base64 HTML page + await loadBase64Html({ executor, isIOS, url }) + // 2. Inject an overlay on top of the webview with an event listener that stores the click position in the webview + await executor(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({ executor, 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 executor(getMobileWebviewClickAndDimensions, '[data-test="ics-overlay"]') + // 4.b reset the url + await 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 + } + + // No WebView detected, return empty values + return initialDeviceRectangles +} + +/** + * Get the value of a method or the default value + */ +export function getMethodOrWicOption( + method: Partial | undefined, + wic: T, + key: K +): T[K] { + return method?.[key] ?? wic[key] +} diff --git a/packages/webdriver-image-comparison/src/index.ts b/packages/webdriver-image-comparison/src/index.ts index 3eaaecd85..a9315d9c8 100644 --- a/packages/webdriver-image-comparison/src/index.ts +++ b/packages/webdriver-image-comparison/src/index.ts @@ -9,7 +9,8 @@ import checkFullPageScreen from './commands/checkFullPageScreen.js' import checkTabbablePage from './commands/checkTabbablePage.js' import { ClassOptions } from './helpers/options.interfaces.js' import { ImageCompareResult } from './methods/images.interfaces.js' -import { IOS_OFFSETS, FOLDERS, DEFAULT_TEST_CONTEXT } from './helpers/constants.js' +import { DEFAULT_TEST_CONTEXT, DEVICE_RECTANGLES, IOS_OFFSETS, FOLDERS, NOT_KNOWN } from './helpers/constants.js' +import { getMobileScreenSize, getMobileViewPortPosition } from './helpers/utils.js' export type { ScreenshotOutput } from './helpers/afterScreenshot.interfaces.js' export type { @@ -28,6 +29,7 @@ export type { export type { TestContext } from './commands/check.interfaces.js' export type { Folders } from './base.interfaces.js' export type { InstanceData } from './methods/instanceData.interfaces.js' +export type { DeviceRectangles } from './methods/rectangles.interfaces.js' export type { ResultReport } from './methods/createCompareReport.js' export { @@ -35,8 +37,10 @@ export { ClassOptions, ImageCompareResult, DEFAULT_TEST_CONTEXT, + DEVICE_RECTANGLES, IOS_OFFSETS, FOLDERS, + NOT_KNOWN, saveScreen, saveElement, saveFullPageScreen, @@ -45,4 +49,6 @@ export { checkElement, checkFullPageScreen, checkTabbablePage, + getMobileScreenSize, + getMobileViewPortPosition, } diff --git a/packages/webdriver-image-comparison/src/methods/__snapshots__/rectangles.test.ts.snap b/packages/webdriver-image-comparison/src/methods/__snapshots__/rectangles.test.ts.snap index f78e0f9aa..e1e5e8f55 100644 --- a/packages/webdriver-image-comparison/src/methods/__snapshots__/rectangles.test.ts.snap +++ b/packages/webdriver-image-comparison/src/methods/__snapshots__/rectangles.test.ts.snap @@ -11,10 +11,10 @@ exports[`rectangles > determineElementRectangles > should determine them for And exports[`rectangles > determineElementRectangles > should determine them for Android Native webscreenshot 1`] = ` { - "height": 360, - "width": 360, - "x": 300, - "y": 30, + "height": 900, + "width": 600, + "x": 1200, + "y": 630, } `; @@ -31,8 +31,8 @@ exports[`rectangles > determineElementRectangles > should determine them for iOS { "height": 240, "width": 240, - "x": 200, - "y": 20, + "x": 260, + "y": 60, } `; @@ -63,52 +63,33 @@ exports[`rectangles > determineScreenRectangles > should determine them for iOS } `; -exports[`rectangles > determineStatusAddressToolBarRectangles > should determine the rectangles for Android with a status and toolbar blockout 1`] = ` +exports[`rectangles > determineStatusAddressToolBarRectangles > should determine the rectangles that there are no rectangles for this device 1`] = `[]`; + +exports[`rectangles > determineStatusAddressToolBarRectangles > should determine the rectangles with a status and toolbar blockout 1`] = ` [ { - "height": 40, - "width": 320, + "height": 320, + "width": 1344, "x": 0, "y": 0, }, { - "height": 100, - "width": 320, + "height": 71, + "width": 1344, "x": 0, - "y": 600, + "y": 2921, }, { - "height": 0, + "height": 2601, "width": 0, "x": 0, - "y": 0, - }, -] -`; - -exports[`rectangles > determineStatusAddressToolBarRectangles > should determine the rectangles for the iOS with a status and toolbar blockout 1`] = ` -[ - { - "height": 94, - "width": 375, - "x": 0, - "y": 0, + "y": 320, }, { - "height": 5, - "width": 135, - "x": 120, - "y": 799, - }, - { - "height": 240, - "width": 120, - "x": 0, - "y": 70, + "height": 2601, + "width": 0, + "x": 1344, + "y": 320, }, ] `; - -exports[`rectangles > determineStatusAddressToolBarRectangles > should determine the rectangles for the iOS without a status and toolbar blockout 1`] = `[]`; - -exports[`rectangles > determineStatusAddressToolBarRectangles > should determine the rectangles that there are no rectangles for this device 1`] = `[]`; diff --git a/packages/webdriver-image-comparison/src/methods/__snapshots__/screenshots.test.ts.snap b/packages/webdriver-image-comparison/src/methods/__snapshots__/screenshots.test.ts.snap index 87e0291c3..041dab3c6 100644 --- a/packages/webdriver-image-comparison/src/methods/__snapshots__/screenshots.test.ts.snap +++ b/packages/webdriver-image-comparison/src/methods/__snapshots__/screenshots.test.ts.snap @@ -4,26 +4,26 @@ exports[`screenshots > getBase64FullPageScreenshotsData > should get hide elemen { "data": [ { - "canvasWidth": 2732, + "canvasWidth": 1366, "canvasYPosition": 0, - "imageHeight": 1176, - "imageWidth": 2732, + "imageHeight": 756, + "imageWidth": 1366, "imageXPosition": 0, - "imageYPosition": 124, + "imageYPosition": 6, "screenshot": "mocked-screenshot-string", }, { - "canvasWidth": 2732, - "canvasYPosition": 1176, - "imageHeight": 400, - "imageWidth": 2732, + "canvasWidth": 1366, + "canvasYPosition": 756, + "imageHeight": 20, + "imageWidth": 1366, "imageXPosition": 0, - "imageYPosition": 924, + "imageYPosition": 742, "screenshot": "mocked-screenshot-string", }, ], - "fullPageHeight": 1552, - "fullPageWidth": 2732, + "fullPageHeight": 776, + "fullPageWidth": 1366, } `; @@ -58,17 +58,26 @@ exports[`screenshots > getBase64FullPageScreenshotsData > should get the Android { "data": [ { - "canvasWidth": 2732, + "canvasWidth": 1366, "canvasYPosition": 0, - "imageHeight": 1576, - "imageWidth": 2732, + "imageHeight": 756, + "imageWidth": 1366, "imageXPosition": 0, - "imageYPosition": 148, + "imageYPosition": 6, + "screenshot": "mocked-screenshot-string", + }, + { + "canvasWidth": 1366, + "canvasYPosition": 756, + "imageHeight": 20, + "imageWidth": 1366, + "imageXPosition": 0, + "imageYPosition": 742, "screenshot": "mocked-screenshot-string", }, ], - "fullPageHeight": 1552, - "fullPageWidth": 2732, + "fullPageHeight": 776, + "fullPageWidth": 1366, } `; @@ -132,19 +141,19 @@ exports[`screenshots > getBase64FullPageScreenshotsData > should get the iOS ful { "canvasWidth": 2732, "canvasYPosition": 0, - "imageHeight": 1576, + "imageHeight": 1512, "imageWidth": 2732, "imageXPosition": 0, - "imageYPosition": 200, + "imageYPosition": 12, "screenshot": "mocked-screenshot-string", }, { "canvasWidth": 2732, - "canvasYPosition": 1576, - "imageHeight": 824, + "canvasYPosition": 1512, + "imageHeight": 864, "imageWidth": 2732, "imageXPosition": 0, - "imageYPosition": 976, + "imageYPosition": 660, "screenshot": "mocked-screenshot-string", }, ], @@ -157,26 +166,17 @@ exports[`screenshots > getBase64FullPageScreenshotsData > should get the iOS ful { "data": [ { - "canvasWidth": 2412, + "canvasWidth": 2732, "canvasYPosition": 0, - "imageHeight": 776, - "imageWidth": 2412, - "imageXPosition": 320, - "imageYPosition": 106, - "screenshot": "mocked-screenshot-string", - }, - { - "canvasWidth": 2412, - "canvasYPosition": 776, - "imageHeight": 424, - "imageWidth": 2412, - "imageXPosition": 320, - "imageYPosition": 482, + "imageHeight": 1176, + "imageWidth": 2732, + "imageXPosition": 0, + "imageYPosition": 348, "screenshot": "mocked-screenshot-string", }, ], "fullPageHeight": 1176, - "fullPageWidth": 2412, + "fullPageWidth": 2732, } `; @@ -267,19 +267,19 @@ exports[`screenshots > getBase64FullPageScreenshotsData > should hide elements f { "canvasWidth": 2732, "canvasYPosition": 0, - "imageHeight": 1576, + "imageHeight": 1512, "imageWidth": 2732, "imageXPosition": 0, - "imageYPosition": 200, + "imageYPosition": 12, "screenshot": "mocked-screenshot-string", }, { "canvasWidth": 2732, - "canvasYPosition": 1576, - "imageHeight": 824, + "canvasYPosition": 1512, + "imageHeight": 864, "imageWidth": 2732, "imageXPosition": 0, - "imageYPosition": 976, + "imageYPosition": 660, "screenshot": "mocked-screenshot-string", }, ], diff --git a/packages/webdriver-image-comparison/src/methods/elementPosition.ts b/packages/webdriver-image-comparison/src/methods/elementPosition.ts index 0cb80333b..a06e19977 100644 --- a/packages/webdriver-image-comparison/src/methods/elementPosition.ts +++ b/packages/webdriver-image-comparison/src/methods/elementPosition.ts @@ -1,11 +1,8 @@ -import getElementPositionTopWindow from '../clientSideScripts/getElementPositionTopWindow.js' import getElementPositionTopDom from '../clientSideScripts/getElementPositionTopDom.js' -import { getElementPositionTopScreenNativeMobile } from '../clientSideScripts/getElementPositionTopScreenNativeMobile.js' -import { ANDROID_OFFSETS, IOS_OFFSETS } from '../helpers/constants.js' import type { Executor } from './methods.interfaces.js' import type { ElementPosition } from '../clientSideScripts/elementPosition.interfaces.js' -import getAndroidStatusAddressToolBarOffsets from '../clientSideScripts/getAndroidStatusAddressToolBarOffsets.js' -import getIosStatusAddressToolBarOffsets from '../clientSideScripts/getIosStatusAddressToolBarOffsets.js' +import { getBoundingClientRect } from '../clientSideScripts/getBoundingClientRect.js' +import type { DeviceRectangles } from './rectangles.interfaces.js' /** * Get the element position on a Android device @@ -13,30 +10,18 @@ import getIosStatusAddressToolBarOffsets from '../clientSideScripts/getIosStatus export async function getElementPositionAndroid( executor: Executor, element: HTMLElement, - { isAndroidNativeWebScreenshot, isLandscape }: { isAndroidNativeWebScreenshot: boolean; isLandscape: boolean }, + { deviceRectangles, isAndroidNativeWebScreenshot }: { + deviceRectangles: DeviceRectangles, + isAndroidNativeWebScreenshot: boolean; + }, ): Promise { // This is the native web screenshot if (isAndroidNativeWebScreenshot) { - const { - safeArea, - screenHeight, - screenWidth, - sideBar: { width: sideBarWidth }, - statusAddressBar: { height }, - } = await executor(getAndroidStatusAddressToolBarOffsets, ANDROID_OFFSETS, { isHybridApp: false, isLandscape }) - - return executor(getElementPositionTopScreenNativeMobile, element, { - isLandscape, - safeArea, - screenHeight, - screenWidth, - sideBarWidth, - statusBarAddressBarHeight: height, - }) + return getElementWebviewPosition(executor, element, { deviceRectangles } ) } // This is the ChromeDriver screenshot - return executor(getElementPositionTopWindow, element) + return executor(getBoundingClientRect, element) } /** @@ -63,32 +48,23 @@ export async function getElementPositionDesktop( return executor(getElementPositionTopDom, element) } - return executor(getElementPositionTopWindow, element) + return executor(getBoundingClientRect, element) } /** - * Get the element position on iOS Safari + * Get the element position calculated from the webview */ -export async function getElementPositionIos( +export async function getElementWebviewPosition( executor: Executor, element: HTMLElement, - { isLandscape }: { isLandscape: boolean }, + { deviceRectangles: { viewport:{ x, y } } }: { deviceRectangles: DeviceRectangles }, ): Promise { - // Determine status and address bar height - const { - safeArea, - screenHeight, - screenWidth, - sideBar: { width: sideBarWidth }, - statusAddressBar: { height }, - } = await executor(getIosStatusAddressToolBarOffsets, IOS_OFFSETS, isLandscape) + const { height, width, x:boundingClientX, y:boundingClientY } = (await executor(getBoundingClientRect, element)) as ElementPosition - return executor(getElementPositionTopScreenNativeMobile, element, { - isLandscape, - safeArea, - screenHeight, - screenWidth, - sideBarWidth, - statusBarAddressBarHeight: height, - }) + return { + height, + width, + x: boundingClientX + x, + y: boundingClientY + y, + } } diff --git a/packages/webdriver-image-comparison/src/methods/images.interfaces.ts b/packages/webdriver-image-comparison/src/methods/images.interfaces.ts index 8661feb50..ca1be669d 100644 --- a/packages/webdriver-image-comparison/src/methods/images.interfaces.ts +++ b/packages/webdriver-image-comparison/src/methods/images.interfaces.ts @@ -1,7 +1,7 @@ import type { RectanglesOutput } from './rectangles.interfaces.js' import type { Folders } from '../base.interfaces.js' import type { TestContext } from 'src/commands/check.interfaces.js' -import type { Executor } from './methods.interfaces.js' +import type { DeviceRectangles } from './rectangles.interfaces.js' export interface ResizeDimensions { // The bottom margin @@ -15,7 +15,6 @@ export interface ResizeDimensions { } export interface ExecuteImageCompare { - executor: Executor; options: ImageCompareOptions; testContext: TestContext; isViewPortScreenshot: boolean; @@ -32,18 +31,14 @@ export interface ImageCompareOptions { wic: WicImageCompareOptions; method: ScreenMethodImageCompareCompareOptions; }; + // The device rectangles + deviceRectangles: DeviceRectangles; // The name of the file fileName: string; // The folders object folderOptions: ImageCompareFolderOptions; - // Is it an hybrid app or not - isHybridApp: boolean; // Is this an Android device - isAndroid?: boolean; - // If it's in Landscape mode - isLandscape: boolean; - // The name of the platform - platformName: string; + isAndroid: boolean; // If this is a native web screenshot isAndroidNativeWebScreenshot: boolean; } diff --git a/packages/webdriver-image-comparison/src/methods/images.ts b/packages/webdriver-image-comparison/src/methods/images.ts index a422a4b3b..dda3bc13a 100644 --- a/packages/webdriver-image-comparison/src/methods/images.ts +++ b/packages/webdriver-image-comparison/src/methods/images.ts @@ -5,7 +5,7 @@ import { dirname, join } from 'node:path' import { Jimp, JimpMime } from 'jimp' import logger from '@wdio/logger' import compareImages from '../resemble/compareImages.js' -import { calculateDprData, getAndCreatePath, getIosBezelImageNames, getScreenshotSize, updateVisualBaseline } from '../helpers/utils.js' +import { calculateDprData, getAndCreatePath, getIosBezelImageNames, getBase64ScreenshotSize, updateVisualBaseline } from '../helpers/utils.js' import { DEFAULT_RESIZE_DIMENSIONS, supportedIosBezelDevices } from '../helpers/constants.js' import { determineStatusAddressToolBarRectangles, isWdioElement } from './rectangles.js' import type { @@ -112,7 +112,7 @@ export async function checkBaselineImageExists( * Get the rotated image if needed */ async function getRotatedImageIfNeeded({ isWebDriverElementScreenshot, isLandscape, base64Image }: RotatedImage): Promise { - const { height: screenshotHeight, width: screenshotWidth } = getScreenshotSize(base64Image) + const { height: screenshotHeight, width: screenshotWidth } = getBase64ScreenshotSize(base64Image) const isRotated = !isWebDriverElementScreenshot && isLandscape && screenshotHeight > screenshotWidth return isRotated ? await rotateBase64Image({ base64Image, degrees: 90 }) : base64Image @@ -209,8 +209,8 @@ async function handleIOSBezelCorners({ image.composite(await Jimp.read(Buffer.from(topBase64Image, 'base64')), 0, 0) image.composite(await Jimp.read(Buffer.from(bottomBase64Image, 'base64')), - isLandscape ? width - getScreenshotSize(bottomImage).height : 0, - isLandscape ? 0 : height - getScreenshotSize(bottomImage).height + isLandscape ? width - getBase64ScreenshotSize(bottomImage).height : 0, + isLandscape ? 0 : height - getBase64ScreenshotSize(bottomImage).height ) } else { isIosBezelError = true @@ -279,7 +279,7 @@ export async function makeCroppedBase64Image({ }: CroppedBase64Image): Promise { // Rotate the image if needed and get the screenshot size const newBase64Image = await getRotatedImageIfNeeded({ isWebDriverElementScreenshot, isLandscape, base64Image }) - const { height: screenshotHeight, width: screenshotWidth } = getScreenshotSize(base64Image) + const { height: screenshotHeight, width: screenshotWidth } = getBase64ScreenshotSize(base64Image) // Determine/Get the size of the cropped screenshot and cut out dimensions const { top, right, bottom, left } = { ...DEFAULT_RESIZE_DIMENSIONS, ...resizeDimensions } @@ -321,7 +321,6 @@ export async function makeCroppedBase64Image({ */ export async function executeImageCompare( { - executor, isViewPortScreenshot, isNativeContext, options, @@ -330,14 +329,12 @@ export async function executeImageCompare( ): Promise { // 1. Set some variables const { - ignoreRegions = [], devicePixelRatio, - fileName, + deviceRectangles, + ignoreRegions = [], isAndroidNativeWebScreenshot, isAndroid, - isHybridApp, - isLandscape, - platformName, + fileName, } = options const { actualFolder, autoSaveBaseline, baselineFolder, browserName, deviceName, diffFolder, isMobile, savePerInstance } = options.folderOptions @@ -376,18 +373,19 @@ export async function executeImageCompare( blockOutSideBar: imageCompareOptions.blockOutSideBar, blockOutStatusBar: imageCompareOptions.blockOutStatusBar, blockOutToolBar: imageCompareOptions.blockOutToolBar, - isHybridApp, - isLandscape, + isAndroid, + isAndroidNativeWebScreenshot, isMobile, isViewPortScreenshot, - isAndroidNativeWebScreenshot, - platformName, } - webStatusAddressToolBarOptions.push(...(await determineStatusAddressToolBarRectangles(executor, statusAddressToolBarOptions)) || []) + webStatusAddressToolBarOptions.push( + ...(determineStatusAddressToolBarRectangles({ deviceRectangles, options: statusAddressToolBarOptions })) || [] + ) 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 - webStatusAddressToolBarOptions = webStatusAddressToolBarOptions.filter((rectangle) => !(rectangle.x === 0 && rectangle.y === 0 && rectangle.width === 0 && rectangle.height === 0)) + webStatusAddressToolBarOptions = webStatusAddressToolBarOptions + .filter((rectangle) => !(rectangle.x === 0 && rectangle.y === 0 && rectangle.width === 0 && rectangle.height === 0)) } } const ignoredBoxes = [ @@ -467,9 +465,9 @@ export async function executeImageCompare( ...(storeDiffs && { diffFolderPath: diffFolderPath }), }, size: { - actual: getScreenshotSize(readFileSync(actualFilePath).toString('base64'), devicePixelRatio), - baseline: getScreenshotSize(readFileSync(baselineFilePath).toString('base64'), devicePixelRatio), - ...(storeDiffs && { diff: getScreenshotSize(readFileSync(diffFilePath).toString('base64'), devicePixelRatio) }), + actual: getBase64ScreenshotSize(readFileSync(actualFilePath).toString('base64'), devicePixelRatio), + baseline: getBase64ScreenshotSize(readFileSync(baselineFilePath).toString('base64'), devicePixelRatio), + ...(storeDiffs && { diff: getBase64ScreenshotSize(readFileSync(diffFilePath).toString('base64'), devicePixelRatio) }), }, testContext, }) @@ -509,7 +507,7 @@ export async function makeFullPageBase64Image( // Load all the images for (let i = 0; i < amountOfScreenshots; i++) { const currentScreenshot = screenshotsData.data[i].screenshot - const { height: screenshotHeight, width: screenshotWidth } = getScreenshotSize(currentScreenshot, devicePixelRatio) + const { height: screenshotHeight, width: screenshotWidth } = getBase64ScreenshotSize(currentScreenshot, devicePixelRatio) const isRotated = isLandscape && screenshotHeight > screenshotWidth const newBase64Image = isRotated ? await rotateBase64Image({ base64Image: currentScreenshot, degrees: 90 }) : currentScreenshot const { canvasYPosition, imageHeight, imageWidth, imageXPosition, imageYPosition } = screenshotsData.data[i] diff --git a/packages/webdriver-image-comparison/src/methods/instanceData.interfaces.ts b/packages/webdriver-image-comparison/src/methods/instanceData.interfaces.ts index 2497d29ab..7d70aa293 100644 --- a/packages/webdriver-image-comparison/src/methods/instanceData.interfaces.ts +++ b/packages/webdriver-image-comparison/src/methods/instanceData.interfaces.ts @@ -1,5 +1,6 @@ + import type { ScreenDimensions } from '../clientSideScripts/screenDimensions.interfaces.js' -import type { RectanglesOutput } from './rectangles.interfaces.js' +import type { DeviceRectangles } from './rectangles.interfaces.js' export interface InstanceData { // The name of the app @@ -12,18 +13,8 @@ export interface InstanceData { deviceName: string; // The devicePixelRatio of the instance devicePixelRatio: number; - // The Device Platform Rect - devicePlatformRect: { - statusBar: RectanglesOutput; - homeBar: RectanglesOutput; - } - // The Device Screen size - deviceScreenSize: { - // The Device Screen Height - height: number; - // The Device Screen Width - width: number; - }; + // The mobile viewport position + deviceRectangles: DeviceRectangles; // Is this an Android device isAndroid: boolean; // Is this an iOS device diff --git a/packages/webdriver-image-comparison/src/methods/methods.interfaces.ts b/packages/webdriver-image-comparison/src/methods/methods.interfaces.ts index 9703c9458..38ad4a2e4 100644 --- a/packages/webdriver-image-comparison/src/methods/methods.interfaces.ts +++ b/packages/webdriver-image-comparison/src/methods/methods.interfaces.ts @@ -1,19 +1,37 @@ import type { RectanglesOutput } from './rectangles.interfaces.js' -/** Binding to the `await browser.execute()` method */ -export type Executor = ( - fn: string | ((...args: InnerArguments) => ReturnValue), - ...args: InnerArguments +// There a multiple ways to call the executor method, for mobile and web +type ExecuteScript = ( + fn: (...args: Args) => ReturnValue, + ...args: Args + ) => Promise; + +type ExecuteMobile = ( + fn: string, + args?: Record ) => Promise; -export type GetElementRect = (elementId: string) => Promise +interface BrowsingContextCaptureScreenshotParameters { + context: string; + origin?: 'viewport' | 'document'; + format?: {type: string; quality?: number;}; + clip?: { type: 'box'; x: number; y: number; width: number; height: number;}; +} +export type BidiScreenshot = (options: BrowsingContextCaptureScreenshotParameters) => Promise<{ data: string }>; +export type Executor = ExecuteScript & ExecuteMobile; +export type GetElementRect = (elementId: string) => Promise; +export type GetWindowHandle = () => Promise; export type TakeScreenShot = () => Promise; export type TakeElementScreenshot = (elementId: string) => Promise; export interface Methods { + // The method to take a bidi screenshot + bidiScreenshot?: BidiScreenshot; // The method to inject JS in the running instance executor: Executor; // Get the element rectangles getElementRect?: GetElementRect + // The method to get the window handle + getWindowHandle?: GetWindowHandle; // The screenshot method screenShot: TakeScreenShot; // The method to take an element screenshot diff --git a/packages/webdriver-image-comparison/src/methods/rectangles.interfaces.ts b/packages/webdriver-image-comparison/src/methods/rectangles.interfaces.ts index b863db58c..24076c3a8 100644 --- a/packages/webdriver-image-comparison/src/methods/rectangles.interfaces.ts +++ b/packages/webdriver-image-comparison/src/methods/rectangles.interfaces.ts @@ -12,10 +12,10 @@ export interface RectanglesOptions { } export interface ElementRectanglesOptions extends RectanglesOptions { + // The device rectangles + deviceRectangles: DeviceRectangles; // If this is an Android device isAndroid: boolean; - // If it's landscape - isLandscape: boolean; } export interface ScreenRectanglesOptions extends RectanglesOptions { @@ -34,6 +34,17 @@ export interface RectanglesOutput { y: number; } +export type DeviceRectangles = { + bottomBar: RectanglesOutput, + homeBar: RectanglesOutput, + leftSidePadding: RectanglesOutput, + rightSidePadding: RectanglesOutput, + screenSize: { height: number, width: number}, + statusBarAndAddressBar: RectanglesOutput, + statusBar: RectanglesOutput, + viewport: RectanglesOutput, +} + export interface StatusAddressToolBarRectanglesOptions { // If the side bar needs to be blocked out blockOutSideBar: boolean; @@ -41,18 +52,14 @@ export interface StatusAddressToolBarRectanglesOptions { blockOutStatusBar: boolean; // If the tool bar needs to be blocked out blockOutToolBar: boolean; + // Determine if it's an Android device + isAndroid: boolean; // The name of the platform isAndroidNativeWebScreenshot: boolean; - // Is it an hybrid app or not - isHybridApp: boolean; - // If it's in landscape mode - isLandscape: boolean; // If the instance is a mobile phone isMobile: boolean; // If the comparison needs to be done for a viewport screenshot or not isViewPortScreenshot: boolean; - // The name of the platform - platformName: string; } export type StatusAddressToolBarRectangles = Array; diff --git a/packages/webdriver-image-comparison/src/methods/rectangles.test.ts b/packages/webdriver-image-comparison/src/methods/rectangles.test.ts index c8c77dfff..341391e1b 100644 --- a/packages/webdriver-image-comparison/src/methods/rectangles.test.ts +++ b/packages/webdriver-image-comparison/src/methods/rectangles.test.ts @@ -8,35 +8,23 @@ describe('rectangles', () => { const options = { isAndroid: false, devicePixelRatio: 2, + deviceRectangles: { + bottomBar: { y: 0, x: 0, width: 0, height: 0 }, + homeBar: { y: 0, x: 0, width: 0, height: 0 }, + leftSidePadding: { y: 0, x: 0, width: 0, height: 0 }, + rightSidePadding: { y: 0, x: 0, width: 0, height: 0 }, + screenSize: { height: 0, width: 0 }, + statusBar: { y: 0, x: 0, width: 0, height: 0 }, + statusBarAndAddressBar: { y: 0, x: 0, width: 0, height: 0 }, + viewport: { y: 20, x: 30, width: 0, height: 0 }, + }, isAndroidNativeWebScreenshot: false, innerHeight: 678, isIOS: true, - isLandscape: false, } const MOCKED_EXECUTOR = vi .fn() - // getElementPositionIos for: getIosStatusAddressToolBarOffsets - .mockResolvedValueOnce({ - sideBar: { - height: 240, - width: 120, - x: 0, - y: 70, - }, - statusAddressBar: { - height: 94, - width: 375, - x: 0, - y: 0, - }, - toolBar: { - height: 5, - width: 135, - x: 120, - y: 799, - }, - }) - // getElementPositionIos for: getElementPositionTopScreenNativeMobile + // getBoundingClientRect .mockResolvedValueOnce({ height: 120, width: 120, @@ -58,38 +46,26 @@ describe('rectangles', () => { const options = { isAndroid: true, devicePixelRatio: 3, + deviceRectangles: { + homeBar: { y: 0, x: 0, width: 0, height: 0 }, + statusBar: { y: 0, x: 0, width: 0, height: 0 }, + screenSize: { height: 0, width: 0 }, + statusBarAndAddressBar: { y: 0, x: 0, width: 0, height: 0 }, + viewport: { y: 200, x: 300, width: 0, height: 0 }, + bottomBar: { y: 0, x: 0, width: 0, height: 0 }, + leftSidePadding: { y: 0, x: 0, width: 0, height: 0 }, + rightSidePadding: { y: 0, x: 0, width: 0, height: 0 }, + }, isAndroidNativeWebScreenshot: true, innerHeight: 678, isIOS: false, - isLandscape: false, } const MOCKED_EXECUTOR = vi .fn() - // getElementPositionAndroid for: getAndroidStatusAddressToolBarOffsets + // getBoundingClientRect .mockResolvedValueOnce({ - sideBar: { - height: 0, - width: 0, - x: 0, - y: 0, - }, - statusAddressBar: { - height: 20, - width: 375, - x: 0, - y: 0, - }, - toolBar: { - height: 5, - width: 135, - x: 120, - y: 799, - }, - }) - // getElementPositionIos for: getElementPositionTopScreenNativeMobile - .mockResolvedValueOnce({ - height: 120, - width: 120, + height: 300, + width: 200, x: 100, y: 10, }) @@ -108,14 +84,23 @@ describe('rectangles', () => { const options = { isAndroid: true, devicePixelRatio: 1, + deviceRectangles: { + homeBar: { y: 0, x: 0, width: 0, height: 0 }, + statusBar: { y: 0, x: 0, width: 0, height: 0 }, + screenSize: { height: 0, width: 0 }, + statusBarAndAddressBar: { y: 0, x: 0, width: 0, height: 0 }, + viewport: { y: 200, x: 300, width: 0, height: 0 }, + bottomBar: { y: 0, x: 0, width: 0, height: 0 }, + leftSidePadding: { y: 0, x: 0, width: 0, height: 0 }, + rightSidePadding: { y: 0, x: 0, width: 0, height: 0 }, + }, isAndroidNativeWebScreenshot: false, innerHeight: 678, isIOS: false, - isLandscape: false, } const MOCKED_EXECUTOR = vi .fn() - // getElementPositionAndroid for: getElementPositionTopWindow + // getBoundingClientRect .mockResolvedValueOnce({ height: 20, width: 375, @@ -137,10 +122,19 @@ describe('rectangles', () => { const options = { isAndroid: false, devicePixelRatio: 2, + deviceRectangles: { + homeBar: { y: 0, x: 0, width: 0, height: 0 }, + statusBar: { y: 0, x: 0, width: 0, height: 0 }, + screenSize: { height: 0, width: 0 }, + statusBarAndAddressBar: { y: 0, x: 0, width: 0, height: 0 }, + viewport: { y: 0, x: 0, width: 0, height: 0 }, + bottomBar: { y: 0, x: 0, width: 0, height: 0 }, + leftSidePadding: { y: 0, x: 0, width: 0, height: 0 }, + rightSidePadding: { y: 0, x: 0, width: 0, height: 0 }, + }, isAndroidNativeWebScreenshot: false, innerHeight: 500, isIOS: false, - isLandscape: false, } const MOCKED_EXECUTOR = vi .fn() @@ -166,10 +160,19 @@ describe('rectangles', () => { const options = { isAndroid: false, devicePixelRatio: 2, + deviceRectangles: { + homeBar: { y: 0, x: 0, width: 0, height: 0 }, + statusBar: { y: 0, x: 0, width: 0, height: 0 }, + screenSize: { height: 0, width: 0 }, + statusBarAndAddressBar: { y: 0, x: 0, width: 0, height: 0 }, + viewport: { y: 0, x: 0, width: 0, height: 0 }, + bottomBar: { y: 0, x: 0, width: 0, height: 0 }, + leftSidePadding: { y: 0, x: 0, width: 0, height: 0 }, + rightSidePadding: { y: 0, x: 0, width: 0, height: 0 }, + }, isAndroidNativeWebScreenshot: false, innerHeight: 500, isIOS: false, - isLandscape: false, } const MOCKED_EXECUTOR = vi.fn().mockResolvedValueOnce({ height: 0, @@ -196,10 +199,19 @@ describe('rectangles', () => { const options = { isAndroid: false, devicePixelRatio: 2, + deviceRectangles: { + homeBar: { y: 0, x: 0, width: 0, height: 0 }, + statusBar: { y: 0, x: 0, width: 0, height: 0 }, + screenSize: { height: 0, width: 0 }, + statusBarAndAddressBar: { y: 0, x: 0, width: 0, height: 0 }, + viewport: { y: 0, x: 0, width: 0, height: 0 }, + bottomBar: { y: 0, x: 0, width: 0, height: 0 }, + leftSidePadding: { y: 0, x: 0, width: 0, height: 0 }, + rightSidePadding: { y: 0, x: 0, width: 0, height: 0 }, + }, isAndroidNativeWebScreenshot: false, innerHeight: 500, isIOS: false, - isLandscape: false, } const MOCKED_EXECUTOR = vi.fn().mockResolvedValueOnce({ height: 375, @@ -226,10 +238,19 @@ describe('rectangles', () => { const options = { isAndroid: false, devicePixelRatio: 2, + deviceRectangles: { + homeBar: { y: 0, x: 0, width: 0, height: 0 }, + statusBar: { y: 0, x: 0, width: 0, height: 0 }, + screenSize: { height: 0, width: 0 }, + statusBarAndAddressBar: { y: 0, x: 0, width: 0, height: 0 }, + viewport: { y: 0, x: 0, width: 0, height: 0 }, + bottomBar: { y: 0, x: 0, width: 0, height: 0 }, + leftSidePadding: { y: 0, x: 0, width: 0, height: 0 }, + rightSidePadding: { y: 0, x: 0, width: 0, height: 0 }, + }, isAndroidNativeWebScreenshot: false, innerHeight: 500, isIOS: false, - isLandscape: false, } const MOCKED_EXECUTOR = vi.fn().mockResolvedValueOnce({ height: 375, @@ -298,121 +319,28 @@ describe('rectangles', () => { }) describe('determineStatusAddressToolBarRectangles', () => { - it('should determine the rectangles for the iOS with a status and toolbar blockout', async () => { - const options = { - blockOutSideBar: true, - blockOutStatusBar: true, - blockOutToolBar: true, - isAndroidNativeWebScreenshot: false, - isHybridApp: false, - isLandscape: false, - isMobile: true, - isViewPortScreenshot: true, - platformName: 'iOS', - } - const MOCKED_EXECUTOR = vi - .fn() - // determineStatusAddressToolBarRectangles for: getIosStatusAddressToolBarOffsets - .mockResolvedValueOnce({ - sideBar: { - height: 240, - width: 120, - x: 0, - y: 70, - }, - statusAddressBar: { - height: 94, - width: 375, - x: 0, - y: 0, - }, - toolBar: { - height: 5, - width: 135, - x: 120, - y: 799, - }, - }) - - expect(await determineStatusAddressToolBarRectangles(MOCKED_EXECUTOR, options)).toMatchSnapshot() - }) - - it('should determine the rectangles for the iOS without a status and toolbar blockout', async () => { - const options = { - blockOutSideBar: false, - blockOutStatusBar: false, - blockOutToolBar: false, - isAndroidNativeWebScreenshot: false, - isHybridApp: false, - isLandscape: false, - isMobile: true, - isViewPortScreenshot: true, - platformName: 'iOS', - } - const MOCKED_EXECUTOR = vi - .fn() - // determineStatusAddressToolBarRectangles for: getIosStatusAddressToolBarOffsets - .mockResolvedValueOnce({ - sideBar: { - height: 240, - width: 120, - x: 0, - y: 70, - }, - statusAddressBar: { - height: 94, - width: 375, - x: 0, - y: 0, - }, - toolBar: { - height: 5, - width: 135, - x: 0, - y: 799, - }, - }) - - expect(await determineStatusAddressToolBarRectangles(MOCKED_EXECUTOR, options)).toMatchSnapshot() - }) - - it('should determine the rectangles for Android with a status and toolbar blockout', async () => { + it('should determine the rectangles with a status and toolbar blockout', async () => { const options = { blockOutSideBar: true, blockOutStatusBar: true, blockOutToolBar: true, + isAndroid: true, isAndroidNativeWebScreenshot: true, - isHybridApp: false, - isLandscape: false, isMobile: true, isViewPortScreenshot: true, - platformName: 'Android', } - const MOCKED_EXECUTOR = vi - .fn() - // determineStatusAddressToolBarRectangles for: getAndroidStatusAddressToolBarOffsets - .mockResolvedValueOnce({ - sideBar: { - height: 0, - width: 0, - x: 0, - y: 0, - }, - statusAddressBar: { - height: 40, - width: 320, - x: 0, - y: 0, - }, - toolBar: { - height: 100, - width: 320, - x: 0, - y: 600, - }, - }) + const deviceRectangles = { + homeBar: { y: 0, x: 0, width: 0, height: 0 }, + statusBar: { y: 0, x: 0, width: 0, height: 0 }, + screenSize: { height: 0, width: 0 }, + statusBarAndAddressBar: { y: 0, x: 0, width: 1344, height: 320 }, + viewport: { y: 320, x: 0, width: 1344, height: 2601 }, + bottomBar: { y: 2921, x: 0, width: 1344, height: 71 }, + leftSidePadding: { y: 320, x: 0, width: 0, height: 2601 }, + rightSidePadding: { y: 320, x: 1344, width: 0, height: 2601 } + } - expect(await determineStatusAddressToolBarRectangles(MOCKED_EXECUTOR, options)).toMatchSnapshot() + expect(await determineStatusAddressToolBarRectangles( { deviceRectangles, options })).toMatchSnapshot() }) it('should determine the rectangles that there are no rectangles for this device', async () => { @@ -420,32 +348,23 @@ describe('rectangles', () => { blockOutSideBar: false, blockOutStatusBar: false, blockOutToolBar: false, + isAndroid: false, isAndroidNativeWebScreenshot: false, - isHybridApp: false, - isLandscape: false, isMobile: true, isViewPortScreenshot: false, - platformName: 'Android', } - const MOCKED_EXECUTOR = vi - .fn() - // determineStatusAddressToolBarRectangles for: getAndroidStatusAddressToolBarOffsets - .mockResolvedValueOnce({ - statusAddressBar: { - height: 40, - width: 320, - x: 0, - y: 0, - }, - toolBar: { - height: 100, - width: 320, - x: 0, - y: 600, - }, - }) + const deviceRectangles = { + homeBar: { y: 0, x: 0, width: 0, height: 0 }, + statusBar: { y: 0, x: 0, width: 0, height: 0 }, + screenSize: { height: 0, width: 0 }, + statusBarAndAddressBar: { y: 0, x: 0, width: 1344, height: 320 }, + viewport: { y: 320, x: 0, width: 1344, height: 2601 }, + bottomBar: { y: 2921, x: 0, width: 1344, height: 71 }, + leftSidePadding: { y: 320, x: 0, width: 0, height: 2601 }, + rightSidePadding: { y: 320, x: 1344, width: 0, height: 2601 } + } - expect(await determineStatusAddressToolBarRectangles(MOCKED_EXECUTOR, options)).toMatchSnapshot() + expect(await determineStatusAddressToolBarRectangles({ deviceRectangles, options })).toMatchSnapshot() }) }) }) diff --git a/packages/webdriver-image-comparison/src/methods/rectangles.ts b/packages/webdriver-image-comparison/src/methods/rectangles.ts index df4d23b03..e04837121 100644 --- a/packages/webdriver-image-comparison/src/methods/rectangles.ts +++ b/packages/webdriver-image-comparison/src/methods/rectangles.ts @@ -1,18 +1,15 @@ import type { ChainablePromiseElement } from 'webdriverio' -import { calculateDprData, checkAndroidNativeWebScreenshot, checkIsIos, getScreenshotSize, isObject } from '../helpers/utils.js' -import { getElementPositionAndroid, getElementPositionDesktop, getElementPositionIos } from './elementPosition.js' -import { IOS_OFFSETS, ANDROID_OFFSETS } from '../helpers/constants.js' +import { calculateDprData, getBase64ScreenshotSize, isObject } from '../helpers/utils.js' +import { getElementPositionAndroid, getElementPositionDesktop, getElementWebviewPosition } from './elementPosition.js' import type { + DeviceRectangles, ElementRectangles, RectanglesOutput, ScreenRectanglesOptions, StatusAddressToolBarRectangles, StatusAddressToolBarRectanglesOptions, } from './rectangles.interfaces.js' -import type { Executor, GetElementRect } from './methods.interfaces.js' -import getIosStatusAddressToolBarOffsets from '../clientSideScripts/getIosStatusAddressToolBarOffsets.js' -import getAndroidStatusAddressToolBarOffsets from '../clientSideScripts/getAndroidStatusAddressToolBarOffsets.js' -import type { StatusAddressToolBarOffsets } from '../clientSideScripts/statusAddressToolBarOffsets.interfaces.js' +import type { GetElementRect } from './methods.interfaces.js' import type { CheckScreenMethodOptions } from '../commands/screen.interfaces.js' import type { InstanceData } from './instanceData.interfaces.js' @@ -28,20 +25,20 @@ export async function determineElementRectangles({ // Determine screenshot data const { devicePixelRatio, + deviceRectangles, innerHeight, isAndroid, isAndroidNativeWebScreenshot, isIOS, - isLandscape, } = options - const { height } = getScreenshotSize(base64Image, devicePixelRatio) + const { height } = getBase64ScreenshotSize(base64Image, devicePixelRatio) let elementPosition // Determine the element position on the screenshot if (isIOS) { - elementPosition = await getElementPositionIos(executor, element, { isLandscape }) + elementPosition = await getElementWebviewPosition(executor, element, { deviceRectangles }) } else if (isAndroid) { - elementPosition = await getElementPositionAndroid(executor, element, { isAndroidNativeWebScreenshot, isLandscape }) + elementPosition = await getElementPositionAndroid(executor, element, { deviceRectangles, isAndroidNativeWebScreenshot }) } else { elementPosition = await getElementPositionDesktop(executor, element, { innerHeight, screenshotHeight: height }) } @@ -82,7 +79,7 @@ export function determineScreenRectangles(base64Image: string, options: ScreenRe isAndroidNativeWebScreenshot, isLandscape, } = options - const { height, width } = getScreenshotSize(base64Image, devicePixelRatio) + const { height, width } = getBase64ScreenshotSize(base64Image, devicePixelRatio) // Determine the width const screenshotWidth = isIOS || isAndroidChromeDriverScreenshot ? width : innerWidth @@ -104,34 +101,42 @@ export function determineScreenRectangles(base64Image: string, options: ScreenRe /** * Determine the rectangles for the mobile devices */ -export async function determineStatusAddressToolBarRectangles( - executor: Executor, +export function determineStatusAddressToolBarRectangles({ deviceRectangles, options }:{ + deviceRectangles: DeviceRectangles, options: StatusAddressToolBarRectanglesOptions, -): Promise { +}): StatusAddressToolBarRectangles { const { blockOutSideBar, blockOutStatusBar, blockOutToolBar, + isAndroid, isAndroidNativeWebScreenshot, - isHybridApp, - isLandscape, isMobile, isViewPortScreenshot, - platformName, } = options const rectangles = [] if ( isViewPortScreenshot && isMobile && - (checkAndroidNativeWebScreenshot(platformName, isAndroidNativeWebScreenshot) || checkIsIos(platformName)) + ( isAndroid && isAndroidNativeWebScreenshot || !isAndroid ) ) { - const { sideBar, statusAddressBar, toolBar } = (await (checkIsIos(platformName) - ? executor(getIosStatusAddressToolBarOffsets, IOS_OFFSETS, isLandscape) - : executor(getAndroidStatusAddressToolBarOffsets, ANDROID_OFFSETS, { - isHybridApp, - isLandscape, - }))) as StatusAddressToolBarOffsets + const statusAddressBar = { + x: deviceRectangles.statusBarAndAddressBar.x, y: deviceRectangles.statusBarAndAddressBar.y, + width: deviceRectangles.statusBarAndAddressBar.width, height: deviceRectangles.statusBarAndAddressBar.height, + } + const toolBar = { + x: deviceRectangles.bottomBar.x, y: deviceRectangles.bottomBar.y, + width: deviceRectangles.bottomBar.width, height: deviceRectangles.bottomBar.height, + } + const leftSidePadding = { + x: deviceRectangles.leftSidePadding.x, y: deviceRectangles.leftSidePadding.y, + width: deviceRectangles.leftSidePadding.width, height: deviceRectangles.leftSidePadding.height, + } + const rightSidePadding = { + x: deviceRectangles.rightSidePadding.x, y: deviceRectangles.rightSidePadding.y, + width: deviceRectangles.rightSidePadding.width, height: deviceRectangles.rightSidePadding.height, + } if (blockOutStatusBar) { rectangles.push(statusAddressBar) @@ -142,7 +147,7 @@ export async function determineStatusAddressToolBarRectangles( } if (blockOutSideBar) { - rectangles.push(sideBar) + rectangles.push(leftSidePadding, rightSidePadding) } } @@ -265,7 +270,7 @@ export async function determineDeviceBlockOuts({ isAndroid, screenCompareOptions }){ const rectangles: RectanglesOutput[] = [] const { blockOutStatusBar, blockOutToolBar } = screenCompareOptions - const { devicePlatformRect:{ homeBar, statusBar } } = instanceData + const { deviceRectangles:{ homeBar, statusBar } } = instanceData if (blockOutStatusBar){ rectangles.push(statusBar) diff --git a/packages/webdriver-image-comparison/src/methods/screenshots.interfaces.ts b/packages/webdriver-image-comparison/src/methods/screenshots.interfaces.ts index 8ec0b2ccf..747875fd4 100644 --- a/packages/webdriver-image-comparison/src/methods/screenshots.interfaces.ts +++ b/packages/webdriver-image-comparison/src/methods/screenshots.interfaces.ts @@ -1,3 +1,4 @@ +import type { DeviceRectangles } from './rectangles.interfaces.js' import type { Executor, TakeElementScreenshot, TakeScreenShot } from './methods.interfaces.js' import type { RectanglesOutput } from './rectangles.interfaces.js' @@ -31,6 +32,8 @@ export interface FullPageScreenshotDataOptions { addressBarShadowPadding: number; // The device pixel ratio devicePixelRatio: number; + // The rectangles of the device + deviceRectangles: DeviceRectangles; // The amount of milliseconds to wait for a new scroll fullPageScrollTimeout: number; // Elements that need to be hidden after the first scroll for a fullpage scroll @@ -43,8 +46,6 @@ export interface FullPageScreenshotDataOptions { isAndroidNativeWebScreenshot: boolean; // If this is an Android ChromeDriver screenshot isAndroidChromeDriverScreenshot: boolean; - // Is it an hybrid app or not - isHybridApp: boolean; // If the instance is an iOS device isIOS: boolean; // If it's landscape or not @@ -62,28 +63,22 @@ export interface FullPageScreenshotNativeMobileOptions { addressBarShadowPadding: number; // The device pixel ratio devicePixelRatio: number; + // The rectangles of the device + deviceRectangles: DeviceRectangles; // The amount of milliseconds to wait for a new scroll fullPageScrollTimeout: number; // Elements that need to be hidden after the first scroll for a fullpage scroll hideAfterFirstScroll: (HTMLElement | HTMLElement[])[]; - // Position of the home bar for iOS - iosHomeBarY?: number; + // If it's an Android device + isAndroid: boolean; // If the device is in landscape mode isLandscape?: boolean; // The innerheight innerHeight: number; - // The size of the safe area for iOS - safeArea: number; - // The height of the status and the address bar - statusAddressBarHeight: number; // The address bar padding for iOS or Android toolBarShadowPadding: number; - // Height of the screen - screenHeight: number; // Width of the screen screenWidth: number; - // When iOS is in landscape mode and it's an iPad there will be a side bar - sideBarWidth: number; } export interface FullPageScreenshotOptions { @@ -99,6 +94,7 @@ export interface FullPageScreenshotOptions { export interface TakeWebElementScreenshot { devicePixelRatio?: number, + deviceRectangles: DeviceRectangles, element: any, executor: Executor, fallback?: boolean, diff --git a/packages/webdriver-image-comparison/src/methods/screenshots.test.ts b/packages/webdriver-image-comparison/src/methods/screenshots.test.ts index fcd9d4f5e..a015126c5 100644 --- a/packages/webdriver-image-comparison/src/methods/screenshots.test.ts +++ b/packages/webdriver-image-comparison/src/methods/screenshots.test.ts @@ -10,13 +10,14 @@ describe('screenshots', () => { it('should get the Android nativeWebScreenshot fullpage screenshot data', async () => { const options: FullPageScreenshotDataOptions = { addressBarShadowPadding: 6, - devicePixelRatio: 2, + devicePixelRatio: 1, + // @ts-expect-error + deviceRectangles: { viewport: { x: 0, y: 0, width: 1366, height: 768 } }, fullPageScrollTimeout: 1, innerHeight: 800, isAndroid: true, isAndroidNativeWebScreenshot: true, isAndroidChromeDriverScreenshot: false, - isHybridApp: false, isIOS: false, isLandscape: false, toolBarShadowPadding: 6, @@ -26,23 +27,21 @@ describe('screenshots', () => { } const MOCKED_EXECUTOR = vi .fn() - // For await executor(getAndroidStatusAddressToolBarOffsets, OFFSETS.ANDROID)) - .mockResolvedValueOnce({ - isLandscape: false, - safeArea: 0, - statusAddressBar: { height: 56 }, - screenHeight: 768, - screenWidth: 1366, - sideBar: { width: 0 }, - }) - // THIS NEEDS TO BE FIXED IN THE FUTURE - // getFullPageScreenshotsDataNativeMobile: For await executor(scrollToPosition, scrollY) + // scrollToPosition .mockResolvedValueOnce({}) - // getFullPageScreenshotsDataNativeMobile: For await executor(hideScrollBars, true); + // hideScrollBars .mockResolvedValueOnce({}) - // getFullPageScreenshotsDataNativeMobile: For await executor(getDocumentScrollHeight) + // getDocumentScrollHeight .mockResolvedValueOnce(788) - // getFullPageScreenshotsDataNativeMobile: For await executor(hideScrollBars, false); + // hideScrollBars + .mockResolvedValueOnce({}) + // scrollToPosition + .mockResolvedValueOnce({}) + // hideScrollBars + .mockResolvedValueOnce({}) + // getDocumentScrollHeight + .mockResolvedValueOnce(788) + // hideScrollBars .mockResolvedValueOnce({}) // Replace the screenshot with a `mocked-screenshot-string`; @@ -55,13 +54,14 @@ describe('screenshots', () => { it('should get hide elements for the Android nativeWebScreenshot fullpage screenshot', async () => { const options: FullPageScreenshotDataOptions = { addressBarShadowPadding: 6, - devicePixelRatio: 2, + devicePixelRatio: 1, + // @ts-expect-error + deviceRectangles: { viewport: { x: 0, y: 0, width: 1366, height: 768 } }, fullPageScrollTimeout: 1, innerHeight: 600, isAndroid: true, isAndroidNativeWebScreenshot: true, isAndroidChromeDriverScreenshot: false, - isHybridApp: false, isIOS: false, isLandscape: false, toolBarShadowPadding: 6, @@ -71,16 +71,6 @@ describe('screenshots', () => { } const MOCKED_EXECUTOR = vi .fn() - // For await executor(getAndroidStatusAddressToolBarOffsets, OFFSETS.ANDROID)) - .mockResolvedValueOnce({ - isLandscape: false, - safeArea: 0, - statusAddressBar: { height: 56 }, - screenHeight: 768, - screenWidth: 1366, - sideBar: { width: 0 }, - }) - // THIS NEEDS TO BE FIXED IN THE FUTURE // getFullPageScreenshotsDataNativeMobile: For await executor(scrollToPosition, scrollY) .mockResolvedValueOnce({}) // getFullPageScreenshotsDataNativeMobile: For await executor(hideScrollBars, true); @@ -114,12 +104,13 @@ describe('screenshots', () => { const options: FullPageScreenshotDataOptions = { addressBarShadowPadding: 6, devicePixelRatio: 2, + // @ts-expect-error + deviceRectangles: { viewport: { left: 0, top: 0, width: 1366, height: 768 } }, fullPageScrollTimeout: 1, innerHeight: 800, isAndroid: true, isAndroidNativeWebScreenshot: false, isAndroidChromeDriverScreenshot: true, - isHybridApp: false, isIOS: false, isLandscape: false, toolBarShadowPadding: 6, @@ -159,12 +150,13 @@ describe('screenshots', () => { const options: FullPageScreenshotDataOptions = { addressBarShadowPadding: 6, devicePixelRatio: 2, + // @ts-expect-error + deviceRectangles: { viewport: { left: 0, top: 0, width: 1366, height: 768 } }, fullPageScrollTimeout: 1, innerHeight: 800, isAndroid: true, isAndroidNativeWebScreenshot: false, isAndroidChromeDriverScreenshot: true, - isHybridApp: false, isIOS: false, isLandscape: false, toolBarShadowPadding: 6, @@ -208,6 +200,8 @@ describe('screenshots', () => { const options: FullPageScreenshotDataOptions = { addressBarShadowPadding: 6, devicePixelRatio: 2, + // @ts-expect-error + deviceRectangles: { viewport: { x: 0, y: 0, width: 1366, height: 768 } }, fullPageScrollTimeout: 1, innerHeight: 800, isAndroid: false, @@ -223,16 +217,6 @@ describe('screenshots', () => { } const MOCKED_EXECUTOR = vi .fn() - .mockResolvedValueOnce({ - isLandscape: false, - safeArea: 44, - screenHeight: 768, - screenWidth: 1366, - sideBar: { width: 0 }, - statusAddressBar: { height: 94 }, - toolBar: { y: 329 }, - }) - // THIS NEEDS TO BE FIXED IN THE FUTURE // getFullPageScreenshotsDataNativeMobile: For await executor(scrollToPosition, scrollY) .mockResolvedValueOnce({}) // getFullPageScreenshotsDataNativeMobile: For await executor(hideScrollBars, true); @@ -262,12 +246,13 @@ describe('screenshots', () => { const options: FullPageScreenshotDataOptions = { addressBarShadowPadding: 6, devicePixelRatio: 2, + // @ts-expect-error + deviceRectangles: { viewport: { x: 0, y: 0, width: 1366, height: 768 } }, fullPageScrollTimeout: 1, innerHeight: 400, isAndroid: false, isAndroidNativeWebScreenshot: false, isAndroidChromeDriverScreenshot: false, - isHybridApp: false, isIOS: true, isLandscape: false, toolBarShadowPadding: 6, @@ -277,16 +262,6 @@ describe('screenshots', () => { } const MOCKED_EXECUTOR = vi .fn() - .mockResolvedValueOnce({ - isLandscape: true, - safeArea: 0, - screenHeight: 384, - screenWidth: 683, - sideBar: { width: 160 }, - statusAddressBar: { height: 47 }, - toolBar: { y: 75 }, - }) - // THIS NEEDS TO BE FIXED IN THE FUTURE // getFullPageScreenshotsDataNativeMobile: For await executor(scrollToPosition, scrollY) .mockResolvedValueOnce({}) // getFullPageScreenshotsDataNativeMobile: For await executor(hideScrollBars, true); @@ -316,12 +291,13 @@ describe('screenshots', () => { const options: FullPageScreenshotDataOptions = { addressBarShadowPadding: 6, devicePixelRatio: 2, + // @ts-expect-error + deviceRectangles: { viewport: { x: 0, y: 0, width: 1366, height: 768 } }, fullPageScrollTimeout: 1, innerHeight: 800, isAndroid: false, isAndroidNativeWebScreenshot: false, isAndroidChromeDriverScreenshot: false, - isHybridApp: false, isIOS: true, isLandscape: false, toolBarShadowPadding: 6, @@ -331,17 +307,6 @@ describe('screenshots', () => { } const MOCKED_EXECUTOR = vi .fn() - // getBase64FullPageScreenshotsData: For await executor(getIosStatusAddressToolBarOffsets) - .mockResolvedValueOnce({ - isLandscape: false, - safeArea: 44, - screenHeight: 768, - screenWidth: 1366, - sideBar: { width: 0 }, - statusAddressBar: { height: 94 }, - toolBar: { y: 329 }, - }) - // THIS NEEDS TO BE FIXED IN THE FUTURE // getFullPageScreenshotsDataNativeMobile: For await executor(scrollToPosition, scrollY) .mockResolvedValueOnce({}) // getFullPageScreenshotsDataNativeMobile: For await executor(hideScrollBars, true); @@ -375,12 +340,13 @@ describe('screenshots', () => { const options: FullPageScreenshotDataOptions = { addressBarShadowPadding: 6, devicePixelRatio: 2, + // @ts-expect-error + deviceRectangles: { viewport: { x: 0, y: 0, width: 0, height: 0 } }, fullPageScrollTimeout: 1, innerHeight: 768, isAndroid: false, isAndroidNativeWebScreenshot: false, isAndroidChromeDriverScreenshot: false, - isHybridApp: false, isIOS: false, isLandscape: false, toolBarShadowPadding: 6, @@ -427,12 +393,13 @@ describe('screenshots', () => { const options: FullPageScreenshotDataOptions = { addressBarShadowPadding: 6, devicePixelRatio: 2, + // @ts-expect-error + deviceRectangles: { viewport: { left: 0, top: 0, width: 0, height: 0 } }, fullPageScrollTimeout: 1, innerHeight: 768, isAndroid: false, isAndroidNativeWebScreenshot: false, isAndroidChromeDriverScreenshot: false, - isHybridApp: false, isIOS: false, isLandscape: false, toolBarShadowPadding: 6, diff --git a/packages/webdriver-image-comparison/src/methods/screenshots.ts b/packages/webdriver-image-comparison/src/methods/screenshots.ts index d621f789c..58f33c0d0 100644 --- a/packages/webdriver-image-comparison/src/methods/screenshots.ts +++ b/packages/webdriver-image-comparison/src/methods/screenshots.ts @@ -1,10 +1,7 @@ import logger from '@wdio/logger' import scrollToPosition from '../clientSideScripts/scrollToPosition.js' import getDocumentScrollHeight from '../clientSideScripts/getDocumentScrollHeight.js' -import getAndroidStatusAddressToolBarOffsets from '../clientSideScripts/getAndroidStatusAddressToolBarOffsets.js' -import getIosStatusAddressToolBarOffsets from '../clientSideScripts/getIosStatusAddressToolBarOffsets.js' -import { ANDROID_OFFSETS, IOS_OFFSETS } from '../helpers/constants.js' -import { calculateDprData, getScreenshotSize, waitFor } from '../helpers/utils.js' +import { calculateDprData, getBase64ScreenshotSize, waitFor } from '../helpers/utils.js' import type { Executor, TakeScreenShot } from './methods.interfaces.js' import type { FullPageScreenshotOptions, @@ -32,15 +29,14 @@ export async function getBase64FullPageScreenshotsData( const { addressBarShadowPadding, devicePixelRatio, + deviceRectangles, fullPageScrollTimeout, hideAfterFirstScroll, innerHeight, isAndroid, isAndroidNativeWebScreenshot, isAndroidChromeDriverScreenshot, - isHybridApp, isIOS, - isLandscape, screenHeight, screenWidth, toolBarShadowPadding, @@ -51,114 +47,65 @@ export async function getBase64FullPageScreenshotsData( hideAfterFirstScroll, innerHeight, } - const nativeMobileOptions = { + const nativeWebScreenshotOptions = { ...desktopOptions, addressBarShadowPadding, + deviceRectangles, + isAndroid, screenHeight, screenWidth, toolBarShadowPadding, } - if (isAndroid && isAndroidNativeWebScreenshot) { - // Create a fullpage screenshot for Android when native screenshot (so including status, address and toolbar) is created - const { - safeArea, - screenHeight, - screenWidth, - sideBar: { width: sideBarWidth }, - statusAddressBar: { height: statusAddressBarHeight }, - } = await executor(getAndroidStatusAddressToolBarOffsets, ANDROID_OFFSETS, { isHybridApp, isLandscape }) - - const androidNativeMobileOptions = { - ...nativeMobileOptions, - isLandscape, - safeArea, - screenHeight, - screenWidth, - sideBarWidth, - statusAddressBarHeight, - } - - return getFullPageScreenshotsDataNativeMobile(takeScreenshot, executor, androidNativeMobileOptions) + if ((isAndroid && isAndroidNativeWebScreenshot) || isIOS ) { + // Create a fullpage screenshot for Android when a native web screenshot (so including status, address and toolbar) is created + return getMobileFullPageNativeWebScreenshotsData(takeScreenshot, executor, nativeWebScreenshotOptions) } else if (isAndroid && isAndroidChromeDriverScreenshot) { const chromeDriverOptions = { devicePixelRatio, fullPageScrollTimeout, hideAfterFirstScroll, innerHeight } // Create a fullpage screenshot for Android when the ChromeDriver provides the screenshots - return getFullPageScreenshotsDataAndroidChromeDriver(takeScreenshot, executor, chromeDriverOptions) - } else if (isIOS) { - // Create a fullpage screenshot for iOS. iOS screenshots will hold the status, address and toolbar so they need to be removed - const { - safeArea, - screenHeight, - screenWidth, - sideBar: { width: sideBarWidth }, - statusAddressBar: { height: statusAddressBarHeight }, - toolBar: { y: iosHomeBarY }, - } = await executor(getIosStatusAddressToolBarOffsets, IOS_OFFSETS, isLandscape) - - const iosNativeMobileOptions = { - ...nativeMobileOptions, - iosHomeBarY, - isLandscape, - safeArea, - screenHeight, - screenWidth, - sideBarWidth, - statusAddressBarHeight, - } - - return getFullPageScreenshotsDataNativeMobile(takeScreenshot, executor, iosNativeMobileOptions) + return getAndroidChromeDriverFullPageScreenshotsData(takeScreenshot, executor, chromeDriverOptions) } // Create a fullpage screenshot for all desktops - return getFullPageScreenshotsDataDesktop(takeScreenshot, executor, desktopOptions) + return getDesktopFullPageScreenshotsData(takeScreenshot, executor, desktopOptions) } /** * Take a full page screenshots for native mobile */ -export async function getFullPageScreenshotsDataNativeMobile( +export async function getMobileFullPageNativeWebScreenshotsData( takeScreenshot: TakeScreenShot, executor: Executor, options: FullPageScreenshotNativeMobileOptions, ): Promise { const viewportScreenshots = [] - - // The addressBarShadowPadding and toolBarShadowPadding is used because the viewport has a shadow on the address and the tool bar + // The addressBarShadowPadding and toolBarShadowPadding is used because the viewport might have a shadow on the address and the tool bar // so the cutout of the viewport needs to be a little bit smaller const { addressBarShadowPadding, devicePixelRatio, + deviceRectangles: { viewport }, fullPageScrollTimeout, hideAfterFirstScroll, - innerHeight, - iosHomeBarY, - safeArea, + isAndroid, isLandscape, - statusAddressBarHeight, - screenHeight, - sideBarWidth, toolBarShadowPadding, } = options - const iosViewportHeight = - innerHeight - - addressBarShadowPadding - - toolBarShadowPadding - - // This is for iOS devices in landscape mode with a notch. They have a home bar at the bottom of the screen - // which is not part of the bottom toolbar. This home bar is not part of the viewport and needs to be subtracted - // 1133 is for iPads with a home bar, see the constants - (iosHomeBarY && ((isLandscape && safeArea) || screenHeight >= 1133) ? screenHeight - iosHomeBarY : 0) - + // The returned data from the deviceRectangles is in real pixels, not CSS pixels, so we need to divide it by the devicePixelRatio + // but only for Android, because the deviceRectangles are already in CSS pixels for iOS + const viewportHeight = Math.round(viewport.height / (isAndroid ? devicePixelRatio : 1)) - addressBarShadowPadding - toolBarShadowPadding + 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 const amountOfScrollsArray = [] let scrollHeight: number | undefined - let screenshotSizeHeight: number | undefined - let screenshotSizeWidth: number | undefined let isRotated = false for (let i = 0; i <= amountOfScrollsArray.length; i++) { // Determine and start scrolling - const scrollY = iosViewportHeight * i + const scrollY = viewportHeight * i await executor(scrollToPosition, scrollY) // Hide scrollbars before taking a screenshot, we don't want them, on the screenshot @@ -176,38 +123,35 @@ export async function getFullPageScreenshotsDataNativeMobile( } } - // Take the screenshot and get the width + // Take the screenshot and determine if it's rotated const screenshot = await takeBase64Screenshot(takeScreenshot) - screenshotSizeHeight = getScreenshotSize(screenshot, devicePixelRatio).height - sideBarWidth - screenshotSizeWidth = getScreenshotSize(screenshot, devicePixelRatio).width - sideBarWidth - isRotated = Boolean(isLandscape && screenshotSizeHeight > screenshotSizeWidth) + isRotated = Boolean(isLandscape && viewportHeight > viewportWidth) // Determine scroll height and check if we need to scroll again scrollHeight = await executor(getDocumentScrollHeight) - if (scrollHeight && (scrollY + iosViewportHeight < scrollHeight)) { + if (scrollHeight && (scrollY + viewportHeight < scrollHeight)) { amountOfScrollsArray.push(amountOfScrollsArray.length) } // There is no else // The height of the image of the last 1 could be different const imageHeight = amountOfScrollsArray.length === i && scrollHeight - ? scrollHeight - scrollY - : iosViewportHeight - + ? scrollHeight - scrollY - addressBarShadowPadding - toolBarShadowPadding + : viewportHeight // The starting position for cropping could be different for the last image // The cropping always needs to start at status and address bar height and the address bar shadow padding const imageYPosition = - (amountOfScrollsArray.length === i ? innerHeight - imageHeight : 0) + statusAddressBarHeight + addressBarShadowPadding + (amountOfScrollsArray.length === i ? viewportHeight - imageHeight : 0) + viewportY + addressBarShadowPadding // Store all the screenshot data in the screenshot object viewportScreenshots.push({ ...calculateDprData( { - canvasWidth: isRotated ? screenshotSizeHeight : screenshotSizeWidth, + canvasWidth: isRotated ? viewportHeight : viewportWidth, canvasYPosition: scrollY, imageHeight: imageHeight, - imageWidth: isRotated ? screenshotSizeHeight : screenshotSizeWidth, - imageXPosition: sideBarWidth, + imageWidth: isRotated ? viewportHeight : viewportWidth, + imageXPosition: viewportX, imageYPosition: imageYPosition, }, devicePixelRatio, @@ -228,7 +172,7 @@ export async function getFullPageScreenshotsDataNativeMobile( } } - if (!scrollHeight || !screenshotSizeHeight || !screenshotSizeWidth) { + if (!scrollHeight) { throw new Error('Couldn\'t determine scroll height or screenshot size') } @@ -236,7 +180,7 @@ export async function getFullPageScreenshotsDataNativeMobile( ...calculateDprData( { fullPageHeight: scrollHeight - addressBarShadowPadding - toolBarShadowPadding, - fullPageWidth: isRotated ? screenshotSizeHeight : screenshotSizeWidth, + fullPageWidth: isRotated ? viewportHeight : viewportWidth, }, devicePixelRatio, ), @@ -247,7 +191,7 @@ export async function getFullPageScreenshotsDataNativeMobile( /** * Take a full page screenshot for Android with Chromedriver */ -export async function getFullPageScreenshotsDataAndroidChromeDriver( +export async function getAndroidChromeDriverFullPageScreenshotsData( takeScreenshot: TakeScreenShot, executor: Executor, options: FullPageScreenshotOptions, @@ -282,7 +226,7 @@ export async function getFullPageScreenshotsDataAndroidChromeDriver( // Take the screenshot const screenshot = await takeBase64Screenshot(takeScreenshot) - screenshotSize = getScreenshotSize(screenshot, devicePixelRatio) + screenshotSize = getBase64ScreenshotSize(screenshot, devicePixelRatio) // Determine scroll height and check if we need to scroll again scrollHeight = await executor(getDocumentScrollHeight) @@ -347,7 +291,7 @@ export async function getFullPageScreenshotsDataAndroidChromeDriver( /** * Take a full page screenshots */ -export async function getFullPageScreenshotsDataDesktop( +export async function getDesktopFullPageScreenshotsData( takeScreenshot: TakeScreenShot, executor: Executor, options: FullPageScreenshotOptions, @@ -380,7 +324,7 @@ export async function getFullPageScreenshotsDataDesktop( // Take the screenshot const screenshot = await takeBase64Screenshot(takeScreenshot) - screenshotSize = getScreenshotSize(screenshot, devicePixelRatio) + screenshotSize = getBase64ScreenshotSize(screenshot, devicePixelRatio) // The actual screenshot size might be slightly different than the inner height // In that case, use the screenshot size instead of the innerHeight @@ -484,6 +428,7 @@ function logHiddenRemovedError(error: any) { */ export async function takeWebElementScreenshot({ devicePixelRatio, + deviceRectangles, element, executor, fallback = false, @@ -495,18 +440,18 @@ export async function takeWebElementScreenshot({ screenShot, takeElementScreenshot, }:TakeWebElementScreenshot): Promise{ - if (isIOS || fallback){ + if (fallback) { const base64Image = await takeBase64Screenshot(screenShot) const elementRectangleOptions: ElementRectanglesOptions = { /** * ToDo: handle NaA case */ devicePixelRatio: devicePixelRatio || NaN, + deviceRectangles, innerHeight: innerHeight || NaN, isAndroidNativeWebScreenshot, isAndroid, isIOS, - isLandscape, } const rectangles = await determineElementRectangles({ executor, @@ -524,7 +469,7 @@ export async function takeWebElementScreenshot({ try { const base64Image = await takeElementScreenshot!((await element as WebdriverIO.Element).elementId) - const { height, width } = getScreenshotSize(base64Image) + const { height, width } = getBase64ScreenshotSize(base64Image) const rectangles = { x: 0, y: 0, width, height } if (rectangles.width === 0 || rectangles.height === 0) { @@ -537,8 +482,10 @@ export async function takeWebElementScreenshot({ rectangles, } } catch (_e) { + log.warn('The element screenshot failed, falling back to cutting the full device/viewport screenshot:', _e) return takeWebElementScreenshot({ devicePixelRatio, + deviceRectangles, element, executor, fallback: true, @@ -551,5 +498,4 @@ export async function takeWebElementScreenshot({ takeElementScreenshot, }) } - } diff --git a/packages/webdriver-image-comparison/src/mocks/mocks.ts b/packages/webdriver-image-comparison/src/mocks/mocks.ts index 735707c9a..d13dc0d99 100644 --- a/packages/webdriver-image-comparison/src/mocks/mocks.ts +++ b/packages/webdriver-image-comparison/src/mocks/mocks.ts @@ -7,13 +7,15 @@ export const BEFORE_SCREENSHOT_OPTIONS: BeforeScreenshotOptions = { browserVersion: '75.0.1', deviceName: '', devicePixelRatio: 1, - deviceScreenSize: { - height:1, - width: 1, - }, - devicePlatformRect: { - statusBar: { x: 0, y:0, width: 0, height: 0 }, - homeBar: { x: 0, y:0, width: 0, height: 0 }, + deviceRectangles: { + statusBar: { y: 0, x: 0, width: 0, height: 0 }, + homeBar: { y: 0, x: 0, width: 0, height: 0 }, + screenSize: { height: 0, width: 0 }, + statusBarAndAddressBar: { y: 0, x: 0, width: 0, height: 0 }, + viewport: { y: 0, x: 0, width: 0, height: 0 }, + bottomBar: { y: 0, x: 0, width: 0, height: 0 }, + leftSidePadding: { y: 0, x: 0, width: 0, height: 0 }, + rightSidePadding: { y: 0, x: 0, width: 0, height: 0 }, }, isAndroid: false, isIOS: false, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 77fd2364e..4fe92d7b2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -19,8 +19,8 @@ importers: version: link:packages/webdriver-image-comparison devDependencies: '@changesets/cli': - specifier: ^2.29.0 - version: 2.29.0 + specifier: ^2.29.2 + version: 2.29.2 '@tsconfig/node20': specifier: ^20.1.5 version: 20.1.5 @@ -40,59 +40,59 @@ importers: specifier: ~0.4.14 version: 0.4.14 '@typescript-eslint/eslint-plugin': - specifier: ^8.29.1 - version: 8.29.1(@typescript-eslint/parser@8.29.1(eslint@9.23.0(jiti@1.21.7))(typescript@5.7.3))(eslint@9.23.0(jiti@1.21.7))(typescript@5.7.3) + specifier: ^8.30.1 + version: 8.30.1(@typescript-eslint/parser@8.30.1(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3))(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) '@typescript-eslint/parser': - specifier: ^8.29.1 - version: 8.29.1(eslint@9.23.0(jiti@1.21.7))(typescript@5.7.3) + specifier: ^8.30.1 + version: 8.30.1(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) '@typescript-eslint/utils': - specifier: ^8.29.1 - version: 8.29.1(eslint@9.23.0(jiti@1.21.7))(typescript@5.7.3) + specifier: ^8.30.1 + version: 8.30.1(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) '@vitest/coverage-v8': - specifier: ^3.0.8 - version: 3.0.8(vitest@3.0.8) + specifier: ^3.1.1 + version: 3.1.1(vitest@3.1.1) '@vitest/ui': - specifier: ^3.0.8 - version: 3.0.8(vitest@3.0.8) + specifier: ^3.1.1 + version: 3.1.1(vitest@3.1.1) '@wdio/appium-service': - specifier: ^9.12.4 - version: 9.12.4 + specifier: ^9.12.6 + version: 9.12.6 '@wdio/cli': - specifier: ^9.12.5 - version: 9.12.5 + specifier: ^9.12.6 + version: 9.12.6 '@wdio/globals': - specifier: ^9.9.1 - version: 9.9.1(@wdio/logger@9.4.4) + specifier: ^9.12.6 + version: 9.12.6(@wdio/logger@9.4.4) '@wdio/local-runner': - specifier: ^9.12.5 - version: 9.12.5 + specifier: ^9.12.6 + version: 9.12.6 '@wdio/mocha-framework': - specifier: ^9.12.5 - version: 9.12.5 + specifier: ^9.12.6 + version: 9.12.6 '@wdio/sauce-service': - specifier: ^9.12.5 - version: 9.12.5 + specifier: ^9.12.6 + version: 9.12.6 '@wdio/shared-store-service': - specifier: ^9.12.5 - version: 9.12.5 + specifier: ^9.12.6 + version: 9.12.6 '@wdio/spec-reporter': - specifier: ^9.12.3 - version: 9.12.3 + specifier: ^9.12.6 + version: 9.12.6 '@wdio/types': - specifier: ^9.12.2 - version: 9.12.2 + specifier: ^9.12.6 + version: 9.12.6 cross-env: specifier: ^7.0.3 version: 7.0.3 eslint: - specifier: ^9.23.0 - version: 9.23.0(jiti@1.21.7) + specifier: ^9.24.0 + version: 9.24.0(jiti@1.21.7) eslint-plugin-import: specifier: ^2.31.0 - version: 2.31.0(@typescript-eslint/parser@8.29.1(eslint@9.23.0(jiti@1.21.7))(typescript@5.7.3))(eslint-import-resolver-typescript@3.8.0)(eslint@9.23.0(jiti@1.21.7)) + version: 2.31.0(@typescript-eslint/parser@8.30.1(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.0)(eslint@9.24.0(jiti@1.21.7)) eslint-plugin-unicorn: specifier: ^56.0.1 - version: 56.0.1(eslint@9.23.0(jiti@1.21.7)) + version: 56.0.1(eslint@9.24.0(jiti@1.21.7)) eslint-plugin-wdio: specifier: ^9.9.1 version: 9.9.1 @@ -100,50 +100,50 @@ importers: specifier: ^9.1.7 version: 9.1.7 jsdom: - specifier: ^25.0.1 - version: 25.0.1 + specifier: ^26.1.0 + version: 26.1.0 npm-run-all2: specifier: ^7.0.2 version: 7.0.2 release-it: - specifier: ^17.11.0 - version: 17.11.0(typescript@5.7.3) + specifier: ^18.1.2 + version: 18.1.2(@types/node@22.14.0)(typescript@5.8.3) rimraf: specifier: ^6.0.1 version: 6.0.1 saucelabs: - specifier: ^8.0.0 - version: 8.0.0 + specifier: ^9.0.2 + version: 9.0.2 ts-node: specifier: ^10.9.2 - version: 10.9.2(@types/node@22.14.0)(typescript@5.7.3) + version: 10.9.2(@types/node@22.14.0)(typescript@5.8.3) typescript: - specifier: ^5.7.3 - version: 5.7.3 + specifier: ^5.8.3 + version: 5.8.3 vitest: - specifier: ^3.0.8 - version: 3.0.8(@types/debug@4.1.12)(@types/node@22.14.0)(@vitest/ui@3.0.8)(jsdom@25.0.1) + specifier: ^3.1.1 + version: 3.1.1(@types/debug@4.1.12)(@types/node@22.14.0)(@vitest/ui@3.1.1)(jsdom@26.1.0) wdio-lambdatest-service: specifier: ^4.0.0 - version: 4.0.0(@wdio/cli@9.12.5)(@wdio/types@9.12.2)(webdriverio@9.12.4) + version: 4.0.0(@wdio/cli@9.12.6)(@wdio/types@9.12.6)(webdriverio@9.12.6) webdriverio: - specifier: ^9.12.4 - version: 9.12.4 + specifier: ^9.12.6 + version: 9.12.6 packages/ocr-service: dependencies: '@inquirer/prompts': - specifier: 7.3.2 - version: 7.3.2(@types/node@22.14.1) + specifier: 7.4.1 + version: 7.4.1(@types/node@22.14.0) '@wdio/globals': - specifier: ^9.9.1 - version: 9.9.1(@wdio/logger@9.4.4) + specifier: ^9.12.6 + version: 9.12.6(@wdio/logger@9.4.4) '@wdio/logger': specifier: ^9.4.4 version: 9.4.4 '@wdio/types': - specifier: ^9.12.2 - version: 9.12.2 + specifier: ^9.12.6 + version: 9.12.6 fuse.js: specifier: ^7.1.0 version: 7.1.0 @@ -170,8 +170,8 @@ importers: packages/visual-reporter: dependencies: '@inquirer/prompts': - specifier: ^7.3.2 - version: 7.3.2(@types/node@22.14.1) + specifier: ^7.4.1 + version: 7.4.1(@types/node@22.14.0) ora: specifier: ^8.2.0 version: 8.2.0 @@ -179,54 +179,54 @@ importers: specifier: ^0.34.1 version: 0.34.1 sirv-cli: - specifier: ^3.0.0 - version: 3.0.0 + specifier: ^3.0.1 + version: 3.0.1 devDependencies: '@remix-run/dev': - specifier: ^2.15.3 - version: 2.15.3(@remix-run/react@2.16.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.7.3))(@remix-run/serve@2.16.4(typescript@5.7.3))(@types/node@22.14.1)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.14.1)(typescript@5.7.3))(typescript@5.7.3)(vite@5.4.18(@types/node@22.14.1)) + specifier: ^2.16.5 + version: 2.16.5(@remix-run/react@2.16.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3))(@remix-run/serve@2.16.5(typescript@5.8.3))(@types/node@22.14.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.14.0)(typescript@5.8.3))(typescript@5.8.3)(vite@5.4.18(@types/node@22.14.0)) '@remix-run/node': specifier: ^2.16.5 - version: 2.16.5(typescript@5.7.3) + version: 2.16.5(typescript@5.8.3) '@remix-run/react': specifier: ^2.16.5 - version: 2.16.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.7.3) + version: 2.16.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3) '@remix-run/serve': - specifier: ^2.16.4 - version: 2.16.4(typescript@5.7.3) + specifier: ^2.16.5 + version: 2.16.5(typescript@5.8.3) '@types/react': - specifier: ^18.3.18 - version: 18.3.18 + specifier: ^18.3.20 + version: 18.3.20 '@types/react-dom': - specifier: ^18.3.5 - version: 18.3.5(@types/react@18.3.18) + specifier: ^18.3.6 + version: 18.3.6(@types/react@18.3.20) '@typescript-eslint/eslint-plugin': - specifier: ^8.29.1 - version: 8.29.1(@typescript-eslint/parser@8.29.1(eslint@9.23.0(jiti@1.21.7))(typescript@5.7.3))(eslint@9.23.0(jiti@1.21.7))(typescript@5.7.3) + specifier: ^8.30.1 + version: 8.30.1(@typescript-eslint/parser@8.30.1(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3))(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) '@typescript-eslint/parser': - specifier: ^8.29.1 - version: 8.29.1(eslint@9.23.0(jiti@1.21.7))(typescript@5.7.3) + specifier: ^8.30.1 + version: 8.30.1(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) autoprefixer: specifier: ^10.4.21 version: 10.4.21(postcss@8.5.3) eslint: - specifier: ^9.23.0 - version: 9.23.0(jiti@1.21.7) + specifier: ^9.24.0 + version: 9.24.0(jiti@1.21.7) eslint-import-resolver-typescript: - specifier: ^3.8.0 - version: 3.8.0(eslint-plugin-import@2.31.0)(eslint@9.23.0(jiti@1.21.7)) + specifier: ^3.10.0 + version: 3.10.0(eslint-plugin-import@2.31.0)(eslint@9.24.0(jiti@1.21.7)) eslint-plugin-import: specifier: ^2.31.0 - version: 2.31.0(@typescript-eslint/parser@8.29.1(eslint@9.23.0(jiti@1.21.7))(typescript@5.7.3))(eslint-import-resolver-typescript@3.8.0)(eslint@9.23.0(jiti@1.21.7)) + version: 2.31.0(@typescript-eslint/parser@8.30.1(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.0)(eslint@9.24.0(jiti@1.21.7)) eslint-plugin-jsx-a11y: specifier: ^6.10.2 - version: 6.10.2(eslint@9.23.0(jiti@1.21.7)) + version: 6.10.2(eslint@9.24.0(jiti@1.21.7)) eslint-plugin-react: specifier: ^7.37.5 - version: 7.37.5(eslint@9.23.0(jiti@1.21.7)) + version: 7.37.5(eslint@9.24.0(jiti@1.21.7)) eslint-plugin-react-hooks: specifier: ^5.2.0 - version: 5.2.0(eslint@9.23.0(jiti@1.21.7)) + version: 5.2.0(eslint@9.24.0(jiti@1.21.7)) isbot: specifier: ^5.1.26 version: 5.1.26 @@ -244,34 +244,34 @@ importers: version: 5.5.0(react@18.3.1) react-select: specifier: ^5.10.1 - version: 5.10.1(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 5.10.1(@types/react@18.3.20)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) tailwindcss: specifier: ^3.4.17 - version: 3.4.17(ts-node@10.9.2(@types/node@22.14.1)(typescript@5.7.3)) + version: 3.4.17(ts-node@10.9.2(@types/node@22.14.0)(typescript@5.8.3)) typescript: - specifier: ^5.7.3 - version: 5.7.3 + specifier: ^5.8.3 + version: 5.8.3 vite: specifier: ^5.4.18 - version: 5.4.18(@types/node@22.14.1) + version: 5.4.18(@types/node@22.14.0) vite-tsconfig-paths: specifier: ^5.1.4 - version: 5.1.4(typescript@5.7.3)(vite@5.4.18(@types/node@22.14.1)) + version: 5.1.4(typescript@5.8.3)(vite@5.4.18(@types/node@22.14.0)) packages/visual-service: dependencies: '@wdio/globals': - specifier: ^9.9.1 - version: 9.9.1(@wdio/logger@9.4.4) + specifier: ^9.12.6 + version: 9.12.6(@wdio/logger@9.4.4) '@wdio/logger': specifier: ^9.4.4 version: 9.4.4 '@wdio/types': - specifier: ^9.12.2 - version: 9.12.2 + specifier: ^9.12.6 + version: 9.12.6 expect-webdriverio: specifier: ^5.1.0 - version: 5.1.0(@wdio/globals@9.9.1(@wdio/logger@9.4.4))(@wdio/logger@9.4.4)(webdriverio@9.12.5) + version: 5.1.0(@wdio/globals@9.12.6(@wdio/logger@9.4.4))(@wdio/logger@9.4.4)(webdriverio@9.12.6) webdriver-image-comparison: specifier: workspace:* version: link:../webdriver-image-comparison @@ -292,8 +292,8 @@ importers: specifier: ^11.0.4 version: 11.0.4 webdriverio: - specifier: ^9.12.4 - version: 9.12.4 + specifier: ^9.12.6 + version: 9.12.6 packages: @@ -309,8 +309,8 @@ packages: resolution: {integrity: sha512-UQFQ6SgyJ6LX42W8rHCs8KVc0JS0tzVL9ct4XYedJukskYVWTo49tNiMEK9C2HTyarbNiT/RVIRSY82vH+6sTg==} engines: {node: '>=4'} - '@asamuzakjp/css-color@2.8.3': - resolution: {integrity: sha512-GIc76d9UI1hCvOATjZPyHFmE5qhRccp3/zGfMPapK3jBi+yocEzp6BBB0UnfRYP9NP4FANqUZYb0hnfs3TM3hw==} + '@asamuzakjp/css-color@3.1.1': + resolution: {integrity: sha512-hpRD68SV2OMcZCsrbdkccTw5FXjNDLo5OuqSHyHZfwweGsDWZwDJ2+gONyNAbazZclobMirACLw0lk8WVxIqxA==} '@babel/code-frame@7.26.2': resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} @@ -320,12 +320,8 @@ packages: resolution: {integrity: sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==} engines: {node: '>=6.9.0'} - '@babel/core@7.26.9': - resolution: {integrity: sha512-lWBYIrF7qK5+GjY5Uy+/hEgp8OJWOD/rpy74GplYRhEauvbHDeFB8t5hPOZxCZ0Oxf4Cc36tK51/l3ymJysrKw==} - engines: {node: '>=6.9.0'} - - '@babel/generator@7.26.9': - resolution: {integrity: sha512-kEWdzjOAUMW4hAyrzJ0ZaTOu9OmpyDIQicIh0zg0EEcEkYXZb2TjtBhnHi2ViX7PKwZqF4xwqfAm299/QMP3lg==} + '@babel/core@7.26.10': + resolution: {integrity: sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==} engines: {node: '>=6.9.0'} '@babel/generator@7.27.0': @@ -336,12 +332,12 @@ packages: resolution: {integrity: sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==} engines: {node: '>=6.9.0'} - '@babel/helper-compilation-targets@7.26.5': - resolution: {integrity: sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA==} + '@babel/helper-compilation-targets@7.27.0': + resolution: {integrity: sha512-LVk7fbXml0H2xH34dFzKQ7TDZ2G4/rVTOrq9V+icbbadjbVxxeFeDsNHv2SrZeWoA+6ZiTyWYWtScEIW07EAcA==} engines: {node: '>=6.9.0'} - '@babel/helper-create-class-features-plugin@7.26.9': - resolution: {integrity: sha512-ubbUqCofvxPRurw5L8WTsCLSkQiVpov4Qx0WMA+jUN+nXBK8ADPlJO1grkFw5CWKC5+sZSOfuGMdX1aI1iT9Sg==} + '@babel/helper-create-class-features-plugin@7.27.0': + resolution: {integrity: sha512-vSGCvMecvFCd/BdpGlhpXYNhhC4ccxyvQWpbGL4CWbvfEoLFWUZuSuf7s9Aw70flgQF+6vptvgK2IfOnKlRmBg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 @@ -390,15 +386,10 @@ packages: resolution: {integrity: sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==} engines: {node: '>=6.9.0'} - '@babel/helpers@7.26.9': - resolution: {integrity: sha512-Mz/4+y8udxBKdmzt/UjPACs4G3j5SshJJEFFKxlCGPydG4JAHXxjWjAwjd09tf6oINvl1VfMJo+nB7H2YKQ0dA==} + '@babel/helpers@7.27.0': + resolution: {integrity: sha512-U5eyP/CTFPuNE3qk+WZMxFkp/4zUzdceQlfzf7DdGdhp+Fezd7HD+i8Y24ZuTMKX3wQBld449jijbGq6OdGNQg==} engines: {node: '>=6.9.0'} - '@babel/parser@7.26.9': - resolution: {integrity: sha512-81NWa1njQblgZbQHxWHpxxCzNsa3ZwvFqpUg7P+NNUU6f3UU2jBEg4OlF/J6rl8+PQGh1q6/zWScd001YwcA5A==} - engines: {node: '>=6.0.0'} - hasBin: true - '@babel/parser@7.27.0': resolution: {integrity: sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==} engines: {node: '>=6.0.0'} @@ -428,14 +419,14 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-typescript@7.26.8': - resolution: {integrity: sha512-bME5J9AC8ChwA7aEPJ6zym3w7aObZULHhbNLU0bKUhKsAkylkzUdq+0kdymh9rzi8nlNFl2bmldFBCKNJBUpuw==} + '@babel/plugin-transform-typescript@7.27.0': + resolution: {integrity: sha512-fRGGjO2UEGPjvEcyAZXRXAS8AfdaQoq7HnxAbJoAoW10B9xOKesmmndJv+Sym2a+9FHWZ9KbyyLCe9s0Sn5jtg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/preset-typescript@7.26.0': - resolution: {integrity: sha512-NMk1IGZ5I/oHhoXEElcm+xUnL/szL6xflkFZmoEU9xj1qSJXpiS7rsspYo92B4DRCDvZn2erT5LdsCeXAKNCkg==} + '@babel/preset-typescript@7.27.0': + resolution: {integrity: sha512-vxaPFfJtHhgeOVXRKuHpHPAOgymmy8V8I65T1q53R7GCZlefKeCaTyDs3zOPHTTbmquvNlQYC5klEvWsBAtrBQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 @@ -444,26 +435,14 @@ packages: resolution: {integrity: sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==} engines: {node: '>=6.9.0'} - '@babel/template@7.26.9': - resolution: {integrity: sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==} - engines: {node: '>=6.9.0'} - '@babel/template@7.27.0': resolution: {integrity: sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==} engines: {node: '>=6.9.0'} - '@babel/traverse@7.26.9': - resolution: {integrity: sha512-ZYW7L+pL8ahU5fXmNbPF+iZFHCv5scFak7MZ9bwaRPLUhHh7QQEMjZUg0HevihoqCM5iSYHN61EyCoZvqC+bxg==} - engines: {node: '>=6.9.0'} - '@babel/traverse@7.27.0': resolution: {integrity: sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA==} engines: {node: '>=6.9.0'} - '@babel/types@7.26.9': - resolution: {integrity: sha512-Y3IR1cRnOxOCDvMmNiym7XpXQ93iGDDPHx+Zj+NM+rg0fBaShfQLkg+hKPaZCEvg5N/LeCo4+Rj/i3FuJsIQaw==} - engines: {node: '>=6.9.0'} - '@babel/types@7.27.0': resolution: {integrity: sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==} engines: {node: '>=6.9.0'} @@ -472,8 +451,8 @@ packages: resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} engines: {node: '>=18'} - '@changesets/apply-release-plan@7.0.10': - resolution: {integrity: sha512-wNyeIJ3yDsVspYvHnEz1xQDq18D9ifed3lI+wxRQRK4pArUcuHgCTrHv0QRnnwjhVCQACxZ+CBih3wgOct6UXw==} + '@changesets/apply-release-plan@7.0.12': + resolution: {integrity: sha512-EaET7As5CeuhTzvXTQCRZeBUcisoYPDDcXvgTE/2jmmypKp0RC7LxKj/yzqeh/1qFTZI7oDGFcL1PHRuQuketQ==} '@changesets/assemble-release-plan@6.0.6': resolution: {integrity: sha512-Frkj8hWJ1FRZiY3kzVCKzS0N5mMwWKwmv9vpam7vt8rZjLL1JMthdh6pSDVSPumHPshTTkKZ0VtNbE0cJHZZUg==} @@ -481,8 +460,8 @@ packages: '@changesets/changelog-git@0.2.1': resolution: {integrity: sha512-x/xEleCFLH28c3bQeQIyeZf8lFXyDFVn1SgcBiR2Tw/r4IAWlk1fzxCEZ6NxQAjF2Nwtczoen3OA2qR+UawQ8Q==} - '@changesets/cli@2.29.0': - resolution: {integrity: sha512-VQdSo9L/Y+PgX1HbytCSftadmHIOK20Y8mOhORDBwaelgjHccxYtO3YBDDhDdaZEPctcuH1YPmIyodHJADXwZA==} + '@changesets/cli@2.29.2': + resolution: {integrity: sha512-vwDemKjGYMOc0l6WUUTGqyAWH3AmueeyoJa1KmFRtCYiCoY5K3B68ErYpDB6H48T4lLI4czum4IEjh6ildxUeg==} hasBin: true '@changesets/config@3.1.1': @@ -494,14 +473,14 @@ packages: '@changesets/get-dependents-graph@2.1.3': resolution: {integrity: sha512-gphr+v0mv2I3Oxt19VdWRRUxq3sseyUpX9DaHpTUmLj92Y10AGy+XOtV+kbM6L/fDcpx7/ISDFK6T8A/P3lOdQ==} - '@changesets/get-release-plan@4.0.8': - resolution: {integrity: sha512-MM4mq2+DQU1ZT7nqxnpveDMTkMBLnwNX44cX7NSxlXmr7f8hO6/S2MXNiXG54uf/0nYnefv0cfy4Czf/ZL/EKQ==} + '@changesets/get-release-plan@4.0.10': + resolution: {integrity: sha512-CCJ/f3edYaA3MqoEnWvGGuZm0uMEMzNJ97z9hdUR34AOvajSwySwsIzC/bBu3+kuGDsB+cny4FljG8UBWAa7jg==} '@changesets/get-version-range-type@0.4.0': resolution: {integrity: sha512-hwawtob9DryoGTpixy1D3ZXbGgJu1Rhr+ySH2PvTLHvkZuQ7sRT4oQwMh0hbqZH1weAooedEjRsbrWcGLCeyVQ==} - '@changesets/git@3.0.2': - resolution: {integrity: sha512-r1/Kju9Y8OxRRdvna+nxpQIsMsRQn9dhhAZt94FLDeu0Hij2hnOozW8iqnHBgvu+KdnJppCveQwK4odwfw/aWQ==} + '@changesets/git@3.0.4': + resolution: {integrity: sha512-BXANzRFkX+XcC1q/d27NKvlJ1yf7PSAgi8JG6dt8EfbHFHi4neau7mufcSca5zRhwOL8j9s6EqsxmT+s+/E6Sw==} '@changesets/logger@0.1.1': resolution: {integrity: sha512-OQtR36ZlnuTxKqoW4Sv6x5YIhOmClRd5pWsjZsddYxpWs517R0HkyiefQPIytCVh4ZcC5x9XaG8KTdd5iRQUfg==} @@ -512,8 +491,8 @@ packages: '@changesets/pre@2.0.2': resolution: {integrity: sha512-HaL/gEyFVvkf9KFg6484wR9s0qjAXlZ8qWPDkTyKF6+zqjBe/I2mygg3MbpZ++hdi0ToqNUF8cjj7fBy0dg8Ug==} - '@changesets/read@0.6.3': - resolution: {integrity: sha512-9H4p/OuJ3jXEUTjaVGdQEhBdqoT2cO5Ts95JTFsQyawmKzpL8FnIeJSyhTDPW1MBRDnwZlHFEM9SpPwJDY5wIg==} + '@changesets/read@0.6.5': + resolution: {integrity: sha512-UPzNGhsSjHD3Veb0xO/MwvasGe8eMyNrR/sT9gR8Q3DhOQZirgKhhXv/8hVsI0QpPjR004Z9iFxoJU6in3uGMg==} '@changesets/should-skip-package@0.1.2': resolution: {integrity: sha512-qAK/WrqWLNCP22UDdBTMPH5f41elVDlsNyat180A33dWxuUDyNpg6fPi/FyTZwRriVjg0L8gnjJn2F9XAoF0qw==} @@ -535,19 +514,19 @@ packages: resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} - '@csstools/color-helpers@5.0.1': - resolution: {integrity: sha512-MKtmkA0BX87PKaO1NFRTFH+UnkgnmySQOvNxJubsadusqPEC2aJ9MOQiMceZJJ6oitUl/i0L6u0M1IrmAOmgBA==} + '@csstools/color-helpers@5.0.2': + resolution: {integrity: sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==} engines: {node: '>=18'} - '@csstools/css-calc@2.1.1': - resolution: {integrity: sha512-rL7kaUnTkL9K+Cvo2pnCieqNpTKgQzy5f+N+5Iuko9HAoasP+xgprVh7KN/MaJVvVL1l0EzQq2MoqBHKSrDrag==} + '@csstools/css-calc@2.1.2': + resolution: {integrity: sha512-TklMyb3uBB28b5uQdxjReG4L80NxAqgrECqLZFQbyLekwwlcDDS8r3f07DKqeo8C4926Br0gf/ZDe17Zv4wIuw==} engines: {node: '>=18'} peerDependencies: '@csstools/css-parser-algorithms': ^3.0.4 '@csstools/css-tokenizer': ^3.0.3 - '@csstools/css-color-parser@3.0.7': - resolution: {integrity: sha512-nkMp2mTICw32uE5NN+EsJ4f5N+IGFeCFu4bGpiKgb2Pq/7J/MpyLBeQ5ry4KKtRFZaYs6sTmcMYrSRIyj5DFKA==} + '@csstools/css-color-parser@3.0.8': + resolution: {integrity: sha512-pdwotQjCCnRPuNi06jFuP68cykU1f3ZWExLe/8MQ1LOs8Xq+fTkYgd+2V8mWUWMrOn9iS2HftPVaMZDaXzGbhQ==} engines: {node: '>=18'} peerDependencies: '@csstools/css-parser-algorithms': ^3.0.4 @@ -569,8 +548,14 @@ packages: '@eggjs/yauzl@2.11.0': resolution: {integrity: sha512-Jq+k2fCZJ3i3HShb0nxLUiAgq5pwo8JTT1TrH22JoehZQ0Nm2dvByGIja1NYfNyuE4Tx5/Dns5nVsBN/mlC8yg==} - '@emnapi/runtime@1.4.1': - resolution: {integrity: sha512-LMshMVP0ZhACNjQNYXiU1iZJ6QCcv0lUdPDPugqGvCGXt5xtRVBPdtA0qU12pEXZzpWAhWlZYptfdAFq10DOVQ==} + '@emnapi/core@1.4.0': + resolution: {integrity: sha512-H+N/FqT07NmLmt6OFFtDfwe8PNygprzBikrEMyQfgqSmT0vzE515Pz7R8izwB9q/zsH/MA64AKoul3sA6/CzVg==} + + '@emnapi/runtime@1.4.0': + resolution: {integrity: sha512-64WYIf4UYcdLnbKn/umDlNjQDSS8AgZrI/R9+x5ilkUVFxXcA1Ebl+gQLc/6mERA4407Xof0R7wEyEuj091CVw==} + + '@emnapi/wasi-threads@1.0.1': + resolution: {integrity: sha512-iIBu7mwkq4UQGeMEM8bLwNK962nXdhodeScX4slfQnRhEMMzvYivHhutCIk8uojvmASXXPC2WNEjwxFWk72Oqw==} '@emotion/babel-plugin@11.13.5': resolution: {integrity: sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==} @@ -1033,54 +1018,46 @@ packages: cpu: [x64] os: [win32] - '@eslint-community/eslint-utils@4.4.1': - resolution: {integrity: sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==} - 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.5.1': resolution: {integrity: sha512-soEIOALTfTK6EjmKMMoLugwaP0rzkad90iIWd1hMO9ARkSAyjfMfkRRhLvD5qH7vvM0Cg72pieUfR6yh6XxC4w==} 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.6.0': - resolution: {integrity: sha512-WhCn7Z7TauhBtmzhvKpoQs0Wwb/kBcy4CwpuI0/eEIr2Lx2auxmulAzLr91wVZJaz47iUZdkXOK7WlAfxGKCnA==} - 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/config-array@0.19.2': - resolution: {integrity: sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w==} + '@eslint/config-array@0.20.0': + resolution: {integrity: sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/config-helpers@0.2.0': - resolution: {integrity: sha512-yJLLmLexii32mGrhW29qvU3QBVTu0GUmEf/J4XsBtVhp4JkIUFN/BjWqTF63yRvGApIDpZm5fa97LtYtINmfeQ==} + '@eslint/config-helpers@0.2.1': + resolution: {integrity: sha512-RI17tsD2frtDu/3dmI7QRrD4bedNKPM08ziRYaC5AhkGrzIAJelm9kJU1TznK+apx6V+cqRz8tfpEeG3oIyjxw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/core@0.12.0': resolution: {integrity: sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/core@0.13.0': + resolution: {integrity: sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/eslintrc@3.3.1': resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/js@9.23.0': - resolution: {integrity: sha512-35MJ8vCPU0ZMxo7zfev2pypqTwWTofFZO6m4KAtdoFhRpLJUpHTZZ+KB3C7Hb1d7bULYwO4lJXGCi5Se+8OMbw==} + '@eslint/js@9.24.0': + resolution: {integrity: sha512-uIY/y3z0uvOGX8cp1C2fiC4+ZmBhp6yZWkojtHL1YEMnRt1Y63HB9TM17proGEmeG7HeUY+UP36F0aknKYTpYA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/object-schema@2.1.6': resolution: {integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/plugin-kit@0.2.7': - resolution: {integrity: sha512-JubJ5B2pJ4k4yGxaNLdbjrnk9d/iDz6/q8wOilpIowd6PJPgaxCuHBnBszq7Ce2TyMrywm5r4PnKm6V3iiZF+g==} + '@eslint/plugin-kit@0.2.8': + resolution: {integrity: sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@floating-ui/core@1.6.9': @@ -1229,8 +1206,8 @@ packages: resolution: {integrity: sha512-0hm2nrToWUdD6/UHnel/UKGdk1//ke5zGUpHIvk5ZWmaKezlGxZkOJXNSWsdxO/rEqTkbB3lNC2J6nBElV2aAQ==} engines: {node: '>=18'} - '@inquirer/checkbox@4.1.2': - resolution: {integrity: sha512-PL9ixC5YsPXzXhAZFUPmkXGxfgjkdfZdPEPPmt4kFwQ4LBMDG9n/nHXYRGGZSKZJs+d1sGKWgS2GiPzVRKUdtQ==} + '@inquirer/checkbox@4.1.5': + resolution: {integrity: sha512-swPczVU+at65xa5uPfNP9u3qx/alNwiaykiI/ExpsmMSQW55trmZcwhYWzw/7fj+n6Q8z1eENvR7vFfq9oPSAQ==} engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' @@ -1242,8 +1219,8 @@ packages: resolution: {integrity: sha512-46yL28o2NJ9doViqOy0VDcoTzng7rAb6yPQKU7VDLqkmbCaH4JqK4yk4XqlzNWy9PVC5pG1ZUXPBQv+VqnYs2w==} engines: {node: '>=18'} - '@inquirer/confirm@5.1.6': - resolution: {integrity: sha512-6ZXYK3M1XmaVBZX6FCfChgtponnL0R6I7k8Nu+kaoNkT828FVZTcca1MqmWQipaW2oNREQl5AaPCUOOCVNdRMw==} + '@inquirer/confirm@5.1.9': + resolution: {integrity: sha512-NgQCnHqFTjF7Ys2fsqK2WtnA8X1kHyInyG+nMIuHowVTIgIuS10T4AznI/PvbqSpJqjCUqNBlKGh1v3bwLFL4w==} engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' @@ -1251,8 +1228,8 @@ packages: '@types/node': optional: true - '@inquirer/core@10.1.7': - resolution: {integrity: sha512-AA9CQhlrt6ZgiSy6qoAigiA1izOa751ugX6ioSjqgJ+/Gd+tEN/TORk5sUYNjXuHWfW0r1n/a6ak4u/NqHHrtA==} + '@inquirer/core@10.1.10': + resolution: {integrity: sha512-roDaKeY1PYY0aCqhRmXihrHjoSW2A00pV3Ke5fTpMCkzcGF64R8e0lw3dK+eLEHwS4vB5RnW1wuQmvzoRul8Mw==} engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' @@ -1268,8 +1245,8 @@ packages: resolution: {integrity: sha512-VA96GPFaSOVudjKFraokEEmUQg/Lub6OXvbIEZU1SDCmBzRkHGhxoFAVaF30nyiB4m5cEbDgiI2QRacXZ2hw9Q==} engines: {node: '>=18'} - '@inquirer/editor@4.2.7': - resolution: {integrity: sha512-gktCSQtnSZHaBytkJKMKEuswSk2cDBuXX5rxGFv306mwHfBPjg5UAldw9zWGoEyvA9KpRDkeM4jfrx0rXn0GyA==} + '@inquirer/editor@4.2.10': + resolution: {integrity: sha512-5GVWJ+qeI6BzR6TIInLP9SXhWCEcvgFQYmcRG6d6RIlhFjM5TyG18paTGBgRYyEouvCmzeco47x9zX9tQEofkw==} engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' @@ -1281,8 +1258,8 @@ packages: resolution: {integrity: sha512-ToG8d6RIbnVpbdPdiN7BCxZGiHOTomOX94C2FaT5KOHupV40tKEDozp12res6cMIfRKrXLJyexAZhWVHgbALSQ==} engines: {node: '>=18'} - '@inquirer/expand@4.0.9': - resolution: {integrity: sha512-Xxt6nhomWTAmuSX61kVgglLjMEFGa+7+F6UUtdEUeg7fg4r9vaFttUUKrtkViYYrQBA5Ia1tkOJj2koP9BuLig==} + '@inquirer/expand@4.0.12': + resolution: {integrity: sha512-jV8QoZE1fC0vPe6TnsOfig+qwu7Iza1pkXoUJ3SroRagrt2hxiL+RbM432YAihNR7m7XnU0HWl/WQ35RIGmXHw==} engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' @@ -1290,10 +1267,6 @@ packages: '@types/node': optional: true - '@inquirer/figures@1.0.10': - resolution: {integrity: sha512-Ey6176gZmeqZuY/W/nZiUyvmb1/qInjcpiZjXWi6nON+nxJpD1bxtSoBxNliGISae32n6OwbY+TSXPZ1CfS4bw==} - engines: {node: '>=18'} - '@inquirer/figures@1.0.11': resolution: {integrity: sha512-eOg92lvrn/aRUqbxRyvpEWnrvRuTYRifixHkYVpJiygTgVSBIHDqLh0SrMQXkafvULg3ck11V7xvR+zcgvpHFw==} engines: {node: '>=18'} @@ -1302,8 +1275,8 @@ packages: resolution: {integrity: sha512-BDuPBmpvi8eMCxqC5iacloWqv+5tQSJlUafYWUe31ow1BVXjW2a5qe3dh4X/Z25Wp22RwvcaLCc2siHobEOfzg==} engines: {node: '>=18'} - '@inquirer/input@4.1.6': - resolution: {integrity: sha512-1f5AIsZuVjPT4ecA8AwaxDFNHny/tSershP/cTvTDxLdiIGTeILNcKozB0LaYt6mojJLUbOYhpIxicaYf7UKIQ==} + '@inquirer/input@4.1.9': + resolution: {integrity: sha512-mshNG24Ij5KqsQtOZMgj5TwEjIf+F2HOESk6bjMwGWgcH5UBe8UoljwzNFHqdMbGYbgAf6v2wU/X9CAdKJzgOA==} engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' @@ -1315,8 +1288,8 @@ packages: resolution: {integrity: sha512-QpR8jPhRjSmlr/mD2cw3IR8HRO7lSVOnqUvQa8scv1Lsr3xoAMMworcYW3J13z3ppjBFBD2ef1Ci6AE5Qn8goQ==} engines: {node: '>=18'} - '@inquirer/number@3.0.9': - resolution: {integrity: sha512-iN2xZvH3tyIYXLXBvlVh0npk1q/aVuKXZo5hj+K3W3D4ngAEq/DkLpofRzx6oebTUhBvOgryZ+rMV0yImKnG3w==} + '@inquirer/number@3.0.12': + resolution: {integrity: sha512-7HRFHxbPCA4e4jMxTQglHJwP+v/kpFsCf2szzfBHy98Wlc3L08HL76UDiA87TOdX5fwj2HMOLWqRWv9Pnn+Z5Q==} engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' @@ -1328,8 +1301,8 @@ packages: resolution: {integrity: sha512-haoeEPUisD1NeE2IanLOiFr4wcTXGWrBOyAyPZi1FfLJuXOzNmxCJPgUrGYKVh+Y8hfGJenIfz5Wb/DkE9KkMQ==} engines: {node: '>=18'} - '@inquirer/password@4.0.9': - resolution: {integrity: sha512-xBEoOw1XKb0rIN208YU7wM7oJEHhIYkfG7LpTJAEW913GZeaoQerzf5U/LSHI45EVvjAdgNXmXgH51cUXKZcJQ==} + '@inquirer/password@4.0.12': + resolution: {integrity: sha512-FlOB0zvuELPEbnBYiPaOdJIaDzb2PmJ7ghi/SVwIHDDSQ2K4opGBkF+5kXOg6ucrtSUQdLhVVY5tycH0j0l+0g==} engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' @@ -1341,8 +1314,8 @@ packages: resolution: {integrity: sha512-yl43JD/86CIj3Mz5mvvLJqAOfIup7ncxfJ0Btnl0/v5TouVUyeEdcpknfgc+yMevS/48oH9WAkkw93m7otLb/A==} engines: {node: '>=18'} - '@inquirer/prompts@7.3.2': - resolution: {integrity: sha512-G1ytyOoHh5BphmEBxSwALin3n1KGNYB6yImbICcRQdzXfOGbuJ9Jske/Of5Sebk339NSGGNfUshnzK8YWkTPsQ==} + '@inquirer/prompts@7.4.1': + resolution: {integrity: sha512-UlmM5FVOZF0gpoe1PT/jN4vk8JmpIWBlMvTL8M+hlvPmzN89K6z03+IFmyeu/oFCenwdwHDr2gky7nIGSEVvlA==} engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' @@ -1354,8 +1327,8 @@ packages: resolution: {integrity: sha512-VgRtFIwZInUzTiPLSfDXK5jLrnpkuSOh1ctfaoygKAdPqjcjKYmGh6sCY1pb0aGnCGsmhUxoqLDUAU0ud+lGXQ==} engines: {node: '>=18'} - '@inquirer/rawlist@4.0.9': - resolution: {integrity: sha512-+5t6ebehKqgoxV8fXwE49HkSF2Rc9ijNiVGEQZwvbMI61/Q5RcD+jWD6Gs1tKdz5lkI8GRBL31iO0HjGK1bv+A==} + '@inquirer/rawlist@4.0.12': + resolution: {integrity: sha512-wNPJZy8Oc7RyGISPxp9/MpTOqX8lr0r+lCCWm7hQra+MDtYRgINv1hxw7R+vKP71Bu/3LszabxOodfV/uTfsaA==} engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' @@ -1367,8 +1340,8 @@ packages: resolution: {integrity: sha512-r5hBKZk3g5MkIzLVoSgE4evypGqtOannnB3PKTG9NRZxyFRKcfzrdxXXPcoJQsxJPzvdSU2Rn7pB7lw0GCmGAg==} engines: {node: '>=18'} - '@inquirer/search@3.0.9': - resolution: {integrity: sha512-DWmKztkYo9CvldGBaRMr0ETUHgR86zE6sPDVOHsqz4ISe9o1LuiWfgJk+2r75acFclA93J/lqzhT0dTjCzHuoA==} + '@inquirer/search@3.0.12': + resolution: {integrity: sha512-H/kDJA3kNlnNIjB8YsaXoQI0Qccgf0Na14K1h8ExWhNmUg2E941dyFPrZeugihEa9AZNW5NdsD/NcvUME83OPQ==} engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' @@ -1380,8 +1353,8 @@ packages: resolution: {integrity: sha512-lUDGUxPhdWMkN/fHy1Lk7pF3nK1fh/gqeyWXmctefhxLYxlDsc7vsPBEpxrfVGDsVdyYJsiJoD4bJ1b623cV1Q==} engines: {node: '>=18'} - '@inquirer/select@4.0.9': - resolution: {integrity: sha512-BpJyJe7Dkhv2kz7yG7bPSbJLQuu/rqyNlF1CfiiFeFwouegfH+zh13KDyt6+d9DwucKo7hqM3wKLLyJxZMO+Xg==} + '@inquirer/select@4.1.1': + resolution: {integrity: sha512-IUXzzTKVdiVNMA+2yUvPxWsSgOG4kfX93jOM4Zb5FgujeInotv5SPIJVeXQ+fO4xu7tW8VowFhdG5JRmmCyQ1Q==} engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' @@ -1393,8 +1366,8 @@ packages: resolution: {integrity: sha512-XvJRx+2KR3YXyYtPUUy+qd9i7p+GO9Ko6VIIpWlBrpWwXDv8WLFeHTxz35CfQFUiBMLXlGHhGzys7lqit9gWag==} engines: {node: '>=18'} - '@inquirer/type@3.0.4': - resolution: {integrity: sha512-2MNFrDY8jkFYc9Il9DgLsHhMzuHnOYM1+CUYVWbzu9oT0hC7V7EcYvdCKeoll/Fcci04A+ERZ9wcc7cQ8lTkIA==} + '@inquirer/type@3.0.6': + resolution: {integrity: sha512-/mKVCtVpyBu3IDarv0G+59KC4stsD5mDsGpYh+GKs1NZT88Jh52+cuoA1AtLk2Q0r/quNl+1cSUyLRHBFeD0XA==} engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' @@ -1555,11 +1528,11 @@ packages: '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} - '@jspm/core@2.0.1': - resolution: {integrity: sha512-Lg3PnLp0QXpxwLIAuuJboLeRaIhrgJjeuh797QADg3xz8wGLugQOS5DpsE8A6i6Adgzf+bacllkKZG3J0tGfDw==} + '@jspm/core@2.1.0': + resolution: {integrity: sha512-3sRl+pkyFY/kLmHl0cgHiFp2xEqErA8N3ECjMs7serSUBmoJ70lBa0PG5t0IM6WJgdZNyyI0R8YFfi5wM8+mzg==} - '@lambdatest/node-tunnel@4.0.8': - resolution: {integrity: sha512-IY42aDD4Ryqjug9V4wpCjckKpHjC2zrU/XhhorR5ztX088XITRFKUo8U6+gOjy/V8kAB+EgDuIXfK0izXbt9Ow==} + '@lambdatest/node-tunnel@4.0.9': + resolution: {integrity: sha512-n4s2MpgqVkWZzYwEpoRUsJZJfsE2UCcbfd88zqTqZStWIw7Y4+fZfxP/6QK/yWTRNLK0/ZwwGkP814beQU1mzA==} '@manypkg/find-root@1.1.0': resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==} @@ -1570,6 +1543,9 @@ packages: '@mdx-js/mdx@2.3.0': resolution: {integrity: sha512-jLuwRlz8DQfQNiUCJR50Y09CGPq3fLtmtUQfVrj79E0JWu3dvsVcxVIcfhR5h0iXu+/z++zDrYeiJqifRynJkA==} + '@napi-rs/wasm-runtime@0.2.8': + resolution: {integrity: sha512-OBlgKdX7gin7OIq4fadsjpg+cp2ZphvAIKucHsNfTdJiqdOmOEwQd/bHi0VwNrcw5xpBJyUw6cK/QilCqy1BSg==} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -1602,57 +1578,57 @@ packages: resolution: {integrity: sha512-gGq0NJkIGSwdbUt4yhdF8ZrmkGKVz9vAdVzpOfnom+V8PLSmSOVhZwbNvZZS1EYcJN5hzzKBxmmVVAInM6HQLg==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - '@octokit/auth-token@4.0.0': - resolution: {integrity: sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA==} + '@octokit/auth-token@5.1.2': + resolution: {integrity: sha512-JcQDsBdg49Yky2w2ld20IHAlwr8d/d8N6NiOXbtuoPCqzbsiJgF633mVUw3x4mo0H5ypataQIX7SFu3yy44Mpw==} engines: {node: '>= 18'} - '@octokit/core@5.2.0': - resolution: {integrity: sha512-1LFfa/qnMQvEOAdzlQymH0ulepxbxnCYAKJZfMci/5XJyIHWgEYnDmgnKakbTh7CH2tFQ5O60oYDvns4i9RAIg==} + '@octokit/core@6.1.4': + resolution: {integrity: sha512-lAS9k7d6I0MPN+gb9bKDt7X8SdxknYqAMh44S5L+lNqIN2NuV8nvv3g8rPp7MuRxcOpxpUIATWprO0C34a8Qmg==} engines: {node: '>= 18'} - '@octokit/endpoint@9.0.6': - resolution: {integrity: sha512-H1fNTMA57HbkFESSt3Y9+FBICv+0jFceJFPWDePYlR/iMGrwM5ph+Dd4XRQs+8X+PUFURLQgX9ChPfhJ/1uNQw==} + '@octokit/endpoint@10.1.3': + resolution: {integrity: sha512-nBRBMpKPhQUxCsQQeW+rCJ/OPSMcj3g0nfHn01zGYZXuNDvvXudF/TYY6APj5THlurerpFN4a/dQAIAaM6BYhA==} engines: {node: '>= 18'} - '@octokit/graphql@7.1.0': - resolution: {integrity: sha512-r+oZUH7aMFui1ypZnAvZmn0KSqAUgE1/tUXIWaqUCa1758ts/Jio84GZuzsvUkme98kv0WFY8//n0J1Z+vsIsQ==} + '@octokit/graphql@8.2.1': + resolution: {integrity: sha512-n57hXtOoHrhwTWdvhVkdJHdhTv0JstjDbDRhJfwIRNfFqmSo1DaK/mD2syoNUoLCyqSjBpGAKOG0BuwF392slw==} engines: {node: '>= 18'} - '@octokit/openapi-types@23.0.1': - resolution: {integrity: sha512-izFjMJ1sir0jn0ldEKhZ7xegCTj/ObmEDlEfpFrx4k/JyZSMRHbO3/rBwgE7f3m2DHt+RrNGIVw4wSmwnm3t/g==} + '@octokit/openapi-types@24.2.0': + resolution: {integrity: sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==} - '@octokit/plugin-paginate-rest@11.3.1': - resolution: {integrity: sha512-ryqobs26cLtM1kQxqeZui4v8FeznirUsksiA+RYemMPJ7Micju0WSkv50dBksTuZks9O5cg4wp+t8fZ/cLY56g==} + '@octokit/plugin-paginate-rest@11.6.0': + resolution: {integrity: sha512-n5KPteiF7pWKgBIBJSk8qzoZWcUkza2O6A0za97pMGVrGfPdltxrfmfF5GucHYvHGZD8BdaZmmHGz5cX/3gdpw==} engines: {node: '>= 18'} peerDependencies: - '@octokit/core': '5' + '@octokit/core': '>=6' - '@octokit/plugin-request-log@4.0.1': - resolution: {integrity: sha512-GihNqNpGHorUrO7Qa9JbAl0dbLnqJVrV8OXe2Zm5/Y4wFkZQDfTreBzVmiRfJVfE4mClXdihHnbpyyO9FSX4HA==} + '@octokit/plugin-request-log@5.3.1': + resolution: {integrity: sha512-n/lNeCtq+9ofhC15xzmJCNKP2BWTv8Ih2TTy+jatNCCq/gQP/V7rK3fjIfuz0pDWDALO/o/4QY4hyOF6TQQFUw==} engines: {node: '>= 18'} peerDependencies: - '@octokit/core': '5' + '@octokit/core': '>=6' - '@octokit/plugin-rest-endpoint-methods@13.2.2': - resolution: {integrity: sha512-EI7kXWidkt3Xlok5uN43suK99VWqc8OaIMktY9d9+RNKl69juoTyxmLoWPIZgJYzi41qj/9zU7G/ljnNOJ5AFA==} + '@octokit/plugin-rest-endpoint-methods@13.5.0': + resolution: {integrity: sha512-9Pas60Iv9ejO3WlAX3maE1+38c5nqbJXV5GrncEfkndIpZrJ/WPMRd2xYDcPPEt5yzpxcjw9fWNoPhsSGzqKqw==} engines: {node: '>= 18'} peerDependencies: - '@octokit/core': ^5 + '@octokit/core': '>=6' - '@octokit/request-error@5.1.1': - resolution: {integrity: sha512-v9iyEQJH6ZntoENr9/yXxjuezh4My67CBSu9r6Ve/05Iu5gNgnisNWOsoJHTP6k0Rr0+HQIpnH+kyammu90q/g==} + '@octokit/request-error@6.1.7': + resolution: {integrity: sha512-69NIppAwaauwZv6aOzb+VVLwt+0havz9GT5YplkeJv7fG7a40qpLt/yZKyiDxAhgz0EtgNdNcb96Z0u+Zyuy2g==} engines: {node: '>= 18'} - '@octokit/request@8.4.1': - resolution: {integrity: sha512-qnB2+SY3hkCmBxZsR/MPCybNmbJe4KAlfWErXq+rBKkQJlbjdJeS85VI9r8UqeLYLvnAenU8Q1okM/0MBsAGXw==} + '@octokit/request@9.2.2': + resolution: {integrity: sha512-dZl0ZHx6gOQGcffgm1/Sf6JfEpmh34v3Af2Uci02vzUYz6qEN6zepoRtmybWXIGXFIK8K9ylE3b+duCWqhArtg==} engines: {node: '>= 18'} - '@octokit/rest@20.1.1': - resolution: {integrity: sha512-MB4AYDsM5jhIHro/dq4ix1iWTLGToIGk6cWF5L6vanFaMble5jTX/UBQyiv05HsWnwUtY8JrfHy2LWfKwihqMw==} + '@octokit/rest@21.0.2': + resolution: {integrity: sha512-+CiLisCoyWmYicH25y1cDfCrv41kRSvTq6pPWtRroRJzhsCZWZyCqGyI8foJT5LmScADSwRAnr/xo+eewL04wQ==} engines: {node: '>= 18'} - '@octokit/types@13.8.0': - resolution: {integrity: sha512-x7DjTIbEpEWXK99DMd01QfWy0hd5h4EN+Q7shkdKds3otGQP+oWE/y0A76i1OvH9fygo4ddvNf7ZvF0t78P98A==} + '@octokit/types@13.10.0': + resolution: {integrity: sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==} '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} @@ -1682,30 +1658,20 @@ packages: '@promptbook/utils@0.69.5': resolution: {integrity: sha512-xm5Ti/Hp3o4xHrsK9Yy3MS6KbDxYbq485hDsFvxqaNA7equHLPdo8H8faTitTeb14QCDfLW4iwCxdVYu5sn6YQ==} - '@puppeteer/browsers@2.10.0': - resolution: {integrity: sha512-HdHF4rny4JCvIcm7V1dpvpctIGqM3/Me255CB44vW7hDG1zYMmcBMjpNqZEDxdCfXGLkx5kP0+Jz5DUS+ukqtA==} - engines: {node: '>=18'} - hasBin: true - - '@puppeteer/browsers@2.7.1': - resolution: {integrity: sha512-MK7rtm8JjaxPN7Mf1JdZIZKPD2Z+W7osvrC1vjpvfOX1K0awDIHYbNi89f7eotp7eMUn2shWnt03HwVbriXtKQ==} - engines: {node: '>=18'} - hasBin: true - '@puppeteer/browsers@2.9.0': resolution: {integrity: sha512-8+xM+cFydYET4X/5/3yZMHs7sjS6c9I6H5I3xJdb6cinzxWUT/I2QVw4avxCQ8QDndwdHkG/FiSZIrCjAbaKvQ==} engines: {node: '>=18'} hasBin: true - '@remix-run/dev@2.15.3': - resolution: {integrity: sha512-agndQJHs7qISPXXH/Zet0VHWvcwtQGoEOXxltjerNQ2zWcAJQBm9i06tS6n6v/ZP0sHIVdo/IsvgAA4wetqmNw==} + '@remix-run/dev@2.16.5': + resolution: {integrity: sha512-vr34lMxekgO9rM91iVwg5/EVreJ++PAK9mGJpyW8AGe50cJ3QBFuH1PQWKiworvBYNdzEbW9Sxc52iFugnLMCQ==} engines: {node: '>=18.0.0'} hasBin: true peerDependencies: - '@remix-run/react': ^2.15.3 - '@remix-run/serve': ^2.15.3 + '@remix-run/react': ^2.16.5 + '@remix-run/serve': ^2.16.5 typescript: ^5.1.0 - vite: ^5.1.0 + vite: ^5.1.0 || ^6.0.0 wrangler: ^3.28.2 peerDependenciesMeta: '@remix-run/serve': @@ -1717,8 +1683,8 @@ packages: wrangler: optional: true - '@remix-run/express@2.16.4': - resolution: {integrity: sha512-VQ78Jq1BctEDXRkkPsR1ixMDuwUzW89MaPa+dFJ5f7V2et+0LMXEKmriLFWdCqToN88QM0GjMhSi5UkUUWLIjQ==} + '@remix-run/express@2.16.5': + resolution: {integrity: sha512-FBaHxTaHYqGEjBN/WGMcXsicU7NakQ/+1463fYo8eTshNhxXDktqRkPuScbEJlwvAtZ051RancyoPx2a0O8nAQ==} engines: {node: '>=18.0.0'} peerDependencies: express: ^4.20.0 @@ -1727,24 +1693,6 @@ packages: typescript: optional: true - '@remix-run/node@2.15.3': - resolution: {integrity: sha512-TYfS6BPhbABBpSRZ6WBA4qIWSwWvJhRVQGXCHUtgOwkuW863rcFmjh9g2Xj/IHyTmbOYPdcjHsIgZ9el4CHOKQ==} - engines: {node: '>=18.0.0'} - peerDependencies: - typescript: ^5.1.0 - peerDependenciesMeta: - typescript: - optional: true - - '@remix-run/node@2.16.4': - resolution: {integrity: sha512-scZwpc78cJS8dx6LMLemVHqnaAVbPWasx36TxYWUASA63hxPx5XbGbb6pe4cSpT8dWqhBtsPpHZoFTrM1aqx7A==} - engines: {node: '>=18.0.0'} - peerDependencies: - typescript: ^5.1.0 - peerDependenciesMeta: - typescript: - optional: true - '@remix-run/node@2.16.5': resolution: {integrity: sha512-awunS1kgFmc8q7sGz7FpGf66RXQm2Vw0yk5IFTIJa0WdQrskqyF/6CO+tEf/0np/OCu2fQ23+EfxY2qGHm1aOg==} engines: {node: '>=18.0.0'} @@ -1765,37 +1713,15 @@ packages: typescript: optional: true - '@remix-run/router@1.22.0': - resolution: {integrity: sha512-MBOl8MeOzpK0HQQQshKB7pABXbmyHizdTpqnrIseTbsv0nAepwC2ENZa1aaBExNQcpLoXmWthhak8SABLzvGPw==} - engines: {node: '>=14.0.0'} - '@remix-run/router@1.23.0': resolution: {integrity: sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==} engines: {node: '>=14.0.0'} - '@remix-run/serve@2.16.4': - resolution: {integrity: sha512-9jYxxMI1RTKv7t1N53XzG3vQruWAaj6AYg4G5OoejtO7PZ6CbgbbHEiNa/f5ww3vhfcqZH7dfetDBdUYQ5PxVQ==} + '@remix-run/serve@2.16.5': + resolution: {integrity: sha512-hPrTY7ez/FVTxZamvODkjHlGEkbzurBB3krR+VwPLorQGz6+C2/eOJaQ3V1vVuBoeDWS5XgvwFQWtyi5CSezEA==} engines: {node: '>=18.0.0'} hasBin: true - '@remix-run/server-runtime@2.15.3': - resolution: {integrity: sha512-taHBe1DEqxZNjjj6OfkSYbup+sZPjbTgUhykaI+nHqrC2NDQuTiisBXhLwtx60GctONR/x0lWhF7R9ZGC5WsHw==} - engines: {node: '>=18.0.0'} - peerDependencies: - typescript: ^5.1.0 - peerDependenciesMeta: - typescript: - optional: true - - '@remix-run/server-runtime@2.16.4': - resolution: {integrity: sha512-6M1Lq2mrH5zfGN++ay+a2KzdPqOh2TB7n6wYPPXA0rxQan8c5NZCvDFF635KS65LSzZDB+2VfFTgoPBERdYkYg==} - engines: {node: '>=18.0.0'} - peerDependencies: - typescript: ^5.1.0 - peerDependenciesMeta: - typescript: - optional: true - '@remix-run/server-runtime@2.16.5': resolution: {integrity: sha512-LGGNEJoior2zvgtqyPC5tVPucAvewovQvL4ztC5yE8ZszHmLri9bB9YYWvHqsiT+EaunKHNLmI8jpJHe3PEKhA==} engines: {node: '>=18.0.0'} @@ -1821,103 +1747,103 @@ packages: '@remix-run/web-stream@1.1.0': resolution: {integrity: sha512-KRJtwrjRV5Bb+pM7zxcTJkhIqWWSy+MYsIxHK+0m5atcznsf15YwUBWHWulZerV2+vvHH1Lp1DD7pw6qKW8SgA==} - '@rollup/rollup-android-arm-eabi@4.40.0': - resolution: {integrity: sha512-+Fbls/diZ0RDerhE8kyC6hjADCXA1K4yVNlH0EYfd2XjyH0UGgzaQ8MlT0pCXAThfxv3QUAczHaL+qSv1E4/Cg==} + '@rollup/rollup-android-arm-eabi@4.39.0': + resolution: {integrity: sha512-lGVys55Qb00Wvh8DMAocp5kIcaNzEFTmGhfFd88LfaogYTRKrdxgtlO5H6S49v2Nd8R2C6wLOal0qv6/kCkOwA==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.40.0': - resolution: {integrity: sha512-PPA6aEEsTPRz+/4xxAmaoWDqh67N7wFbgFUJGMnanCFs0TV99M0M8QhhaSCks+n6EbQoFvLQgYOGXxlMGQe/6w==} + '@rollup/rollup-android-arm64@4.39.0': + resolution: {integrity: sha512-It9+M1zE31KWfqh/0cJLrrsCPiF72PoJjIChLX+rEcujVRCb4NLQ5QzFkzIZW8Kn8FTbvGQBY5TkKBau3S8cCQ==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.40.0': - resolution: {integrity: sha512-GwYOcOakYHdfnjjKwqpTGgn5a6cUX7+Ra2HeNj/GdXvO2VJOOXCiYYlRFU4CubFM67EhbmzLOmACKEfvp3J1kQ==} + '@rollup/rollup-darwin-arm64@4.39.0': + resolution: {integrity: sha512-lXQnhpFDOKDXiGxsU9/l8UEGGM65comrQuZ+lDcGUx+9YQ9dKpF3rSEGepyeR5AHZ0b5RgiligsBhWZfSSQh8Q==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.40.0': - resolution: {integrity: sha512-CoLEGJ+2eheqD9KBSxmma6ld01czS52Iw0e2qMZNpPDlf7Z9mj8xmMemxEucinev4LgHalDPczMyxzbq+Q+EtA==} + '@rollup/rollup-darwin-x64@4.39.0': + resolution: {integrity: sha512-mKXpNZLvtEbgu6WCkNij7CGycdw9cJi2k9v0noMb++Vab12GZjFgUXD69ilAbBh034Zwn95c2PNSz9xM7KYEAQ==} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.40.0': - resolution: {integrity: sha512-r7yGiS4HN/kibvESzmrOB/PxKMhPTlz+FcGvoUIKYoTyGd5toHp48g1uZy1o1xQvybwwpqpe010JrcGG2s5nkg==} + '@rollup/rollup-freebsd-arm64@4.39.0': + resolution: {integrity: sha512-jivRRlh2Lod/KvDZx2zUR+I4iBfHcu2V/BA2vasUtdtTN2Uk3jfcZczLa81ESHZHPHy4ih3T/W5rPFZ/hX7RtQ==} cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.40.0': - resolution: {integrity: sha512-mVDxzlf0oLzV3oZOr0SMJ0lSDd3xC4CmnWJ8Val8isp9jRGl5Dq//LLDSPFrasS7pSm6m5xAcKaw3sHXhBjoRw==} + '@rollup/rollup-freebsd-x64@4.39.0': + resolution: {integrity: sha512-8RXIWvYIRK9nO+bhVz8DwLBepcptw633gv/QT4015CpJ0Ht8punmoHU/DuEd3iw9Hr8UwUV+t+VNNuZIWYeY7Q==} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.40.0': - resolution: {integrity: sha512-y/qUMOpJxBMy8xCXD++jeu8t7kzjlOCkoxxajL58G62PJGBZVl/Gwpm7JK9+YvlB701rcQTzjUZ1JgUoPTnoQA==} + '@rollup/rollup-linux-arm-gnueabihf@4.39.0': + resolution: {integrity: sha512-mz5POx5Zu58f2xAG5RaRRhp3IZDK7zXGk5sdEDj4o96HeaXhlUwmLFzNlc4hCQi5sGdR12VDgEUqVSHer0lI9g==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm-musleabihf@4.40.0': - resolution: {integrity: sha512-GoCsPibtVdJFPv/BOIvBKO/XmwZLwaNWdyD8TKlXuqp0veo2sHE+A/vpMQ5iSArRUz/uaoj4h5S6Pn0+PdhRjg==} + '@rollup/rollup-linux-arm-musleabihf@4.39.0': + resolution: {integrity: sha512-+YDwhM6gUAyakl0CD+bMFpdmwIoRDzZYaTWV3SDRBGkMU/VpIBYXXEvkEcTagw/7VVkL2vA29zU4UVy1mP0/Yw==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm64-gnu@4.40.0': - resolution: {integrity: sha512-L5ZLphTjjAD9leJzSLI7rr8fNqJMlGDKlazW2tX4IUF9P7R5TMQPElpH82Q7eNIDQnQlAyiNVfRPfP2vM5Avvg==} + '@rollup/rollup-linux-arm64-gnu@4.39.0': + resolution: {integrity: sha512-EKf7iF7aK36eEChvlgxGnk7pdJfzfQbNvGV/+l98iiMwU23MwvmV0Ty3pJ0p5WQfm3JRHOytSIqD9LB7Bq7xdQ==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-arm64-musl@4.40.0': - resolution: {integrity: sha512-ATZvCRGCDtv1Y4gpDIXsS+wfFeFuLwVxyUBSLawjgXK2tRE6fnsQEkE4csQQYWlBlsFztRzCnBvWVfcae/1qxQ==} + '@rollup/rollup-linux-arm64-musl@4.39.0': + resolution: {integrity: sha512-vYanR6MtqC7Z2SNr8gzVnzUul09Wi1kZqJaek3KcIlI/wq5Xtq4ZPIZ0Mr/st/sv/NnaPwy/D4yXg5x0B3aUUA==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-loongarch64-gnu@4.40.0': - resolution: {integrity: sha512-wG9e2XtIhd++QugU5MD9i7OnpaVb08ji3P1y/hNbxrQ3sYEelKJOq1UJ5dXczeo6Hj2rfDEL5GdtkMSVLa/AOg==} + '@rollup/rollup-linux-loongarch64-gnu@4.39.0': + resolution: {integrity: sha512-NMRUT40+h0FBa5fb+cpxtZoGAggRem16ocVKIv5gDB5uLDgBIwrIsXlGqYbLwW8YyO3WVTk1FkFDjMETYlDqiw==} cpu: [loong64] os: [linux] - '@rollup/rollup-linux-powerpc64le-gnu@4.40.0': - resolution: {integrity: sha512-vgXfWmj0f3jAUvC7TZSU/m/cOE558ILWDzS7jBhiCAFpY2WEBn5jqgbqvmzlMjtp8KlLcBlXVD2mkTSEQE6Ixw==} + '@rollup/rollup-linux-powerpc64le-gnu@4.39.0': + resolution: {integrity: sha512-0pCNnmxgduJ3YRt+D+kJ6Ai/r+TaePu9ZLENl+ZDV/CdVczXl95CbIiwwswu4L+K7uOIGf6tMo2vm8uadRaICQ==} cpu: [ppc64] os: [linux] - '@rollup/rollup-linux-riscv64-gnu@4.40.0': - resolution: {integrity: sha512-uJkYTugqtPZBS3Z136arevt/FsKTF/J9dEMTX/cwR7lsAW4bShzI2R0pJVw+hcBTWF4dxVckYh72Hk3/hWNKvA==} + '@rollup/rollup-linux-riscv64-gnu@4.39.0': + resolution: {integrity: sha512-t7j5Zhr7S4bBtksT73bO6c3Qa2AV/HqiGlj9+KB3gNF5upcVkx+HLgxTm8DK4OkzsOYqbdqbLKwvGMhylJCPhQ==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-riscv64-musl@4.40.0': - resolution: {integrity: sha512-rKmSj6EXQRnhSkE22+WvrqOqRtk733x3p5sWpZilhmjnkHkpeCgWsFFo0dGnUGeA+OZjRl3+VYq+HyCOEuwcxQ==} + '@rollup/rollup-linux-riscv64-musl@4.39.0': + resolution: {integrity: sha512-m6cwI86IvQ7M93MQ2RF5SP8tUjD39Y7rjb1qjHgYh28uAPVU8+k/xYWvxRO3/tBN2pZkSMa5RjnPuUIbrwVxeA==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-s390x-gnu@4.40.0': - resolution: {integrity: sha512-SpnYlAfKPOoVsQqmTFJ0usx0z84bzGOS9anAC0AZ3rdSo3snecihbhFTlJZ8XMwzqAcodjFU4+/SM311dqE5Sw==} + '@rollup/rollup-linux-s390x-gnu@4.39.0': + resolution: {integrity: sha512-iRDJd2ebMunnk2rsSBYlsptCyuINvxUfGwOUldjv5M4tpa93K8tFMeYGpNk2+Nxl+OBJnBzy2/JCscGeO507kA==} cpu: [s390x] os: [linux] - '@rollup/rollup-linux-x64-gnu@4.40.0': - resolution: {integrity: sha512-RcDGMtqF9EFN8i2RYN2W+64CdHruJ5rPqrlYw+cgM3uOVPSsnAQps7cpjXe9be/yDp8UC7VLoCoKC8J3Kn2FkQ==} + '@rollup/rollup-linux-x64-gnu@4.39.0': + resolution: {integrity: sha512-t9jqYw27R6Lx0XKfEFe5vUeEJ5pF3SGIM6gTfONSMb7DuG6z6wfj2yjcoZxHg129veTqU7+wOhY6GX8wmf90dA==} cpu: [x64] os: [linux] - '@rollup/rollup-linux-x64-musl@4.40.0': - resolution: {integrity: sha512-HZvjpiUmSNx5zFgwtQAV1GaGazT2RWvqeDi0hV+AtC8unqqDSsaFjPxfsO6qPtKRRg25SisACWnJ37Yio8ttaw==} + '@rollup/rollup-linux-x64-musl@4.39.0': + resolution: {integrity: sha512-ThFdkrFDP55AIsIZDKSBWEt/JcWlCzydbZHinZ0F/r1h83qbGeenCt/G/wG2O0reuENDD2tawfAj2s8VK7Bugg==} cpu: [x64] os: [linux] - '@rollup/rollup-win32-arm64-msvc@4.40.0': - resolution: {integrity: sha512-UtZQQI5k/b8d7d3i9AZmA/t+Q4tk3hOC0tMOMSq2GlMYOfxbesxG4mJSeDp0EHs30N9bsfwUvs3zF4v/RzOeTQ==} + '@rollup/rollup-win32-arm64-msvc@4.39.0': + resolution: {integrity: sha512-jDrLm6yUtbOg2TYB3sBF3acUnAwsIksEYjLeHL+TJv9jg+TmTwdyjnDex27jqEMakNKf3RwwPahDIt7QXCSqRQ==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.40.0': - resolution: {integrity: sha512-+m03kvI2f5syIqHXCZLPVYplP8pQch9JHyXKZ3AGMKlg8dCyr2PKHjwRLiW53LTrN/Nc3EqHOKxUxzoSPdKddA==} + '@rollup/rollup-win32-ia32-msvc@4.39.0': + resolution: {integrity: sha512-6w9uMuza+LbLCVoNKL5FSLE7yvYkq9laSd09bwS0tMjkwXrmib/4KmoJcrKhLWHvw19mwU+33ndC69T7weNNjQ==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.40.0': - resolution: {integrity: sha512-lpPE1cLfP5oPzVjKMx10pgBmKELQnFJXHgvtHCtuJWOv8MxqdEIMNtgHgBFf7Ea2/7EuVwa9fodWUfXAlXZLZQ==} + '@rollup/rollup-win32-x64-msvc@4.39.0': + resolution: {integrity: sha512-yAkUOkIKZlK5dl7u6dg897doBgLXmUHhIINM2c+sND3DZwnrdQkkSiDh7N75Ll4mM4dxSkYfXqU9fW3lLkMFug==} cpu: [x64] os: [win32] @@ -1967,6 +1893,9 @@ packages: '@tsconfig/node20@20.1.5': resolution: {integrity: sha512-Vm8e3WxDTqMGPU4GATF9keQAIy1Drd7bPwlgzKJnZtoOsTm1tduUTbDjg0W5qERvGuxPI2h9RbMufH0YdfBylA==} + '@tybys/wasm-util@0.9.0': + resolution: {integrity: sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==} + '@types/acorn@4.0.6': resolution: {integrity: sha512-veQTnWP+1D/xbxVrPC3zHnCZRjSrKfhbMUlEA43iMZLu7EsnTtkJklIuwrCPbOi8YkvDQAiW05VQQFvvz9oieQ==} @@ -1985,9 +1914,6 @@ packages: '@types/estree-jsx@1.0.5': resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} - '@types/estree@1.0.6': - resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} - '@types/estree@1.0.7': resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==} @@ -2048,32 +1974,26 @@ packages: '@types/node@16.9.1': resolution: {integrity: sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g==} - '@types/node@20.17.28': - resolution: {integrity: sha512-DHlH/fNL6Mho38jTy7/JT7sn2wnXI+wULR6PV4gy4VHLVvnrV/d3pHAMQHhc4gjdLmK2ZiPoMxzp6B3yRajLSQ==} - '@types/node@20.17.30': resolution: {integrity: sha512-7zf4YyHA+jvBNfVrk2Gtvs6x7E8V+YDW05bNfG2XkWDJfYRXrTiP/DsB2zSYTaHX0bGIujTBQdMVAhb+j7mwpg==} - '@types/node@22.13.4': - resolution: {integrity: sha512-ywP2X0DYtX3y08eFVx5fNIw7/uIv8hYUKgXoK8oayJlLnKcRfEYCxWMVE1XagUdVtCJlZT1AU4LXEABW+L1Peg==} - '@types/node@22.14.0': resolution: {integrity: sha512-Kmpl+z84ILoG+3T/zQFyAJsU6EPTmOCj8/2+83fSN6djd6I4o7uOuGIH6vq3PrjY5BGitSbFuMN18j3iknubbA==} - '@types/node@22.14.1': - resolution: {integrity: sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw==} - '@types/normalize-package-data@2.4.4': resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} '@types/parse-json@4.0.2': resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} + '@types/parse-path@7.0.3': + resolution: {integrity: sha512-LriObC2+KYZD3FzCrgWGv/qufdUy4eXrxcLgQMfYXgPbLIecKIsVBaQgUPmxSSLcjmYbDTQbMgr6qr6l/eb7Bg==} + '@types/prop-types@15.7.14': resolution: {integrity: sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==} - '@types/react-dom@18.3.5': - resolution: {integrity: sha512-P4t6saawp+b/dFrUr2cvkVsfvPguwsxtH6dNIYRllMsefqFzkZk5UIjzyDOv5g1dXIPdG4Sp1yCR4Z6RCUsG/Q==} + '@types/react-dom@18.3.6': + resolution: {integrity: sha512-nf22//wEbKXusP6E9pfOCDwFdHAX4u172eaJI4YkDRQEZiorm6KfYnSC2SWLDMVWUOWPERmJnN0ujeAfTBLvrw==} peerDependencies: '@types/react': ^18.0.0 @@ -2082,8 +2002,8 @@ packages: peerDependencies: '@types/react': '*' - '@types/react@18.3.18': - resolution: {integrity: sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==} + '@types/react@18.3.20': + resolution: {integrity: sha512-IPaCZN7PShZK/3t6Q87pfTkRm6oLTd4vztyoj+cbHUF1g3FfVb2tFIL79uCRKEfv16AhqDMBywP2VW3KIZUvcg==} '@types/responselike@1.0.3': resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==} @@ -2115,9 +2035,6 @@ packages: '@types/ws@8.18.1': resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} - '@types/ws@8.5.14': - resolution: {integrity: sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw==} - '@types/xml2js@0.4.14': resolution: {integrity: sha512-4YnrRemBShWRO2QjvUin8ESA41rH+9nQGLUGZV/1IDhi3SL9OhdpNC/MrulTWuptXKwhx/aDxE7toV0f/ypIXQ==} @@ -2130,53 +2047,128 @@ packages: '@types/yauzl@2.10.3': resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} - '@typescript-eslint/eslint-plugin@8.29.1': - resolution: {integrity: sha512-ba0rr4Wfvg23vERs3eB+P3lfj2E+2g3lhWcCVukUuhtcdUx5lSIFZlGFEBHKr+3zizDa/TvZTptdNHVZWAkSBg==} + '@typescript-eslint/eslint-plugin@8.30.1': + resolution: {integrity: sha512-v+VWphxMjn+1t48/jO4t950D6KR8JaJuNXzi33Ve6P8sEmPr5k6CEXjdGwT6+LodVnEa91EQCtwjWNUCPweo+Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: '@typescript-eslint/parser': ^8.0.0 || ^8.0.0-alpha.0 eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/parser@8.29.1': - resolution: {integrity: sha512-zczrHVEqEaTwh12gWBIJWj8nx+ayDcCJs06yoNMY0kwjMWDM6+kppljY+BxWI06d2Ja+h4+WdufDcwMnnMEWmg==} + '@typescript-eslint/parser@8.30.1': + resolution: {integrity: sha512-H+vqmWwT5xoNrXqWs/fesmssOW70gxFlgcMlYcBaWNPIEWDgLa4W9nkSPmhuOgLnXq9QYgkZ31fhDyLhleCsAg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/scope-manager@8.29.1': - resolution: {integrity: sha512-2nggXGX5F3YrsGN08pw4XpMLO1Rgtnn4AzTegC2MDesv6q3QaTU5yU7IbS1tf1IwCR0Hv/1EFygLn9ms6LIpDA==} + '@typescript-eslint/scope-manager@8.30.1': + resolution: {integrity: sha512-+C0B6ChFXZkuaNDl73FJxRYT0G7ufVPOSQkqkpM/U198wUwUFOtgo1k/QzFh1KjpBitaK7R1tgjVz6o9HmsRPg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/type-utils@8.29.1': - resolution: {integrity: sha512-DkDUSDwZVCYN71xA4wzySqqcZsHKic53A4BLqmrWFFpOpNSoxX233lwGu/2135ymTCR04PoKiEEEvN1gFYg4Tw==} + '@typescript-eslint/type-utils@8.30.1': + resolution: {integrity: sha512-64uBF76bfQiJyHgZISC7vcNz3adqQKIccVoKubyQcOnNcdJBvYOILV1v22Qhsw3tw3VQu5ll8ND6hycgAR5fEA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/types@8.29.1': - resolution: {integrity: sha512-VT7T1PuJF1hpYC3AGm2rCgJBjHL3nc+A/bhOp9sGMKfi5v0WufsX/sHCFBfNTx2F+zA6qBc/PD0/kLRLjdt8mQ==} + '@typescript-eslint/types@8.30.1': + resolution: {integrity: sha512-81KawPfkuulyWo5QdyG/LOKbspyyiW+p4vpn4bYO7DM/hZImlVnFwrpCTnmNMOt8CvLRr5ojI9nU1Ekpw4RcEw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.29.1': - resolution: {integrity: sha512-l1enRoSaUkQxOQnbi0KPUtqeZkSiFlqrx9/3ns2rEDhGKfTa+88RmXqedC1zmVTOWrLc2e6DEJrTA51C9iLH5g==} + '@typescript-eslint/typescript-estree@8.30.1': + resolution: {integrity: sha512-kQQnxymiUy9tTb1F2uep9W6aBiYODgq5EMSk6Nxh4Z+BDUoYUSa029ISs5zTzKBFnexQEh71KqwjKnRz58lusQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/utils@8.29.1': - resolution: {integrity: sha512-QAkFEbytSaB8wnmB+DflhUPz6CLbFWE2SnSCrRMEa+KnXIzDYbpsn++1HGvnfAsUY44doDXmvRkO5shlM/3UfA==} + '@typescript-eslint/utils@8.30.1': + resolution: {integrity: sha512-T/8q4R9En2tcEsWPQgB5BQ0XJVOtfARcUvOa8yJP3fh9M/mXraLxZrkCfGb6ChrO/V3W+Xbd04RacUEqk1CFEQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/visitor-keys@8.29.1': - resolution: {integrity: sha512-RGLh5CRaUEf02viP5c1Vh1cMGffQscyHe7HPAzGpfmfflFg1wUz2rYxd+OZqwpeypYvZ8UxSxuIpF++fmOzEcg==} + '@typescript-eslint/visitor-keys@8.30.1': + resolution: {integrity: sha512-aEhgas7aJ6vZnNFC7K4/vMGDGyOiqWcYZPpIWrTKuTAlsvDNKy2GFDqh9smL+iq069ZvR0YzEeq0B8NJlLzjFA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@unrs/resolver-binding-darwin-arm64@1.3.3': + resolution: {integrity: sha512-EpRILdWr3/xDa/7MoyfO7JuBIJqpBMphtu4+80BK1bRfFcniVT74h3Z7q1+WOc92FuIAYatB1vn9TJR67sORGw==} + cpu: [arm64] + os: [darwin] + + '@unrs/resolver-binding-darwin-x64@1.3.3': + resolution: {integrity: sha512-ntj/g7lPyqwinMJWZ+DKHBse8HhVxswGTmNgFKJtdgGub3M3zp5BSZ3bvMP+kBT6dnYJLSVlDqdwOq1P8i0+/g==} + cpu: [x64] + os: [darwin] + + '@unrs/resolver-binding-freebsd-x64@1.3.3': + resolution: {integrity: sha512-l6BT8f2CU821EW7U8hSUK8XPq4bmyTlt9Mn4ERrfjJNoCw0/JoHAh9amZZtV3cwC3bwwIat+GUnrcHTG9+qixw==} + cpu: [x64] + os: [freebsd] + + '@unrs/resolver-binding-linux-arm-gnueabihf@1.3.3': + resolution: {integrity: sha512-8ScEc5a4y7oE2BonRvzJ+2GSkBaYWyh0/Ko4Q25e/ix6ANpJNhwEPZvCR6GVRmsQAYMIfQvYLdM6YEN+qRjnAQ==} + cpu: [arm] + os: [linux] + + '@unrs/resolver-binding-linux-arm-musleabihf@1.3.3': + resolution: {integrity: sha512-8qQ6l1VTzLNd3xb2IEXISOKwMGXDCzY/UNy/7SovFW2Sp0K3YbL7Ao7R18v6SQkLqQlhhqSBIFRk+u6+qu5R5A==} + cpu: [arm] + os: [linux] + + '@unrs/resolver-binding-linux-arm64-gnu@1.3.3': + resolution: {integrity: sha512-v81R2wjqcWXJlQY23byqYHt9221h4anQ6wwN64oMD/WAE+FmxPHFZee5bhRkNVtzqO/q7wki33VFWlhiADwUeQ==} + cpu: [arm64] + os: [linux] + + '@unrs/resolver-binding-linux-arm64-musl@1.3.3': + resolution: {integrity: sha512-cAOx/j0u5coMg4oct/BwMzvWJdVciVauUvsd+GQB/1FZYKQZmqPy0EjJzJGbVzFc6gbnfEcSqvQE6gvbGf2N8Q==} + cpu: [arm64] + os: [linux] + + '@unrs/resolver-binding-linux-ppc64-gnu@1.3.3': + resolution: {integrity: sha512-mq2blqwErgDJD4gtFDlTX/HZ7lNP8YCHYFij2gkXPtMzrXxPW1hOtxL6xg4NWxvnj4bppppb0W3s/buvM55yfg==} + cpu: [ppc64] + os: [linux] + + '@unrs/resolver-binding-linux-s390x-gnu@1.3.3': + resolution: {integrity: sha512-u0VRzfFYysarYHnztj2k2xr+eu9rmgoTUUgCCIT37Nr+j0A05Xk2c3RY8Mh5+DhCl2aYibihnaAEJHeR0UOFIQ==} + cpu: [s390x] + os: [linux] + + '@unrs/resolver-binding-linux-x64-gnu@1.3.3': + resolution: {integrity: sha512-OrVo5ZsG29kBF0Ug95a2KidS16PqAMmQNozM6InbquOfW/udouk063e25JVLqIBhHLB2WyBnixOQ19tmeC/hIg==} + cpu: [x64] + os: [linux] + + '@unrs/resolver-binding-linux-x64-musl@1.3.3': + resolution: {integrity: sha512-PYnmrwZ4HMp9SkrOhqPghY/aoL+Rtd4CQbr93GlrRTjK6kDzfMfgz3UH3jt6elrQAfupa1qyr1uXzeVmoEAxUA==} + cpu: [x64] + os: [linux] + + '@unrs/resolver-binding-wasm32-wasi@1.3.3': + resolution: {integrity: sha512-81AnQY6fShmktQw4hWDUIilsKSdvr/acdJ5azAreu2IWNlaJOKphJSsUVWE+yCk6kBMoQyG9ZHCb/krb5K0PEA==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@unrs/resolver-binding-win32-arm64-msvc@1.3.3': + resolution: {integrity: sha512-X/42BMNw7cW6xrB9syuP5RusRnWGoq+IqvJO8IDpp/BZg64J1uuIW6qA/1Cl13Y4LyLXbJVYbYNSKwR/FiHEng==} + cpu: [arm64] + os: [win32] + + '@unrs/resolver-binding-win32-ia32-msvc@1.3.3': + resolution: {integrity: sha512-EGNnNGQxMU5aTN7js3ETYvuw882zcO+dsVjs+DwO2j/fRVKth87C8e2GzxW1L3+iWAXMyJhvFBKRavk9Og1Z6A==} + cpu: [ia32] + os: [win32] + + '@unrs/resolver-binding-win32-x64-msvc@1.3.3': + resolution: {integrity: sha512-GraLbYqOJcmW1qY3osB+2YIiD62nVf2/bVLHZmrb4t/YSUwE03l7TwcDJl08T/Tm3SVhepX8RQkpzWbag/Sb4w==} + cpu: [x64] + os: [win32] + '@vanilla-extract/babel-plugin-debug-ids@1.2.0': resolution: {integrity: sha512-z5nx2QBnOhvmlmBKeRX5sPVLz437wV30u+GJL+Hzj1rGiJYVNvgIIlzUpRNjVQ0MgAgiQIqIUbqPnmMc6HmDlQ==} @@ -2189,20 +2181,20 @@ packages: '@vanilla-extract/private@1.0.6': resolution: {integrity: sha512-ytsG/JLweEjw7DBuZ/0JCN4WAQgM9erfSTdS1NQY778hFQSZ6cfCDEZZ0sgVm4k54uNz6ImKB33AYvSR//fjxw==} - '@vitest/coverage-v8@3.0.8': - resolution: {integrity: sha512-y7SAKsQirsEJ2F8bulBck4DoluhI2EEgTimHd6EEUgJBGKy9tC25cpywh1MH4FvDGoG2Unt7+asVd1kj4qOSAw==} + '@vitest/coverage-v8@3.1.1': + resolution: {integrity: sha512-MgV6D2dhpD6Hp/uroUoAIvFqA8AuvXEFBC2eepG3WFc1pxTfdk1LEqqkWoWhjz+rytoqrnUUCdf6Lzco3iHkLQ==} peerDependencies: - '@vitest/browser': 3.0.8 - vitest: 3.0.8 + '@vitest/browser': 3.1.1 + vitest: 3.1.1 peerDependenciesMeta: '@vitest/browser': optional: true - '@vitest/expect@3.0.8': - resolution: {integrity: sha512-Xu6TTIavTvSSS6LZaA3EebWFr6tsoXPetOWNMOlc7LO88QVVBwq2oQWBoDiLCN6YTvNYsGSjqOO8CAdjom5DCQ==} + '@vitest/expect@3.1.1': + resolution: {integrity: sha512-q/zjrW9lgynctNbwvFtQkGK9+vvHA5UzVi2V8APrp1C6fG6/MuYYkmlx4FubuqLycCeSdHD5aadWfua/Vr0EUA==} - '@vitest/mocker@3.0.8': - resolution: {integrity: sha512-n3LjS7fcW1BCoF+zWZxG7/5XvuYH+lsFg+BDwwAz0arIwHQJFUEsKBQ0BLU49fCxuM/2HSeBPHQD8WjgrxMfow==} + '@vitest/mocker@3.1.1': + resolution: {integrity: sha512-bmpJJm7Y7i9BBELlLuuM1J1Q6EQ6K5Ye4wcyOpOMXMcePYKSIYlpcrCm4l/O6ja4VJA5G2aMJiuZkZdnxlC3SA==} peerDependencies: msw: ^2.4.9 vite: ^5.0.0 || ^6.0.0 @@ -2215,64 +2207,52 @@ packages: '@vitest/pretty-format@2.1.9': resolution: {integrity: sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==} - '@vitest/pretty-format@3.0.8': - resolution: {integrity: sha512-BNqwbEyitFhzYMYHUVbIvepOyeQOSFA/NeJMIP9enMntkkxLgOcgABH6fjyXG85ipTgvero6noreavGIqfJcIg==} + '@vitest/pretty-format@3.1.1': + resolution: {integrity: sha512-dg0CIzNx+hMMYfNmSqJlLSXEmnNhMswcn3sXO7Tpldr0LiGmg3eXdLLhwkv2ZqgHb/d5xg5F7ezNFRA1fA13yA==} - '@vitest/runner@3.0.8': - resolution: {integrity: sha512-c7UUw6gEcOzI8fih+uaAXS5DwjlBaCJUo7KJ4VvJcjL95+DSR1kova2hFuRt3w41KZEFcOEiq098KkyrjXeM5w==} + '@vitest/runner@3.1.1': + resolution: {integrity: sha512-X/d46qzJuEDO8ueyjtKfxffiXraPRfmYasoC4i5+mlLEJ10UvPb0XH5M9C3gWuxd7BAQhpK42cJgJtq53YnWVA==} '@vitest/snapshot@2.1.9': resolution: {integrity: sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==} - '@vitest/snapshot@3.0.8': - resolution: {integrity: sha512-x8IlMGSEMugakInj44nUrLSILh/zy1f2/BgH0UeHpNyOocG18M9CWVIFBaXPt8TrqVZWmcPjwfG/ht5tnpba8A==} + '@vitest/snapshot@3.1.1': + resolution: {integrity: sha512-bByMwaVWe/+1WDf9exFxWWgAixelSdiwo2p33tpqIlM14vW7PRV5ppayVXtfycqze4Qhtwag5sVhX400MLBOOw==} - '@vitest/spy@3.0.8': - resolution: {integrity: sha512-MR+PzJa+22vFKYb934CejhR4BeRpMSoxkvNoDit68GQxRLSf11aT6CTj3XaqUU9rxgWJFnqicN/wxw6yBRkI1Q==} + '@vitest/spy@3.1.1': + resolution: {integrity: sha512-+EmrUOOXbKzLkTDwlsc/xrwOlPDXyVk3Z6P6K4oiCndxz7YLpp/0R0UsWVOKT0IXWjjBJuSMk6D27qipaupcvQ==} - '@vitest/ui@3.0.8': - resolution: {integrity: sha512-MfTjaLU+Gw/lYorgwFZ06Cym+Mj9hPfZh/Q91d4JxyAHiicAakPTvS7zYCSHF+5cErwu2PVBe1alSjuh6L/UiA==} + '@vitest/ui@3.1.1': + resolution: {integrity: sha512-2HpiRIYg3dlvAJBV9RtsVswFgUSJK4Sv7QhpxoP0eBGkYwzGIKP34PjaV00AULQi9Ovl6LGyZfsetxDWY5BQdQ==} peerDependencies: - vitest: 3.0.8 + vitest: 3.1.1 - '@vitest/utils@3.0.8': - resolution: {integrity: sha512-nkBC3aEhfX2PdtQI/QwAWp8qZWwzASsU4Npbcd5RdMPBSSLCpkZp52P3xku3s3uA0HIEhGvEcF8rNkBsz9dQ4Q==} + '@vitest/utils@3.1.1': + resolution: {integrity: sha512-1XIjflyaU2k3HMArJ50bwSh3wKWPD6Q47wz/NUSmRV0zNywPc4w79ARjg/i/aNINHwA+mIALhUVqD9/aUvZNgg==} - '@wdio/appium-service@9.12.4': - resolution: {integrity: sha512-JzwFWMeQ6bIqUndAe1cLB38R1g6wC3d6OIQieU01GByZFRTCEUroRus8rYzhL092nqXPA+5810oMboSZLMmB9Q==} + '@wdio/appium-service@9.12.6': + resolution: {integrity: sha512-00i1Du4/12W54sXE3vozKIyCW7vfhSv+GkwL3TVQBJyNixXKJkQdHBZe09Qe8E8jN0L1tUjvrnjlIffLEIegZQ==} engines: {node: '>=18.20.0'} - '@wdio/cli@9.12.5': - resolution: {integrity: sha512-ZsUzPvkBtw5IWvOmLwtvrgtVbAXw3HIBuox7yn0NiSJkdl0V5QHSGKGLgkt27azktwPThRymrgx9723egEONIw==} + '@wdio/cli@9.12.6': + resolution: {integrity: sha512-ZPmbN9o64dBZoVpAEdZm7A0QwuuGh/xLQVdx5twAQ6XIhK30sQB8uQ/yycD2rTmG5UKsVbwv6ceCqDOl8Ggr1w==} engines: {node: '>=18.20.0'} hasBin: true - '@wdio/config@9.12.3': - resolution: {integrity: sha512-xPLrwIm043OQkWZSlErR0e4LNYPMaLFAyDEtVEcbXgRhOH/Nl91kCUPiJMQSXJIoQ+qRxoMQzDvv01mQrcVwCA==} - engines: {node: '>=18.20.0'} - - '@wdio/config@9.12.5': - resolution: {integrity: sha512-T4pOgY7FLj0+SBc58n81JZidCJKfqaSb9Ql9lOd38tmorEwTKjcPAzQQY1Ftzqv49kjBHvXdlupy685VVKNepA==} - engines: {node: '>=18.20.0'} - - '@wdio/config@9.9.0': - resolution: {integrity: sha512-TonCzSBjfk6fLV9zEvH58Opg3te4gl+VapZeShwfJWuL5T8YAWfSKIUVbb9auIEaOWx2OtOap4DK+jK9CLSTVA==} - engines: {node: '>=18.20.0'} - - '@wdio/dot-reporter@9.12.3': - resolution: {integrity: sha512-8kqC+hwRDyhTLJLBITr96p/GXi1cokYzQVQEeW2d4Nbf8jgGw9MDIatlsPN0OTJm17w8MY0934WxsfSXoYq8KQ==} + '@wdio/config@9.12.6': + resolution: {integrity: sha512-zlOJixJUHxeoyfIN/KdM797HwJj/oNgBaEdftgJARqbXt5AVZu18vJ3zljb+wzbY2M0pl7Y4+5OFH06WlDgQ+A==} engines: {node: '>=18.20.0'} - '@wdio/globals@9.12.5': - resolution: {integrity: sha512-BixX9uOJNmtdlitqHLFzexo3RgM+IRljhR8iujdpgV5bMrt4K+sup0SchZTl80jZB95WBKgv87N+RoOZxiZEKg==} + '@wdio/dot-reporter@9.12.6': + resolution: {integrity: sha512-den2sRD+blw6ymI97X808ESxQ5cVVuNeu5V2VUJk3NWA7Q36cuaJ8s/rPKzcF0CdfdbkkKF8pEQWUVDrrWsK7Q==} engines: {node: '>=18.20.0'} - '@wdio/globals@9.9.1': - resolution: {integrity: sha512-0sOgQcbX3QSsTpSmN+3mofNGam4d7hQze5D5WmRGmXHtWWmRztusmByWm+LbdJiheReEf6ynfGBpOvd+CuQnpA==} + '@wdio/globals@9.12.6': + resolution: {integrity: sha512-/JrmgoZ/VR6fj3z5v9C1rx2tc0kPSuDNyju+cluWyNHJ8oPJCNr6ZkcFWrWAU5WYz3CBT+bZI7EyqNMpHNOJBg==} engines: {node: '>=18.20.0'} - '@wdio/local-runner@9.12.5': - resolution: {integrity: sha512-yaO2pwCXgCOptkmwMQWQmy0Ouv6ra/bVVxFJauB/w+fzltIuPpJNozhlATCqiDtjBCwUoFetlxYNCsb4b7pFTA==} + '@wdio/local-runner@9.12.6': + resolution: {integrity: sha512-+ZSz0YXrlVglQWm8kra88kb3zlHWOde79q8gybxywE7qwCUqFrfhG0CpJeqWn39HoEHqNce4VdQ4Q/CkQ+xToQ==} engines: {node: '>=18.20.0'} '@wdio/logger@7.26.0': @@ -2283,65 +2263,43 @@ packages: resolution: {integrity: sha512-BXx8RXFUW2M4dcO6t5Le95Hi2ZkTQBRsvBQqLekT2rZ6Xmw8ZKZBPf0FptnoftFGg6dYmwnDidYv/0+4PiHjpQ==} engines: {node: '>=18.20.0'} - '@wdio/mocha-framework@9.12.5': - resolution: {integrity: sha512-QNyvTjz2mgLn80NwtE3P1XhMa6ve65FZhb/N9XMYl5ATKSLh/HTl458eVdpVaSh/9FHaZpfkruEJdCB8milYeQ==} + '@wdio/mocha-framework@9.12.6': + resolution: {integrity: sha512-vBAtVY+PLCGTZqTqsfxNtPVthk6tKI4JSffgGNlWDK/uCcjUOZdvsRRs7VRLr8COyeP1QQFzJ8cHDpCu8nd7Fw==} engines: {node: '>=18.20.0'} - '@wdio/protocols@9.12.3': - resolution: {integrity: sha512-M4K8/YuVUvp6h7LflD0jNM56BnZyntoMhKSYB/4bSauVmEQKrbKh1iOZ7zYdp9R7oojjsiVspjLMN0L313JKiA==} - '@wdio/protocols@9.12.5': resolution: {integrity: sha512-i+yc0EZtZOh5fFuwHxvcnXeTXk2ZjFICRbcAxTNE0F2Jr4uOydvcAOw4EIIRmb9NWUSPf/bGZAA+4SEXmxmjUA==} - '@wdio/protocols@9.7.0': - resolution: {integrity: sha512-5DI8cqJqT9K6oQn8UpaSTmcGAl4ufkUWC5FoPT3oXdLjILfxvweZDf/2XNBCbGMk4+VOMKqB2ofOqKhDIB2nAg==} - '@wdio/repl@9.4.4': resolution: {integrity: sha512-kchPRhoG/pCn4KhHGiL/ocNhdpR8OkD2e6sANlSUZ4TGBVi86YSIEjc2yXUwLacHknC/EnQk/SFnqd4MsNjGGg==} engines: {node: '>=18.20.0'} - '@wdio/reporter@9.12.3': - resolution: {integrity: sha512-T0dO6xQ3AYTwvgmHpVP/w/8pgDtv3SFo8JKO0EDBFM6uNOyKlh+9ChvU2KednY1Qt7Pw11sUrSFsFzY7TXW1iQ==} - engines: {node: '>=18.20.0'} - - '@wdio/runner@9.12.5': - resolution: {integrity: sha512-AJf8i0MA8sIidTGb2ffCCUSNQ5li9FENYx1Unxud0kAH3tF7/myJIFYteu/QKtwDy1nE08dPwoUTO3YxI4rMAA==} - engines: {node: '>=18.20.0'} - - '@wdio/sauce-service@9.12.5': - resolution: {integrity: sha512-h2zSu9iG93XDt+Bl8Cw3mBG2h4QGr/37gHAYov8IPjRgd57ObKK/IFENSIXelvrnLsAVVx8Gdnf3P98PbAZrHw==} - engines: {node: '>=18.20.0'} - - '@wdio/shared-store-service@9.12.5': - resolution: {integrity: sha512-ryJy71iAJ+tGxbj1iyEj56gWZE2CzDHZhioWwvhKOOj13e1xPFs7cjga93NJgBXFhQjG+0UvZG/He1EVMskdZA==} - engines: {node: '>=18.20.0'} - - '@wdio/spec-reporter@9.12.3': - resolution: {integrity: sha512-d/ZLu6gwzuuzYMObfC8Kaf2Lv8GkxfrrPr+ZUVbwWiJI0MxRJJc2STb2fchbSc1eTRkzKpuTHVPYF932yaCrsA==} + '@wdio/reporter@9.12.6': + resolution: {integrity: sha512-8cR74tEp5nzC8nP59n4hkDUpoaHUNDbJvP3jD9EfX+ZO4OgPMyJMVTGubo1o88Wuty/Gd2jvOZLHoGD8KlHcKw==} engines: {node: '>=18.20.0'} - '@wdio/types@9.12.2': - resolution: {integrity: sha512-nlsSLtE70y8Jx0GA91X+q6da5JfoH//VLLAqqQnMG1Mjq8Cz0lLNIF0MlwsNBumHJ3HCqWEAmZyYIB3QZ7O/iQ==} + '@wdio/runner@9.12.6': + resolution: {integrity: sha512-2t59euER8jnZkTQ7Co2OsW7TIA5oWNgQu2vzMU1+vlK+PGt3FQeMSB1JYtfMYDlnWdyNAXx1wrM7Odv6tLcTpg==} engines: {node: '>=18.20.0'} - '@wdio/types@9.12.3': - resolution: {integrity: sha512-MlnQ3WG1CQAjmUmeKtv3timGR91hSsCwQW9T1kqpu0VaJ/qbw3sWgtArMqRvgWB2H6IGueqQwDQ9qHlP013w9Q==} + '@wdio/sauce-service@9.12.6': + resolution: {integrity: sha512-LMOrFfsynXD3+XTCl/yQsgm3K1kGiY1pNV33dVpjw1qPUy8GVSKrSsLs5XOlPHy6QSfW+vTPTsGvHK0NfIi39w==} engines: {node: '>=18.20.0'} - '@wdio/types@9.9.0': - resolution: {integrity: sha512-Mh7ryL7uWKECStKcF6pWSbYkC51OemOwQR2pmvymP5HOfG74s6RVbJ+Z6Om8ffiJeTI5nZuvNDzYNkUpm7Elzg==} + '@wdio/shared-store-service@9.12.6': + resolution: {integrity: sha512-5D95o27hf8rIAZgK1l0BB41Vj4I45WVmGqs4BkxGDD4Y6MPqhUgxmG64K8JHVcQzdnD8mub5o8O8aZnMVt3MLg==} engines: {node: '>=18.20.0'} - '@wdio/utils@9.12.3': - resolution: {integrity: sha512-G7qpiLux89jqhzz33TvIIWaMIKIUUmmBaBH8ylCu3W/7h8i1/jPrYryOvWw75yVWl1ch4NYEo4uJey1NsJnkAQ==} + '@wdio/spec-reporter@9.12.6': + resolution: {integrity: sha512-zLBbp5tuCdwyxCdh7IXdFk7fideP/e/U8GjuWpRMMuGHCrfIdVxShM5CEq1XEv7Lnw4RWkO6Yo00LU4F0Lafmg==} engines: {node: '>=18.20.0'} - '@wdio/utils@9.12.5': - resolution: {integrity: sha512-yddJj7VyA3kGuAuDU63ZdRBK4D1jwSU+52KwlZtOeqDdT/i6KAwRVYNYMwwmsGuM4GpY3q5h944YylBQNkKkjQ==} + '@wdio/types@9.12.6': + resolution: {integrity: sha512-WzZhaN834du9wjqT/Go9qPyB7VkzV2bjr6pr06DrIzxIpJq/snWOv96C6OjJu8nmYNRjV769mAxyggBUf+sUoQ==} engines: {node: '>=18.20.0'} - '@wdio/utils@9.9.0': - resolution: {integrity: sha512-CgPE/fh4SLTZmQZO99/B/swrQ8uwaavlVfeUtxQ5iZ5rTpXKx+V4ScCSuU0qX5Kwm9e1ZG6ALuzDTo8zQ1gJ4w==} + '@wdio/utils@9.12.6': + resolution: {integrity: sha512-JfI4CxBRQCOgToJeQNaZLv+wYNIGyJG1gqrpxUOvkrJvBgdOAmIu3dzlcKP/WviXlcxvwLQF2FK8bQVTjHv0fQ==} engines: {node: '>=18.20.0'} '@web3-storage/multipart-parser@1.0.0': @@ -2371,11 +2329,6 @@ packages: resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==} engines: {node: '>=0.4.0'} - acorn@8.14.0: - resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==} - engines: {node: '>=0.4.0'} - hasBin: true - acorn@8.14.1: resolution: {integrity: sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==} engines: {node: '>=0.4.0'} @@ -2484,8 +2437,8 @@ packages: resolution: {integrity: sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==} engines: {node: '>= 0.4'} - array.prototype.findlastindex@1.2.5: - resolution: {integrity: sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==} + array.prototype.findlastindex@1.2.6: + resolution: {integrity: sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==} engines: {node: '>= 0.4'} array.prototype.flat@1.3.3: @@ -2554,12 +2507,12 @@ packages: resolution: {integrity: sha512-zJAaP9zxTcvTHRlejau3ZOY4V7SRpiByf3/dxx2uyKxxor19tpmpV2QRsTKikckwhaPmr2dVpxxMr7jOCYVp5g==} engines: {node: '>=6.0.0'} - axe-core@4.10.2: - resolution: {integrity: sha512-RE3mdQ7P3FRSe7eqCWoeQ/Z9QXrtniSjp1wUjt5nRC3WIpz5rSCve6o3fsZ2aCpJtrZjSZgjwXAoTO5k4tEI0w==} + axe-core@4.10.3: + resolution: {integrity: sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==} engines: {node: '>=4'} - axios@1.7.9: - resolution: {integrity: sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==} + axios@1.8.4: + resolution: {integrity: sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==} axobject-query@4.1.0: resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} @@ -2619,8 +2572,8 @@ packages: resolution: {integrity: sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==} engines: {node: '>=10.0.0'} - before-after-hook@2.2.3: - resolution: {integrity: sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==} + before-after-hook@3.0.2: + resolution: {integrity: sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A==} better-path-resolve@1.0.0: resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==} @@ -2735,10 +2688,6 @@ packages: resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==} engines: {node: '>= 0.4'} - call-bound@1.0.3: - resolution: {integrity: sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==} - engines: {node: '>= 0.4'} - call-bound@1.0.4: resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} engines: {node: '>= 0.4'} @@ -2762,8 +2711,8 @@ packages: resolution: {integrity: sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==} engines: {node: '>=16'} - caniuse-lite@1.0.30001707: - resolution: {integrity: sha512-3qtRjw/HQSMlDWf+X79N206fepf4SOOU6SQLMaq/0KkZLmSjPxAkBOQQ+FxbHKfHmYLZFfdWsO3KA90ceHPSnw==} + caniuse-lite@1.0.30001711: + resolution: {integrity: sha512-OpFA8GsKtoV3lCcsI3U5XBAV+oVrMu96OS8XafKqnhOaEAW2mveD1Mx81Sx/02chERwhDakuXs28zbyEc4QMKg==} capital-case@1.0.4: resolution: {integrity: sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==} @@ -2834,8 +2783,8 @@ packages: resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} engines: {node: '>=8'} - ci-info@4.1.0: - resolution: {integrity: sha512-HutrvTNsF48wnxkzERIXOe5/mlcfFcbfCmwcg6CJnizbSue78AbDt+1cgl26zwn61WFxhcPykPfZrbqjGmBb4A==} + ci-info@4.2.0: + resolution: {integrity: sha512-cYY9mypksY8NRqgDB1XD1RiJL338v/551niynFTGkZOO2LHuB2OmOYxDIe/ttN9AHwrqdum1360G3ald0W9kCg==} engines: {node: '>=8'} clean-regexp@1.0.0: @@ -2947,6 +2896,9 @@ packages: confbox@0.1.8: resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + confbox@0.2.2: + resolution: {integrity: sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==} + config-chain@1.1.13: resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==} @@ -2982,10 +2934,6 @@ packages: resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} engines: {node: '>=6.6.0'} - cookie@0.6.0: - resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} - engines: {node: '>= 0.6'} - cookie@0.7.1: resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==} engines: {node: '>= 0.6'} @@ -2994,8 +2942,8 @@ packages: resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} engines: {node: '>= 0.6'} - core-js-compat@3.40.0: - resolution: {integrity: sha512-0XEDpr5y5mijvw8Lbc6E5AkjrHfp7eEoPlu36SWeAbcL8fn1G1ANe8DBlo2XoNN89oVpxWwOjYIPVzR4ZvsKCQ==} + core-js-compat@3.41.0: + resolution: {integrity: sha512-RFsU9LySVue9RTwdDVX/T0e2Y6jRYWXERKElIjpuEOEnxaXffI0X7RUwVzfYLfzuLXSNJDYoRYUAmRUcyln20A==} core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} @@ -3052,8 +3000,8 @@ packages: engines: {node: '>=4'} hasBin: true - cssstyle@4.2.1: - resolution: {integrity: sha512-9+vem03dMXG7gDmZ62uqmRiMRNtinIZ9ZyuF6BdxzfOD+FdN5hretzynkn0ReS2DO2GSw76RWHs0UmJPI2zUjw==} + cssstyle@4.3.0: + resolution: {integrity: sha512-6r0NiY0xizYqfBvWp1G7WXJ06/bZyrk7Dc6PHql82C/pKGUTKu4yAX4Y8JPamb1ob9nBKuxWzCGTRuGwU3yxJQ==} engines: {node: '>=18'} csstype@3.1.3: @@ -3126,8 +3074,8 @@ packages: decimal.js@10.5.0: resolution: {integrity: sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==} - decode-named-character-reference@1.0.2: - resolution: {integrity: sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==} + decode-named-character-reference@1.1.0: + resolution: {integrity: sha512-Wy+JTSbFThEOXQIR2L6mxJvEs+veIzpmqD7ynWxMXGpnk3smkHQOp6forLdHsKpAMW9iJpaBBIxz285t1n1C3w==} decode-uri-component@0.2.2: resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==} @@ -3159,10 +3107,6 @@ packages: deep-object-diff@1.1.9: resolution: {integrity: sha512-Rn+RuwkmkDwCi2/oXOFS9Gsr5lJZu/yTGpK7wAaAIE75CC+LCGEZHpY6VQJa/RoJcrmaA/docWJZvYohlNkWPA==} - deepmerge-ts@7.1.4: - resolution: {integrity: sha512-fxqo6nHGQ9zOVgI4KXqtWXJR/yCLtC7aXIVq+6jc8tHPFUxlFmuUcm2kC4vztQ+LJxQ3gER/XAWearGYQ8niGA==} - engines: {node: '>=16.0.0'} - deepmerge-ts@7.1.5: resolution: {integrity: sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==} engines: {node: '>=16.0.0'} @@ -3210,9 +3154,6 @@ packages: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} - deprecation@2.3.1: - resolution: {integrity: sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==} - dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} @@ -3286,10 +3227,6 @@ packages: resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==} engines: {node: '>=12'} - dotenv@16.5.0: - resolution: {integrity: sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==} - engines: {node: '>=12'} - dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} @@ -3320,8 +3257,8 @@ packages: engines: {node: '>=0.10.0'} hasBin: true - electron-to-chromium@1.5.128: - resolution: {integrity: sha512-bo1A4HH/NS522Ws0QNFIzyPcyUUNV/yyy70Ho1xqfGYzPUme2F/xr4tlEOuM6/A538U1vDA7a4XfCd1CKRegKQ==} + electron-to-chromium@1.5.132: + resolution: {integrity: sha512-QgX9EBvWGmvSRa74zqfnG7+Eno0Ak0vftBll0Pt2/z5b3bEGYL6OUXLgKPtvx73dn3dvwrlyVkjPKRRlhLYTEg==} emoji-regex@10.4.0: resolution: {integrity: sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==} @@ -3349,10 +3286,6 @@ packages: end-of-stream@1.4.4: resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} - enhanced-resolve@5.18.1: - resolution: {integrity: sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==} - engines: {node: '>=10.13.0'} - enquirer@2.4.1: resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} engines: {node: '>=8.6'} @@ -3406,11 +3339,11 @@ packages: resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} engines: {node: '>= 0.4'} - esbuild-plugins-node-modules-polyfill@1.6.8: - resolution: {integrity: sha512-bRB4qbgUDWrdY1eMk123KiaCSW9VzQ+QLZrmU7D//cCFkmksPd9mUMpmWoFK/rxjIeTfTSOpKCoGoimlvI+AWw==} + esbuild-plugins-node-modules-polyfill@1.7.0: + resolution: {integrity: sha512-Z81w5ReugIBAgufGeGWee+Uxzgs5Na4LprUAK3XlJEh2ktY3LkNuEGMaZyBXxQxGK8SQDS5yKLW5QKGF5qLjYA==} engines: {node: '>=14.0.0'} peerDependencies: - esbuild: '>=0.14.0 <=0.24.x' + esbuild: '>=0.14.0 <=0.25.x' esbuild@0.17.6: resolution: {integrity: sha512-TKFRp9TxrJDdRWfSsSERKEovm6v30iHnrjlcGhLBOtReE28Yp1VSBRfO3GTaOFMoxsNerx4TjrhzSuma9ha83Q==} @@ -3458,8 +3391,8 @@ packages: eslint-import-resolver-node@0.3.9: resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} - eslint-import-resolver-typescript@3.8.0: - resolution: {integrity: sha512-fItUrP/+xwpavWgadrn6lsvcMe80s08xIVFXkUXvhR4cZD2ga96kRF/z/iFGDI7ZDnvtlaZ0wGic7Tw+DhgVnA==} + eslint-import-resolver-typescript@3.10.0: + resolution: {integrity: sha512-aV3/dVsT0/H9BtpNwbaqvl+0xGMRGzncLyhm793NFGvbwGGvzyAykqWZ8oZlZuGwuHkwJjhWJkG1cM3ynvd2pQ==} engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: eslint: '*' @@ -3542,8 +3475,8 @@ packages: resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - eslint@9.23.0: - resolution: {integrity: sha512-jV7AbNoFPAY1EkFYpLq5bslU9NLNO8xnEeQXwErNibVryjk67wHVmddTBilc5srIttJDBrB0eMHKZBFbSIABCw==} + eslint@9.24.0: + resolution: {integrity: sha512-eh/jxIEJyZrvbWRe4XuVclLPDYSYYYgLy5zXGGxD6j8zjSAxFEzI2fL/8xNq6O2yKqVt+eF2YhV+hxjV6UKXwQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} hasBin: true peerDependencies: @@ -3622,8 +3555,8 @@ packages: resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} engines: {node: '>=10'} - execa@8.0.0: - resolution: {integrity: sha512-CTNS0BcKBcoOsawKBlpcKNmK4Kjuyz5jVLhf+PUsHGMqiKMVTa4cN3U7r7bRY8KTpfOGpXMo27fdy0dYVg2pqA==} + execa@8.0.1: + resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} engines: {node: '>=16.17'} execa@9.5.2: @@ -3637,8 +3570,8 @@ packages: resolution: {integrity: sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw==} engines: {node: '>=6'} - expect-type@1.1.0: - resolution: {integrity: sha512-bFi65yM+xZgk+u/KRIpekdSYkTB5W1pEf0Lt8Q8Msh7b+eQ7LXVtIB1Bkm4fvclDEL1b2CZkMhv2mOeF8tMdkA==} + expect-type@1.2.1: + resolution: {integrity: sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw==} engines: {node: '>=12.0.0'} expect-webdriverio@5.1.0: @@ -3657,6 +3590,9 @@ packages: resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==} engines: {node: '>= 0.10.0'} + exsolve@1.0.4: + resolution: {integrity: sha512-xsZH6PXaER4XoV+NiT7JHp1bJodJVT+cxeSH1G0f0tlT0lJqYuHUP3bUx2HtfTDvOagMINYp8rsqusxud3RXhw==} + extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} @@ -3672,6 +3608,9 @@ packages: engines: {node: '>= 10.17.0'} hasBin: true + fast-content-type-parse@2.0.1: + resolution: {integrity: sha512-nGqtvLrj5w0naR6tDPfB4cUmYCqouzyQiz6C5y/LtcDllJdrcc6WaWW6iXyIIOErTa/XRybj28aasdn4LkVk6Q==} + fast-deep-equal@2.0.1: resolution: {integrity: sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==} @@ -3695,8 +3634,8 @@ packages: resolution: {integrity: sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig==} hasBin: true - fastq@1.19.0: - resolution: {integrity: sha512-7SFSRCNjBQIZH/xZR3iy5iQYR8aGBE0h3VG6/cwlbrpdciNYBMotQav8c1XI3HjHH+NikUpP53nPdlZSdWmFzA==} + fastq@1.19.1: + resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} fault@2.0.1: resolution: {integrity: sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ==} @@ -3797,8 +3736,8 @@ packages: resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} engines: {node: '>= 0.4'} - foreground-child@3.3.0: - resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==} + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} form-data@4.0.2: @@ -3947,11 +3886,11 @@ packages: gifwrap@0.10.1: resolution: {integrity: sha512-2760b1vpJHNmLzZ/ubTtNnEx5WApN/PYWJvXvgS+tL1egTTthayFYIQQNi136FLEDcN/IyEY2EcGpIITD6eYUw==} - git-up@7.0.0: - resolution: {integrity: sha512-ONdIrbBCFusq1Oy0sC71F5azx8bVkvtZtMJAsv+a6lz5YAmbNnLD6HAB4gptHZVLPR8S2/kVN6Gab7lryq5+lQ==} + git-up@8.1.0: + resolution: {integrity: sha512-cT2f5ERrhFDMPS5wLHURcjRiacC8HonX0zIAWBTwHv1fS6HheP902l6pefOX/H9lNmvCHDwomw0VeN7nhg5bxg==} - git-url-parse@14.0.0: - resolution: {integrity: sha512-NnLweV+2A4nCvn4U/m2AoYu0pPKlsmhK9cknG7IMwsjFY1S2jxM+mAhsDxyxfCIGfGaD+dozsyX4b6vkYc83yQ==} + git-url-parse@16.0.0: + resolution: {integrity: sha512-Y8iAF0AmCaqXc6a5GYgPQW9ESbncNLOL+CeQAJRhmWUOmnPkKpBYeWYp4mFd3LA5j53CdGDdslzX12yEBVHQQg==} glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} @@ -4098,11 +4037,8 @@ packages: html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} - htmlfy@0.6.0: - resolution: {integrity: sha512-EV1RNjYuG6xIxwA8zDjAUQVeS/SsPE0nhFsdjM8ALopS22ZRAcePocdrhKaaV26PYiTkUrKplJuSZkGRN6Y0Rg==} - - htmlfy@0.6.7: - resolution: {integrity: sha512-r8hRd+oIM10lufovN+zr3VKPTYEIvIwqXGucidh2XQufmiw6sbUXFUFjWlfjo3AnefIDTyzykVzQ8IUVuT1peQ==} + htmlfy@0.6.6: + resolution: {integrity: sha512-NsJ/YV/FQ9XpU0t+TkCMqXVsedNqSSImrI1E4aRBX8uDSaXzF7FOBl/8HKnnqm50RngNfLIoAVLyPQbyVi8EaQ==} htmlparser2@9.1.0: resolution: {integrity: sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==} @@ -4221,9 +4157,11 @@ packages: resolution: {integrity: sha512-CmLAZT65GG/v30c+D2Fk8+ceP6pxD6RL+hIUOWAltCmeyEqWYwqu9v76q03OvjyZ3AB0C1Ala2stn1z/rMqGEw==} engines: {node: '>=18'} - inquirer@9.3.2: - resolution: {integrity: sha512-+ynEbhWKhyomnaX0n2aLIMSkgSlGB5RrWbNXnEqj6mdaIydu6y40MdBjL38SAB0JcdmOaIaMua1azdjLEr3sdw==} + inquirer@12.3.0: + resolution: {integrity: sha512-3NixUXq+hM8ezj2wc7wC37b32/rHq1MwNZDYdvx+d6jokOD+r+i8Q4Pkylh9tISYP114A128LCX8RKhopC5RfQ==} engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' internal-slot@1.1.0: resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} @@ -4285,8 +4223,8 @@ packages: resolution: {integrity: sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==} engines: {node: '>=6'} - is-bun-module@1.3.0: - resolution: {integrity: sha512-DgXeu5UWI0IsMQundYb5UAOzm6G2eVnarJ0byP6Tm55iZNKceD59LNPA2L4VvsScTtHcw0yEkVwSf7PC+QoLSA==} + is-bun-module@2.0.0: + resolution: {integrity: sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==} is-callable@1.2.7: resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} @@ -4527,8 +4465,8 @@ packages: jackspeak@3.4.3: resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} - jackspeak@4.0.3: - resolution: {integrity: sha512-oSwM7q8PTHQWuZAlp995iPpPJ4Vkl7qT0ZRD+9duL9j2oBy6KcTfyxc8mEuHJYC+z/kbps80aJLkaNzTOrf/kw==} + jackspeak@4.1.0: + resolution: {integrity: sha512-9DDdhb5j6cpeitCbvLO7n7J4IxnbM6hoF6O1g4HQ5TfhvvKN8ywDM7668ZhMHRqVmxqhps/F6syWK2KcPxYlkw==} engines: {node: 20 || >=22} jake@10.9.2: @@ -4584,11 +4522,11 @@ packages: jsbn@1.1.0: resolution: {integrity: sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==} - jsdom@25.0.1: - resolution: {integrity: sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==} + jsdom@26.1.0: + resolution: {integrity: sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==} engines: {node: '>=18'} peerDependencies: - canvas: ^2.11.2 + canvas: ^3.0.0 peerDependenciesMeta: canvas: optional: true @@ -4659,8 +4597,8 @@ packages: kuler@2.0.0: resolution: {integrity: sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==} - ky@1.7.5: - resolution: {integrity: sha512-HzhziW6sc5m0pwi5M196+7cEBtbt0lCYi67wNsiwMUmz833wloE0gbzJPWKs1gliFKQb34huItDQX97LyOdPdA==} + ky@1.8.0: + resolution: {integrity: sha512-DoKGmG27nT8t/1F9gV8vNzggJ3mLAyD49J8tTMWHeZvS8qLc7GlyTieicYtFzvDznMe/q2u38peOjkWc5/pjvw==} engines: {node: '>=18'} language-subtag-registry@0.3.23: @@ -4704,8 +4642,8 @@ packages: resolution: {integrity: sha512-XfegD5pyTAfb+GY6chk283Ox5z8WexG56OvM06RWLpAc/UHozO8X6xAxEkIitZOtsSMM1Yr3DkHgW5W+onLhCw==} engines: {node: '>=6'} - local-pkg@0.5.1: - resolution: {integrity: sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==} + local-pkg@1.1.1: + resolution: {integrity: sha512-WunYko2W1NcdfAFpuLUoucsgULmgDBRkdxHxWQ7mK0cQqwPiy8E1enjuRBrhLtZkB5iScJ1XIPdhVEFK8aOLSg==} engines: {node: '>=14'} locate-app@2.5.0: @@ -4811,8 +4749,8 @@ packages: lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} - lru-cache@11.0.2: - resolution: {integrity: sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA==} + lru-cache@11.1.0: + resolution: {integrity: sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==} engines: {node: 20 || >=22} lru-cache@5.1.1: @@ -5133,8 +5071,8 @@ packages: resolution: {integrity: sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==} engines: {node: '>=10'} - mrmime@2.0.0: - resolution: {integrity: sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==} + mrmime@2.0.1: + resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} engines: {node: '>=10'} ms@2.0.0: @@ -5268,8 +5206,8 @@ packages: nth-check@2.1.1: resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} - nwsapi@2.2.16: - resolution: {integrity: sha512-F1I/bimDpj3ncaNDhfyMWuFqmQDBwDB0Fogc2qpL3BWvkQteFD/8BzWuIRl83rq0DXfm8SGt/HFhLXZyljTXcQ==} + nwsapi@2.2.20: + resolution: {integrity: sha512-/ieB+mDe4MrrKMT8z+mQL8klXydZWGR5Dowt4RAGKbJ3kIGEx3X4ljUo+6V73IXtUPWgfOlU5B9MlGxFO5T+cA==} object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} @@ -5364,9 +5302,9 @@ packages: resolution: {integrity: sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==} engines: {node: '>=18'} - os-name@5.1.0: - resolution: {integrity: sha512-YEIoAnM6zFmzw3PQ201gCVCIWbXNyKObGlVvpAVvraAeOHnlYVKFssbA/riRX5R40WA6kKrZ7Dr7dWzO3nKSeQ==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + os-name@6.0.0: + resolution: {integrity: sha512-bv608E0UX86atYi2GMGjDe0vF/X1TJjemNS8oEW6z22YW1Rc3QykSYoGfkQbX0zZX9H0ZB6CQP/3GTf1I5hURg==} + engines: {node: '>=18'} os-tmpdir@1.0.2: resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} @@ -5426,8 +5364,8 @@ packages: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} - pac-proxy-agent@7.1.0: - resolution: {integrity: sha512-Z5FnLVVZSnX7WjBg0mhDtydeRZ1xMcATZThjySQUHqr+0ksP8kqaw23fNKkaaN/Z8gwLUs/W7xdl0I75eP2Xyw==} + pac-proxy-agent@7.2.0: + resolution: {integrity: sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==} engines: {node: '>= 14'} pac-resolver@7.0.1: @@ -5488,8 +5426,9 @@ packages: parse-path@7.0.1: resolution: {integrity: sha512-6ReLMptznuuOEzLoGEa+I1oWRSj2Zna5jLWC+l6zlfAI4dbbSaIES29ThzuPkbhNahT65dWzfoZEO6cfJw2Ksg==} - parse-url@8.1.0: - resolution: {integrity: sha512-xDvOoLU5XRrcOZvnI6b8zA6n9O9ejNk/GExuz1yBuWUGn9KA97GI6HTs6u02wKara1CeVmZhH+0TZFdWScR89w==} + parse-url@9.2.0: + resolution: {integrity: sha512-bCgsFI+GeGWPAvAiUv63ZorMeif3/U0zaXABGJbOWt5OH2KCaPHF6S+0ok4aqM9RuIPGyZdx9tR9l13PsW4AYQ==} + engines: {node: '>=14.13.0'} parse5-htmlparser2-tree-adapter@7.1.0: resolution: {integrity: sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==} @@ -5599,8 +5538,8 @@ packages: resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} engines: {node: '>=6'} - pirates@4.0.6: - resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} + pirates@4.0.7: + resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} engines: {node: '>= 6'} pixelmatch@5.3.0: @@ -5610,6 +5549,9 @@ packages: pkg-types@1.3.1: resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + pkg-types@2.1.0: + resolution: {integrity: sha512-wmJwA+8ihJixSoHKxZJRBQG1oY8Yr9pGLzRmSsNms0iNWyHHAlZCa7mmKiFR10YPZuz/2k169JiS/inOjBCZ2A==} + pluralize@8.0.0: resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} engines: {node: '>=4'} @@ -5973,9 +5915,9 @@ packages: resolution: {integrity: sha512-qx+xQGZVsy55CH0a1hiVwHmqjLryfh7wQyF5HO07XJ9f7dQMY/gPQHhlyDkIzJKC+x2fUCpCcUODUUUFrm7SHA==} hasBin: true - release-it@17.11.0: - resolution: {integrity: sha512-qQGgfMbUZ3/vpXUPmngsgjFObOLjlkwtiozHUYen9fo9AEGciXjG1ZpGr+FNmuBT8R7TOSY+x/s84wOCRKJjbA==} - engines: {node: ^18.18.0 || ^20.9.0 || ^22.0.0} + release-it@18.1.2: + resolution: {integrity: sha512-HOVRcicehCgoCsPFOu0iCBlEC8GDOoKS5s6ICkWmqomGEoZtRQ88D3RCsI5MciSU8vAQU+aWZW2z57NQNNb74w==} + engines: {node: ^20.9.0 || >=22.0.0} hasBin: true remark-frontmatter@4.0.1: @@ -6050,8 +5992,8 @@ packages: resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} engines: {node: '>= 4'} - reusify@1.0.4: - resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} rgb2hex@0.2.5: @@ -6062,14 +6004,11 @@ packages: engines: {node: 20 || >=22} hasBin: true - rollup@4.40.0: - resolution: {integrity: sha512-Noe455xmA96nnqH5piFtLobsGbCij7Tu+tb3c1vYjNbTkfzGqXqQXG3wJaYXkRZuQ0vEYN4bhwg7QnIrqB5B+w==} + rollup@4.39.0: + resolution: {integrity: sha512-thI8kNc02yNvnmJp8dr3fNWJ9tCONDhp6TV35X6HkKGGs9E6q7YWCHbe5vKiTa7TAiNcFEmXKj3X/pG2b3ci0g==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true - rrweb-cssom@0.7.1: - resolution: {integrity: sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==} - rrweb-cssom@0.8.0: resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==} @@ -6084,9 +6023,6 @@ packages: run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} - rxjs@7.8.1: - resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} - rxjs@7.8.2: resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} @@ -6123,10 +6059,6 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - saucelabs@8.0.0: - resolution: {integrity: sha512-Rj9m4OCniYk+c4MFuZGqvz64RPX6oRzMqt0bTr9T27IXGnA7Ic7Ms/VHgPtRcJFP6H3sQ169WOzazPZcW4BIAg==} - hasBin: true - saucelabs@9.0.2: resolution: {integrity: sha512-37QGEOgp9BP1re6S06qpNcBZ0Hw+ZSkZkDepbXHT9VjYoRQwRzUoLtKqE4yyVeK7dzcQXQapmTGF1kp1jO2VDw==} hasBin: true @@ -6256,15 +6188,11 @@ packages: resolution: {integrity: sha512-kWJDCr9EWtZ+/EYYM5MareWj2cRnZGF93YDNpH4jQiHB+hBIZnfPFSQiVMzZOdk+zXWqTZ/9fTeQNu2DqeiudA==} engines: {node: '>=20.12.2'} - sirv-cli@3.0.0: - resolution: {integrity: sha512-p88yHl8DmTOUJroRiW2o9ezJc/YRLxphBydX2NGQc3naKBA09B3EM4Q/yaN8FYF0e50fRSZP7dyatr72b1u5Jw==} + sirv-cli@3.0.1: + resolution: {integrity: sha512-ICXaF2u6IQhLZ0EXF6nqUF4YODfSQSt+mGykt4qqO5rY+oIiwdg7B8w2PVDBJlQulaS2a3J8666CUoDoAuCGvg==} engines: {node: '>=18'} hasBin: true - sirv@3.0.0: - resolution: {integrity: sha512-BPwJGUeDaDCHihkORDchNyyTvWFhcusy1XMmhEVTQTwGeybFbp8YEmB+njbPnth1FibULBSBVwCQni25XlCUDg==} - engines: {node: '>=18'} - sirv@3.0.1: resolution: {integrity: sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A==} engines: {node: '>=18'} @@ -6353,8 +6281,8 @@ packages: resolution: {integrity: sha512-MGrFH9Z4NP9Iyhqn16sDtBpRRNJ0Y2hNa6D65h736fVSaPCHr4DM4sWUNvVaSuC+0OBGhwsrydQwmgfg5LncqQ==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - stable-hash@0.0.4: - resolution: {integrity: sha512-LjdcbuBeLcdETCrPn9i8AYAZ1eCtu4ECAWtP7UleOiZ9LzVxRzzUZEoZ8zB24nhkQnDWyET0I+3sWokSDS3E7g==} + stable-hash@0.0.5: + resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==} stack-trace@0.0.10: resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==} @@ -6370,8 +6298,8 @@ packages: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} engines: {node: '>= 0.8'} - std-env@3.8.0: - resolution: {integrity: sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w==} + std-env@3.9.0: + resolution: {integrity: sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==} stdin-discarder@0.2.2: resolution: {integrity: sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==} @@ -6522,10 +6450,6 @@ packages: engines: {node: '>=14.0.0'} hasBin: true - tapable@2.2.1: - resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} - engines: {node: '>=6'} - tar-fs@2.1.2: resolution: {integrity: sha512-EsaAXwxmx8UB7FRKqeozqEPop69DXcmYwTQwXvyAPF352HJsPdkVhvTaDPYqfNgruveJIJy3TA2l+2zj8LJIJA==} @@ -6593,10 +6517,6 @@ packages: tinyexec@0.3.2: resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} - tinyglobby@0.2.10: - resolution: {integrity: sha512-Zc+8eJlFMvgatPZTl6A9L/yht8QqdmUNtURHaKZLmKBE12hNPSrqNkUp2cs3M/UKmNVVAMFQYSjYIVHDjW5zew==} - engines: {node: '>=12.0.0'} - tinyglobby@0.2.12: resolution: {integrity: sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww==} engines: {node: '>=12.0.0'} @@ -6617,11 +6537,11 @@ packages: resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} engines: {node: '>=14.0.0'} - tldts-core@6.1.77: - resolution: {integrity: sha512-bCaqm24FPk8OgBkM0u/SrEWJgHnhBWYqeBo6yUmcZJDCHt/IfyWBb+14CXdGi4RInMv4v7eUAin15W0DoA+Ytg==} + tldts-core@6.1.85: + resolution: {integrity: sha512-DTjUVvxckL1fIoPSb3KE7ISNtkWSawZdpfxGxwiIrZoO6EbHVDXXUIlIuWympPaeS+BLGyggozX/HTMsRAdsoA==} - tldts@6.1.77: - resolution: {integrity: sha512-lBpoWgy+kYmuXWQ83+R7LlJCnsd9YW8DGpZSHhrMl4b8Ly/1vzOie3OdtmUJDkKxcgRGOehDu5btKkty+JEe+g==} + tldts@6.1.85: + resolution: {integrity: sha512-gBdZ1RjCSevRPFix/hpaUWeak2/RNUZB4/8frF1r5uYMHjFptkiT0JXIebWvgI/0ZHXvxaUDDJshiA0j6GdL3w==} hasBin: true tmp@0.0.33: @@ -6650,15 +6570,15 @@ packages: resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} engines: {node: '>=6'} - tough-cookie@5.1.1: - resolution: {integrity: sha512-Ek7HndSVkp10hmHP9V4qZO1u+pn1RU5sI0Fw+jCU3lyvuMZcgqsNgc6CmJJZyByK4Vm/qotGRJlfgAX8q+4JiA==} + tough-cookie@5.1.2: + resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==} engines: {node: '>=16'} tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} - tr46@5.0.0: - resolution: {integrity: sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==} + tr46@5.1.0: + resolution: {integrity: sha512-IUWnUK7ADYR5Sl1fZlO1INDUhVhatWl7BtJWsIhwJ0UAK7ilzzIa8uIqOO/aYVWHZPJkKbEL+362wrzoeRF7bw==} engines: {node: '>=18'} tree-kill@1.2.2: @@ -6786,8 +6706,8 @@ packages: resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} engines: {node: '>= 0.4'} - typescript@5.7.3: - resolution: {integrity: sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==} + typescript@5.8.3: + resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} engines: {node: '>=14.17'} hasBin: true @@ -6801,12 +6721,13 @@ packages: undici-types@6.19.8: resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} - undici-types@6.20.0: - resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==} - undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + undici@6.21.1: + resolution: {integrity: sha512-q/1rj5D0/zayJB2FraXdaWxbhWiNKDvu8naDT2dl1yTlvJp4BLtOcp2a5BvgGNQpYYJzau7tf1WgKv3b+7mqpQ==} + engines: {node: '>=18.17'} + undici@6.21.2: resolution: {integrity: sha512-uROZWze0R0itiAKVPsYhFov9LxrPMHLMEQFszeI2gCN6bnIIZ8twzBCJcN2LJrBBLfrP0t1FW0g+JmKVl8Vk1g==} engines: {node: '>=18.17'} @@ -6854,8 +6775,8 @@ packages: unist-util-visit@4.1.2: resolution: {integrity: sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==} - universal-user-agent@6.0.1: - resolution: {integrity: sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==} + universal-user-agent@7.0.2: + resolution: {integrity: sha512-0JCqzSKnStlRRQfCdowvqy3cy0Dvtlb8xecj/H8JFZuCze4rwjPZQOgvFvn0Ws/usCHQFGpyr+pB9adaGwXn4Q==} universalify@0.1.2: resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} @@ -6869,6 +6790,9 @@ packages: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} + unrs-resolver@1.3.3: + resolution: {integrity: sha512-PFLAGQzYlyjniXdbmQ3dnGMZJXX5yrl2YS4DLRfR3BhgUsE1zpRIrccp9XMOGRfIHpdFvCn/nr5N1KMVda4x3A==} + update-browserslist-db@1.1.3: resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==} hasBin: true @@ -6959,8 +6883,13 @@ packages: engines: {node: ^18.0.0 || >=20.0.0} hasBin: true - vite-node@3.0.8: - resolution: {integrity: sha512-6PhR4H9VGlcwXZ+KWCdMqbtG649xCPZqfI9j2PsK1FcXgEzro5bGHcVKFCTqPLaNKZES8Evqv4LwvZARsq5qlg==} + vite-node@3.0.0-beta.2: + resolution: {integrity: sha512-ofTf6cfRdL30Wbl9n/BX81EyIR5s4PReLmSurrxQ+koLaWUNOEo8E0lCM53OJkb8vpa2URM2nSrxZsIFyvY1rg==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + + vite-node@3.1.1: + resolution: {integrity: sha512-V+IxPAE2FvXpTCHXyNem0M+gWm6J7eRyWPR6vYoG/Gl+IscNOjXzztUhimQgTxaAoUoj40Qqimaa0NLIOOAH4w==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true @@ -6972,6 +6901,37 @@ packages: vite: optional: true + vite@5.4.17: + resolution: {integrity: sha512-5+VqZryDj4wgCs55o9Lp+p8GE78TLVg0lasCH5xFZ4jacZjtqZa6JUw9/p0WeAojaOfncSM6v77InkFPGnvPvg==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + vite@5.4.18: resolution: {integrity: sha512-1oDcnEp3lVyHCuQ2YFelM4Alm2o91xNoMncRm1U7S+JdYfYOvbiGZ3/CxGttrOu2M/KcGz7cRC2DoNUA6urmMA==} engines: {node: ^18.0.0 || >=20.0.0} @@ -7003,16 +6963,16 @@ packages: terser: optional: true - vitest@3.0.8: - resolution: {integrity: sha512-dfqAsNqRGUc8hB9OVR2P0w8PZPEckti2+5rdZip0WIz9WW0MnImJ8XiR61QhqLa92EQzKP2uPkzenKOAHyEIbA==} + vitest@3.1.1: + resolution: {integrity: sha512-kiZc/IYmKICeBAZr9DQ5rT7/6bD9G7uqQEki4fxazi1jdVl2mWGzedtBs5s6llz59yQhVb7FFY2MbHzHCnT79Q==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' '@types/debug': ^4.1.12 '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 - '@vitest/browser': 3.0.8 - '@vitest/ui': 3.0.8 + '@vitest/browser': 3.1.1 + '@vitest/ui': 3.1.1 happy-dom: '*' jsdom: '*' peerDependenciesMeta: @@ -7061,29 +7021,12 @@ packages: resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} engines: {node: '>= 8'} - webdriver@9.12.4: - resolution: {integrity: sha512-oGFjtmLxyE3WSMb6DshJkDQPNyYqFmXyepOBbG9rOBhVgIFjmur8eS56wgAPZT0fVcmFKRbY+7xcIlxS/dEr7g==} - engines: {node: '>=18.20.0'} - - webdriver@9.12.5: - resolution: {integrity: sha512-CQCb1kDh52VtzPOIWc6XOdRz9q07LMAm9XwL+ABLSd0gueJq+GZoUTqHVX1YwVF0EQlFnw0JYJok0hxGH7m7cw==} - engines: {node: '>=18.20.0'} - - webdriver@9.9.1: - resolution: {integrity: sha512-VqHDph80Pd/HmeEtoNiqX/ixML/ub8Rw54oviVYm6V7cbnzACrSbSlt9zpdWfjEk+Qkm/CytyYFggan30RfAiQ==} + webdriver@9.12.6: + resolution: {integrity: sha512-Alz+JiaVW15b/Qy6zSmJeYXxvmtMIVpEAg7QDfCWqG9miZSKJYWwgWE3xoSrwYn5kTylUszqb17Pb5wyrj7YFw==} engines: {node: '>=18.20.0'} - webdriverio@9.12.4: - resolution: {integrity: sha512-to2VLGmlm5OR4cMAwZ5uYTLUl/av/9XkIiRrY1FxEfCwI0+rWPHfE/xsHs7xtHMjQpEaaGKovrjCMoQ5W2WSOg==} - engines: {node: '>=18.20.0'} - peerDependencies: - puppeteer-core: ^22.3.0 - peerDependenciesMeta: - puppeteer-core: - optional: true - - webdriverio@9.12.5: - resolution: {integrity: sha512-ho7gEOdPkpMlZJ5fbCX6+zAllnVdYl8X9RZ4x3tDabf3ByEzReqexaTVou8ayWmNngGjarWlXX3ov1BIdhQTLQ==} + webdriverio@9.12.6: + resolution: {integrity: sha512-cAuDFz2NgDxxvhC2/xEW7Pc5lC2TGMcohOuRmbQmSx4hNc82Hp2QpM+fVAS0ZDr9lzg9h3u0bL/blyV2Ah5NCg==} engines: {node: '>=18.20.0'} peerDependencies: puppeteer-core: '>=22.x || <=24.x' @@ -7091,15 +7034,6 @@ packages: puppeteer-core: optional: true - webdriverio@9.9.1: - resolution: {integrity: sha512-3TO8JcA2fylti2ExsIKgyicwdDvft5slWdq1wz50BXw41/3yOwyg4z8UkT6fUuSUYDOt8QPlfddALtOdQqvuKA==} - engines: {node: '>=18.20.0'} - peerDependencies: - puppeteer-core: ^22.3.0 - peerDependenciesMeta: - puppeteer-core: - optional: true - webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} @@ -7115,8 +7049,8 @@ packages: resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} engines: {node: '>=18'} - whatwg-url@14.1.1: - resolution: {integrity: sha512-mDGf9diDad/giZ/Sm9Xi2YcyzaFpbdLpJPr+E9fSkyQ7KpQD4SdFcugkRQYzhmfI4KeV4Qpnn2sKPdo+kmsgRQ==} + whatwg-url@14.2.0: + resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} engines: {node: '>=18'} whatwg-url@5.0.0: @@ -7168,9 +7102,9 @@ packages: wildcard-match@5.1.4: resolution: {integrity: sha512-wldeCaczs8XXq7hj+5d/F38JE2r7EXgb6WQDM84RVwxy81T/sxB5e9+uZLK9Q9oNz1mlvjut+QtvgaOQFPVq/g==} - windows-release@5.1.1: - resolution: {integrity: sha512-NMD00arvqcq2nwqc5Q6KtrSRHK+fVD31erE5FEMahAw5PmVCgD7MUXodq3pdZSUkqA9Cda2iWx6s1XYwiJWRmw==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + windows-release@6.0.1: + resolution: {integrity: sha512-MS3BzG8QK33dAyqwxfYJCJ03arkwKaddUOvvnnlFdXLudflsQF6I8yAxrLBeQk4yO8wjdH/+ax0YzxJEDrOftg==} + engines: {node: '>=18'} winston-transport@4.9.0: resolution: {integrity: sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==} @@ -7218,18 +7152,6 @@ packages: utf-8-validate: optional: true - ws@8.18.0: - resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} - engines: {node: '>=10.0.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: '>=5.0.2' - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - ws@8.18.1: resolution: {integrity: sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==} engines: {node: '>=10.0.0'} @@ -7286,8 +7208,8 @@ packages: resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} engines: {node: '>= 6'} - yaml@2.7.0: - resolution: {integrity: sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==} + yaml@2.7.1: + resolution: {integrity: sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==} engines: {node: '>= 14'} hasBin: true @@ -7361,10 +7283,10 @@ snapshots: '@arr/every@1.0.1': {} - '@asamuzakjp/css-color@2.8.3': + '@asamuzakjp/css-color@3.1.1': dependencies: - '@csstools/css-calc': 2.1.1(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) - '@csstools/css-color-parser': 3.0.7(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) + '@csstools/css-calc': 2.1.2(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) + '@csstools/css-color-parser': 3.0.8(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) '@csstools/css-tokenizer': 3.0.3 lru-cache: 10.4.3 @@ -7377,18 +7299,18 @@ snapshots: '@babel/compat-data@7.26.8': {} - '@babel/core@7.26.9': + '@babel/core@7.26.10': dependencies: '@ampproject/remapping': 2.3.0 '@babel/code-frame': 7.26.2 - '@babel/generator': 7.26.9 - '@babel/helper-compilation-targets': 7.26.5 - '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.9) - '@babel/helpers': 7.26.9 - '@babel/parser': 7.26.9 - '@babel/template': 7.26.9 - '@babel/traverse': 7.26.9 - '@babel/types': 7.26.9 + '@babel/generator': 7.27.0 + '@babel/helper-compilation-targets': 7.27.0 + '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.10) + '@babel/helpers': 7.27.0 + '@babel/parser': 7.27.0 + '@babel/template': 7.27.0 + '@babel/traverse': 7.27.0 + '@babel/types': 7.27.0 convert-source-map: 2.0.0 debug: 4.4.0(supports-color@8.1.1) gensync: 1.0.0-beta.2 @@ -7397,14 +7319,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/generator@7.26.9': - dependencies: - '@babel/parser': 7.26.9 - '@babel/types': 7.26.9 - '@jridgewell/gen-mapping': 0.3.8 - '@jridgewell/trace-mapping': 0.3.25 - jsesc: 3.0.2 - '@babel/generator@7.27.0': dependencies: '@babel/parser': 7.27.0 @@ -7415,9 +7329,9 @@ snapshots: '@babel/helper-annotate-as-pure@7.25.9': dependencies: - '@babel/types': 7.26.9 + '@babel/types': 7.27.0 - '@babel/helper-compilation-targets@7.26.5': + '@babel/helper-compilation-targets@7.27.0': dependencies: '@babel/compat-data': 7.26.8 '@babel/helper-validator-option': 7.25.9 @@ -7425,23 +7339,23 @@ snapshots: lru-cache: 5.1.1 semver: 6.3.1 - '@babel/helper-create-class-features-plugin@7.26.9(@babel/core@7.26.9)': + '@babel/helper-create-class-features-plugin@7.27.0(@babel/core@7.26.10)': dependencies: - '@babel/core': 7.26.9 + '@babel/core': 7.26.10 '@babel/helper-annotate-as-pure': 7.25.9 '@babel/helper-member-expression-to-functions': 7.25.9 '@babel/helper-optimise-call-expression': 7.25.9 - '@babel/helper-replace-supers': 7.26.5(@babel/core@7.26.9) + '@babel/helper-replace-supers': 7.26.5(@babel/core@7.26.10) '@babel/helper-skip-transparent-expression-wrappers': 7.25.9 - '@babel/traverse': 7.26.9 + '@babel/traverse': 7.27.0 semver: 6.3.1 transitivePeerDependencies: - supports-color '@babel/helper-member-expression-to-functions@7.25.9': dependencies: - '@babel/traverse': 7.26.9 - '@babel/types': 7.26.9 + '@babel/traverse': 7.27.0 + '@babel/types': 7.27.0 transitivePeerDependencies: - supports-color @@ -7452,34 +7366,34 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/helper-module-transforms@7.26.0(@babel/core@7.26.9)': + '@babel/helper-module-transforms@7.26.0(@babel/core@7.26.10)': dependencies: - '@babel/core': 7.26.9 + '@babel/core': 7.26.10 '@babel/helper-module-imports': 7.25.9 '@babel/helper-validator-identifier': 7.25.9 - '@babel/traverse': 7.26.9 + '@babel/traverse': 7.27.0 transitivePeerDependencies: - supports-color '@babel/helper-optimise-call-expression@7.25.9': dependencies: - '@babel/types': 7.26.9 + '@babel/types': 7.27.0 '@babel/helper-plugin-utils@7.26.5': {} - '@babel/helper-replace-supers@7.26.5(@babel/core@7.26.9)': + '@babel/helper-replace-supers@7.26.5(@babel/core@7.26.10)': dependencies: - '@babel/core': 7.26.9 + '@babel/core': 7.26.10 '@babel/helper-member-expression-to-functions': 7.25.9 '@babel/helper-optimise-call-expression': 7.25.9 - '@babel/traverse': 7.26.9 + '@babel/traverse': 7.27.0 transitivePeerDependencies: - supports-color '@babel/helper-skip-transparent-expression-wrappers@7.25.9': dependencies: - '@babel/traverse': 7.26.9 - '@babel/types': 7.26.9 + '@babel/traverse': 7.27.0 + '@babel/types': 7.27.0 transitivePeerDependencies: - supports-color @@ -7489,61 +7403,57 @@ snapshots: '@babel/helper-validator-option@7.25.9': {} - '@babel/helpers@7.26.9': - dependencies: - '@babel/template': 7.26.9 - '@babel/types': 7.26.9 - - '@babel/parser@7.26.9': + '@babel/helpers@7.27.0': dependencies: - '@babel/types': 7.26.9 + '@babel/template': 7.27.0 + '@babel/types': 7.27.0 '@babel/parser@7.27.0': dependencies: '@babel/types': 7.27.0 - '@babel/plugin-syntax-decorators@7.25.9(@babel/core@7.26.9)': + '@babel/plugin-syntax-decorators@7.25.9(@babel/core@7.26.10)': dependencies: - '@babel/core': 7.26.9 + '@babel/core': 7.26.10 '@babel/helper-plugin-utils': 7.26.5 - '@babel/plugin-syntax-jsx@7.25.9(@babel/core@7.26.9)': + '@babel/plugin-syntax-jsx@7.25.9(@babel/core@7.26.10)': dependencies: - '@babel/core': 7.26.9 + '@babel/core': 7.26.10 '@babel/helper-plugin-utils': 7.26.5 - '@babel/plugin-syntax-typescript@7.25.9(@babel/core@7.26.9)': + '@babel/plugin-syntax-typescript@7.25.9(@babel/core@7.26.10)': dependencies: - '@babel/core': 7.26.9 + '@babel/core': 7.26.10 '@babel/helper-plugin-utils': 7.26.5 - '@babel/plugin-transform-modules-commonjs@7.26.3(@babel/core@7.26.9)': + '@babel/plugin-transform-modules-commonjs@7.26.3(@babel/core@7.26.10)': dependencies: - '@babel/core': 7.26.9 - '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.9) + '@babel/core': 7.26.10 + '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.10) '@babel/helper-plugin-utils': 7.26.5 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-typescript@7.26.8(@babel/core@7.26.9)': + '@babel/plugin-transform-typescript@7.27.0(@babel/core@7.26.10)': dependencies: - '@babel/core': 7.26.9 + '@babel/core': 7.26.10 '@babel/helper-annotate-as-pure': 7.25.9 - '@babel/helper-create-class-features-plugin': 7.26.9(@babel/core@7.26.9) + '@babel/helper-create-class-features-plugin': 7.27.0(@babel/core@7.26.10) '@babel/helper-plugin-utils': 7.26.5 '@babel/helper-skip-transparent-expression-wrappers': 7.25.9 - '@babel/plugin-syntax-typescript': 7.25.9(@babel/core@7.26.9) + '@babel/plugin-syntax-typescript': 7.25.9(@babel/core@7.26.10) transitivePeerDependencies: - supports-color - '@babel/preset-typescript@7.26.0(@babel/core@7.26.9)': + '@babel/preset-typescript@7.27.0(@babel/core@7.26.10)': dependencies: - '@babel/core': 7.26.9 + '@babel/core': 7.26.10 '@babel/helper-plugin-utils': 7.26.5 '@babel/helper-validator-option': 7.25.9 - '@babel/plugin-syntax-jsx': 7.25.9(@babel/core@7.26.9) - '@babel/plugin-transform-modules-commonjs': 7.26.3(@babel/core@7.26.9) - '@babel/plugin-transform-typescript': 7.26.8(@babel/core@7.26.9) + '@babel/plugin-syntax-jsx': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-transform-modules-commonjs': 7.26.3(@babel/core@7.26.10) + '@babel/plugin-transform-typescript': 7.27.0(@babel/core@7.26.10) transitivePeerDependencies: - supports-color @@ -7551,30 +7461,12 @@ snapshots: dependencies: regenerator-runtime: 0.14.1 - '@babel/template@7.26.9': - dependencies: - '@babel/code-frame': 7.26.2 - '@babel/parser': 7.26.9 - '@babel/types': 7.26.9 - - '@babel/template@7.27.0': + '@babel/template@7.27.0': dependencies: '@babel/code-frame': 7.26.2 '@babel/parser': 7.27.0 '@babel/types': 7.27.0 - '@babel/traverse@7.26.9': - dependencies: - '@babel/code-frame': 7.26.2 - '@babel/generator': 7.26.9 - '@babel/parser': 7.26.9 - '@babel/template': 7.26.9 - '@babel/types': 7.26.9 - debug: 4.4.0(supports-color@8.1.1) - globals: 11.12.0 - transitivePeerDependencies: - - supports-color - '@babel/traverse@7.27.0': dependencies: '@babel/code-frame': 7.26.2 @@ -7587,11 +7479,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/types@7.26.9': - dependencies: - '@babel/helper-string-parser': 7.25.9 - '@babel/helper-validator-identifier': 7.25.9 - '@babel/types@7.27.0': dependencies: '@babel/helper-string-parser': 7.25.9 @@ -7599,11 +7486,11 @@ snapshots: '@bcoe/v8-coverage@1.0.2': {} - '@changesets/apply-release-plan@7.0.10': + '@changesets/apply-release-plan@7.0.12': dependencies: '@changesets/config': 3.1.1 '@changesets/get-version-range-type': 0.4.0 - '@changesets/git': 3.0.2 + '@changesets/git': 3.0.4 '@changesets/should-skip-package': 0.1.2 '@changesets/types': 6.1.0 '@manypkg/get-packages': 1.1.3 @@ -7628,19 +7515,19 @@ snapshots: dependencies: '@changesets/types': 6.1.0 - '@changesets/cli@2.29.0': + '@changesets/cli@2.29.2': dependencies: - '@changesets/apply-release-plan': 7.0.10 + '@changesets/apply-release-plan': 7.0.12 '@changesets/assemble-release-plan': 6.0.6 '@changesets/changelog-git': 0.2.1 '@changesets/config': 3.1.1 '@changesets/errors': 0.2.0 '@changesets/get-dependents-graph': 2.1.3 - '@changesets/get-release-plan': 4.0.8 - '@changesets/git': 3.0.2 + '@changesets/get-release-plan': 4.0.10 + '@changesets/git': 3.0.4 '@changesets/logger': 0.1.1 '@changesets/pre': 2.0.2 - '@changesets/read': 0.6.3 + '@changesets/read': 0.6.5 '@changesets/should-skip-package': 0.1.2 '@changesets/types': 6.1.0 '@changesets/write': 0.4.0 @@ -7680,18 +7567,18 @@ snapshots: picocolors: 1.1.1 semver: 7.7.1 - '@changesets/get-release-plan@4.0.8': + '@changesets/get-release-plan@4.0.10': dependencies: '@changesets/assemble-release-plan': 6.0.6 '@changesets/config': 3.1.1 '@changesets/pre': 2.0.2 - '@changesets/read': 0.6.3 + '@changesets/read': 0.6.5 '@changesets/types': 6.1.0 '@manypkg/get-packages': 1.1.3 '@changesets/get-version-range-type@0.4.0': {} - '@changesets/git@3.0.2': + '@changesets/git@3.0.4': dependencies: '@changesets/errors': 0.2.0 '@manypkg/get-packages': 1.1.3 @@ -7715,9 +7602,9 @@ snapshots: '@manypkg/get-packages': 1.1.3 fs-extra: 7.0.1 - '@changesets/read@0.6.3': + '@changesets/read@0.6.5': dependencies: - '@changesets/git': 3.0.2 + '@changesets/git': 3.0.4 '@changesets/logger': 0.1.1 '@changesets/parse': 0.4.1 '@changesets/types': 6.1.0 @@ -7747,17 +7634,17 @@ snapshots: dependencies: '@jridgewell/trace-mapping': 0.3.9 - '@csstools/color-helpers@5.0.1': {} + '@csstools/color-helpers@5.0.2': {} - '@csstools/css-calc@2.1.1(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3)': + '@csstools/css-calc@2.1.2(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3)': dependencies: '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) '@csstools/css-tokenizer': 3.0.3 - '@csstools/css-color-parser@3.0.7(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3)': + '@csstools/css-color-parser@3.0.8(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3)': dependencies: - '@csstools/color-helpers': 5.0.1 - '@csstools/css-calc': 2.1.1(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) + '@csstools/color-helpers': 5.0.2 + '@csstools/css-calc': 2.1.2(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) '@csstools/css-tokenizer': 3.0.3 @@ -7778,7 +7665,18 @@ snapshots: buffer-crc32: 0.2.13 fd-slicer2: 1.2.0 - '@emnapi/runtime@1.4.1': + '@emnapi/core@1.4.0': + dependencies: + '@emnapi/wasi-threads': 1.0.1 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.4.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.0.1': dependencies: tslib: 2.8.1 optional: true @@ -7811,7 +7709,7 @@ snapshots: '@emotion/memoize@0.9.0': {} - '@emotion/react@11.14.0(@types/react@18.3.18)(react@18.3.1)': + '@emotion/react@11.14.0(@types/react@18.3.20)(react@18.3.1)': dependencies: '@babel/runtime': 7.27.0 '@emotion/babel-plugin': 11.13.5 @@ -7823,7 +7721,7 @@ snapshots: hoist-non-react-statics: 3.3.2 react: 18.3.1 optionalDependencies: - '@types/react': 18.3.18 + '@types/react': 18.3.20 transitivePeerDependencies: - supports-color @@ -8057,24 +7955,14 @@ snapshots: '@esbuild/win32-x64@0.25.2': optional: true - '@eslint-community/eslint-utils@4.4.1(eslint@9.23.0(jiti@1.21.7))': + '@eslint-community/eslint-utils@4.5.1(eslint@9.24.0(jiti@1.21.7))': dependencies: - eslint: 9.23.0(jiti@1.21.7) - eslint-visitor-keys: 3.4.3 - - '@eslint-community/eslint-utils@4.5.1(eslint@9.23.0(jiti@1.21.7))': - dependencies: - eslint: 9.23.0(jiti@1.21.7) - eslint-visitor-keys: 3.4.3 - - '@eslint-community/eslint-utils@4.6.0(eslint@9.23.0(jiti@1.21.7))': - dependencies: - eslint: 9.23.0(jiti@1.21.7) + eslint: 9.24.0(jiti@1.21.7) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.1': {} - '@eslint/config-array@0.19.2': + '@eslint/config-array@0.20.0': dependencies: '@eslint/object-schema': 2.1.6 debug: 4.4.0(supports-color@8.1.1) @@ -8082,12 +7970,16 @@ snapshots: transitivePeerDependencies: - supports-color - '@eslint/config-helpers@0.2.0': {} + '@eslint/config-helpers@0.2.1': {} '@eslint/core@0.12.0': dependencies: '@types/json-schema': 7.0.15 + '@eslint/core@0.13.0': + dependencies: + '@types/json-schema': 7.0.15 + '@eslint/eslintrc@3.3.1': dependencies: ajv: 6.12.6 @@ -8102,13 +7994,13 @@ snapshots: transitivePeerDependencies: - supports-color - '@eslint/js@9.23.0': {} + '@eslint/js@9.24.0': {} '@eslint/object-schema@2.1.6': {} - '@eslint/plugin-kit@0.2.7': + '@eslint/plugin-kit@0.2.8': dependencies: - '@eslint/core': 0.12.0 + '@eslint/core': 0.13.0 levn: 0.4.1 '@floating-ui/core@1.6.9': @@ -8206,7 +8098,7 @@ snapshots: '@img/sharp-wasm32@0.34.1': dependencies: - '@emnapi/runtime': 1.4.1 + '@emnapi/runtime': 1.4.0 optional: true '@img/sharp-win32-ia32@0.34.1': @@ -8223,32 +8115,32 @@ snapshots: ansi-escapes: 4.3.2 yoctocolors-cjs: 2.1.2 - '@inquirer/checkbox@4.1.2(@types/node@22.14.1)': + '@inquirer/checkbox@4.1.5(@types/node@22.14.0)': dependencies: - '@inquirer/core': 10.1.7(@types/node@22.14.1) - '@inquirer/figures': 1.0.10 - '@inquirer/type': 3.0.4(@types/node@22.14.1) + '@inquirer/core': 10.1.10(@types/node@22.14.0) + '@inquirer/figures': 1.0.11 + '@inquirer/type': 3.0.6(@types/node@22.14.0) ansi-escapes: 4.3.2 yoctocolors-cjs: 2.1.2 optionalDependencies: - '@types/node': 22.14.1 + '@types/node': 22.14.0 '@inquirer/confirm@4.0.1': dependencies: '@inquirer/core': 9.2.1 '@inquirer/type': 2.0.0 - '@inquirer/confirm@5.1.6(@types/node@22.14.1)': + '@inquirer/confirm@5.1.9(@types/node@22.14.0)': dependencies: - '@inquirer/core': 10.1.7(@types/node@22.14.1) - '@inquirer/type': 3.0.4(@types/node@22.14.1) + '@inquirer/core': 10.1.10(@types/node@22.14.0) + '@inquirer/type': 3.0.6(@types/node@22.14.0) optionalDependencies: - '@types/node': 22.14.1 + '@types/node': 22.14.0 - '@inquirer/core@10.1.7(@types/node@22.14.1)': + '@inquirer/core@10.1.10(@types/node@22.14.0)': dependencies: - '@inquirer/figures': 1.0.10 - '@inquirer/type': 3.0.4(@types/node@22.14.1) + '@inquirer/figures': 1.0.11 + '@inquirer/type': 3.0.6(@types/node@22.14.0) ansi-escapes: 4.3.2 cli-width: 4.1.0 mute-stream: 2.0.0 @@ -8256,14 +8148,14 @@ snapshots: wrap-ansi: 6.2.0 yoctocolors-cjs: 2.1.2 optionalDependencies: - '@types/node': 22.14.1 + '@types/node': 22.14.0 '@inquirer/core@9.2.1': dependencies: '@inquirer/figures': 1.0.11 '@inquirer/type': 2.0.0 '@types/mute-stream': 0.0.4 - '@types/node': 22.14.1 + '@types/node': 22.14.0 '@types/wrap-ansi': 3.0.0 ansi-escapes: 4.3.2 cli-width: 4.1.0 @@ -8279,13 +8171,13 @@ snapshots: '@inquirer/type': 2.0.0 external-editor: 3.1.0 - '@inquirer/editor@4.2.7(@types/node@22.14.1)': + '@inquirer/editor@4.2.10(@types/node@22.14.0)': dependencies: - '@inquirer/core': 10.1.7(@types/node@22.14.1) - '@inquirer/type': 3.0.4(@types/node@22.14.1) + '@inquirer/core': 10.1.10(@types/node@22.14.0) + '@inquirer/type': 3.0.6(@types/node@22.14.0) external-editor: 3.1.0 optionalDependencies: - '@types/node': 22.14.1 + '@types/node': 22.14.0 '@inquirer/expand@3.0.1': dependencies: @@ -8293,15 +8185,13 @@ snapshots: '@inquirer/type': 2.0.0 yoctocolors-cjs: 2.1.2 - '@inquirer/expand@4.0.9(@types/node@22.14.1)': + '@inquirer/expand@4.0.12(@types/node@22.14.0)': dependencies: - '@inquirer/core': 10.1.7(@types/node@22.14.1) - '@inquirer/type': 3.0.4(@types/node@22.14.1) + '@inquirer/core': 10.1.10(@types/node@22.14.0) + '@inquirer/type': 3.0.6(@types/node@22.14.0) yoctocolors-cjs: 2.1.2 optionalDependencies: - '@types/node': 22.14.1 - - '@inquirer/figures@1.0.10': {} + '@types/node': 22.14.0 '@inquirer/figures@1.0.11': {} @@ -8310,24 +8200,24 @@ snapshots: '@inquirer/core': 9.2.1 '@inquirer/type': 2.0.0 - '@inquirer/input@4.1.6(@types/node@22.14.1)': + '@inquirer/input@4.1.9(@types/node@22.14.0)': dependencies: - '@inquirer/core': 10.1.7(@types/node@22.14.1) - '@inquirer/type': 3.0.4(@types/node@22.14.1) + '@inquirer/core': 10.1.10(@types/node@22.14.0) + '@inquirer/type': 3.0.6(@types/node@22.14.0) optionalDependencies: - '@types/node': 22.14.1 + '@types/node': 22.14.0 '@inquirer/number@2.0.1': dependencies: '@inquirer/core': 9.2.1 '@inquirer/type': 2.0.0 - '@inquirer/number@3.0.9(@types/node@22.14.1)': + '@inquirer/number@3.0.12(@types/node@22.14.0)': dependencies: - '@inquirer/core': 10.1.7(@types/node@22.14.1) - '@inquirer/type': 3.0.4(@types/node@22.14.1) + '@inquirer/core': 10.1.10(@types/node@22.14.0) + '@inquirer/type': 3.0.6(@types/node@22.14.0) optionalDependencies: - '@types/node': 22.14.1 + '@types/node': 22.14.0 '@inquirer/password@3.0.1': dependencies: @@ -8335,13 +8225,13 @@ snapshots: '@inquirer/type': 2.0.0 ansi-escapes: 4.3.2 - '@inquirer/password@4.0.9(@types/node@22.14.1)': + '@inquirer/password@4.0.12(@types/node@22.14.0)': dependencies: - '@inquirer/core': 10.1.7(@types/node@22.14.1) - '@inquirer/type': 3.0.4(@types/node@22.14.1) + '@inquirer/core': 10.1.10(@types/node@22.14.0) + '@inquirer/type': 3.0.6(@types/node@22.14.0) ansi-escapes: 4.3.2 optionalDependencies: - '@types/node': 22.14.1 + '@types/node': 22.14.0 '@inquirer/prompts@6.0.1': dependencies: @@ -8356,20 +8246,20 @@ snapshots: '@inquirer/search': 2.0.1 '@inquirer/select': 3.0.1 - '@inquirer/prompts@7.3.2(@types/node@22.14.1)': - dependencies: - '@inquirer/checkbox': 4.1.2(@types/node@22.14.1) - '@inquirer/confirm': 5.1.6(@types/node@22.14.1) - '@inquirer/editor': 4.2.7(@types/node@22.14.1) - '@inquirer/expand': 4.0.9(@types/node@22.14.1) - '@inquirer/input': 4.1.6(@types/node@22.14.1) - '@inquirer/number': 3.0.9(@types/node@22.14.1) - '@inquirer/password': 4.0.9(@types/node@22.14.1) - '@inquirer/rawlist': 4.0.9(@types/node@22.14.1) - '@inquirer/search': 3.0.9(@types/node@22.14.1) - '@inquirer/select': 4.0.9(@types/node@22.14.1) + '@inquirer/prompts@7.4.1(@types/node@22.14.0)': + dependencies: + '@inquirer/checkbox': 4.1.5(@types/node@22.14.0) + '@inquirer/confirm': 5.1.9(@types/node@22.14.0) + '@inquirer/editor': 4.2.10(@types/node@22.14.0) + '@inquirer/expand': 4.0.12(@types/node@22.14.0) + '@inquirer/input': 4.1.9(@types/node@22.14.0) + '@inquirer/number': 3.0.12(@types/node@22.14.0) + '@inquirer/password': 4.0.12(@types/node@22.14.0) + '@inquirer/rawlist': 4.0.12(@types/node@22.14.0) + '@inquirer/search': 3.0.12(@types/node@22.14.0) + '@inquirer/select': 4.1.1(@types/node@22.14.0) optionalDependencies: - '@types/node': 22.14.1 + '@types/node': 22.14.0 '@inquirer/rawlist@3.0.1': dependencies: @@ -8377,13 +8267,13 @@ snapshots: '@inquirer/type': 2.0.0 yoctocolors-cjs: 2.1.2 - '@inquirer/rawlist@4.0.9(@types/node@22.14.1)': + '@inquirer/rawlist@4.0.12(@types/node@22.14.0)': dependencies: - '@inquirer/core': 10.1.7(@types/node@22.14.1) - '@inquirer/type': 3.0.4(@types/node@22.14.1) + '@inquirer/core': 10.1.10(@types/node@22.14.0) + '@inquirer/type': 3.0.6(@types/node@22.14.0) yoctocolors-cjs: 2.1.2 optionalDependencies: - '@types/node': 22.14.1 + '@types/node': 22.14.0 '@inquirer/search@2.0.1': dependencies: @@ -8392,14 +8282,14 @@ snapshots: '@inquirer/type': 2.0.0 yoctocolors-cjs: 2.1.2 - '@inquirer/search@3.0.9(@types/node@22.14.1)': + '@inquirer/search@3.0.12(@types/node@22.14.0)': dependencies: - '@inquirer/core': 10.1.7(@types/node@22.14.1) - '@inquirer/figures': 1.0.10 - '@inquirer/type': 3.0.4(@types/node@22.14.1) + '@inquirer/core': 10.1.10(@types/node@22.14.0) + '@inquirer/figures': 1.0.11 + '@inquirer/type': 3.0.6(@types/node@22.14.0) yoctocolors-cjs: 2.1.2 optionalDependencies: - '@types/node': 22.14.1 + '@types/node': 22.14.0 '@inquirer/select@3.0.1': dependencies: @@ -8409,23 +8299,23 @@ snapshots: ansi-escapes: 4.3.2 yoctocolors-cjs: 2.1.2 - '@inquirer/select@4.0.9(@types/node@22.14.1)': + '@inquirer/select@4.1.1(@types/node@22.14.0)': dependencies: - '@inquirer/core': 10.1.7(@types/node@22.14.1) - '@inquirer/figures': 1.0.10 - '@inquirer/type': 3.0.4(@types/node@22.14.1) + '@inquirer/core': 10.1.10(@types/node@22.14.0) + '@inquirer/figures': 1.0.11 + '@inquirer/type': 3.0.6(@types/node@22.14.0) ansi-escapes: 4.3.2 yoctocolors-cjs: 2.1.2 optionalDependencies: - '@types/node': 22.14.1 + '@types/node': 22.14.0 '@inquirer/type@2.0.0': dependencies: mute-stream: 1.0.0 - '@inquirer/type@3.0.4(@types/node@22.14.1)': + '@inquirer/type@3.0.6(@types/node@22.14.0)': optionalDependencies: - '@types/node': 22.14.1 + '@types/node': 22.14.0 '@isaacs/cliui@8.0.2': dependencies: @@ -8451,7 +8341,7 @@ snapshots: '@jest/schemas': 29.6.3 '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 22.14.1 + '@types/node': 22.14.0 '@types/yargs': 17.0.33 chalk: 4.1.2 @@ -8666,12 +8556,12 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.0 - '@jspm/core@2.0.1': {} + '@jspm/core@2.1.0': {} - '@lambdatest/node-tunnel@4.0.8': + '@lambdatest/node-tunnel@4.0.9': dependencies: adm-zip: 0.5.16 - axios: 1.7.9 + axios: 1.8.4 get-port: 1.0.0 https-proxy-agent: 5.0.1 split: 1.0.1 @@ -8717,6 +8607,13 @@ snapshots: transitivePeerDependencies: - supports-color + '@napi-rs/wasm-runtime@0.2.8': + dependencies: + '@emnapi/core': 1.4.0 + '@emnapi/runtime': 1.4.0 + '@tybys/wasm-util': 0.9.0 + optional: true + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -8727,7 +8624,7 @@ snapshots: '@nodelib/fs.walk@1.2.8': dependencies: '@nodelib/fs.scandir': 2.1.5 - fastq: 1.19.0 + fastq: 1.19.1 '@nolyfill/is-core-module@1.0.39': {} @@ -8764,68 +8661,67 @@ snapshots: dependencies: which: 3.0.1 - '@octokit/auth-token@4.0.0': {} + '@octokit/auth-token@5.1.2': {} - '@octokit/core@5.2.0': + '@octokit/core@6.1.4': dependencies: - '@octokit/auth-token': 4.0.0 - '@octokit/graphql': 7.1.0 - '@octokit/request': 8.4.1 - '@octokit/request-error': 5.1.1 - '@octokit/types': 13.8.0 - before-after-hook: 2.2.3 - universal-user-agent: 6.0.1 + '@octokit/auth-token': 5.1.2 + '@octokit/graphql': 8.2.1 + '@octokit/request': 9.2.2 + '@octokit/request-error': 6.1.7 + '@octokit/types': 13.10.0 + before-after-hook: 3.0.2 + universal-user-agent: 7.0.2 - '@octokit/endpoint@9.0.6': + '@octokit/endpoint@10.1.3': dependencies: - '@octokit/types': 13.8.0 - universal-user-agent: 6.0.1 + '@octokit/types': 13.10.0 + universal-user-agent: 7.0.2 - '@octokit/graphql@7.1.0': + '@octokit/graphql@8.2.1': dependencies: - '@octokit/request': 8.4.1 - '@octokit/types': 13.8.0 - universal-user-agent: 6.0.1 + '@octokit/request': 9.2.2 + '@octokit/types': 13.10.0 + universal-user-agent: 7.0.2 - '@octokit/openapi-types@23.0.1': {} + '@octokit/openapi-types@24.2.0': {} - '@octokit/plugin-paginate-rest@11.3.1(@octokit/core@5.2.0)': + '@octokit/plugin-paginate-rest@11.6.0(@octokit/core@6.1.4)': dependencies: - '@octokit/core': 5.2.0 - '@octokit/types': 13.8.0 + '@octokit/core': 6.1.4 + '@octokit/types': 13.10.0 - '@octokit/plugin-request-log@4.0.1(@octokit/core@5.2.0)': + '@octokit/plugin-request-log@5.3.1(@octokit/core@6.1.4)': dependencies: - '@octokit/core': 5.2.0 + '@octokit/core': 6.1.4 - '@octokit/plugin-rest-endpoint-methods@13.2.2(@octokit/core@5.2.0)': + '@octokit/plugin-rest-endpoint-methods@13.5.0(@octokit/core@6.1.4)': dependencies: - '@octokit/core': 5.2.0 - '@octokit/types': 13.8.0 + '@octokit/core': 6.1.4 + '@octokit/types': 13.10.0 - '@octokit/request-error@5.1.1': + '@octokit/request-error@6.1.7': dependencies: - '@octokit/types': 13.8.0 - deprecation: 2.3.1 - once: 1.4.0 + '@octokit/types': 13.10.0 - '@octokit/request@8.4.1': + '@octokit/request@9.2.2': dependencies: - '@octokit/endpoint': 9.0.6 - '@octokit/request-error': 5.1.1 - '@octokit/types': 13.8.0 - universal-user-agent: 6.0.1 + '@octokit/endpoint': 10.1.3 + '@octokit/request-error': 6.1.7 + '@octokit/types': 13.10.0 + fast-content-type-parse: 2.0.1 + universal-user-agent: 7.0.2 - '@octokit/rest@20.1.1': + '@octokit/rest@21.0.2': dependencies: - '@octokit/core': 5.2.0 - '@octokit/plugin-paginate-rest': 11.3.1(@octokit/core@5.2.0) - '@octokit/plugin-request-log': 4.0.1(@octokit/core@5.2.0) - '@octokit/plugin-rest-endpoint-methods': 13.2.2(@octokit/core@5.2.0) + '@octokit/core': 6.1.4 + '@octokit/plugin-paginate-rest': 11.6.0(@octokit/core@6.1.4) + '@octokit/plugin-request-log': 5.3.1(@octokit/core@6.1.4) + '@octokit/plugin-rest-endpoint-methods': 13.5.0(@octokit/core@6.1.4) - '@octokit/types@13.8.0': + '@octokit/types@13.10.0': dependencies: - '@octokit/openapi-types': 23.0.1 + '@octokit/openapi-types': 24.2.0 '@pkgjs/parseargs@0.11.0': optional: true @@ -8852,33 +8748,6 @@ snapshots: dependencies: spacetrim: 0.11.59 - '@puppeteer/browsers@2.10.0': - dependencies: - debug: 4.4.0(supports-color@8.1.1) - extract-zip: 2.0.1 - progress: 2.0.3 - proxy-agent: 6.5.0 - semver: 7.7.1 - tar-fs: 3.0.8 - yargs: 17.7.2 - transitivePeerDependencies: - - bare-buffer - - supports-color - - '@puppeteer/browsers@2.7.1': - dependencies: - debug: 4.4.0(supports-color@8.1.1) - extract-zip: 2.0.1 - progress: 2.0.3 - proxy-agent: 6.5.0 - semver: 7.7.1 - tar-fs: 3.0.8 - yargs: 17.7.2 - transitivePeerDependencies: - - bare-buffer - - supports-color - optional: true - '@puppeteer/browsers@2.9.0': dependencies: debug: 4.4.0(supports-color@8.1.1) @@ -8892,24 +8761,24 @@ snapshots: - bare-buffer - supports-color - '@remix-run/dev@2.15.3(@remix-run/react@2.16.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.7.3))(@remix-run/serve@2.16.4(typescript@5.7.3))(@types/node@22.14.1)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.14.1)(typescript@5.7.3))(typescript@5.7.3)(vite@5.4.18(@types/node@22.14.1))': + '@remix-run/dev@2.16.5(@remix-run/react@2.16.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3))(@remix-run/serve@2.16.5(typescript@5.8.3))(@types/node@22.14.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.14.0)(typescript@5.8.3))(typescript@5.8.3)(vite@5.4.18(@types/node@22.14.0))': dependencies: - '@babel/core': 7.26.9 - '@babel/generator': 7.26.9 - '@babel/parser': 7.26.9 - '@babel/plugin-syntax-decorators': 7.25.9(@babel/core@7.26.9) - '@babel/plugin-syntax-jsx': 7.25.9(@babel/core@7.26.9) - '@babel/preset-typescript': 7.26.0(@babel/core@7.26.9) - '@babel/traverse': 7.26.9 - '@babel/types': 7.26.9 + '@babel/core': 7.26.10 + '@babel/generator': 7.27.0 + '@babel/parser': 7.27.0 + '@babel/plugin-syntax-decorators': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-syntax-jsx': 7.25.9(@babel/core@7.26.10) + '@babel/preset-typescript': 7.27.0(@babel/core@7.26.10) + '@babel/traverse': 7.27.0 + '@babel/types': 7.27.0 '@mdx-js/mdx': 2.3.0 '@npmcli/package-json': 4.0.1 - '@remix-run/node': 2.15.3(typescript@5.7.3) - '@remix-run/react': 2.16.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.7.3) - '@remix-run/router': 1.22.0 - '@remix-run/server-runtime': 2.15.3(typescript@5.7.3) + '@remix-run/node': 2.16.5(typescript@5.8.3) + '@remix-run/react': 2.16.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3) + '@remix-run/router': 1.23.0 + '@remix-run/server-runtime': 2.16.5(typescript@5.8.3) '@types/mdx': 2.0.13 - '@vanilla-extract/integration': 6.5.0(@types/node@22.14.1)(babel-plugin-macros@3.1.0) + '@vanilla-extract/integration': 6.5.0(@types/node@22.14.0)(babel-plugin-macros@3.1.0) arg: 5.0.2 cacache: 17.1.4 chalk: 4.1.2 @@ -8918,7 +8787,7 @@ snapshots: dotenv: 16.4.7 es-module-lexer: 1.6.0 esbuild: 0.17.6 - esbuild-plugins-node-modules-polyfill: 1.6.8(esbuild@0.17.6) + esbuild-plugins-node-modules-polyfill: 1.7.0(esbuild@0.17.6) execa: 5.1.1 exit-hook: 2.2.1 express: 4.21.2 @@ -8931,12 +8800,13 @@ snapshots: lodash.debounce: 4.0.8 minimatch: 9.0.5 ora: 5.4.1 + pathe: 1.1.2 picocolors: 1.1.1 picomatch: 2.3.1 pidtree: 0.6.0 postcss: 8.5.3 postcss-discard-duplicates: 5.1.0(postcss@8.5.3) - postcss-load-config: 4.0.2(postcss@8.5.3)(ts-node@10.9.2(@types/node@22.14.1)(typescript@5.7.3)) + postcss-load-config: 4.0.2(postcss@8.5.3)(ts-node@10.9.2(@types/node@22.14.0)(typescript@5.8.3)) postcss-modules: 6.0.1(postcss@8.5.3) prettier: 2.8.8 pretty-ms: 7.0.1 @@ -8947,13 +8817,13 @@ snapshots: set-cookie-parser: 2.7.1 tar-fs: 2.1.2 tsconfig-paths: 4.2.0 - valibot: 0.41.0(typescript@5.7.3) - vite-node: 1.6.1(@types/node@22.14.1) + valibot: 0.41.0(typescript@5.8.3) + vite-node: 3.0.0-beta.2(@types/node@22.14.0) ws: 7.5.10 optionalDependencies: - '@remix-run/serve': 2.16.4(typescript@5.7.3) - typescript: 5.7.3 - vite: 5.4.18(@types/node@22.14.1) + '@remix-run/serve': 2.16.5(typescript@5.8.3) + typescript: 5.8.3 + vite: 5.4.18(@types/node@22.14.0) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -8970,16 +8840,16 @@ snapshots: - ts-node - utf-8-validate - '@remix-run/express@2.16.4(express@4.21.2)(typescript@5.7.3)': + '@remix-run/express@2.16.5(express@4.21.2)(typescript@5.8.3)': dependencies: - '@remix-run/node': 2.16.4(typescript@5.7.3) + '@remix-run/node': 2.16.5(typescript@5.8.3) express: 4.21.2 optionalDependencies: - typescript: 5.7.3 + typescript: 5.8.3 - '@remix-run/node@2.15.3(typescript@5.7.3)': + '@remix-run/node@2.16.5(typescript@5.8.3)': dependencies: - '@remix-run/server-runtime': 2.15.3(typescript@5.7.3) + '@remix-run/server-runtime': 2.16.5(typescript@5.8.3) '@remix-run/web-fetch': 4.4.2 '@web3-storage/multipart-parser': 1.0.0 cookie-signature: 1.2.2 @@ -8987,52 +8857,26 @@ snapshots: stream-slice: 0.1.2 undici: 6.21.2 optionalDependencies: - typescript: 5.7.3 + typescript: 5.8.3 - '@remix-run/node@2.16.4(typescript@5.7.3)': - dependencies: - '@remix-run/server-runtime': 2.16.4(typescript@5.7.3) - '@remix-run/web-fetch': 4.4.2 - '@web3-storage/multipart-parser': 1.0.0 - cookie-signature: 1.2.2 - source-map-support: 0.5.21 - stream-slice: 0.1.2 - undici: 6.21.2 - optionalDependencies: - typescript: 5.7.3 - - '@remix-run/node@2.16.5(typescript@5.7.3)': - dependencies: - '@remix-run/server-runtime': 2.16.5(typescript@5.7.3) - '@remix-run/web-fetch': 4.4.2 - '@web3-storage/multipart-parser': 1.0.0 - cookie-signature: 1.2.2 - source-map-support: 0.5.21 - stream-slice: 0.1.2 - undici: 6.21.2 - optionalDependencies: - typescript: 5.7.3 - - '@remix-run/react@2.16.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.7.3)': + '@remix-run/react@2.16.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3)': dependencies: '@remix-run/router': 1.23.0 - '@remix-run/server-runtime': 2.16.5(typescript@5.7.3) + '@remix-run/server-runtime': 2.16.5(typescript@5.8.3) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) react-router: 6.30.0(react@18.3.1) react-router-dom: 6.30.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) turbo-stream: 2.4.0 optionalDependencies: - typescript: 5.7.3 - - '@remix-run/router@1.22.0': {} + typescript: 5.8.3 '@remix-run/router@1.23.0': {} - '@remix-run/serve@2.16.4(typescript@5.7.3)': + '@remix-run/serve@2.16.5(typescript@5.8.3)': dependencies: - '@remix-run/express': 2.16.4(express@4.21.2)(typescript@5.7.3) - '@remix-run/node': 2.16.4(typescript@5.7.3) + '@remix-run/express': 2.16.5(express@4.21.2)(typescript@5.8.3) + '@remix-run/node': 2.16.5(typescript@5.8.3) chokidar: 3.6.0 compression: 1.8.0 express: 4.21.2 @@ -9043,19 +8887,7 @@ snapshots: - supports-color - typescript - '@remix-run/server-runtime@2.15.3(typescript@5.7.3)': - dependencies: - '@remix-run/router': 1.22.0 - '@types/cookie': 0.6.0 - '@web3-storage/multipart-parser': 1.0.0 - cookie: 0.6.0 - set-cookie-parser: 2.7.1 - source-map: 0.7.4 - turbo-stream: 2.4.0 - optionalDependencies: - typescript: 5.7.3 - - '@remix-run/server-runtime@2.16.4(typescript@5.7.3)': + '@remix-run/server-runtime@2.16.5(typescript@5.8.3)': dependencies: '@remix-run/router': 1.23.0 '@types/cookie': 0.6.0 @@ -9065,19 +8897,7 @@ snapshots: source-map: 0.7.4 turbo-stream: 2.4.0 optionalDependencies: - typescript: 5.7.3 - - '@remix-run/server-runtime@2.16.5(typescript@5.7.3)': - dependencies: - '@remix-run/router': 1.23.0 - '@types/cookie': 0.6.0 - '@web3-storage/multipart-parser': 1.0.0 - cookie: 0.7.2 - set-cookie-parser: 2.7.1 - source-map: 0.7.4 - turbo-stream: 2.4.0 - optionalDependencies: - typescript: 5.7.3 + typescript: 5.8.3 '@remix-run/web-blob@3.1.0': dependencies: @@ -9107,64 +8927,64 @@ snapshots: dependencies: web-streams-polyfill: 3.3.3 - '@rollup/rollup-android-arm-eabi@4.40.0': + '@rollup/rollup-android-arm-eabi@4.39.0': optional: true - '@rollup/rollup-android-arm64@4.40.0': + '@rollup/rollup-android-arm64@4.39.0': optional: true - '@rollup/rollup-darwin-arm64@4.40.0': + '@rollup/rollup-darwin-arm64@4.39.0': optional: true - '@rollup/rollup-darwin-x64@4.40.0': + '@rollup/rollup-darwin-x64@4.39.0': optional: true - '@rollup/rollup-freebsd-arm64@4.40.0': + '@rollup/rollup-freebsd-arm64@4.39.0': optional: true - '@rollup/rollup-freebsd-x64@4.40.0': + '@rollup/rollup-freebsd-x64@4.39.0': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.40.0': + '@rollup/rollup-linux-arm-gnueabihf@4.39.0': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.40.0': + '@rollup/rollup-linux-arm-musleabihf@4.39.0': optional: true - '@rollup/rollup-linux-arm64-gnu@4.40.0': + '@rollup/rollup-linux-arm64-gnu@4.39.0': optional: true - '@rollup/rollup-linux-arm64-musl@4.40.0': + '@rollup/rollup-linux-arm64-musl@4.39.0': optional: true - '@rollup/rollup-linux-loongarch64-gnu@4.40.0': + '@rollup/rollup-linux-loongarch64-gnu@4.39.0': optional: true - '@rollup/rollup-linux-powerpc64le-gnu@4.40.0': + '@rollup/rollup-linux-powerpc64le-gnu@4.39.0': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.40.0': + '@rollup/rollup-linux-riscv64-gnu@4.39.0': optional: true - '@rollup/rollup-linux-riscv64-musl@4.40.0': + '@rollup/rollup-linux-riscv64-musl@4.39.0': optional: true - '@rollup/rollup-linux-s390x-gnu@4.40.0': + '@rollup/rollup-linux-s390x-gnu@4.39.0': optional: true - '@rollup/rollup-linux-x64-gnu@4.40.0': + '@rollup/rollup-linux-x64-gnu@4.39.0': optional: true - '@rollup/rollup-linux-x64-musl@4.40.0': + '@rollup/rollup-linux-x64-musl@4.39.0': optional: true - '@rollup/rollup-win32-arm64-msvc@4.40.0': + '@rollup/rollup-win32-arm64-msvc@4.39.0': optional: true - '@rollup/rollup-win32-ia32-msvc@4.40.0': + '@rollup/rollup-win32-ia32-msvc@4.39.0': optional: true - '@rollup/rollup-win32-x64-msvc@4.40.0': + '@rollup/rollup-win32-x64-msvc@4.39.0': optional: true '@rtsao/scc@1.1.0': {} @@ -9197,6 +9017,11 @@ snapshots: '@tsconfig/node20@20.1.5': {} + '@tybys/wasm-util@0.9.0': + dependencies: + tslib: 2.8.1 + optional: true + '@types/acorn@4.0.6': dependencies: '@types/estree': 1.0.7 @@ -9205,7 +9030,7 @@ snapshots: dependencies: '@types/http-cache-semantics': 4.0.4 '@types/keyv': 3.1.4 - '@types/node': 22.14.1 + '@types/node': 22.14.0 '@types/responselike': 1.0.3 '@types/cookie@0.6.0': {} @@ -9216,21 +9041,19 @@ snapshots: '@types/eslint@9.6.1': dependencies: - '@types/estree': 1.0.6 + '@types/estree': 1.0.7 '@types/json-schema': 7.0.15 '@types/estree-jsx@1.0.5': dependencies: '@types/estree': 1.0.7 - '@types/estree@1.0.6': {} - '@types/estree@1.0.7': {} '@types/fs-extra@11.0.4': dependencies: '@types/jsonfile': 6.1.4 - '@types/node': 22.13.4 + '@types/node': 22.14.0 '@types/hast@2.3.10': dependencies: @@ -9241,7 +9064,7 @@ snapshots: '@types/inquirer@9.0.7': dependencies: '@types/through': 0.0.33 - rxjs: 7.8.1 + rxjs: 7.8.2 '@types/istanbul-lib-coverage@2.0.6': {} @@ -9265,11 +9088,11 @@ snapshots: '@types/jsonfile@6.1.4': dependencies: - '@types/node': 22.13.4 + '@types/node': 22.14.0 '@types/keyv@3.1.4': dependencies: - '@types/node': 22.14.1 + '@types/node': 22.14.0 '@types/mdast@3.0.15': dependencies: @@ -9283,54 +9106,44 @@ snapshots: '@types/mute-stream@0.0.4': dependencies: - '@types/node': 22.14.1 + '@types/node': 22.14.0 '@types/node@12.20.55': {} '@types/node@16.9.1': {} - '@types/node@20.17.28': - dependencies: - undici-types: 6.19.8 - '@types/node@20.17.30': dependencies: undici-types: 6.19.8 - '@types/node@22.13.4': - dependencies: - undici-types: 6.20.0 - '@types/node@22.14.0': dependencies: undici-types: 6.21.0 - '@types/node@22.14.1': - dependencies: - undici-types: 6.21.0 - '@types/normalize-package-data@2.4.4': {} '@types/parse-json@4.0.2': {} + '@types/parse-path@7.0.3': {} + '@types/prop-types@15.7.14': {} - '@types/react-dom@18.3.5(@types/react@18.3.18)': + '@types/react-dom@18.3.6(@types/react@18.3.20)': dependencies: - '@types/react': 18.3.18 + '@types/react': 18.3.20 - '@types/react-transition-group@4.4.12(@types/react@18.3.18)': + '@types/react-transition-group@4.4.12(@types/react@18.3.20)': dependencies: - '@types/react': 18.3.18 + '@types/react': 18.3.20 - '@types/react@18.3.18': + '@types/react@18.3.20': dependencies: '@types/prop-types': 15.7.14 csstype: 3.1.3 '@types/responselike@1.0.3': dependencies: - '@types/node': 22.14.1 + '@types/node': 22.14.0 '@types/sinonjs__fake-timers@8.1.5': {} @@ -9352,16 +9165,11 @@ snapshots: '@types/ws@8.18.1': dependencies: - '@types/node': 22.14.1 - - '@types/ws@8.5.14': - dependencies: - '@types/node': 22.14.1 - optional: true + '@types/node': 22.14.0 '@types/xml2js@0.4.14': dependencies: - '@types/node': 22.13.4 + '@types/node': 22.14.0 '@types/yargs-parser@21.0.3': {} @@ -9371,89 +9179,136 @@ snapshots: '@types/yauzl@2.10.3': dependencies: - '@types/node': 22.14.1 + '@types/node': 22.14.0 optional: true - '@typescript-eslint/eslint-plugin@8.29.1(@typescript-eslint/parser@8.29.1(eslint@9.23.0(jiti@1.21.7))(typescript@5.7.3))(eslint@9.23.0(jiti@1.21.7))(typescript@5.7.3)': + '@typescript-eslint/eslint-plugin@8.30.1(@typescript-eslint/parser@8.30.1(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3))(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3)': dependencies: '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 8.29.1(eslint@9.23.0(jiti@1.21.7))(typescript@5.7.3) - '@typescript-eslint/scope-manager': 8.29.1 - '@typescript-eslint/type-utils': 8.29.1(eslint@9.23.0(jiti@1.21.7))(typescript@5.7.3) - '@typescript-eslint/utils': 8.29.1(eslint@9.23.0(jiti@1.21.7))(typescript@5.7.3) - '@typescript-eslint/visitor-keys': 8.29.1 - eslint: 9.23.0(jiti@1.21.7) + '@typescript-eslint/parser': 8.30.1(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) + '@typescript-eslint/scope-manager': 8.30.1 + '@typescript-eslint/type-utils': 8.30.1(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) + '@typescript-eslint/utils': 8.30.1(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) + '@typescript-eslint/visitor-keys': 8.30.1 + eslint: 9.24.0(jiti@1.21.7) graphemer: 1.4.0 ignore: 5.3.2 natural-compare: 1.4.0 - ts-api-utils: 2.1.0(typescript@5.7.3) - typescript: 5.7.3 + ts-api-utils: 2.1.0(typescript@5.8.3) + typescript: 5.8.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.29.1(eslint@9.23.0(jiti@1.21.7))(typescript@5.7.3)': + '@typescript-eslint/parser@8.30.1(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3)': dependencies: - '@typescript-eslint/scope-manager': 8.29.1 - '@typescript-eslint/types': 8.29.1 - '@typescript-eslint/typescript-estree': 8.29.1(typescript@5.7.3) - '@typescript-eslint/visitor-keys': 8.29.1 + '@typescript-eslint/scope-manager': 8.30.1 + '@typescript-eslint/types': 8.30.1 + '@typescript-eslint/typescript-estree': 8.30.1(typescript@5.8.3) + '@typescript-eslint/visitor-keys': 8.30.1 debug: 4.4.0(supports-color@8.1.1) - eslint: 9.23.0(jiti@1.21.7) - typescript: 5.7.3 + eslint: 9.24.0(jiti@1.21.7) + typescript: 5.8.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.29.1': + '@typescript-eslint/scope-manager@8.30.1': dependencies: - '@typescript-eslint/types': 8.29.1 - '@typescript-eslint/visitor-keys': 8.29.1 + '@typescript-eslint/types': 8.30.1 + '@typescript-eslint/visitor-keys': 8.30.1 - '@typescript-eslint/type-utils@8.29.1(eslint@9.23.0(jiti@1.21.7))(typescript@5.7.3)': + '@typescript-eslint/type-utils@8.30.1(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3)': dependencies: - '@typescript-eslint/typescript-estree': 8.29.1(typescript@5.7.3) - '@typescript-eslint/utils': 8.29.1(eslint@9.23.0(jiti@1.21.7))(typescript@5.7.3) + '@typescript-eslint/typescript-estree': 8.30.1(typescript@5.8.3) + '@typescript-eslint/utils': 8.30.1(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) debug: 4.4.0(supports-color@8.1.1) - eslint: 9.23.0(jiti@1.21.7) - ts-api-utils: 2.1.0(typescript@5.7.3) - typescript: 5.7.3 + eslint: 9.24.0(jiti@1.21.7) + ts-api-utils: 2.1.0(typescript@5.8.3) + typescript: 5.8.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.29.1': {} + '@typescript-eslint/types@8.30.1': {} - '@typescript-eslint/typescript-estree@8.29.1(typescript@5.7.3)': + '@typescript-eslint/typescript-estree@8.30.1(typescript@5.8.3)': dependencies: - '@typescript-eslint/types': 8.29.1 - '@typescript-eslint/visitor-keys': 8.29.1 + '@typescript-eslint/types': 8.30.1 + '@typescript-eslint/visitor-keys': 8.30.1 debug: 4.4.0(supports-color@8.1.1) fast-glob: 3.3.3 is-glob: 4.0.3 minimatch: 9.0.5 semver: 7.7.1 - ts-api-utils: 2.1.0(typescript@5.7.3) - typescript: 5.7.3 + ts-api-utils: 2.1.0(typescript@5.8.3) + typescript: 5.8.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.29.1(eslint@9.23.0(jiti@1.21.7))(typescript@5.7.3)': + '@typescript-eslint/utils@8.30.1(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3)': dependencies: - '@eslint-community/eslint-utils': 4.6.0(eslint@9.23.0(jiti@1.21.7)) - '@typescript-eslint/scope-manager': 8.29.1 - '@typescript-eslint/types': 8.29.1 - '@typescript-eslint/typescript-estree': 8.29.1(typescript@5.7.3) - eslint: 9.23.0(jiti@1.21.7) - typescript: 5.7.3 + '@eslint-community/eslint-utils': 4.5.1(eslint@9.24.0(jiti@1.21.7)) + '@typescript-eslint/scope-manager': 8.30.1 + '@typescript-eslint/types': 8.30.1 + '@typescript-eslint/typescript-estree': 8.30.1(typescript@5.8.3) + eslint: 9.24.0(jiti@1.21.7) + typescript: 5.8.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.29.1': + '@typescript-eslint/visitor-keys@8.30.1': dependencies: - '@typescript-eslint/types': 8.29.1 + '@typescript-eslint/types': 8.30.1 eslint-visitor-keys: 4.2.0 + '@unrs/resolver-binding-darwin-arm64@1.3.3': + optional: true + + '@unrs/resolver-binding-darwin-x64@1.3.3': + optional: true + + '@unrs/resolver-binding-freebsd-x64@1.3.3': + optional: true + + '@unrs/resolver-binding-linux-arm-gnueabihf@1.3.3': + optional: true + + '@unrs/resolver-binding-linux-arm-musleabihf@1.3.3': + optional: true + + '@unrs/resolver-binding-linux-arm64-gnu@1.3.3': + optional: true + + '@unrs/resolver-binding-linux-arm64-musl@1.3.3': + optional: true + + '@unrs/resolver-binding-linux-ppc64-gnu@1.3.3': + optional: true + + '@unrs/resolver-binding-linux-s390x-gnu@1.3.3': + optional: true + + '@unrs/resolver-binding-linux-x64-gnu@1.3.3': + optional: true + + '@unrs/resolver-binding-linux-x64-musl@1.3.3': + optional: true + + '@unrs/resolver-binding-wasm32-wasi@1.3.3': + dependencies: + '@napi-rs/wasm-runtime': 0.2.8 + optional: true + + '@unrs/resolver-binding-win32-arm64-msvc@1.3.3': + optional: true + + '@unrs/resolver-binding-win32-ia32-msvc@1.3.3': + optional: true + + '@unrs/resolver-binding-win32-x64-msvc@1.3.3': + optional: true + '@vanilla-extract/babel-plugin-debug-ids@1.2.0': dependencies: - '@babel/core': 7.26.9 + '@babel/core': 7.26.10 transitivePeerDependencies: - supports-color @@ -9474,10 +9329,10 @@ snapshots: transitivePeerDependencies: - babel-plugin-macros - '@vanilla-extract/integration@6.5.0(@types/node@22.14.1)(babel-plugin-macros@3.1.0)': + '@vanilla-extract/integration@6.5.0(@types/node@22.14.0)(babel-plugin-macros@3.1.0)': dependencies: - '@babel/core': 7.26.9 - '@babel/plugin-syntax-typescript': 7.25.9(@babel/core@7.26.9) + '@babel/core': 7.26.10 + '@babel/plugin-syntax-typescript': 7.25.9(@babel/core@7.26.10) '@vanilla-extract/babel-plugin-debug-ids': 1.2.0 '@vanilla-extract/css': 1.17.1(babel-plugin-macros@3.1.0) esbuild: 0.17.6 @@ -9487,8 +9342,8 @@ snapshots: lodash: 4.17.21 mlly: 1.7.4 outdent: 0.8.0 - vite: 5.4.18(@types/node@22.14.1) - vite-node: 1.6.1(@types/node@22.14.1) + vite: 5.4.18(@types/node@22.14.0) + vite-node: 1.6.1(@types/node@22.14.0) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -9503,7 +9358,7 @@ snapshots: '@vanilla-extract/private@1.0.6': {} - '@vitest/coverage-v8@3.0.8(vitest@3.0.8)': + '@vitest/coverage-v8@3.1.1(vitest@3.1.1)': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 1.0.2 @@ -9514,39 +9369,39 @@ snapshots: istanbul-reports: 3.1.7 magic-string: 0.30.17 magicast: 0.3.5 - std-env: 3.8.0 + std-env: 3.9.0 test-exclude: 7.0.1 tinyrainbow: 2.0.0 - vitest: 3.0.8(@types/debug@4.1.12)(@types/node@22.14.0)(@vitest/ui@3.0.8)(jsdom@25.0.1) + vitest: 3.1.1(@types/debug@4.1.12)(@types/node@22.14.0)(@vitest/ui@3.1.1)(jsdom@26.1.0) transitivePeerDependencies: - supports-color - '@vitest/expect@3.0.8': + '@vitest/expect@3.1.1': dependencies: - '@vitest/spy': 3.0.8 - '@vitest/utils': 3.0.8 + '@vitest/spy': 3.1.1 + '@vitest/utils': 3.1.1 chai: 5.2.0 tinyrainbow: 2.0.0 - '@vitest/mocker@3.0.8(vite@5.4.18(@types/node@22.14.0))': + '@vitest/mocker@3.1.1(vite@5.4.17(@types/node@22.14.0))': dependencies: - '@vitest/spy': 3.0.8 + '@vitest/spy': 3.1.1 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: - vite: 5.4.18(@types/node@22.14.0) + vite: 5.4.17(@types/node@22.14.0) '@vitest/pretty-format@2.1.9': dependencies: tinyrainbow: 1.2.0 - '@vitest/pretty-format@3.0.8': + '@vitest/pretty-format@3.1.1': dependencies: tinyrainbow: 2.0.0 - '@vitest/runner@3.0.8': + '@vitest/runner@3.1.1': dependencies: - '@vitest/utils': 3.0.8 + '@vitest/utils': 3.1.1 pathe: 2.0.3 '@vitest/snapshot@2.1.9': @@ -9555,44 +9410,44 @@ snapshots: magic-string: 0.30.17 pathe: 1.1.2 - '@vitest/snapshot@3.0.8': + '@vitest/snapshot@3.1.1': dependencies: - '@vitest/pretty-format': 3.0.8 + '@vitest/pretty-format': 3.1.1 magic-string: 0.30.17 pathe: 2.0.3 - '@vitest/spy@3.0.8': + '@vitest/spy@3.1.1': dependencies: tinyspy: 3.0.2 - '@vitest/ui@3.0.8(vitest@3.0.8)': + '@vitest/ui@3.1.1(vitest@3.1.1)': dependencies: - '@vitest/utils': 3.0.8 + '@vitest/utils': 3.1.1 fflate: 0.8.2 flatted: 3.3.3 pathe: 2.0.3 sirv: 3.0.1 tinyglobby: 0.2.12 tinyrainbow: 2.0.0 - vitest: 3.0.8(@types/debug@4.1.12)(@types/node@22.14.0)(@vitest/ui@3.0.8)(jsdom@25.0.1) + vitest: 3.1.1(@types/debug@4.1.12)(@types/node@22.14.0)(@vitest/ui@3.1.1)(jsdom@26.1.0) - '@vitest/utils@3.0.8': + '@vitest/utils@3.1.1': dependencies: - '@vitest/pretty-format': 3.0.8 + '@vitest/pretty-format': 3.1.1 loupe: 3.1.3 tinyrainbow: 2.0.0 - '@wdio/appium-service@9.12.4': + '@wdio/appium-service@9.12.6': dependencies: - '@wdio/config': 9.12.3 + '@wdio/config': 9.12.6 '@wdio/logger': 9.4.4 - '@wdio/types': 9.12.3 - '@wdio/utils': 9.12.3 + '@wdio/types': 9.12.6 + '@wdio/utils': 9.12.6 change-case: 5.4.4 get-port: 7.1.0 import-meta-resolve: 4.1.0 tree-kill: 1.2.2 - webdriverio: 9.12.4 + webdriverio: 9.12.6 transitivePeerDependencies: - bare-buffer - bufferutil @@ -9600,20 +9455,20 @@ snapshots: - supports-color - utf-8-validate - '@wdio/cli@9.12.5': + '@wdio/cli@9.12.6': dependencies: '@types/node': 20.17.30 '@vitest/snapshot': 2.1.9 - '@wdio/config': 9.12.5 - '@wdio/globals': 9.12.5(@wdio/logger@9.4.4) + '@wdio/config': 9.12.6 + '@wdio/globals': 9.12.6(@wdio/logger@9.4.4) '@wdio/logger': 9.4.4 '@wdio/protocols': 9.12.5 - '@wdio/types': 9.12.3 - '@wdio/utils': 9.12.5 + '@wdio/types': 9.12.6 + '@wdio/utils': 9.12.6 async-exit-hook: 2.0.1 chalk: 5.4.1 chokidar: 4.0.3 - dotenv: 16.5.0 + dotenv: 16.4.7 ejs: 3.1.10 execa: 9.5.2 import-meta-resolve: 4.1.0 @@ -9624,7 +9479,7 @@ snapshots: read-pkg-up: 10.1.0 recursive-readdir: 2.2.3 tsx: 4.19.3 - webdriverio: 9.12.5 + webdriverio: 9.12.6 yargs: 17.7.2 transitivePeerDependencies: - bare-buffer @@ -9633,11 +9488,11 @@ snapshots: - supports-color - utf-8-validate - '@wdio/config@9.12.3': + '@wdio/config@9.12.6': dependencies: '@wdio/logger': 9.4.4 - '@wdio/types': 9.12.3 - '@wdio/utils': 9.12.3 + '@wdio/types': 9.12.6 + '@wdio/utils': 9.12.6 deepmerge-ts: 7.1.5 glob: 10.4.5 import-meta-resolve: 4.1.0 @@ -9645,41 +9500,16 @@ snapshots: - bare-buffer - supports-color - '@wdio/config@9.12.5': - dependencies: - '@wdio/logger': 9.4.4 - '@wdio/types': 9.12.3 - '@wdio/utils': 9.12.5 - deepmerge-ts: 7.1.5 - glob: 10.4.5 - import-meta-resolve: 4.1.0 - transitivePeerDependencies: - - bare-buffer - - supports-color - - '@wdio/config@9.9.0': - dependencies: - '@wdio/logger': 9.4.4 - '@wdio/types': 9.9.0 - '@wdio/utils': 9.9.0 - deepmerge-ts: 7.1.4 - glob: 10.4.5 - import-meta-resolve: 4.1.0 - transitivePeerDependencies: - - bare-buffer - - supports-color - optional: true - - '@wdio/dot-reporter@9.12.3': + '@wdio/dot-reporter@9.12.6': dependencies: - '@wdio/reporter': 9.12.3 - '@wdio/types': 9.12.3 + '@wdio/reporter': 9.12.6 + '@wdio/types': 9.12.6 chalk: 5.4.1 - '@wdio/globals@9.12.5(@wdio/logger@9.4.4)': + '@wdio/globals@9.12.6(@wdio/logger@9.4.4)': optionalDependencies: - expect-webdriverio: 5.1.0(@wdio/globals@9.12.5(@wdio/logger@9.4.4))(@wdio/logger@9.4.4)(webdriverio@9.12.5) - webdriverio: 9.12.5 + expect-webdriverio: 5.1.0(@wdio/globals@9.12.6(@wdio/logger@9.4.4))(@wdio/logger@9.4.4)(webdriverio@9.12.6) + webdriverio: 9.12.6 transitivePeerDependencies: - '@wdio/logger' - bare-buffer @@ -9688,25 +9518,13 @@ snapshots: - supports-color - utf-8-validate - '@wdio/globals@9.9.1(@wdio/logger@9.4.4)': - optionalDependencies: - expect-webdriverio: 5.1.0(@wdio/globals@9.9.1(@wdio/logger@9.4.4))(@wdio/logger@9.4.4)(webdriverio@9.9.1) - webdriverio: 9.9.1 - transitivePeerDependencies: - - '@wdio/logger' - - bare-buffer - - bufferutil - - puppeteer-core - - supports-color - - utf-8-validate - - '@wdio/local-runner@9.12.5': + '@wdio/local-runner@9.12.6': dependencies: '@types/node': 20.17.30 '@wdio/logger': 9.4.4 '@wdio/repl': 9.4.4 - '@wdio/runner': 9.12.5 - '@wdio/types': 9.12.3 + '@wdio/runner': 9.12.6 + '@wdio/types': 9.12.6 async-exit-hook: 2.0.1 split2: 4.2.0 stream-buffers: 3.0.3 @@ -9731,50 +9549,45 @@ snapshots: loglevel-plugin-prefix: 0.8.4 strip-ansi: 7.1.0 - '@wdio/mocha-framework@9.12.5': + '@wdio/mocha-framework@9.12.6': dependencies: '@types/mocha': 10.0.10 '@types/node': 20.17.30 '@wdio/logger': 9.4.4 - '@wdio/types': 9.12.3 - '@wdio/utils': 9.12.5 + '@wdio/types': 9.12.6 + '@wdio/utils': 9.12.6 mocha: 10.8.2 transitivePeerDependencies: - bare-buffer - supports-color - '@wdio/protocols@9.12.3': {} - '@wdio/protocols@9.12.5': {} - '@wdio/protocols@9.7.0': - optional: true - '@wdio/repl@9.4.4': dependencies: '@types/node': 20.17.30 - '@wdio/reporter@9.12.3': + '@wdio/reporter@9.12.6': dependencies: '@types/node': 20.17.30 '@wdio/logger': 9.4.4 - '@wdio/types': 9.12.3 + '@wdio/types': 9.12.6 diff: 7.0.0 object-inspect: 1.13.4 - '@wdio/runner@9.12.5': + '@wdio/runner@9.12.6': dependencies: '@types/node': 20.17.30 - '@wdio/config': 9.12.5 - '@wdio/dot-reporter': 9.12.3 - '@wdio/globals': 9.12.5(@wdio/logger@9.4.4) + '@wdio/config': 9.12.6 + '@wdio/dot-reporter': 9.12.6 + '@wdio/globals': 9.12.6(@wdio/logger@9.4.4) '@wdio/logger': 9.4.4 - '@wdio/types': 9.12.3 - '@wdio/utils': 9.12.5 + '@wdio/types': 9.12.6 + '@wdio/utils': 9.12.6 deepmerge-ts: 7.1.5 - expect-webdriverio: 5.1.0(@wdio/globals@9.12.5(@wdio/logger@9.4.4))(@wdio/logger@9.4.4)(webdriverio@9.12.5) - webdriver: 9.12.5 - webdriverio: 9.12.5 + expect-webdriverio: 5.1.0(@wdio/globals@9.12.6(@wdio/logger@9.4.4))(@wdio/logger@9.4.4)(webdriverio@9.12.6) + webdriver: 9.12.6 + webdriverio: 9.12.6 transitivePeerDependencies: - bare-buffer - bufferutil @@ -9782,13 +9595,13 @@ snapshots: - supports-color - utf-8-validate - '@wdio/sauce-service@9.12.5': + '@wdio/sauce-service@9.12.6': dependencies: '@wdio/logger': 9.4.4 - '@wdio/types': 9.12.3 - '@wdio/utils': 9.12.5 + '@wdio/types': 9.12.6 + '@wdio/utils': 9.12.6 saucelabs: 9.0.2 - webdriverio: 9.12.5 + webdriverio: 9.12.6 transitivePeerDependencies: - bare-buffer - bufferutil @@ -9796,13 +9609,13 @@ snapshots: - supports-color - utf-8-validate - '@wdio/shared-store-service@9.12.5': + '@wdio/shared-store-service@9.12.6': dependencies: '@polka/parse': 1.0.0-next.0 '@wdio/logger': 9.4.4 - '@wdio/types': 9.12.3 + '@wdio/types': 9.12.6 polka: 0.5.2 - webdriverio: 9.12.5 + webdriverio: 9.12.6 transitivePeerDependencies: - bare-buffer - bufferutil @@ -9810,51 +9623,23 @@ snapshots: - supports-color - utf-8-validate - '@wdio/spec-reporter@9.12.3': + '@wdio/spec-reporter@9.12.6': dependencies: - '@wdio/reporter': 9.12.3 - '@wdio/types': 9.12.3 + '@wdio/reporter': 9.12.6 + '@wdio/types': 9.12.6 chalk: 5.4.1 easy-table: 1.2.0 pretty-ms: 9.2.0 - '@wdio/types@9.12.2': - dependencies: - '@types/node': 20.17.28 - - '@wdio/types@9.12.3': + '@wdio/types@9.12.6': dependencies: '@types/node': 20.17.30 - '@wdio/types@9.9.0': - dependencies: - '@types/node': 20.17.30 - optional: true - - '@wdio/utils@9.12.3': + '@wdio/utils@9.12.6': dependencies: '@puppeteer/browsers': 2.9.0 '@wdio/logger': 9.4.4 - '@wdio/types': 9.12.3 - decamelize: 6.0.0 - deepmerge-ts: 7.1.5 - edgedriver: 6.1.1 - geckodriver: 5.0.0 - get-port: 7.1.0 - import-meta-resolve: 4.1.0 - locate-app: 2.5.0 - safaridriver: 1.0.0 - split2: 4.2.0 - wait-port: 1.1.0 - transitivePeerDependencies: - - bare-buffer - - supports-color - - '@wdio/utils@9.12.5': - dependencies: - '@puppeteer/browsers': 2.10.0 - '@wdio/logger': 9.4.4 - '@wdio/types': 9.12.3 + '@wdio/types': 9.12.6 decamelize: 6.0.0 deepmerge-ts: 7.1.5 edgedriver: 6.1.1 @@ -9869,26 +9654,6 @@ snapshots: - bare-buffer - supports-color - '@wdio/utils@9.9.0': - dependencies: - '@puppeteer/browsers': 2.7.1 - '@wdio/logger': 9.4.4 - '@wdio/types': 9.9.0 - decamelize: 6.0.0 - deepmerge-ts: 7.1.4 - edgedriver: 6.1.1 - geckodriver: 5.0.0 - get-port: 7.1.0 - import-meta-resolve: 4.1.0 - locate-app: 2.5.0 - safaridriver: 1.0.0 - split2: 4.2.0 - wait-port: 1.1.0 - transitivePeerDependencies: - - bare-buffer - - supports-color - optional: true - '@web3-storage/multipart-parser@1.0.0': {} '@zip.js/zip.js@2.7.60': {} @@ -9911,9 +9676,7 @@ snapshots: acorn-walk@8.3.4: dependencies: - acorn: 8.14.0 - - acorn@8.14.0: {} + acorn: 8.14.1 acorn@8.14.1: {} @@ -10029,9 +9792,10 @@ snapshots: es-object-atoms: 1.1.1 es-shim-unscopables: 1.1.0 - array.prototype.findlastindex@1.2.5: + array.prototype.findlastindex@1.2.6: dependencies: call-bind: 1.0.8 + call-bound: 1.0.4 define-properties: 1.2.1 es-abstract: 1.23.9 es-errors: 1.3.0 @@ -10100,7 +9864,7 @@ snapshots: autoprefixer@10.4.21(postcss@8.5.3): dependencies: browserslist: 4.24.4 - caniuse-lite: 1.0.30001707 + caniuse-lite: 1.0.30001711 fraction.js: 4.3.7 normalize-range: 0.1.2 picocolors: 1.1.1 @@ -10113,9 +9877,9 @@ snapshots: await-to-js@3.0.0: {} - axe-core@4.10.2: {} + axe-core@4.10.3: {} - axios@1.7.9: + axios@1.8.4: dependencies: follow-redirects: 1.15.9 form-data: 4.0.2 @@ -10170,7 +9934,7 @@ snapshots: basic-ftp@5.0.5: {} - before-after-hook@2.2.3: {} + before-after-hook@3.0.2: {} better-path-resolve@1.0.0: dependencies: @@ -10244,8 +10008,8 @@ snapshots: browserslist@4.24.4: dependencies: - caniuse-lite: 1.0.30001707 - electron-to-chromium: 1.5.128 + caniuse-lite: 1.0.30001711 + electron-to-chromium: 1.5.132 node-releases: 2.0.19 update-browserslist-db: 1.1.3(browserslist@4.24.4) @@ -10323,11 +10087,6 @@ snapshots: get-intrinsic: 1.3.0 set-function-length: 1.2.2 - call-bound@1.0.3: - dependencies: - call-bind-apply-helpers: 1.0.2 - get-intrinsic: 1.3.0 - call-bound@1.0.4: dependencies: call-bind-apply-helpers: 1.0.2 @@ -10346,7 +10105,7 @@ snapshots: camelcase@8.0.0: {} - caniuse-lite@1.0.30001707: {} + caniuse-lite@1.0.30001711: {} capital-case@1.0.4: dependencies: @@ -10445,7 +10204,7 @@ snapshots: ci-info@3.9.0: {} - ci-info@4.1.0: {} + ci-info@4.2.0: {} clean-regexp@1.0.0: dependencies: @@ -10569,6 +10328,8 @@ snapshots: confbox@0.1.8: {} + confbox@0.2.2: {} + config-chain@1.1.13: dependencies: ini: 1.3.8 @@ -10603,13 +10364,11 @@ snapshots: cookie-signature@1.2.2: {} - cookie@0.6.0: {} - cookie@0.7.1: {} cookie@0.7.2: {} - core-js-compat@3.40.0: + core-js-compat@3.41.0: dependencies: browserslist: 4.24.4 @@ -10623,14 +10382,14 @@ snapshots: path-type: 4.0.0 yaml: 1.10.2 - cosmiconfig@9.0.0(typescript@5.7.3): + cosmiconfig@9.0.0(typescript@5.8.3): dependencies: env-paths: 2.2.1 import-fresh: 3.3.1 js-yaml: 4.1.0 parse-json: 5.2.0 optionalDependencies: - typescript: 5.7.3 + typescript: 5.8.3 crc-32@1.2.2: {} @@ -10667,9 +10426,9 @@ snapshots: cssesc@3.0.0: {} - cssstyle@4.2.1: + cssstyle@4.3.0: dependencies: - '@asamuzakjp/css-color': 2.8.3 + '@asamuzakjp/css-color': 3.1.1 rrweb-cssom: 0.8.0 csstype@3.1.3: {} @@ -10685,7 +10444,7 @@ snapshots: data-urls@5.0.0: dependencies: whatwg-mimetype: 4.0.0 - whatwg-url: 14.1.1 + whatwg-url: 14.2.0 data-view-buffer@1.0.2: dependencies: @@ -10725,7 +10484,7 @@ snapshots: decimal.js@10.5.0: {} - decode-named-character-reference@1.0.2: + decode-named-character-reference@1.1.0: dependencies: character-entities: 2.0.2 @@ -10747,9 +10506,6 @@ snapshots: deep-object-diff@1.1.9: {} - deepmerge-ts@7.1.4: - optional: true - deepmerge-ts@7.1.5: {} deepmerge@4.3.1: {} @@ -10791,8 +10547,6 @@ snapshots: depd@2.0.0: {} - deprecation@2.3.1: {} - dequal@2.0.3: {} destroy@1.2.0: {} @@ -10855,8 +10609,6 @@ snapshots: dotenv@16.4.7: {} - dotenv@16.5.0: {} - dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 @@ -10903,7 +10655,7 @@ snapshots: dependencies: jake: 10.9.2 - electron-to-chromium@1.5.128: {} + electron-to-chromium@1.5.132: {} emoji-regex@10.4.0: {} @@ -10926,11 +10678,6 @@ snapshots: dependencies: once: 1.4.0 - enhanced-resolve@5.18.1: - dependencies: - graceful-fs: 4.2.11 - tapable: 2.2.1 - enquirer@2.4.1: dependencies: ansi-colors: 4.1.3 @@ -11046,11 +10793,11 @@ snapshots: is-date-object: 1.1.0 is-symbol: 1.1.1 - esbuild-plugins-node-modules-polyfill@1.6.8(esbuild@0.17.6): + esbuild-plugins-node-modules-polyfill@1.7.0(esbuild@0.17.6): dependencies: - '@jspm/core': 2.0.1 + '@jspm/core': 2.1.0 esbuild: 0.17.6 - local-pkg: 0.5.1 + local-pkg: 1.1.1 resolve.exports: 2.0.3 esbuild@0.17.6: @@ -11160,44 +10907,44 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.8.0(eslint-plugin-import@2.31.0)(eslint@9.23.0(jiti@1.21.7)): + eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0)(eslint@9.24.0(jiti@1.21.7)): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.0(supports-color@8.1.1) - enhanced-resolve: 5.18.1 - eslint: 9.23.0(jiti@1.21.7) + eslint: 9.24.0(jiti@1.21.7) get-tsconfig: 4.10.0 - is-bun-module: 1.3.0 - stable-hash: 0.0.4 - tinyglobby: 0.2.10 + is-bun-module: 2.0.0 + stable-hash: 0.0.5 + tinyglobby: 0.2.12 + unrs-resolver: 1.3.3 optionalDependencies: - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.29.1(eslint@9.23.0(jiti@1.21.7))(typescript@5.7.3))(eslint-import-resolver-typescript@3.8.0)(eslint@9.23.0(jiti@1.21.7)) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.30.1(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.0)(eslint@9.24.0(jiti@1.21.7)) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@8.29.1(eslint@9.23.0(jiti@1.21.7))(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.8.0)(eslint@9.23.0(jiti@1.21.7)): + eslint-module-utils@2.12.0(@typescript-eslint/parser@8.30.1(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.0)(eslint@9.24.0(jiti@1.21.7)): dependencies: debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 8.29.1(eslint@9.23.0(jiti@1.21.7))(typescript@5.7.3) - eslint: 9.23.0(jiti@1.21.7) + '@typescript-eslint/parser': 8.30.1(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) + eslint: 9.24.0(jiti@1.21.7) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.8.0(eslint-plugin-import@2.31.0)(eslint@9.23.0(jiti@1.21.7)) + eslint-import-resolver-typescript: 3.10.0(eslint-plugin-import@2.31.0)(eslint@9.24.0(jiti@1.21.7)) transitivePeerDependencies: - supports-color - eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.29.1(eslint@9.23.0(jiti@1.21.7))(typescript@5.7.3))(eslint-import-resolver-typescript@3.8.0)(eslint@9.23.0(jiti@1.21.7)): + eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.30.1(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.0)(eslint@9.24.0(jiti@1.21.7)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.8 - array.prototype.findlastindex: 1.2.5 + array.prototype.findlastindex: 1.2.6 array.prototype.flat: 1.3.3 array.prototype.flatmap: 1.3.3 debug: 3.2.7 doctrine: 2.1.0 - eslint: 9.23.0(jiti@1.21.7) + eslint: 9.24.0(jiti@1.21.7) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.29.1(eslint@9.23.0(jiti@1.21.7))(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.8.0)(eslint@9.23.0(jiti@1.21.7)) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.30.1(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.0)(eslint@9.24.0(jiti@1.21.7)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -11209,23 +10956,23 @@ snapshots: string.prototype.trimend: 1.0.9 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 8.29.1(eslint@9.23.0(jiti@1.21.7))(typescript@5.7.3) + '@typescript-eslint/parser': 8.30.1(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack - supports-color - eslint-plugin-jsx-a11y@6.10.2(eslint@9.23.0(jiti@1.21.7)): + eslint-plugin-jsx-a11y@6.10.2(eslint@9.24.0(jiti@1.21.7)): dependencies: aria-query: 5.3.2 array-includes: 3.1.8 array.prototype.flatmap: 1.3.3 ast-types-flow: 0.0.8 - axe-core: 4.10.2 + axe-core: 4.10.3 axobject-query: 4.1.0 damerau-levenshtein: 1.0.8 emoji-regex: 9.2.2 - eslint: 9.23.0(jiti@1.21.7) + eslint: 9.24.0(jiti@1.21.7) hasown: 2.0.2 jsx-ast-utils: 3.3.5 language-tags: 1.0.9 @@ -11234,11 +10981,11 @@ snapshots: safe-regex-test: 1.1.0 string.prototype.includes: 2.0.1 - eslint-plugin-react-hooks@5.2.0(eslint@9.23.0(jiti@1.21.7)): + eslint-plugin-react-hooks@5.2.0(eslint@9.24.0(jiti@1.21.7)): dependencies: - eslint: 9.23.0(jiti@1.21.7) + eslint: 9.24.0(jiti@1.21.7) - eslint-plugin-react@7.37.5(eslint@9.23.0(jiti@1.21.7)): + eslint-plugin-react@7.37.5(eslint@9.24.0(jiti@1.21.7)): dependencies: array-includes: 3.1.8 array.prototype.findlast: 1.2.5 @@ -11246,7 +10993,7 @@ snapshots: array.prototype.tosorted: 1.1.4 doctrine: 2.1.0 es-iterator-helpers: 1.2.1 - eslint: 9.23.0(jiti@1.21.7) + eslint: 9.24.0(jiti@1.21.7) estraverse: 5.3.0 hasown: 2.0.2 jsx-ast-utils: 3.3.5 @@ -11260,14 +11007,14 @@ snapshots: string.prototype.matchall: 4.0.12 string.prototype.repeat: 1.0.0 - eslint-plugin-unicorn@56.0.1(eslint@9.23.0(jiti@1.21.7)): + eslint-plugin-unicorn@56.0.1(eslint@9.24.0(jiti@1.21.7)): dependencies: '@babel/helper-validator-identifier': 7.25.9 - '@eslint-community/eslint-utils': 4.4.1(eslint@9.23.0(jiti@1.21.7)) - ci-info: 4.1.0 + '@eslint-community/eslint-utils': 4.5.1(eslint@9.24.0(jiti@1.21.7)) + ci-info: 4.2.0 clean-regexp: 1.0.0 - core-js-compat: 3.40.0 - eslint: 9.23.0(jiti@1.21.7) + core-js-compat: 3.41.0 + eslint: 9.24.0(jiti@1.21.7) esquery: 1.6.0 globals: 15.15.0 indent-string: 4.0.0 @@ -11291,16 +11038,16 @@ snapshots: eslint-visitor-keys@4.2.0: {} - eslint@9.23.0(jiti@1.21.7): + eslint@9.24.0(jiti@1.21.7): dependencies: - '@eslint-community/eslint-utils': 4.5.1(eslint@9.23.0(jiti@1.21.7)) + '@eslint-community/eslint-utils': 4.5.1(eslint@9.24.0(jiti@1.21.7)) '@eslint-community/regexpp': 4.12.1 - '@eslint/config-array': 0.19.2 - '@eslint/config-helpers': 0.2.0 + '@eslint/config-array': 0.20.0 + '@eslint/config-helpers': 0.2.1 '@eslint/core': 0.12.0 '@eslint/eslintrc': 3.3.1 - '@eslint/js': 9.23.0 - '@eslint/plugin-kit': 0.2.7 + '@eslint/js': 9.24.0 + '@eslint/plugin-kit': 0.2.8 '@humanfs/node': 0.16.6 '@humanwhocodes/module-importer': 1.0.1 '@humanwhocodes/retry': 0.4.2 @@ -11390,7 +11137,7 @@ snapshots: eval@0.1.8: dependencies: - '@types/node': 22.14.1 + '@types/node': 22.14.0 require-like: 0.1.2 event-target-shim@5.0.1: {} @@ -11409,7 +11156,7 @@ snapshots: signal-exit: 3.0.7 strip-final-newline: 2.0.0 - execa@8.0.0: + execa@8.0.1: dependencies: cross-spawn: 7.0.6 get-stream: 8.0.1 @@ -11440,38 +11187,17 @@ snapshots: exit-hook@2.2.1: {} - expect-type@1.1.0: {} + expect-type@1.2.1: {} - expect-webdriverio@5.1.0(@wdio/globals@9.12.5(@wdio/logger@9.4.4))(@wdio/logger@9.4.4)(webdriverio@9.12.5): + expect-webdriverio@5.1.0(@wdio/globals@9.12.6(@wdio/logger@9.4.4))(@wdio/logger@9.4.4)(webdriverio@9.12.6): dependencies: '@vitest/snapshot': 2.1.9 - '@wdio/globals': 9.12.5(@wdio/logger@9.4.4) + '@wdio/globals': 9.12.6(@wdio/logger@9.4.4) '@wdio/logger': 9.4.4 expect: 29.7.0 jest-matcher-utils: 29.7.0 lodash.isequal: 4.5.0 - webdriverio: 9.12.5 - - expect-webdriverio@5.1.0(@wdio/globals@9.9.1(@wdio/logger@9.4.4))(@wdio/logger@9.4.4)(webdriverio@9.12.5): - dependencies: - '@vitest/snapshot': 2.1.9 - '@wdio/globals': 9.9.1(@wdio/logger@9.4.4) - '@wdio/logger': 9.4.4 - expect: 29.7.0 - jest-matcher-utils: 29.7.0 - lodash.isequal: 4.5.0 - webdriverio: 9.12.5 - - expect-webdriverio@5.1.0(@wdio/globals@9.9.1(@wdio/logger@9.4.4))(@wdio/logger@9.4.4)(webdriverio@9.9.1): - dependencies: - '@vitest/snapshot': 2.1.9 - '@wdio/globals': 9.9.1(@wdio/logger@9.4.4) - '@wdio/logger': 9.4.4 - expect: 29.7.0 - jest-matcher-utils: 29.7.0 - lodash.isequal: 4.5.0 - webdriverio: 9.9.1 - optional: true + webdriverio: 9.12.6 expect@29.7.0: dependencies: @@ -11517,6 +11243,8 @@ snapshots: transitivePeerDependencies: - supports-color + exsolve@1.0.4: {} + extend@3.0.2: {} extendable-error@0.1.7: {} @@ -11537,6 +11265,8 @@ snapshots: transitivePeerDependencies: - supports-color + fast-content-type-parse@2.0.1: {} + fast-deep-equal@2.0.1: {} fast-deep-equal@3.1.3: {} @@ -11559,9 +11289,9 @@ snapshots: dependencies: strnum: 1.1.2 - fastq@1.19.0: + fastq@1.19.1: dependencies: - reusify: 1.0.4 + reusify: 1.1.0 fault@2.0.1: dependencies: @@ -11660,7 +11390,7 @@ snapshots: dependencies: is-callable: 1.2.7 - foreground-child@3.3.0: + foreground-child@3.3.1: dependencies: cross-spawn: 7.0.6 signal-exit: 4.1.0 @@ -11824,14 +11554,14 @@ snapshots: image-q: 4.0.0 omggif: 1.0.10 - git-up@7.0.0: + git-up@8.1.0: dependencies: is-ssh: 1.4.1 - parse-url: 8.1.0 + parse-url: 9.2.0 - git-url-parse@14.0.0: + git-url-parse@16.0.0: dependencies: - git-up: 7.0.0 + git-up: 8.1.0 glob-parent@5.1.2: dependencies: @@ -11843,7 +11573,7 @@ snapshots: glob@10.4.5: dependencies: - foreground-child: 3.3.0 + foreground-child: 3.3.1 jackspeak: 3.4.3 minimatch: 9.0.5 minipass: 7.1.2 @@ -11852,8 +11582,8 @@ snapshots: glob@11.0.1: dependencies: - foreground-child: 3.3.0 - jackspeak: 4.0.3 + foreground-child: 3.3.1 + jackspeak: 4.1.0 minimatch: 10.0.1 minipass: 7.1.2 package-json-from-dist: 1.0.1 @@ -12020,10 +11750,7 @@ snapshots: html-escaper@2.0.2: {} - htmlfy@0.6.0: - optional: true - - htmlfy@0.6.7: {} + htmlfy@0.6.6: {} htmlparser2@9.1.0: dependencies: @@ -12141,20 +11868,16 @@ snapshots: run-async: 3.0.0 rxjs: 7.8.2 - inquirer@9.3.2: + inquirer@12.3.0(@types/node@22.14.0): dependencies: - '@inquirer/figures': 1.0.10 + '@inquirer/core': 10.1.10(@types/node@22.14.0) + '@inquirer/prompts': 7.4.1(@types/node@22.14.0) + '@inquirer/type': 3.0.6(@types/node@22.14.0) + '@types/node': 22.14.0 ansi-escapes: 4.3.2 - cli-width: 4.1.0 - external-editor: 3.1.0 - mute-stream: 1.0.0 - ora: 5.4.1 + mute-stream: 2.0.0 run-async: 3.0.0 - rxjs: 7.8.1 - string-width: 4.2.3 - strip-ansi: 6.0.1 - wrap-ansi: 6.2.0 - yoctocolors-cjs: 2.1.2 + rxjs: 7.8.2 internal-slot@1.1.0: dependencies: @@ -12220,7 +11943,7 @@ snapshots: dependencies: builtin-modules: 3.3.0 - is-bun-module@1.3.0: + is-bun-module@2.0.0: dependencies: semver: 7.7.1 @@ -12314,7 +12037,7 @@ snapshots: is-regex@1.2.1: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 gopd: 1.2.0 has-tostringtag: 1.0.2 hasown: 2.0.2 @@ -12433,7 +12156,7 @@ snapshots: optionalDependencies: '@pkgjs/parseargs': 0.11.0 - jackspeak@4.0.3: + jackspeak@4.1.0: dependencies: '@isaacs/cliui': 8.0.2 @@ -12477,7 +12200,7 @@ snapshots: jest-util@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 22.14.1 + '@types/node': 22.14.0 chalk: 4.1.2 ci-info: 3.9.0 graceful-fs: 4.2.11 @@ -12530,28 +12253,27 @@ snapshots: jsbn@1.1.0: {} - jsdom@25.0.1: + jsdom@26.1.0: dependencies: - cssstyle: 4.2.1 + cssstyle: 4.3.0 data-urls: 5.0.0 decimal.js: 10.5.0 - form-data: 4.0.2 html-encoding-sniffer: 4.0.0 http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 is-potential-custom-element-name: 1.0.1 - nwsapi: 2.2.16 + nwsapi: 2.2.20 parse5: 7.2.1 - rrweb-cssom: 0.7.1 + rrweb-cssom: 0.8.0 saxes: 6.0.0 symbol-tree: 3.2.4 - tough-cookie: 5.1.1 + tough-cookie: 5.1.2 w3c-xmlserializer: 5.0.0 webidl-conversions: 7.0.0 whatwg-encoding: 3.1.1 whatwg-mimetype: 4.0.0 - whatwg-url: 14.1.1 - ws: 8.18.0 + whatwg-url: 14.2.0 + ws: 8.18.1 xml-name-validator: 5.0.0 transitivePeerDependencies: - bufferutil @@ -12614,7 +12336,7 @@ snapshots: kuler@2.0.0: {} - ky@1.7.5: {} + ky@1.8.0: {} language-subtag-registry@0.3.23: {} @@ -12649,10 +12371,11 @@ snapshots: local-access@1.1.0: {} - local-pkg@0.5.1: + local-pkg@1.1.1: dependencies: mlly: 1.7.4 - pkg-types: 1.3.1 + pkg-types: 2.1.0 + quansync: 0.2.10 locate-app@2.5.0: dependencies: @@ -12743,7 +12466,7 @@ snapshots: lru-cache@10.4.3: {} - lru-cache@11.0.2: {} + lru-cache@11.1.0: {} lru-cache@5.1.1: dependencies: @@ -12759,8 +12482,8 @@ snapshots: magicast@0.3.5: dependencies: - '@babel/parser': 7.26.9 - '@babel/types': 7.26.9 + '@babel/parser': 7.27.0 + '@babel/types': 7.27.0 source-map-js: 1.2.1 make-dir@4.0.0: @@ -12787,7 +12510,7 @@ snapshots: dependencies: '@types/mdast': 3.0.15 '@types/unist': 2.0.11 - decode-named-character-reference: 1.0.2 + decode-named-character-reference: 1.1.0 mdast-util-to-string: 3.2.0 micromark: 3.2.0 micromark-util-decode-numeric-character-reference: 1.1.0 @@ -12904,7 +12627,7 @@ snapshots: micromark-core-commonmark@1.1.0: dependencies: - decode-named-character-reference: 1.0.2 + decode-named-character-reference: 1.1.0 micromark-factory-destination: 1.1.0 micromark-factory-label: 1.1.0 micromark-factory-space: 1.1.0 @@ -13048,7 +12771,7 @@ snapshots: micromark-util-decode-string@1.1.0: dependencies: - decode-named-character-reference: 1.0.2 + decode-named-character-reference: 1.1.0 micromark-util-character: 1.2.0 micromark-util-decode-numeric-character-reference: 1.1.0 micromark-util-symbol: 1.1.0 @@ -13097,7 +12820,7 @@ snapshots: dependencies: '@types/debug': 4.1.12 debug: 4.4.0(supports-color@8.1.1) - decode-named-character-reference: 1.0.2 + decode-named-character-reference: 1.1.0 micromark-core-commonmark: 1.1.0 micromark-factory-space: 1.1.0 micromark-util-character: 1.2.0 @@ -13243,7 +12966,7 @@ snapshots: mrmime@1.0.1: {} - mrmime@2.0.0: {} + mrmime@2.0.1: {} ms@2.0.0: {} @@ -13370,7 +13093,7 @@ snapshots: dependencies: boolbase: 1.0.0 - nwsapi@2.2.16: {} + nwsapi@2.2.20: {} object-assign@4.1.1: {} @@ -13502,10 +13225,10 @@ snapshots: string-width: 7.2.0 strip-ansi: 7.1.0 - os-name@5.1.0: + os-name@6.0.0: dependencies: macos-release: 3.3.0 - windows-release: 5.1.1 + windows-release: 6.0.1 os-tmpdir@1.0.2: {} @@ -13557,7 +13280,7 @@ snapshots: p-try@2.2.0: {} - pac-proxy-agent@7.1.0: + pac-proxy-agent@7.2.0: dependencies: '@tootallnate/quickjs-emscripten': 0.23.0 agent-base: 7.1.3 @@ -13579,10 +13302,10 @@ snapshots: package-json@10.0.1: dependencies: - ky: 1.7.5 + ky: 1.8.0 registry-auth-token: 5.1.0 registry-url: 6.0.1 - semver: 7.7.1 + semver: 7.6.3 package-manager-detector@0.2.11: dependencies: @@ -13615,7 +13338,7 @@ snapshots: '@types/unist': 2.0.11 character-entities-legacy: 3.0.0 character-reference-invalid: 2.0.1 - decode-named-character-reference: 1.0.2 + decode-named-character-reference: 1.1.0 is-alphanumerical: 2.0.1 is-decimal: 2.0.1 is-hexadecimal: 2.0.1 @@ -13643,8 +13366,9 @@ snapshots: dependencies: protocols: 2.0.2 - parse-url@8.1.0: + parse-url@9.2.0: dependencies: + '@types/parse-path': 7.0.3 parse-path: 7.0.1 parse5-htmlparser2-tree-adapter@7.1.0: @@ -13691,7 +13415,7 @@ snapshots: path-scurry@2.0.0: dependencies: - lru-cache: 11.0.2 + lru-cache: 11.1.0 minipass: 7.1.2 path-to-regexp@0.1.12: {} @@ -13734,7 +13458,7 @@ snapshots: pify@4.0.1: {} - pirates@4.0.6: {} + pirates@4.0.7: {} pixelmatch@5.3.0: dependencies: @@ -13746,6 +13470,12 @@ snapshots: mlly: 1.7.4 pathe: 2.0.3 + pkg-types@2.1.0: + dependencies: + confbox: 0.2.2 + exsolve: 1.0.4 + pathe: 2.0.3 + pluralize@8.0.0: {} pngjs@6.0.0: {} @@ -13775,13 +13505,13 @@ snapshots: camelcase-css: 2.0.1 postcss: 8.5.3 - postcss-load-config@4.0.2(postcss@8.5.3)(ts-node@10.9.2(@types/node@22.14.1)(typescript@5.7.3)): + postcss-load-config@4.0.2(postcss@8.5.3)(ts-node@10.9.2(@types/node@22.14.0)(typescript@5.8.3)): dependencies: lilconfig: 3.1.3 - yaml: 2.7.0 + yaml: 2.7.1 optionalDependencies: postcss: 8.5.3 - ts-node: 10.9.2(@types/node@22.14.1)(typescript@5.7.3) + ts-node: 10.9.2(@types/node@22.14.0)(typescript@5.8.3) postcss-modules-extract-imports@3.1.0(postcss@8.5.3): dependencies: @@ -13896,7 +13626,7 @@ snapshots: http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 lru-cache: 7.18.3 - pac-proxy-agent: 7.1.0 + pac-proxy-agent: 7.2.0 proxy-from-env: 1.1.0 socks-proxy-agent: 8.0.5 transitivePeerDependencies: @@ -13993,19 +13723,19 @@ snapshots: '@remix-run/router': 1.23.0 react: 18.3.1 - react-select@5.10.1(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + react-select@5.10.1(@types/react@18.3.20)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@babel/runtime': 7.27.0 '@emotion/cache': 11.14.0 - '@emotion/react': 11.14.0(@types/react@18.3.18)(react@18.3.1) + '@emotion/react': 11.14.0(@types/react@18.3.20)(react@18.3.1) '@floating-ui/dom': 1.6.13 - '@types/react-transition-group': 4.4.12(@types/react@18.3.18) + '@types/react-transition-group': 4.4.12(@types/react@18.3.20) 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.0(@types/react@18.3.18)(react@18.3.1) + use-isomorphic-layout-effect: 1.2.0(@types/react@18.3.20)(react@18.3.1) transitivePeerDependencies: - '@types/react' - supports-color @@ -14149,33 +13879,35 @@ snapshots: dependencies: jsesc: 0.5.0 - release-it@17.11.0(typescript@5.7.3): + release-it@18.1.2(@types/node@22.14.0)(typescript@5.8.3): dependencies: '@iarna/toml': 2.2.5 - '@octokit/rest': 20.1.1 + '@octokit/rest': 21.0.2 async-retry: 1.3.3 chalk: 5.4.1 - ci-info: 4.1.0 - cosmiconfig: 9.0.0(typescript@5.7.3) - execa: 8.0.0 - git-url-parse: 14.0.0 + ci-info: 4.2.0 + cosmiconfig: 9.0.0(typescript@5.8.3) + execa: 9.5.2 + git-url-parse: 16.0.0 globby: 14.0.2 - inquirer: 9.3.2 + inquirer: 12.3.0(@types/node@22.14.0) issue-parser: 7.0.1 lodash: 4.17.21 mime-types: 2.1.35 new-github-release-url: 2.0.0 open: 10.1.0 ora: 8.1.1 - os-name: 5.1.0 + os-name: 6.0.0 proxy-agent: 6.5.0 semver: 7.6.3 shelljs: 0.8.5 + undici: 6.21.1 update-notifier: 7.3.1 url-join: 5.0.0 wildcard-match: 5.1.4 yargs-parser: 21.1.1 transitivePeerDependencies: + - '@types/node' - supports-color - typescript @@ -14263,7 +13995,7 @@ snapshots: retry@0.13.1: {} - reusify@1.0.4: {} + reusify@1.1.0: {} rgb2hex@0.2.5: {} @@ -14272,34 +14004,32 @@ snapshots: glob: 11.0.1 package-json-from-dist: 1.0.1 - rollup@4.40.0: + rollup@4.39.0: dependencies: '@types/estree': 1.0.7 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.40.0 - '@rollup/rollup-android-arm64': 4.40.0 - '@rollup/rollup-darwin-arm64': 4.40.0 - '@rollup/rollup-darwin-x64': 4.40.0 - '@rollup/rollup-freebsd-arm64': 4.40.0 - '@rollup/rollup-freebsd-x64': 4.40.0 - '@rollup/rollup-linux-arm-gnueabihf': 4.40.0 - '@rollup/rollup-linux-arm-musleabihf': 4.40.0 - '@rollup/rollup-linux-arm64-gnu': 4.40.0 - '@rollup/rollup-linux-arm64-musl': 4.40.0 - '@rollup/rollup-linux-loongarch64-gnu': 4.40.0 - '@rollup/rollup-linux-powerpc64le-gnu': 4.40.0 - '@rollup/rollup-linux-riscv64-gnu': 4.40.0 - '@rollup/rollup-linux-riscv64-musl': 4.40.0 - '@rollup/rollup-linux-s390x-gnu': 4.40.0 - '@rollup/rollup-linux-x64-gnu': 4.40.0 - '@rollup/rollup-linux-x64-musl': 4.40.0 - '@rollup/rollup-win32-arm64-msvc': 4.40.0 - '@rollup/rollup-win32-ia32-msvc': 4.40.0 - '@rollup/rollup-win32-x64-msvc': 4.40.0 + '@rollup/rollup-android-arm-eabi': 4.39.0 + '@rollup/rollup-android-arm64': 4.39.0 + '@rollup/rollup-darwin-arm64': 4.39.0 + '@rollup/rollup-darwin-x64': 4.39.0 + '@rollup/rollup-freebsd-arm64': 4.39.0 + '@rollup/rollup-freebsd-x64': 4.39.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.39.0 + '@rollup/rollup-linux-arm-musleabihf': 4.39.0 + '@rollup/rollup-linux-arm64-gnu': 4.39.0 + '@rollup/rollup-linux-arm64-musl': 4.39.0 + '@rollup/rollup-linux-loongarch64-gnu': 4.39.0 + '@rollup/rollup-linux-powerpc64le-gnu': 4.39.0 + '@rollup/rollup-linux-riscv64-gnu': 4.39.0 + '@rollup/rollup-linux-riscv64-musl': 4.39.0 + '@rollup/rollup-linux-s390x-gnu': 4.39.0 + '@rollup/rollup-linux-x64-gnu': 4.39.0 + '@rollup/rollup-linux-x64-musl': 4.39.0 + '@rollup/rollup-win32-arm64-msvc': 4.39.0 + '@rollup/rollup-win32-ia32-msvc': 4.39.0 + '@rollup/rollup-win32-x64-msvc': 4.39.0 fsevents: 2.3.3 - rrweb-cssom@0.7.1: {} - rrweb-cssom@0.8.0: {} run-applescript@7.0.0: {} @@ -14310,10 +14040,6 @@ snapshots: dependencies: queue-microtask: 1.2.3 - rxjs@7.8.1: - dependencies: - tslib: 2.8.1 - rxjs@7.8.2: dependencies: tslib: 2.8.1 @@ -14343,7 +14069,7 @@ snapshots: safe-regex-test@1.1.0: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 es-errors: 1.3.0 is-regex: 1.2.1 @@ -14351,17 +14077,6 @@ snapshots: safer-buffer@2.1.2: {} - saucelabs@8.0.0: - dependencies: - change-case: 4.1.2 - compressing: 1.10.1 - form-data: 4.0.2 - got: 11.8.6 - hash.js: 1.1.7 - query-string: 7.1.3 - tunnel: 0.0.6 - yargs: 17.7.2 - saucelabs@9.0.2: dependencies: change-case: 4.1.2 @@ -14543,7 +14258,7 @@ snapshots: simple-xml-to-json@1.2.3: {} - sirv-cli@3.0.0: + sirv-cli@3.0.1: dependencies: console-clear: 1.1.1 get-port: 5.1.1 @@ -14551,19 +14266,13 @@ snapshots: local-access: 1.1.0 sade: 1.8.1 semiver: 1.1.0 - sirv: 3.0.0 + sirv: 3.0.1 tinydate: 1.3.0 - sirv@3.0.0: - dependencies: - '@polka/url': 1.0.0-next.28 - mrmime: 2.0.0 - totalist: 3.0.1 - sirv@3.0.1: dependencies: '@polka/url': 1.0.0-next.28 - mrmime: 2.0.0 + mrmime: 2.0.1 totalist: 3.0.1 slash@3.0.0: {} @@ -14642,7 +14351,7 @@ snapshots: dependencies: minipass: 7.1.2 - stable-hash@0.0.4: {} + stable-hash@0.0.5: {} stack-trace@0.0.10: {} @@ -14654,7 +14363,7 @@ snapshots: statuses@2.0.1: {} - std-env@3.8.0: {} + std-env@3.9.0: {} stdin-discarder@0.2.2: {} @@ -14735,7 +14444,7 @@ snapshots: string.prototype.trimend@1.0.9: dependencies: call-bind: 1.0.8 - call-bound: 1.0.3 + call-bound: 1.0.4 define-properties: 1.2.1 es-object-atoms: 1.1.1 @@ -14804,7 +14513,7 @@ snapshots: glob: 10.4.5 lines-and-columns: 1.2.4 mz: 2.7.0 - pirates: 4.0.6 + pirates: 4.0.7 ts-interface-checker: 0.1.13 supports-color@7.2.0: @@ -14819,7 +14528,7 @@ snapshots: symbol-tree@3.2.4: {} - tailwindcss@3.4.17(ts-node@10.9.2(@types/node@22.14.1)(typescript@5.7.3)): + tailwindcss@3.4.17(ts-node@10.9.2(@types/node@22.14.0)(typescript@5.8.3)): dependencies: '@alloc/quick-lru': 5.2.0 arg: 5.0.2 @@ -14838,7 +14547,7 @@ snapshots: postcss: 8.5.3 postcss-import: 15.1.0(postcss@8.5.3) postcss-js: 4.0.1(postcss@8.5.3) - postcss-load-config: 4.0.2(postcss@8.5.3)(ts-node@10.9.2(@types/node@22.14.1)(typescript@5.7.3)) + postcss-load-config: 4.0.2(postcss@8.5.3)(ts-node@10.9.2(@types/node@22.14.0)(typescript@5.8.3)) postcss-nested: 6.2.0(postcss@8.5.3) postcss-selector-parser: 6.1.2 resolve: 1.22.10 @@ -14846,8 +14555,6 @@ snapshots: transitivePeerDependencies: - ts-node - tapable@2.2.1: {} - tar-fs@2.1.2: dependencies: chownr: 1.1.4 @@ -14952,11 +14659,6 @@ snapshots: tinyexec@0.3.2: {} - tinyglobby@0.2.10: - dependencies: - fdir: 6.4.3(picomatch@4.0.2) - picomatch: 4.0.2 - tinyglobby@0.2.12: dependencies: fdir: 6.4.3(picomatch@4.0.2) @@ -14970,11 +14672,11 @@ snapshots: tinyspy@3.0.2: {} - tldts-core@6.1.77: {} + tldts-core@6.1.85: {} - tldts@6.1.77: + tldts@6.1.85: dependencies: - tldts-core: 6.1.77 + tldts-core: 6.1.85 tmp@0.0.33: dependencies: @@ -14997,13 +14699,13 @@ snapshots: totalist@3.0.1: {} - tough-cookie@5.1.1: + tough-cookie@5.1.2: dependencies: - tldts: 6.1.77 + tldts: 6.1.85 tr46@0.0.3: {} - tr46@5.0.0: + tr46@5.1.0: dependencies: punycode: 2.3.1 @@ -15019,13 +14721,13 @@ snapshots: dependencies: matchit: 1.1.0 - ts-api-utils@2.1.0(typescript@5.7.3): + ts-api-utils@2.1.0(typescript@5.8.3): dependencies: - typescript: 5.7.3 + typescript: 5.8.3 ts-interface-checker@0.1.13: {} - ts-node@10.9.2(@types/node@22.14.0)(typescript@5.7.3): + ts-node@10.9.2(@types/node@22.14.0)(typescript@5.8.3): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.11 @@ -15033,38 +14735,19 @@ snapshots: '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.4 '@types/node': 22.14.0 - acorn: 8.14.0 - acorn-walk: 8.3.4 - arg: 4.1.3 - create-require: 1.1.1 - diff: 4.0.2 - make-error: 1.3.6 - typescript: 5.7.3 - v8-compile-cache-lib: 3.0.1 - yn: 3.1.1 - - ts-node@10.9.2(@types/node@22.14.1)(typescript@5.7.3): - dependencies: - '@cspotcode/source-map-support': 0.8.1 - '@tsconfig/node10': 1.0.11 - '@tsconfig/node12': 1.0.11 - '@tsconfig/node14': 1.0.3 - '@tsconfig/node16': 1.0.4 - '@types/node': 22.14.1 - acorn: 8.14.0 + acorn: 8.14.1 acorn-walk: 8.3.4 arg: 4.1.3 create-require: 1.1.1 diff: 4.0.2 make-error: 1.3.6 - typescript: 5.7.3 + typescript: 5.8.3 v8-compile-cache-lib: 3.0.1 yn: 3.1.1 - optional: true - tsconfck@3.1.5(typescript@5.7.3): + tsconfck@3.1.5(typescript@5.8.3): optionalDependencies: - typescript: 5.7.3 + typescript: 5.8.3 tsconfig-paths@3.15.0: dependencies: @@ -15148,7 +14831,7 @@ snapshots: possible-typed-array-names: 1.1.0 reflect.getprototypeof: 1.0.10 - typescript@5.7.3: {} + typescript@5.8.3: {} ufo@1.5.4: {} @@ -15161,10 +14844,10 @@ snapshots: undici-types@6.19.8: {} - undici-types@6.20.0: {} - undici-types@6.21.0: {} + undici@6.21.1: {} + undici@6.21.2: {} unicorn-magic@0.1.0: {} @@ -15223,7 +14906,7 @@ snapshots: unist-util-is: 5.2.1 unist-util-visit-parents: 5.1.3 - universal-user-agent@6.0.1: {} + universal-user-agent@7.0.2: {} universalify@0.1.2: {} @@ -15231,6 +14914,24 @@ snapshots: unpipe@1.0.0: {} + unrs-resolver@1.3.3: + optionalDependencies: + '@unrs/resolver-binding-darwin-arm64': 1.3.3 + '@unrs/resolver-binding-darwin-x64': 1.3.3 + '@unrs/resolver-binding-freebsd-x64': 1.3.3 + '@unrs/resolver-binding-linux-arm-gnueabihf': 1.3.3 + '@unrs/resolver-binding-linux-arm-musleabihf': 1.3.3 + '@unrs/resolver-binding-linux-arm64-gnu': 1.3.3 + '@unrs/resolver-binding-linux-arm64-musl': 1.3.3 + '@unrs/resolver-binding-linux-ppc64-gnu': 1.3.3 + '@unrs/resolver-binding-linux-s390x-gnu': 1.3.3 + '@unrs/resolver-binding-linux-x64-gnu': 1.3.3 + '@unrs/resolver-binding-linux-x64-musl': 1.3.3 + '@unrs/resolver-binding-wasm32-wasi': 1.3.3 + '@unrs/resolver-binding-win32-arm64-msvc': 1.3.3 + '@unrs/resolver-binding-win32-ia32-msvc': 1.3.3 + '@unrs/resolver-binding-win32-x64-msvc': 1.3.3 + update-browserslist-db@1.1.3(browserslist@4.24.4): dependencies: browserslist: 4.24.4 @@ -15247,7 +14948,7 @@ snapshots: is-npm: 6.0.0 latest-version: 9.0.0 pupa: 3.1.0 - semver: 7.7.1 + semver: 7.6.3 xdg-basedir: 5.1.0 upper-case-first@2.0.2: @@ -15266,11 +14967,11 @@ snapshots: urlpattern-polyfill@10.0.0: {} - use-isomorphic-layout-effect@1.2.0(@types/react@18.3.18)(react@18.3.1): + use-isomorphic-layout-effect@1.2.0(@types/react@18.3.20)(react@18.3.1): dependencies: react: 18.3.1 optionalDependencies: - '@types/react': 18.3.18 + '@types/react': 18.3.20 userhome@1.0.1: {} @@ -15299,9 +15000,9 @@ snapshots: v8-compile-cache-lib@3.0.1: {} - valibot@0.41.0(typescript@5.7.3): + valibot@0.41.0(typescript@5.8.3): optionalDependencies: - typescript: 5.7.3 + typescript: 5.8.3 validate-npm-package-license@3.0.4: dependencies: @@ -15324,13 +15025,13 @@ snapshots: unist-util-stringify-position: 3.0.3 vfile-message: 3.1.4 - vite-node@1.6.1(@types/node@22.14.1): + vite-node@1.6.1(@types/node@22.14.0): dependencies: cac: 6.7.14 debug: 4.4.0(supports-color@8.1.1) pathe: 1.1.2 picocolors: 1.1.1 - vite: 5.4.18(@types/node@22.14.1) + vite: 5.4.18(@types/node@22.14.0) transitivePeerDependencies: - '@types/node' - less @@ -15342,7 +15043,25 @@ snapshots: - supports-color - terser - vite-node@3.0.8(@types/node@22.14.0): + vite-node@3.0.0-beta.2(@types/node@22.14.0): + dependencies: + cac: 6.7.14 + debug: 4.4.0(supports-color@8.1.1) + es-module-lexer: 1.6.0 + pathe: 1.1.2 + vite: 5.4.18(@types/node@22.14.0) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + vite-node@3.1.1(@types/node@22.14.0): dependencies: cac: 6.7.14 debug: 4.4.0(supports-color@8.1.1) @@ -15360,62 +15079,62 @@ snapshots: - supports-color - terser - vite-tsconfig-paths@5.1.4(typescript@5.7.3)(vite@5.4.18(@types/node@22.14.1)): + vite-tsconfig-paths@5.1.4(typescript@5.8.3)(vite@5.4.18(@types/node@22.14.0)): dependencies: debug: 4.4.0(supports-color@8.1.1) globrex: 0.1.2 - tsconfck: 3.1.5(typescript@5.7.3) + tsconfck: 3.1.5(typescript@5.8.3) optionalDependencies: - vite: 5.4.18(@types/node@22.14.1) + vite: 5.4.18(@types/node@22.14.0) transitivePeerDependencies: - supports-color - typescript - vite@5.4.18(@types/node@22.14.0): + vite@5.4.17(@types/node@22.14.0): dependencies: esbuild: 0.21.5 postcss: 8.5.3 - rollup: 4.40.0 + rollup: 4.39.0 optionalDependencies: '@types/node': 22.14.0 fsevents: 2.3.3 - vite@5.4.18(@types/node@22.14.1): + vite@5.4.18(@types/node@22.14.0): dependencies: esbuild: 0.21.5 postcss: 8.5.3 - rollup: 4.40.0 + rollup: 4.39.0 optionalDependencies: - '@types/node': 22.14.1 + '@types/node': 22.14.0 fsevents: 2.3.3 - vitest@3.0.8(@types/debug@4.1.12)(@types/node@22.14.0)(@vitest/ui@3.0.8)(jsdom@25.0.1): + vitest@3.1.1(@types/debug@4.1.12)(@types/node@22.14.0)(@vitest/ui@3.1.1)(jsdom@26.1.0): dependencies: - '@vitest/expect': 3.0.8 - '@vitest/mocker': 3.0.8(vite@5.4.18(@types/node@22.14.0)) - '@vitest/pretty-format': 3.0.8 - '@vitest/runner': 3.0.8 - '@vitest/snapshot': 3.0.8 - '@vitest/spy': 3.0.8 - '@vitest/utils': 3.0.8 + '@vitest/expect': 3.1.1 + '@vitest/mocker': 3.1.1(vite@5.4.17(@types/node@22.14.0)) + '@vitest/pretty-format': 3.1.1 + '@vitest/runner': 3.1.1 + '@vitest/snapshot': 3.1.1 + '@vitest/spy': 3.1.1 + '@vitest/utils': 3.1.1 chai: 5.2.0 debug: 4.4.0(supports-color@8.1.1) - expect-type: 1.1.0 + expect-type: 1.2.1 magic-string: 0.30.17 pathe: 2.0.3 - std-env: 3.8.0 + std-env: 3.9.0 tinybench: 2.9.0 tinyexec: 0.3.2 tinypool: 1.0.2 tinyrainbow: 2.0.0 - vite: 5.4.18(@types/node@22.14.0) - vite-node: 3.0.8(@types/node@22.14.0) + vite: 5.4.17(@types/node@22.14.0) + vite-node: 3.1.1(@types/node@22.14.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/debug': 4.1.12 '@types/node': 22.14.0 - '@vitest/ui': 3.0.8(vitest@3.0.8) - jsdom: 25.0.1 + '@vitest/ui': 3.1.1(vitest@3.1.1) + jsdom: 26.1.0 transitivePeerDependencies: - less - lightningcss @@ -15445,17 +15164,17 @@ snapshots: dependencies: defaults: 1.0.4 - wdio-lambdatest-service@4.0.0(@wdio/cli@9.12.5)(@wdio/types@9.12.2)(webdriverio@9.12.4): + wdio-lambdatest-service@4.0.0(@wdio/cli@9.12.6)(@wdio/types@9.12.6)(webdriverio@9.12.6): dependencies: - '@lambdatest/node-tunnel': 4.0.8 - '@wdio/cli': 9.12.5 + '@lambdatest/node-tunnel': 4.0.9 + '@wdio/cli': 9.12.6 '@wdio/logger': 7.26.0 - '@wdio/types': 9.12.2 - axios: 1.7.9 + '@wdio/types': 9.12.6 + axios: 1.8.4 colors: 1.4.0 form-data: 4.0.2 source-map-support: 0.5.21 - webdriverio: 9.12.4 + webdriverio: 9.12.6 winston: 3.17.0 transitivePeerDependencies: - debug @@ -15469,33 +15188,15 @@ snapshots: web-streams-polyfill@3.3.3: {} - webdriver@9.12.4: + webdriver@9.12.6: dependencies: '@types/node': 20.17.30 '@types/ws': 8.18.1 - '@wdio/config': 9.12.3 - '@wdio/logger': 9.4.4 - '@wdio/protocols': 9.12.3 - '@wdio/types': 9.12.3 - '@wdio/utils': 9.12.3 - deepmerge-ts: 7.1.5 - undici: 6.21.2 - ws: 8.18.1 - transitivePeerDependencies: - - bare-buffer - - bufferutil - - supports-color - - utf-8-validate - - webdriver@9.12.5: - dependencies: - '@types/node': 20.17.30 - '@types/ws': 8.18.1 - '@wdio/config': 9.12.5 + '@wdio/config': 9.12.6 '@wdio/logger': 9.4.4 '@wdio/protocols': 9.12.5 - '@wdio/types': 9.12.3 - '@wdio/utils': 9.12.5 + '@wdio/types': 9.12.6 + '@wdio/utils': 9.12.6 deepmerge-ts: 7.1.5 undici: 6.21.2 ws: 8.18.1 @@ -15505,75 +15206,23 @@ snapshots: - supports-color - utf-8-validate - webdriver@9.9.1: - dependencies: - '@types/node': 20.17.30 - '@types/ws': 8.5.14 - '@wdio/config': 9.9.0 - '@wdio/logger': 9.4.4 - '@wdio/protocols': 9.7.0 - '@wdio/types': 9.9.0 - '@wdio/utils': 9.9.0 - deepmerge-ts: 7.1.4 - undici: 6.21.2 - ws: 8.18.0 - transitivePeerDependencies: - - bare-buffer - - bufferutil - - supports-color - - utf-8-validate - optional: true - - webdriverio@9.12.4: - dependencies: - '@types/node': 20.17.30 - '@types/sinonjs__fake-timers': 8.1.5 - '@wdio/config': 9.12.3 - '@wdio/logger': 9.4.4 - '@wdio/protocols': 9.12.3 - '@wdio/repl': 9.4.4 - '@wdio/types': 9.12.3 - '@wdio/utils': 9.12.3 - archiver: 7.0.1 - aria-query: 5.3.2 - cheerio: 1.0.0 - css-shorthand-properties: 1.1.2 - css-value: 0.0.1 - grapheme-splitter: 1.0.4 - htmlfy: 0.6.7 - is-plain-obj: 4.1.0 - jszip: 3.10.1 - lodash.clonedeep: 4.5.0 - lodash.zip: 4.2.0 - query-selector-shadow-dom: 1.0.1 - resq: 1.11.0 - rgb2hex: 0.2.5 - serialize-error: 11.0.3 - urlpattern-polyfill: 10.0.0 - webdriver: 9.12.4 - transitivePeerDependencies: - - bare-buffer - - bufferutil - - supports-color - - utf-8-validate - - webdriverio@9.12.5: + webdriverio@9.12.6: dependencies: '@types/node': 20.17.30 '@types/sinonjs__fake-timers': 8.1.5 - '@wdio/config': 9.12.5 + '@wdio/config': 9.12.6 '@wdio/logger': 9.4.4 '@wdio/protocols': 9.12.5 '@wdio/repl': 9.4.4 - '@wdio/types': 9.12.3 - '@wdio/utils': 9.12.5 + '@wdio/types': 9.12.6 + '@wdio/utils': 9.12.6 archiver: 7.0.1 aria-query: 5.3.2 cheerio: 1.0.0 css-shorthand-properties: 1.1.2 css-value: 0.0.1 grapheme-splitter: 1.0.4 - htmlfy: 0.6.7 + htmlfy: 0.6.6 is-plain-obj: 4.1.0 jszip: 3.10.1 lodash.clonedeep: 4.5.0 @@ -15583,47 +15232,13 @@ snapshots: rgb2hex: 0.2.5 serialize-error: 11.0.3 urlpattern-polyfill: 10.0.0 - webdriver: 9.12.5 + webdriver: 9.12.6 transitivePeerDependencies: - bare-buffer - bufferutil - supports-color - utf-8-validate - webdriverio@9.9.1: - dependencies: - '@types/node': 20.17.30 - '@types/sinonjs__fake-timers': 8.1.5 - '@wdio/config': 9.9.0 - '@wdio/logger': 9.4.4 - '@wdio/protocols': 9.7.0 - '@wdio/repl': 9.4.4 - '@wdio/types': 9.9.0 - '@wdio/utils': 9.9.0 - archiver: 7.0.1 - aria-query: 5.3.2 - cheerio: 1.0.0 - css-shorthand-properties: 1.1.2 - css-value: 0.0.1 - grapheme-splitter: 1.0.4 - htmlfy: 0.6.0 - is-plain-obj: 4.1.0 - jszip: 3.10.1 - lodash.clonedeep: 4.5.0 - lodash.zip: 4.2.0 - query-selector-shadow-dom: 1.0.1 - resq: 1.11.0 - rgb2hex: 0.2.5 - serialize-error: 11.0.3 - urlpattern-polyfill: 10.0.0 - webdriver: 9.9.1 - transitivePeerDependencies: - - bare-buffer - - bufferutil - - supports-color - - utf-8-validate - optional: true - webidl-conversions@3.0.1: {} webidl-conversions@7.0.0: {} @@ -15634,9 +15249,9 @@ snapshots: whatwg-mimetype@4.0.0: {} - whatwg-url@14.1.1: + whatwg-url@14.2.0: dependencies: - tr46: 5.0.0 + tr46: 5.1.0 webidl-conversions: 7.0.0 whatwg-url@5.0.0: @@ -15710,9 +15325,9 @@ snapshots: wildcard-match@5.1.4: {} - windows-release@5.1.1: + windows-release@6.0.1: dependencies: - execa: 5.1.1 + execa: 8.0.1 winston-transport@4.9.0: dependencies: @@ -15766,8 +15381,6 @@ snapshots: ws@7.5.10: {} - ws@8.18.0: {} - ws@8.18.1: {} xdg-basedir@5.1.0: {} @@ -15800,7 +15413,7 @@ snapshots: yaml@1.10.2: {} - yaml@2.7.0: {} + yaml@2.7.1: {} yargs-parser@20.2.9: {} diff --git a/tests/configs/lambdatest.android.emus.web.ts b/tests/configs/lambdatest.android.emus.web.ts index 49be370e1..39fca5770 100644 --- a/tests/configs/lambdatest.android.emus.web.ts +++ b/tests/configs/lambdatest.android.emus.web.ts @@ -12,16 +12,7 @@ export function lambdaTestAndroidEmusWeb({ buildName }: { buildName: string }) { mobileSpecs, build: buildName, deviceOrientation: orientation, - // @TODO: There are issues to get certain screenshots with nativeWebScreenshot in LANDSCAPE mode - // There is also a small issue with out of bound offsets with landscape mode on Pixel 4 Android 14. - // So we limit it to portrait mode - // Error: The value of "offset" is out of range. It must be >= 0 and <= 9849596. Received 9849600 => this 4 is coming from Jimp - // @TODO: investigate the issue with out of bound offsets in landscape mode - wdioIcsCommands: [ - 'checkScreen', - 'checkElement', - platformVersion !== '14' && platformVersion !== '11' ? 'checkFullPageScreen' : '' - ], + wdioIcsCommands: ['checkScreen', 'checkElement', 'checkFullPageScreen'], }) ) ) @@ -39,12 +30,7 @@ export function lambdaTestAndroidEmusWeb({ buildName }: { buildName: string }) { // @TODO: There are issues to get certain screenshots with nativeWebScreenshot in LANDSCAPE mode // - element screenshots are not perfect // - Fullpage screenshots have the address bar in the screenshot - wdioIcsCommands: [ - 'checkScreen', - orientation !== 'landscape' ? 'checkElement' : '', - // @TODO: navigation bar and so on are not set properly, not only in landscape but also portrait mode - // orientation !== 'landscape' ? 'checkFullPageScreen' : '' - ], + wdioIcsCommands: ['checkScreen', 'checkElement', 'checkFullPageScreen'], }) }) ) diff --git a/tests/configs/lambdatest.desktop.browsers.ts b/tests/configs/lambdatest.desktop.browsers.ts index 6d9311915..c4b83fb82 100644 --- a/tests/configs/lambdatest.desktop.browsers.ts +++ b/tests/configs/lambdatest.desktop.browsers.ts @@ -113,7 +113,7 @@ export function lambdaDesktopBrowsers({ platformName: 'MacOS Sequoia', }, 'wdio-ics:options': { - logName: 'BigSurSafari14', + logName: 'SafariLatest', }, }, ] diff --git a/tests/configs/lambdatest.ios.sims.web.ts b/tests/configs/lambdatest.ios.sims.web.ts index cd474a8bf..02cf109a0 100644 --- a/tests/configs/lambdatest.ios.sims.web.ts +++ b/tests/configs/lambdatest.ios.sims.web.ts @@ -6,31 +6,39 @@ export function lambdaTestIosSimWeb({ buildName }: { buildName: string }) { const iOSDevices = [ { // 812 + appiumVersion: '2.16.2', deviceName: 'iPhone 13 mini', platformVersion: '17.5', }, { // 844 + appiumVersion: '2.16.2', deviceName: 'iPhone 13 Pro', platformVersion: '16.0', }, { // 852 + appiumVersion: '2.16.2', deviceName: 'iPhone 14 Pro', platformVersion: '17.5', }, { // 932 + appiumVersion: '2.16.2', deviceName: 'iPhone 15 Pro Max', platformVersion: '18.0', } ] return [ - ...(['landscape', 'portrait'] as DeviceOrientation[]) + ...([ + 'landscape', + 'portrait' + ] as DeviceOrientation[]) .map((orientation) => iOSDevices - .map(({ deviceName, platformVersion }) => ({ + .map(({ appiumVersion, deviceName, platformVersion }) => ({ 'lt:options': { + ...(appiumVersion ? { appiumVersion } : {}), deviceName, platformName: 'ios', platformVersion, diff --git a/tests/configs/sauce.android.emus.app.ts b/tests/configs/sauce.android.emus.app.ts index 6ead5dfb6..2de8f3d06 100644 --- a/tests/configs/sauce.android.emus.app.ts +++ b/tests/configs/sauce.android.emus.app.ts @@ -70,6 +70,8 @@ function createCaps({ 'appium:platformVersion': platformVersion, 'appium:orientation': orientation.toUpperCase(), 'appium:automationName': 'UIAutomator2', + // @ts-expect-error + 'appium:nativeWebScreenshot': true, 'wdio-ics:options': { logName: `app-Emulator${deviceName.replace( /(\s+|\(+|\)+|Emulator)/g, diff --git a/tests/configs/wdio.local.android.emus.web.conf.ts b/tests/configs/wdio.local.android.emus.web.conf.ts index 501c72c53..cad43f783 100644 --- a/tests/configs/wdio.local.android.emus.web.conf.ts +++ b/tests/configs/wdio.local.android.emus.web.conf.ts @@ -12,18 +12,8 @@ export const config: WebdriverIO.Config = { // Capabilities // ============ capabilities: [ - // androidCaps('Pixel_2_XL_Android_9_API_28', 'PORTRAIT', '9.0'), - // androidCaps('Pixel_2_XL_Android_9_API_28', 'PORTRAIT', '9.0', true), - // androidCaps('Pixel_3_XL_Android_10_API_29', 'PORTRAIT', '10.0', ), - // androidCaps('Pixel_3_XL_Android_10_API_29', 'PORTRAIT', '10.0', true), - // androidCaps('Pixel_4_XL_Android_11_API_30', 'PORTRAIT', '11.0'), - // androidCaps('Pixel_4_XL_Android_11_API_30', 'PORTRAIT', '11.0', true), - // androidCaps('Pixel_5_Android_12_API_32', 'PORTRAIT', '12.0'), - // androidCaps('Pixel_5_Android_12_API_32', 'PORTRAIT', '12.0', true), - // androidCaps('Pixel_6_Pro_Android_13_API_33', 'PORTRAIT', '13.0'), - // androidCaps('Pixel_6_Pro_Android_13_API_33', 'PORTRAIT', '13.0', true), - androidCaps('Pixel_7_Pro_Android_14_API_34', 'PORTRAIT', '14.0'), - // androidCaps('Pixel_7_Pro_Android_14_API_34', 'PORTRAIT', '14.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), ], } diff --git a/tests/configs/wdio.local.ios.sims.web.conf.ts b/tests/configs/wdio.local.ios.sims.web.conf.ts index 21ba4372d..3b52da796 100644 --- a/tests/configs/wdio.local.ios.sims.web.conf.ts +++ b/tests/configs/wdio.local.ios.sims.web.conf.ts @@ -12,16 +12,9 @@ export const config: WebdriverIO.Config = { // Capabilities // ============ capabilities: [ - // iOSCaps('iPhone 14', 'PORTRAIT', '16.0'), - // iOSCaps('iPhone 14', 'LANDSCAPE', '16.0'), - // iOSCaps('iPhone 14 Plus', 'PORTRAIT', '16.0'), - // iOSCaps('iPhone 14 Plus', 'LANDSCAPE', '16.0'), - // iOSCaps('iPhone 14 Pro', 'PORTRAIT', '16.0'), - // iOSCaps('iPhone 14 Pro', 'LANDSCAPE', '16.0'), - // iOSCaps("iPhone 14 Pro Max", "PORTRAIT", "16.0"), - // iOSCaps('iPhone 14 Pro Max', 'LANDSCAPE', '16.0'), - iOSCaps('iPhone 15', 'PORTRAIT', '17.2'), - // iOSCaps('iPhone 15', 'LANDSCAPE', '17.2'), + // iOSCaps('iPhone 15 Pro', 'PORTRAIT', '17.5', ['checkFullPageScreen']), + iOSCaps('iPhone 15 Pro', 'LANDSCAPE', '17.5', ['checkFullPageScreen']), + // iOSCaps('iPhone 16 Pro', 'PORTRAIT', '18.2'), ], } @@ -32,6 +25,7 @@ function iOSCaps( // The commands that need to be executed, none means all, // otherwise an array of strings with the commands that // need to be executed + // Options are: 'checkScreen', 'checkElement', 'checkFullPageScreen' wdioIcsCommands: string[] = [] ) { return { diff --git a/tests/lambdaTestBaseline/desktop_chrome/tabbable-chrome-latest-1366x768.png b/tests/lambdaTestBaseline/desktop_chrome/tabbable-chrome-latest-1366x768.png index 4a3209b30..d6d2b3a4b 100644 Binary files a/tests/lambdaTestBaseline/desktop_chrome/tabbable-chrome-latest-1366x768.png and b/tests/lambdaTestBaseline/desktop_chrome/tabbable-chrome-latest-1366x768.png differ diff --git a/tests/lambdaTestBaseline/desktop_safari/fullPage-BigSurSafari14-1366x768.png b/tests/lambdaTestBaseline/desktop_safari/fullPage-BigSurSafari14-1366x768.png deleted file mode 100644 index 26b35cf39..000000000 Binary files a/tests/lambdaTestBaseline/desktop_safari/fullPage-BigSurSafari14-1366x768.png and /dev/null differ diff --git a/tests/lambdaTestBaseline/desktop_safari/fullPage-SafariLatest-1366x768.png b/tests/lambdaTestBaseline/desktop_safari/fullPage-SafariLatest-1366x768.png new file mode 100644 index 000000000..ab4e6490a Binary files /dev/null and b/tests/lambdaTestBaseline/desktop_safari/fullPage-SafariLatest-1366x768.png differ diff --git a/tests/lambdaTestBaseline/desktop_safari/tabbable-BigSurSafari14-1366x768.png b/tests/lambdaTestBaseline/desktop_safari/tabbable-BigSurSafari14-1366x768.png deleted file mode 100644 index 871e2311c..000000000 Binary files a/tests/lambdaTestBaseline/desktop_safari/tabbable-BigSurSafari14-1366x768.png and /dev/null differ diff --git a/tests/lambdaTestBaseline/desktop_safari/tabbable-SafariLatest-1366x768.png b/tests/lambdaTestBaseline/desktop_safari/tabbable-SafariLatest-1366x768.png new file mode 100644 index 000000000..23194198f Binary files /dev/null and b/tests/lambdaTestBaseline/desktop_safari/tabbable-SafariLatest-1366x768.png differ diff --git a/tests/lambdaTestBaseline/desktop_safari/viewportScreenshot-BigSurSafari14-1366x768.png b/tests/lambdaTestBaseline/desktop_safari/viewportScreenshot-BigSurSafari14-1366x768.png deleted file mode 100644 index 521ccda73..000000000 Binary files a/tests/lambdaTestBaseline/desktop_safari/viewportScreenshot-BigSurSafari14-1366x768.png and /dev/null differ diff --git a/tests/lambdaTestBaseline/desktop_safari/viewportScreenshot-SafariLatest-1366x768.png b/tests/lambdaTestBaseline/desktop_safari/viewportScreenshot-SafariLatest-1366x768.png new file mode 100644 index 000000000..31f89fbbb Binary files /dev/null and b/tests/lambdaTestBaseline/desktop_safari/viewportScreenshot-SafariLatest-1366x768.png differ diff --git a/tests/lambdaTestBaseline/desktop_safari/wdioLogo-BigSurSafari14-1366x768.png b/tests/lambdaTestBaseline/desktop_safari/wdioLogo-SafariLatest-1366x768.png similarity index 100% rename from tests/lambdaTestBaseline/desktop_safari/wdioLogo-BigSurSafari14-1366x768.png rename to tests/lambdaTestBaseline/desktop_safari/wdioLogo-SafariLatest-1366x768.png diff --git a/tests/lambdaTestBaseline/galaxy_tab_s8/fullPage-EmulatorGalaxyTabS8LandscapeNativeWebScreenshot13-1707x1067.png b/tests/lambdaTestBaseline/galaxy_tab_s8/fullPage-EmulatorGalaxyTabS8LandscapeNativeWebScreenshot13-1707x1067.png new file mode 100644 index 000000000..daa0af9ae Binary files /dev/null and b/tests/lambdaTestBaseline/galaxy_tab_s8/fullPage-EmulatorGalaxyTabS8LandscapeNativeWebScreenshot13-1707x1067.png differ diff --git a/tests/lambdaTestBaseline/galaxy_tab_s8/fullPage-EmulatorGalaxyTabS8LandscapeNativeWebScreenshot14-1707x1067.png b/tests/lambdaTestBaseline/galaxy_tab_s8/fullPage-EmulatorGalaxyTabS8LandscapeNativeWebScreenshot14-1707x1067.png new file mode 100644 index 000000000..92ba44ec1 Binary files /dev/null and b/tests/lambdaTestBaseline/galaxy_tab_s8/fullPage-EmulatorGalaxyTabS8LandscapeNativeWebScreenshot14-1707x1067.png differ diff --git a/tests/lambdaTestBaseline/galaxy_tab_s8/fullPage-EmulatorGalaxyTabS8PortraitNativeWebScreenshot13-1067x1707.png b/tests/lambdaTestBaseline/galaxy_tab_s8/fullPage-EmulatorGalaxyTabS8PortraitNativeWebScreenshot13-1067x1707.png index fcacf7ebe..6d266678a 100644 Binary files a/tests/lambdaTestBaseline/galaxy_tab_s8/fullPage-EmulatorGalaxyTabS8PortraitNativeWebScreenshot13-1067x1707.png and b/tests/lambdaTestBaseline/galaxy_tab_s8/fullPage-EmulatorGalaxyTabS8PortraitNativeWebScreenshot13-1067x1707.png differ diff --git a/tests/lambdaTestBaseline/galaxy_tab_s8/fullPage-EmulatorGalaxyTabS8PortraitNativeWebScreenshot14-1067x1707.png b/tests/lambdaTestBaseline/galaxy_tab_s8/fullPage-EmulatorGalaxyTabS8PortraitNativeWebScreenshot14-1067x1707.png index 6e452c061..6d266678a 100644 Binary files a/tests/lambdaTestBaseline/galaxy_tab_s8/fullPage-EmulatorGalaxyTabS8PortraitNativeWebScreenshot14-1067x1707.png and b/tests/lambdaTestBaseline/galaxy_tab_s8/fullPage-EmulatorGalaxyTabS8PortraitNativeWebScreenshot14-1067x1707.png differ diff --git a/tests/lambdaTestBaseline/galaxy_tab_s8/screenshot-landscape-EmulatorGalaxyTabS8PortraitNativeWebScreenshot13-1707x1067.png b/tests/lambdaTestBaseline/galaxy_tab_s8/screenshot-landscape-EmulatorGalaxyTabS8PortraitNativeWebScreenshot13-1707x1067.png new file mode 100644 index 000000000..920d6568e Binary files /dev/null and b/tests/lambdaTestBaseline/galaxy_tab_s8/screenshot-landscape-EmulatorGalaxyTabS8PortraitNativeWebScreenshot13-1707x1067.png differ diff --git a/tests/lambdaTestBaseline/galaxy_tab_s8/screenshot-landscape-EmulatorGalaxyTabS8PortraitNativeWebScreenshot14-1707x1067.png b/tests/lambdaTestBaseline/galaxy_tab_s8/screenshot-landscape-EmulatorGalaxyTabS8PortraitNativeWebScreenshot14-1707x1067.png new file mode 100644 index 000000000..b1eabd64e Binary files /dev/null and b/tests/lambdaTestBaseline/galaxy_tab_s8/screenshot-landscape-EmulatorGalaxyTabS8PortraitNativeWebScreenshot14-1707x1067.png differ diff --git a/tests/lambdaTestBaseline/galaxy_tab_s8/screenshot-portrait-EmulatorGalaxyTabS8LandscapeNativeWebScreenshot13-1067x1707.png b/tests/lambdaTestBaseline/galaxy_tab_s8/screenshot-portrait-EmulatorGalaxyTabS8LandscapeNativeWebScreenshot13-1067x1707.png new file mode 100644 index 000000000..89947ce80 Binary files /dev/null and b/tests/lambdaTestBaseline/galaxy_tab_s8/screenshot-portrait-EmulatorGalaxyTabS8LandscapeNativeWebScreenshot13-1067x1707.png differ diff --git a/tests/lambdaTestBaseline/galaxy_tab_s8/screenshot-portrait-EmulatorGalaxyTabS8LandscapeNativeWebScreenshot14-1067x1707.png b/tests/lambdaTestBaseline/galaxy_tab_s8/screenshot-portrait-EmulatorGalaxyTabS8LandscapeNativeWebScreenshot14-1067x1707.png new file mode 100644 index 000000000..410698160 Binary files /dev/null and b/tests/lambdaTestBaseline/galaxy_tab_s8/screenshot-portrait-EmulatorGalaxyTabS8LandscapeNativeWebScreenshot14-1067x1707.png differ diff --git a/tests/lambdaTestBaseline/galaxy_tab_s8/wdioLogo-EmulatorGalaxyTabS8LandscapeNativeWebScreenshot13-1707x1067.png b/tests/lambdaTestBaseline/galaxy_tab_s8/wdioLogo-EmulatorGalaxyTabS8LandscapeNativeWebScreenshot13-1707x1067.png new file mode 100644 index 000000000..53c1c6c72 Binary files /dev/null and b/tests/lambdaTestBaseline/galaxy_tab_s8/wdioLogo-EmulatorGalaxyTabS8LandscapeNativeWebScreenshot13-1707x1067.png differ diff --git a/tests/lambdaTestBaseline/galaxy_tab_s8/wdioLogo-EmulatorGalaxyTabS8LandscapeNativeWebScreenshot14-1707x1067.png b/tests/lambdaTestBaseline/galaxy_tab_s8/wdioLogo-EmulatorGalaxyTabS8LandscapeNativeWebScreenshot14-1707x1067.png new file mode 100644 index 000000000..53c1c6c72 Binary files /dev/null and b/tests/lambdaTestBaseline/galaxy_tab_s8/wdioLogo-EmulatorGalaxyTabS8LandscapeNativeWebScreenshot14-1707x1067.png differ diff --git a/tests/lambdaTestBaseline/iphone_13_mini/fullPage-Iphone13MiniLandscape17-375x812.png b/tests/lambdaTestBaseline/iphone_13_mini/fullPage-Iphone13MiniLandscape17-375x812.png index 7b4047b89..d26740de9 100644 Binary files a/tests/lambdaTestBaseline/iphone_13_mini/fullPage-Iphone13MiniLandscape17-375x812.png and b/tests/lambdaTestBaseline/iphone_13_mini/fullPage-Iphone13MiniLandscape17-375x812.png differ diff --git a/tests/lambdaTestBaseline/iphone_13_mini/fullPage-Iphone13MiniPortrait17-375x812.png b/tests/lambdaTestBaseline/iphone_13_mini/fullPage-Iphone13MiniPortrait17-375x812.png index dd7f53f44..5d0206c8b 100644 Binary files a/tests/lambdaTestBaseline/iphone_13_mini/fullPage-Iphone13MiniPortrait17-375x812.png and b/tests/lambdaTestBaseline/iphone_13_mini/fullPage-Iphone13MiniPortrait17-375x812.png differ diff --git a/tests/lambdaTestBaseline/iphone_13_mini/screenshot-Iphone13MiniLandscape17-375x812.png b/tests/lambdaTestBaseline/iphone_13_mini/screenshot-Iphone13MiniLandscape17-375x812.png index 8ed25b3e1..388a99d74 100644 Binary files a/tests/lambdaTestBaseline/iphone_13_mini/screenshot-Iphone13MiniLandscape17-375x812.png and b/tests/lambdaTestBaseline/iphone_13_mini/screenshot-Iphone13MiniLandscape17-375x812.png differ diff --git a/tests/lambdaTestBaseline/iphone_13_mini/screenshot-Iphone13MiniPortrait17-375x812.png b/tests/lambdaTestBaseline/iphone_13_mini/screenshot-Iphone13MiniPortrait17-375x812.png index 8d4301ce4..01a0a724b 100644 Binary files a/tests/lambdaTestBaseline/iphone_13_mini/screenshot-Iphone13MiniPortrait17-375x812.png and b/tests/lambdaTestBaseline/iphone_13_mini/screenshot-Iphone13MiniPortrait17-375x812.png differ diff --git a/tests/lambdaTestBaseline/iphone_13_mini/screenshot-landscape-Iphone13MiniPortrait17-375x812.png b/tests/lambdaTestBaseline/iphone_13_mini/screenshot-landscape-Iphone13MiniPortrait17-375x812.png new file mode 100644 index 000000000..388a99d74 Binary files /dev/null and b/tests/lambdaTestBaseline/iphone_13_mini/screenshot-landscape-Iphone13MiniPortrait17-375x812.png differ diff --git a/tests/lambdaTestBaseline/iphone_13_mini/screenshot-portrait-Iphone13MiniLandscape17-375x812.png b/tests/lambdaTestBaseline/iphone_13_mini/screenshot-portrait-Iphone13MiniLandscape17-375x812.png new file mode 100644 index 000000000..5542f7b57 Binary files /dev/null and b/tests/lambdaTestBaseline/iphone_13_mini/screenshot-portrait-Iphone13MiniLandscape17-375x812.png differ diff --git a/tests/lambdaTestBaseline/iphone_13_mini/wdioLogo-Iphone13MiniLandscape17-375x812.png b/tests/lambdaTestBaseline/iphone_13_mini/wdioLogo-Iphone13MiniLandscape17-375x812.png index db2af82f2..d4e512f54 100644 Binary files a/tests/lambdaTestBaseline/iphone_13_mini/wdioLogo-Iphone13MiniLandscape17-375x812.png and b/tests/lambdaTestBaseline/iphone_13_mini/wdioLogo-Iphone13MiniLandscape17-375x812.png differ diff --git a/tests/lambdaTestBaseline/iphone_13_mini/wdioLogo-Iphone13MiniPortrait17-375x812.png b/tests/lambdaTestBaseline/iphone_13_mini/wdioLogo-Iphone13MiniPortrait17-375x812.png index d57c53033..f1d8c1d4f 100644 Binary files a/tests/lambdaTestBaseline/iphone_13_mini/wdioLogo-Iphone13MiniPortrait17-375x812.png and b/tests/lambdaTestBaseline/iphone_13_mini/wdioLogo-Iphone13MiniPortrait17-375x812.png differ diff --git a/tests/lambdaTestBaseline/iphone_13_pro/fullPage-Iphone13ProLandscape16-390x844.png b/tests/lambdaTestBaseline/iphone_13_pro/fullPage-Iphone13ProLandscape16-390x844.png index ae33c7318..3cdeebd18 100644 Binary files a/tests/lambdaTestBaseline/iphone_13_pro/fullPage-Iphone13ProLandscape16-390x844.png and b/tests/lambdaTestBaseline/iphone_13_pro/fullPage-Iphone13ProLandscape16-390x844.png differ diff --git a/tests/lambdaTestBaseline/iphone_13_pro/screenshot-landscape-Iphone13ProPortrait16-390x844.png b/tests/lambdaTestBaseline/iphone_13_pro/screenshot-landscape-Iphone13ProPortrait16-390x844.png new file mode 100644 index 000000000..23ee3a6d2 Binary files /dev/null and b/tests/lambdaTestBaseline/iphone_13_pro/screenshot-landscape-Iphone13ProPortrait16-390x844.png differ diff --git a/tests/lambdaTestBaseline/iphone_13_pro/screenshot-portrait-Iphone13ProLandscape16-390x844.png b/tests/lambdaTestBaseline/iphone_13_pro/screenshot-portrait-Iphone13ProLandscape16-390x844.png new file mode 100644 index 000000000..54cc8670f Binary files /dev/null and b/tests/lambdaTestBaseline/iphone_13_pro/screenshot-portrait-Iphone13ProLandscape16-390x844.png differ diff --git a/tests/lambdaTestBaseline/iphone_13_pro/wdioLogo-Iphone13ProLandscape16-390x844.png b/tests/lambdaTestBaseline/iphone_13_pro/wdioLogo-Iphone13ProLandscape16-390x844.png index 559194ad4..d4e512f54 100644 Binary files a/tests/lambdaTestBaseline/iphone_13_pro/wdioLogo-Iphone13ProLandscape16-390x844.png and b/tests/lambdaTestBaseline/iphone_13_pro/wdioLogo-Iphone13ProLandscape16-390x844.png differ diff --git a/tests/lambdaTestBaseline/iphone_13_pro/wdioLogo-Iphone13ProPortrait16-390x844.png b/tests/lambdaTestBaseline/iphone_13_pro/wdioLogo-Iphone13ProPortrait16-390x844.png index 559194ad4..d4e512f54 100644 Binary files a/tests/lambdaTestBaseline/iphone_13_pro/wdioLogo-Iphone13ProPortrait16-390x844.png and b/tests/lambdaTestBaseline/iphone_13_pro/wdioLogo-Iphone13ProPortrait16-390x844.png differ diff --git a/tests/lambdaTestBaseline/iphone_14_pro/fullPage-Iphone14ProLandscape17-393x852.png b/tests/lambdaTestBaseline/iphone_14_pro/fullPage-Iphone14ProLandscape17-393x852.png index 4ee1ba49c..81701482a 100644 Binary files a/tests/lambdaTestBaseline/iphone_14_pro/fullPage-Iphone14ProLandscape17-393x852.png and b/tests/lambdaTestBaseline/iphone_14_pro/fullPage-Iphone14ProLandscape17-393x852.png differ diff --git a/tests/lambdaTestBaseline/iphone_14_pro/fullPage-Iphone14ProPortrait17-393x852.png b/tests/lambdaTestBaseline/iphone_14_pro/fullPage-Iphone14ProPortrait17-393x852.png index 20ad548dd..b22ccac0d 100644 Binary files a/tests/lambdaTestBaseline/iphone_14_pro/fullPage-Iphone14ProPortrait17-393x852.png and b/tests/lambdaTestBaseline/iphone_14_pro/fullPage-Iphone14ProPortrait17-393x852.png differ diff --git a/tests/lambdaTestBaseline/iphone_14_pro/screenshot-Iphone14ProLandscape17-393x852.png b/tests/lambdaTestBaseline/iphone_14_pro/screenshot-Iphone14ProLandscape17-393x852.png index 2edde8162..8f9f1ab76 100644 Binary files a/tests/lambdaTestBaseline/iphone_14_pro/screenshot-Iphone14ProLandscape17-393x852.png and b/tests/lambdaTestBaseline/iphone_14_pro/screenshot-Iphone14ProLandscape17-393x852.png differ diff --git a/tests/lambdaTestBaseline/iphone_14_pro/screenshot-Iphone14ProPortrait17-393x852.png b/tests/lambdaTestBaseline/iphone_14_pro/screenshot-Iphone14ProPortrait17-393x852.png index af64903ad..c71ff93fe 100644 Binary files a/tests/lambdaTestBaseline/iphone_14_pro/screenshot-Iphone14ProPortrait17-393x852.png and b/tests/lambdaTestBaseline/iphone_14_pro/screenshot-Iphone14ProPortrait17-393x852.png differ diff --git a/tests/lambdaTestBaseline/iphone_14_pro/screenshot-landscape-Iphone14ProPortrait17-393x852.png b/tests/lambdaTestBaseline/iphone_14_pro/screenshot-landscape-Iphone14ProPortrait17-393x852.png new file mode 100644 index 000000000..8f9f1ab76 Binary files /dev/null and b/tests/lambdaTestBaseline/iphone_14_pro/screenshot-landscape-Iphone14ProPortrait17-393x852.png differ diff --git a/tests/lambdaTestBaseline/iphone_14_pro/screenshot-portrait-Iphone14ProLandscape17-393x852.png b/tests/lambdaTestBaseline/iphone_14_pro/screenshot-portrait-Iphone14ProLandscape17-393x852.png new file mode 100644 index 000000000..c93206191 Binary files /dev/null and b/tests/lambdaTestBaseline/iphone_14_pro/screenshot-portrait-Iphone14ProLandscape17-393x852.png differ diff --git a/tests/lambdaTestBaseline/iphone_14_pro/wdioLogo-Iphone14ProLandscape17-393x852.png b/tests/lambdaTestBaseline/iphone_14_pro/wdioLogo-Iphone14ProLandscape17-393x852.png index 8ec6a4d64..d4e512f54 100644 Binary files a/tests/lambdaTestBaseline/iphone_14_pro/wdioLogo-Iphone14ProLandscape17-393x852.png and b/tests/lambdaTestBaseline/iphone_14_pro/wdioLogo-Iphone14ProLandscape17-393x852.png differ diff --git a/tests/lambdaTestBaseline/iphone_14_pro/wdioLogo-Iphone14ProPortrait17-393x852.png b/tests/lambdaTestBaseline/iphone_14_pro/wdioLogo-Iphone14ProPortrait17-393x852.png index 559194ad4..f1d8c1d4f 100644 Binary files a/tests/lambdaTestBaseline/iphone_14_pro/wdioLogo-Iphone14ProPortrait17-393x852.png and b/tests/lambdaTestBaseline/iphone_14_pro/wdioLogo-Iphone14ProPortrait17-393x852.png differ diff --git a/tests/lambdaTestBaseline/iphone_15_pro_max/fullPage-Iphone15ProMaxLandscape18-430x932.png b/tests/lambdaTestBaseline/iphone_15_pro_max/fullPage-Iphone15ProMaxLandscape18-430x932.png index f1d1adecb..809be45f9 100644 Binary files a/tests/lambdaTestBaseline/iphone_15_pro_max/fullPage-Iphone15ProMaxLandscape18-430x932.png and b/tests/lambdaTestBaseline/iphone_15_pro_max/fullPage-Iphone15ProMaxLandscape18-430x932.png differ diff --git a/tests/lambdaTestBaseline/iphone_15_pro_max/screenshot-Iphone15ProMaxLandscape18-430x932.png b/tests/lambdaTestBaseline/iphone_15_pro_max/screenshot-Iphone15ProMaxLandscape18-430x932.png index 045a90919..6dd6dbff8 100644 Binary files a/tests/lambdaTestBaseline/iphone_15_pro_max/screenshot-Iphone15ProMaxLandscape18-430x932.png and b/tests/lambdaTestBaseline/iphone_15_pro_max/screenshot-Iphone15ProMaxLandscape18-430x932.png differ diff --git a/tests/lambdaTestBaseline/iphone_15_pro_max/screenshot-Iphone15ProMaxPortrait18-430x932.png b/tests/lambdaTestBaseline/iphone_15_pro_max/screenshot-Iphone15ProMaxPortrait18-430x932.png index 986d95905..6205bb847 100644 Binary files a/tests/lambdaTestBaseline/iphone_15_pro_max/screenshot-Iphone15ProMaxPortrait18-430x932.png and b/tests/lambdaTestBaseline/iphone_15_pro_max/screenshot-Iphone15ProMaxPortrait18-430x932.png differ diff --git a/tests/lambdaTestBaseline/iphone_15_pro_max/screenshot-landscape-Iphone15ProMaxPortrait18-430x932.png b/tests/lambdaTestBaseline/iphone_15_pro_max/screenshot-landscape-Iphone15ProMaxPortrait18-430x932.png new file mode 100644 index 000000000..6dd6dbff8 Binary files /dev/null and b/tests/lambdaTestBaseline/iphone_15_pro_max/screenshot-landscape-Iphone15ProMaxPortrait18-430x932.png differ diff --git a/tests/lambdaTestBaseline/iphone_15_pro_max/screenshot-portrait-Iphone15ProMaxLandscape18-430x932.png b/tests/lambdaTestBaseline/iphone_15_pro_max/screenshot-portrait-Iphone15ProMaxLandscape18-430x932.png new file mode 100644 index 000000000..c61923049 Binary files /dev/null and b/tests/lambdaTestBaseline/iphone_15_pro_max/screenshot-portrait-Iphone15ProMaxLandscape18-430x932.png differ diff --git a/tests/lambdaTestBaseline/iphone_15_pro_max/wdioLogo-Iphone15ProMaxLandscape18-430x932.png b/tests/lambdaTestBaseline/iphone_15_pro_max/wdioLogo-Iphone15ProMaxLandscape18-430x932.png index 559194ad4..d4e512f54 100644 Binary files a/tests/lambdaTestBaseline/iphone_15_pro_max/wdioLogo-Iphone15ProMaxLandscape18-430x932.png and b/tests/lambdaTestBaseline/iphone_15_pro_max/wdioLogo-Iphone15ProMaxLandscape18-430x932.png differ diff --git a/tests/lambdaTestBaseline/iphone_15_pro_max/wdioLogo-Iphone15ProMaxPortrait18-430x932.png b/tests/lambdaTestBaseline/iphone_15_pro_max/wdioLogo-Iphone15ProMaxPortrait18-430x932.png index 559194ad4..d4e512f54 100644 Binary files a/tests/lambdaTestBaseline/iphone_15_pro_max/wdioLogo-Iphone15ProMaxPortrait18-430x932.png and b/tests/lambdaTestBaseline/iphone_15_pro_max/wdioLogo-Iphone15ProMaxPortrait18-430x932.png differ diff --git a/tests/lambdaTestBaseline/pixel_4/fullPage-EmulatorPixel4LandscapeNativeWebScreenshot11-652x309.png b/tests/lambdaTestBaseline/pixel_4/fullPage-EmulatorPixel4LandscapeNativeWebScreenshot11-652x309.png new file mode 100644 index 000000000..1fa29b28a Binary files /dev/null and b/tests/lambdaTestBaseline/pixel_4/fullPage-EmulatorPixel4LandscapeNativeWebScreenshot11-652x309.png differ diff --git a/tests/lambdaTestBaseline/pixel_4/fullPage-EmulatorPixel4LandscapeNativeWebScreenshot12-760x360.png b/tests/lambdaTestBaseline/pixel_4/fullPage-EmulatorPixel4LandscapeNativeWebScreenshot12-760x360.png index d24f7a5cd..b114532bf 100644 Binary files a/tests/lambdaTestBaseline/pixel_4/fullPage-EmulatorPixel4LandscapeNativeWebScreenshot12-760x360.png and b/tests/lambdaTestBaseline/pixel_4/fullPage-EmulatorPixel4LandscapeNativeWebScreenshot12-760x360.png differ diff --git a/tests/lambdaTestBaseline/pixel_4/fullPage-EmulatorPixel4LandscapeNativeWebScreenshot13-760x360.png b/tests/lambdaTestBaseline/pixel_4/fullPage-EmulatorPixel4LandscapeNativeWebScreenshot13-760x360.png index 171fee56d..b277906bc 100644 Binary files a/tests/lambdaTestBaseline/pixel_4/fullPage-EmulatorPixel4LandscapeNativeWebScreenshot13-760x360.png and b/tests/lambdaTestBaseline/pixel_4/fullPage-EmulatorPixel4LandscapeNativeWebScreenshot13-760x360.png differ diff --git a/tests/lambdaTestBaseline/pixel_4/fullPage-EmulatorPixel4PortraitNativeWebScreenshot11-309x652.png b/tests/lambdaTestBaseline/pixel_4/fullPage-EmulatorPixel4PortraitNativeWebScreenshot11-309x652.png index 105216106..93e4885b2 100644 Binary files a/tests/lambdaTestBaseline/pixel_4/fullPage-EmulatorPixel4PortraitNativeWebScreenshot11-309x652.png and b/tests/lambdaTestBaseline/pixel_4/fullPage-EmulatorPixel4PortraitNativeWebScreenshot11-309x652.png differ diff --git a/tests/lambdaTestBaseline/pixel_4/fullPage-EmulatorPixel4PortraitNativeWebScreenshot12-360x760.png b/tests/lambdaTestBaseline/pixel_4/fullPage-EmulatorPixel4PortraitNativeWebScreenshot12-360x760.png index 44639962d..d746f5b00 100644 Binary files a/tests/lambdaTestBaseline/pixel_4/fullPage-EmulatorPixel4PortraitNativeWebScreenshot12-360x760.png and b/tests/lambdaTestBaseline/pixel_4/fullPage-EmulatorPixel4PortraitNativeWebScreenshot12-360x760.png differ diff --git a/tests/lambdaTestBaseline/pixel_4/fullPage-EmulatorPixel4PortraitNativeWebScreenshot13-360x760.png b/tests/lambdaTestBaseline/pixel_4/fullPage-EmulatorPixel4PortraitNativeWebScreenshot13-360x760.png index 405c87cbf..d16dd7904 100644 Binary files a/tests/lambdaTestBaseline/pixel_4/fullPage-EmulatorPixel4PortraitNativeWebScreenshot13-360x760.png and b/tests/lambdaTestBaseline/pixel_4/fullPage-EmulatorPixel4PortraitNativeWebScreenshot13-360x760.png differ diff --git a/tests/lambdaTestBaseline/pixel_4/screenshot-landscape-EmulatorPixel4PortraitNativeWebScreenshot11-652x309.png b/tests/lambdaTestBaseline/pixel_4/screenshot-landscape-EmulatorPixel4PortraitNativeWebScreenshot11-652x309.png new file mode 100644 index 000000000..23e7b3cad Binary files /dev/null and b/tests/lambdaTestBaseline/pixel_4/screenshot-landscape-EmulatorPixel4PortraitNativeWebScreenshot11-652x309.png differ diff --git a/tests/lambdaTestBaseline/pixel_4/screenshot-landscape-EmulatorPixel4PortraitNativeWebScreenshot12-760x360.png b/tests/lambdaTestBaseline/pixel_4/screenshot-landscape-EmulatorPixel4PortraitNativeWebScreenshot12-760x360.png new file mode 100644 index 000000000..622bd5c15 Binary files /dev/null and b/tests/lambdaTestBaseline/pixel_4/screenshot-landscape-EmulatorPixel4PortraitNativeWebScreenshot12-760x360.png differ diff --git a/tests/lambdaTestBaseline/pixel_4/screenshot-landscape-EmulatorPixel4PortraitNativeWebScreenshot13-760x360.png b/tests/lambdaTestBaseline/pixel_4/screenshot-landscape-EmulatorPixel4PortraitNativeWebScreenshot13-760x360.png new file mode 100644 index 000000000..874476c9b Binary files /dev/null and b/tests/lambdaTestBaseline/pixel_4/screenshot-landscape-EmulatorPixel4PortraitNativeWebScreenshot13-760x360.png differ diff --git a/tests/lambdaTestBaseline/pixel_4/screenshot-portrait-EmulatorPixel4LandscapeNativeWebScreenshot11-309x652.png b/tests/lambdaTestBaseline/pixel_4/screenshot-portrait-EmulatorPixel4LandscapeNativeWebScreenshot11-309x652.png new file mode 100644 index 000000000..0fe2d4c4b Binary files /dev/null and b/tests/lambdaTestBaseline/pixel_4/screenshot-portrait-EmulatorPixel4LandscapeNativeWebScreenshot11-309x652.png differ diff --git a/tests/lambdaTestBaseline/pixel_4/screenshot-portrait-EmulatorPixel4LandscapeNativeWebScreenshot12-360x760.png b/tests/lambdaTestBaseline/pixel_4/screenshot-portrait-EmulatorPixel4LandscapeNativeWebScreenshot12-360x760.png new file mode 100644 index 000000000..99b8e60b4 Binary files /dev/null and b/tests/lambdaTestBaseline/pixel_4/screenshot-portrait-EmulatorPixel4LandscapeNativeWebScreenshot12-360x760.png differ diff --git a/tests/lambdaTestBaseline/pixel_4/screenshot-portrait-EmulatorPixel4LandscapeNativeWebScreenshot13-360x760.png b/tests/lambdaTestBaseline/pixel_4/screenshot-portrait-EmulatorPixel4LandscapeNativeWebScreenshot13-360x760.png new file mode 100644 index 000000000..4db99a3f2 Binary files /dev/null and b/tests/lambdaTestBaseline/pixel_4/screenshot-portrait-EmulatorPixel4LandscapeNativeWebScreenshot13-360x760.png differ diff --git a/tests/lambdaTestBaseline/pixel_9_pro/fullPage-EmulatorPixel9ProLandscapeNativeWebScreenshot14-952x427.png b/tests/lambdaTestBaseline/pixel_9_pro/fullPage-EmulatorPixel9ProLandscapeNativeWebScreenshot14-952x427.png index f0e2748e2..40a489522 100644 Binary files a/tests/lambdaTestBaseline/pixel_9_pro/fullPage-EmulatorPixel9ProLandscapeNativeWebScreenshot14-952x427.png and b/tests/lambdaTestBaseline/pixel_9_pro/fullPage-EmulatorPixel9ProLandscapeNativeWebScreenshot14-952x427.png differ diff --git a/tests/lambdaTestBaseline/pixel_9_pro/fullPage-EmulatorPixel9ProLandscapeNativeWebScreenshot15-952x427.png b/tests/lambdaTestBaseline/pixel_9_pro/fullPage-EmulatorPixel9ProLandscapeNativeWebScreenshot15-952x427.png index 561eb1f6e..c5d59715f 100644 Binary files a/tests/lambdaTestBaseline/pixel_9_pro/fullPage-EmulatorPixel9ProLandscapeNativeWebScreenshot15-952x427.png and b/tests/lambdaTestBaseline/pixel_9_pro/fullPage-EmulatorPixel9ProLandscapeNativeWebScreenshot15-952x427.png differ diff --git a/tests/lambdaTestBaseline/pixel_9_pro/fullPage-EmulatorPixel9ProPortraitNativeWebScreenshot14-427x952.png b/tests/lambdaTestBaseline/pixel_9_pro/fullPage-EmulatorPixel9ProPortraitNativeWebScreenshot14-427x952.png index 215de23ca..f2b9da36f 100644 Binary files a/tests/lambdaTestBaseline/pixel_9_pro/fullPage-EmulatorPixel9ProPortraitNativeWebScreenshot14-427x952.png and b/tests/lambdaTestBaseline/pixel_9_pro/fullPage-EmulatorPixel9ProPortraitNativeWebScreenshot14-427x952.png differ diff --git a/tests/lambdaTestBaseline/pixel_9_pro/fullPage-EmulatorPixel9ProPortraitNativeWebScreenshot15-427x952.png b/tests/lambdaTestBaseline/pixel_9_pro/fullPage-EmulatorPixel9ProPortraitNativeWebScreenshot15-427x952.png index 215de23ca..44715e41c 100644 Binary files a/tests/lambdaTestBaseline/pixel_9_pro/fullPage-EmulatorPixel9ProPortraitNativeWebScreenshot15-427x952.png and b/tests/lambdaTestBaseline/pixel_9_pro/fullPage-EmulatorPixel9ProPortraitNativeWebScreenshot15-427x952.png differ diff --git a/tests/lambdaTestBaseline/pixel_9_pro/screenshot-EmulatorPixel9ProPortraitNativeWebScreenshot15-427x952.png b/tests/lambdaTestBaseline/pixel_9_pro/screenshot-EmulatorPixel9ProPortraitNativeWebScreenshot15-427x952.png index 32b406bcf..8fac7cac2 100644 Binary files a/tests/lambdaTestBaseline/pixel_9_pro/screenshot-EmulatorPixel9ProPortraitNativeWebScreenshot15-427x952.png and b/tests/lambdaTestBaseline/pixel_9_pro/screenshot-EmulatorPixel9ProPortraitNativeWebScreenshot15-427x952.png differ diff --git a/tests/lambdaTestBaseline/pixel_9_pro/screenshot-landscape-EmulatorPixel9ProPortraitNativeWebScreenshot14-952x427.png b/tests/lambdaTestBaseline/pixel_9_pro/screenshot-landscape-EmulatorPixel9ProPortraitNativeWebScreenshot14-952x427.png new file mode 100644 index 000000000..1de1f0d7a Binary files /dev/null and b/tests/lambdaTestBaseline/pixel_9_pro/screenshot-landscape-EmulatorPixel9ProPortraitNativeWebScreenshot14-952x427.png differ diff --git a/tests/lambdaTestBaseline/pixel_9_pro/screenshot-landscape-EmulatorPixel9ProPortraitNativeWebScreenshot15-952x427.png b/tests/lambdaTestBaseline/pixel_9_pro/screenshot-landscape-EmulatorPixel9ProPortraitNativeWebScreenshot15-952x427.png new file mode 100644 index 000000000..3a103fe24 Binary files /dev/null and b/tests/lambdaTestBaseline/pixel_9_pro/screenshot-landscape-EmulatorPixel9ProPortraitNativeWebScreenshot15-952x427.png differ diff --git a/tests/lambdaTestBaseline/pixel_9_pro/screenshot-portrait-EmulatorPixel9ProLandscapeNativeWebScreenshot14-427x952.png b/tests/lambdaTestBaseline/pixel_9_pro/screenshot-portrait-EmulatorPixel9ProLandscapeNativeWebScreenshot14-427x952.png new file mode 100644 index 000000000..f2acc63fe Binary files /dev/null and b/tests/lambdaTestBaseline/pixel_9_pro/screenshot-portrait-EmulatorPixel9ProLandscapeNativeWebScreenshot14-427x952.png differ diff --git a/tests/lambdaTestBaseline/pixel_9_pro/screenshot-portrait-EmulatorPixel9ProLandscapeNativeWebScreenshot15-427x952.png b/tests/lambdaTestBaseline/pixel_9_pro/screenshot-portrait-EmulatorPixel9ProLandscapeNativeWebScreenshot15-427x952.png new file mode 100644 index 000000000..f6e4875f3 Binary files /dev/null and b/tests/lambdaTestBaseline/pixel_9_pro/screenshot-portrait-EmulatorPixel9ProLandscapeNativeWebScreenshot15-427x952.png differ diff --git a/tests/sauceLabsBaseline/google_pixel_4_xl_googleapi_emulator/app-forms-app-EmulatorGooglePixel4XLGoogleAPIPortrait11.0-1440x3040.png b/tests/sauceLabsBaseline/google_pixel_4_xl_googleapi_emulator/app-forms-app-EmulatorGooglePixel4XLGoogleAPIPortrait11.0-1440x3040.png new file mode 100644 index 000000000..7c309293c Binary files /dev/null and b/tests/sauceLabsBaseline/google_pixel_4_xl_googleapi_emulator/app-forms-app-EmulatorGooglePixel4XLGoogleAPIPortrait11.0-1440x3040.png differ diff --git a/tests/sauceLabsBaseline/google_pixel_4_xl_googleapi_emulator/app-forms-app-EmulatorGooglePixel4XLGoogleAPIPortrait12.0-1440x3040.png b/tests/sauceLabsBaseline/google_pixel_4_xl_googleapi_emulator/app-forms-app-EmulatorGooglePixel4XLGoogleAPIPortrait12.0-1440x3040.png new file mode 100644 index 000000000..0b8740fcf Binary files /dev/null and b/tests/sauceLabsBaseline/google_pixel_4_xl_googleapi_emulator/app-forms-app-EmulatorGooglePixel4XLGoogleAPIPortrait12.0-1440x3040.png differ diff --git a/tests/sauceLabsBaseline/google_pixel_4_xl_googleapi_emulator/app-forms-app-EmulatorGooglePixel4XLGoogleAPIPortrait13.0-1440x3040.png b/tests/sauceLabsBaseline/google_pixel_4_xl_googleapi_emulator/app-forms-app-EmulatorGooglePixel4XLGoogleAPIPortrait13.0-1440x3040.png new file mode 100644 index 000000000..dbf199796 Binary files /dev/null and b/tests/sauceLabsBaseline/google_pixel_4_xl_googleapi_emulator/app-forms-app-EmulatorGooglePixel4XLGoogleAPIPortrait13.0-1440x3040.png differ diff --git a/tests/sauceLabsBaseline/google_pixel_4_xl_googleapi_emulator/app-forms-app-EmulatorGooglePixel4XLGoogleAPIPortrait14.0-1440x3040.png b/tests/sauceLabsBaseline/google_pixel_4_xl_googleapi_emulator/app-forms-app-EmulatorGooglePixel4XLGoogleAPIPortrait14.0-1440x3040.png new file mode 100644 index 000000000..3f234085f Binary files /dev/null and b/tests/sauceLabsBaseline/google_pixel_4_xl_googleapi_emulator/app-forms-app-EmulatorGooglePixel4XLGoogleAPIPortrait14.0-1440x3040.png differ diff --git a/tests/sauceLabsBaseline/google_pixel_4_xl_googleapi_emulator/app-login-button-app-EmulatorGooglePixel4XLGoogleAPIPortrait11.0-1440x3040.png b/tests/sauceLabsBaseline/google_pixel_4_xl_googleapi_emulator/app-login-button-app-EmulatorGooglePixel4XLGoogleAPIPortrait11.0-1440x3040.png new file mode 100644 index 000000000..9007254ee Binary files /dev/null and b/tests/sauceLabsBaseline/google_pixel_4_xl_googleapi_emulator/app-login-button-app-EmulatorGooglePixel4XLGoogleAPIPortrait11.0-1440x3040.png differ diff --git a/tests/sauceLabsBaseline/google_pixel_4_xl_googleapi_emulator/app-login-button-app-EmulatorGooglePixel4XLGoogleAPIPortrait12.0-1440x3040.png b/tests/sauceLabsBaseline/google_pixel_4_xl_googleapi_emulator/app-login-button-app-EmulatorGooglePixel4XLGoogleAPIPortrait12.0-1440x3040.png new file mode 100644 index 000000000..9007254ee Binary files /dev/null and b/tests/sauceLabsBaseline/google_pixel_4_xl_googleapi_emulator/app-login-button-app-EmulatorGooglePixel4XLGoogleAPIPortrait12.0-1440x3040.png differ diff --git a/tests/sauceLabsBaseline/google_pixel_4_xl_googleapi_emulator/app-login-button-resized-app-EmulatorGooglePixel4XLGoogleAPIPortrait11.0-1440x3040.png b/tests/sauceLabsBaseline/google_pixel_4_xl_googleapi_emulator/app-login-button-resized-app-EmulatorGooglePixel4XLGoogleAPIPortrait11.0-1440x3040.png new file mode 100644 index 000000000..c0f3275d7 Binary files /dev/null and b/tests/sauceLabsBaseline/google_pixel_4_xl_googleapi_emulator/app-login-button-resized-app-EmulatorGooglePixel4XLGoogleAPIPortrait11.0-1440x3040.png differ diff --git a/tests/sauceLabsBaseline/google_pixel_4_xl_googleapi_emulator/app-login-button-resized-app-EmulatorGooglePixel4XLGoogleAPIPortrait12.0-1440x3040.png b/tests/sauceLabsBaseline/google_pixel_4_xl_googleapi_emulator/app-login-button-resized-app-EmulatorGooglePixel4XLGoogleAPIPortrait12.0-1440x3040.png new file mode 100644 index 000000000..c0f3275d7 Binary files /dev/null and b/tests/sauceLabsBaseline/google_pixel_4_xl_googleapi_emulator/app-login-button-resized-app-EmulatorGooglePixel4XLGoogleAPIPortrait12.0-1440x3040.png differ diff --git a/tests/sauceLabsBaseline/google_pixel_4_xl_googleapi_emulator/web-app-app-EmulatorGooglePixel4XLGoogleAPIPortrait13.0-412x869.png b/tests/sauceLabsBaseline/google_pixel_4_xl_googleapi_emulator/web-app-app-EmulatorGooglePixel4XLGoogleAPIPortrait13.0-412x869.png new file mode 100644 index 000000000..bb4478407 Binary files /dev/null and b/tests/sauceLabsBaseline/google_pixel_4_xl_googleapi_emulator/web-app-app-EmulatorGooglePixel4XLGoogleAPIPortrait13.0-412x869.png differ diff --git a/tests/sauceLabsBaseline/google_pixel_4_xl_googleapi_emulator/web-app-app-EmulatorGooglePixel4XLGoogleAPIPortrait14.0-412x869.png b/tests/sauceLabsBaseline/google_pixel_4_xl_googleapi_emulator/web-app-app-EmulatorGooglePixel4XLGoogleAPIPortrait14.0-412x869.png new file mode 100644 index 000000000..454749a47 Binary files /dev/null and b/tests/sauceLabsBaseline/google_pixel_4_xl_googleapi_emulator/web-app-app-EmulatorGooglePixel4XLGoogleAPIPortrait14.0-412x869.png differ diff --git a/tests/sauceLabsBaseline/google_pixel_4_xl_googleapi_emulator/web-app-app-EmulatorGooglePixel4XLGoogleAPIPortrait15.0-412x869.png b/tests/sauceLabsBaseline/google_pixel_4_xl_googleapi_emulator/web-app-app-EmulatorGooglePixel4XLGoogleAPIPortrait15.0-412x869.png new file mode 100644 index 000000000..8066ffc2c Binary files /dev/null and b/tests/sauceLabsBaseline/google_pixel_4_xl_googleapi_emulator/web-app-app-EmulatorGooglePixel4XLGoogleAPIPortrait15.0-412x869.png differ diff --git a/tests/sauceLabsBaseline/iphone_11_simulator/web-app-app-Iphone11Portrait15-414x896.png b/tests/sauceLabsBaseline/iphone_11_simulator/web-app-app-Iphone11Portrait15-414x896.png new file mode 100644 index 000000000..cf3c6c218 Binary files /dev/null and b/tests/sauceLabsBaseline/iphone_11_simulator/web-app-app-Iphone11Portrait15-414x896.png differ diff --git a/tests/sauceLabsBaseline/iphone_12_pro_max_simulator/web-app-app-Iphone12ProMaxPortrait16-428x926.png b/tests/sauceLabsBaseline/iphone_12_pro_max_simulator/web-app-app-Iphone12ProMaxPortrait16-428x926.png new file mode 100644 index 000000000..25134bc99 Binary files /dev/null and b/tests/sauceLabsBaseline/iphone_12_pro_max_simulator/web-app-app-Iphone12ProMaxPortrait16-428x926.png differ diff --git a/tests/sauceLabsBaseline/iphone_12_simulator/web-app-app-Iphone12Portrait16-390x844.png b/tests/sauceLabsBaseline/iphone_12_simulator/web-app-app-Iphone12Portrait16-390x844.png new file mode 100644 index 000000000..b6001fdd6 Binary files /dev/null and b/tests/sauceLabsBaseline/iphone_12_simulator/web-app-app-Iphone12Portrait16-390x844.png differ diff --git a/tests/sauceLabsBaseline/iphone_8_plus_simulator/web-app-app-Iphone8PlusPortrait14-414x736.png b/tests/sauceLabsBaseline/iphone_8_plus_simulator/web-app-app-Iphone8PlusPortrait14-414x736.png new file mode 100644 index 000000000..36a5cbbe4 Binary files /dev/null and b/tests/sauceLabsBaseline/iphone_8_plus_simulator/web-app-app-Iphone8PlusPortrait14-414x736.png differ diff --git a/tests/sauceLabsBaseline/iphone_8_simulator/web-app-app-Iphone8Portrait14-375x667.png b/tests/sauceLabsBaseline/iphone_8_simulator/web-app-app-Iphone8Portrait14-375x667.png new file mode 100644 index 000000000..4ada6c029 Binary files /dev/null and b/tests/sauceLabsBaseline/iphone_8_simulator/web-app-app-Iphone8Portrait14-375x667.png differ diff --git a/tests/sauceLabsBaseline/iphone_xs_simulator/web-app-app-IphoneXsPortrait15-375x812.png b/tests/sauceLabsBaseline/iphone_xs_simulator/web-app-app-IphoneXsPortrait15-375x812.png new file mode 100644 index 000000000..d967f8375 Binary files /dev/null and b/tests/sauceLabsBaseline/iphone_xs_simulator/web-app-app-IphoneXsPortrait15-375x812.png differ diff --git a/tests/specs/__snapshots__/desktop.ocr.spec.ts.snap b/tests/specs/__snapshots__/desktop.ocr.spec.ts.snap deleted file mode 100644 index a677a01ed..000000000 --- a/tests/specs/__snapshots__/desktop.ocr.spec.ts.snap +++ /dev/null @@ -1,19 +0,0 @@ -// Snapshot v1 - -exports[`@wdio/visual-service:ocr desktop > should get the position of matching text on the screen based on OCR on local 1`] = ` -{ - "bottom": 111.3037037037037, - "left": 1193.9851851851852, - "right": 1255.7081481481482, - "top": 98.14962962962963, -} -`; - -exports[`@wdio/visual-service:ocr desktop > should get the position of matching text on the screen based on OCR on local 2`] = ` -{ - "bottom": 110, - "left": 1180, - "right": 1241, - "top": 97, -} -`; diff --git a/tests/specs/desktop.ocr.spec.ts b/tests/specs/desktop.ocr.spec.ts index b889bdfb5..a162a78f9 100644 --- a/tests/specs/desktop.ocr.spec.ts +++ b/tests/specs/desktop.ocr.spec.ts @@ -23,10 +23,22 @@ describe('@wdio/visual-service:ocr desktop', () => { text: string, }) const matchesFilePath = /ocr\/desktop-\d+\.png/.test(elementPosition.filePath) - expect(elementPosition.dprPosition).toMatchSnapshot() + // We don't care about the exact position now, just that it returns a position + expect(elementPosition.dprPosition).toEqual(expect.objectContaining({ + left: expect.any(Number), + top: expect.any(Number), + right: expect.any(Number), + bottom: expect.any(Number), + })) + + expect(elementPosition.originalPosition).toEqual(expect.objectContaining({ + left: expect.any(Number), + top: expect.any(Number), + right: expect.any(Number), + bottom: expect.any(Number), + })) expect(matchesFilePath) expect(elementPosition.matchedString).toEqual(string) - expect(elementPosition.originalPosition).toMatchSnapshot() expect(elementPosition.score).toEqual(100) expect(elementPosition.searchValue).toEqual(string) }) diff --git a/tests/specs/mobile.app.spec.ts b/tests/specs/mobile.app.spec.ts index 6895230b5..5006b6a5d 100644 --- a/tests/specs/mobile.app.spec.ts +++ b/tests/specs/mobile.app.spec.ts @@ -6,12 +6,10 @@ import { driver } from '@wdio/globals' describe('@wdio/visual-service mobile app', () => { // Get the commands that need to be executed // 0 means all, otherwise it will only execute the commands that are specified - // @ts-ignore const wdioIcsCommands = driver.requestedCapabilities['wdio-ics:options'].commands - // @ts-ignore const deviceName = driver.requestedCapabilities.deviceName - // @ts-ignore const orientation = driver.requestedCapabilities.orientation + const platformVersion = driver.requestedCapabilities.platformVersion beforeEach(async () => { await relaunchApp() @@ -41,6 +39,25 @@ describe('@wdio/visual-service mobile app', () => { // We're accepting 0.05%, which is 500 pixels, to be a max difference expect(result < 0.05 ? 0 : result).toEqual(0) }) + + if (driver.isIOS || (driver.isAndroid && parseFloat(platformVersion) >= 13)) { + it(`should compare a webview screenshot successful for '${deviceName}' in ${orientation}-mode`, async () => { + await $('~Webview').click() + await driver.pause(2000) + await driver.switchContext({ title: /.*WebdriverIO.*/ }) + await driver.pause(2000) + await driver.url('https://guinea-pig.webdriver.io/image-compare.html') + const result = await browser.checkScreen('web-app', { + ignore: [$('.navbar__brand')], + hideElements: [$('.hero__title')] + } + ) as number + // Rest the context because the rest will be for native + await driver.switchContext('NATIVE_APP') + + await expect(result < 0.05 ? 0 : result).toEqual(0) + }) + } } if ( diff --git a/tests/specs/mobile.web.spec.ts b/tests/specs/mobile.web.spec.ts index 3e56c24a1..fe3829578 100644 --- a/tests/specs/mobile.web.spec.ts +++ b/tests/specs/mobile.web.spec.ts @@ -6,16 +6,11 @@ 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 - // @ts-ignore const wdioIcsCommands = driver.requestedCapabilities['wdio-ics:options'].commands - // @ts-ignore const deviceName = (driver.requestedCapabilities['lt:options'] || driver.requestedCapabilities).deviceName - // @ts-ignore const platformName = (driver.requestedCapabilities['lt:options'] || driver.requestedCapabilities).platformName.toLowerCase() === 'android' ? 'Android' : 'iOS' - // @ts-ignore const platformVersion = (driver.requestedCapabilities['lt:options'] || driver.requestedCapabilities).platformVersion - // @ts-ignore - const orientation = (driver.requestedCapabilities['lt:options'].deviceOrientation || driver.requestedCapabilities.orientation).toLowerCase() + const orientation = (driver.requestedCapabilities['lt:options']?.deviceOrientation || driver.requestedCapabilities.orientation).toLowerCase() beforeEach(async () => { await browser.url('') @@ -43,6 +38,19 @@ describe('@wdio/visual-service mobile web', () => { 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) + + const newOrientation = orientation.toUpperCase() === 'LANDSCAPE' ? 'PORTRAIT' : 'LANDSCAPE' + + await browser.pause(2000) + await browser.setOrientation(newOrientation) + await browser.pause(2000) + const newResult = await browser.checkScreen(`screenshot-${newOrientation.toLowerCase()}`) as number + if (newResult > 0 && result < 0.05) { + console.log(`\n\n\n'Screenshot for ${deviceName}' with ${platformName}:${platformVersion} in new orientation mode ${newOrientation} has a difference of ${result}%\n\n\n`) + } + // Before the expect we need to revert the orientation otherwise the next test will not start in the default orientation + await browser.setOrientation(orientation) + await expect(newResult < 0.05 ? 0 : newResult).toEqual(0) }) }