diff --git a/.gitignore b/.gitignore index d6b930e824..0d96d9e327 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,7 @@ browserstack.err !.yarn/releases !.yarn/sdks !.yarn/versions +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 7627b7c9da..227c576150 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -219,24 +219,17 @@ e2e: interruptible: true artifacts: when: always - paths: ['test-report/e2e/specs.log'] reports: junit: test-report/e2e/*.xml script: - yarn + - yarn build + - yarn build:app + - yarn playwright install chromium --with-deps - FORCE_COLOR=1 yarn test:e2e after_script: - node ./scripts/test/export-test-result.js e2e -e2e-developer-extension: - extends: - - .base-configuration - - .test-allowed-branches - interruptible: true - script: - - yarn - - yarn test:e2e:developer-extension - check-licenses: extends: - .base-configuration @@ -267,6 +260,7 @@ check-schemas: unit-bs: stage: browserstack + needs: ['unit'] extends: - .base-configuration - .bs-allowed-branches @@ -283,6 +277,7 @@ unit-bs: e2e-bs: stage: browserstack + needs: ['e2e'] extends: - .base-configuration - .bs-allowed-branches @@ -292,11 +287,13 @@ e2e-bs: timeout: 35 minutes artifacts: when: always - paths: ['test-report/e2e-bs/specs.log'] reports: junit: test-report/e2e-bs/*.xml script: - yarn + - yarn build + - yarn build:app + - yarn playwright install chromium-headless-shell --with-deps - FORCE_COLOR=1 ./scripts/test/ci-bs.sh test:e2e after_script: - node ./scripts/test/export-test-result.js e2e-bs diff --git a/LICENSE-3rdparty.csv b/LICENSE-3rdparty.csv index 20ecee5b51..fb52fa481d 100644 --- a/LICENSE-3rdparty.csv +++ b/LICENSE-3rdparty.csv @@ -12,6 +12,7 @@ prod,react,MIT,Copyright (c) Facebook, Inc. and its affiliates. prod,react-dom,MIT,Copyright (c) Facebook, Inc. and its affiliates. dev,@eslint/js,MIT,Copyright OpenJS Foundation and other contributors, dev,@jsdevtools/coverage-istanbul-loader,MIT,Copyright (c) 2015 James Messinger +dev,@playwright/test,Apache-2.0,Copyright Microsoft Corporation dev,@types/chrome,MIT,Copyright Microsoft Corporation dev,@types/connect-busboy,MIT,Copyright Microsoft Corporation dev,@types/cors,MIT,Copyright Microsoft Corporation @@ -22,12 +23,6 @@ dev,@types/node-forge,MIT,Copyright Microsoft Corporation dev,@types/pako,MIT,Copyright Microsoft Corporation dev,@types/react,MIT,Copyright Microsoft Corporation dev,@types/react-dom,MIT,Copyright Microsoft Corporation -dev,@wdio/browserstack-service,MIT,Copyright JS Foundation and other contributors -dev,@wdio/cli,MIT,Copyright JS Foundation and other contributors -dev,@wdio/jasmine-framework,MIT,Copyright JS Foundation and other contributors -dev,@wdio/junit-reporter,MIT,Copyright (c) OpenJS Foundation and other contributors -dev,@wdio/local-runner,MIT,Copyright JS Foundation and other contributors -dev,@wdio/spec-reporter,MIT,Copyright JS Foundation and other contributors dev,@webextension-toolbox/webpack-webextension-plugin,MIT,Copyright 2018 Henrik Wenz (handtrix@gmail.com) dev,ajv,MIT,Copyright 2015-2017 Evgeny Poberezkin dev,browserstack-local,MIT,Copyright 2016 BrowserStack diff --git a/package.json b/package.json index 38a8fb891c..dfa56d675e 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "postinstall": "scripts/cli init_submodule", "build": "lerna run build --stream", "build:bundle": "lerna run build:bundle --stream", + "build:app": "cd test/app && rm -rf node_modules && yarn && yarn build", "format": "prettier --check .", "lint": "scripts/cli lint .", "typecheck": "scripts/cli typecheck . && scripts/cli typecheck developer-extension", @@ -22,8 +23,8 @@ "test:script": "node --test --experimental-test-module-mocks './scripts/**/*.spec.*'", "test:unit:watch": "yarn test:unit --no-single-run", "test:unit:bs": "node ./scripts/test/bs-wrapper.js karma start test/unit/karma.bs.conf.js", - "test:e2e": "yarn build && (cd test/app && rm -rf node_modules && yarn && yarn build) && wdio test/e2e/wdio.local.conf.ts", - "test:e2e:bs": "yarn build && (cd test/app && rm -rf node_modules && yarn && yarn build) && node ./scripts/test/bs-wrapper.js wdio test/e2e/wdio.bs.conf.ts", + "test:e2e": "playwright test --config test/e2e/playwright.local.config.ts --project chromium", + "test:e2e:bs": "node ./scripts/test/bs-wrapper.js playwright test --config test/e2e/playwright.bs.config.ts", "test:e2e:developer-extension": "yarn build && wdio test/e2e/wdio.developer-extension.conf.ts", "test:compat:tsc": "scripts/cli check_typescript_compatibility", "test:compat:ssr": "scripts/cli check_server_side_rendering_compatibility", @@ -33,17 +34,13 @@ "devDependencies": { "@eslint/js": "9.19.0", "@jsdevtools/coverage-istanbul-loader": "3.0.5", + "@playwright/test": "1.49.0", "@types/chrome": "0.0.301", "@types/connect-busboy": "1.0.3", "@types/cors": "2.8.17", "@types/express": "4.17.21", "@types/jasmine": "3.10.18", - "@wdio/browserstack-service": "8.41.0", - "@wdio/cli": "8.41.0", - "@wdio/jasmine-framework": "8.41.0", - "@wdio/junit-reporter": "8.41.0", - "@wdio/local-runner": "8.41.0", - "@wdio/spec-reporter": "8.41.0", + "@types/node": "22.10.2", "ajv": "6.12.6", "browserstack-local": "1.5.6", "chrome-webstore-upload": "3.1.4", diff --git a/packages/rum/test/mutationPayloadValidator.ts b/packages/rum/test/mutationPayloadValidator.ts index f1cdf04a5f..fee146719f 100644 --- a/packages/rum/test/mutationPayloadValidator.ts +++ b/packages/rum/test/mutationPayloadValidator.ts @@ -1,3 +1,4 @@ +import { getGlobalObject } from '@datadog/browser-core' import { NodeType, IncrementalSource } from '../src/types' import type { SerializedNodeWithId, @@ -12,6 +13,9 @@ import type { import { findAllIncrementalSnapshots, findFullSnapshot } from './segments' import { findTextNode, findElementWithTagName, findElementWithIdAttribute } from './nodes' +// Should match both jasmine and playwright 'expect' functions +type Expect = (actual: any) => { toEqual(expected: any): void } + interface NodeSelector { // Select the first node with the given tag name from the initial full snapshot tag?: string @@ -121,7 +125,11 @@ export function createMutationPayloadValidator(initialDocument: SerializedNodeWi /** * Validates the mutation payload against the expected text, attribute, add and remove mutations. */ - validate: (payload: BrowserMutationPayload, expected: ExpectedMutationsPayload) => { + validate: ( + payload: BrowserMutationPayload, + expected: ExpectedMutationsPayload, + { expect = getGlobalObject().expect }: { expect?: Expect } = {} + ) => { payload = removeUndefinedValues(payload) expect(payload.adds).toEqual( @@ -201,18 +209,23 @@ export function createMutationPayloadValidator(initialDocument: SerializedNodeWi * Validate the first and only mutation record of a segment against the expected text, attribute, * add and remove mutations. */ -export function createMutationPayloadValidatorFromSegment(segment: BrowserSegment) { +export function createMutationPayloadValidatorFromSegment(segment: BrowserSegment, options?: { expect?: Expect }) { const fullSnapshot = findFullSnapshot(segment)! - expect(fullSnapshot).toBeTruthy() + if (!fullSnapshot) { + throw new Error('Full snapshot not found') + } const mutations = findAllIncrementalSnapshots(segment, IncrementalSource.Mutation) as Array<{ data: BrowserMutationData }> - expect(mutations.length).toBe(1) + if (mutations.length !== 1) { + throw new Error(`Expected 1 mutation, found ${mutations.length}`) + } const mutationPayloadValidator = createMutationPayloadValidator(fullSnapshot.data.node) return { ...mutationPayloadValidator, - validate: (expected: ExpectedMutationsPayload) => mutationPayloadValidator.validate(mutations[0].data, expected), + validate: (expected: ExpectedMutationsPayload) => + mutationPayloadValidator.validate(mutations[0].data, expected, options), } } diff --git a/scripts/test/bs-wrapper.js b/scripts/test/bs-wrapper.js index 4252af3ac7..fb5e6c5fdf 100644 --- a/scripts/test/bs-wrapper.js +++ b/scripts/test/bs-wrapper.js @@ -15,6 +15,7 @@ // to in the future. const spawn = require('child_process').spawn +const browserStack = require('browserstack-local') const { printLog, runMain, timeout, printError } = require('../lib/executionUtils') const { command } = require('../lib/command') const { browserStackRequest } = require('../lib/bsUtils') @@ -23,13 +24,23 @@ const AVAILABILITY_CHECK_DELAY = 30_000 const NO_OUTPUT_TIMEOUT = 5 * 60_000 const BS_BUILD_URL = 'https://api.browserstack.com/automate/builds.json?status=running' +const bsLocal = new browserStack.Local() + runMain(async () => { if (command`git tag --points-at HEAD`.run()) { printLog('Skip bs execution on tags') return } + + if (!process.env.BS_USERNAME || !process.env.BS_ACCESS_KEY) { + printError('Missing Browserstack credentials (BS_ACCESS_KEY and BS_USERNAME env variables)') + return + } + await waitForAvailability() + await startBsLocal() const isSuccess = await runTests() + await stopBsLocal() process.exit(isSuccess ? 0 : 1) }) @@ -44,6 +55,38 @@ async function hasRunningBuild() { return (await browserStackRequest(BS_BUILD_URL)).length > 0 } +function startBsLocal() { + printLog('Starting BrowserStackLocal...') + + return new Promise((resolve) => { + bsLocal.start( + { + key: process.env.BS_ACCESS_KEY, + forceLocal: true, + forceKill: true, + onlyAutomate: true, + }, + (error) => { + if (error) { + printError('Failed to start BrowserStackLocal:', error) + process.exit(1) + } + printLog('BrowserStackLocal started', bsLocal.isRunning()) + resolve() + } + ) + }) +} + +function stopBsLocal() { + return new Promise((resolve) => { + bsLocal.stop(() => { + printLog('BrowserStackLocal stopped') + resolve() + }) + }) +} + function runTests() { return new Promise((resolve) => { const [command, ...args] = process.argv.slice(2) diff --git a/test/browsers.conf.d.ts b/test/browsers.conf.d.ts index b6cb5a1ed6..572b50de9f 100644 --- a/test/browsers.conf.d.ts +++ b/test/browsers.conf.d.ts @@ -1,8 +1,8 @@ -export type BrowserConfigurations = Array<{ +export type BrowserConfiguration = { sessionName: string name: string version?: string os: string osVersion: string device?: string -}> +} diff --git a/test/e2e/browsers.conf.js b/test/e2e/browsers.conf.js index 65a3b1e889..a6662f644a 100644 --- a/test/e2e/browsers.conf.js +++ b/test/e2e/browsers.conf.js @@ -1,34 +1,33 @@ // Capabilities: https://www.browserstack.com/automate/capabilities /** - * @type {import('../browsers.conf').BrowserConfigurations} + * @type {Array} */ const browserConfigurations = [ { sessionName: 'Edge', - name: 'Edge', + name: 'edge', version: '100.0', os: 'Windows', osVersion: '11', }, { sessionName: 'Firefox', - name: 'Firefox', - version: '91.0', + name: 'playwright-firefox', + version: '119', os: 'Windows', osVersion: '11', }, { sessionName: 'Safari desktop', - name: 'Safari', - version: '14.1', + name: 'playwright-webkit', + version: '17.4', os: 'OS X', osVersion: 'Big Sur', }, { sessionName: 'Chrome mobile', name: 'chrome', - os: 'android', osVersion: '12.0', device: 'Google Pixel 6 Pro', }, diff --git a/test/e2e/lib/framework/createTest.ts b/test/e2e/lib/framework/createTest.ts index e91cb6ba9b..75aba91fae 100644 --- a/test/e2e/lib/framework/createTest.ts +++ b/test/e2e/lib/framework/createTest.ts @@ -1,15 +1,21 @@ import type { LogsInitConfiguration } from '@datadog/browser-logs' import type { RumInitConfiguration } from '@datadog/browser-rum-core' import { DefaultPrivacyLevel } from '@datadog/browser-rum' +import type { AndroidDevice, BrowserContext, Page, PlaywrightWorkerOptions } from '@playwright/test' +import { test, expect } from '@playwright/test' +import { connectToAndroidDevice } from '../helpers/playwright' +import type { Tag } from '../helpers/tags' +import { addTag, addBrowserConfigurationTags } from '../helpers/tags' import { getRunId } from '../../../envUtils' -import { deleteAllCookies, getBrowserName, withBrowserLogs } from '../helpers/browser' +import type { BrowserLog } from '../helpers/browser' +import { BrowserLogsManager, deleteAllCookies, sendXhr } from '../helpers/browser' import { APPLICATION_ID, CLIENT_TOKEN } from '../helpers/configuration' import { validateRumFormat } from '../helpers/validation' +import type { BrowserConfiguration } from '../../../browsers.conf' import { IntakeRegistry } from './intakeRegistry' import { flushEvents } from './flushEvents' import type { Servers } from './httpServers' import { getTestServers, waitForServersIdle } from './httpServers' -import { log } from './logger' import type { SetupFactory, SetupOptions } from './pageSetups' import { DEFAULT_SETUPS, npmSetup } from './pageSetups' import { createIntakeServerApp } from './serverApps/intake' @@ -47,9 +53,17 @@ interface TestContext { crossOriginUrl: string intakeRegistry: IntakeRegistry servers: Servers + page: Page + browserContext: BrowserContext + browserName: PlaywrightWorkerOptions['browserName'] + withBrowserLogs: (cb: (logs: BrowserLog[]) => void) => void + flushBrowserLogs: () => void + flushEvents: () => Promise + deleteAllCookies: () => Promise + sendXhr: (url: string, headers?: string[][]) => Promise } -type TestRunner = (testContext: TestContext) => Promise +type TestRunner = (testContext: TestContext) => Promise | void class TestBuilder { private rumConfiguration: RumInitConfiguration | undefined = undefined @@ -136,7 +150,7 @@ class TestBuilder { } if (this.alsoRunWithRumSlim) { - describe(this.title, () => { + test.describe(this.title, () => { declareTestsForSetups('rum', setups, setupOptions, runner) declareTestsForSetups( 'rum-slim', @@ -159,11 +173,6 @@ class TestBuilder { } } -interface ItResult { - getFullName(): string -} -declare function it(expectation: string, assertion?: jasmine.ImplementationCallback, timeout?: number): ItResult - function declareTestsForSetups( title: string, setups: Array<{ factory: SetupFactory; name?: string }>, @@ -171,7 +180,7 @@ function declareTestsForSetups( runner: TestRunner ) { if (setups.length > 1) { - describe(title, () => { + test.describe(title, () => { for (const { name, factory } of setups) { declareTest(name!, setupOptions, factory, runner) } @@ -182,53 +191,124 @@ function declareTestsForSetups( } function declareTest(title: string, setupOptions: SetupOptions, factory: SetupFactory, runner: TestRunner) { - const spec = it(title, async () => { - log(`Start '${spec.getFullName()}' in ${getBrowserName()}`) - setupOptions.context.test_name = spec.getFullName() + test(title, async ({ page, context, browserName, playwright }) => { + const projectMetadata = test.info().project.metadata as BrowserConfiguration + + addTag('browserName' as any as Tag, browserName) + addBrowserConfigurationTags(projectMetadata) + + const title = test.info().titlePath.join(' > ') + setupOptions.context.test_name = title const servers = await getTestServers() + const browserLogs = new BrowserLogsManager() + + let _page = page + let _context = context + let _device: AndroidDevice | undefined + + if (projectMetadata.device) { + const { page, context, device } = await connectToAndroidDevice(playwright._android, projectMetadata) + _page = page + _context = context + _device = device + } - const testContext = createTestContext(servers, setupOptions) + const testContext = createTestContext(servers, _page, _context, browserLogs, browserName, setupOptions) servers.intake.bindServerApp(createIntakeServerApp(testContext.intakeRegistry)) const setup = factory(setupOptions, servers) servers.base.bindServerApp(createMockServerApp(servers, setup)) servers.crossOrigin.bindServerApp(createMockServerApp(servers, setup)) - await setUpTest(testContext) + await setUpTest(browserLogs, testContext) try { await runner(testContext) + tearDownPassedTest(testContext) } finally { - await tearDownTest(testContext) - log(`End '${spec.getFullName()}'`) + await tearDownTest(testContext, _device) } }) } -function createTestContext(servers: Servers, { basePath }: SetupOptions): TestContext { +function createTestContext( + servers: Servers, + page: Page, + browserContext: BrowserContext, + browserLogsManager: BrowserLogsManager, + browserName: TestContext['browserName'], + { basePath }: SetupOptions +): TestContext { return { baseUrl: servers.base.url + basePath, crossOriginUrl: servers.crossOrigin.url, intakeRegistry: new IntakeRegistry(), servers, + page, + browserContext, + browserName, + withBrowserLogs: (cb: (logs: BrowserLog[]) => void) => { + try { + cb(browserLogsManager.get()) + } finally { + browserLogsManager.clear() + } + }, + flushBrowserLogs: () => browserLogsManager.clear(), + flushEvents: () => flushEvents(page), + deleteAllCookies: () => deleteAllCookies(browserContext), + sendXhr: (url: string, headers?: string[][]) => sendXhr(page, url, headers), } } -async function setUpTest({ baseUrl }: TestContext) { - await browser.url(baseUrl) +async function setUpTest(browserLogsManager: BrowserLogsManager, { baseUrl, page, browserContext }: TestContext) { + browserContext.on('console', (msg) => { + browserLogsManager.add({ + level: msg.type() as BrowserLog['level'], + message: msg.text(), + source: 'console', + timestamp: Date.now(), + }) + }) + + browserContext.on('weberror', (webError) => { + browserLogsManager.add({ + level: 'error', + message: webError.error().message, + source: 'console', + timestamp: Date.now(), + }) + }) + + await page.goto(baseUrl) await waitForServersIdle() } -async function tearDownTest({ intakeRegistry }: TestContext) { - await flushEvents() - expect(intakeRegistry.telemetryErrorEvents).toEqual([]) +function tearDownPassedTest({ intakeRegistry, withBrowserLogs }: TestContext) { + expect(intakeRegistry.telemetryErrorEvents).toHaveLength(0) validateRumFormat(intakeRegistry.rumEvents) - await withBrowserLogs((logs) => { - logs.forEach((browserLog) => { - log(`Browser ${browserLog.source}: ${browserLog.level} ${browserLog.message}`) - }) - expect(logs.filter((l) => (l as any).level === 'SEVERE')).toEqual([]) + withBrowserLogs((logs) => { + expect(logs.filter((log) => log.level === 'error')).toHaveLength(0) }) +} + +async function tearDownTest({ flushEvents, deleteAllCookies, page }: TestContext, device?: AndroidDevice) { + await flushEvents() await deleteAllCookies() + + if (device) { + await page.close() + await device.close() + } + + const skipReason = test.info().annotations.find((annotation) => annotation.type === 'skip')?.description + if (skipReason) { + addTag('skip', skipReason) + } + + const fixmeReason = test.info().annotations.find((annotation) => annotation.type === 'fixme')?.description + if (fixmeReason) { + addTag('fixme', fixmeReason) + } } diff --git a/test/e2e/lib/framework/flushEvents.ts b/test/e2e/lib/framework/flushEvents.ts index f2181c6cb3..39c6d39874 100644 --- a/test/e2e/lib/framework/flushEvents.ts +++ b/test/e2e/lib/framework/flushEvents.ts @@ -1,8 +1,9 @@ +import type { Page } from '@playwright/test' import { getTestServers, waitForServersIdle } from './httpServers' import { waitForRequests } from './waitForRequests' -export async function flushEvents() { - await waitForRequests() +export async function flushEvents(page: Page) { + await waitForRequests(page) const servers = await getTestServers() @@ -20,6 +21,6 @@ export async function flushEvents() { // The issue mainly occurs with local e2e tests (not browserstack), because the network latency is // very low (same machine), so the request resolves very quickly. In real life conditions, this // issue is mitigated, because requests will likely take a few milliseconds to reach the server. - await browser.url(`${servers.base.url}/ok?duration=200`) + await page.goto(`${servers.base.url}/ok?duration=200`) await waitForServersIdle() } diff --git a/test/e2e/lib/framework/httpServers.ts b/test/e2e/lib/framework/httpServers.ts index f05c5106c4..4c8e250e61 100644 --- a/test/e2e/lib/framework/httpServers.ts +++ b/test/e2e/lib/framework/httpServers.ts @@ -1,7 +1,6 @@ import * as http from 'http' import type { AddressInfo } from 'net' import { getIp } from '../../../envUtils' -import { log } from './logger' const MAX_SERVER_CREATION_RETRY = 5 // Not all port are available with BrowserStack, see https://www.browserstack.com/question/664 @@ -57,17 +56,6 @@ async function createServer(): Promise> { } }) - server.on('request', (req: http.IncomingMessage, res: http.ServerResponse) => { - let body = '' - req.on('data', (chunk) => { - body += chunk - }) - res.on('close', () => { - const requestUrl = `${req.headers.host!}${req.url!}` - log(`${req.method!} ${requestUrl} ${res.statusCode}${body ? `\n${body}` : ''}`) - }) - }) - return { bindServerApp(newServerApp: App) { serverApp = newServerApp diff --git a/test/e2e/lib/framework/logger.ts b/test/e2e/lib/framework/logger.ts deleted file mode 100644 index 91880f9de2..0000000000 --- a/test/e2e/lib/framework/logger.ts +++ /dev/null @@ -1,14 +0,0 @@ -import * as fs from 'fs' -import { inspect } from 'util' - -const logsPath = (browser.options as WebdriverIO.Config & { logsPath: string }).logsPath -const stream: { write(s: string): void } = logsPath ? fs.createWriteStream(logsPath, { flags: 'a' }) : process.stdout - -export function log(...args: any[]) { - const prefix = `[${process.pid}] ${new Date().toISOString()}` - stream.write(`${prefix}: ${formatArgs(args)}\n`) -} - -function formatArgs(args: any[]) { - return args.map((arg) => (typeof arg === 'string' ? arg : inspect(arg))).join(' ') -} diff --git a/test/e2e/lib/framework/pageSetups.ts b/test/e2e/lib/framework/pageSetups.ts index 0645200b44..e08ab71da2 100644 --- a/test/e2e/lib/framework/pageSetups.ts +++ b/test/e2e/lib/framework/pageSetups.ts @@ -21,10 +21,7 @@ export interface SetupOptions { export type SetupFactory = (options: SetupOptions, servers: Servers) => string -const isBrowserStack = - 'services' in browser.options && - browser.options.services && - browser.options.services.some((service) => (Array.isArray(service) ? service[0] : service) === 'browserstack') +const isBrowserStack = process.env.BS_USERNAME && process.env.BS_ACCESS_KEY const isContinuousIntegration = Boolean(process.env.CI_JOB_ID) diff --git a/test/e2e/lib/framework/serverApps/mock.ts b/test/e2e/lib/framework/serverApps/mock.ts index 6e748345c4..a18f9c824a 100644 --- a/test/e2e/lib/framework/serverApps/mock.ts +++ b/test/e2e/lib/framework/serverApps/mock.ts @@ -74,6 +74,7 @@ export function createMockServerApp(servers: Servers, setup: string): MockServer }) app.get('/ok', (req, res) => { + res.header('Content-Type', 'text/plain') if (req.query['timing-allow-origin'] === 'true') { res.set('Timing-Allow-Origin', '*') } diff --git a/test/e2e/lib/framework/waitForRequests.ts b/test/e2e/lib/framework/waitForRequests.ts index ffe560d8ad..03b302b3f6 100644 --- a/test/e2e/lib/framework/waitForRequests.ts +++ b/test/e2e/lib/framework/waitForRequests.ts @@ -1,3 +1,4 @@ +import type { Page } from '@playwright/test' import { waitForServersIdle } from './httpServers' /** @@ -10,11 +11,14 @@ import { waitForServersIdle } from './httpServers' * As a workaround, this function delays the `waitForServersIdle()` call by doing a browser * roundtrip, ensuring requests have plenty of time to reach the local server. */ -export async function waitForRequests() { - await browser.executeAsync((done) => - setTimeout(() => { - done(undefined) - }, 200) +export async function waitForRequests(page: Page) { + await page.evaluate( + () => + new Promise((resolve) => { + setTimeout(() => { + resolve(undefined) + }, 200) + }) ) await waitForServersIdle() } diff --git a/test/e2e/lib/helpers/browser.ts b/test/e2e/lib/helpers/browser.ts index 260d098f19..f2a6753fc2 100644 --- a/test/e2e/lib/helpers/browser.ts +++ b/test/e2e/lib/helpers/browser.ts @@ -1,126 +1,69 @@ -import * as os from 'os' +import type { BrowserContext, Page } from '@playwright/test' +import { addTag } from './tags' -// To keep tests sane, ensure we got a fixed list of possible platforms and browser names. -const validPlatformNames = ['windows', 'macos', 'linux', 'ios', 'android'] as const -const validBrowserNames = ['edge', 'safari', 'chrome', 'firefox'] as const - -export function getBrowserName(): (typeof validBrowserNames)[number] { - const capabilities = browser.capabilities - - // Look for the browser name in capabilities. It should always be there as long as we don't change - // the browser capabilities format. - if (!('browserName' in capabilities) || typeof capabilities.browserName !== 'string') { - throw new Error("Can't get browser name (no browser name)") - } - let browserName = capabilities.browserName.toLowerCase() - if (browserName === 'msedge') { - browserName = 'edge' - } else if (browserName === 'chrome-headless-shell') { - browserName = 'chrome' - } - if (!includes(validBrowserNames, browserName)) { - throw new Error(`Can't get browser name (invalid browser name ${browserName})`) - } - - return browserName +export interface BrowserLog { + level: 'log' | 'debug' | 'info' | 'error' | 'warning' + message: string + source: string + timestamp: number } -export function getPlatformName(): (typeof validPlatformNames)[number] { - const capabilities = browser.capabilities +export class BrowserLogsManager { + private logs: BrowserLog[] = [] - let platformName: string - if ('bstack:options' in capabilities && capabilities['bstack:options']) { - // Look for the platform name in browserstack options. It might not be always there, for example - // when we run the test locally. This should be adjusted when we are changing the browser - // capabilities format. - platformName = (capabilities['bstack:options'] as any).os - } else { - // The test is run locally, use the local os name - platformName = os.type() - } - - platformName = platformName.toLowerCase() - if (/^(mac ?os|os ?x|mac ?os ?x|darwin)$/.test(platformName)) { - platformName = 'macos' - } else if (platformName === 'windows_nt') { - platformName = 'windows' - } - if (!includes(validPlatformNames, platformName)) { - throw new Error(`Can't get platform name (invalid platform name ${platformName})`) + add(log: BrowserLog) { + this.logs.push(log) } - return platformName -} - -function includes(list: readonly T[], item: unknown): item is T { - return list.includes(item as any) -} + get() { + const filteredLogs = this.logs.filter((log) => !log.message.includes('Ignoring unsupported entryTypes: ')) -interface BrowserLog { - level: string - message: string - source: string - timestamp: number -} + if (filteredLogs.length !== this.logs.length) { + // FIXME: fix this at the perfomance observer level as it is visible to customers + // It used to pass before because it was only happening in Firefox but wdio io did not support console logs for FF + addTag('fixme', 'Unnexpected Console log message: "Ignoring unsupported entryTypes: *"') + } -export async function withBrowserLogs(fn: (logs: BrowserLog[]) => void) { - // browser.getLogs is not defined when using a remote webdriver service. We should find an - // alternative at some point. - // https://github.com/webdriverio/webdriverio/issues/4275 - if (browser.getLogs) { - const logs = (await browser.getLogs('browser')) as BrowserLog[] - fn(logs) + return filteredLogs } -} -export async function flushBrowserLogs() { - await withBrowserLogs(() => { - // Ignore logs - }) + clear() { + this.logs = [] + } } -// wdio method does not work for some browsers -export function deleteAllCookies() { - return browser.execute(() => { - const cookies = document.cookie.split(';') - for (const cookie of cookies) { - const eqPos = cookie.indexOf('=') - const name = eqPos > -1 ? cookie.substr(0, eqPos) : cookie - document.cookie = `${name}=;expires=Thu, 01 Jan 1970 00:00:00 GMT;samesite=strict` - } - }) +export function deleteAllCookies(context: BrowserContext) { + return context.clearCookies() } -export function setCookie(name: string, value: string, expiresDelay: number = 0) { - return browser.execute( - (name, value, expiresDelay) => { +export function setCookie(page: Page, name: string, value: string, expiresDelay: number = 0) { + return page.evaluate( + ({ name, value, expiresDelay }: { name: string; value: string; expiresDelay: number }) => { const expires = new Date(Date.now() + expiresDelay).toUTCString() document.cookie = `${name}=${value};expires=${expires};` }, - name, - value, - expiresDelay + { name, value, expiresDelay } ) } -export async function sendXhr(url: string, headers: string[][] = []): Promise { +export async function sendXhr(page: Page, url: string, headers: string[][] = []): Promise { type State = { state: 'success'; response: string } | { state: 'error' } - const result: State = await browser.executeAsync( - (url, headers, done) => { - const xhr = new XMLHttpRequest() - let state: State = { state: 'error' } - xhr.addEventListener('load', () => { - state = { state: 'success', response: xhr.response as string } - }) - xhr.addEventListener('loadend', () => done(state)) - xhr.open('GET', url) - headers.forEach((header) => xhr.setRequestHeader(header[0], header[1])) - xhr.send() - }, - url, - headers + const result: State = await page.evaluate( + ([url, headers]) => + new Promise((resolve) => { + const xhr = new XMLHttpRequest() + let state: State = { state: 'error' } + xhr.addEventListener('load', () => { + state = { state: 'success', response: xhr.response as string } + }) + xhr.addEventListener('loadend', () => resolve(state)) + xhr.open('GET', url) + headers.forEach((header) => xhr.setRequestHeader(header[0], header[1])) + xhr.send() + }), + [url, headers] as const ) if (result.state === 'error') { diff --git a/test/e2e/lib/helpers/playwright.ts b/test/e2e/lib/helpers/playwright.ts new file mode 100644 index 0000000000..4afa3854f3 --- /dev/null +++ b/test/e2e/lib/helpers/playwright.ts @@ -0,0 +1,59 @@ +import type { Android, PlaywrightWorkerOptions } from '@playwright/test' +import type { BrowserConfiguration } from '../../../browsers.conf' +import { getBuildInfos } from '../../../envUtils' + +export function getBrowserName(name: string): PlaywrightWorkerOptions['browserName'] { + if (name.includes('firefox')) { + return 'firefox' + } + + if (name.includes('webkit')) { + return 'webkit' + } + + return 'chromium' +} + +export function getEncodedCapabilities(configuration: BrowserConfiguration) { + return encodeURIComponent(JSON.stringify(getCapabilities(configuration))) +} + +// see: https://www.browserstack.com/docs/automate/playwright/playwright-capabilities +function getCapabilities(configuration: BrowserConfiguration) { + const capabilities: Record = { + os: configuration.os, + os_version: configuration.osVersion, + browser: configuration.name, + browser_version: configuration.version, + 'browserstack.username': process.env.BS_USERNAME, + 'browserstack.accessKey': process.env.BS_ACCESS_KEY, + project: 'browser sdk e2e', + build: getBuildInfos(), + name: configuration.sessionName, + 'browserstack.local': true, + 'browserstack.playwrightVersion': '1.latest', + 'client.playwrightVersion': '1.latest', + 'browserstack.debug': false, + 'browserstack.console': 'info', + 'browserstack.networkLogs': false, + 'browserstack.interactiveDebugging': false, + } + + if (configuration.device) { + capabilities.deviceName = configuration.device + capabilities.realMobile = true + } + + return capabilities +} + +export async function connectToAndroidDevice(android: Android, configuration: BrowserConfiguration) { + const device = await android.connect( + `wss://cdp.browserstack.com/playwright?caps=${getEncodedCapabilities(configuration)}` + ) + await device.shell('am force-stop com.android.chrome') + const context = await device.launchBrowser() + const page = await context.newPage() + + return { page, context, device } +} diff --git a/test/e2e/lib/helpers/session.ts b/test/e2e/lib/helpers/session.ts index ada8a60a0a..3d978b98f7 100644 --- a/test/e2e/lib/helpers/session.ts +++ b/test/e2e/lib/helpers/session.ts @@ -1,31 +1,34 @@ import { SESSION_STORE_KEY, SESSION_TIME_OUT_DELAY } from '@datadog/browser-core' import type { SessionState } from '@datadog/browser-core' +import type { BrowserContext, Page } from '@playwright/test' +import { expect } from '@playwright/test' + import { setCookie } from './browser' -export async function renewSession() { - await expireSession() - const documentElement = await $('html') +export async function renewSession(page: Page, browserContext: BrowserContext) { + await expireSession(page, browserContext) + const documentElement = page.locator('html') await documentElement.click() - expect((await findSessionCookie())?.isExpired).not.toEqual('1') + expect((await findSessionCookie(browserContext))?.isExpired).not.toEqual('1') } -export async function expireSession() { +export async function expireSession(page: Page, browserContext: BrowserContext) { // mock expire session with anonymous id - const cookies = await browser.getCookies(SESSION_STORE_KEY) + const cookies = await browserContext.cookies() const anonymousId = cookies[0]?.value.match(/aid=[a-z0-9]+/) const expireCookie = `isExpired=1&${anonymousId && anonymousId[0]}` - await setCookie(SESSION_STORE_KEY, expireCookie, SESSION_TIME_OUT_DELAY) + await setCookie(page, SESSION_STORE_KEY, expireCookie, SESSION_TIME_OUT_DELAY) - expect((await findSessionCookie())?.isExpired).toEqual('1') + expect((await findSessionCookie(browserContext))?.isExpired).toEqual('1') // Cookies are cached for 1s, wait until the cache expires - await browser.pause(1100) + await page.waitForTimeout(1100) } -export async function findSessionCookie() { - const cookies = await browser.getCookies(SESSION_STORE_KEY) +export async function findSessionCookie(browserContext: BrowserContext) { + const cookies = await browserContext.cookies() // In some case, the session cookie is returned but with an empty value. Let's consider it expired // in this case. const rawValue = cookies[0]?.value diff --git a/test/e2e/lib/helpers/tags.ts b/test/e2e/lib/helpers/tags.ts new file mode 100644 index 0000000000..499d56ae99 --- /dev/null +++ b/test/e2e/lib/helpers/tags.ts @@ -0,0 +1,27 @@ +import { test } from '@playwright/test' +import type { BrowserConfiguration } from '../../../browsers.conf' + +export type Tag = 'skip' | 'fixme' | 'flaky' + +export function addTag(name: Tag, value: string) { + test.info().annotations.push({ + type: `dd_tags[test.${name}]`, + description: value, + }) +} + +export function addBrowserConfigurationTags(metadata: BrowserConfiguration | Record) { + // eslint-disable-next-line prefer-const + for (let [tag, value] of Object.entries(metadata)) { + if (tag === 'name') { + tag = 'browser' + } else if (tag === 'version') { + tag = 'browserVersion' + } + + test.info().annotations.push({ + type: `dd_tags[test.${tag}]`, + description: value, + }) + } +} diff --git a/test/e2e/noticeReporter.ts b/test/e2e/noticeReporter.ts new file mode 100644 index 0000000000..abe13cd0ac --- /dev/null +++ b/test/e2e/noticeReporter.ts @@ -0,0 +1,15 @@ +import type { Reporter } from '@playwright/test/reporter' +import { getRunId } from '../envUtils' +import { APPLICATION_ID } from './lib/helpers/configuration' + +// eslint-disable-next-line import/no-default-export +export default class NoticeReporter implements Reporter { + onBegin() { + console.log( + `[RUM events] https://app.datadoghq.com/rum/explorer?query=${encodeURIComponent( + `@application.id:${APPLICATION_ID} @context.run_id:"${getRunId()}"` + )}` + ) + console.log(`[Log events] https://app.datadoghq.com/logs?query=${encodeURIComponent(`@run_id:"${getRunId()}"`)}\n`) + } +} diff --git a/test/e2e/playwright.base.config.ts b/test/e2e/playwright.base.config.ts new file mode 100644 index 0000000000..f3063f3d12 --- /dev/null +++ b/test/e2e/playwright.base.config.ts @@ -0,0 +1,29 @@ +import path from 'path' +import type { ReporterDescription, Config } from '@playwright/test' +import { getTestReportDirectory } from '../envUtils' + +const testReportDirectory = getTestReportDirectory() + +const reporters: ReporterDescription[] = [['line'], ['./noticeReporter.ts']] + +if (testReportDirectory) { + reporters.push(['junit', { outputFile: path.join(process.cwd(), testReportDirectory, 'results.xml') }]) +} else { + reporters.push(['html']) +} + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export const config: Config = { + testDir: './scenario', + testMatch: ['**/*.scenario.ts'], + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: 25, + reporter: reporters, + use: { + trace: process.env.CI ? 'off' : 'retain-on-failure', + }, +} diff --git a/test/e2e/playwright.bs.config.ts b/test/e2e/playwright.bs.config.ts new file mode 100644 index 0000000000..a940798a43 --- /dev/null +++ b/test/e2e/playwright.bs.config.ts @@ -0,0 +1,36 @@ +import type { Project } from '@playwright/test' +import { defineConfig } from '@playwright/test' +import { getBrowserName, getEncodedCapabilities } from './lib/helpers/playwright' +import { config as baseConfig } from './playwright.base.config' +import { browserConfigurations } from './browsers.conf' + +// eslint-disable-next-line import/no-default-export +export default defineConfig({ + ...baseConfig, + workers: 5, // BrowserStack has a limit of 5 parallel sessions + testIgnore: ['**/developerExtension.scenario.ts', '**/s8sInject.scenario.ts'], // The following test won't run in the BrowserStack + // maxFailures: process.env.CI ? 1 : 0, + projects: browserConfigurations.map((configuration) => { + const project: Project = { + name: configuration.sessionName, + metadata: configuration, + } + + if (configuration.device) { + return { + ...project, + timeout: 60_000, + } + } + + return { + ...project, + use: { + browserName: getBrowserName(configuration.name), + connectOptions: { + wsEndpoint: `wss://cdp.browserstack.com/playwright?caps=${getEncodedCapabilities(configuration)}`, + }, + }, + } + }), +}) diff --git a/test/e2e/playwright.local.config.ts b/test/e2e/playwright.local.config.ts new file mode 100644 index 0000000000..d0b3e8b2ac --- /dev/null +++ b/test/e2e/playwright.local.config.ts @@ -0,0 +1,27 @@ +import { defineConfig, devices } from '@playwright/test' +import { config as baseConfig } from './playwright.base.config' + +const projects = [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + { + name: 'firefox', + use: { ...devices['Desktop Firefox'] }, + }, + { + name: 'webkit', + use: { ...devices['Desktop Safari'] }, + }, + { + name: 'android', + use: { ...devices['Pixel 7'] }, + }, +] + +// eslint-disable-next-line import/no-default-export +export default defineConfig({ + ...baseConfig, + projects, +}) diff --git a/test/e2e/scenario/developer-extension/developerExtension.scenario.ts b/test/e2e/scenario/developer-extension/developerExtension.scenario.ts index 91af823c89..dcb4aa8fbf 100644 --- a/test/e2e/scenario/developer-extension/developerExtension.scenario.ts +++ b/test/e2e/scenario/developer-extension/developerExtension.scenario.ts @@ -1,30 +1,56 @@ -describe('developer-extension', () => { - it('should switch between tabs', async () => { - const panel = new DeveloperExtensionPanel() - await panel.open() - expect(await panel.getSelectedTab()).toEqual('Events') - - await panel.getTab('Infos').click() - expect(await panel.getSelectedTab()).toEqual('Infos') +import path from 'path' +import { test as base, chromium, expect } from '@playwright/test' +import type { Page, BrowserContext } from '@playwright/test' + +const test = base.extend<{ + context: BrowserContext + extensionId: string + developerExtension: DeveloperExtensionPage +}>({ + // eslint-disable-next-line no-empty-pattern + context: async ({}, use) => { + const pathToExtension = path.join(process.cwd(), 'developer-extension', 'dist') + + const context = await chromium.launchPersistentContext('', { + channel: 'chromium', + args: [`--disable-extensions-except=${pathToExtension}`, `--load-extension=${pathToExtension}`], + }) + await use(context) + await context.close() + }, + extensionId: async ({ context }, use) => { + let [background] = context.serviceWorkers() + if (!background) { + background = await context.waitForEvent('serviceworker') + } + + const extensionId = background.url().split('/')[2] + await use(extensionId) + }, + developerExtension: async ({ page, extensionId }, use) => { + await page.goto(`chrome-extension://${extensionId}/panel.html`) + + await use(new DeveloperExtensionPage(page)) + }, +}) + +test.describe('developer-extension', () => { + test('should switch between tabs', async ({ developerExtension: page }) => { + expect(await page.getSelectedTab().innerText()).toEqual('Events') + + await page.getTab('Infos').click() + expect(await page.getSelectedTab().innerText()).toEqual('Infos') }) }) -class DeveloperExtensionPanel { - async open() { - await browser.url('chrome://extensions') - // extensions page is built with custom elements, >>> selector to traverse shadow DOM - // cf https://webdriver.io/docs/selectors/#deep-selectors - const extensionId = await $('>>>extensions-item').getAttribute('id') - const url = `chrome-extension://${extensionId}/panel.html` - await browser.url(url) - expect(await browser.getUrl()).toEqual(url) - } +class DeveloperExtensionPage { + constructor(public readonly page: Page) {} - getSelectedTab() { - return $("button[role='tab'][aria-selected='true']").getText() + getTab(name: string) { + return this.page.getByRole('tab', { name }) } - getTab(content: string) { - return $(`button[role='tab']=${content}`) + getSelectedTab() { + return this.page.getByRole('tab', { selected: true }) } } diff --git a/test/e2e/scenario/eventBridge.scenario.ts b/test/e2e/scenario/eventBridge.scenario.ts index 59fd301082..ff406d526a 100644 --- a/test/e2e/scenario/eventBridge.scenario.ts +++ b/test/e2e/scenario/eventBridge.scenario.ts @@ -1,7 +1,7 @@ -import { flushBrowserLogs } from '../lib/helpers/browser' -import { createTest, flushEvents, html } from '../lib/framework' +import { test, expect } from '@playwright/test' +import { createTest, html } from '../lib/framework' -describe('bridge present', () => { +test.describe('bridge present', () => { createTest('send action') .withRum({ trackUserInteractions: true }) .withEventBridge() @@ -14,36 +14,36 @@ describe('bridge present', () => { }) `) - .run(async ({ intakeRegistry }) => { - const button = await $('button') + .run(async ({ flushEvents, intakeRegistry, page }) => { + const button = page.locator('button') await button.click() // wait for click chain to close - await browser.pause(1000) + await page.waitForTimeout(1000) await flushEvents() - expect(intakeRegistry.rumActionEvents.length).toBe(1) + expect(intakeRegistry.rumActionEvents).toHaveLength(1) expect(intakeRegistry.hasOnlyBridgeRequests).toBe(true) }) createTest('send error') .withRum() .withEventBridge() - .run(async ({ intakeRegistry }) => { - await browser.execute(() => { + .run(async ({ flushBrowserLogs, flushEvents, intakeRegistry, page }) => { + await page.evaluate(() => { console.error('oh snap') }) - await flushBrowserLogs() + flushBrowserLogs() await flushEvents() - expect(intakeRegistry.rumErrorEvents.length).toBe(1) + expect(intakeRegistry.rumErrorEvents).toHaveLength(1) expect(intakeRegistry.hasOnlyBridgeRequests).toBe(true) }) createTest('send resource') .withRum() .withEventBridge() - .run(async ({ intakeRegistry }) => { + .run(async ({ flushEvents, intakeRegistry }) => { await flushEvents() expect(intakeRegistry.rumResourceEvents.length).toBeGreaterThan(0) @@ -53,7 +53,7 @@ describe('bridge present', () => { createTest('send view') .withRum() .withEventBridge() - .run(async ({ intakeRegistry }) => { + .run(async ({ flushEvents, intakeRegistry }) => { await flushEvents() expect(intakeRegistry.rumViewEvents.length).toBeGreaterThan(0) @@ -63,8 +63,8 @@ describe('bridge present', () => { createTest('forward telemetry to the bridge') .withLogs() .withEventBridge() - .run(async ({ intakeRegistry }) => { - await browser.execute(() => { + .run(async ({ flushEvents, intakeRegistry, page }) => { + await page.evaluate(() => { const context = { get foo() { throw new window.Error('bar') @@ -74,7 +74,7 @@ describe('bridge present', () => { }) await flushEvents() - expect(intakeRegistry.telemetryErrorEvents.length).toBe(1) + expect(intakeRegistry.telemetryErrorEvents).toHaveLength(1) expect(intakeRegistry.hasOnlyBridgeRequests).toBe(true) intakeRegistry.empty() }) @@ -82,20 +82,20 @@ describe('bridge present', () => { createTest('forward logs to the bridge') .withLogs() .withEventBridge() - .run(async ({ intakeRegistry }) => { - await browser.execute(() => { + .run(async ({ flushEvents, intakeRegistry, page }) => { + await page.evaluate(() => { window.DD_LOGS!.logger.log('hello') }) await flushEvents() - expect(intakeRegistry.logsEvents.length).toBe(1) + expect(intakeRegistry.logsEvents).toHaveLength(1) expect(intakeRegistry.hasOnlyBridgeRequests).toBe(true) }) createTest('send records to the bridge') .withRum() .withEventBridge() - .run(async ({ intakeRegistry }) => { + .run(async ({ flushEvents, intakeRegistry }) => { await flushEvents() expect(intakeRegistry.replayRecords.length).toBeGreaterThan(0) @@ -105,12 +105,12 @@ describe('bridge present', () => { createTest('do not send records when the recording is stopped') .withRum() .withEventBridge() - .run(async ({ intakeRegistry }) => { + .run(async ({ flushEvents, intakeRegistry, page }) => { // wait for recorder to be properly started - await browser.pause(200) + await page.waitForTimeout(200) const preStopRecordsCount = intakeRegistry.replayRecords.length - await browser.execute(() => { + await page.evaluate(() => { window.DD_RUM!.stopSessionReplayRecording() // trigger a new record diff --git a/test/e2e/scenario/logs.scenario.ts b/test/e2e/scenario/logs.scenario.ts index ffbd1b0c54..903e7ea36a 100644 --- a/test/e2e/scenario/logs.scenario.ts +++ b/test/e2e/scenario/logs.scenario.ts @@ -1,70 +1,74 @@ import { DEFAULT_REQUEST_ERROR_RESPONSE_LENGTH_LIMIT } from '@datadog/browser-logs/cjs/domain/configuration' -import { createTest, flushEvents } from '../lib/framework' +import { test, expect } from '@playwright/test' +import { createTest } from '../lib/framework' import { APPLICATION_ID } from '../lib/helpers/configuration' -import { flushBrowserLogs, withBrowserLogs } from '../lib/helpers/browser' const UNREACHABLE_URL = 'http://localhost:9999/unreachable' -describe('logs', () => { +test.describe('logs', () => { createTest('send logs') .withLogs() - .run(async ({ intakeRegistry }) => { - await browser.execute(() => { + .run(async ({ intakeRegistry, flushEvents, page }) => { + await page.evaluate(() => { window.DD_LOGS!.logger.log('hello') }) await flushEvents() - expect(intakeRegistry.logsEvents.length).toBe(1) + expect(intakeRegistry.logsEvents).toHaveLength(1) expect(intakeRegistry.logsEvents[0].message).toBe('hello') }) createTest('display logs in the console') .withLogs() - .run(async ({ intakeRegistry }) => { - await browser.execute(() => { + .run(async ({ intakeRegistry, flushEvents, page, withBrowserLogs }) => { + await page.evaluate(() => { window.DD_LOGS!.logger.setHandler('console') window.DD_LOGS!.logger.warn('hello') }) await flushEvents() - expect(intakeRegistry.logsEvents.length).toBe(0) + expect(intakeRegistry.logsEvents).toHaveLength(0) - await withBrowserLogs((logs) => { - expect(logs.length).toBe(1) - expect(logs[0].level).toBe('WARNING') - expect(logs[0].message).not.toEqual(jasmine.stringContaining('Datadog Browser SDK')) - expect(logs[0].message).toEqual(jasmine.stringContaining('hello')) + withBrowserLogs((logs) => { + expect(logs).toHaveLength(1) + expect(logs[0].level).toBe('warning') + expect(logs[0].message).not.toEqual(expect.stringContaining('Datadog Browser SDK')) + expect(logs[0].message).toEqual(expect.stringContaining('hello')) }) }) createTest('send console errors') .withLogs({ forwardErrorsToLogs: true }) - .run(async ({ intakeRegistry }) => { - await browser.execute(() => { + .run(async ({ intakeRegistry, flushEvents, page, withBrowserLogs }) => { + await page.evaluate(() => { console.error('oh snap') }) await flushEvents() - expect(intakeRegistry.logsEvents.length).toBe(1) + expect(intakeRegistry.logsEvents).toHaveLength(1) expect(intakeRegistry.logsEvents[0].message).toBe('oh snap') - await withBrowserLogs((browserLogs) => { - expect(browserLogs.length).toEqual(1) + withBrowserLogs((browserLogs) => { + expect(browserLogs).toHaveLength(1) }) }) createTest('send XHR network errors') .withLogs({ forwardErrorsToLogs: true }) - .run(async ({ intakeRegistry }) => { - await browser.executeAsync((unreachableUrl, done) => { - const xhr = new XMLHttpRequest() - xhr.addEventListener('error', () => done(undefined)) - xhr.open('GET', unreachableUrl) - xhr.send() - }, UNREACHABLE_URL) + .run(async ({ intakeRegistry, flushEvents, withBrowserLogs, page }) => { + await page.evaluate( + (unreachableUrl) => + new Promise((resolve) => { + const xhr = new XMLHttpRequest() + xhr.addEventListener('error', () => resolve()) + xhr.open('GET', unreachableUrl) + xhr.send() + }), + UNREACHABLE_URL + ) await flushEvents() - expect(intakeRegistry.logsEvents.length).toBe(1) + expect(intakeRegistry.logsEvents).toHaveLength(1) expect(intakeRegistry.logsEvents[0].message).toBe(`XHR error GET ${UNREACHABLE_URL}`) expect(intakeRegistry.logsEvents[0].origin).toBe('network') - await withBrowserLogs((browserLogs) => { + withBrowserLogs((browserLogs) => { // Some browser report two errors: // * failed to load resource // * blocked by CORS policy @@ -74,19 +78,15 @@ describe('logs', () => { createTest('send fetch network errors') .withLogs({ forwardErrorsToLogs: true }) - .run(async ({ intakeRegistry }) => { - await browser.executeAsync((unreachableUrl, done) => { - fetch(unreachableUrl).catch(() => { - done(undefined) - }) - }, UNREACHABLE_URL) + .run(async ({ intakeRegistry, flushEvents, page, withBrowserLogs }) => { + await page.evaluate((unreachableUrl) => fetch(unreachableUrl).catch(() => undefined), UNREACHABLE_URL) await flushEvents() - expect(intakeRegistry.logsEvents.length).toBe(1) + expect(intakeRegistry.logsEvents).toHaveLength(1) expect(intakeRegistry.logsEvents[0].message).toBe(`Fetch error GET ${UNREACHABLE_URL}`) expect(intakeRegistry.logsEvents[0].origin).toBe('network') - await withBrowserLogs((browserLogs) => { + withBrowserLogs((browserLogs) => { // Some browser report two errors: // * failed to load resource // * blocked by CORS policy @@ -96,18 +96,16 @@ describe('logs', () => { createTest('keep only the first bytes of the response') .withLogs({ forwardErrorsToLogs: true }) - .run(async ({ intakeRegistry, baseUrl, servers }) => { - await browser.executeAsync((done) => { - fetch('/throw-large-response').then(() => done(undefined), console.log) - }) + .run(async ({ intakeRegistry, baseUrl, servers, flushEvents, page, withBrowserLogs, browserName }) => { + await page.evaluate(() => fetch('/throw-large-response')) await flushEvents() - expect(intakeRegistry.logsEvents.length).toBe(1) + expect(intakeRegistry.logsEvents).toHaveLength(1) expect(intakeRegistry.logsEvents[0].message).toBe(`Fetch error GET ${baseUrl}/throw-large-response`) expect(intakeRegistry.logsEvents[0].origin).toBe('network') const ellipsisSize = 3 - expect(intakeRegistry.logsEvents[0].error?.stack?.length).toBe( + expect(intakeRegistry.logsEvents[0].error?.stack).toHaveLength( DEFAULT_REQUEST_ERROR_RESPONSE_LENGTH_LIMIT + ellipsisSize ) @@ -115,42 +113,49 @@ describe('logs', () => { DEFAULT_REQUEST_ERROR_RESPONSE_LENGTH_LIMIT ) - await withBrowserLogs((browserLogs) => { - // Some browser report two errors: - // * the server responded with a status of 500 - // * canceling the body stream is reported as a network error (net::ERR_FAILED) - expect(browserLogs.length).toBeGreaterThanOrEqual(1) + withBrowserLogs((browserLogs) => { + if (browserName.includes('firefox')) { + // Firefox does not report the error message + expect(browserLogs).toHaveLength(0) + } else { + expect(browserLogs).toHaveLength(1) + expect(browserLogs[0].message).toContain('the server responded with a status of 500') + } }) }) createTest('track fetch error') .withLogs({ forwardErrorsToLogs: true }) - .run(async ({ intakeRegistry, baseUrl }) => { - await browser.executeAsync((unreachableUrl, done) => { - let count = 0 - fetch('/throw') - .then(() => (count += 1)) - .catch((err) => console.error(err)) - fetch('/unknown') - .then(() => (count += 1)) - .catch((err) => console.error(err)) - fetch(unreachableUrl).catch(() => (count += 1)) - fetch('/ok') - .then(() => (count += 1)) - .catch((err) => console.error(err)) - - const interval = setInterval(() => { - if (count === 4) { - clearInterval(interval) - done(undefined) - } - }, 500) - }, UNREACHABLE_URL) - - await flushBrowserLogs() + .run(async ({ intakeRegistry, baseUrl, flushEvents, flushBrowserLogs, page }) => { + await page.evaluate( + (unreachableUrl) => + new Promise((resolve) => { + let count = 0 + fetch('/throw') + .then(() => (count += 1)) + .catch((err) => console.error(err)) + fetch('/unknown') + .then(() => (count += 1)) + .catch((err) => console.error(err)) + fetch(unreachableUrl).catch(() => (count += 1)) + fetch('/ok') + .then(() => (count += 1)) + .catch((err) => console.error(err)) + + const interval = setInterval(() => { + if (count === 4) { + clearInterval(interval) + resolve() + } + }, 500) + }), + UNREACHABLE_URL + ) + + flushBrowserLogs() await flushEvents() - expect(intakeRegistry.logsEvents.length).toEqual(2) + expect(intakeRegistry.logsEvents).toHaveLength(2) const unreachableRequest = intakeRegistry.logsEvents.find((log) => log.http!.url.includes('/unreachable'))! const throwRequest = intakeRegistry.logsEvents.find((log) => log.http!.url.includes('/throw'))! @@ -167,12 +172,12 @@ describe('logs', () => { createTest('add RUM internal context to logs') .withRum() .withLogs() - .run(async ({ intakeRegistry }) => { - await browser.execute(() => { + .run(async ({ intakeRegistry, flushEvents, page }) => { + await page.evaluate(() => { window.DD_LOGS!.logger.log('hello') }) await flushEvents() - expect(intakeRegistry.logsEvents.length).toBe(1) + expect(intakeRegistry.logsEvents).toHaveLength(1) expect(intakeRegistry.logsEvents[0].view.id).toBeDefined() expect(intakeRegistry.logsEvents[0].application_id).toBe(APPLICATION_ID) }) @@ -184,12 +189,12 @@ describe('logs', () => { return true }, }) - .run(async ({ intakeRegistry }) => { - await browser.execute(() => { + .run(async ({ intakeRegistry, flushEvents, page }) => { + await page.evaluate(() => { window.DD_LOGS!.logger.log('hello', {}) }) await flushEvents() - expect(intakeRegistry.logsEvents.length).toBe(1) + expect(intakeRegistry.logsEvents).toHaveLength(1) expect(intakeRegistry.logsEvents[0].foo).toBe('bar') }) }) diff --git a/test/e2e/scenario/microfrontend.scenario.ts b/test/e2e/scenario/microfrontend.scenario.ts index 71a593d8fc..2df86818c6 100644 --- a/test/e2e/scenario/microfrontend.scenario.ts +++ b/test/e2e/scenario/microfrontend.scenario.ts @@ -1,7 +1,7 @@ import type { RumEvent, RumEventDomainContext, RumInitConfiguration } from '@datadog/browser-rum-core' import type { LogsEvent, LogsInitConfiguration, LogsEventDomainContext } from '@datadog/browser-logs' -import { flushBrowserLogs, withBrowserLogs } from '../lib/helpers/browser' -import { flushEvents, createTest } from '../lib/framework' +import { test, expect } from '@playwright/test' +import { createTest } from '../lib/framework' const HANDLING_STACK_REGEX = /^Error: \n\s+at testHandlingStack @/ @@ -28,7 +28,7 @@ const LOGS_CONFIG: Partial = { }, } -describe('microfrontend', () => { +test.describe('microfrontend', () => { createTest('expose handling stack for fetch requests') .withRum(RUM_CONFIG) .withRumInit((configuration) => { @@ -42,7 +42,7 @@ describe('microfrontend', () => { testHandlingStack() }) - .run(async ({ intakeRegistry }) => { + .run(async ({ intakeRegistry, flushEvents }) => { await flushEvents() const event = intakeRegistry.rumResourceEvents.find((event) => event.resource.type === 'fetch') @@ -64,7 +64,7 @@ describe('microfrontend', () => { testHandlingStack() }) - .run(async ({ intakeRegistry }) => { + .run(async ({ intakeRegistry, flushEvents }) => { await flushEvents() const event = intakeRegistry.rumResourceEvents.find((event) => event.resource.type === 'xhr') @@ -84,7 +84,7 @@ describe('microfrontend', () => { testHandlingStack() }) - .run(async ({ intakeRegistry }) => { + .run(async ({ intakeRegistry, flushEvents }) => { await flushEvents() const event = intakeRegistry.rumActionEvents[0] @@ -104,7 +104,7 @@ describe('microfrontend', () => { testHandlingStack() }) - .run(async ({ intakeRegistry }) => { + .run(async ({ intakeRegistry, flushEvents }) => { await flushEvents() const event = intakeRegistry.rumErrorEvents[0] @@ -124,21 +124,21 @@ describe('microfrontend', () => { testHandlingStack() }) - .run(async ({ intakeRegistry }) => { + .run(async ({ intakeRegistry, flushEvents, withBrowserLogs }) => { await flushEvents() const event = intakeRegistry.rumErrorEvents[0] - await withBrowserLogs((logs) => { - expect(logs.length).toBe(1) - expect(logs[0].message).toMatch(/"foo"$/) + withBrowserLogs((logs) => { + expect(logs).toHaveLength(1) + expect(logs[0].message).toMatch(/foo$/) // TODO(playwright migration): it looks like chrome is logging the string without double quotes, but some other browser might }) expect(event).toBeTruthy() expect(event?.context?.handlingStack).toMatch(HANDLING_STACK_REGEX) }) - describe('console apis', () => { + test.describe('console apis', () => { createTest('expose handling stack for console.log') .withLogs(LOGS_CONFIG) .withLogsInit((configuration) => { @@ -150,21 +150,21 @@ describe('microfrontend', () => { testHandlingStack() }) - .run(async ({ intakeRegistry }) => { + .run(async ({ intakeRegistry, flushEvents, flushBrowserLogs }) => { await flushEvents() const event = intakeRegistry.logsEvents[0] - await flushBrowserLogs() + flushBrowserLogs() expect(event).toBeTruthy() expect(event?.context).toEqual({ - handlingStack: jasmine.stringMatching(HANDLING_STACK_REGEX), + handlingStack: expect.stringMatching(HANDLING_STACK_REGEX), }) }) }) - describe('logger apis', () => { + test.describe('logger apis', () => { createTest('expose handling stack for DD_LOGS.logger.log') .withLogs(LOGS_CONFIG) .withLogsInit((configuration) => { @@ -176,16 +176,16 @@ describe('microfrontend', () => { testHandlingStack() }) - .run(async ({ intakeRegistry }) => { + .run(async ({ intakeRegistry, flushEvents, flushBrowserLogs }) => { await flushEvents() const event = intakeRegistry.logsEvents[0] - await flushBrowserLogs() + flushBrowserLogs() expect(event).toBeTruthy() expect(event?.context).toEqual({ - handlingStack: jasmine.stringMatching(HANDLING_STACK_REGEX), + handlingStack: expect.stringMatching(HANDLING_STACK_REGEX), }) }) }) @@ -205,7 +205,7 @@ describe('microfrontend', () => { }, }) }) - .run(async ({ intakeRegistry }) => { + .run(async ({ intakeRegistry, flushEvents }) => { await flushEvents() const viewEvent = intakeRegistry.rumViewEvents[0] diff --git a/test/e2e/scenario/recorder/recorder.scenario.ts b/test/e2e/scenario/recorder/recorder.scenario.ts index 63ef90340b..7d1c83236e 100644 --- a/test/e2e/scenario/recorder/recorder.scenario.ts +++ b/test/e2e/scenario/recorder/recorder.scenario.ts @@ -8,48 +8,49 @@ import { DefaultPrivacyLevel } from '@datadog/browser-rum' import { findElement, findElementWithIdAttribute, + findTextContent, + findElementWithTagName, +} from '@datadog/browser-rum/test/nodes' +import { findFullSnapshot, findIncrementalSnapshot, findAllIncrementalSnapshots, findMeta, - findTextContent, - createMutationPayloadValidatorFromSegment, findAllFrustrationRecords, findMouseInteractionRecords, - findElementWithTagName, -} from '@datadog/browser-rum/test' -import { SESSION_STORE_KEY } from '@datadog/browser-core' -import { flushEvents, createTest, bundleSetup, html } from '../../lib/framework' +} from '@datadog/browser-rum/test/segments' +import { createMutationPayloadValidatorFromSegment } from '@datadog/browser-rum/test/mutationPayloadValidator' +import { test, expect } from '@playwright/test' +import { wait } from '@datadog/browser-core/test/wait' +import { createTest, bundleSetup, html } from '../../lib/framework' -const TIMESTAMP_RE = /^\d{13}$/ const UUID_RE = /^[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}$/ -describe('recorder', () => { +test.describe('recorder', () => { createTest('record mouse move') .withRum() - .run(async ({ intakeRegistry }) => { - await browser.execute(() => document.documentElement.outerHTML) - const html = await $('html') - await html.click() + .run(async ({ intakeRegistry, flushEvents, page }) => { + await page.evaluate(() => document.documentElement.outerHTML) + await page.locator('html').click() await flushEvents() - expect(intakeRegistry.replaySegments.length).toBe(1) + expect(intakeRegistry.replaySegments).toHaveLength(1) const { segment, metadata, segmentFile: { encoding, filename, mimetype }, } = intakeRegistry.replayRequests[0] expect(metadata).toEqual({ - application: { id: jasmine.stringMatching(UUID_RE) }, + application: { id: expect.stringMatching(UUID_RE) }, creation_reason: 'init', - end: jasmine.stringMatching(TIMESTAMP_RE), + end: expect.any(Number), has_full_snapshot: true, - records_count: jasmine.any(Number), - session: { id: jasmine.stringMatching(UUID_RE) }, - start: jasmine.stringMatching(TIMESTAMP_RE), - view: { id: jasmine.stringMatching(UUID_RE) }, - raw_segment_size: jasmine.any(Number), - compressed_segment_size: jasmine.any(Number), + records_count: expect.any(Number), + session: { id: expect.stringMatching(UUID_RE) }, + start: expect.any(Number), + view: { id: expect.stringMatching(UUID_RE) }, + raw_segment_size: expect.any(Number), + compressed_segment_size: expect.any(Number), index_in_view: 0, source: 'browser', }) @@ -58,7 +59,7 @@ describe('recorder', () => { creation_reason: metadata.creation_reason, end: Number(metadata.end), has_full_snapshot: true, - records: jasmine.any(Array), + records: expect.any(Array), records_count: Number(metadata.records_count), session: { id: metadata.session.id }, start: Number(metadata.start), @@ -66,18 +67,19 @@ describe('recorder', () => { index_in_view: 0, source: 'browser', }) - expect(encoding).toEqual(jasmine.any(String)) + expect(encoding).toEqual(expect.any(String)) expect(filename).toBe(`${metadata.session.id}-${metadata.start}`) expect(mimetype).toBe('application/octet-stream') - expect(findMeta(segment)).toBeTruthy('have a Meta record') - expect(findFullSnapshot(segment)).toBeTruthy('have a FullSnapshot record') - expect(findIncrementalSnapshot(segment, IncrementalSource.MouseInteraction)).toBeTruthy( + expect(findMeta(segment), 'have a Meta record').toBeTruthy() + expect(findFullSnapshot(segment), 'have a FullSnapshot record').toBeTruthy() + expect( + findIncrementalSnapshot(segment, IncrementalSource.MouseInteraction), 'have a IncrementalSnapshot/MouseInteraction record' - ) + ).toBeTruthy() }) - describe('full snapshot', () => { + test.describe('full snapshot', () => { createTest('obfuscate elements') .withRum() .withSetup(bundleSetup) @@ -88,10 +90,10 @@ describe('recorder', () => { `) - .run(async ({ intakeRegistry }) => { + .run(async ({ intakeRegistry, flushEvents }) => { await flushEvents() - expect(intakeRegistry.replaySegments.length).toBe(1) + expect(intakeRegistry.replaySegments).toHaveLength(1) const fullSnapshot = findFullSnapshot(intakeRegistry.replaySegments[0])! @@ -102,13 +104,13 @@ describe('recorder', () => { const hiddenNodeByAttribute = findElement(fullSnapshot.data.node, (node) => node.tagName === 'p') expect(hiddenNodeByAttribute).toBeTruthy() expect(hiddenNodeByAttribute!.attributes['data-dd-privacy']).toBe('hidden') - expect(hiddenNodeByAttribute!.childNodes.length).toBe(0) + expect(hiddenNodeByAttribute!.childNodes).toHaveLength(0) const hiddenNodeByClassName = findElement(fullSnapshot.data.node, (node) => node.tagName === 'span') expect(hiddenNodeByClassName).toBeTruthy() expect(hiddenNodeByClassName!.attributes.class).toBeUndefined() expect(hiddenNodeByClassName!.attributes['data-dd-privacy']).toBe('hidden') - expect(hiddenNodeByClassName!.childNodes.length).toBe(0) + expect(hiddenNodeByClassName!.childNodes).toHaveLength(0) const inputIgnored = findElementWithIdAttribute(fullSnapshot.data.node, 'input-not-obfuscated') expect(inputIgnored).toBeTruthy() @@ -120,7 +122,7 @@ describe('recorder', () => { }) }) - describe('mutations observer', () => { + test.describe('mutations observer', () => { createTest('record mutations') .withRum() .withSetup(bundleSetup) @@ -130,8 +132,8 @@ describe('recorder', () => {
  • `) - .run(async ({ intakeRegistry }) => { - await browser.execute(() => { + .run(async ({ intakeRegistry, page, flushEvents }) => { + await page.evaluate(() => { const li = document.createElement('li') const ul = document.querySelector('ul') as HTMLUListElement @@ -146,7 +148,8 @@ describe('recorder', () => { await flushEvents() const { validate, expectNewNode, expectInitialNode } = createMutationPayloadValidatorFromSegment( - intakeRegistry.replaySegments[0] + intakeRegistry.replaySegments[0], + { expect } ) validate({ @@ -174,8 +177,8 @@ describe('recorder', () => {
  • `) - .run(async ({ intakeRegistry }) => { - await browser.execute(() => { + .run(async ({ intakeRegistry, page, flushEvents }) => { + await page.evaluate(() => { const li = document.createElement('li') const ul = document.querySelector('ul') as HTMLUListElement @@ -192,7 +195,8 @@ describe('recorder', () => { await flushEvents() const { validate, expectNewNode, expectInitialNode } = createMutationPayloadValidatorFromSegment( - intakeRegistry.replaySegments[0] + intakeRegistry.replaySegments[0], + { expect } ) validate({ @@ -224,8 +228,8 @@ describe('recorder', () => {
  • `) - .run(async ({ intakeRegistry }) => { - await browser.execute(() => { + .run(async ({ intakeRegistry, page, flushEvents }) => { + await page.evaluate(() => { const li = document.createElement('li') const ul = document.querySelector('ul') as HTMLUListElement @@ -240,7 +244,8 @@ describe('recorder', () => { await flushEvents() const { validate, expectInitialNode } = createMutationPayloadValidatorFromSegment( - intakeRegistry.replaySegments[0] + intakeRegistry.replaySegments[0], + { expect } ) validate({ @@ -269,8 +274,8 @@ describe('recorder', () => { `) - .run(async ({ intakeRegistry }) => { - await browser.execute(() => { + .run(async ({ intakeRegistry, page, flushEvents }) => { + await page.evaluate(() => { document.querySelector('div')!.setAttribute('foo', 'bar') document.querySelector('li')!.textContent = 'hop' document.querySelector('div')!.appendChild(document.createElement('p')) @@ -278,10 +283,10 @@ describe('recorder', () => { await flushEvents() - expect(intakeRegistry.replaySegments.length).toBe(1) + expect(intakeRegistry.replaySegments).toHaveLength(1) const segment = intakeRegistry.replaySegments[0] - expect(findAllIncrementalSnapshots(segment, IncrementalSource.Mutation)).toEqual([]) + expect(findAllIncrementalSnapshots(segment, IncrementalSource.Mutation)).toHaveLength(0) }) createTest('record DOM node movement 1') @@ -294,8 +299,8 @@ describe('recorder', () => { cdefg ` ) - .run(async ({ intakeRegistry }) => { - await browser.execute(() => { + .run(async ({ intakeRegistry, page, flushEvents }) => { + await page.evaluate(() => { const div = document.querySelector('div')! const p = document.querySelector('p')! const span = document.querySelector('span')! @@ -308,7 +313,8 @@ describe('recorder', () => { await flushEvents() const { validate, expectInitialNode } = createMutationPayloadValidatorFromSegment( - intakeRegistry.replaySegments[0] + intakeRegistry.replaySegments[0], + { expect } ) validate({ adds: [ @@ -343,8 +349,8 @@ describe('recorder', () => { cdefg ` ) - .run(async ({ intakeRegistry }) => { - await browser.execute(() => { + .run(async ({ intakeRegistry, page, flushEvents }) => { + await page.evaluate(() => { const div = document.createElement('div') const span = document.querySelector('span')! document.body.appendChild(div) @@ -354,7 +360,8 @@ describe('recorder', () => { await flushEvents() const { validate, expectInitialNode, expectNewNode } = createMutationPayloadValidatorFromSegment( - intakeRegistry.replaySegments[0] + intakeRegistry.replaySegments[0], + { expect } ) const div = expectNewNode({ type: NodeType.Element, tagName: 'div' }) @@ -394,8 +401,8 @@ describe('recorder', () => {
    ` ) - .run(async ({ intakeRegistry }) => { - await browser.execute(() => { + .run(async ({ intakeRegistry, page, flushEvents }) => { + await page.evaluate(() => { const ul = document.querySelector('ul') as HTMLUListElement let count = 3 while (count > 0) { @@ -408,7 +415,8 @@ describe('recorder', () => { await flushEvents() const { validate, expectInitialNode, expectNewNode } = createMutationPayloadValidatorFromSegment( - intakeRegistry.replaySegments[0] + intakeRegistry.replaySegments[0], + { expect } ) const ul = expectInitialNode({ tag: 'ul' }) @@ -437,7 +445,7 @@ describe('recorder', () => { }) }) - describe('input observers', () => { + test.describe('input observers', () => { createTest('record input interactions') .withRum({ defaultPrivacyLevel: DefaultPrivacyLevel.ALLOW, @@ -465,21 +473,21 @@ describe('recorder', () => { `) - .run(async ({ intakeRegistry }) => { - const textInput = await $('#text-input') - await textInput.setValue('test') + .run(async ({ intakeRegistry, page, flushEvents }) => { + const textInput = page.locator('#text-input') + await textInput.pressSequentially('test') - const radioInput = await $('#radio-input') + const radioInput = page.locator('#radio-input') await radioInput.click() - const checkboxInput = await $('#checkbox-input') + const checkboxInput = page.locator('#checkbox-input') await checkboxInput.click() - const textarea = await $('#textarea') - await textarea.setValue('textarea test') + const textarea = page.locator('#textarea') + await textarea.pressSequentially('textarea test') - const select = await $('#select') - await select.selectByAttribute('value', '2') + const select = page.locator('#select') + await select.selectOption({ value: '2' }) await flushEvents() @@ -488,12 +496,12 @@ describe('recorder', () => { expect((textInputRecords[textInputRecords.length - 1].data as { text?: string }).text).toBe('test') const radioInputRecords = filterRecordsByIdAttribute('radio-input') - expect(radioInputRecords.length).toBe(1) + expect(radioInputRecords).toHaveLength(1) expect((radioInputRecords[0].data as { text?: string }).text).toBe(undefined) expect((radioInputRecords[0].data as { isChecked?: boolean }).isChecked).toBe(true) const checkboxInputRecords = filterRecordsByIdAttribute('checkbox-input') - expect(checkboxInputRecords.length).toBe(1) + expect(checkboxInputRecords).toHaveLength(1) expect((checkboxInputRecords[0].data as { text?: string }).text).toBe(undefined) expect((checkboxInputRecords[0].data as { isChecked?: boolean }).isChecked).toBe(true) @@ -502,7 +510,7 @@ describe('recorder', () => { expect((textareaRecords[textareaRecords.length - 1].data as { text?: string }).text).toBe('textarea test') const selectRecords = filterRecordsByIdAttribute('select') - expect(selectRecords.length).toBe(1) + expect(selectRecords).toHaveLength(1) expect((selectRecords[0].data as { text?: string }).text).toBe('2') function filterRecordsByIdAttribute(idAttribute: string) { @@ -527,18 +535,18 @@ describe('recorder', () => { `) - .run(async ({ intakeRegistry }) => { - const firstInput = await $('#first') - await firstInput.setValue('foo') + .run(async ({ intakeRegistry, flushEvents, page }) => { + const firstInput = page.locator('#first') + await firstInput.fill('foo') - const secondInput = await $('#second') - await secondInput.setValue('bar') + const secondInput = page.locator('#second') + await secondInput.fill('bar') - const thirdInput = await $('#third') - await thirdInput.setValue('baz') + const thirdInput = page.locator('#third') + await thirdInput.fill('baz') - const fourthInput = await $('#fourth') - await fourthInput.setValue('quux') + const fourthInput = page.locator('#fourth') + await fourthInput.fill('quux') await flushEvents() @@ -558,16 +566,16 @@ describe('recorder', () => { `) - .run(async ({ intakeRegistry }) => { - const firstInput = await $('#by-data-attribute') - await firstInput.setValue('foo') + .run(async ({ intakeRegistry, flushEvents, page }) => { + const firstInput = page.locator('#by-data-attribute') + await firstInput.fill('foo') - const secondInput = await $('#by-classname') - await secondInput.setValue('bar') + const secondInput = page.locator('#by-classname') + await secondInput.fill('bar') await flushEvents() - expect(intakeRegistry.replaySegments.length).toBe(1) + expect(intakeRegistry.replaySegments).toHaveLength(1) const segment = intakeRegistry.replaySegments[0] @@ -581,7 +589,7 @@ describe('recorder', () => { }) }) - describe('stylesheet rules observer', () => { + test.describe('stylesheet rules observer', () => { createTest('record dynamic CSS changes') .withRum() .withSetup(bundleSetup) @@ -593,15 +601,15 @@ describe('recorder', () => { } `) - .run(async ({ intakeRegistry }) => { - await browser.execute(() => { + .run(async ({ intakeRegistry, page, flushEvents }) => { + await page.evaluate(() => { document.styleSheets[0].deleteRule(0) document.styleSheets[0].insertRule('.added {}', 0) }) await flushEvents() - expect(intakeRegistry.replaySegments.length).toBe(1) + expect(intakeRegistry.replaySegments).toHaveLength(1) const segment = intakeRegistry.replaySegments[0] @@ -609,7 +617,7 @@ describe('recorder', () => { data: StyleSheetRuleData }> - expect(styleSheetRules.length).toBe(2) + expect(styleSheetRules).toHaveLength(2) expect(styleSheetRules[0].data.removes).toEqual([{ index: 0 }]) expect(styleSheetRules[1].data.adds).toEqual([{ rule: '.added {}', index: 0 }]) }) @@ -631,8 +639,8 @@ describe('recorder', () => { } `) - .run(async ({ intakeRegistry }) => { - await browser.execute(() => { + .run(async ({ intakeRegistry, page, flushEvents }) => { + await page.evaluate(() => { const supportsRule = document.styleSheets[0].cssRules[0] as CSSGroupingRule const mediaRule = document.styleSheets[0].cssRules[1] as CSSGroupingRule @@ -643,7 +651,7 @@ describe('recorder', () => { await flushEvents() - expect(intakeRegistry.replaySegments.length).toBe(1) + expect(intakeRegistry.replaySegments).toHaveLength(1) const segment = intakeRegistry.replaySegments[0] @@ -651,31 +659,31 @@ describe('recorder', () => { data: StyleSheetRuleData }> - expect(styleSheetRules.length).toBe(3) + expect(styleSheetRules).toHaveLength(3) expect(styleSheetRules[0].data.adds).toEqual([{ rule: '.inserted {}', index: [0, 0] }]) expect(styleSheetRules[1].data.adds).toEqual([{ rule: '.added {}', index: [0, 1] }]) expect(styleSheetRules[2].data.removes).toEqual([{ index: [1, 1] }]) }) }) - describe('frustration records', () => { + test.describe('frustration records', () => { createTest('should detect a dead click and match it to mouse interaction record') .withRum({ trackUserInteractions: true }) .withSetup(bundleSetup) - .run(async ({ intakeRegistry }) => { - const html = await $('html') + .run(async ({ intakeRegistry, flushEvents, page }) => { + const html = page.locator('html') await html.click() await flushEvents() - expect(intakeRegistry.replaySegments.length).toBe(1) + expect(intakeRegistry.replaySegments).toHaveLength(1) const segment = intakeRegistry.replaySegments[0] const mouseupRecords = findMouseInteractionRecords(segment, MouseInteractionType.MouseUp) const frustrationRecords = findAllFrustrationRecords(segment) - expect(mouseupRecords.length).toBe(1) - expect(mouseupRecords[0].id).toBeTruthy('mouse interaction record should have an id') - expect(frustrationRecords.length).toBe(1) + expect(mouseupRecords).toHaveLength(1) + expect(mouseupRecords[0].id, 'mouse interaction record should have an id').toBeTruthy() + expect(frustrationRecords).toHaveLength(1) expect(frustrationRecords[0].data).toEqual({ frustrationTypes: [FrustrationType.DEAD_CLICK], recordIds: [mouseupRecords[0].id!], @@ -692,10 +700,10 @@ describe('recorder', () => { onclick="document.querySelector('#main-div').appendChild(document.createElement('div'));" /> `) - .run(async ({ intakeRegistry }) => { + .run(async ({ intakeRegistry, page, flushEvents }) => { // We don't use the wdio's `$('button').click()` here because the latency of the command is too high and the // clicks won't be recognised as rage clicks. - await browser.execute(() => { + await page.evaluate(() => { const button = document.querySelector('button')! function click() { @@ -717,14 +725,14 @@ describe('recorder', () => { await flushEvents() - expect(intakeRegistry.replaySegments.length).toBe(1) + expect(intakeRegistry.replaySegments).toHaveLength(1) const segment = intakeRegistry.replaySegments[0] const mouseupRecords = findMouseInteractionRecords(segment, MouseInteractionType.MouseUp) const frustrationRecords = findAllFrustrationRecords(segment) - expect(mouseupRecords.length).toBe(4) - expect(frustrationRecords.length).toBe(1) + expect(mouseupRecords).toHaveLength(4) + expect(frustrationRecords).toHaveLength(1) expect(frustrationRecords[0].data).toEqual({ frustrationTypes: [FrustrationType.RAGE_CLICK], recordIds: mouseupRecords.map((r) => r.id!), @@ -732,7 +740,7 @@ describe('recorder', () => { }) }) - describe('scroll positions', () => { + test.describe('scroll positions', () => { createTest('should be recorded across navigation') // to control initial position before recording .withRum({ startSessionReplayRecordingManually: true }) @@ -756,53 +764,53 @@ describe('recorder', () => {
    `) - .run(async ({ intakeRegistry }) => { + .run(async ({ intakeRegistry, page, flushEvents }) => { function scroll({ windowY, containerX }: { windowY: number; containerX: number }) { - return browser.executeAsync( - (windowY, containerX, done) => { - let scrollCount = 0 - - document.addEventListener( - 'scroll', - () => { - scrollCount++ - if (scrollCount === 2) { - // ensure to bypass observer throttling - setTimeout(done, 100) - } - }, - { capture: true, passive: true } - ) - - window.scrollTo(0, windowY) - document.getElementById('container')!.scrollTo(containerX, 0) - }, - windowY, - containerX + return page.evaluate( + ({ windowY, containerX }) => + new Promise((resolve) => { + let scrollCount = 0 + + document.addEventListener( + 'scroll', + () => { + scrollCount++ + if (scrollCount === 2) { + // ensure to bypass observer throttling + setTimeout(resolve, 100) + } + }, + { capture: true, passive: true } + ) + + window.scrollTo(0, windowY) + document.getElementById('container')!.scrollTo(containerX, 0) + }), + { windowY, containerX } ) } // initial scroll positions await scroll({ windowY: 100, containerX: 10 }) - await browser.execute(() => { + await page.evaluate(() => { window.DD_RUM!.startSessionReplayRecording() }) // wait for recorder to be properly started - await browser.pause(200) + await wait(100) // update scroll positions await scroll({ windowY: 150, containerX: 20 }) // trigger new full snapshot - await browser.execute(() => { + await page.evaluate(() => { window.DD_RUM!.startView() }) await flushEvents() - expect(intakeRegistry.replaySegments.length).toBe(2) + expect(intakeRegistry.replaySegments).toHaveLength(2) const firstSegment = intakeRegistry.replaySegments[0] const firstFullSnapshot = findFullSnapshot(firstSegment)! @@ -812,7 +820,7 @@ describe('recorder', () => { expect(containerElement.attributes.rr_scrollLeft).toBe(10) const scrollRecords = findAllIncrementalSnapshots(firstSegment, IncrementalSource.Scroll) - expect(scrollRecords.length).toBe(2) + expect(scrollRecords).toHaveLength(2) const [windowScrollData, containerScrollData] = scrollRecords.map((record) => record.data as ScrollData) expect(windowScrollData.y).toEqual(150) expect(containerScrollData.x).toEqual(20) @@ -825,63 +833,63 @@ describe('recorder', () => { }) }) - describe('recording of sampled out sessions', () => { + test.describe('recording of sampled out sessions', () => { createTest('should not start recording when session is sampled out') .withRum({ sessionReplaySampleRate: 0 }) .withSetup(bundleSetup) - .run(async ({ intakeRegistry }) => { - await browser.execute(() => { + .run(async ({ intakeRegistry, page, flushEvents }) => { + await page.evaluate(() => { window.DD_RUM!.startSessionReplayRecording() }) await flushEvents() - expect(intakeRegistry.replaySegments.length).toBe(0) + expect(intakeRegistry.replaySegments).toHaveLength(0) }) createTest('should start recording if forced when session is sampled out') .withRum({ sessionReplaySampleRate: 0 }) .withSetup(bundleSetup) - .run(async ({ intakeRegistry }) => { - await browser.execute(() => { + .run(async ({ intakeRegistry, page, flushEvents, browserContext }) => { + await page.evaluate(() => { window.DD_RUM!.startSessionReplayRecording({ force: true }) }) - const [cookie] = await browser.getCookies([SESSION_STORE_KEY]) + const [cookie] = await browserContext.cookies() expect(cookie.value).toContain('forcedReplay=1') await flushEvents() - expect(intakeRegistry.replaySegments.length).toBe(1) + expect(intakeRegistry.replaySegments).toHaveLength(1) }) }) createTest('restarting recording should send a new full snapshot') .withRum() .withSetup(bundleSetup) - .run(async ({ intakeRegistry }) => { - await browser.execute(() => { + .run(async ({ intakeRegistry, page, flushEvents }) => { + await page.evaluate(() => { window.DD_RUM!.stopSessionReplayRecording() window.DD_RUM!.startSessionReplayRecording() }) await flushEvents() - expect(intakeRegistry.replaySegments.length).toBe(2) + expect(intakeRegistry.replaySegments).toHaveLength(2) const firstSegment = intakeRegistry.replaySegments[0] - expect(findFullSnapshot(firstSegment)).toBeTruthy('first segment have a FullSnapshot record') + expect(findFullSnapshot(firstSegment), 'first segment have a FullSnapshot record').toBeTruthy() const secondSegment = intakeRegistry.replaySegments[1] - expect(findFullSnapshot(secondSegment)).toBeTruthy('second segment have a FullSnapshot record') + expect(findFullSnapshot(secondSegment), 'second segment have a FullSnapshot record').toBeTruthy() }) createTest('workerUrl initialization parameter') .withRum({ workerUrl: '/worker.js' }) .withSetup(bundleSetup) .withBasePath('/no-blob-worker-csp') - .run(async ({ intakeRegistry }) => { + .run(async ({ intakeRegistry, flushEvents }) => { await flushEvents() - expect(intakeRegistry.replaySegments.length).toBe(1) + expect(intakeRegistry.replaySegments).toHaveLength(1) }) }) diff --git a/test/e2e/scenario/recorder/shadowDom.scenario.ts b/test/e2e/scenario/recorder/shadowDom.scenario.ts index 52512988c8..311a4e6cdc 100644 --- a/test/e2e/scenario/recorder/shadowDom.scenario.ts +++ b/test/e2e/scenario/recorder/shadowDom.scenario.ts @@ -6,19 +6,22 @@ import type { } from '@datadog/browser-rum/src/types' import { IncrementalSource, MouseInteractionType, NodeType } from '@datadog/browser-rum/src/types' +import { createMutationPayloadValidatorFromSegment } from '@datadog/browser-rum/test/mutationPayloadValidator' import { - createMutationPayloadValidatorFromSegment, findElementWithIdAttribute, findElementWithTagName, - findFullSnapshot, - findIncrementalSnapshot, - findMouseInteractionRecords, findNode, findTextContent, findTextNode, -} from '@datadog/browser-rum/test' +} from '@datadog/browser-rum/test/nodes' +import { + findFullSnapshot, + findIncrementalSnapshot, + findMouseInteractionRecords, +} from '@datadog/browser-rum/test/segments' -import { flushEvents, createTest, bundleSetup, html } from '../../lib/framework' +import { test, expect } from '@playwright/test' +import { createTest, bundleSetup, html } from '../../lib/framework' /** Will generate the following HTML * ```html @@ -156,7 +159,7 @@ class DivWithStyle extends HTMLElement { ` -describe('recorder with shadow DOM', () => { +test.describe('recorder with shadow DOM', () => { createTest('can record fullsnapshot with the detail inside the shadow root') .withRum({ defaultPrivacyLevel: 'allow' }) .withSetup(bundleSetup) @@ -164,10 +167,10 @@ describe('recorder with shadow DOM', () => { ${divShadowDom} `) - .run(async ({ intakeRegistry }) => { + .run(async ({ flushEvents, intakeRegistry }) => { await flushEvents() - expect(intakeRegistry.replaySegments.length).toBe(1) + expect(intakeRegistry.replaySegments).toHaveLength(1) const fullSnapshot = findFullSnapshot(intakeRegistry.replaySegments[0])! expect(fullSnapshot).toBeTruthy() @@ -184,13 +187,13 @@ describe('recorder with shadow DOM', () => { ${divWithStyleShadowDom} `) - .run(async ({ intakeRegistry }) => { - if (!(await isAdoptedStyleSheetsSupported())) { - return pending('adoptedStyleSheets is not supported in this browser') - } + .run(async ({ flushEvents, intakeRegistry, page }) => { + const isAdoptedStyleSheetsSupported = await page.evaluate(() => document.adoptedStyleSheets !== undefined) + test.skip(!isAdoptedStyleSheetsSupported, 'adoptedStyleSheets is not supported in this browser') + await flushEvents() - expect(intakeRegistry.replaySegments.length).toBe(1) + expect(intakeRegistry.replaySegments).toHaveLength(1) const fullSnapshot = findFullSnapshot(intakeRegistry.replaySegments[0])! expect(fullSnapshot).toBeTruthy() @@ -210,10 +213,10 @@ describe('recorder with shadow DOM', () => {
    `) - .run(async ({ intakeRegistry }) => { + .run(async ({ flushEvents, intakeRegistry }) => { await flushEvents() - expect(intakeRegistry.replaySegments.length).toBe(1) + expect(intakeRegistry.replaySegments).toHaveLength(1) const fullSnapshot = findFullSnapshot(intakeRegistry.replaySegments[0])! expect(fullSnapshot).toBeTruthy() @@ -223,7 +226,7 @@ describe('recorder with shadow DOM', () => { shadowRoot: outsideShadowRoot, textContent: outsideTextContent, } = findElementsInShadowDom(fullSnapshot.data.node, 'privacy-set-outside') - expect(outsideShadowRoot?.isShadowRoot).toBeTrue() + expect(outsideShadowRoot?.isShadowRoot).toBe(true) expect(outsideInput?.attributes.value).toBe('***') expect(outsideTextContent).toBe('field privacy-set-outside: ') @@ -232,7 +235,7 @@ describe('recorder with shadow DOM', () => { shadowRoot: insideShadowRoot, textContent: insideTextContent, } = findElementsInShadowDom(fullSnapshot.data.node, 'privacy-set-inside') - expect(insideShadowRoot?.isShadowRoot).toBeTrue() + expect(insideShadowRoot?.isShadowRoot).toBe(true) expect(insideInput?.attributes.value).toBe('***') expect(insideTextContent).toBe('field privacy-set-inside: ') }) @@ -244,11 +247,11 @@ describe('recorder with shadow DOM', () => { ${divShadowDom} `) - .run(async ({ intakeRegistry }) => { - const div = await getNodeInsideShadowDom('my-div', 'div') + .run(async ({ flushEvents, intakeRegistry, page }) => { + const div = page.locator('my-div div') await div.click() await flushEvents() - expect(intakeRegistry.replaySegments.length).toBe(1) + expect(intakeRegistry.replaySegments).toHaveLength(1) const fullSnapshot = findFullSnapshot(intakeRegistry.replaySegments[0])! const divNode = findElementWithTagName(fullSnapshot.data.node, 'div')! const mouseInteraction = findMouseInteractionRecords( @@ -266,16 +269,17 @@ describe('recorder with shadow DOM', () => { ${divShadowDom} `) - .run(async ({ intakeRegistry }) => { - await browser.execute(() => { + .run(async ({ flushEvents, intakeRegistry, page }) => { + await page.evaluate(() => { const host = document.body.querySelector('#host') as HTMLElement const div = host.shadowRoot!.querySelector('div') as HTMLElement div.innerText = 'titi' }) await flushEvents() - expect(intakeRegistry.replaySegments.length).toBe(1) + expect(intakeRegistry.replaySegments).toHaveLength(1) const { validate, expectInitialNode, expectNewNode } = createMutationPayloadValidatorFromSegment( - intakeRegistry.replaySegments[0] + intakeRegistry.replaySegments[0], + { expect } ) validate({ adds: [ @@ -300,15 +304,15 @@ describe('recorder with shadow DOM', () => { ${scrollableDivShadowDom} `) - .run(async ({ intakeRegistry }) => { - const button = await getNodeInsideShadowDom('my-scrollable-div', 'button') + .run(async ({ flushEvents, intakeRegistry, page }) => { + const button = page.locator('my-scrollable-div button') // Triggering scrollTo from the test itself is not allowed // Thus, a callback to scroll the div was added to the button 'click' event await button.click() await flushEvents() - expect(intakeRegistry.replaySegments.length).toBe(1) + expect(intakeRegistry.replaySegments).toHaveLength(1) const scrollRecord = findIncrementalSnapshot(intakeRegistry.replaySegments[0], IncrementalSource.Scroll) const fullSnapshot = findFullSnapshot(intakeRegistry.replaySegments[0])! const divNode = findElementWithIdAttribute(fullSnapshot.data.node, 'scrollable-div')! @@ -337,12 +341,3 @@ function findElementsInShadowDom(node: SerializedNodeWithId, id: string) { expect(textContent).toBeTruthy() return { shadowHost, shadowRoot, input, text, textContent } } - -async function getNodeInsideShadowDom(hostTag: string, selector: string) { - const host = await $(hostTag) - return host.shadow$(selector) -} - -function isAdoptedStyleSheetsSupported(): Promise { - return browser.execute(() => document.adoptedStyleSheets !== undefined) -} diff --git a/test/e2e/scenario/recorder/viewports.scenario.ts b/test/e2e/scenario/recorder/viewports.scenario.ts index b6b3163d7d..7c056107b0 100644 --- a/test/e2e/scenario/recorder/viewports.scenario.ts +++ b/test/e2e/scenario/recorder/viewports.scenario.ts @@ -1,10 +1,12 @@ import type { ViewportResizeData, ScrollData } from '@datadog/browser-rum/cjs/types' import { IncrementalSource } from '@datadog/browser-rum/cjs/types' -import { findAllIncrementalSnapshots, findAllVisualViewports } from '@datadog/browser-rum/test' +import { findAllIncrementalSnapshots, findAllVisualViewports } from '@datadog/browser-rum/test/segments' +import type { Page } from '@playwright/test' +import { test, expect } from '@playwright/test' +import { wait } from '@datadog/browser-core/test/wait' import type { IntakeRegistry } from '../../lib/framework' -import { flushEvents, createTest, bundleSetup, html } from '../../lib/framework' -import { getBrowserName, getPlatformName } from '../../lib/helpers/browser' +import { createTest, bundleSetup, html } from '../../lib/framework' const NAVBAR_HEIGHT_CHANGE_UPPER_BOUND = 30 const VIEWPORT_META_TAGS = ` @@ -15,39 +17,40 @@ const VIEWPORT_META_TAGS = ` > ` -describe('recorder', () => { - beforeEach(() => { - if (isGestureUnsupported()) { - pending('no touch gesture support') - } +test.describe('recorder', () => { + test.beforeEach(({ hasTouch, browserName }, testInfo) => { + testInfo.skip(!hasTouch, 'no touch gesture support') + testInfo.skip(browserName !== 'chromium', 'only chromium supports touch gestures emulation for now (via CDP)') }) - describe('layout viewport properties', () => { - createTest('getWindowWidth/Height should not be affected by pinch zoom') - .withRum() - .withSetup(bundleSetup) - .withBody(html`${VIEWPORT_META_TAGS}`) - .run(async ({ intakeRegistry }) => { - await buildScrollablePage() + test.describe('layout viewport properties', () => { + test.describe('', () => { + createTest('getWindowWidth/Height should not be affected by pinch zoom') + .withRum() + .withSetup(bundleSetup) + .withBody(html`${VIEWPORT_META_TAGS}`) + .run(async ({ intakeRegistry, page, flushEvents }) => { + await buildScrollablePage(page) - const { innerWidth, innerHeight } = await getWindowInnerDimensions() - await performSignificantZoom() + const { innerWidth, innerHeight } = await getWindowInnerDimensions(page) - await browser.execute(() => { - window.dispatchEvent(new Event('resize')) - }) + await performSignificantZoom(page) + + await page.evaluate(() => { + window.dispatchEvent(new Event('resize')) + }) - const lastViewportResizeData = ( - await getLastRecord(intakeRegistry, (segment) => + await flushEvents() + const lastViewportResizeData = getLastRecord(intakeRegistry, (segment) => findAllIncrementalSnapshots(segment, IncrementalSource.ViewportResize) - ) - ).data as ViewportResizeData + ).data as ViewportResizeData - const scrollbarThicknessCorrection = await getScrollbarThicknessCorrection() + const scrollbarThicknessCorrection = getScrollbarThicknessCorrection(page) - expectToBeNearby(lastViewportResizeData.width, innerWidth - scrollbarThicknessCorrection) - expectToBeNearby(lastViewportResizeData.height, innerHeight - scrollbarThicknessCorrection) - }) + expectToBeNearby(lastViewportResizeData.width, innerWidth - scrollbarThicknessCorrection) + expectToBeNearby(lastViewportResizeData.height, innerHeight - scrollbarThicknessCorrection) + }) + }) /** * window.ScrollX/Y on some devices/browsers are changed by pinch zoom @@ -57,30 +60,29 @@ describe('recorder', () => { .withRum() .withSetup(bundleSetup) .withBody(html`${VIEWPORT_META_TAGS}`) - .run(async ({ intakeRegistry }) => { + .run(async ({ intakeRegistry, flushEvents, page }) => { const VISUAL_SCROLL_DOWN_PX = 60 const LAYOUT_SCROLL_AMOUNT = 20 - await buildScrollablePage() - await performSignificantZoom() - await resetWindowScroll() + await buildScrollablePage(page) + await performSignificantZoom(page) + await resetWindowScroll(page) - const initialVisualViewport = await getVisualViewport() - const { scrollX: initialScrollX, scrollY: initialScrollY } = await getWindowScroll() + const initialVisualViewport = await getVisualViewport(page) + const { scrollX: initialScrollX, scrollY: initialScrollY } = await getWindowScroll(page) // Add Visual Viewport Scroll - await visualScrollVerticallyDown(VISUAL_SCROLL_DOWN_PX) + await visualScrollVerticallyDown(page, VISUAL_SCROLL_DOWN_PX) // Add Layout Viewport Scroll - await layoutScrollTo(LAYOUT_SCROLL_AMOUNT, LAYOUT_SCROLL_AMOUNT) + await layoutScrollTo(page, LAYOUT_SCROLL_AMOUNT, LAYOUT_SCROLL_AMOUNT) - const nextVisualViewport = await getVisualViewport() - const { scrollX: nextScrollX, scrollY: nextScrollY } = await getWindowScroll() + const nextVisualViewport = await getVisualViewport(page) + const { scrollX: nextScrollX, scrollY: nextScrollY } = await getWindowScroll(page) - const lastScrollData = ( - await getLastRecord(intakeRegistry, (segment) => - findAllIncrementalSnapshots(segment, IncrementalSource.Scroll) - ) + await flushEvents() + const lastScrollData = getLastRecord(intakeRegistry, (segment) => + findAllIncrementalSnapshots(segment, IncrementalSource.Scroll) ).data as ScrollData // Height changes because URL address bar changes due to scrolling @@ -95,18 +97,19 @@ describe('recorder', () => { }) }) - describe('visual viewport properties', () => { + test.describe('visual viewport properties', () => { createTest('pinch zoom "scroll" event reports visual viewport position') .withRum() .withSetup(bundleSetup) .withBody(html`${VIEWPORT_META_TAGS}`) - .run(async ({ intakeRegistry }) => { + .run(async ({ intakeRegistry, page, flushEvents }) => { const VISUAL_SCROLL_DOWN_PX = 100 - await buildScrollablePage() - await performSignificantZoom() - await visualScrollVerticallyDown(VISUAL_SCROLL_DOWN_PX) - const nextVisualViewportDimension = await getVisualViewport() - const lastVisualViewportRecord = await getLastRecord(intakeRegistry, findAllVisualViewports) + await buildScrollablePage(page) + await performSignificantZoom(page) + await visualScrollVerticallyDown(page, VISUAL_SCROLL_DOWN_PX) + const nextVisualViewportDimension = await getVisualViewport(page) + await flushEvents() + const lastVisualViewportRecord = getLastRecord(intakeRegistry, findAllVisualViewports) expectToBeNearby(lastVisualViewportRecord.data.pageTop, nextVisualViewportDimension.pageTop) }) @@ -114,18 +117,16 @@ describe('recorder', () => { .withRum() .withSetup(bundleSetup) .withBody(html`${VIEWPORT_META_TAGS}`) - .run(async ({ intakeRegistry }) => { - await performSignificantZoom() - const nextVisualViewportDimension = await getVisualViewport() - const lastVisualViewportRecord = await getLastRecord(intakeRegistry, findAllVisualViewports) + .run(async ({ intakeRegistry, page, flushEvents }) => { + await performSignificantZoom(page) + const nextVisualViewportDimension = await getVisualViewport(page) + await flushEvents() + const lastVisualViewportRecord = getLastRecord(intakeRegistry, findAllVisualViewports) expectToBeNearby(lastVisualViewportRecord.data.scale, nextVisualViewportDimension.scale) }) }) }) -const isGestureUnsupported = () => - /firefox|safari|edge/.test(getBrowserName()) || /windows|linux/.test(getPlatformName()) - // Flakiness: Working with viewport sizes has variations per device of a few pixels function expectToBeNearby(numA: number, numB: number) { const test = Math.abs(numA - numB) <= 5 @@ -135,54 +136,49 @@ function expectToBeNearby(numA: number, numB: number) { } } -async function pinchZoom(xChange: number) { +async function pinchZoom(page: Page, xChange: number) { // Cannot exceed the bounds of a device's screen, at start or end positions. // So pick a midpoint on small devices, roughly 180px. const xBase = 180 const yBase = 180 const xOffsetFingerTwo = 25 - // Scrolling too fast can show or hide the address bar on some device browsers. - const moveDurationMs = 400 const pauseDurationMs = 150 - const actions = [ - { - type: 'pointer', - id: 'finger1', - parameters: { pointerType: 'touch' }, - actions: [ - { type: 'pointerMove', duration: 0, x: xBase, y: yBase }, - { type: 'pointerDown', button: 0 }, - { type: 'pause', duration: pauseDurationMs }, - { type: 'pointerMove', duration: moveDurationMs, origin: 'pointer', x: -xChange, y: 0 }, - { type: 'pointerUp', button: 0 }, - ], - }, - { - type: 'pointer', - id: 'finger2', - parameters: { pointerType: 'touch' }, - actions: [ - { type: 'pointerMove', duration: 0, x: xBase + xOffsetFingerTwo, y: yBase }, - { type: 'pointerDown', button: 0 }, - { type: 'pause', duration: pauseDurationMs }, - { type: 'pointerMove', duration: moveDurationMs, origin: 'pointer', x: +xChange, y: 0 }, - { type: 'pointerUp', button: 0 }, - ], - }, - ] - await browser.performActions(actions) + + const cdp = await page.context().newCDPSession(page) + await cdp.send('Input.dispatchTouchEvent', { + type: 'touchStart', + touchPoints: [ + { x: xBase, y: yBase, id: 0 }, + { x: xBase + xOffsetFingerTwo, y: yBase, id: 1 }, + ], + }) + await wait(pauseDurationMs) + await cdp.send('Input.dispatchTouchEvent', { + type: 'touchMove', + touchPoints: [ + { x: xBase, y: yBase, id: 0 }, + { x: xBase + xChange, y: yBase, id: 1 }, + ], + }) + await cdp.send('Input.dispatchTouchEvent', { + type: 'touchEnd', + touchPoints: [ + { x: xBase, y: yBase, id: 0 }, + { x: xBase + xChange, y: yBase, id: 1 }, + ], + }) } -async function performSignificantZoom() { - const initialVisualViewport = await getVisualViewport() - await pinchZoom(150) - await pinchZoom(150) - const nextVisualViewport = await getVisualViewport() +async function performSignificantZoom(page: Page) { + const initialVisualViewport = await getVisualViewport(page) + await pinchZoom(page, 150) + await pinchZoom(page, 150) + const nextVisualViewport = await getVisualViewport(page) // Test the test: ensure pinch zoom was applied expect(initialVisualViewport.scale < nextVisualViewport.scale).toBeTruthy() } -async function visualScrollVerticallyDown(yChange: number) { +async function visualScrollVerticallyDown(page: Page, yChange: number) { // Providing a negative offset value will scroll up. // NOTE: Some devices may invert scroll direction // Cannot exceed the bounds of a device's screen, at start or end positions. @@ -190,28 +186,26 @@ async function visualScrollVerticallyDown(yChange: number) { const xBase = 180 const yBase = 180 // Scrolling too fast can show or hide the address bar on some device browsers. - const moveDurationMs = 800 const pauseDurationMs = 150 - const actions = [ - { - type: 'pointer', - id: 'finger1', - parameters: { pointerType: 'touch' }, - actions: [ - { type: 'pointerMove', duration: 0, x: xBase, y: yBase }, - { type: 'pointerDown', button: 0 }, - { type: 'pause', duration: pauseDurationMs }, - { type: 'pointerMove', duration: moveDurationMs, origin: 'pointer', x: 0, y: -yChange }, - { type: 'pointerUp', button: 0 }, - ], - }, - ] - await browser.performActions(actions) + const cdp = await page.context().newCDPSession(page) + await cdp.send('Input.dispatchTouchEvent', { + type: 'touchStart', + touchPoints: [{ x: xBase, y: yBase, id: 0 }], + }) + await wait(pauseDurationMs) + await cdp.send('Input.dispatchTouchEvent', { + type: 'touchMove', + touchPoints: [{ x: xBase, y: yBase - yChange, id: 0 }], + }) + await cdp.send('Input.dispatchTouchEvent', { + type: 'touchEnd', + touchPoints: [{ x: xBase, y: yBase - yChange, id: 0 }], + }) } -async function buildScrollablePage() { - await browser.execute(() => { +async function buildScrollablePage(page: Page) { + await page.evaluate(() => { document.documentElement.style.setProperty('width', '5000px') document.documentElement.style.setProperty('height', '5000px') document.documentElement.style.setProperty('margin', '0px') @@ -233,8 +227,8 @@ interface VisualViewportData { pageTop: number } -function getVisualViewport(): Promise { - return browser.execute(() => { +function getVisualViewport(page: Page): Promise { + return page.evaluate(() => { const visual = window.visualViewport || ({} as Record) return { scale: visual.scale, @@ -248,76 +242,78 @@ function getVisualViewport(): Promise { }) as Promise } -function getWindowScroll() { - return browser.execute(() => ({ +function getWindowScroll(page: Page) { + return page.evaluate(() => ({ scrollX: window.scrollX, scrollY: window.scrollY, })) as Promise<{ scrollX: number; scrollY: number }> } -function getScrollbarThickness(): Promise { - // https://stackoverflow.com/questions/13382516/getting-scroll-bar-width-using-javascript#answer-13382873 - return browser.execute(() => { - // Creating invisible container - const outer = document.createElement('div') - outer.style.visibility = 'hidden' - outer.style.overflow = 'scroll' // forcing scrollbar to appear - ;(outer.style as any).msOverflowStyle = 'scrollbar' // needed for WinJS apps - document.body.appendChild(outer) - // Creating inner element and placing it in the container - const inner = document.createElement('div') - outer.appendChild(inner) - // Calculating difference between container's full width and the child width - const scrollbarThickness = outer.offsetWidth - inner.offsetWidth - // Removing temporary elements from the DOM - document.body.removeChild(outer) - return scrollbarThickness - }) -} +// TODO(playwright migration): I'm not sure if this is still needed? +// function getScrollbarThickness(page: Page): Promise { +// // https://stackoverflow.com/questions/13382516/getting-scroll-bar-width-using-javascript#answer-13382873 +// return page.evaluate(() => { +// // Creating invisible container +// const outer = document.createElement('div') +// outer.style.visibility = 'hidden' +// outer.style.overflow = 'scroll' // forcing scrollbar to appear +// ;(outer.style as any).msOverflowStyle = 'scrollbar' // needed for WinJS apps +// document.body.appendChild(outer) +// // Creating inner element and placing it in the container +// const inner = document.createElement('div') +// outer.appendChild(inner) +// // Calculating difference between container's full width and the child width +// const scrollbarThickness = outer.offsetWidth - inner.offsetWidth +// // Removing temporary elements from the DOM +// document.body.removeChild(outer) +// return scrollbarThickness +// }) +// } // Mac OS X Chrome scrollbars are included here (~15px) which seems to be against spec // Scrollbar edge-case handling not considered right now, further investigation needed -async function getScrollbarThicknessCorrection(): Promise { - let scrollbarThickness = 0 - if (getBrowserName() === 'chrome' && getPlatformName() === 'macos') { - scrollbarThickness = await getScrollbarThickness() - } +function getScrollbarThicknessCorrection(_page: Page): number { + const scrollbarThickness = 0 + + // TODO(playwright migration): I'm not sure if this is still needed? + // if (getBrowserName() === 'chrome' && getPlatformName() === 'macos') { + // scrollbarThickness = await getScrollbarThickness(page) + // } + return scrollbarThickness } -async function getLastRecord(intakeRegistry: IntakeRegistry, filterMethod: (segment: any) => T[]): Promise { - await flushEvents() +function getLastRecord(intakeRegistry: IntakeRegistry, filterMethod: (segment: any) => T[]): T { const segment = intakeRegistry.replaySegments.at(-1) const foundRecords = filterMethod(segment) return foundRecords[foundRecords.length - 1] } -function getWindowInnerDimensions() { - return browser.execute(() => ({ +function getWindowInnerDimensions(page: Page) { + return page.evaluate(() => ({ innerWidth: window.innerWidth, innerHeight: window.innerHeight, })) as Promise<{ innerWidth: number; innerHeight: number }> } -async function resetWindowScroll() { - await browser.execute(() => { +async function resetWindowScroll(page: Page) { + await page.evaluate(() => { window.scrollTo(-500, -500) }) - const { scrollX: nextScrollX, scrollY: nextScrollY } = await getWindowScroll() + const { scrollX: nextScrollX, scrollY: nextScrollY } = await getWindowScroll(page) // Ensure our methods are applied correctly expect(nextScrollX).toBe(0) expect(nextScrollY).toBe(0) } -async function layoutScrollTo(scrollX: number, scrollY: number) { - await browser.execute( - (x, y) => { - window.scrollTo(x, y) +async function layoutScrollTo(page: Page, scrollX: number, scrollY: number) { + await page.evaluate( + ({ scrollX, scrollY }) => { + window.scrollTo(scrollX, scrollY) }, - scrollX, - scrollY + { scrollX, scrollY } ) - const { scrollX: nextScrollX, scrollY: nextScrollY } = await getWindowScroll() + const { scrollX: nextScrollX, scrollY: nextScrollY } = await getWindowScroll(page) // Ensure our methods are applied correctly expect(scrollX).toBe(nextScrollX) expect(scrollY).toBe(nextScrollY) diff --git a/test/e2e/scenario/rum/actions.scenario.ts b/test/e2e/scenario/rum/actions.scenario.ts index 927aca0ea6..632d4d1d27 100644 --- a/test/e2e/scenario/rum/actions.scenario.ts +++ b/test/e2e/scenario/rum/actions.scenario.ts @@ -1,7 +1,7 @@ -import { getBrowserName, withBrowserLogs } from '../../lib/helpers/browser' -import { createTest, flushEvents, html, waitForServersIdle } from '../../lib/framework' +import { test, expect } from '@playwright/test' +import { createTest, html, waitForServersIdle } from '../../lib/framework' -describe('action collection', () => { +test.describe('action collection', () => { createTest('track a click action') .withRum({ trackUserInteractions: true, enableExperimentalFeatures: ['action_name_masking'] }) .withBody(html` @@ -13,23 +13,23 @@ describe('action collection', () => { }) `) - .run(async ({ intakeRegistry }) => { - const button = await $('button') + .run(async ({ intakeRegistry, flushEvents, page }) => { + const button = page.locator('button') await button.click() await flushEvents() const actionEvents = intakeRegistry.rumActionEvents - expect(actionEvents.length).toBe(1) + expect(actionEvents).toHaveLength(1) expect(actionEvents[0]).toEqual( - jasmine.objectContaining({ + expect.objectContaining({ action: { error: { count: 0, }, - id: jasmine.any(String), - loading_time: jasmine.any(Number), + id: expect.any(String), + loading_time: expect.any(Number), long_task: { - count: jasmine.any(Number), + count: expect.any(Number), }, resource: { count: 0, @@ -42,17 +42,17 @@ describe('action collection', () => { type: [], }, }, - _dd: jasmine.objectContaining({ + _dd: expect.objectContaining({ action: { - target: jasmine.objectContaining({ - selector: jasmine.any(String), - width: jasmine.any(Number), - height: jasmine.any(Number), + target: expect.objectContaining({ + selector: expect.any(String), + width: expect.any(Number), + height: expect.any(Number), }), name_source: 'text_content', position: { - x: jasmine.any(Number), - y: jasmine.any(Number), + x: expect.any(Number), + y: expect.any(Number), }, }, }), @@ -73,43 +73,44 @@ describe('action collection', () => { }) `) - .run(async ({ intakeRegistry }) => { - const button = await $('button') + .run(async ({ intakeRegistry, flushEvents, page }) => { + const button = page.locator('button') await button.click() await flushEvents() const actionEvents = intakeRegistry.rumActionEvents - expect(actionEvents.length).toBe(1) + expect(actionEvents).toHaveLength(1) expect(actionEvents[0].action?.target?.name).toBe('click me') expect(actionEvents[0]._dd.action?.target?.selector).toBe('BODY>BUTTON') }) - // When the target element changes between mousedown and mouseup, Firefox does not dispatch a - // click event. Skip this test. - if (getBrowserName() !== 'firefox') { - createTest('does not report a click on the body when the target element changes between mousedown and mouseup') - .withRum({ trackUserInteractions: true }) - .withBody(html` - - - `) - .run(async ({ intakeRegistry }) => { - const button = await $('button') - await button.click() - await flushEvents() - const actionEvents = intakeRegistry.rumActionEvents - - expect(actionEvents.length).toBe(1) - expect(actionEvents[0].action?.target?.name).toBe('click me') - }) - } + createTest('does not report a click on the body when the target element changes between mousedown and mouseup') + .withRum({ trackUserInteractions: true }) + .withBody(html` + + + `) + .run(async ({ intakeRegistry, flushEvents, browserName, page }) => { + test.skip( + browserName.includes('firefox'), + 'When the target element changes between mousedown and mouseup, Firefox does not dispatch a click event.' + ) + + const button = page.locator('button') + await button.click() + await flushEvents() + const actionEvents = intakeRegistry.rumActionEvents + + expect(actionEvents).toHaveLength(1) + expect(actionEvents[0].action?.target?.name).toBe('click me') + }) createTest('associate a request to its action') .withRum({ trackUserInteractions: true }) @@ -122,23 +123,23 @@ describe('action collection', () => { }) `) - .run(async ({ intakeRegistry }) => { - const button = await $('button') + .run(async ({ intakeRegistry, flushEvents, page }) => { + const button = page.locator('button') await button.click() await waitForServersIdle() await flushEvents() const actionEvents = intakeRegistry.rumActionEvents const resourceEvents = intakeRegistry.rumResourceEvents.filter((event) => event.resource.type === 'fetch') - expect(actionEvents.length).toBe(1) + expect(actionEvents).toHaveLength(1) expect(actionEvents[0].action).toEqual({ error: { count: 0, }, - id: jasmine.any(String) as unknown as string, - loading_time: jasmine.any(Number) as unknown as number, + id: expect.any(String) as unknown as string, + loading_time: expect.any(Number) as unknown as number, long_task: { - count: jasmine.any(Number) as unknown as number, + count: expect.any(Number) as unknown as number, }, resource: { count: 1, @@ -152,9 +153,9 @@ describe('action collection', () => { }, }) - expect(resourceEvents.length).toBe(1) + expect(resourceEvents).toHaveLength(1) // resource action id should contain the collected action id + the discarded rage click id - expect(resourceEvents[0].action!.id.length).toBe(2) + expect(resourceEvents[0].action!.id).toHaveLength(2) expect(resourceEvents[0].action!.id).toContain(actionEvents[0].action.id!) }) @@ -169,12 +170,12 @@ describe('action collection', () => { }) `) - .run(async ({ intakeRegistry }) => { - const button = await $('button') + .run(async ({ intakeRegistry, flushEvents, page }) => { + const button = page.locator('button') await button.click() await flushEvents() const actionEvents = intakeRegistry.rumActionEvents - expect(actionEvents.length).toBe(1) + expect(actionEvents).toHaveLength(1) const viewEvents = intakeRegistry.rumViewEvents const originalViewEvent = viewEvents.find((view) => view.view.url.endsWith('/'))! @@ -195,33 +196,33 @@ describe('action collection', () => { }) `) - .run(async ({ intakeRegistry }) => { - const button = await $('button') + .run(async ({ intakeRegistry, flushEvents, withBrowserLogs, page }) => { + const button = page.locator('button') await button.click() await flushEvents() const actionEvents = intakeRegistry.rumActionEvents - expect(actionEvents.length).toBe(1) + expect(actionEvents).toHaveLength(1) expect(actionEvents[0].action.frustration!.type).toEqual(['error_click']) expect(actionEvents[0].action.error!.count).toBe(1) expect(intakeRegistry.rumViewEvents[0].view.frustration!.count).toBe(1) - await withBrowserLogs((browserLogs) => { - expect(browserLogs.length).toEqual(1) + withBrowserLogs((browserLogs) => { + expect(browserLogs).toHaveLength(1) }) }) createTest('collect a "dead click"') .withRum({ trackUserInteractions: true }) .withBody(html` `) - .run(async ({ intakeRegistry }) => { - const button = await $('button') + .run(async ({ intakeRegistry, flushEvents, page }) => { + const button = page.locator('button') await button.click() await flushEvents() const actionEvents = intakeRegistry.rumActionEvents - expect(actionEvents.length).toBe(1) + expect(actionEvents).toHaveLength(1) expect(actionEvents[0].action.frustration!.type).toEqual(['dead_click']) expect(intakeRegistry.rumViewEvents[0].view.frustration!.count).toBe(1) @@ -230,53 +231,53 @@ describe('action collection', () => { createTest('do not consider a click on a checkbox as "dead_click"') .withRum({ trackUserInteractions: true }) .withBody(html` `) - .run(async ({ intakeRegistry }) => { - const input = await $('input') + .run(async ({ intakeRegistry, flushEvents, page }) => { + const input = page.locator('input') await input.click() await flushEvents() const actionEvents = intakeRegistry.rumActionEvents - expect(actionEvents.length).toBe(1) - expect(actionEvents[0].action.frustration!.type).toEqual([]) + expect(actionEvents).toHaveLength(1) + expect(actionEvents[0].action.frustration!.type).toHaveLength(0) }) createTest('do not consider a click to change the value of a "range" input as "dead_click"') .withRum({ trackUserInteractions: true }) .withBody(html` `) - .run(async ({ intakeRegistry }) => { - const input = await $('input') - await input.click({ x: 10 }) + .run(async ({ intakeRegistry, flushEvents, page }) => { + const input = page.locator('input') + await input.click({ position: { x: 10, y: 0 } }) await flushEvents() const actionEvents = intakeRegistry.rumActionEvents - expect(actionEvents.length).toBe(1) - expect(actionEvents[0].action.frustration!.type).toEqual([]) + expect(actionEvents).toHaveLength(1) + expect(actionEvents[0].action.frustration!.type).toHaveLength(0) }) createTest('consider a click on an already checked "radio" input as "dead_click"') .withRum({ trackUserInteractions: true }) .withBody(html` `) - .run(async ({ intakeRegistry }) => { - const input = await $('input') + .run(async ({ intakeRegistry, flushEvents, page }) => { + const input = page.locator('input') await input.click() await flushEvents() const actionEvents = intakeRegistry.rumActionEvents - expect(actionEvents.length).toBe(1) + expect(actionEvents).toHaveLength(1) expect(actionEvents[0].action.frustration!.type).toEqual(['dead_click']) }) createTest('do not consider a click on text input as "dead_click"') .withRum({ trackUserInteractions: true }) .withBody(html` `) - .run(async ({ intakeRegistry }) => { - const input = await $('input') + .run(async ({ intakeRegistry, flushEvents, page }) => { + const input = page.locator('input') await input.click() await flushEvents() const actionEvents = intakeRegistry.rumActionEvents - expect(actionEvents.length).toBe(1) - expect(actionEvents[0].action.frustration!.type).toEqual([]) + expect(actionEvents).toHaveLength(1) + expect(actionEvents[0].action.frustration!.type).toHaveLength(0) }) createTest('do not consider clicks leading to scrolls as "dead_click"') @@ -292,15 +293,15 @@ describe('action collection', () => { `) - .run(async ({ intakeRegistry }) => { - const button = await $('button') + .run(async ({ intakeRegistry, flushEvents, page }) => { + const button = page.locator('button') await button.click() await flushEvents() const actionEvents = intakeRegistry.rumActionEvents - expect(actionEvents.length).toBe(1) - expect(actionEvents[0].action.frustration!.type).toEqual([]) + expect(actionEvents).toHaveLength(1) + expect(actionEvents[0].action.frustration!.type).toHaveLength(0) }) createTest('do not consider clicks leading to scrolls as "rage_click"') @@ -316,15 +317,17 @@ describe('action collection', () => { `) - .run(async ({ intakeRegistry }) => { - const button = await $('button') - await Promise.all([button.click(), button.click(), button.click()]) + .run(async ({ intakeRegistry, flushEvents, page }) => { + const button = page.locator('button') + await button.click() + await button.click() + await button.click() await flushEvents() const actionEvents = intakeRegistry.rumActionEvents - expect(actionEvents.length).toBe(3) - expect(actionEvents[0].action.frustration!.type).toEqual([]) + expect(actionEvents).toHaveLength(3) + expect(actionEvents[0].action.frustration!.type).toHaveLength(0) }) createTest('do not consider a click that open a new window as "dead_click"') @@ -338,20 +341,15 @@ describe('action collection', () => { }) `) - .run(async ({ intakeRegistry }) => { - const windowHandle = await browser.getWindowHandle() - const button = await $('button') + .run(async ({ intakeRegistry, flushEvents, page }) => { + const button = page.locator('button') await button.click() - // Ideally, we would close the newly created window. But on Safari desktop (at least), it is - // not possible to do so: calling `browser.closeWindow()` is failing with "no such window: - // unknown error". Instead, just switch back to the original window. - await browser.switchToWindow(windowHandle) await flushEvents() const actionEvents = intakeRegistry.rumActionEvents - expect(actionEvents.length).toBe(1) - expect(actionEvents[0].action.frustration!.type).toEqual([]) + expect(actionEvents).toHaveLength(1) + expect(actionEvents[0].action.frustration!.type).toHaveLength(0) }) createTest('collect a "rage click"') @@ -365,10 +363,10 @@ describe('action collection', () => { }) `) - .run(async ({ intakeRegistry }) => { + .run(async ({ intakeRegistry, page, flushEvents }) => { // We don't use the wdio's `$('button').click()` here because the latency of the command is too high and the // clicks won't be recognised as rage clicks. - await browser.execute(() => { + await page.evaluate(() => { const button = document.querySelector('button')! function click() { @@ -386,7 +384,7 @@ describe('action collection', () => { await flushEvents() const actionEvents = intakeRegistry.rumActionEvents - expect(actionEvents.length).toBe(1) + expect(actionEvents).toHaveLength(1) expect(actionEvents[0].action.frustration!.type).toEqual(['rage_click']) }) @@ -401,25 +399,23 @@ describe('action collection', () => { }) `) - .run(async ({ intakeRegistry }) => { - const button = await $('button') + .run(async ({ intakeRegistry, flushEvents, withBrowserLogs, page }) => { + const button = page.locator('button') await button.click() await flushEvents() const actionEvents = intakeRegistry.rumActionEvents - expect(actionEvents.length).toBe(1) - expect(actionEvents[0].action.frustration!.type).toEqual( - jasmine.arrayWithExactContents(['error_click', 'dead_click']) - ) + expect(actionEvents).toHaveLength(1) + expect(actionEvents[0].action.frustration!.type).toStrictEqual(['error_click', 'dead_click']) expect(intakeRegistry.rumViewEvents[0].view.frustration!.count).toBe(2) - await withBrowserLogs((browserLogs) => { - expect(browserLogs.length).toEqual(1) + withBrowserLogs((browserLogs) => { + expect(browserLogs).toHaveLength(1) }) }) - // We don't use the wdio's `$('button').click()` here because it makes the test slower + // We don't use the playwright's `page.locator('button').click()` here because it makes the test slower createTest('dont crash when clicking on a button') .withRum({ trackUserInteractions: true }) .withBody(html` @@ -441,13 +437,13 @@ describe('action collection', () => { click() } - window.open('foo') + window.open('/empty') `) - .run(async () => { - await withBrowserLogs((logs) => { + .run(({ withBrowserLogs }) => { + withBrowserLogs((logs) => { // A failing test would have a log with message "Uncaught RangeError: Maximum call stack size exceeded" - expect(logs).toEqual([]) + expect(logs).toHaveLength(0) }) }) }) diff --git a/test/e2e/scenario/rum/errors.scenario.ts b/test/e2e/scenario/rum/errors.scenario.ts index 6fb7ab360d..0e7af431b9 100644 --- a/test/e2e/scenario/rum/errors.scenario.ts +++ b/test/e2e/scenario/rum/errors.scenario.ts @@ -1,6 +1,6 @@ import type { RumErrorEvent } from '@datadog/browser-rum-core' -import { createTest, flushEvents, html } from '../../lib/framework' -import { getBrowserName, getPlatformName, withBrowserLogs } from '../../lib/helpers/browser' +import { test, expect } from '@playwright/test' +import { createTest, html } from '../../lib/framework' // Note: using `browser.execute` to throw exceptions may result in "Script error." being reported, // because WDIO is evaluating the script in a different context than the page. @@ -19,36 +19,36 @@ function createBody(errorGenerator: string) { ` } -describe('rum errors', () => { +test.describe('rum errors', () => { createTest('send console.error errors') .withRum() .withBody(createBody('console.error("oh snap")')) - .run(async ({ intakeRegistry, baseUrl }) => { - const button = await $('button') + .run(async ({ page, intakeRegistry, baseUrl, flushEvents, withBrowserLogs }) => { + const button = page.locator('button') await button.click() await flushEvents() - expect(intakeRegistry.rumErrorEvents.length).toBe(1) + expect(intakeRegistry.rumErrorEvents).toHaveLength(1) expectError(intakeRegistry.rumErrorEvents[0].error, { message: 'oh snap', source: 'console', handlingStack: ['Error: ', `handler @ ${baseUrl}/:`], handling: 'handled', }) - await withBrowserLogs((browserLogs) => { - expect(browserLogs.length).toEqual(1) + withBrowserLogs((browserLogs) => { + expect(browserLogs).toHaveLength(1) }) }) createTest('pass Error instance to console.error') .withRum() .withBody(createBody('console.error("Foo:", foo())')) - .run(async ({ intakeRegistry, baseUrl }) => { - const button = await $('button') + .run(async ({ page, flushEvents, intakeRegistry, baseUrl, withBrowserLogs }) => { + const button = page.locator('button') await button.click() await flushEvents() - expect(intakeRegistry.rumErrorEvents.length).toBe(1) + expect(intakeRegistry.rumErrorEvents).toHaveLength(1) expectError(intakeRegistry.rumErrorEvents[0].error, { message: 'Foo: Error: oh snap', source: 'console', @@ -56,60 +56,60 @@ describe('rum errors', () => { handlingStack: ['Error: ', `handler @ ${baseUrl}/:`], handling: 'handled', }) - await withBrowserLogs((browserLogs) => { - expect(browserLogs.length).toEqual(1) + withBrowserLogs((browserLogs) => { + expect(browserLogs).toHaveLength(1) }) }) createTest('send uncaught exceptions') .withRum() .withBody(createBody('throw foo()')) - .run(async ({ intakeRegistry, baseUrl }) => { - const button = await $('button') + .run(async ({ page, flushEvents, intakeRegistry, baseUrl, withBrowserLogs }) => { + const button = page.locator('button') await button.click() await flushEvents() - expect(intakeRegistry.rumErrorEvents.length).toBe(1) + expect(intakeRegistry.rumErrorEvents).toHaveLength(1) expectError(intakeRegistry.rumErrorEvents[0].error, { message: 'oh snap', source: 'source', stack: ['Error: oh snap', `at foo @ ${baseUrl}/:`, `handler @ ${baseUrl}/:`], handling: 'unhandled', }) - await withBrowserLogs((browserLogs) => { - expect(browserLogs.length).toEqual(1) + withBrowserLogs((browserLogs) => { + expect(browserLogs).toHaveLength(1) }) }) createTest('send unhandled rejections') .withRum() .withBody(createBody('Promise.reject(foo())')) - .run(async ({ intakeRegistry, baseUrl }) => { - const button = await $('button') + .run(async ({ flushEvents, page, intakeRegistry, baseUrl, withBrowserLogs }) => { + const button = page.locator('button') await button.click() await flushEvents() - expect(intakeRegistry.rumErrorEvents.length).toBe(1) + expect(intakeRegistry.rumErrorEvents).toHaveLength(1) expectError(intakeRegistry.rumErrorEvents[0].error, { message: 'oh snap', source: 'source', stack: ['Error: oh snap', `at foo @ ${baseUrl}/:`, `handler @ ${baseUrl}/:`], handling: 'unhandled', }) - await withBrowserLogs((browserLogs) => { - expect(browserLogs.length).toEqual(1) + withBrowserLogs((browserLogs) => { + expect(browserLogs).toHaveLength(1) }) }) createTest('send custom errors') .withRum() .withBody(createBody('DD_RUM.addError(foo())')) - .run(async ({ intakeRegistry, baseUrl }) => { - const button = await $('button') + .run(async ({ flushEvents, page, intakeRegistry, baseUrl, withBrowserLogs }) => { + const button = page.locator('button') await button.click() await flushEvents() - expect(intakeRegistry.rumErrorEvents.length).toBe(1) + expect(intakeRegistry.rumErrorEvents).toHaveLength(1) expectError(intakeRegistry.rumErrorEvents[0].error, { message: 'oh snap', source: 'custom', @@ -117,8 +117,8 @@ describe('rum errors', () => { handlingStack: ['Error: ', `handler @ ${baseUrl}/:`], handling: 'handled', }) - await withBrowserLogs((browserLogs) => { - expect(browserLogs.length).toEqual(0) + withBrowserLogs((browserLogs) => { + expect(browserLogs).toHaveLength(0) }) }) @@ -126,40 +126,41 @@ describe('rum errors', () => { // - Safari < 15 don't report the property disposition // - Firefox < 99 don't report csp violation at all // TODO: Remove this condition when upgrading to Safari 15 and Firefox 99 (see: https://datadoghq.atlassian.net/browse/RUM-1063) - if (!((getBrowserName() === 'safari' && getPlatformName() === 'macos') || getBrowserName() === 'firefox')) { - createTest('send CSP violation errors') - .withRum() - .withBody( - createBody(` + createTest('send CSP violation errors') + .withRum() + .withBody( + createBody(` const script = document.createElement('script'); script.src = "https://example.com/foo.js" document.body.appendChild(script) `) - ) - .run(async ({ intakeRegistry, baseUrl }) => { - const button = await $('button') - await button.click() - - await flushEvents() - - expect(intakeRegistry.rumErrorEvents.length).toBe(1) - expectError(intakeRegistry.rumErrorEvents[0].error, { - message: /^csp_violation: 'https:\/\/example\.com\/foo\.js' blocked by 'script-src(-elem)?' directive$/, - source: 'report', - stack: [ - /^script-src(-elem)?: 'https:\/\/example\.com\/foo\.js' blocked by 'script-src(-elem)?' directive of the policy/, - ` at @ ${baseUrl}/:`, - ], - handling: 'unhandled', - csp: { - disposition: 'enforce', - }, - }) - await withBrowserLogs((browserLogs) => { - expect(browserLogs.length).toEqual(1) - }) + ) + .run(async ({ page, browserName, intakeRegistry, baseUrl, flushEvents, withBrowserLogs }) => { + const userAgent = await page.evaluate(() => navigator.userAgent) + test.skip(browserName.includes('firefox') || (browserName.includes('webkit') && userAgent.includes('Mac OS X'))) + + const button = page.locator('button') + await button.click() + + await flushEvents() + + expect(intakeRegistry.rumErrorEvents).toHaveLength(1) + expectError(intakeRegistry.rumErrorEvents[0].error, { + message: /^csp_violation: 'https:\/\/example\.com\/foo\.js' blocked by 'script-src(-elem)?' directive$/, + source: 'report', + stack: [ + /^script-src(-elem)?: 'https:\/\/example\.com\/foo\.js' blocked by 'script-src(-elem)?' directive of the policy/, + ` at @ ${baseUrl}/:`, + ], + handling: 'unhandled', + csp: { + disposition: 'enforce', + }, }) - } + withBrowserLogs((browserLogs) => { + expect(browserLogs).toHaveLength(1) + }) + }) }) function expectError( @@ -189,7 +190,9 @@ function expectStack(stack: string | undefined, expectedLines?: Array { if (typeof line !== 'string') { return expect(actualLines[i]).toMatch(line) diff --git a/test/e2e/scenario/rum/init.scenario.ts b/test/e2e/scenario/rum/init.scenario.ts index 003bf68bdf..38a2b5d652 100644 --- a/test/e2e/scenario/rum/init.scenario.ts +++ b/test/e2e/scenario/rum/init.scenario.ts @@ -1,19 +1,19 @@ import type { Context } from '@datadog/browser-core' +import { test, expect } from '@playwright/test' import type { IntakeRegistry } from '../../lib/framework' -import { flushEvents, createTest } from '../../lib/framework' -import { withBrowserLogs } from '../../lib/helpers/browser' +import { createTest } from '../../lib/framework' -describe('API calls and events around init', () => { +test.describe('API calls and events around init', () => { createTest('should display a console log when calling init without configuration') .withRum() .withRumInit(() => { ;(window.DD_RUM! as unknown as { init(): void }).init() }) - .run(async () => { - await withBrowserLogs((logs) => { - expect(logs.length).toBe(1) - expect(logs[0].message).toEqual(jasmine.stringContaining('Datadog Browser SDK')) - expect(logs[0].message).toEqual(jasmine.stringContaining('Missing configuration')) + .run(({ withBrowserLogs }) => { + withBrowserLogs((logs) => { + expect(logs).toHaveLength(1) + expect(logs[0].message).toEqual(expect.stringContaining('Datadog Browser SDK')) + expect(logs[0].message).toEqual(expect.stringContaining('Missing configuration')) }) }) @@ -35,19 +35,19 @@ describe('API calls and events around init', () => { setTimeout(() => window.DD_RUM!.init(configuration), 30) }) - .run(async ({ intakeRegistry }) => { + .run(async ({ intakeRegistry, flushEvents }) => { await flushEvents() const initialView = intakeRegistry.rumViewEvents[0] expect(initialView.view.name).toBeUndefined() expect(initialView.view.custom_timings).toEqual({ - before_manual_view: jasmine.any(Number), + before_manual_view: expect.any(Number), }) const manualView = intakeRegistry.rumViewEvents[1] expect(manualView.view.name).toBe('manual view') expect(manualView.view.custom_timings).toEqual({ - after_manual_view: jasmine.any(Number), + after_manual_view: expect.any(Number), }) const documentEvent = intakeRegistry.rumResourceEvents.find((event) => event.resource.type === 'document')! @@ -92,15 +92,15 @@ describe('API calls and events around init', () => { window.DD_RUM!.setViewName('after manual view') }, 40) }) - .run(async ({ intakeRegistry }) => { + .run(async ({ intakeRegistry, flushEvents }) => { await flushEvents() const initialView = intakeRegistry.rumViewEvents[0] expect(initialView.view.name).toBe('after manual view') expect(initialView.view.custom_timings).toEqual({ - before_init: jasmine.any(Number), - before_manual_view: jasmine.any(Number), - after_manual_view: jasmine.any(Number), + before_init: expect.any(Number), + before_manual_view: expect.any(Number), + after_manual_view: expect.any(Number), }) const documentEvent = intakeRegistry.rumResourceEvents.find((event) => event.resource.type === 'document')! @@ -140,12 +140,13 @@ describe('API calls and events around init', () => { window.DD_RUM!.addError('after manual view') }, 20) }) - .run(async ({ intakeRegistry }) => { + .run(async ({ intakeRegistry, flushEvents }) => { await flushEvents() const initialView = intakeRegistry.rumViewEvents[0] const nextView = intakeRegistry.rumViewEvents[1] - expect(initialView.context).toEqual(jasmine.objectContaining({ foo: 'bar', bar: 'foo' })) + + expect(initialView.context).toEqual(expect.objectContaining({ foo: 'bar', bar: 'foo' })) expect(nextView.context!.foo).toBeUndefined() expectToHaveActions( @@ -180,13 +181,13 @@ describe('API calls and events around init', () => { window.DD_RUM!.init(configuration) window.DD_RUM!.setViewContext({ foo: 'bar' }) }) - .run(async () => { - const viewContext = await browser.execute(() => window.DD_RUM?.getViewContext()) + .run(async ({ page }) => { + const viewContext = await page.evaluate(() => window.DD_RUM?.getViewContext()) expect(viewContext).toEqual({ foo: 'bar' }) }) }) -describe('beforeSend', () => { +test.describe('beforeSend', () => { createTest('allows to edit events context with feature flag') .withRum({ beforeSend: (event: any) => { @@ -195,13 +196,13 @@ describe('beforeSend', () => { }, }) .withRumSlim() - .run(async ({ intakeRegistry }) => { + .run(async ({ intakeRegistry, flushEvents }) => { await flushEvents() const initialView = intakeRegistry.rumViewEvents[0] - expect(initialView.context).toEqual(jasmine.objectContaining({ foo: 'bar' })) + expect(initialView.context).toEqual(expect.objectContaining({ foo: 'bar' })) const initialDocument = intakeRegistry.rumResourceEvents[0] - expect(initialDocument.context).toEqual(jasmine.objectContaining({ foo: 'bar' })) + expect(initialDocument.context).toEqual(expect.objectContaining({ foo: 'bar' })) }) createTest('allows to replace events context') @@ -217,13 +218,13 @@ describe('beforeSend', () => { window.DD_RUM!.setGlobalContextProperty('foo', 'baz') window.DD_RUM!.setGlobalContextProperty('zig', 'zag') }) - .run(async ({ intakeRegistry }) => { + .run(async ({ intakeRegistry, flushEvents }) => { await flushEvents() const initialView = intakeRegistry.rumViewEvents[0] - expect(initialView.context).toEqual(jasmine.objectContaining({ foo: 'bar' })) + expect(initialView.context).toEqual(expect.objectContaining({ foo: 'bar' })) const initialDocument = intakeRegistry.rumResourceEvents[0] - expect(initialDocument.context).toEqual(jasmine.objectContaining({ foo: 'bar' })) + expect(initialDocument.context).toEqual(expect.objectContaining({ foo: 'bar' })) }) }) @@ -231,14 +232,14 @@ function expectToHaveErrors( events: IntakeRegistry, ...errors: Array<{ message: string; viewId: string; context?: Context }> ) { - expect(events.rumErrorEvents.length).toBe(errors.length) + expect(events.rumErrorEvents).toHaveLength(errors.length) for (let i = 0; i < errors.length; i++) { const registryError = events.rumErrorEvents[i] const expectedError = errors[i] expect(registryError.error.message).toBe(expectedError.message) expect(registryError.view.id).toBe(expectedError.viewId) if (expectedError.context) { - expect(registryError.context).toEqual(jasmine.objectContaining(expectedError.context)) + expect(registryError.context).toEqual(expect.objectContaining(expectedError.context)) } } } @@ -247,7 +248,7 @@ function expectToHaveActions( events: IntakeRegistry, ...actions: Array<{ name: string; viewId: string; viewName?: string; context?: Context }> ) { - expect(events.rumActionEvents.length).toBe(actions.length) + expect(events.rumActionEvents).toHaveLength(actions.length) for (let i = 0; i < actions.length; i++) { const registryAction = events.rumActionEvents[i] const expectedAction = actions[i] @@ -257,7 +258,7 @@ function expectToHaveActions( expect(registryAction.view.name).toBe(expectedAction.viewName) } if (expectedAction.context) { - expect(registryAction.context).toEqual(jasmine.objectContaining(expectedAction.context)) + expect(registryAction.context).toEqual(expect.objectContaining(expectedAction.context)) } } } diff --git a/test/e2e/scenario/rum/resources.scenario.ts b/test/e2e/scenario/rum/resources.scenario.ts index a7081344c5..6a7df00ff8 100644 --- a/test/e2e/scenario/rum/resources.scenario.ts +++ b/test/e2e/scenario/rum/resources.scenario.ts @@ -1,14 +1,14 @@ +import { test, expect } from '@playwright/test' import type { RumResourceEvent } from '@datadog/browser-rum' import type { IntakeRegistry } from '../../lib/framework' -import { flushEvents, bundleSetup, createTest, html } from '../../lib/framework' -import { sendXhr } from '../../lib/helpers/browser' +import { bundleSetup, createTest, html } from '../../lib/framework' const REQUEST_DURATION = 200 -describe('rum resources', () => { +test.describe('rum resources', () => { createTest('track xhr timings') .withRum() - .run(async ({ intakeRegistry }) => { + .run(async ({ intakeRegistry, flushEvents, sendXhr }) => { await sendXhr(`/ok?duration=${REQUEST_DURATION}`) await flushEvents() const resourceEvent = intakeRegistry.rumResourceEvents.find((r) => r.resource.url.includes('/ok'))! @@ -20,7 +20,7 @@ describe('rum resources', () => { createTest('track redirect xhr timings') .withRum() - .run(async ({ intakeRegistry }) => { + .run(async ({ intakeRegistry, flushEvents, sendXhr }) => { await sendXhr(`/redirect?duration=${REQUEST_DURATION}`) await flushEvents() const resourceEvent = intakeRegistry.rumResourceEvents.find((r) => r.resource.url.includes('/redirect'))! @@ -34,7 +34,7 @@ describe('rum resources', () => { createTest("don't track disallowed cross origin xhr timings") .withRum() - .run(async ({ crossOriginUrl, intakeRegistry }) => { + .run(async ({ crossOriginUrl, intakeRegistry, flushEvents, sendXhr }) => { await sendXhr(`${crossOriginUrl}/ok?duration=${REQUEST_DURATION}`) await flushEvents() const resourceEvent = intakeRegistry.rumResourceEvents.find((r) => r.resource.url.includes('/ok'))! @@ -47,7 +47,7 @@ describe('rum resources', () => { createTest('track allowed cross origin xhr timings') .withRum() - .run(async ({ crossOriginUrl, intakeRegistry }) => { + .run(async ({ crossOriginUrl, intakeRegistry, flushEvents, sendXhr }) => { await sendXhr(`${crossOriginUrl}/ok?timing-allow-origin=true&duration=${REQUEST_DURATION}`) await flushEvents() const resourceEvent = intakeRegistry.rumResourceEvents.find((r) => r.resource.url.includes('/ok'))! @@ -60,7 +60,7 @@ describe('rum resources', () => { createTest('retrieve early requests timings') .withRum() .withHead(html` `) - .run(async ({ intakeRegistry }) => { + .run(async ({ intakeRegistry, flushEvents }) => { await flushEvents() const resourceEvent = intakeRegistry.rumResourceEvents.find((event) => event.resource.url.includes('empty.css')) expect(resourceEvent).toBeDefined() @@ -69,7 +69,7 @@ describe('rum resources', () => { createTest('retrieve initial document timings') .withRum() - .run(async ({ baseUrl, intakeRegistry }) => { + .run(async ({ baseUrl, intakeRegistry, flushEvents }) => { await flushEvents() const resourceEvent = intakeRegistry.rumResourceEvents.find((event) => event.resource.type === 'document') expect(resourceEvent).toBeDefined() @@ -77,20 +77,23 @@ describe('rum resources', () => { expectToHaveValidTimings(resourceEvent!) }) - describe('XHR abort support', () => { + test.describe('XHR abort support', () => { createTest('track aborted XHR') .withRum() .withSetup(bundleSetup) - .run(async ({ intakeRegistry }) => { - await browser.executeAsync((done) => { - const xhr = new XMLHttpRequest() - xhr.open('GET', '/ok?duration=1000') - xhr.send() - setTimeout(() => { - xhr.abort() - done(undefined) - }, 100) - }) + .run(async ({ intakeRegistry, flushEvents, page }) => { + await page.evaluate( + () => + new Promise((resolve) => { + const xhr = new XMLHttpRequest() + xhr.open('GET', '/ok?duration=1000') + xhr.send() + setTimeout(() => { + xhr.abort() + resolve(undefined) + }, 100) + }) + ) await flushEvents() @@ -100,14 +103,17 @@ describe('rum resources', () => { createTest('aborting an unsent XHR should be ignored') .withRum() .withSetup(bundleSetup) - .run(async ({ intakeRegistry }) => { - await browser.executeAsync((done) => { - const xhr = new XMLHttpRequest() - xhr.open('GET', '/ok') - xhr.abort() - xhr.send() - xhr.addEventListener('loadend', () => done(undefined)) - }) + .run(async ({ intakeRegistry, flushEvents, page }) => { + await page.evaluate( + () => + new Promise((resolve) => { + const xhr = new XMLHttpRequest() + xhr.open('GET', '/ok') + xhr.abort() + xhr.send() + xhr.addEventListener('loadend', () => resolve(undefined)) + }) + ) await flushEvents() @@ -117,18 +123,21 @@ describe('rum resources', () => { createTest('aborting an XHR when state becomes DONE and before the loadend event should be ignored') .withRum() .withSetup(bundleSetup) - .run(async ({ intakeRegistry }) => { - await browser.executeAsync((done) => { - const xhr = new XMLHttpRequest() - xhr.open('GET', '/ok') - xhr.onreadystatechange = () => { - if (xhr.readyState === XMLHttpRequest.DONE) { - xhr.abort() - done(undefined) - } - } - xhr.send() - }) + .run(async ({ intakeRegistry, flushEvents, page }) => { + await page.evaluate( + () => + new Promise((resolve) => { + const xhr = new XMLHttpRequest() + xhr.open('GET', '/ok') + xhr.onreadystatechange = () => { + if (xhr.readyState === XMLHttpRequest.DONE) { + xhr.abort() + resolve(undefined) + } + } + xhr.send() + }) + ) await flushEvents() @@ -138,18 +147,21 @@ describe('rum resources', () => { createTest('aborting an XHR after the loadend event should be ignored') .withRum() .withSetup(bundleSetup) - .run(async ({ intakeRegistry }) => { - await browser.executeAsync((done) => { - const xhr = new XMLHttpRequest() - xhr.open('GET', '/ok') - xhr.addEventListener('loadend', () => { - setTimeout(() => { - xhr.abort() - done(undefined) + .run(async ({ intakeRegistry, flushEvents, page }) => { + await page.evaluate( + () => + new Promise((resolve) => { + const xhr = new XMLHttpRequest() + xhr.open('GET', '/ok') + xhr.addEventListener('loadend', () => { + setTimeout(() => { + xhr.abort() + resolve(undefined) + }) + }) + xhr.send() }) - }) - xhr.send() - }) + ) await flushEvents() @@ -172,21 +184,24 @@ describe('rum resources', () => { } }) - describe('fetch abort support', () => { + test.describe('fetch abort support', () => { createTest('track aborted fetch') .withRum() .withSetup(bundleSetup) - .run(async ({ intakeRegistry }) => { - await browser.executeAsync((done) => { - const controller = new AbortController() - fetch('/ok?duration=1000', { signal: controller.signal }).catch(() => { - // ignore abortion error - done(undefined) - }) - setTimeout(() => { - controller.abort() - }, 100) - }) + .run(async ({ intakeRegistry, flushEvents, page }) => { + await page.evaluate( + () => + new Promise((resolve) => { + const controller = new AbortController() + fetch('/ok?duration=1000', { signal: controller.signal }).catch(() => { + // ignore abortion error + resolve(undefined) + }) + setTimeout(() => { + controller.abort() + }, 100) + }) + ) await flushEvents() @@ -198,15 +213,18 @@ describe('rum resources', () => { createTest('track redirect fetch timings') .withRum() - .run(async ({ intakeRegistry }) => { - await browser.executeAsync((done) => { - fetch('/redirect?duration=200').then( - () => done(undefined), - () => { - throw Error('Issue with fetch call') - } - ) - }) + .run(async ({ intakeRegistry, flushEvents, page }) => { + await page.evaluate( + () => + new Promise((resolve) => { + fetch('/redirect?duration=200').then( + () => resolve(undefined), + () => { + throw Error('Issue with fetch call') + } + ) + }) + ) await flushEvents() const resourceEvent = intakeRegistry.rumResourceEvents.find((r) => r.resource.url.includes('/redirect'))! expect(resourceEvent).not.toBeUndefined() @@ -220,16 +238,20 @@ describe('rum resources', () => { createTest('track concurrent fetch to same resource') .withRum() .withSetup(bundleSetup) - .run(async ({ intakeRegistry }) => { - await browser.executeAsync((done) => { - Promise.all([fetch('/ok'), fetch('/ok')]) - .then(() => done()) - .catch(() => done()) - }) + .run(async ({ intakeRegistry, flushEvents, page, browserName }) => { + await page.evaluate( + () => + new Promise((resolve) => { + Promise.all([fetch('/ok'), fetch('/ok')]) + .then(() => resolve()) + .catch(() => resolve()) + }) + ) - if (!browser.isChromium) { - pending('Only Chromium based browsers will emit predictable timings events for concurrent fetches') - } + test.skip( + browserName !== 'chromium', + 'Only Chromium based browsers will emit predictable timings events for concurrent fetches' + ) await flushEvents() @@ -242,29 +264,32 @@ describe('rum resources', () => { expect(resourceEvents[1]?.resource.size).toBeDefined() }) - describe('support XHRs with same XMLHttpRequest instance', () => { + test.describe('support XHRs with same XMLHttpRequest instance', () => { createTest('track XHRs when calling requests one after another') .withRum() .withSetup(bundleSetup) - .run(async ({ intakeRegistry }) => { - await browser.executeAsync((done) => { - const xhr = new XMLHttpRequest() - const triggerSecondCall = () => { - xhr.removeEventListener('loadend', triggerSecondCall) - xhr.addEventListener('loadend', () => done(undefined)) - xhr.open('GET', '/ok?duration=100&call=2') - xhr.send() - } - xhr.addEventListener('loadend', triggerSecondCall) - xhr.open('GET', '/ok?duration=100&call=1') - xhr.send() - }) + .run(async ({ intakeRegistry, flushEvents, page }) => { + await page.evaluate( + () => + new Promise((resolve) => { + const xhr = new XMLHttpRequest() + const triggerSecondCall = () => { + xhr.removeEventListener('loadend', triggerSecondCall) + xhr.addEventListener('loadend', () => resolve(undefined)) + xhr.open('GET', '/ok?duration=100&call=2') + xhr.send() + } + xhr.addEventListener('loadend', triggerSecondCall) + xhr.open('GET', '/ok?duration=100&call=1') + xhr.send() + }) + ) await flushEvents() const resourceEvents = intakeRegistry.rumResourceEvents.filter((event) => event.resource.type === 'xhr') - expect(resourceEvents.length).toEqual(2) - expect(intakeRegistry.rumErrorEvents.length).toBe(0) + expect(resourceEvents).toHaveLength(2) + expect(intakeRegistry.rumErrorEvents).toHaveLength(0) expect(resourceEvents[0].resource.url).toContain('/ok?duration=100&call=1') expect(resourceEvents[0].resource.status_code).toEqual(200) expect(resourceEvents[1].resource.url).toContain('/ok?duration=100&call=2') diff --git a/test/e2e/scenario/rum/s8sInject.scenario.ts b/test/e2e/scenario/rum/s8sInject.scenario.ts index b13c1d76fc..54b6f0daca 100644 --- a/test/e2e/scenario/rum/s8sInject.scenario.ts +++ b/test/e2e/scenario/rum/s8sInject.scenario.ts @@ -1,10 +1,12 @@ import * as fs from 'fs' +import puppeteer from 'puppeteer' +import { test, expect } from '@playwright/test' import { RUM_BUNDLE } from '../../lib/framework' import { APPLICATION_ID, CLIENT_TOKEN } from '../../lib/helpers/configuration' -describe('Inject RUM with Puppeteer', () => { +test.describe('Inject RUM with Puppeteer', () => { // S8s tests inject RUM with puppeteer evaluateOnNewDocument - it('should not throw error in chrome', async () => { + test('should not throw error in chrome', async () => { const isInjected = await injectRumWithPuppeteer() expect(isInjected).toBe(true) }) @@ -12,13 +14,12 @@ describe('Inject RUM with Puppeteer', () => { async function injectRumWithPuppeteer() { const ddRUM = fs.readFileSync(RUM_BUNDLE, 'utf8') - const puppeteerBrowser = await browser.getPuppeteer() + const puppeteerBrowser = await puppeteer.launch({ headless: true, devtools: true, args: ['--no-sandbox'] }) let injected = true - await browser.call(async () => { - const page = await puppeteerBrowser.newPage() - await page.evaluateOnNewDocument( - ` + const page = await puppeteerBrowser.newPage() + await page.evaluateOnNewDocument( + ` if (location.href !== 'about:blank') { ${ddRUM} window.DD_RUM._setDebug(true) @@ -29,13 +30,13 @@ async function injectRumWithPuppeteer() { window.DD_RUM.startView() } ` - ) - page.on('console', (msg) => { - if (msg.type() === 'error') { - injected = false - } - }) - await page.goto('https://webdriver.io') + ) + page.on('console', (msg) => { + if (msg.type() === 'error') { + injected = false + } }) + await page.goto('https://example.com') + return injected } diff --git a/test/e2e/scenario/rum/sessions.scenario.ts b/test/e2e/scenario/rum/sessions.scenario.ts index 8efe06d563..e2def67f99 100644 --- a/test/e2e/scenario/rum/sessions.scenario.ts +++ b/test/e2e/scenario/rum/sessions.scenario.ts @@ -1,14 +1,15 @@ import { RecordType } from '@datadog/browser-rum/src/types' +import { test, expect } from '@playwright/test' +import { addTag } from '../../lib/helpers/tags' import { expireSession, findSessionCookie, renewSession } from '../../lib/helpers/session' -import { bundleSetup, createTest, flushEvents, waitForRequests } from '../../lib/framework' -import { deleteAllCookies, sendXhr } from '../../lib/helpers/browser' +import { bundleSetup, createTest, waitForRequests } from '../../lib/framework' -describe('rum sessions', () => { - describe('session renewal', () => { +test.describe('rum sessions', () => { + test.describe('session renewal', () => { createTest('create a new View when the session is renewed') .withRum() - .run(async ({ intakeRegistry }) => { - await renewSession() + .run(async ({ intakeRegistry, flushEvents, browserContext, page }) => { + await renewSession(page, browserContext) await flushEvents() const viewEvents = intakeRegistry.rumViewEvents const firstViewEvent = viewEvents[0] @@ -23,12 +24,12 @@ describe('rum sessions', () => { createTest('a single fullSnapshot is taken when the session is renewed') .withRum() .withSetup(bundleSetup) - .run(async ({ intakeRegistry }) => { - await renewSession() + .run(async ({ intakeRegistry, flushEvents, browserContext, page }) => { + await renewSession(page, browserContext) await flushEvents() - expect(intakeRegistry.replaySegments.length).toBe(2) + expect(intakeRegistry.replaySegments).toHaveLength(2) const segment = intakeRegistry.replaySegments.at(-1)! expect(segment.creation_reason).toBe('init') @@ -39,145 +40,153 @@ describe('rum sessions', () => { }) }) - describe('session expiration', () => { + test.describe('session expiration', () => { createTest("don't send events when session is expired") // prevent recording start to generate late events .withRum({ startSessionReplayRecordingManually: true }) - .run(async ({ intakeRegistry }) => { - await expireSession() + .run(async ({ intakeRegistry, sendXhr, browserContext, page }) => { + await expireSession(page, browserContext) intakeRegistry.empty() await sendXhr('/ok') expect(intakeRegistry.isEmpty).toBe(true) }) }) - describe('anonymous user id', () => { + test.describe('anonymous user id', () => { createTest('persists when session is expired') .withRum() - .run(async () => { - const anonymousId = (await findSessionCookie())?.aid + .run(async ({ flushEvents, browserContext, page }) => { + const anonymousId = (await findSessionCookie(browserContext))?.aid - await expireSession() + await expireSession(page, browserContext) await flushEvents() - expect((await findSessionCookie())?.aid).toEqual(anonymousId) + expect((await findSessionCookie(browserContext))?.aid).toEqual(anonymousId) }) createTest('persists when session renewed') .withRum() - .run(async () => { - const anonymousId = (await findSessionCookie())?.aid + .run(async ({ browserContext, page }) => { + const anonymousId = (await findSessionCookie(browserContext))?.aid expect(anonymousId).not.toBeNull() - await browser.execute(() => { + await page.evaluate(() => { window.DD_RUM!.stopSession() }) - await (await $('html')).click() + await page.locator('html').click() // The session is not created right away, let's wait until we see a cookie - await browser.waitUntil(async () => Boolean(await findSessionCookie())) + await page.waitForTimeout(1000) - expect((await findSessionCookie())?.aid).toEqual(anonymousId) + expect((await findSessionCookie(browserContext))?.aid).toEqual(anonymousId) + + expect(true).toBeTruthy() }) createTest('generated when cookie is cleared') .withRum() - .run(async () => { + .run(async ({ deleteAllCookies, flushEvents, browserContext, page }) => { await deleteAllCookies() - await renewSession() + await renewSession(page, browserContext) await flushEvents() - expect((await findSessionCookie())?.aid).toBeDefined() + expect((await findSessionCookie(browserContext))?.aid).toBeDefined() }) }) - describe('manual session expiration', () => { + test.describe('manual session expiration', () => { createTest('calling stopSession() stops the session') .withRum() - .run(async ({ intakeRegistry }) => { - await browser.executeAsync((done) => { - window.DD_RUM!.stopSession() - setTimeout(() => { - // If called directly after `stopSession`, the action start time may be the same as the - // session end time. In this case, the sopped session is used, and the action is - // collected. - // We might want to improve this by having a strict comparison between the event start - // time and session end time. - window.DD_RUM!.addAction('foo') - done() - }, 5) - }) + .run(async ({ intakeRegistry, flushEvents, browserContext, page }) => { + await page.evaluate( + () => + new Promise((resolve) => { + window.DD_RUM!.stopSession() + setTimeout(() => { + // If called directly after `stopSession`, the action start time may be the same as the + // session end time. In this case, the sopped session is used, and the action is + // collected. + // We might want to improve this by having a strict comparison between the event start + // time and session end time. + window.DD_RUM!.addAction('foo') + resolve() + }, 5) + }) + ) await flushEvents() - expect((await findSessionCookie())?.isExpired).toEqual('1') - expect(intakeRegistry.rumActionEvents.length).toBe(0) + expect((await findSessionCookie(browserContext))?.isExpired).toEqual('1') + expect(intakeRegistry.rumActionEvents).toHaveLength(0) }) - createTest('after calling stopSession(), a user interaction starts a new session') + createTest('after calling stopSession(), a user interaction starts a new session @flaky') .withRum() - .run(async ({ intakeRegistry }) => { - await browser.execute(() => { + .run(async ({ intakeRegistry, flushEvents, browserContext, page }) => { + await page.evaluate(() => { window.DD_RUM!.stopSession() }) - await (await $('html')).click() + + addTag('flaky', 'This test is known to be flacky, especially in FF') + + await page.locator('html').click() // The session is not created right away, let's wait until we see a cookie - await browser.waitUntil(async () => Boolean(await findSessionCookie())) + await page.waitForTimeout(1000) - await browser.execute(() => { + await page.evaluate(() => { window.DD_RUM!.addAction('foo') }) await flushEvents() - expect((await findSessionCookie())?.isExpired).not.toEqual('1') - expect((await findSessionCookie())?.id).toBeDefined() - expect(intakeRegistry.rumActionEvents.length).toBe(1) + expect((await findSessionCookie(browserContext))?.isExpired).not.toEqual('1') + expect((await findSessionCookie(browserContext))?.id).toBeDefined() + expect(intakeRegistry.rumActionEvents).toHaveLength(1) }) createTest('flush events when the session expires') .withRum() .withLogs() - .run(async ({ intakeRegistry }) => { - expect(intakeRegistry.rumViewEvents.length).toBe(0) - expect(intakeRegistry.logsEvents.length).toBe(0) - expect(intakeRegistry.replaySegments.length).toBe(0) + .run(async ({ intakeRegistry, page }) => { + expect(intakeRegistry.rumViewEvents).toHaveLength(0) + expect(intakeRegistry.logsEvents).toHaveLength(0) + expect(intakeRegistry.replaySegments).toHaveLength(0) - await browser.execute(() => { + await page.evaluate(() => { window.DD_LOGS!.logger.log('foo') window.DD_RUM!.stopSession() }) - await waitForRequests() + await waitForRequests(page) - expect(intakeRegistry.rumViewEvents.length).toBe(1) + expect(intakeRegistry.rumViewEvents).toHaveLength(1) expect(intakeRegistry.rumViewEvents[0].session.is_active).toBe(false) - expect(intakeRegistry.logsEvents.length).toBe(1) - expect(intakeRegistry.replaySegments.length).toBe(1) + expect(intakeRegistry.logsEvents).toHaveLength(1) + expect(intakeRegistry.replaySegments).toHaveLength(1) }) }) - describe('third party cookie clearing', () => { + test.describe('third party cookie clearing', () => { createTest('after a 3rd party clears the cookies, do not restart a session on user interaction') .withRum() - .run(async ({ intakeRegistry }) => { + .run(async ({ intakeRegistry, deleteAllCookies, flushEvents, browserContext, page }) => { await deleteAllCookies() // Cookies are cached for 1s, wait until the cache expires - await browser.pause(1100) + await page.waitForTimeout(1100) - await (await $('html')).click() + await page.locator('html').click() - await browser.pause(1100) + await page.waitForTimeout(1100) - await browser.execute(() => { + await page.evaluate(() => { window.DD_RUM!.addAction('foo') }) await flushEvents() - expect(await findSessionCookie()).toBeUndefined() - expect(intakeRegistry.rumActionEvents.length).toBe(0) - expect(intakeRegistry.rumViewEvents.length).toBe(1) + expect(await findSessionCookie(browserContext)).toBeUndefined() + expect(intakeRegistry.rumActionEvents).toHaveLength(0) + expect(intakeRegistry.rumViewEvents).toHaveLength(1) expect(intakeRegistry.rumViewEvents[0].session.is_active).toBe(false) }) }) diff --git a/test/e2e/scenario/rum/tracing.scenario.ts b/test/e2e/scenario/rum/tracing.scenario.ts index f980b63e92..ee88ef9f05 100644 --- a/test/e2e/scenario/rum/tracing.scenario.ts +++ b/test/e2e/scenario/rum/tracing.scenario.ts @@ -1,11 +1,11 @@ +import { test, expect } from '@playwright/test' import type { IntakeRegistry } from '../../lib/framework' -import { flushEvents, createTest } from '../../lib/framework' -import { sendXhr } from '../../lib/helpers/browser' +import { createTest } from '../../lib/framework' -describe('tracing', () => { +test.describe('tracing', () => { createTest('trace xhr') .withRum({ service: 'service', allowedTracingUrls: ['LOCATION_ORIGIN'] }) - .run(async ({ intakeRegistry }) => { + .run(async ({ intakeRegistry, sendXhr, flushEvents }) => { const rawHeaders = await sendXhr('/headers', [ ['x-foo', 'bar'], ['x-foo', 'baz'], @@ -19,19 +19,22 @@ describe('tracing', () => { createTest('trace fetch') .withRum({ service: 'service', allowedTracingUrls: ['LOCATION_ORIGIN'] }) - .run(async ({ intakeRegistry }) => { - const rawHeaders = await browser.executeAsync((done) => { - window - .fetch('/headers', { - headers: [ - ['x-foo', 'bar'], - ['x-foo', 'baz'], - ], + .run(async ({ intakeRegistry, flushEvents, page }) => { + const rawHeaders = await page.evaluate( + () => + new Promise((resolve) => { + window + .fetch('/headers', { + headers: [ + ['x-foo', 'bar'], + ['x-foo', 'baz'], + ], + }) + .then((response) => response.text()) + .then(resolve) + .catch(() => resolve(new Error('Fetch request failed!'))) }) - .then((response) => response.text()) - .then(done) - .catch(() => done(new Error('Fetch request failed!'))) - }) + ) const headers = parseHeaders(rawHeaders) checkRequestHeaders(headers) expect(headers['x-foo']).toBe('bar, baz') @@ -41,14 +44,17 @@ describe('tracing', () => { createTest('trace fetch with Request argument') .withRum({ service: 'service', allowedTracingUrls: ['LOCATION_ORIGIN'] }) - .run(async ({ intakeRegistry }) => { - const rawHeaders = await browser.executeAsync((done) => { - window - .fetch(new Request('/headers', { headers: { 'x-foo': 'bar, baz' } })) - .then((response) => response.text()) - .then(done) - .catch(() => done(new Error('Fetch request failed!'))) - }) + .run(async ({ intakeRegistry, flushEvents, page }) => { + const rawHeaders = await page.evaluate( + () => + new Promise((resolve) => { + window + .fetch(new Request('/headers', { headers: { 'x-foo': 'bar, baz' } })) + .then((response) => response.text()) + .then(resolve) + .catch(() => resolve(new Error('Fetch request failed!'))) + }) + ) const headers = parseHeaders(rawHeaders) checkRequestHeaders(headers) expect(headers['x-foo']).toBe('bar, baz') @@ -58,14 +64,17 @@ describe('tracing', () => { createTest('trace single argument fetch') .withRum({ service: 'service', allowedTracingUrls: ['LOCATION_ORIGIN'] }) - .run(async ({ intakeRegistry }) => { - const rawHeaders = await browser.executeAsync((done) => { - window - .fetch('/headers') - .then((response) => response.text()) - .then(done) - .catch(() => done(new Error('Fetch request failed!'))) - }) + .run(async ({ intakeRegistry, flushEvents, page }) => { + const rawHeaders = await page.evaluate( + () => + new Promise((resolve) => { + window + .fetch('/headers') + .then((response) => response.text()) + .then(resolve) + .catch(() => resolve(new Error('Fetch request failed!'))) + }) + ) const headers = parseHeaders(rawHeaders) checkRequestHeaders(headers) await flushEvents() @@ -96,7 +105,7 @@ describe('tracing', () => { const requests = intakeRegistry.rumResourceEvents.filter( (event) => event.resource.type === 'xhr' || event.resource.type === 'fetch' ) - expect(requests.length).toBe(1) + expect(requests).toHaveLength(1) expect(requests[0]._dd.trace_id).toMatch(/\d+/) expect(requests[0]._dd.span_id).toMatch(/\d+/) expect(requests[0].resource.id).toBeDefined() diff --git a/test/e2e/scenario/rum/views.scenario.ts b/test/e2e/scenario/rum/views.scenario.ts index be757269b5..bd00503772 100644 --- a/test/e2e/scenario/rum/views.scenario.ts +++ b/test/e2e/scenario/rum/views.scenario.ts @@ -1,10 +1,10 @@ -import { createTest, flushEvents, html } from '../../lib/framework' -import { getBrowserName } from '../../lib/helpers/browser' +import { test, expect } from '@playwright/test' +import { createTest, html } from '../../lib/framework' -describe('rum views', () => { +test.describe('rum views', () => { createTest('send performance timings along the view events') .withRum() - .run(async ({ intakeRegistry }) => { + .run(async ({ flushEvents, intakeRegistry }) => { await flushEvents() const viewEvent = intakeRegistry.rumViewEvents[0] expect(viewEvent).toBeDefined() @@ -15,50 +15,55 @@ describe('rum views', () => { expect(viewEvent.view.load_event).toBeGreaterThan(0) }) - // When run via WebDriver, Safari <= 14 (at least) have an issue with `event.timeStamp`, - // so the 'first-input' polyfill is ignoring it and doesn't send a performance entry. - // See https://bugs.webkit.org/show_bug.cgi?id=211101 - if (getBrowserName() !== 'safari') { - createTest('send performance first input delay') - .withRum() - .withBody(html` `) - .run(async ({ intakeRegistry }) => { - await (await $('button')).click() - await flushEvents() - const viewEvent = intakeRegistry.rumViewEvents[0] - expect(viewEvent).toBeDefined() - expect(viewEvent.view.first_input_delay).toBeGreaterThanOrEqual(0) - }) - } + createTest('send performance first input delay') + .withRum() + .withBody(html` `) + .run(async ({ browserName, flushEvents, intakeRegistry, page }) => { + test.skip( + browserName.includes('webkit'), + ` + // When run via WebDriver, Safari <= 14 (at least) have an issue with 'event.timeStamp', + // so the 'first-input' polyfill is ignoring it and doesn't send a performance entry. + // See https://bugs.webkit.org/show_bug.cgi?id=211101 + ` + ) + const button = page.locator('button') + await button.click() + await flushEvents() + const viewEvent = intakeRegistry.rumViewEvents[0] + expect(viewEvent).toBeDefined() + expect(viewEvent.view.first_input_delay).toBeGreaterThanOrEqual(0) + }) - describe('anchor navigation', () => { + test.describe('anchor navigation', () => { createTest("don't create a new view when it is an Anchor navigation") .withRum() .withBody(html` anchor link
    `) - .run(async ({ intakeRegistry }) => { - await (await $('a')).click() + .run(async ({ flushEvents, intakeRegistry, page }) => { + const anchor = page.locator('a') + await anchor.click() await flushEvents() const viewEvents = intakeRegistry.rumViewEvents - expect(viewEvents.length).toBe(1) + expect(viewEvents).toHaveLength(1) expect(viewEvents[0].view.loading_type).toBe('initial_load') }) createTest('create a new view on hash change') .withRum() - .run(async ({ intakeRegistry }) => { - await browser.execute(() => { + .run(async ({ flushEvents, intakeRegistry, page }) => { + await page.evaluate(() => { window.location.hash = '#bar' }) await flushEvents() const viewEvents = intakeRegistry.rumViewEvents - expect(viewEvents.length).toBe(2) + expect(viewEvents).toHaveLength(2) expect(viewEvents[0].view.loading_type).toBe('initial_load') expect(viewEvents[1].view.loading_type).toBe('route_change') }) diff --git a/test/e2e/scenario/rum/vitals.scenario.ts b/test/e2e/scenario/rum/vitals.scenario.ts index 93d5a65d2e..86417157d3 100644 --- a/test/e2e/scenario/rum/vitals.scenario.ts +++ b/test/e2e/scenario/rum/vitals.scenario.ts @@ -1,20 +1,23 @@ -import { createTest, flushEvents } from '../../lib/framework' +import { test, expect } from '@playwright/test' +import { createTest } from '../../lib/framework' -describe('vital collection', () => { +test.describe('vital collection', () => { createTest('send custom duration vital') .withRum() - .run(async ({ intakeRegistry }) => { - await browser.executeAsync((done) => { + .run(async ({ flushEvents, intakeRegistry, page }) => { + await page.evaluate(() => { const vital = window.DD_RUM!.startDurationVital('foo') - setTimeout(() => { - window.DD_RUM!.stopDurationVital(vital) - done() - }, 5) + return new Promise((resolve) => { + setTimeout(() => { + window.DD_RUM!.stopDurationVital(vital) + resolve() + }, 5) + }) }) await flushEvents() - expect(intakeRegistry.rumVitalEvents.length).toBe(1) + expect(intakeRegistry.rumVitalEvents).toHaveLength(1) expect(intakeRegistry.rumVitalEvents[0].vital.name).toEqual('foo') - expect(intakeRegistry.rumVitalEvents[0].vital.duration).toEqual(jasmine.any(Number)) + expect(intakeRegistry.rumVitalEvents[0].vital.duration).toEqual(expect.any(Number)) }) }) diff --git a/test/e2e/scenario/sessionStore.scenario.ts b/test/e2e/scenario/sessionStore.scenario.ts index 955ee70042..d7ae87bf27 100644 --- a/test/e2e/scenario/sessionStore.scenario.ts +++ b/test/e2e/scenario/sessionStore.scenario.ts @@ -1,20 +1,21 @@ import { SESSION_STORE_KEY } from '@datadog/browser-core' +import type { BrowserContext, Page } from '@playwright/test' +import { test, expect } from '@playwright/test' import { createTest } from '../lib/framework' const DISABLE_LOCAL_STORAGE = '' const DISABLE_COOKIES = '' const SESSION_ID_REGEX = /(? { - describe('Cookies', () => { +test.describe('Session Stores', () => { + test.describe('Cookies', () => { createTest('uses cookies to store the session') .withLogs() .withRum() - .run(async () => { - const cookieSessionId = await getSessionIdFromCookie() - - const logsContext = await browser.execute(() => window.DD_LOGS?.getInternalContext()) - const rumContext = await browser.execute(() => window.DD_RUM?.getInternalContext()) + .run(async ({ browserContext, page }) => { + const cookieSessionId = await getSessionIdFromCookie(browserContext) + const logsContext = await page.evaluate(() => window.DD_LOGS?.getInternalContext()) + const rumContext = await page.evaluate(() => window.DD_RUM?.getInternalContext()) expect(logsContext?.session_id).toBe(cookieSessionId) expect(rumContext?.session_id).toBe(cookieSessionId) @@ -24,24 +25,24 @@ describe('Session Stores', () => { .withLogs() .withRum() .withHead(DISABLE_COOKIES) - .run(async () => { - const logsContext = await browser.execute(() => window.DD_LOGS?.getInternalContext()) - const rumContext = await browser.execute(() => window.DD_RUM?.getInternalContext()) + .run(async ({ page }) => { + const logsContext = await page.evaluate(() => window.DD_LOGS?.getInternalContext()) + const rumContext = await page.evaluate(() => window.DD_RUM?.getInternalContext()) - expect(logsContext).not.toBeNull() - expect(rumContext).toBeNull() + expect(logsContext).not.toBeUndefined() + expect(rumContext).toBeUndefined() }) }) - describe('Local Storage', () => { + test.describe('Local Storage', () => { createTest('uses localStorage to store the session') .withLogs({ sessionPersistence: 'local-storage' }) .withRum({ sessionPersistence: 'local-storage' }) - .run(async () => { - const sessionId = await getSessionIdFromLocalStorage() + .run(async ({ page }) => { + const sessionId = await getSessionIdFromLocalStorage(page) - const logsContext = await browser.execute(() => window.DD_LOGS?.getInternalContext()) - const rumContext = await browser.execute(() => window.DD_RUM?.getInternalContext()) + const logsContext = await page.evaluate(() => window.DD_LOGS?.getInternalContext()) + const rumContext = await page.evaluate(() => window.DD_RUM?.getInternalContext()) expect(logsContext?.session_id).toBe(sessionId) expect(rumContext?.session_id).toBe(sessionId) @@ -51,12 +52,12 @@ describe('Session Stores', () => { .withLogs({ sessionPersistence: 'local-storage' }) .withRum({ sessionPersistence: 'local-storage' }) .withHead(DISABLE_LOCAL_STORAGE) - .run(async () => { - const logsContext = await browser.execute(() => window.DD_LOGS?.getInternalContext()) - const rumContext = await browser.execute(() => window.DD_RUM?.getInternalContext()) + .run(async ({ page }) => { + const logsContext = await page.evaluate(() => window.DD_LOGS?.getInternalContext()) + const rumContext = await page.evaluate(() => window.DD_RUM?.getInternalContext()) - expect(logsContext).not.toBeNull() - expect(rumContext).toBeNull() + expect(logsContext).not.toBeUndefined() + expect(rumContext).toBeUndefined() }) }) @@ -64,23 +65,23 @@ describe('Session Stores', () => { .withLogs({ allowFallbackToLocalStorage: true }) .withRum({ allowFallbackToLocalStorage: true }) .withHead(DISABLE_COOKIES) - .run(async () => { - const sessionId = await getSessionIdFromLocalStorage() + .run(async ({ page }) => { + const sessionId = await getSessionIdFromLocalStorage(page) - const logsContext = await browser.execute(() => window.DD_LOGS?.getInternalContext()) - const rumContext = await browser.execute(() => window.DD_RUM?.getInternalContext()) + const logsContext = await page.evaluate(() => window.DD_LOGS?.getInternalContext()) + const rumContext = await page.evaluate(() => window.DD_RUM?.getInternalContext()) expect(logsContext?.session_id).toBe(sessionId) expect(rumContext?.session_id).toBe(sessionId) }) }) -async function getSessionIdFromLocalStorage(): Promise { - const sessionStateString = await browser.execute((key) => window.localStorage.getItem(key), SESSION_STORE_KEY) +async function getSessionIdFromLocalStorage(page: Page): Promise { + const sessionStateString = await page.evaluate((key) => window.localStorage.getItem(key), SESSION_STORE_KEY) return sessionStateString?.match(SESSION_ID_REGEX)?.[1] } -async function getSessionIdFromCookie(): Promise { - const [cookie] = await browser.getCookies([SESSION_STORE_KEY]) +async function getSessionIdFromCookie(browserContext: BrowserContext): Promise { + const [cookie] = await browserContext.cookies() return cookie.value.match(SESSION_ID_REGEX)?.[1] } diff --git a/test/e2e/scenario/telemetry.scenario.ts b/test/e2e/scenario/telemetry.scenario.ts index 3246b37e53..6a13a616a7 100644 --- a/test/e2e/scenario/telemetry.scenario.ts +++ b/test/e2e/scenario/telemetry.scenario.ts @@ -1,11 +1,12 @@ -import { bundleSetup, createTest, flushEvents } from '../lib/framework' +import { test, expect } from '@playwright/test' +import { bundleSetup, createTest } from '../lib/framework' -describe('telemetry', () => { +test.describe('telemetry', () => { createTest('send errors for logs') .withSetup(bundleSetup) .withLogs() - .run(async ({ intakeRegistry }) => { - await browser.execute(() => { + .run(async ({ intakeRegistry, page, flushEvents }) => { + await page.evaluate(() => { const context = { get foo() { throw new window.Error('expected error') @@ -14,7 +15,7 @@ describe('telemetry', () => { window.DD_LOGS!.logger.log('hop', context as any) }) await flushEvents() - expect(intakeRegistry.telemetryErrorEvents.length).toBe(1) + expect(intakeRegistry.telemetryErrorEvents).toHaveLength(1) const event = intakeRegistry.telemetryErrorEvents[0] expect(event.service).toEqual('browser-logs-sdk') expect(event.telemetry.message).toBe('expected error') @@ -26,8 +27,8 @@ describe('telemetry', () => { createTest('send errors for RUM') .withSetup(bundleSetup) .withRum() - .run(async ({ intakeRegistry }) => { - await browser.execute(() => { + .run(async ({ intakeRegistry, flushEvents, page }) => { + await page.evaluate(() => { const context = { get foo() { throw new window.Error('expected error') @@ -36,7 +37,7 @@ describe('telemetry', () => { window.DD_RUM!.addAction('hop', context as any) }) await flushEvents() - expect(intakeRegistry.telemetryErrorEvents.length).toBe(1) + expect(intakeRegistry.telemetryErrorEvents).toHaveLength(1) const event = intakeRegistry.telemetryErrorEvents[0] expect(event.service).toEqual('browser-rum-sdk') expect(event.telemetry.message).toBe('expected error') @@ -50,9 +51,9 @@ describe('telemetry', () => { .withLogs({ forwardErrorsToLogs: true, }) - .run(async ({ intakeRegistry }) => { + .run(async ({ intakeRegistry, flushEvents }) => { await flushEvents() - expect(intakeRegistry.telemetryConfigurationEvents.length).toBe(1) + expect(intakeRegistry.telemetryConfigurationEvents).toHaveLength(1) const event = intakeRegistry.telemetryConfigurationEvents[0] expect(event.service).toEqual('browser-logs-sdk') expect(event.telemetry.configuration.forward_errors_to_logs).toEqual(true) @@ -63,9 +64,9 @@ describe('telemetry', () => { .withRum({ trackUserInteractions: true, }) - .run(async ({ intakeRegistry }) => { + .run(async ({ intakeRegistry, flushEvents }) => { await flushEvents() - expect(intakeRegistry.telemetryConfigurationEvents.length).toBe(1) + expect(intakeRegistry.telemetryConfigurationEvents).toHaveLength(1) const event = intakeRegistry.telemetryConfigurationEvents[0] expect(event.service).toEqual('browser-rum-sdk') expect(event.telemetry.configuration.track_user_interactions).toEqual(true) @@ -74,13 +75,13 @@ describe('telemetry', () => { createTest('send usage telemetry for RUM') .withSetup(bundleSetup) .withRum() - .run(async ({ intakeRegistry }) => { - await browser.execute(() => { + .run(async ({ intakeRegistry, flushEvents, page }) => { + await page.evaluate(() => { window.DD_RUM!.addAction('foo') }) await flushEvents() - expect(intakeRegistry.telemetryUsageEvents.length).toBe(2) + expect(intakeRegistry.telemetryUsageEvents).toHaveLength(2) const event = intakeRegistry.telemetryUsageEvents[1] // first event is 'set-global-context' done in pageSetup.ts expect(event.service).toEqual('browser-rum-sdk') expect(event.telemetry.usage.feature).toEqual('add-action') @@ -89,13 +90,13 @@ describe('telemetry', () => { createTest('send usage telemetry for logs') .withSetup(bundleSetup) .withLogs() - .run(async ({ intakeRegistry }) => { - await browser.execute(() => { + .run(async ({ intakeRegistry, flushEvents, page }) => { + await page.evaluate(() => { window.DD_LOGS!.setTrackingConsent('granted') }) await flushEvents() - expect(intakeRegistry.telemetryUsageEvents.length).toBe(1) + expect(intakeRegistry.telemetryUsageEvents).toHaveLength(1) const event = intakeRegistry.telemetryUsageEvents[0] expect(event.service).toEqual('browser-logs-sdk') expect(event.telemetry.usage.feature).toEqual('set-tracking-consent') diff --git a/test/e2e/scenario/trackingConsent.scenario.ts b/test/e2e/scenario/trackingConsent.scenario.ts index deadf16d6f..8254b52f1c 100644 --- a/test/e2e/scenario/trackingConsent.scenario.ts +++ b/test/e2e/scenario/trackingConsent.scenario.ts @@ -1,52 +1,53 @@ -import { createTest, flushEvents } from '../lib/framework' +import { test, expect } from '@playwright/test' import { findSessionCookie } from '../lib/helpers/session' +import { createTest } from '../lib/framework' -describe('tracking consent', () => { - describe('RUM', () => { +test.describe('tracking consent', () => { + test.describe('RUM', () => { createTest('does not start the SDK if tracking consent is not given at init') .withRum({ trackingConsent: 'not-granted' }) - .run(async ({ intakeRegistry }) => { + .run(async ({ intakeRegistry, flushEvents, browserContext }) => { await flushEvents() expect(intakeRegistry.isEmpty).toBe(true) - expect(await findSessionCookie()).toBeUndefined() + expect(await findSessionCookie(browserContext)).toBeUndefined() }) createTest('starts the SDK once tracking consent is granted') .withRum({ trackingConsent: 'not-granted' }) - .run(async ({ intakeRegistry }) => { - await browser.execute(() => { + .run(async ({ intakeRegistry, flushEvents, browserContext, page }) => { + await page.evaluate(() => { window.DD_RUM!.setTrackingConsent('granted') }) await flushEvents() expect(intakeRegistry.isEmpty).toBe(false) - expect(await findSessionCookie()).toBeDefined() + expect(await findSessionCookie(browserContext)).toBeDefined() }) createTest('stops sending events if tracking consent is revoked') .withRum({ trackUserInteractions: true }) - .run(async ({ intakeRegistry }) => { - await browser.execute(() => { + .run(async ({ intakeRegistry, flushEvents, browserContext, page }) => { + await page.evaluate(() => { window.DD_RUM!.setTrackingConsent('not-granted') }) - const htmlElement = await $('html') + const htmlElement = page.locator('html') await htmlElement.click() await flushEvents() - expect(intakeRegistry.rumActionEvents).toEqual([]) - expect((await findSessionCookie())?.isExpired).toEqual('1') + expect(intakeRegistry.rumActionEvents).toHaveLength(0) + expect((await findSessionCookie(browserContext))?.isExpired).toEqual('1') }) createTest('starts a new session when tracking consent is granted again') .withRum() - .run(async ({ intakeRegistry }) => { - const initialSessionId = await findSessionCookie() + .run(async ({ intakeRegistry, flushEvents, browserContext, page }) => { + const initialSessionId = await findSessionCookie(browserContext) - await browser.execute(() => { + await page.evaluate(() => { window.DD_RUM!.setTrackingConsent('not-granted') window.DD_RUM!.setTrackingConsent('granted') }) @@ -57,7 +58,7 @@ describe('tracking consent', () => { const lastView = intakeRegistry.rumViewEvents.at(-1)! expect(firstView.session.id).not.toEqual(lastView.session.id) expect(firstView.view.id).not.toEqual(lastView.view.id) - expect(await findSessionCookie()).not.toEqual(initialSessionId) + expect(await findSessionCookie(browserContext)).not.toEqual(initialSessionId) }) createTest('using setTrackingConsent before init overrides the init parameter') @@ -66,35 +67,35 @@ describe('tracking consent', () => { window.DD_RUM!.setTrackingConsent('granted') window.DD_RUM!.init(configuration) }) - .run(async ({ intakeRegistry }) => { + .run(async ({ intakeRegistry, flushEvents, browserContext }) => { await flushEvents() expect(intakeRegistry.isEmpty).toBe(false) - expect(await findSessionCookie()).toBeDefined() + expect(await findSessionCookie(browserContext)).toBeDefined() }) }) - describe('Logs', () => { + test.describe('Logs', () => { createTest('does not start the SDK if tracking consent is not given at init') .withLogs({ trackingConsent: 'not-granted' }) - .run(async ({ intakeRegistry }) => { + .run(async ({ intakeRegistry, flushEvents, browserContext }) => { await flushEvents() expect(intakeRegistry.isEmpty).toBe(true) - expect(await findSessionCookie()).toBeUndefined() + expect(await findSessionCookie(browserContext)).toBeUndefined() }) createTest('starts the SDK once tracking consent is granted') .withLogs({ trackingConsent: 'not-granted' }) - .run(async ({ intakeRegistry }) => { - await browser.execute(() => { + .run(async ({ intakeRegistry, flushEvents, browserContext, page }) => { + await page.evaluate(() => { window.DD_LOGS!.setTrackingConsent('granted') }) await flushEvents() expect(intakeRegistry.isEmpty).toBe(false) - expect(await findSessionCookie()).toBeDefined() + expect(await findSessionCookie(browserContext)).toBeDefined() }) }) }) diff --git a/test/e2e/scenario/transport.scenario.ts b/test/e2e/scenario/transport.scenario.ts index 42ba400dfb..99641de18e 100644 --- a/test/e2e/scenario/transport.scenario.ts +++ b/test/e2e/scenario/transport.scenario.ts @@ -1,54 +1,60 @@ -import { createTest, flushEvents } from '../lib/framework' -import { getBrowserName, getPlatformName, withBrowserLogs } from '../lib/helpers/browser' +import { test, expect } from '@playwright/test' +import { createTest } from '../lib/framework' -describe('transport', () => { - describe('data compression', () => { +test.describe('transport', () => { + test.describe('data compression', () => { createTest('send RUM data compressed') .withRum({ compressIntakeRequests: true, }) - .run(async ({ intakeRegistry }) => { + .run(async ({ flushEvents, intakeRegistry }) => { await flushEvents() - expect(intakeRegistry.rumRequests.length).toBe(2) + expect(intakeRegistry.rumRequests).toHaveLength(2) const plainRequest = intakeRegistry.rumRequests.find((request) => request.encoding === null) const deflateRequest = intakeRegistry.rumRequests.find((request) => request.encoding === 'deflate') // The last view update should be sent without compression - expect(plainRequest!.events).toEqual([ - jasmine.objectContaining({ - type: 'view', - }), - ]) + expect(plainRequest?.events).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + type: 'view', + }), + ]) + ) // Other data should be sent encoded expect(deflateRequest!.events.length).toBeGreaterThan(0) }) - // Ignore this test on Safari desktop and Firefox because the Worker actually works even with - // CSP restriction. - // TODO: Remove this condition when upgrading to Safari 15 and Firefox 99 - if (!((getBrowserName() === 'safari' && getPlatformName() === 'macos') || getBrowserName() === 'firefox')) { - createTest("displays a message if the worker can't be started") - .withRum({ - compressIntakeRequests: true, - }) - .withBasePath('/no-blob-worker-csp') - .run(async ({ intakeRegistry }) => { - await flushEvents() - - // Some non-deflate request can still be sent because on some browsers the Worker fails - // asynchronously - expect(intakeRegistry.rumRequests.filter((request) => request.encoding === 'deflate').length).toBe(0) - - await withBrowserLogs((logs) => { - const failedToStartLog = logs.find((log) => log.message.includes('Datadog RUM failed to start')) - const cspDocLog = logs.find((log) => log.message.includes('Please make sure CSP')) - expect(failedToStartLog).withContext("'Failed to start' log").toBeTruthy() - expect(cspDocLog).withContext("'CSP doc' log").toBeTruthy() - }) + createTest("displays a message if the worker can't be started") + .withRum({ + compressIntakeRequests: true, + }) + .withBasePath('/no-blob-worker-csp') + .run(async ({ browserName, flushEvents, intakeRegistry, page, withBrowserLogs }) => { + const userAgent = await page.evaluate(() => navigator.userAgent) + test.skip( + browserName.includes('firefox') || (browserName.includes('webkit') && userAgent.includes('Mac OS X')), + ` + // Ignore this test on Safari desktop and Firefox because the Worker actually works even with + // CSP restriction. + // TODO: Remove this condition when upgrading to Safari 15 and Firefox 99 + ` + ) + await flushEvents() + + // Some non-deflate request can still be sent because on some browsers the Worker fails + // asynchronously + expect(intakeRegistry.rumRequests.filter((request) => request.encoding === 'deflate')).toHaveLength(0) + + withBrowserLogs((logs) => { + const failedToStartLog = logs.find((log) => log.message.includes('Datadog RUM failed to start')) + const cspDocLog = logs.find((log) => log.message.includes('Please make sure CSP')) + expect(failedToStartLog, "'Failed to start' log").toBeTruthy() + expect(cspDocLog, "'CSP doc' log").toBeTruthy() }) - } + }) }) }) diff --git a/test/e2e/tsconfig.json b/test/e2e/tsconfig.json index e25079306a..9ba040652f 100644 --- a/test/e2e/tsconfig.json +++ b/test/e2e/tsconfig.json @@ -8,7 +8,7 @@ "moduleResolution": "node", "resolveJsonModule": true, "target": "ES2015", - "types": ["node", "jasmine", "@wdio/globals/types", "ajv"], + "types": ["node", "ajv"], "allowJs": true }, "ts-node": { diff --git a/test/e2e/wdio.base.conf.ts b/test/e2e/wdio.base.conf.ts deleted file mode 100644 index 2c387bc140..0000000000 --- a/test/e2e/wdio.base.conf.ts +++ /dev/null @@ -1,89 +0,0 @@ -import path from 'path' -import { unlinkSync, mkdirSync } from 'fs' -import type { Options, Reporters } from '@wdio/types' -import { browser, $, $$ } from '@wdio/globals' -import { getRunId, getTestReportDirectory } from '../envUtils' -import { APPLICATION_ID } from './lib/helpers/configuration' - -const reporters: Reporters.ReporterEntry[] = [['spec', { onlyFailures: true }]] -let logsPath: string | undefined - -const testReportDirectory = getTestReportDirectory() -if (testReportDirectory) { - reporters.push([ - 'junit', - { - outputDir: testReportDirectory, - outputFileFormat(options) { - const browserName = - 'browserName' in options.capabilities && typeof options.capabilities.browserName === 'string' - ? options.capabilities.browserName - : 'unknown' - return `results-${options.cid}.${browserName}.xml` - }, - }, - ]) - logsPath = path.join(testReportDirectory, 'specs.log') -} else if (!process.env.LOGS_STDOUT) { - logsPath = 'specs.log' -} - -type OptionsWithLogsPath = Options.Testrunner & { logsPath?: string } -export const config: OptionsWithLogsPath = { - runner: 'local', - autoCompileOpts: { - autoCompile: true, - tsNodeOpts: { - project: './tsconfig.json', - }, - }, - // We do not inject @wdio globals to keep Jasmine's expect - injectGlobals: false, - specs: ['./scenario/**/*.scenario.ts'], - exclude: ['./scenario/developer-extension/*.scenario.ts'], - capabilities: [], - maxInstances: 5, - logLevel: 'warn', - bail: 0, - waitforTimeout: 10000, - connectionRetryTimeout: 90000, - connectionRetryCount: 0, - framework: 'jasmine', - reporters, - jasmineOpts: { - defaultTimeoutInterval: 60000, - }, - onPrepare: (_config, _capabilities) => { - console.log( - `[RUM events] https://app.datadoghq.com/rum/explorer?query=${encodeURIComponent( - `@application.id:${APPLICATION_ID} @context.run_id:"${getRunId()}"` - )}` - ) - console.log(`[Log events] https://app.datadoghq.com/logs?query=${encodeURIComponent(`@run_id:"${getRunId()}"`)}\n`) - - if (testReportDirectory) { - try { - mkdirSync(testReportDirectory, { recursive: true }) - } catch (e) { - console.log(`Failed to create the test report directory: ${(e as Error).message}`) - } - } - - if (logsPath) { - try { - unlinkSync(logsPath) - } catch (e) { - if ((e as NodeJS.ErrnoException).code !== 'ENOENT') { - console.log(`Failed to remove previous logs: ${(e as Error).message}`) - } - } - } - }, - beforeSession: (_config, _capabilities, _specs, _cid) => { - // Expose everything besides expect, as we want to keep the one from Jasmine - global.browser = browser - global.$ = $ - global.$$ = $$ - }, - logsPath, -} diff --git a/test/e2e/wdio.bs.conf.ts b/test/e2e/wdio.bs.conf.ts deleted file mode 100644 index 3e369cd75f..0000000000 --- a/test/e2e/wdio.bs.conf.ts +++ /dev/null @@ -1,44 +0,0 @@ -import type { Options } from '@wdio/types' -import { getBuildInfos } from '../envUtils' -import { browserConfigurations } from './browsers.conf' -import { config as baseConfig } from './wdio.base.conf' - -export const config: Options.Testrunner = { - ...baseConfig, - - specFileRetries: 1, - exclude: [...baseConfig.exclude!, './scenario/rum/s8sInject.scenario.ts'], - capabilities: browserConfigurations.map((configuration) => - // See https://www.browserstack.com/automate/capabilities?tag=selenium-4 - // Make sure to look at the "W3C Protocol" tab - ({ - browserName: configuration.name, - browserVersion: configuration.version, - 'bstack:options': { - os: configuration.os, - osVersion: configuration.osVersion, - deviceName: configuration.device, - - appiumVersion: '1.22.0', - seleniumVersion: '4.1.2', - - sessionName: configuration.sessionName, - projectName: 'browser sdk e2e', - buildName: getBuildInfos(), - }, - }) - ), - logLevels: { - '@wdio/browserstack-service': 'info', - }, - services: [ - [ - 'browserstack', - { - browserstackLocal: true, - }, - ], - ], - user: process.env.BS_USERNAME, - key: process.env.BS_ACCESS_KEY, -} diff --git a/test/e2e/wdio.developer-extension.conf.ts b/test/e2e/wdio.developer-extension.conf.ts deleted file mode 100644 index fd822dad69..0000000000 --- a/test/e2e/wdio.developer-extension.conf.ts +++ /dev/null @@ -1,26 +0,0 @@ -import path from 'path' -import type { Options } from '@wdio/types' -import { config as baseConfig } from './wdio.base.conf' - -export const config: Options.Testrunner = { - ...baseConfig, - - specs: ['./scenario/developer-extension/*.scenario.ts'], - exclude: [], - - capabilities: [ - { - browserName: 'chrome', - 'goog:chromeOptions': { - args: [ - `--load-extension=${path.join(process.cwd(), 'developer-extension', 'dist')}`, - '--headless=new', // "new" headless needed for extensions https://www.selenium.dev/blog/2023/headless-is-going-away/ - '--no-sandbox', - ], - }, - }, - ], - - // eslint-disable-next-line @typescript-eslint/no-empty-function - onPrepare() {}, -} diff --git a/test/e2e/wdio.local.conf.ts b/test/e2e/wdio.local.conf.ts deleted file mode 100644 index 770b45f8ad..0000000000 --- a/test/e2e/wdio.local.conf.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { Options } from '@wdio/types' -import { config as baseConfig } from './wdio.base.conf' - -export const config: Options.Testrunner = { - ...baseConfig, - - capabilities: [ - { - browserName: 'chrome', - 'goog:chromeOptions': { - args: ['--headless', '--no-sandbox'], - }, - }, - ], -} diff --git a/test/unit/browsers.conf.js b/test/unit/browsers.conf.js index 83ce85398b..39122ee098 100644 --- a/test/unit/browsers.conf.js +++ b/test/unit/browsers.conf.js @@ -1,7 +1,7 @@ // Capabilities: https://www.browserstack.com/automate/capabilities /** - * @type {import('../browsers.conf').BrowserConfigurations} + * @type {Array} */ const browserConfigurations = [ { diff --git a/yarn.lock b/yarn.lock index 6ac3343658..09d99bb4ff 100644 --- a/yarn.lock +++ b/yarn.lock @@ -24,7 +24,7 @@ __metadata: languageName: node linkType: hard -"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.12.13, @babel/code-frame@npm:^7.14.5, @babel/code-frame@npm:^7.21.4": +"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.14.5": version: 7.22.10 resolution: "@babel/code-frame@npm:7.22.10" dependencies: @@ -291,16 +291,6 @@ __metadata: languageName: node linkType: hard -"@browserstack/ai-sdk-node@npm:1.5.9": - version: 1.5.9 - resolution: "@browserstack/ai-sdk-node@npm:1.5.9" - dependencies: - axios: "npm:^1.7.4" - uuid: "npm:9.0.1" - checksum: 10c0/71f3c13d6d2712e676a2f08d9a671cd9adf8be9cb50e1e4b4f9257acc1330735792f155e73fa24cbb16fe6ec9faf0c35f43fe454753950d42bb3f319089041c1 - languageName: node - linkType: hard - "@colors/colors@npm:1.5.0": version: 1.5.0 resolution: "@colors/colors@npm:1.5.0" @@ -692,15 +682,6 @@ __metadata: languageName: node linkType: hard -"@jest/expect-utils@npm:^29.7.0": - version: 29.7.0 - resolution: "@jest/expect-utils@npm:29.7.0" - dependencies: - jest-get-type: "npm:^29.6.3" - checksum: 10c0/60b79d23a5358dc50d9510d726443316253ecda3a7fb8072e1526b3e0d3b14f066ee112db95699b7a43ad3f0b61b750c72e28a5a1cac361d7a2bb34747fa938a - languageName: node - linkType: hard - "@jest/schemas@npm:^29.6.3": version: 29.6.3 resolution: "@jest/schemas@npm:29.6.3" @@ -710,20 +691,6 @@ __metadata: languageName: node linkType: hard -"@jest/types@npm:^29.6.3": - version: 29.6.3 - resolution: "@jest/types@npm:29.6.3" - dependencies: - "@jest/schemas": "npm:^29.6.3" - "@types/istanbul-lib-coverage": "npm:^2.0.0" - "@types/istanbul-reports": "npm:^3.0.0" - "@types/node": "npm:*" - "@types/yargs": "npm:^17.0.8" - chalk: "npm:^4.0.0" - checksum: 10c0/ea4e493dd3fb47933b8ccab201ae573dcc451f951dc44ed2a86123cd8541b82aa9d2b1031caf9b1080d6673c517e2dcc25a44b2dc4f3fbc37bfc965d444888c0 - languageName: node - linkType: hard - "@jridgewell/gen-mapping@npm:^0.3.0": version: 0.3.2 resolution: "@jridgewell/gen-mapping@npm:0.3.2" @@ -759,7 +726,7 @@ __metadata: languageName: node linkType: hard -"@jridgewell/sourcemap-codec@npm:^1.4.10, @jridgewell/sourcemap-codec@npm:^1.4.14, @jridgewell/sourcemap-codec@npm:^1.5.0": +"@jridgewell/sourcemap-codec@npm:^1.4.10, @jridgewell/sourcemap-codec@npm:^1.4.14": version: 1.5.0 resolution: "@jridgewell/sourcemap-codec@npm:1.5.0" checksum: 10c0/2eb864f276eb1096c3c11da3e9bb518f6d9fc0023c78344cdc037abadc725172c70314bdb360f2d4b7bffec7f5d657ce006816bc5d4ecb35e61b66132db00c18 @@ -885,15 +852,6 @@ __metadata: languageName: node linkType: hard -"@ljharb/through@npm:^2.3.11": - version: 2.3.12 - resolution: "@ljharb/through@npm:2.3.12" - dependencies: - call-bind: "npm:^1.0.5" - checksum: 10c0/7560aaef7b6ef88c16783ffde37278e2177c7f0f5427400059a8a7687b144dc711bf5b2347ab27e858a29f25e4b868d77c915c9614bc399b82b8123430614653 - languageName: node - linkType: hard - "@mantine/core@npm:7.16.2": version: 7.16.2 resolution: "@mantine/core@npm:7.16.2" @@ -1425,40 +1383,6 @@ __metadata: languageName: node linkType: hard -"@open-draft/until@npm:^1.0.3": - version: 1.0.3 - resolution: "@open-draft/until@npm:1.0.3" - checksum: 10c0/f88bcd774b55359d14a4fa80f7bfe7d9d6d26a5995e94e823e43b211656daae3663e983f0a996937da286d22f6f5da2087b661845302f236ba27f8529dcd14fb - languageName: node - linkType: hard - -"@percy/appium-app@npm:^2.0.1": - version: 2.0.3 - resolution: "@percy/appium-app@npm:2.0.3" - dependencies: - "@percy/sdk-utils": "npm:^1.27.0-beta.0" - tmp: "npm:^0.2.1" - checksum: 10c0/d7a8211f2cf19c5efa8b1414b072f51e334fabdfa8e5fcc686df11d632d022c14b98338f6b4a5cac05e5e8f9d1dd90920cc3e143833186ba7ac99addd877fee2 - languageName: node - linkType: hard - -"@percy/sdk-utils@npm:^1.27.0-beta.0, @percy/sdk-utils@npm:^1.27.2": - version: 1.27.7 - resolution: "@percy/sdk-utils@npm:1.27.7" - checksum: 10c0/35100ab397f9bb0e0d41b4105c5e0f60975cb4b1152fe58780c3aa860a7806d0c629d162be02d7caf07588f5643158fd638d1684fcb5bc612c69607fe4d6bdec - languageName: node - linkType: hard - -"@percy/selenium-webdriver@npm:^2.0.3": - version: 2.0.3 - resolution: "@percy/selenium-webdriver@npm:2.0.3" - dependencies: - "@percy/sdk-utils": "npm:^1.27.2" - node-request-interceptor: "npm:^0.6.3" - checksum: 10c0/3d2e3e7fba2606a6df172d8e139e0db488cd2edbe70fc883efa909ceca7519ef8d6870acac7c8bb2030e0f0586ead4b4fe529a2fecc6bb3114d1f663bdce1636 - languageName: node - linkType: hard - "@pkgjs/parseargs@npm:^0.11.0": version: 0.11.0 resolution: "@pkgjs/parseargs@npm:0.11.0" @@ -1473,6 +1397,17 @@ __metadata: languageName: node linkType: hard +"@playwright/test@npm:1.49.0": + version: 1.49.0 + resolution: "@playwright/test@npm:1.49.0" + dependencies: + playwright: "npm:1.49.0" + bin: + playwright: cli.js + checksum: 10c0/2890d52ee45bd83b5501f17a77c77f12ba934d257fda4b288405c6d91f94b83c4fcbdff3c0be89c2aaeea3d13576b72ec9a70be667ff844b342044afd72a246e + languageName: node + linkType: hard + "@puppeteer/browsers@npm:1.9.1, @puppeteer/browsers@npm:^1.6.0": version: 1.9.1 resolution: "@puppeteer/browsers@npm:1.9.1" @@ -1851,13 +1786,6 @@ __metadata: languageName: node linkType: hard -"@types/gitconfiglocal@npm:^2.0.1": - version: 2.0.1 - resolution: "@types/gitconfiglocal@npm:2.0.1" - checksum: 10c0/605317d889307b3512d1f2266ee47ff90d8d5ecbe95f02d8a2705fe8bdbf17b8cd680cc1f38791e87ba0685e796df1df47efad7460f88240814dd51146c8bd08 - languageName: node - linkType: hard - "@types/glob@npm:^7.1.3": version: 7.2.0 resolution: "@types/glob@npm:7.2.0" @@ -1889,31 +1817,6 @@ __metadata: languageName: node linkType: hard -"@types/istanbul-lib-coverage@npm:*, @types/istanbul-lib-coverage@npm:^2.0.0": - version: 2.0.1 - resolution: "@types/istanbul-lib-coverage@npm:2.0.1" - checksum: 10c0/2486204ab68c869928d96abd9ba4913050977b3fb26da1113156a7e6d2f203fec53d51ed9b9db3199a5eb75e69049ab600da953efd55fd9329e2c1482abb493e - languageName: node - linkType: hard - -"@types/istanbul-lib-report@npm:*": - version: 3.0.0 - resolution: "@types/istanbul-lib-report@npm:3.0.0" - dependencies: - "@types/istanbul-lib-coverage": "npm:*" - checksum: 10c0/7ced458631276a28082ee40645224c3cdd8b861961039ff811d841069171c987ec7e50bc221845ec0d04df0022b2f457a21fb2f816dab2fbe64d59377b32031f - languageName: node - linkType: hard - -"@types/istanbul-reports@npm:^3.0.0": - version: 3.0.1 - resolution: "@types/istanbul-reports@npm:3.0.1" - dependencies: - "@types/istanbul-lib-report": "npm:*" - checksum: 10c0/e147f0db9346a0cae9a359220bc76f7c78509fb6979a2597feb24d64b6e8328d2d26f9d152abbd59c6bca721e4ea2530af20116d01df50815efafd1e151fd777 - languageName: node - linkType: hard - "@types/jasmine@npm:3.10.18": version: 3.10.18 resolution: "@types/jasmine@npm:3.10.18" @@ -1995,7 +1898,16 @@ __metadata: languageName: node linkType: hard -"@types/normalize-package-data@npm:^2.4.0, @types/normalize-package-data@npm:^2.4.1": +"@types/node@npm:22.10.2": + version: 22.10.2 + resolution: "@types/node@npm:22.10.2" + dependencies: + undici-types: "npm:~6.20.0" + checksum: 10c0/2c7b71a040f1ef5320938eca8ebc946e6905caa9bbf3d5665d9b3774a8d15ea9fab1582b849a6d28c7fc80756a62c5666bc66b69f42f4d5dafd1ccb193cdb4ac + languageName: node + linkType: hard + +"@types/normalize-package-data@npm:^2.4.0": version: 2.4.1 resolution: "@types/normalize-package-data@npm:2.4.1" checksum: 10c0/c90b163741f27a1a4c3b1869d7d5c272adbd355eb50d5f060f9ce122ce4342cf35f5b0005f55ef780596cacfeb69b7eee54cd3c2e02d37f75e664945b6e75fc6 @@ -2076,20 +1988,6 @@ __metadata: languageName: node linkType: hard -"@types/stack-utils@npm:^2.0.0": - version: 2.0.1 - resolution: "@types/stack-utils@npm:2.0.1" - checksum: 10c0/3327ee919a840ffe907bbd5c1d07dfd79137dd9732d2d466cf717ceec5bb21f66296173c53bb56cff95fae4185b9cd6770df3e9745fe4ba528bbc4975f54d13f - languageName: node - linkType: hard - -"@types/triple-beam@npm:^1.3.2": - version: 1.3.2 - resolution: "@types/triple-beam@npm:1.3.2" - checksum: 10c0/2e936cff7cde9df7da854a54a5f63e0a434b2ae1d6c1eb6de5f7a0b1107b023b3c272abecbba28614a54b8831226b29e37a49e3e34a7490a6a24d770a5b44eb9 - languageName: node - linkType: hard - "@types/which@npm:^2.0.1": version: 2.0.2 resolution: "@types/which@npm:2.0.2" @@ -2106,22 +2004,6 @@ __metadata: languageName: node linkType: hard -"@types/yargs-parser@npm:*": - version: 15.0.0 - resolution: "@types/yargs-parser@npm:15.0.0" - checksum: 10c0/0dee6418ca20edd16686198485442780a2004aa53767fbf70f5b66a568a3c5e5f2fcdedcf5e0505c5065a2ab4dcf3353180a2db0ddc82470d0871f225e8da792 - languageName: node - linkType: hard - -"@types/yargs@npm:^17.0.8": - version: 17.0.24 - resolution: "@types/yargs@npm:17.0.24" - dependencies: - "@types/yargs-parser": "npm:*" - checksum: 10c0/fbebf57e1d04199e5e7eb0c67a402566fa27177ee21140664e63da826408793d203d262b48f8f41d4a7665126393d2e952a463e960e761226def247d9bbcdbd0 - languageName: node - linkType: hard - "@types/yauzl@npm:^2.9.1": version: 2.9.1 resolution: "@types/yauzl@npm:2.9.1" @@ -2243,99 +2125,6 @@ __metadata: languageName: node linkType: hard -"@vitest/pretty-format@npm:2.0.5": - version: 2.0.5 - resolution: "@vitest/pretty-format@npm:2.0.5" - dependencies: - tinyrainbow: "npm:^1.2.0" - checksum: 10c0/236c0798c5170a0b5ad5d4bd06118533738e820b4dd30079d8fbcb15baee949d41c60f42a9f769906c4a5ce366d7ef11279546070646c0efc03128c220c31f37 - languageName: node - linkType: hard - -"@vitest/snapshot@npm:^1.2.2": - version: 1.2.2 - resolution: "@vitest/snapshot@npm:1.2.2" - dependencies: - magic-string: "npm:^0.30.5" - pathe: "npm:^1.1.1" - pretty-format: "npm:^29.7.0" - checksum: 10c0/0f8a69a289aa6466c7dd56f8327190d56a0bc7ad10412127de001c94784f6dba5e5bccb757def21f565f4efa3e00c307b92e8b6c302f11fc57889b743ba18a95 - languageName: node - linkType: hard - -"@vitest/snapshot@npm:^2.0.4": - version: 2.0.5 - resolution: "@vitest/snapshot@npm:2.0.5" - dependencies: - "@vitest/pretty-format": "npm:2.0.5" - magic-string: "npm:^0.30.10" - pathe: "npm:^1.1.2" - checksum: 10c0/7bf38474248f5ae0aac6afad511785d2b7a023ac5158803c2868fd172b5b9c1a569fb1dd64a09a49e43fd342cab71ea485ada89b7f08d37b1622a5a0ac00271d - languageName: node - linkType: hard - -"@wdio/browserstack-service@npm:8.41.0": - version: 8.41.0 - resolution: "@wdio/browserstack-service@npm:8.41.0" - dependencies: - "@browserstack/ai-sdk-node": "npm:1.5.9" - "@percy/appium-app": "npm:^2.0.1" - "@percy/selenium-webdriver": "npm:^2.0.3" - "@types/gitconfiglocal": "npm:^2.0.1" - "@wdio/logger": "npm:8.38.0" - "@wdio/reporter": "npm:8.41.0" - "@wdio/types": "npm:8.41.0" - browserstack-local: "npm:^1.5.1" - chalk: "npm:^5.3.0" - csv-writer: "npm:^1.6.0" - formdata-node: "npm:5.0.1" - git-repo-info: "npm:^2.1.1" - gitconfiglocal: "npm:^2.1.0" - got: "npm:^12.6.1" - uuid: "npm:^10.0.0" - webdriverio: "npm:8.41.0" - winston-transport: "npm:^4.5.0" - yauzl: "npm:^3.0.0" - peerDependencies: - "@wdio/cli": ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 - checksum: 10c0/d495f34c0763f391efdcf070fec9714a7ada84f986cb2be8d7b732c5c90463e2b55e7b0f648e0eebc10d802a011ba29bb4bfe0befb6a19c9fcf34d5eefb44847 - languageName: node - linkType: hard - -"@wdio/cli@npm:8.41.0": - version: 8.41.0 - resolution: "@wdio/cli@npm:8.41.0" - dependencies: - "@types/node": "npm:^22.2.0" - "@vitest/snapshot": "npm:^2.0.4" - "@wdio/config": "npm:8.41.0" - "@wdio/globals": "npm:8.41.0" - "@wdio/logger": "npm:8.38.0" - "@wdio/protocols": "npm:8.40.3" - "@wdio/types": "npm:8.41.0" - "@wdio/utils": "npm:8.41.0" - async-exit-hook: "npm:^2.0.1" - chalk: "npm:^5.2.0" - chokidar: "npm:^4.0.0" - cli-spinners: "npm:^2.9.0" - dotenv: "npm:^16.3.1" - ejs: "npm:^3.1.9" - execa: "npm:^8.0.1" - import-meta-resolve: "npm:^4.0.0" - inquirer: "npm:9.2.12" - lodash.flattendeep: "npm:^4.4.0" - lodash.pickby: "npm:^4.6.0" - lodash.union: "npm:^4.6.0" - read-pkg-up: "npm:10.0.0" - recursive-readdir: "npm:^2.2.3" - webdriverio: "npm:8.41.0" - yargs: "npm:^17.7.2" - bin: - wdio: bin/wdio.js - checksum: 10c0/9394eace29b54488485ca7249ae30f274e7b6a37329c2bea24c97fbf80a223e8764b41d22f3a8571954e2d46b3337b4bac55756f0d29de419e4c4a00102c8285 - languageName: node - linkType: hard - "@wdio/config@npm:8.41.0": version: 8.41.0 resolution: "@wdio/config@npm:8.41.0" @@ -2351,64 +2140,6 @@ __metadata: languageName: node linkType: hard -"@wdio/globals@npm:8.41.0, @wdio/globals@npm:^8.29.3": - version: 8.41.0 - resolution: "@wdio/globals@npm:8.41.0" - dependencies: - expect-webdriverio: "npm:^4.11.2" - webdriverio: "npm:8.41.0" - dependenciesMeta: - expect-webdriverio: - optional: true - webdriverio: - optional: true - checksum: 10c0/bd7688bcb7b4e16afac528c8577458585b4dccb7515dce4d2726e9a8030666737ea1fda81bcf3cbfa33a0530d0d4630f804a57ba01d528160cba17b217f465c7 - languageName: node - linkType: hard - -"@wdio/jasmine-framework@npm:8.41.0": - version: 8.41.0 - resolution: "@wdio/jasmine-framework@npm:8.41.0" - dependencies: - "@types/node": "npm:^22.2.0" - "@wdio/globals": "npm:8.41.0" - "@wdio/logger": "npm:8.38.0" - "@wdio/types": "npm:8.41.0" - "@wdio/utils": "npm:8.41.0" - expect-webdriverio: "npm:^4.11.2" - jasmine: "npm:^5.0.0" - checksum: 10c0/ef9c869f6852013e5ffee99278a4da1678d509963329289f50c0aaed4c31079b78d950afe1efe2ac46d2ac6045d0da339195def9f8a3ecaba519086759293002 - languageName: node - linkType: hard - -"@wdio/junit-reporter@npm:8.41.0": - version: 8.41.0 - resolution: "@wdio/junit-reporter@npm:8.41.0" - dependencies: - "@wdio/reporter": "npm:8.41.0" - "@wdio/types": "npm:8.41.0" - json-stringify-safe: "npm:^5.0.1" - junit-report-builder: "npm:^5.0.0" - checksum: 10c0/2c00b1e10b55aadf6f185b0d89ea5227ec21f4e00ad58f94e9bacc27425e1bfec3ca9dae5d8540a1df984f9f71488223477faa230294026db276f0b0607ed33f - languageName: node - linkType: hard - -"@wdio/local-runner@npm:8.41.0": - version: 8.41.0 - resolution: "@wdio/local-runner@npm:8.41.0" - dependencies: - "@types/node": "npm:^22.2.0" - "@wdio/logger": "npm:8.38.0" - "@wdio/repl": "npm:8.40.3" - "@wdio/runner": "npm:8.41.0" - "@wdio/types": "npm:8.41.0" - async-exit-hook: "npm:^2.0.1" - split2: "npm:^4.1.0" - stream-buffers: "npm:^3.0.2" - checksum: 10c0/c86f546449a8618e1bce223d0abf26360bd05867ae6c217bc90205eb2c4d3311f6c188e46c49e178bd3a687f9d44eaf7e45cca7e4764c023915e534a9670965a - languageName: node - linkType: hard - "@wdio/logger@npm:8.38.0, @wdio/logger@npm:^8.11.0, @wdio/logger@npm:^8.28.0": version: 8.38.0 resolution: "@wdio/logger@npm:8.38.0" @@ -2437,51 +2168,6 @@ __metadata: languageName: node linkType: hard -"@wdio/reporter@npm:8.41.0": - version: 8.41.0 - resolution: "@wdio/reporter@npm:8.41.0" - dependencies: - "@types/node": "npm:^22.2.0" - "@wdio/logger": "npm:8.38.0" - "@wdio/types": "npm:8.41.0" - diff: "npm:^7.0.0" - object-inspect: "npm:^1.12.0" - checksum: 10c0/a8158fd0508b8018d35958825193c8e4b835b8c179a9bb20dc7869346bc6cdd50804917efef8632e0563fc6dca37a2f1915a86baa93b7f1cb5ee4efd0ad193b4 - languageName: node - linkType: hard - -"@wdio/runner@npm:8.41.0": - version: 8.41.0 - resolution: "@wdio/runner@npm:8.41.0" - dependencies: - "@types/node": "npm:^22.2.0" - "@wdio/config": "npm:8.41.0" - "@wdio/globals": "npm:8.41.0" - "@wdio/logger": "npm:8.38.0" - "@wdio/types": "npm:8.41.0" - "@wdio/utils": "npm:8.41.0" - deepmerge-ts: "npm:^5.1.0" - expect-webdriverio: "npm:^4.12.0" - gaze: "npm:^1.1.3" - webdriver: "npm:8.41.0" - webdriverio: "npm:8.41.0" - checksum: 10c0/97f22ad0cc48bf6b66d95345d4000b1cf484a39606766db7d15708324d62913369acf043db8f81e3a56cedba3188e4137551df1f7bc409cf399d9d274da483ed - languageName: node - linkType: hard - -"@wdio/spec-reporter@npm:8.41.0": - version: 8.41.0 - resolution: "@wdio/spec-reporter@npm:8.41.0" - dependencies: - "@wdio/reporter": "npm:8.41.0" - "@wdio/types": "npm:8.41.0" - chalk: "npm:^5.1.2" - easy-table: "npm:^1.2.0" - pretty-ms: "npm:^7.0.0" - checksum: 10c0/ca472c2066b14d341df3960a3b4bb64be1c9170023a1839267c9f84bd1018e2bd7ec46fe65d1e88149dbfe42ef1e1018525558fc9df28a1b4ecb1ba7779e1cfd - languageName: node - linkType: hard - "@wdio/types@npm:8.41.0": version: 8.41.0 resolution: "@wdio/types@npm:8.41.0" @@ -2940,7 +2626,7 @@ __metadata: languageName: node linkType: hard -"ansi-escapes@npm:^4.2.1, ansi-escapes@npm:^4.3.2": +"ansi-escapes@npm:^4.2.1": version: 4.3.2 resolution: "ansi-escapes@npm:4.3.2" dependencies: @@ -3232,13 +2918,6 @@ __metadata: languageName: node linkType: hard -"async-exit-hook@npm:^2.0.1": - version: 2.0.1 - resolution: "async-exit-hook@npm:2.0.1" - checksum: 10c0/81407a440ef0aab328df2369f1a9d957ee53e9a5a43e3b3dcb2be05151a68de0e4ff5e927f4718c88abf85800731f5b3f69a47a6642ce135f5e7d43ca0fce41d - languageName: node - linkType: hard - "async@npm:^3.2.3, async@npm:^3.2.4": version: 3.2.4 resolution: "async@npm:3.2.4" @@ -3488,17 +3167,13 @@ __metadata: dependencies: "@eslint/js": "npm:9.19.0" "@jsdevtools/coverage-istanbul-loader": "npm:3.0.5" + "@playwright/test": "npm:1.49.0" "@types/chrome": "npm:0.0.301" "@types/connect-busboy": "npm:1.0.3" "@types/cors": "npm:2.8.17" "@types/express": "npm:4.17.21" "@types/jasmine": "npm:3.10.18" - "@wdio/browserstack-service": "npm:8.41.0" - "@wdio/cli": "npm:8.41.0" - "@wdio/jasmine-framework": "npm:8.41.0" - "@wdio/junit-reporter": "npm:8.41.0" - "@wdio/local-runner": "npm:8.41.0" - "@wdio/spec-reporter": "npm:8.41.0" + "@types/node": "npm:22.10.2" ajv: "npm:6.12.6" browserstack-local: "npm:1.5.6" chrome-webstore-upload: "npm:3.1.4" @@ -3558,7 +3233,7 @@ __metadata: languageName: node linkType: hard -"browserstack-local@npm:1.5.6, browserstack-local@npm:^1.3.7, browserstack-local@npm:^1.5.1": +"browserstack-local@npm:1.5.6, browserstack-local@npm:^1.3.7": version: 1.5.6 resolution: "browserstack-local@npm:1.5.6" dependencies: @@ -3835,7 +3510,7 @@ __metadata: languageName: node linkType: hard -"chalk@npm:^5.1.2, chalk@npm:^5.2.0, chalk@npm:^5.3.0": +"chalk@npm:^5.1.2": version: 5.3.0 resolution: "chalk@npm:5.3.0" checksum: 10c0/8297d436b2c0f95801103ff2ef67268d362021b8210daf8ddbe349695333eb3610a71122172ff3b0272f1ef2cf7cc2c41fdaa4715f52e49ffe04c56340feed09 @@ -3868,15 +3543,6 @@ __metadata: languageName: node linkType: hard -"chokidar@npm:^4.0.0": - version: 4.0.1 - resolution: "chokidar@npm:4.0.1" - dependencies: - readdirp: "npm:^4.0.1" - checksum: 10c0/4bb7a3adc304059810bb6c420c43261a15bb44f610d77c35547addc84faa0374265c3adc67f25d06f363d9a4571962b02679268c40de07676d260de1986efea9 - languageName: node - linkType: hard - "chownr@npm:^2.0.0": version: 2.0.0 resolution: "chownr@npm:2.0.0" @@ -3992,7 +3658,7 @@ __metadata: languageName: node linkType: hard -"cli-spinners@npm:^2.5.0, cli-spinners@npm:^2.9.0": +"cli-spinners@npm:^2.5.0": version: 2.9.0 resolution: "cli-spinners@npm:2.9.0" checksum: 10c0/c0d5437acc1ace7361b1c58a4fda3c92c2d8691ff3169ac658ce30faee71280b7aa706c072bcb6d0e380c232f3495f7d5ad4668c1391fe02c4d3a39d37798f44 @@ -4006,13 +3672,6 @@ __metadata: languageName: node linkType: hard -"cli-width@npm:^4.1.0": - version: 4.1.0 - resolution: "cli-width@npm:4.1.0" - checksum: 10c0/1fbd56413578f6117abcaf858903ba1f4ad78370a4032f916745fa2c7e390183a9d9029cf837df320b0fdce8137668e522f60a30a5f3d6529ff3872d265a955f - languageName: node - linkType: hard - "cliui@npm:^7.0.2": version: 7.0.4 resolution: "cliui@npm:7.0.4" @@ -4587,13 +4246,6 @@ __metadata: languageName: node linkType: hard -"csv-writer@npm:^1.6.0": - version: 1.6.0 - resolution: "csv-writer@npm:1.6.0" - checksum: 10c0/9d30518c6cc7489e0cd8a3a83d6b014c27ed537d6e4a866f901065ddf49e06e4815a4204ce02c3c0024fd8e648e4a2efbdb318a1c63ddb3026282682d3a665e3 - languageName: node - linkType: hard - "custom-event@npm:~1.0.0": version: 1.0.1 resolution: "custom-event@npm:1.0.1" @@ -4688,7 +4340,7 @@ __metadata: languageName: node linkType: hard -"debug@npm:4, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.0, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4, debug@npm:^4.3.6, debug@npm:^4.4.0": +"debug@npm:4, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4, debug@npm:^4.3.6, debug@npm:^4.4.0": version: 4.4.0 resolution: "debug@npm:4.4.0" dependencies: @@ -4946,13 +4598,6 @@ __metadata: languageName: node linkType: hard -"diff@npm:^7.0.0": - version: 7.0.0 - resolution: "diff@npm:7.0.0" - checksum: 10c0/251fd15f85ffdf814cfc35a728d526b8d2ad3de338dcbd011ac6e57c461417090766b28995f8ff733135b5fbc3699c392db1d5e27711ac4e00244768cd1d577b - languageName: node - linkType: hard - "dir-glob@npm:^3.0.1": version: 3.0.1 resolution: "dir-glob@npm:3.0.1" @@ -5058,7 +4703,7 @@ __metadata: languageName: node linkType: hard -"dotenv@npm:^16.3.1, dotenv@npm:^16.4.4, dotenv@npm:~16.4.5": +"dotenv@npm:^16.4.4, dotenv@npm:~16.4.5": version: 16.4.5 resolution: "dotenv@npm:16.4.5" checksum: 10c0/48d92870076832af0418b13acd6e5a5a3e83bb00df690d9812e94b24aff62b88ade955ac99a05501305b8dc8f1b0ee7638b18493deb6fe93d680e5220936292f @@ -5088,19 +4733,6 @@ __metadata: languageName: node linkType: hard -"easy-table@npm:^1.2.0": - version: 1.2.0 - resolution: "easy-table@npm:1.2.0" - dependencies: - ansi-regex: "npm:^5.0.1" - wcwidth: "npm:^1.0.1" - dependenciesMeta: - wcwidth: - optional: true - checksum: 10c0/2d37937cd608586ba02e1ec479f90ccec581d366b3b0d1bb26b99ee6005f8d724e32a07a873759893461ca45b99e2d08c30326529d967ce9eedc1e9b68d4aa63 - languageName: node - linkType: hard - "edge-paths@npm:^3.0.5": version: 3.0.5 resolution: "edge-paths@npm:3.0.5" @@ -5134,7 +4766,7 @@ __metadata: languageName: node linkType: hard -"ejs@npm:^3.1.7, ejs@npm:^3.1.9": +"ejs@npm:^3.1.7": version: 3.1.10 resolution: "ejs@npm:3.1.10" dependencies: @@ -5313,7 +4945,7 @@ __metadata: languageName: node linkType: hard -"error-ex@npm:^1.3.1, error-ex@npm:^1.3.2": +"error-ex@npm:^1.3.1": version: 1.3.2 resolution: "error-ex@npm:1.3.2" dependencies: @@ -5521,13 +5153,6 @@ __metadata: languageName: node linkType: hard -"escape-string-regexp@npm:^2.0.0": - version: 2.0.0 - resolution: "escape-string-regexp@npm:2.0.0" - checksum: 10c0/2530479fe8db57eace5e8646c9c2a9c80fa279614986d16dcc6bcaceb63ae77f05a851ba6c43756d816c61d7f4534baf56e3c705e3e0d884818a46808811c507 - languageName: node - linkType: hard - "escape-string-regexp@npm:^4.0.0": version: 4.0.0 resolution: "escape-string-regexp@npm:4.0.0" @@ -5535,13 +5160,6 @@ __metadata: languageName: node linkType: hard -"escape-string-regexp@npm:^5.0.0": - version: 5.0.0 - resolution: "escape-string-regexp@npm:5.0.0" - checksum: 10c0/6366f474c6f37a802800a435232395e04e9885919873e382b157ab7e8f0feb8fed71497f84a6f6a81a49aab41815522f5839112bd38026d203aea0c91622df95 - languageName: node - linkType: hard - "escodegen@npm:^2.1.0": version: 2.1.0 resolution: "escodegen@npm:2.1.0" @@ -5891,23 +5509,6 @@ __metadata: languageName: node linkType: hard -"execa@npm:^8.0.1": - version: 8.0.1 - resolution: "execa@npm:8.0.1" - dependencies: - cross-spawn: "npm:^7.0.3" - get-stream: "npm:^8.0.1" - human-signals: "npm:^5.0.0" - is-stream: "npm:^3.0.0" - merge-stream: "npm:^2.0.0" - npm-run-path: "npm:^5.1.0" - onetime: "npm:^6.0.0" - signal-exit: "npm:^4.1.0" - strip-final-newline: "npm:^3.0.0" - checksum: 10c0/2c52d8775f5bf103ce8eec9c7ab3059909ba350a5164744e9947ed14a53f51687c040a250bda833f906d1283aa8803975b84e6c8f7a7c42f99dc8ef80250d1af - languageName: node - linkType: hard - "exit-on-epipe@npm:~1.0.1": version: 1.0.1 resolution: "exit-on-epipe@npm:1.0.1" @@ -5915,41 +5516,6 @@ __metadata: languageName: node linkType: hard -"expect-webdriverio@npm:^4.11.2, expect-webdriverio@npm:^4.12.0": - version: 4.12.2 - resolution: "expect-webdriverio@npm:4.12.2" - dependencies: - "@vitest/snapshot": "npm:^1.2.2" - "@wdio/globals": "npm:^8.29.3" - "@wdio/logger": "npm:^8.28.0" - expect: "npm:^29.7.0" - jest-matcher-utils: "npm:^29.7.0" - lodash.isequal: "npm:^4.5.0" - webdriverio: "npm:^8.29.3" - dependenciesMeta: - "@wdio/globals": - optional: true - "@wdio/logger": - optional: true - webdriverio: - optional: true - checksum: 10c0/bc5d124893bd7f1ee310eed4f91a7b2eb45b15995f021aea5a037f5ccfe63211fef0c975aca79a868c1c9345ff2911bd46dd81763cc190666bc4e53549c99df5 - languageName: node - linkType: hard - -"expect@npm:^29.7.0": - version: 29.7.0 - resolution: "expect@npm:29.7.0" - dependencies: - "@jest/expect-utils": "npm:^29.7.0" - jest-get-type: "npm:^29.6.3" - jest-matcher-utils: "npm:^29.7.0" - jest-message-util: "npm:^29.7.0" - jest-util: "npm:^29.7.0" - checksum: 10c0/2eddeace66e68b8d8ee5f7be57f3014b19770caaf6815c7a08d131821da527fb8c8cb7b3dcd7c883d2d3d8d184206a4268984618032d1e4b16dc8d6596475d41 - languageName: node - linkType: hard - "exponential-backoff@npm:^3.1.1": version: 3.1.1 resolution: "exponential-backoff@npm:3.1.1" @@ -6012,7 +5578,7 @@ __metadata: languageName: node linkType: hard -"external-editor@npm:^3.0.3, external-editor@npm:^3.1.0": +"external-editor@npm:^3.0.3": version: 3.1.0 resolution: "external-editor@npm:3.1.0" dependencies: @@ -6113,13 +5679,6 @@ __metadata: languageName: node linkType: hard -"fecha@npm:^4.2.0": - version: 4.2.3 - resolution: "fecha@npm:4.2.3" - checksum: 10c0/0e895965959cf6a22bb7b00f0bf546f2783836310f510ddf63f463e1518d4c96dec61ab33fdfd8e79a71b4856a7c865478ce2ee8498d560fe125947703c9b1cf - languageName: node - linkType: hard - "fetch-blob@npm:^3.1.2, fetch-blob@npm:^3.1.4": version: 3.1.5 resolution: "fetch-blob@npm:3.1.5" @@ -6139,16 +5698,6 @@ __metadata: languageName: node linkType: hard -"figures@npm:^5.0.0": - version: 5.0.0 - resolution: "figures@npm:5.0.0" - dependencies: - escape-string-regexp: "npm:^5.0.0" - is-unicode-supported: "npm:^1.2.0" - checksum: 10c0/ce0f17d4ea8b0fc429c5207c343534a2f5284ecfb22aa08607da7dc84ed9e1cf754f5b97760e8dcb98d3c9d1a1e4d3d578fe3b5b99c426f05d0f06c7ba618e16 - languageName: node - linkType: hard - "file-entry-cache@npm:^8.0.0": version: 8.0.0 resolution: "file-entry-cache@npm:8.0.0" @@ -6235,16 +5784,6 @@ __metadata: languageName: node linkType: hard -"find-up@npm:^6.3.0": - version: 6.3.0 - resolution: "find-up@npm:6.3.0" - dependencies: - locate-path: "npm:^7.1.0" - path-exists: "npm:^5.0.0" - checksum: 10c0/07e0314362d316b2b13f7f11ea4692d5191e718ca3f7264110127520f3347996349bf9e16805abae3e196805814bc66ef4bff2b8904dc4a6476085fc9b0eba07 - languageName: node - linkType: hard - "flat-cache@npm:^4.0.0": version: 4.0.1 resolution: "flat-cache@npm:4.0.1" @@ -6318,16 +5857,6 @@ __metadata: languageName: node linkType: hard -"formdata-node@npm:5.0.1": - version: 5.0.1 - resolution: "formdata-node@npm:5.0.1" - dependencies: - node-domexception: "npm:1.0.0" - web-streams-polyfill: "npm:4.0.0-beta.3" - checksum: 10c0/5254255d85f82308020d389818e38fceb93005a523744e0da9b7e1739910a05d3c6390887d6c4075385234cd7f0b79a1032a49ac905c1bfe2f4d043283263dfc - languageName: node - linkType: hard - "formdata-polyfill@npm:^4.0.10": version: 4.0.10 resolution: "formdata-polyfill@npm:4.0.10" @@ -6432,7 +5961,7 @@ __metadata: languageName: node linkType: hard -"fsevents@npm:~2.3.2": +"fsevents@npm:2.3.2, fsevents@npm:~2.3.2": version: 2.3.2 resolution: "fsevents@npm:2.3.2" dependencies: @@ -6442,7 +5971,7 @@ __metadata: languageName: node linkType: hard -"fsevents@patch:fsevents@npm%3A~2.3.2#optional!builtin": +"fsevents@patch:fsevents@npm%3A2.3.2#optional!builtin, fsevents@patch:fsevents@npm%3A~2.3.2#optional!builtin": version: 2.3.2 resolution: "fsevents@patch:fsevents@npm%3A2.3.2#optional!builtin::version=2.3.2&hash=df0bf1" dependencies: @@ -6505,15 +6034,6 @@ __metadata: languageName: node linkType: hard -"gaze@npm:^1.1.3": - version: 1.1.3 - resolution: "gaze@npm:1.1.3" - dependencies: - globule: "npm:^1.0.0" - checksum: 10c0/5369619e23f6585e3a5efc4b8fad3b9f129fb4a88685bf0d6a98ca5ea0adb3868ede3d05643101deb03c42e15a0d36182d37f0122945935d05eddc82f4d79bfe - languageName: node - linkType: hard - "geckodriver@npm:~4.2.0": version: 4.2.1 resolution: "geckodriver@npm:4.2.1" @@ -6624,13 +6144,6 @@ __metadata: languageName: node linkType: hard -"get-stream@npm:^8.0.1": - version: 8.0.1 - resolution: "get-stream@npm:8.0.1" - checksum: 10c0/5c2181e98202b9dae0bb4a849979291043e5892eb40312b47f0c22b9414fc9b28a3b6063d2375705eb24abc41ecf97894d9a51f64ff021511b504477b27b4290 - languageName: node - linkType: hard - "get-symbol-description@npm:^1.0.2": version: 1.0.2 resolution: "get-symbol-description@npm:1.0.2" @@ -6677,13 +6190,6 @@ __metadata: languageName: node linkType: hard -"git-repo-info@npm:^2.1.1": - version: 2.1.1 - resolution: "git-repo-info@npm:2.1.1" - checksum: 10c0/894d03a1c8338ccddf7de3cd9ddbae5c16371164b84649ed5dfec9a1c7efbc761885f0f541a21bd033c07b91203ab315a63c77f36b142f18ad0f17a9699eb028 - languageName: node - linkType: hard - "git-semver-tags@npm:^5.0.0": version: 5.0.0 resolution: "git-semver-tags@npm:5.0.0" @@ -6724,15 +6230,6 @@ __metadata: languageName: node linkType: hard -"gitconfiglocal@npm:^2.1.0": - version: 2.1.0 - resolution: "gitconfiglocal@npm:2.1.0" - dependencies: - ini: "npm:^1.3.2" - checksum: 10c0/0882267ff1f7d13c2ab42f55bf1e329505054811862f1ae36b650b91f1fe4ea2fb85ef2bb9695b81454330fa30b8bbc179c69886d2e88e5ab2cc998eee3b02af - languageName: node - linkType: hard - "glob-parent@npm:6.0.2, glob-parent@npm:^6.0.1, glob-parent@npm:^6.0.2": version: 6.0.2 resolution: "glob-parent@npm:6.0.2" @@ -6840,20 +6337,6 @@ __metadata: languageName: node linkType: hard -"glob@npm:~7.1.1": - version: 7.1.6 - resolution: "glob@npm:7.1.6" - dependencies: - fs.realpath: "npm:^1.0.0" - inflight: "npm:^1.0.4" - inherits: "npm:2" - minimatch: "npm:^3.0.4" - once: "npm:^1.3.0" - path-is-absolute: "npm:^1.0.0" - checksum: 10c0/2575cce9306ac534388db751f0aa3e78afedb6af8f3b529ac6b2354f66765545145dba8530abf7bff49fb399a047d3f9b6901c38ee4c9503f592960d9af67763 - languageName: node - linkType: hard - "globals@npm:15.14.0, globals@npm:^15.9.0": version: 15.14.0 resolution: "globals@npm:15.14.0" @@ -6912,17 +6395,6 @@ __metadata: languageName: node linkType: hard -"globule@npm:^1.0.0": - version: 1.2.1 - resolution: "globule@npm:1.2.1" - dependencies: - glob: "npm:~7.1.1" - lodash: "npm:~4.17.10" - minimatch: "npm:~3.0.2" - checksum: 10c0/a0cfd9fb6511a1fd0729fd4a4240fb7283afb19603f5e9e00597d9806deaaaaf048091c43238fc2e3fec9fd0328355f0da00c3ab6c1af9850e394ef34e5845e9 - languageName: node - linkType: hard - "gopd@npm:^1.0.1": version: 1.0.1 resolution: "gopd@npm:1.0.1" @@ -6951,7 +6423,7 @@ __metadata: languageName: node linkType: hard -"graceful-fs@npm:4.2.11, graceful-fs@npm:^4.1.11, graceful-fs@npm:^4.1.15, graceful-fs@npm:^4.1.2, graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.10, graceful-fs@npm:^4.2.11, graceful-fs@npm:^4.2.2, graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6, graceful-fs@npm:^4.2.9": +"graceful-fs@npm:4.2.11, graceful-fs@npm:^4.1.11, graceful-fs@npm:^4.1.15, graceful-fs@npm:^4.1.2, graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.10, graceful-fs@npm:^4.2.11, graceful-fs@npm:^4.2.2, graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6": version: 4.2.11 resolution: "graceful-fs@npm:4.2.11" checksum: 10c0/386d011a553e02bc594ac2ca0bd6d9e4c22d7fa8cfbfc448a6d148c59ea881b092db9dbe3547ae4b88e55f1b01f7c4a2ecc53b310c042793e63aa44cf6c257f2 @@ -7075,13 +6547,6 @@ __metadata: languageName: node linkType: hard -"headers-utils@npm:^1.2.0": - version: 1.2.5 - resolution: "headers-utils@npm:1.2.5" - checksum: 10c0/b0777bf1b5da6d60ce3da399efec37c40e4b32e2eed832e95f9c3b491ef28bfaf1c631e9825d910062a42a850a2dea7aab4d345afbc4ffc3d5e05b02c6e6ea8a - languageName: node - linkType: hard - "hosted-git-info@npm:^2.1.4": version: 2.8.9 resolution: "hosted-git-info@npm:2.8.9" @@ -7098,15 +6563,6 @@ __metadata: languageName: node linkType: hard -"hosted-git-info@npm:^6.0.0": - version: 6.1.1 - resolution: "hosted-git-info@npm:6.1.1" - dependencies: - lru-cache: "npm:^7.5.1" - checksum: 10c0/ba7158f81ae29c1b5a1e452fa517082f928051da8797a00788a84ff82b434996d34f78a875bbb688aec162bda1d4cf71d2312f44da3c896058803f5efa6ce77f - languageName: node - linkType: hard - "hosted-git-info@npm:^7.0.0, hosted-git-info@npm:^7.0.2": version: 7.0.2 resolution: "hosted-git-info@npm:7.0.2" @@ -7272,13 +6728,6 @@ __metadata: languageName: node linkType: hard -"human-signals@npm:^5.0.0": - version: 5.0.0 - resolution: "human-signals@npm:5.0.0" - checksum: 10c0/5a9359073fe17a8b58e5a085e9a39a950366d9f00217c4ff5878bd312e09d80f460536ea6a3f260b5943a01fe55c158d1cea3fc7bee3d0520aeef04f6d915c82 - languageName: node - linkType: hard - "humanize-ms@npm:^1.2.1": version: 1.2.1 resolution: "humanize-ms@npm:1.2.1" @@ -7436,31 +6885,8 @@ __metadata: read: "npm:^3.0.1" semver: "npm:^7.3.5" validate-npm-package-license: "npm:^3.0.4" - validate-npm-package-name: "npm:^5.0.0" - checksum: 10c0/a80f024ee041a2cf4d3062ba936abf015cbc32bda625cabe994d1fa4bd942bb9af37a481afd6880d340d3e94d90bf97bed1a0a877cc8c7c9b48e723c2524ae74 - languageName: node - linkType: hard - -"inquirer@npm:9.2.12": - version: 9.2.12 - resolution: "inquirer@npm:9.2.12" - dependencies: - "@ljharb/through": "npm:^2.3.11" - ansi-escapes: "npm:^4.3.2" - chalk: "npm:^5.3.0" - cli-cursor: "npm:^3.1.0" - cli-width: "npm:^4.1.0" - external-editor: "npm:^3.1.0" - figures: "npm:^5.0.0" - lodash: "npm:^4.17.21" - mute-stream: "npm:1.0.0" - ora: "npm:^5.4.1" - run-async: "npm:^3.0.0" - rxjs: "npm:^7.8.1" - string-width: "npm:^4.2.3" - strip-ansi: "npm:^6.0.1" - wrap-ansi: "npm:^6.2.0" - checksum: 10c0/efc19864bea5f4b22a47e686aa88684ee42352db4e96dd6307da7140496c16e5ef0e74be664fba490b068714dc24d72f66dc1907a1ccbaf9d58d6156cbdc5908 + validate-npm-package-name: "npm:^5.0.0" + checksum: 10c0/a80f024ee041a2cf4d3062ba936abf015cbc32bda625cabe994d1fa4bd942bb9af37a481afd6880d340d3e94d90bf97bed1a0a877cc8c7c9b48e723c2524ae74 languageName: node linkType: hard @@ -7594,7 +7020,7 @@ __metadata: languageName: node linkType: hard -"is-core-module@npm:^2.13.0, is-core-module@npm:^2.15.1, is-core-module@npm:^2.5.0, is-core-module@npm:^2.8.1": +"is-core-module@npm:^2.13.0, is-core-module@npm:^2.15.1, is-core-module@npm:^2.5.0": version: 2.15.1 resolution: "is-core-module@npm:2.15.1" dependencies: @@ -7781,13 +7207,6 @@ __metadata: languageName: node linkType: hard -"is-stream@npm:^3.0.0": - version: 3.0.0 - resolution: "is-stream@npm:3.0.0" - checksum: 10c0/eb2f7127af02ee9aa2a0237b730e47ac2de0d4e76a4a905a50a11557f2339df5765eaea4ceb8029f1efa978586abe776908720bfcb1900c20c6ec5145f6f29d8 - languageName: node - linkType: hard - "is-string@npm:^1.0.5, is-string@npm:^1.0.7": version: 1.0.7 resolution: "is-string@npm:1.0.7" @@ -7831,13 +7250,6 @@ __metadata: languageName: node linkType: hard -"is-unicode-supported@npm:^1.2.0": - version: 1.3.0 - resolution: "is-unicode-supported@npm:1.3.0" - checksum: 10c0/b8674ea95d869f6faabddc6a484767207058b91aea0250803cbf1221345cb0c56f466d4ecea375dc77f6633d248d33c47bd296fb8f4cdba0b4edba8917e83d8a - languageName: node - linkType: hard - "is-weakref@npm:^1.0.2": version: 1.0.2 resolution: "is-weakref@npm:1.0.2" @@ -8012,26 +7424,7 @@ __metadata: languageName: node linkType: hard -"jasmine-core@npm:~5.0.0": - version: 5.0.0 - resolution: "jasmine-core@npm:5.0.0" - checksum: 10c0/b4c67d2a60bbf56e28b58098ceec1addd4ff6079e491d8017765615bb5ab4af2c38078a08a7a88fa11dd09b21c8a2d38e59b726f06bb0ff219c739dd3555cc03 - languageName: node - linkType: hard - -"jasmine@npm:^5.0.0": - version: 5.0.0 - resolution: "jasmine@npm:5.0.0" - dependencies: - glob: "npm:^10.2.2" - jasmine-core: "npm:~5.0.0" - bin: - jasmine: bin/jasmine.js - checksum: 10c0/467193d5dac69330a51afc0013ee04abca071ec8f6900b5d5af089043eb74d9e4aec336f774c26ef1adc8c701d15be95273e9fc57d82a94936cf5b4a66a8c2c9 - languageName: node - linkType: hard - -"jest-diff@npm:>=29.4.3 < 30, jest-diff@npm:^29.4.1, jest-diff@npm:^29.7.0": +"jest-diff@npm:>=29.4.3 < 30, jest-diff@npm:^29.4.1": version: 29.7.0 resolution: "jest-diff@npm:29.7.0" dependencies: @@ -8050,49 +7443,6 @@ __metadata: languageName: node linkType: hard -"jest-matcher-utils@npm:^29.7.0": - version: 29.7.0 - resolution: "jest-matcher-utils@npm:29.7.0" - dependencies: - chalk: "npm:^4.0.0" - jest-diff: "npm:^29.7.0" - jest-get-type: "npm:^29.6.3" - pretty-format: "npm:^29.7.0" - checksum: 10c0/0d0e70b28fa5c7d4dce701dc1f46ae0922102aadc24ed45d594dd9b7ae0a8a6ef8b216718d1ab79e451291217e05d4d49a82666e1a3cc2b428b75cd9c933244e - languageName: node - linkType: hard - -"jest-message-util@npm:^29.7.0": - version: 29.7.0 - resolution: "jest-message-util@npm:29.7.0" - dependencies: - "@babel/code-frame": "npm:^7.12.13" - "@jest/types": "npm:^29.6.3" - "@types/stack-utils": "npm:^2.0.0" - chalk: "npm:^4.0.0" - graceful-fs: "npm:^4.2.9" - micromatch: "npm:^4.0.4" - pretty-format: "npm:^29.7.0" - slash: "npm:^3.0.0" - stack-utils: "npm:^2.0.3" - checksum: 10c0/850ae35477f59f3e6f27efac5215f706296e2104af39232bb14e5403e067992afb5c015e87a9243ec4d9df38525ef1ca663af9f2f4766aa116f127247008bd22 - languageName: node - linkType: hard - -"jest-util@npm:^29.7.0": - version: 29.7.0 - resolution: "jest-util@npm:29.7.0" - dependencies: - "@jest/types": "npm:^29.6.3" - "@types/node": "npm:*" - chalk: "npm:^4.0.0" - ci-info: "npm:^3.2.0" - graceful-fs: "npm:^4.2.9" - picomatch: "npm:^2.2.3" - checksum: 10c0/bc55a8f49fdbb8f51baf31d2a4f312fb66c9db1483b82f602c9c990e659cdd7ec529c8e916d5a89452ecbcfae4949b21b40a7a59d4ffc0cd813a973ab08c8150 - languageName: node - linkType: hard - "jest-worker@npm:^27.4.5": version: 27.5.1 resolution: "jest-worker@npm:27.5.1" @@ -8356,17 +7706,6 @@ __metadata: languageName: node linkType: hard -"junit-report-builder@npm:^5.0.0": - version: 5.0.0 - resolution: "junit-report-builder@npm:5.0.0" - dependencies: - lodash: "npm:^4.17.21" - make-dir: "npm:^3.1.0" - xmlbuilder: "npm:^15.1.1" - checksum: 10c0/e056b900901585095b2c072e5c10c5e93ad76e9ebdbf6e13203f950516164f23a485590648854fece71140dcaa0294533d20a639ff9ad463d8cd1b20518e1131 - languageName: node - linkType: hard - "just-diff-apply@npm:^5.2.0": version: 5.5.0 resolution: "just-diff-apply@npm:5.5.0" @@ -8674,7 +8013,7 @@ __metadata: languageName: node linkType: hard -"lines-and-columns@npm:2.0.3, lines-and-columns@npm:^2.0.3": +"lines-and-columns@npm:2.0.3": version: 2.0.3 resolution: "lines-and-columns@npm:2.0.3" checksum: 10c0/09525c10010a925b7efe858f1dd3184eeac34f0a9bc34993075ec490efad71e948147746b18e9540279cc87cd44085b038f986903db3de65ffe96d38a7b91c4c @@ -8776,15 +8115,6 @@ __metadata: languageName: node linkType: hard -"locate-path@npm:^7.1.0": - version: 7.2.0 - resolution: "locate-path@npm:7.2.0" - dependencies: - p-locate: "npm:^6.0.0" - checksum: 10c0/139e8a7fe11cfbd7f20db03923cacfa5db9e14fa14887ea121345597472b4a63c1a42a8a5187defeeff6acf98fd568da7382aa39682d38f0af27433953a97751 - languageName: node - linkType: hard - "lodash.clonedeep@npm:^4.5.0": version: 4.5.0 resolution: "lodash.clonedeep@npm:4.5.0" @@ -8792,20 +8122,6 @@ __metadata: languageName: node linkType: hard -"lodash.flattendeep@npm:^4.4.0": - version: 4.4.0 - resolution: "lodash.flattendeep@npm:4.4.0" - checksum: 10c0/83cb80754b921fb4ed2c222b91a82b2524f3bdc60c3ae91e00688bd4bf1bcc28b8a2cc250e11fdc1b6da3a2de09e57008e13f15a209cafdd4f9163d047f97544 - languageName: node - linkType: hard - -"lodash.isequal@npm:^4.5.0": - version: 4.5.0 - resolution: "lodash.isequal@npm:4.5.0" - checksum: 10c0/dfdb2356db19631a4b445d5f37868a095e2402292d59539a987f134a8778c62a2810c2452d11ae9e6dcac71fc9de40a6fedcb20e2952a15b431ad8b29e50e28f - languageName: node - linkType: hard - "lodash.ismatch@npm:^4.4.0": version: 4.4.0 resolution: "lodash.ismatch@npm:4.4.0" @@ -8820,20 +8136,6 @@ __metadata: languageName: node linkType: hard -"lodash.pickby@npm:^4.6.0": - version: 4.6.0 - resolution: "lodash.pickby@npm:4.6.0" - checksum: 10c0/46befadb64ab0f61159977174b291f87b005cec1c7bd73d1b6949ec4cdff483c1be0e34398df8955b76ce06a3e93a4a5c5a552a4299520390d6993c5420c7ab9 - languageName: node - linkType: hard - -"lodash.union@npm:^4.6.0": - version: 4.6.0 - resolution: "lodash.union@npm:4.6.0" - checksum: 10c0/6da7f72d1facd472f6090b49eefff984c9f9179e13172039c0debca6851d21d37d83c7ad5c43af23bd220f184cd80e6897e8e3206509fae491f9068b02ae6319 - languageName: node - linkType: hard - "lodash.zip@npm:^4.2.0": version: 4.2.0 resolution: "lodash.zip@npm:4.2.0" @@ -8841,7 +8143,7 @@ __metadata: languageName: node linkType: hard -"lodash@npm:^4.17.15, lodash@npm:^4.17.20, lodash@npm:^4.17.21, lodash@npm:~4.17.10": +"lodash@npm:^4.17.15, lodash@npm:^4.17.20, lodash@npm:^4.17.21": version: 4.17.21 resolution: "lodash@npm:4.17.21" checksum: 10c0/d8cbea072bb08655bb4c989da418994b073a608dffa608b09ac04b43a791b12aeae7cd7ad919aa4c925f33b48490b5cfe6c1f71d827956071dae2e7bb3a6b74c @@ -8871,20 +8173,6 @@ __metadata: languageName: node linkType: hard -"logform@npm:^2.3.2": - version: 2.5.1 - resolution: "logform@npm:2.5.1" - dependencies: - "@colors/colors": "npm:1.5.0" - "@types/triple-beam": "npm:^1.3.2" - fecha: "npm:^4.2.0" - ms: "npm:^2.1.1" - safe-stable-stringify: "npm:^2.3.1" - triple-beam: "npm:^1.3.0" - checksum: 10c0/d11c36b4c42063abc816fda2fd149cff9969a9943d42afd95ddd1426804980b4e92e24f2ea6a9916fd490224b1c97578734a37d3b40ce3a9418495ce52e8ef23 - languageName: node - linkType: hard - "loglevel-plugin-prefix@npm:^0.8.4": version: 0.8.4 resolution: "loglevel-plugin-prefix@npm:0.8.4" @@ -8949,7 +8237,7 @@ __metadata: languageName: node linkType: hard -"lru-cache@npm:^7.14.1, lru-cache@npm:^7.5.1, lru-cache@npm:^7.7.1": +"lru-cache@npm:^7.14.1, lru-cache@npm:^7.7.1": version: 7.18.3 resolution: "lru-cache@npm:7.18.3" checksum: 10c0/b3a452b491433db885beed95041eb104c157ef7794b9c9b4d647be503be91769d11206bb573849a16b4cc0d03cbd15ffd22df7960997788b74c1d399ac7a4fed @@ -8965,15 +8253,6 @@ __metadata: languageName: node linkType: hard -"magic-string@npm:^0.30.10, magic-string@npm:^0.30.5": - version: 0.30.11 - resolution: "magic-string@npm:0.30.11" - dependencies: - "@jridgewell/sourcemap-codec": "npm:^1.5.0" - checksum: 10c0/b9eb370773d0bd90ca11a848753409d8e5309b1ad56d2a1aa49d6649da710a6d2fe7237ad1a643c5a5d3800de2b9946ed9690acdfc00e6cc1aeafff3ab1752c4 - languageName: node - linkType: hard - "make-dir@npm:4.0.0": version: 4.0.0 resolution: "make-dir@npm:4.0.0" @@ -8993,7 +8272,7 @@ __metadata: languageName: node linkType: hard -"make-dir@npm:^3.0.0, make-dir@npm:^3.1.0": +"make-dir@npm:^3.0.0": version: 3.1.0 resolution: "make-dir@npm:3.1.0" dependencies: @@ -9232,13 +8511,6 @@ __metadata: languageName: node linkType: hard -"mimic-fn@npm:^4.0.0": - version: 4.0.0 - resolution: "mimic-fn@npm:4.0.0" - checksum: 10c0/de9cc32be9996fd941e512248338e43407f63f6d497abe8441fa33447d922e927de54d4cc3c1a3c6d652857acd770389d5a3823f311a744132760ce2be15ccbf - languageName: node - linkType: hard - "mimic-response@npm:^3.1.0": version: 3.1.0 resolution: "mimic-response@npm:3.1.0" @@ -9287,7 +8559,7 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:^3.0.4, minimatch@npm:^3.0.5, minimatch@npm:^3.1.1, minimatch@npm:^3.1.2": +"minimatch@npm:^3.0.4, minimatch@npm:^3.1.1, minimatch@npm:^3.1.2": version: 3.1.2 resolution: "minimatch@npm:3.1.2" dependencies: @@ -9323,15 +8595,6 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:~3.0.2": - version: 3.0.8 - resolution: "minimatch@npm:3.0.8" - dependencies: - brace-expansion: "npm:^1.1.7" - checksum: 10c0/72b226f452dcfb5075255f53534cb83fc25565b909e79b9be4fad463d735cb1084827f7013ff41d050e77ee6e474408c6073473edd2fb72c2fd630cfb0acc6ad - languageName: node - linkType: hard - "minimist-options@npm:4.1.0": version: 4.1.0 resolution: "minimist-options@npm:4.1.0" @@ -9556,7 +8819,7 @@ __metadata: languageName: node linkType: hard -"mute-stream@npm:1.0.0, mute-stream@npm:^1.0.0, mute-stream@npm:~1.0.0": +"mute-stream@npm:^1.0.0, mute-stream@npm:~1.0.0": version: 1.0.0 resolution: "mute-stream@npm:1.0.0" checksum: 10c0/dce2a9ccda171ec979a3b4f869a102b1343dee35e920146776780de182f16eae459644d187e38d59a3d37adf85685e1c17c38cf7bfda7e39a9880f7a1d10a74c @@ -9642,7 +8905,7 @@ __metadata: languageName: node linkType: hard -"node-domexception@npm:1.0.0, node-domexception@npm:^1.0.0": +"node-domexception@npm:^1.0.0": version: 1.0.0 resolution: "node-domexception@npm:1.0.0" checksum: 10c0/5e5d63cda29856402df9472335af4bb13875e1927ad3be861dc5ebde38917aecbf9ae337923777af52a48c426b70148815e890a5d72760f1b4d758cc671b1a2b @@ -9749,18 +9012,6 @@ __metadata: languageName: node linkType: hard -"node-request-interceptor@npm:^0.6.3": - version: 0.6.3 - resolution: "node-request-interceptor@npm:0.6.3" - dependencies: - "@open-draft/until": "npm:^1.0.3" - debug: "npm:^4.3.0" - headers-utils: "npm:^1.2.0" - strict-event-emitter: "npm:^0.1.0" - checksum: 10c0/d210e40d16da68719d8e676957a05ae5c47357a0eead0e52df1977d1fc2a0fbaf8dac70792e5305961bd2126f71bb38c452ded44fcf2472b4d9ece8e437ca449 - languageName: node - linkType: hard - "nopt@npm:^6.0.0": version: 6.0.0 resolution: "nopt@npm:6.0.0" @@ -9807,18 +9058,6 @@ __metadata: languageName: node linkType: hard -"normalize-package-data@npm:^5.0.0": - version: 5.0.0 - resolution: "normalize-package-data@npm:5.0.0" - dependencies: - hosted-git-info: "npm:^6.0.0" - is-core-module: "npm:^2.8.1" - semver: "npm:^7.3.5" - validate-npm-package-license: "npm:^3.0.4" - checksum: 10c0/705fe66279edad2f93f6e504d5dc37984e404361a3df921a76ab61447eb285132d20ff261cc0bee9566b8ce895d75fcfec913417170add267e2873429fe38392 - languageName: node - linkType: hard - "normalize-package-data@npm:^6.0.0, normalize-package-data@npm:^6.0.1": version: 6.0.2 resolution: "normalize-package-data@npm:6.0.2" @@ -9948,15 +9187,6 @@ __metadata: languageName: node linkType: hard -"npm-run-path@npm:^5.1.0": - version: 5.1.0 - resolution: "npm-run-path@npm:5.1.0" - dependencies: - path-key: "npm:^4.0.0" - checksum: 10c0/ff6d77514489f47fa1c3b1311d09cd4b6d09a874cc1866260f9dea12cbaabda0436ed7f8c2ee44d147bf99a3af29307c6f63b0f83d242b0b6b0ab25dff2629e3 - languageName: node - linkType: hard - "npmlog@npm:^6.0.0": version: 6.0.2 resolution: "npmlog@npm:6.0.2" @@ -10067,7 +9297,7 @@ __metadata: languageName: node linkType: hard -"object-inspect@npm:^1.12.0, object-inspect@npm:^1.13.1": +"object-inspect@npm:^1.13.1": version: 1.13.1 resolution: "object-inspect@npm:1.13.1" checksum: 10c0/fad603f408e345c82e946abdf4bfd774260a5ed3e5997a0b057c44153ac32c7271ff19e3a5ae39c858da683ba045ccac2f65245c12763ce4e8594f818f4a648d @@ -10163,15 +9393,6 @@ __metadata: languageName: node linkType: hard -"onetime@npm:^6.0.0": - version: 6.0.0 - resolution: "onetime@npm:6.0.0" - dependencies: - mimic-fn: "npm:^4.0.0" - checksum: 10c0/4eef7c6abfef697dd4479345a4100c382d73c149d2d56170a54a07418c50816937ad09500e1ed1e79d235989d073a9bade8557122aee24f0576ecde0f392bb6c - languageName: node - linkType: hard - "open@npm:^8.4.0": version: 8.4.0 resolution: "open@npm:8.4.0" @@ -10278,15 +9499,6 @@ __metadata: languageName: node linkType: hard -"p-limit@npm:^4.0.0": - version: 4.0.0 - resolution: "p-limit@npm:4.0.0" - dependencies: - yocto-queue: "npm:^1.0.0" - checksum: 10c0/a56af34a77f8df2ff61ddfb29431044557fcbcb7642d5a3233143ebba805fc7306ac1d448de724352861cb99de934bc9ab74f0d16fe6a5460bdbdf938de875ad - languageName: node - linkType: hard - "p-locate@npm:^2.0.0": version: 2.0.0 resolution: "p-locate@npm:2.0.0" @@ -10314,15 +9526,6 @@ __metadata: languageName: node linkType: hard -"p-locate@npm:^6.0.0": - version: 6.0.0 - resolution: "p-locate@npm:6.0.0" - dependencies: - p-limit: "npm:^4.0.0" - checksum: 10c0/d72fa2f41adce59c198270aa4d3c832536c87a1806e0f69dffb7c1a7ca998fb053915ca833d90f166a8c082d3859eabfed95f01698a3214c20df6bb8de046312 - languageName: node - linkType: hard - "p-map-series@npm:2.1.0": version: 2.1.0 resolution: "p-map-series@npm:2.1.0" @@ -10531,26 +9734,6 @@ __metadata: languageName: node linkType: hard -"parse-json@npm:^7.0.0": - version: 7.0.0 - resolution: "parse-json@npm:7.0.0" - dependencies: - "@babel/code-frame": "npm:^7.21.4" - error-ex: "npm:^1.3.2" - json-parse-even-better-errors: "npm:^3.0.0" - lines-and-columns: "npm:^2.0.3" - type-fest: "npm:^3.8.0" - checksum: 10c0/d7326dee546baab677208a3e829674bdbd383494b1103c7f6f60ed14d5b9f516a25243e3f5726dbba16e59e7089213b8c706bef4545a0108a0d282b2f94c8427 - languageName: node - linkType: hard - -"parse-ms@npm:^2.1.0": - version: 2.1.0 - resolution: "parse-ms@npm:2.1.0" - checksum: 10c0/9c5c0a95c6267c84085685556a6e102ee806c3147ec11cbb9b98e35998eb4a48a757bd6ea7bfd930062de65909a33d24985055b4394e70aa0b65ee40cef16911 - languageName: node - linkType: hard - "parse-path@npm:^7.0.0": version: 7.0.0 resolution: "parse-path@npm:7.0.0" @@ -10600,13 +9783,6 @@ __metadata: languageName: node linkType: hard -"path-exists@npm:^5.0.0": - version: 5.0.0 - resolution: "path-exists@npm:5.0.0" - checksum: 10c0/b170f3060b31604cde93eefdb7392b89d832dfbc1bed717c9718cbe0f230c1669b7e75f87e19901da2250b84d092989a0f9e44d2ef41deb09aa3ad28e691a40a - languageName: node - linkType: hard - "path-is-absolute@npm:^1.0.0": version: 1.0.1 resolution: "path-is-absolute@npm:1.0.1" @@ -10628,13 +9804,6 @@ __metadata: languageName: node linkType: hard -"path-key@npm:^4.0.0": - version: 4.0.0 - resolution: "path-key@npm:4.0.0" - checksum: 10c0/794efeef32863a65ac312f3c0b0a99f921f3e827ff63afa5cb09a377e202c262b671f7b3832a4e64731003fa94af0263713962d317b9887bd1e0c48a342efba3 - languageName: node - linkType: hard - "path-parse@npm:^1.0.7": version: 1.0.7 resolution: "path-parse@npm:1.0.7" @@ -10692,13 +9861,6 @@ __metadata: languageName: node linkType: hard -"pathe@npm:^1.1.1, pathe@npm:^1.1.2": - version: 1.1.2 - resolution: "pathe@npm:1.1.2" - checksum: 10c0/64ee0a4e587fb0f208d9777a6c56e4f9050039268faaaaecd50e959ef01bf847b7872785c36483fa5cdcdbdfdb31fef2ff222684d4fc21c330ab60395c681897 - languageName: node - linkType: hard - "pause-stream@npm:0.0.11": version: 0.0.11 resolution: "pause-stream@npm:0.0.11" @@ -10734,7 +9896,7 @@ __metadata: languageName: node linkType: hard -"picomatch@npm:^2.0.4, picomatch@npm:^2.2.1, picomatch@npm:^2.2.3, picomatch@npm:^2.3.1": +"picomatch@npm:^2.0.4, picomatch@npm:^2.2.1, picomatch@npm:^2.3.1": version: 2.3.1 resolution: "picomatch@npm:2.3.1" checksum: 10c0/26c02b8d06f03206fc2ab8d16f19960f2ff9e81a658f831ecb656d8f17d9edc799e8364b1f4a7873e89d9702dff96204be0fa26fe4181f6843f040f819dac4be @@ -10787,6 +9949,30 @@ __metadata: languageName: node linkType: hard +"playwright-core@npm:1.49.0": + version: 1.49.0 + resolution: "playwright-core@npm:1.49.0" + bin: + playwright-core: cli.js + checksum: 10c0/22c1a72fabdcc87bd1cd4d40a032d2c5b94cf94ba7484dc182048c3fa1c8ec26180b559d8cac4ca9870e8fd6bdf5ef9d9f54e7a31fd60d67d098fcffc5e4253b + languageName: node + linkType: hard + +"playwright@npm:1.49.0": + version: 1.49.0 + resolution: "playwright@npm:1.49.0" + dependencies: + fsevents: "npm:2.3.2" + playwright-core: "npm:1.49.0" + dependenciesMeta: + fsevents: + optional: true + bin: + playwright: cli.js + checksum: 10c0/e94d662747cd147d0573570fec90dadc013c1097595714036fc8934a075c5a82ab04a49111b03b1f762ea86429bdb7c94460901896901e20970b30ce817cc93f + languageName: node + linkType: hard + "pluralize@npm:^8.0.0": version: 8.0.0 resolution: "pluralize@npm:8.0.0" @@ -10919,15 +10105,6 @@ __metadata: languageName: node linkType: hard -"pretty-ms@npm:^7.0.0": - version: 7.0.1 - resolution: "pretty-ms@npm:7.0.1" - dependencies: - parse-ms: "npm:^2.1.0" - checksum: 10c0/069aec9d939e7903846b3db53b020bed92e3dc5909e0fef09ec8ab104a0b7f9a846605a1633c60af900d288582fb333f6f30469e59d6487a2330301fad35a89c - languageName: node - linkType: hard - "printj@npm:~1.3.1": version: 1.3.1 resolution: "printj@npm:1.3.1" @@ -11362,17 +10539,6 @@ __metadata: languageName: node linkType: hard -"read-pkg-up@npm:10.0.0": - version: 10.0.0 - resolution: "read-pkg-up@npm:10.0.0" - dependencies: - find-up: "npm:^6.3.0" - read-pkg: "npm:^8.0.0" - type-fest: "npm:^3.12.0" - checksum: 10c0/92579d91f1bbfd8a062d62286ea6e268a89623ad3d3fceb86f75d5b3336a1e3d0d13c4ea2b575b562086e08432d7c514c7f719be3ec402e34f3c9110deebf193 - languageName: node - linkType: hard - "read-pkg-up@npm:^3.0.0": version: 3.0.0 resolution: "read-pkg-up@npm:3.0.0" @@ -11417,18 +10583,6 @@ __metadata: languageName: node linkType: hard -"read-pkg@npm:^8.0.0": - version: 8.0.0 - resolution: "read-pkg@npm:8.0.0" - dependencies: - "@types/normalize-package-data": "npm:^2.4.1" - normalize-package-data: "npm:^5.0.0" - parse-json: "npm:^7.0.0" - type-fest: "npm:^3.8.0" - checksum: 10c0/708e3fff72e6090e1b189af22e4d00ffabde4295171a98ce910a3800dedd8384ad4cab1915283351a4e5fc884dca2e4111ddb85b28a242ffff98a2e1e1e9ca91 - languageName: node - linkType: hard - "read@npm:^2.0.0": version: 2.1.0 resolution: "read@npm:2.1.0" @@ -11495,13 +10649,6 @@ __metadata: languageName: node linkType: hard -"readdirp@npm:^4.0.1": - version: 4.0.2 - resolution: "readdirp@npm:4.0.2" - checksum: 10c0/a16ecd8ef3286dcd90648c3b103e3826db2b766cdb4a988752c43a83f683d01c7059158d623cbcd8bdfb39e65d302d285be2d208e7d9f34d022d912b929217dd - languageName: node - linkType: hard - "readdirp@npm:~3.6.0": version: 3.6.0 resolution: "readdirp@npm:3.6.0" @@ -11520,15 +10667,6 @@ __metadata: languageName: node linkType: hard -"recursive-readdir@npm:^2.2.3": - version: 2.2.3 - resolution: "recursive-readdir@npm:2.2.3" - dependencies: - minimatch: "npm:^3.0.5" - checksum: 10c0/d0238f137b03af9cd645e1e0b40ae78b6cda13846e3ca57f626fcb58a66c79ae018a10e926b13b3a460f1285acc946a4e512ea8daa2e35df4b76a105709930d1 - languageName: node - linkType: hard - "redent@npm:^3.0.0": version: 3.0.0 resolution: "redent@npm:3.0.0" @@ -11782,13 +10920,6 @@ __metadata: languageName: node linkType: hard -"run-async@npm:^3.0.0": - version: 3.0.0 - resolution: "run-async@npm:3.0.0" - checksum: 10c0/b18b562ae37c3020083dcaae29642e4cc360c824fbfb6b7d50d809a9d5227bb986152d09310255842c8dce40526e82ca768f02f00806c91ba92a8dfa6159cb85 - languageName: node - linkType: hard - "run-parallel@npm:^1.1.9": version: 1.1.10 resolution: "run-parallel@npm:1.1.10" @@ -11796,7 +10927,7 @@ __metadata: languageName: node linkType: hard -"rxjs@npm:^7.5.5, rxjs@npm:^7.8.1": +"rxjs@npm:^7.5.5": version: 7.8.1 resolution: "rxjs@npm:7.8.1" dependencies: @@ -11849,13 +10980,6 @@ __metadata: languageName: node linkType: hard -"safe-stable-stringify@npm:^2.3.1": - version: 2.4.3 - resolution: "safe-stable-stringify@npm:2.4.3" - checksum: 10c0/81dede06b8f2ae794efd868b1e281e3c9000e57b39801c6c162267eb9efda17bd7a9eafa7379e1f1cacd528d4ced7c80d7460ad26f62ada7c9e01dec61b2e768 - languageName: node - linkType: hard - "safer-buffer@npm:>= 2.1.2 < 3, safer-buffer@npm:>= 2.1.2 < 3.0.0": version: 2.1.2 resolution: "safer-buffer@npm:2.1.2" @@ -12098,7 +11222,7 @@ __metadata: languageName: node linkType: hard -"signal-exit@npm:^4.0.1, signal-exit@npm:^4.1.0": +"signal-exit@npm:^4.0.1": version: 4.1.0 resolution: "signal-exit@npm:4.1.0" checksum: 10c0/41602dce540e46d599edba9d9860193398d135f7ff72cab629db5171516cfae628d21e7bfccde1bbfdf11c48726bc2a6d1a8fb8701125852fbfda7cf19c6aa83 @@ -12314,7 +11438,7 @@ __metadata: languageName: node linkType: hard -"split2@npm:^4.1.0, split2@npm:^4.2.0": +"split2@npm:^4.2.0": version: 4.2.0 resolution: "split2@npm:4.2.0" checksum: 10c0/b292beb8ce9215f8c642bb68be6249c5a4c7f332fc8ecadae7be5cbdf1ea95addc95f0459ef2e7ad9d45fd1064698a097e4eb211c83e772b49bc0ee423e91534 @@ -12371,15 +11495,6 @@ __metadata: languageName: node linkType: hard -"stack-utils@npm:^2.0.3": - version: 2.0.5 - resolution: "stack-utils@npm:2.0.5" - dependencies: - escape-string-regexp: "npm:^2.0.0" - checksum: 10c0/059f828eed5b03b963e8200529c27bd92b105f2cac9dffc9edcbc739ea8fa108e4ec45d0da257d8e0f7b5ac98db5643a0787e5c25ceab1396f7123e1ee15a086 - languageName: node - linkType: hard - "statuses@npm:2.0.1": version: 2.0.1 resolution: "statuses@npm:2.0.1" @@ -12394,13 +11509,6 @@ __metadata: languageName: node linkType: hard -"stream-buffers@npm:^3.0.2": - version: 3.0.2 - resolution: "stream-buffers@npm:3.0.2" - checksum: 10c0/5f12f5a3af4d2012b4f5386a05667b16710fdfc3d9c9db12ec64c44d9f0f831b10cf8c941afd5398b6f47ddb3db692894a16e0f82a8a22b43a5ecb424064ce40 - languageName: node - linkType: hard - "stream-combiner@npm:~0.0.4": version: 0.0.4 resolution: "stream-combiner@npm:0.0.4" @@ -12442,13 +11550,6 @@ __metadata: languageName: node linkType: hard -"strict-event-emitter@npm:^0.1.0": - version: 0.1.0 - resolution: "strict-event-emitter@npm:0.1.0" - checksum: 10c0/94cf9f0eac5b34f11b8c40bfb7922bdbdb1e582f98ea1468c1be771a69c5f86f12ef997db234ae4bd4adcecf39024b37129f94916095112a92ec7e511803a627 - languageName: node - linkType: hard - "string-width-cjs@npm:string-width@^4.2.0": version: 4.2.0 resolution: "string-width@npm:4.2.0" @@ -12584,13 +11685,6 @@ __metadata: languageName: node linkType: hard -"strip-final-newline@npm:^3.0.0": - version: 3.0.0 - resolution: "strip-final-newline@npm:3.0.0" - checksum: 10c0/a771a17901427bac6293fd416db7577e2bc1c34a19d38351e9d5478c3c415f523f391003b42ed475f27e33a78233035df183525395f731d3bfb8cdcbd4da08ce - languageName: node - linkType: hard - "strip-indent@npm:^3.0.0": version: 3.0.0 resolution: "strip-indent@npm:3.0.0" @@ -12875,13 +11969,6 @@ __metadata: languageName: node linkType: hard -"tinyrainbow@npm:^1.2.0": - version: 1.2.0 - resolution: "tinyrainbow@npm:1.2.0" - checksum: 10c0/7f78a4b997e5ba0f5ecb75e7ed786f30bab9063716e7dff24dd84013fb338802e43d176cb21ed12480561f5649a82184cf31efb296601a29d38145b1cdb4c192 - languageName: node - linkType: hard - "tmp@npm:^0.0.33": version: 0.0.33 resolution: "tmp@npm:0.0.33" @@ -12951,13 +12038,6 @@ __metadata: languageName: node linkType: hard -"triple-beam@npm:^1.3.0": - version: 1.4.1 - resolution: "triple-beam@npm:1.4.1" - checksum: 10c0/4bf1db71e14fe3ff1c3adbe3c302f1fdb553b74d7591a37323a7badb32dc8e9c290738996cbb64f8b10dc5a3833645b5d8c26221aaaaa12e50d1251c9aba2fea - languageName: node - linkType: hard - "ts-api-utils@npm:^2.0.1": version: 2.0.1 resolution: "ts-api-utils@npm:2.0.1" @@ -13139,13 +12219,6 @@ __metadata: languageName: node linkType: hard -"type-fest@npm:^3.12.0, type-fest@npm:^3.8.0": - version: 3.13.1 - resolution: "type-fest@npm:3.13.1" - checksum: 10c0/547d22186f73a8c04590b70dcf63baff390078c75ea8acd366bbd510fd0646e348bd1970e47ecf795b7cff0b41d26e9c475c1fedd6ef5c45c82075fbf916b629 - languageName: node - linkType: hard - "type-fest@npm:^4.27.0": version: 4.29.0 resolution: "type-fest@npm:4.29.0" @@ -13555,15 +12628,6 @@ __metadata: languageName: node linkType: hard -"uuid@npm:9.0.1": - version: 9.0.1 - resolution: "uuid@npm:9.0.1" - bin: - uuid: dist/bin/uuid - checksum: 10c0/1607dd32ac7fc22f2d8f77051e6a64845c9bce5cd3dd8aa0070c074ec73e666a1f63c7b4e0f4bf2bc8b9d59dc85a15e17807446d9d2b17c8485fbc2147b27f9b - languageName: node - linkType: hard - "uuid@npm:^10.0.0": version: 10.0.0 resolution: "uuid@npm:10.0.0" @@ -13650,13 +12714,6 @@ __metadata: languageName: node linkType: hard -"web-streams-polyfill@npm:4.0.0-beta.3": - version: 4.0.0-beta.3 - resolution: "web-streams-polyfill@npm:4.0.0-beta.3" - checksum: 10c0/a9596779db2766990117ed3a158e0b0e9f69b887a6d6ba0779940259e95f99dc3922e534acc3e5a117b5f5905300f527d6fbf8a9f0957faf1d8e585ce3452e8e - languageName: node - linkType: hard - "web-streams-polyfill@npm:^3.0.3": version: 3.2.1 resolution: "web-streams-polyfill@npm:3.2.1" @@ -13683,7 +12740,7 @@ __metadata: languageName: node linkType: hard -"webdriverio@npm:8.41.0, webdriverio@npm:^8.29.3": +"webdriverio@npm:8.41.0": version: 8.41.0 resolution: "webdriverio@npm:8.41.0" dependencies: @@ -13925,17 +12982,6 @@ __metadata: languageName: node linkType: hard -"winston-transport@npm:^4.5.0": - version: 4.5.0 - resolution: "winston-transport@npm:4.5.0" - dependencies: - logform: "npm:^2.3.2" - readable-stream: "npm:^3.6.0" - triple-beam: "npm:^1.3.0" - checksum: 10c0/110a47c5acc87c3aa0f101741c0a992e52a86802272838c18aede8178d2b5e80254d2433dcac3439cefbc2777d9e22e65f84e9cee3130681c58e4ae5d58f50c3 - languageName: node - linkType: hard - "wordwrap@npm:^1.0.0": version: 1.0.0 resolution: "wordwrap@npm:1.0.0" @@ -13954,17 +13000,6 @@ __metadata: languageName: node linkType: hard -"wrap-ansi@npm:^6.2.0": - version: 6.2.0 - resolution: "wrap-ansi@npm:6.2.0" - dependencies: - ansi-styles: "npm:^4.0.0" - string-width: "npm:^4.1.0" - strip-ansi: "npm:^6.0.0" - checksum: 10c0/baad244e6e33335ea24e86e51868fe6823626e3a3c88d9a6674642afff1d34d9a154c917e74af8d845fd25d170c4ea9cf69a47133c3f3656e1252b3d462d9f6c - languageName: node - linkType: hard - "wrap-ansi@npm:^8.1.0": version: 8.1.0 resolution: "wrap-ansi@npm:8.1.0" @@ -14066,13 +13101,6 @@ __metadata: languageName: node linkType: hard -"xmlbuilder@npm:^15.1.1": - version: 15.1.1 - resolution: "xmlbuilder@npm:15.1.1" - checksum: 10c0/665266a8916498ff8d82b3d46d3993913477a254b98149ff7cff060d9b7cc0db7cf5a3dae99aed92355254a808c0e2e3ec74ad1b04aa1061bdb8dfbea26c18b8 - languageName: node - linkType: hard - "xtend@npm:~4.0.1": version: 4.0.2 resolution: "xtend@npm:4.0.2" @@ -14148,16 +13176,6 @@ __metadata: languageName: node linkType: hard -"yauzl@npm:^3.0.0": - version: 3.1.0 - resolution: "yauzl@npm:3.1.0" - dependencies: - buffer-crc32: "npm:~0.2.3" - pend: "npm:~1.2.0" - checksum: 10c0/610c23ba125c4c838ea563a31efa8df1e370967ff4de11d47dbce1056e62429ce95ce8a9d53ccb35fd1690e594ca4dcb3bd7dbf5e0e9311c250efde33a6653b2 - languageName: node - linkType: hard - "yn@npm:3.1.1": version: 3.1.1 resolution: "yn@npm:3.1.1" @@ -14172,13 +13190,6 @@ __metadata: languageName: node linkType: hard -"yocto-queue@npm:^1.0.0": - version: 1.0.0 - resolution: "yocto-queue@npm:1.0.0" - checksum: 10c0/856117aa15cf5103d2a2fb173f0ab4acb12b4b4d0ed3ab249fdbbf612e55d1cadfd27a6110940e24746fb0a78cf640b522cc8bca76f30a3b00b66e90cf82abe0 - languageName: node - linkType: hard - "zip-stream@npm:^6.0.1": version: 6.0.1 resolution: "zip-stream@npm:6.0.1"