From 0370c99b48b0a11f407d202177f502f128af0f1b Mon Sep 17 00:00:00 2001 From: uid11 Date: Tue, 27 Aug 2024 17:04:44 +0300 Subject: [PATCH] FI-1345 refactor: Playwright config (`use` object) refactor: reexport `devices` from `@playwright/test` fix: add Playwright's browser version check fix: add using Playwright version from `package.json` feat: support UI mode flag feat: add internal `isUiMode` flag fix: remove text style from run errors in HTML report feat: add `enableCsp` flag to pack config and to test options fix: ingore timeouts in UI mode fix: mix up of JS errors and browser console logs from other test runs chore: update `@types/node` to 22.5.1 --- Dockerfile | 4 +- README.md | 6 +- autotests/packs/allTests.ts | 3 +- .../tests/e2edReportExample/browserData.ts | 18 +++--- .../tests/e2edReportExample/fullMocks.ts | 2 +- bin/addPackageJsonToBuildDocker.sh | 4 +- bin/buildDocker.sh | 8 ++- bin/checkPlaywrightBrowserChromiumVersion.sh | 6 ++ bin/{getVersion.sh => getE2edVersion.sh} | 0 bin/getPlaywrightVersion.sh | 4 ++ package-lock.json | 8 +-- package.json | 5 +- src/bin/runE2edInLocalEnvironment.ts | 9 +++ src/config.ts | 63 +++++++------------ src/constants/environment.ts | 8 ++- src/constants/internal.ts | 1 + src/context/consoleMessages.ts | 21 ++++++- src/context/jsError.ts | 20 +++++- src/context/onResponseCallbacks.ts | 20 +++++- src/index.ts | 1 + src/playwright.ts | 1 + src/test.ts | 19 +++++- src/types/config/ownE2edConfig.ts | 16 +++-- src/types/environment.ts | 2 + src/types/startInfo.ts | 2 + src/types/testRun.ts | 1 + src/utils/fullMocks/enableFullMocks.ts | 1 + src/utils/generalLog/successfulTestRuns.ts | 5 ++ src/utils/index.ts | 1 + .../promise/getPromiseWithResolveAndReject.ts | 3 +- .../client/render/renderTestRunError.ts | 9 ++- src/utils/resourceUsage.ts | 5 ++ src/utils/startInfo/getStartInfo.ts | 4 ++ src/utils/test/getShouldRunTest.ts | 5 ++ src/utils/test/getTestStaticOptions.ts | 6 +- src/utils/test/preparePage.ts | 2 +- src/utils/tests/runTests.ts | 5 +- src/utils/uiMode.ts | 21 +++++++ 38 files changed, 234 insertions(+), 85 deletions(-) create mode 100755 bin/checkPlaywrightBrowserChromiumVersion.sh rename bin/{getVersion.sh => getE2edVersion.sh} (100%) create mode 100755 bin/getPlaywrightVersion.sh create mode 100644 src/playwright.ts create mode 100644 src/utils/uiMode.ts diff --git a/Dockerfile b/Dockerfile index 940f766f..7bb1ba67 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,6 @@ -FROM mcr.microsoft.com/playwright:v1.46.1-noble +ARG PLAYWRIGHT_VERSION + +FROM mcr.microsoft.com/playwright:v${PLAYWRIGHT_VERSION}-noble COPY ./build/node_modules/e2ed /node_modules/e2ed diff --git a/README.md b/README.md index dd8598f5..2261834c 100644 --- a/README.md +++ b/README.md @@ -256,9 +256,9 @@ The functions accept a start info object, and can return new full pack config, which in this case will be included in the start info object, and will be used for running pack. Each function can thus access the results of the previous function. -`enableHeadlessMode: boolean`: enables headless mode (if browser supports such mode). +`enableCsp: boolean`: enables Content-Security-Policy checks in browser. -`enableLiveMode: boolean`: enables live mode for test development (only for locally running). +`enableHeadlessMode: boolean`: enables headless mode (if browser supports such mode). `enableMobileDeviceMode: boolean`: enables Chromium [mobile device mode](https://developer.chrome.com/docs/devtools/device-mode). @@ -271,6 +271,8 @@ only those tests for which the function returned `true` get into the pack. `fullMocks: FullMocks | null`: functions that specify the "full mocks" functionality. +`getTestNamePrefixInUiMode: (testOptions: TestOptions) => string`: get prefix for test name in UI mode by test options. + `liteReportFileName: string | null`: the name of the file under which, after running the tests, the lite JSON report will be saved in the `autotests/reports` directory, for example, `lite-report.json`. If `null`, the lite report will not be saved. diff --git a/autotests/packs/allTests.ts b/autotests/packs/allTests.ts index 2faf1001..c30c8f61 100644 --- a/autotests/packs/allTests.ts +++ b/autotests/packs/allTests.ts @@ -49,12 +49,13 @@ export const pack: Pack = { deviceScaleFactor: 1, doAfterPack, doBeforePack, + enableCsp: true, enableHeadlessMode: true, - enableLiveMode: false, enableMobileDeviceMode: false, enableTouchEventEmulation: false, filterTestsIntoPack, fullMocks, + getTestNamePrefixInUiMode: (testOptions) => testOptions.meta.testId, liteReportFileName: 'lite-report.json', logFileName: 'pack-logs.log', mapBackendResponseErrorToLog, diff --git a/autotests/tests/e2edReportExample/browserData.ts b/autotests/tests/e2edReportExample/browserData.ts index 93e1c9df..0c2720ee 100644 --- a/autotests/tests/e2edReportExample/browserData.ts +++ b/autotests/tests/e2edReportExample/browserData.ts @@ -13,8 +13,6 @@ import { test('correctly read data from browser', {meta: {testId: '14'}}, async () => { await navigateToPage(E2edReportExample); - await waitForInterfaceStabilization(100); - await createClientFunction(() => { console.error('error'); console.info('info'); @@ -23,10 +21,7 @@ test('correctly read data from browser', {meta: {testId: '14'}}, async () => { setTimeout(() => { throw new Error('foo'); - }, 8); - setTimeout(() => { - throw new Error('bar'); - }, 32); + }, 100); })(); const consoleMessages = getBrowserConsoleMessages(); @@ -47,5 +42,14 @@ test('correctly read data from browser', {meta: {testId: '14'}}, async () => { const jsErrors = getBrowserJsErrors(); - await expect(jsErrors.length === 0, 'getBrowserJsErrors read JS errors').eql(true); + await expect( + jsErrors.length, + 'getBrowserJsErrors read zero JS errors when there are no errors', + ).eql(0); + + await waitForInterfaceStabilization(100); + + await expect(jsErrors.length, 'getBrowserJsErrors read all JS errors').eql(1); + + await expect(String(jsErrors[0]), 'getBrowserJsErrors read all JS errors').contains('foo'); }); diff --git a/autotests/tests/e2edReportExample/fullMocks.ts b/autotests/tests/e2edReportExample/fullMocks.ts index c966d951..f2881a48 100644 --- a/autotests/tests/e2edReportExample/fullMocks.ts +++ b/autotests/tests/e2edReportExample/fullMocks.ts @@ -8,7 +8,7 @@ import {mockApiRoute, navigateToPage, unmockApiRoute} from 'e2ed/actions'; import type {DeviceId, Product, ProductId} from 'autotests/types'; import type {Url} from 'e2ed/types'; -test('full mocks works correctly', {meta: {testId: '18'}}, async () => { +test('full mocks works correctly', {enableCsp: false, meta: {testId: '18'}}, async () => { await navigateToPage(E2edReportExample); await mockApiRoute(CreateProductRoute, (routeParams, {method, query, requestBody, url}) => { diff --git a/bin/addPackageJsonToBuildDocker.sh b/bin/addPackageJsonToBuildDocker.sh index d01fe95e..386abd8b 100755 --- a/bin/addPackageJsonToBuildDocker.sh +++ b/bin/addPackageJsonToBuildDocker.sh @@ -1,6 +1,6 @@ #!/usr/bin/env sh set -eu -VERSION=`./bin/getVersion.sh` +E2ED_VERSION=`./bin/getE2edVersion.sh` -echo "{\n \"dependencies\": {\n \"e2ed\": \"$VERSION\"\n }\n}" > ./build/docker/package.json +echo "{\n \"dependencies\": {\n \"e2ed\": \"$E2ED_VERSION\"\n }\n}" > ./build/docker/package.json diff --git a/bin/buildDocker.sh b/bin/buildDocker.sh index b4147018..28d62724 100755 --- a/bin/buildDocker.sh +++ b/bin/buildDocker.sh @@ -1,6 +1,10 @@ #!/usr/bin/env sh set -eu -VERSION=`./bin/getVersion.sh` +E2ED_VERSION=`./bin/getE2edVersion.sh` +PLAYWRIGHT_VERSION=`./bin/getPlaywrightVersion.sh` -docker build --tag e2edhub/e2ed:$VERSION --tag e2edhub/e2ed:latest . +docker build \ + --build-arg PLAYWRIGHT_VERSION=$PLAYWRIGHT_VERSION \ + --tag e2edhub/e2ed:$E2ED_VERSION \ + --tag e2edhub/e2ed:latest . diff --git a/bin/checkPlaywrightBrowserChromiumVersion.sh b/bin/checkPlaywrightBrowserChromiumVersion.sh new file mode 100755 index 00000000..95fc4025 --- /dev/null +++ b/bin/checkPlaywrightBrowserChromiumVersion.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env sh +set -eu + +PLAYWRIGHT_VERSION=`./bin/getPlaywrightVersion.sh` + +grep "\"@playwright/browser-chromium\": \"$PLAYWRIGHT_VERSION\"" ./package.json diff --git a/bin/getVersion.sh b/bin/getE2edVersion.sh similarity index 100% rename from bin/getVersion.sh rename to bin/getE2edVersion.sh diff --git a/bin/getPlaywrightVersion.sh b/bin/getPlaywrightVersion.sh new file mode 100755 index 00000000..a0cecee4 --- /dev/null +++ b/bin/getPlaywrightVersion.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env sh +set -eu + +grep -m1 @playwright/test ./package.json | cut -d '"' -f 4 diff --git a/package-lock.json b/package-lock.json index d5517cd4..f1aaa1bd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,7 @@ }, "devDependencies": { "@playwright/browser-chromium": "1.46.1", - "@types/node": "22.5.0", + "@types/node": "22.5.1", "@typescript-eslint/eslint-plugin": "7.18.0", "@typescript-eslint/parser": "7.18.0", "assert-modules-support-case-insensitive-fs": "1.0.1", @@ -215,9 +215,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "22.5.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.0.tgz", - "integrity": "sha512-DkFrJOe+rfdHTqqMg0bSNlGlQ85hSoh2TPzZyhHsXnMtligRWpxUySiyw8FY14ITt24HVCiQPWxS3KO/QlGmWg==", + "version": "22.5.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.1.tgz", + "integrity": "sha512-KkHsxej0j9IW1KKOOAA/XBA0z08UFSrRQHErzEfA3Vgq57eXIMYboIlHJuYIfd+lwCQjtKqUu3UnmKbtUc9yRw==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 73873c88..18bf8a88 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ }, "devDependencies": { "@playwright/browser-chromium": "1.46.1", - "@types/node": "22.5.0", + "@types/node": "22.5.1", "@typescript-eslint/eslint-plugin": "7.18.0", "@typescript-eslint/parser": "7.18.0", "assert-modules-support-case-insensitive-fs": "1.0.1", @@ -67,8 +67,9 @@ "scripts": { "asserts": "assert-modules-support-case-insensitive-fs ./autotests ./src && assert-package-lock-is-consistent", "precheck:all": "npm run asserts && npm run clear:lint:cache && npm run build", - "check:all": "npm audit && npm run parallel check:branch lint test", + "check:all": "npm audit && npm run parallel check:branch check:playwright-version lint test", "check:branch": "node ./build/checkBranch.js", + "check:playwright-version": "./bin/checkPlaywrightBrowserChromiumVersion.sh", "clear:lint:cache": "rm -f ./build/tsconfig.tsbuildinfo ./node_modules/.cache/lint-*", "lint": "npm run parallel lint:es lint:prettier lint:types", "lint:es": "eslint --cache --cache-location=./node_modules/.cache/lint-es --cache-strategy=content --ext=.ts --max-warnings=0 --report-unused-disable-directives .", diff --git a/src/bin/runE2edInLocalEnvironment.ts b/src/bin/runE2edInLocalEnvironment.ts index 70bd72b0..fd31bf7b 100644 --- a/src/bin/runE2edInLocalEnvironment.ts +++ b/src/bin/runE2edInLocalEnvironment.ts @@ -5,9 +5,18 @@ import {setPathToPack} from '../utils/environment'; import {registerEndE2edRunEvent, registerStartE2edRunEvent} from '../utils/events'; import {logStartE2edError} from '../utils/generalLog'; import {runPackWithArgs} from '../utils/pack'; +import {setUiMode} from '../utils/uiMode'; import type {FilePathFromRoot} from '../types/internal'; +const uiFlagIndex = process.argv.indexOf('--ui'); + +if (uiFlagIndex !== -1) { + setUiMode(); + + process.argv.splice(uiFlagIndex, 1); +} + const [pathToPack] = process.argv.splice(2, 1); assertValueIsDefined(pathToPack, 'pathToPack is defined', {argv: process.argv}); diff --git a/src/config.ts b/src/config.ts index c0f7a386..850754cc 100644 --- a/src/config.ts +++ b/src/config.ts @@ -21,11 +21,12 @@ import {assertUserlandPack} from './utils/config/assertUserlandPack'; import {getPathToPack} from './utils/environment'; import {setCustomInspectOnFunction} from './utils/fn'; import {setReadonlyProperty} from './utils/setReadonlyProperty'; +import {isUiMode} from './utils/uiMode'; import {isLocalRun} from './configurator'; import type {FullPackConfig, Mutable, UserlandPack} from './types/internal'; -import {defineConfig} from '@playwright/test'; +import {defineConfig, type PlaywrightTestConfig} from '@playwright/test'; const maxTimeoutInMs = 3600_000; @@ -85,12 +86,29 @@ setCustomInspectOnFunction(mapLogPayloadInConsole); setCustomInspectOnFunction(mapLogPayloadInLogFile); setCustomInspectOnFunction(mapLogPayloadInReport); -if (isDebug) { +if (isDebug || isUiMode) { setReadonlyProperty(userlandPack, 'packTimeout', maxTimeoutInMs); setReadonlyProperty(userlandPack, 'testIdleTimeout', maxTimeoutInMs); setReadonlyProperty(userlandPack, 'testTimeout', maxTimeoutInMs); } +const useOptions: PlaywrightTestConfig['use'] = { + actionTimeout: userlandPack.testIdleTimeout, + browserName: userlandPack.browserName, + // eslint-disable-next-line @typescript-eslint/naming-convention + bypassCSP: !userlandPack.enableCsp, + deviceScaleFactor: userlandPack.deviceScaleFactor, + hasTouch: userlandPack.enableTouchEventEmulation, + headless: isLocalRun ? userlandPack.enableHeadlessMode : true, + isMobile: userlandPack.enableMobileDeviceMode, + launchOptions: {args: [...userlandPack.browserFlags]}, + navigationTimeout: userlandPack.pageRequestTimeout, + trace: 'retain-on-failure', + userAgent: userlandPack.userAgent, + viewport: {height: userlandPack.viewportHeight, width: userlandPack.viewportWidth}, + ...userlandPack.overriddenConfigFields?.use, +}; + const playwrightConfig = defineConfig({ expect: {timeout: userlandPack.assertionTimeout}, @@ -100,19 +118,7 @@ const playwrightConfig = defineConfig({ outputDir: join(relativePathFromInstalledE2edToRoot, INTERNAL_REPORTS_DIRECTORY_PATH), - projects: [ - { - name: userlandPack.browserName, - use: { - browserName: userlandPack.browserName, - deviceScaleFactor: userlandPack.deviceScaleFactor, - hasTouch: userlandPack.enableTouchEventEmulation, - isMobile: userlandPack.enableMobileDeviceMode, - userAgent: userlandPack.userAgent, - viewport: {height: userlandPack.viewportHeight, width: userlandPack.viewportWidth}, - }, - }, - ], + projects: [{name: userlandPack.browserName, use: useOptions}], retries: isLocalRun ? 0 : userlandPack.maxRetriesCountInDocker - 1, @@ -126,32 +132,7 @@ const playwrightConfig = defineConfig({ ...userlandPack.overriddenConfigFields, - use: { - actionTimeout: userlandPack.testIdleTimeout, - - browserName: userlandPack.browserName, - - // eslint-disable-next-line @typescript-eslint/naming-convention - bypassCSP: true, - - deviceScaleFactor: userlandPack.deviceScaleFactor, - - hasTouch: userlandPack.enableTouchEventEmulation, - - headless: isLocalRun ? userlandPack.enableHeadlessMode : true, - - isMobile: userlandPack.enableMobileDeviceMode, - - navigationTimeout: userlandPack.pageRequestTimeout, - - trace: 'retain-on-failure', - - userAgent: userlandPack.userAgent, - - viewport: {height: userlandPack.viewportHeight, width: userlandPack.viewportWidth}, - - ...userlandPack.overriddenConfigFields?.use, - }, + use: useOptions, }); const config: FullPackConfig = Object.assign(playwrightConfig, userlandPack); diff --git a/src/constants/environment.ts b/src/constants/environment.ts index 69bff90d..8f664147 100644 --- a/src/constants/environment.ts +++ b/src/constants/environment.ts @@ -7,7 +7,7 @@ import type {E2edEnvironment} from '../types/internal'; export const e2edEnvironment = process.env as E2edEnvironment; /** - * `true` if e2ed run in debug mode, `false` otherwise. + * `true` if e2ed run in debug mode, and `false` otherwise. */ export const isDebug = Boolean(e2edEnvironment.E2ED_DEBUG); @@ -48,3 +48,9 @@ export const RUN_LABEL_VARIABLE_NAME = '__INTERNAL_E2ED_RUN_LABEL'; * @internal */ export const START_TIME_IN_MS_VARIABLE_NAME = '__INTERNAL_E2ED_START_TIME_IN_MS'; + +/** + * Name of e2ed environment variable for UI-mode flag. + * @internal + */ +export const UI_MODE_VARIABLE_NAME = '__INTERNAL_E2ED_UI_MODE'; diff --git a/src/constants/internal.ts b/src/constants/internal.ts index 7398051c..70681739 100644 --- a/src/constants/internal.ts +++ b/src/constants/internal.ts @@ -12,6 +12,7 @@ export { RUN_ENVIRONMENT_VARIABLE_NAME, RUN_LABEL_VARIABLE_NAME, START_TIME_IN_MS_VARIABLE_NAME, + UI_MODE_VARIABLE_NAME, } from './environment'; export {READ_FILE_OPTIONS} from './fs'; /** @internal */ diff --git a/src/context/consoleMessages.ts b/src/context/consoleMessages.ts index 2246e46f..b1a1a035 100644 --- a/src/context/consoleMessages.ts +++ b/src/context/consoleMessages.ts @@ -2,8 +2,27 @@ import {useContext} from '../useContext'; import type {ConsoleMessage} from '../types/internal'; +/** + * Raw get and set browser console messages array. + * @internal + */ +const [getRawConsoleMessagesFromContext, setRawConsoleMessagesFromContext] = + useContext(); + /** * Get browser console messages array. * @internal */ -export const [getConsoleMessagesFromContext] = useContext([]); +export const getConsoleMessagesFromContext = (): readonly ConsoleMessage[] => { + const maybeConsoleMessages = getRawConsoleMessagesFromContext(); + + if (maybeConsoleMessages !== undefined) { + return maybeConsoleMessages; + } + + const consoleMessages: readonly ConsoleMessage[] = []; + + setRawConsoleMessagesFromContext(consoleMessages); + + return consoleMessages; +}; diff --git a/src/context/jsError.ts b/src/context/jsError.ts index 3ce3a967..d35f031c 100644 --- a/src/context/jsError.ts +++ b/src/context/jsError.ts @@ -1,7 +1,25 @@ import {useContext} from '../useContext'; +/** + * Raw get and set browser JS errors array. + * @internal + */ +const [getRawJsErrorsFromContext, setRawJsErrorsFromContext] = useContext(); + /** * Get browser JS errors array. * @internal */ -export const [getJsErrorsFromContext] = useContext([]); +export const getJsErrorsFromContext = (): readonly Error[] => { + const maybeJsErrors = getRawJsErrorsFromContext(); + + if (maybeJsErrors !== undefined) { + return maybeJsErrors; + } + + const jsErrors: readonly Error[] = []; + + setRawJsErrorsFromContext(jsErrors); + + return jsErrors; +}; diff --git a/src/context/onResponseCallbacks.ts b/src/context/onResponseCallbacks.ts index 80d82a5b..3dad5196 100644 --- a/src/context/onResponseCallbacks.ts +++ b/src/context/onResponseCallbacks.ts @@ -4,8 +4,26 @@ import type {ResponseWithRequest} from '../types/internal'; type Callback = (this: void, response: ResponseWithRequest) => void; +/** + * Raw get and set `onResponseCallbacks` list. + * @internal + */ +const [getRawOnResponseCallbacks, setRawOnResponseCallbacks] = useContext(); + /** * Get `onResponseCallbacks` list. * @internal */ -export const [getOnResponseCallbacks] = useContext([]); +export const getOnResponseCallbacks = (): Callback[] => { + const maybeCallbacks = getRawOnResponseCallbacks(); + + if (maybeCallbacks !== undefined) { + return maybeCallbacks; + } + + const callbacks: Callback[] = []; + + setRawOnResponseCallbacks(callbacks); + + return callbacks; +}; diff --git a/src/index.ts b/src/index.ts index 80537c0c..92372e07 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,6 +4,7 @@ export {ApiRoute} from './ApiRoute'; export {Page} from './Page'; export {PageRoute} from './PageRoute'; +export {devices} from './playwright'; export {Route} from './Route'; export {getPlaywrightPage, useContext} from './useContext'; diff --git a/src/playwright.ts b/src/playwright.ts new file mode 100644 index 00000000..3e452ae3 --- /dev/null +++ b/src/playwright.ts @@ -0,0 +1 @@ +export {devices} from '@playwright/test'; diff --git a/src/test.ts b/src/test.ts index efdf069e..efa6045a 100644 --- a/src/test.ts +++ b/src/test.ts @@ -1,4 +1,6 @@ +import {getFullPackConfig} from './utils/config'; import {getRunTest} from './utils/test'; +import {isUiMode} from './utils/uiMode'; import type {TestFunction} from './types/internal'; @@ -11,5 +13,20 @@ import {test as playwrightTest} from '@playwright/test'; export const test: TestFunction = (name, options, testFn) => { const runTest = getRunTest({name, options, testFn}); - playwrightTest(name, runTest); + let playwrightTestName = name; + + if (isUiMode) { + const {getTestNamePrefixInUiMode} = getFullPackConfig(); + + const prefix = getTestNamePrefixInUiMode(options); + + playwrightTestName = `${prefix} ${name}`; + } + + if (options.enableCsp !== undefined) { + // eslint-disable-next-line @typescript-eslint/naming-convention + playwrightTest.use({bypassCSP: !options.enableCsp}); + } + + playwrightTest(playwrightTestName, runTest); }; diff --git a/src/types/config/ownE2edConfig.ts b/src/types/config/ownE2edConfig.ts index 02f7e0ed..ab9e7dfc 100644 --- a/src/types/config/ownE2edConfig.ts +++ b/src/types/config/ownE2edConfig.ts @@ -4,7 +4,7 @@ import type {FullMocksConfig} from '../fullMocks'; import type {MapBackendResponseToLog, MapLogPayload, MapLogPayloadInReport} from '../log'; import type {MaybePromise} from '../promise'; import type {LiteReport} from '../report'; -import type {TestStaticOptions} from '../testRun'; +import type {TestOptions, TestStaticOptions} from '../testRun'; import type {Void} from '../undefined'; import type { CustomPackPropertiesPlaceholder, @@ -56,15 +56,14 @@ export type OwnE2edConfig< ) => MaybePromise)[]; /** - * Enables headless mode (if browser supports such mode). + * Enables Content-Security-Policy checks in browser. */ - enableHeadlessMode: boolean; + enableCsp: boolean; /** - * Enables TestCafe live mode for test development (only for locally running). - * {@link https://testcafe.io/documentation/403842/guides/intermediate-guides/live-mode} + * Enables headless mode (if browser supports such mode). */ - enableLiveMode: boolean; + enableHeadlessMode: boolean; /** * Enables Chromium mobile device mode. @@ -89,6 +88,11 @@ export type OwnE2edConfig< */ fullMocks: FullMocksConfig | null; + /** + * Get prefix for test name in UI mode by test options. + */ + getTestNamePrefixInUiMode: (this: void, testOptions: TestOptions) => string; + /** * The name of the file under which, after running the tests, * the lite JSON report will be saved in the `autotests/reports` directory, diff --git a/src/types/environment.ts b/src/types/environment.ts index e7e30b3b..a5582b72 100644 --- a/src/types/environment.ts +++ b/src/types/environment.ts @@ -5,6 +5,7 @@ import type { RUN_ENVIRONMENT_VARIABLE_NAME, RUN_LABEL_VARIABLE_NAME, START_TIME_IN_MS_VARIABLE_NAME, + UI_MODE_VARIABLE_NAME, } from '../constants/internal'; import type {RunLabel} from './runLabel'; @@ -26,4 +27,5 @@ export type E2edEnvironment = { [RUN_ENVIRONMENT_VARIABLE_NAME]?: RunEnvironment; [RUN_LABEL_VARIABLE_NAME]?: RunLabel; [START_TIME_IN_MS_VARIABLE_NAME]?: string; + [UI_MODE_VARIABLE_NAME]?: 'true'; }; diff --git a/src/types/startInfo.ts b/src/types/startInfo.ts index 569beb5c..32d3929f 100644 --- a/src/types/startInfo.ts +++ b/src/types/startInfo.ts @@ -24,6 +24,8 @@ export type StartInfo = Readonly<{ e2edEnvironmentVariables: Readonly>; fullPackConfig: FullPackConfigArg; installedE2edDirectoryPath: DirectoryPathFromRoot; + isDebug: boolean; + isUiMode: boolean; nodeVersion: string; pathToPack: FilePathFromRoot; 'process.argv': readonly string[]; diff --git a/src/types/testRun.ts b/src/types/testRun.ts index e8f46ace..d60c51e4 100644 --- a/src/types/testRun.ts +++ b/src/types/testRun.ts @@ -41,6 +41,7 @@ export type TestFn = (testController: PlaywrightTestArgs) => Promise; * Test options with userland metadata. */ export type TestOptions = DeepReadonly<{ + enableCsp?: boolean; meta: TestMeta; takeFullPageScreenshotOnError?: boolean; takeViewportScreenshotOnError?: boolean; diff --git a/src/utils/fullMocks/enableFullMocks.ts b/src/utils/fullMocks/enableFullMocks.ts index dfc85067..77567cec 100644 --- a/src/utils/fullMocks/enableFullMocks.ts +++ b/src/utils/fullMocks/enableFullMocks.ts @@ -52,6 +52,7 @@ export const enableFullMocks = async ( requestKinds: Object.fromEntries( Object.entries(testFullMocks).map(([key, value]) => [key, value.length]), ), + testId: fullMocksState.testId, }, LogEventType.InternalUtil, ); diff --git a/src/utils/generalLog/successfulTestRuns.ts b/src/utils/generalLog/successfulTestRuns.ts index 1f2cb81b..e714df4c 100644 --- a/src/utils/generalLog/successfulTestRuns.ts +++ b/src/utils/generalLog/successfulTestRuns.ts @@ -4,6 +4,7 @@ import {join} from 'node:path'; import {READ_FILE_OPTIONS, TMP_DIRECTORY_PATH} from '../../constants/internal'; import {assertValueIsFalse} from '../asserts'; +import {isUiMode} from '../uiMode'; import type {FilePathFromRoot, TestFilePath} from '../../types/internal'; @@ -36,6 +37,10 @@ export const getSuccessfulTestFilePaths = async (): Promise => { const successfulTestFilePaths = await getSuccessfulTestFilePaths(); + if (isUiMode && successfulTestFilePaths.includes(testFilePath)) { + return; + } + assertValueIsFalse( successfulTestFilePaths.includes(testFilePath), 'There is no duplicate test file path in successful test runs', diff --git a/src/utils/index.ts b/src/utils/index.ts index 5ffebd81..2ed452e1 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -57,5 +57,6 @@ export {setReadonlyProperty} from './setReadonlyProperty'; export {getPackageInfo} from './startInfo'; export {wrapInTestRunTracker} from './testRun'; export {isArray, isThenable} from './typeGuards'; +export {isUiMode} from './uiMode'; export {valueToString} from './valueToString'; export {isSelectorEntirelyInViewport, isSelectorInViewport} from './viewport'; diff --git a/src/utils/promise/getPromiseWithResolveAndReject.ts b/src/utils/promise/getPromiseWithResolveAndReject.ts index 84ea3170..b27245e6 100644 --- a/src/utils/promise/getPromiseWithResolveAndReject.ts +++ b/src/utils/promise/getPromiseWithResolveAndReject.ts @@ -5,6 +5,7 @@ import {E2edError} from '../error'; import {setCustomInspectOnFunction} from '../fn'; import {generalLog} from '../generalLog'; import {getDurationWithUnits} from '../getDurationWithUnits'; +import {isUiMode} from '../uiMode'; import type {AsyncVoid} from '../../types/internal'; @@ -60,7 +61,7 @@ export const getPromiseWithResolveAndReject = < generalLog('Reject timeout function rejected with error', {error, rejectTimeoutFunction}); } }) as () => void, - isDebug ? maxTimeoutInMs : timeoutInMs, + isDebug || isUiMode ? maxTimeoutInMs : timeoutInMs, ); const clearRejectTimeout = (): void => { diff --git a/src/utils/report/client/render/renderTestRunError.ts b/src/utils/report/client/render/renderTestRunError.ts index 8e1bca77..3278cfbd 100644 --- a/src/utils/report/client/render/renderTestRunError.ts +++ b/src/utils/report/client/render/renderTestRunError.ts @@ -5,7 +5,7 @@ import type {RunError, SafeHtml} from '../../../../types/internal'; const sanitizeHtml = clientSanitizeHtml; /** - * Renders TestRun error as simple message. + * Renders `TestRun` error as simple message. * This base client function should not use scope variables (except other base functions). * @internal */ @@ -14,10 +14,15 @@ export function renderTestRunError(runError: RunError): SafeHtml { return sanitizeHtml``; } + // eslint-disable-next-line no-control-regex + const stylesRegexp = /\x1B\[[\d;]+m/gi; + + const runErrorWithoutStyle = String(runError).replace(stylesRegexp, ''); + return sanitizeHtml`
- ${runError} + ${runErrorWithoutStyle}
`; diff --git a/src/utils/resourceUsage.ts b/src/utils/resourceUsage.ts index 9e75472b..505ec509 100644 --- a/src/utils/resourceUsage.ts +++ b/src/utils/resourceUsage.ts @@ -2,6 +2,7 @@ import {availableParallelism, cpus, freemem} from 'node:os'; import {assertValueIsDefined, assertValueIsTrue, assertValueIsUndefined} from './asserts'; import {generalLog} from './generalLog'; +import {isUiMode} from './uiMode'; const Mb = 1024 * 1024; const availableCpuCount = availableParallelism(); @@ -68,6 +69,10 @@ export const startResourceUsageReading = (resourceUsageReadingInternal: number): resourceUsageReadingInternal, }); + if (isUiMode) { + return; + } + timeoutInterval = setInterval(logResourceUsage, resourceUsageReadingInternal); timeoutInterval.unref(); diff --git a/src/utils/startInfo/getStartInfo.ts b/src/utils/startInfo/getStartInfo.ts index 5703f930..6da21884 100644 --- a/src/utils/startInfo/getStartInfo.ts +++ b/src/utils/startInfo/getStartInfo.ts @@ -6,10 +6,12 @@ import { ABSOLUTE_PATH_TO_PROJECT_ROOT_DIRECTORY, e2edEnvironment, INSTALLED_E2ED_DIRECTORY_PATH, + isDebug, } from '../../constants/internal'; import {getFullPackConfig} from '../config'; import {getPathToPack} from '../environment'; +import {isUiMode} from '../uiMode'; import {getPackageInfo} from './getPackageInfo'; @@ -46,6 +48,8 @@ export const getStartInfo = ({configCompileTimeWithUnits}: Options): StartInfo = e2edEnvironmentVariables, fullPackConfig: getFullPackConfig(), installedE2edDirectoryPath: INSTALLED_E2ED_DIRECTORY_PATH, + isDebug, + isUiMode, nodeVersion: process.version, pathToPack: getPathToPack(), 'process.argv': [...process.argv], diff --git a/src/utils/test/getShouldRunTest.ts b/src/utils/test/getShouldRunTest.ts index 8565f8f6..6ae77e31 100644 --- a/src/utils/test/getShouldRunTest.ts +++ b/src/utils/test/getShouldRunTest.ts @@ -1,5 +1,6 @@ import {getSuccessfulTestFilePaths} from '../generalLog'; import {addTestToNotIncludedInPackTests} from '../notIncludedInPackTests'; +import {isUiMode} from '../uiMode'; import {getIsTestIncludedInPack} from './getIsTestIncludedInPack'; @@ -18,6 +19,10 @@ export const getShouldRunTest = async (testStaticOptions: TestStaticOptions): Pr return false; } + if (isUiMode) { + return true; + } + const successfulTestFilePaths = await getSuccessfulTestFilePaths(); return !successfulTestFilePaths.includes(testStaticOptions.filePath); diff --git a/src/utils/test/getTestStaticOptions.ts b/src/utils/test/getTestStaticOptions.ts index c15a7adb..d016f4ce 100644 --- a/src/utils/test/getTestStaticOptions.ts +++ b/src/utils/test/getTestStaticOptions.ts @@ -13,9 +13,5 @@ export const getTestStaticOptions = (test: Test, testInfo: TestInfo): TestStatic const absoluteFilePath = String((testInfo as {_requireFile?: string})._requireFile); const filePath = getRelativeTestFilePath(absoluteFilePath); - return { - filePath, - name: test.name, - options: test.options, - }; + return {filePath, name: test.name, options: test.options}; }; diff --git a/src/utils/test/preparePage.ts b/src/utils/test/preparePage.ts index dd426dd4..cffbdc6c 100644 --- a/src/utils/test/preparePage.ts +++ b/src/utils/test/preparePage.ts @@ -127,6 +127,6 @@ export const preparePage = async (page: Page): Promise<() => Promise> => { page.removeListener('response', responseListener); page.removeListener('requestfinished', requestfinishedListener); - await page.unrouteAll({behavior: 'ignoreErrors'}); + await page.unrouteAll({behavior: 'ignoreErrors'}).catch(() => {}); }; }; diff --git a/src/utils/tests/runTests.ts b/src/utils/tests/runTests.ts index 8a20318b..35bba8e9 100644 --- a/src/utils/tests/runTests.ts +++ b/src/utils/tests/runTests.ts @@ -6,6 +6,7 @@ import {getFullPackConfig} from '../config'; import {getRunLabel, setRunLabel} from '../environment'; import {generalLog} from '../generalLog'; import {startResourceUsageReading} from '../resourceUsage'; +import {isUiMode} from '../uiMode'; import {beforeRunFirstTest} from './beforeRunFirstTest'; import {stripExtraLogs} from './stripExtraLogs'; @@ -23,7 +24,7 @@ export const runTests = async ({runLabel}: RunRetryOptions): Promise => { setRunLabel(runLabel); try { - const {enableLiveMode, testIdleTimeout, resourceUsageReadingInternal} = getFullPackConfig(); + const {testIdleTimeout, resourceUsageReadingInternal} = getFullPackConfig(); startResourceUsageReading(resourceUsageReadingInternal); @@ -49,7 +50,7 @@ export const runTests = async ({runLabel}: RunRetryOptions): Promise => { playwrightArgs.push('--debug'); } - if (enableLiveMode) { + if (isUiMode) { playwrightArgs.push('--ui'); } diff --git a/src/utils/uiMode.ts b/src/utils/uiMode.ts new file mode 100644 index 00000000..eda60dd5 --- /dev/null +++ b/src/utils/uiMode.ts @@ -0,0 +1,21 @@ +import {e2edEnvironment, UI_MODE_VARIABLE_NAME} from '../constants/internal'; + +import {assertValueIsFalse} from './asserts'; + +/** + * `true` if e2ed run in UI mode, and `false` otherwise. + */ +// eslint-disable-next-line import/no-mutable-exports +export let isUiMode = Boolean(e2edEnvironment[UI_MODE_VARIABLE_NAME]); + +/** + * Set current run environment before e2ed start. + * @internal + */ +export const setUiMode = (): void => { + assertValueIsFalse(isUiMode, 'isUiMode is false'); + + isUiMode = true; + + e2edEnvironment[UI_MODE_VARIABLE_NAME] = 'true'; +};