From 9d2a2e75d67cf0fbb26f5877275426b9e4233008 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 fix: add `dateTimeInIso` to browser JS errors and console logs chore: clarify `@typescript-eslint/no-unused-vars` rule --- .eslintrc.yaml | 8 +++ Dockerfile | 4 +- README.md | 6 +- autotests/packs/allTests.ts | 3 +- .../tests/e2edReportExample/browserData.ts | 24 ++++--- .../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/actions/getBrowserConsoleMessages.ts | 4 +- src/actions/getBrowserJsErrors.ts | 8 +-- 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 | 22 ++++++- 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/console.ts | 1 + src/types/environment.ts | 2 + src/types/errors.ts | 8 +++ src/types/index.ts | 2 +- src/types/internal.ts | 2 +- src/types/startInfo.ts | 2 + src/types/testRun.ts | 1 + src/utils/config/getFullPackConfig.ts | 2 +- src/utils/end/endE2ed.ts | 4 +- src/utils/events/registerEndE2edRunEvent.ts | 2 +- src/utils/fullMocks/enableFullMocks.ts | 1 + src/utils/generalLog/logStartE2edError.ts | 2 +- src/utils/generalLog/successfulTestRuns.ts | 5 ++ src/utils/index.ts | 1 + .../promise/getPromiseWithResolveAndReject.ts | 3 +- .../client/render/renderTestRunError.ts | 9 ++- src/utils/report/collectReportData.ts | 5 +- 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 | 13 ++-- src/utils/tests/runTests.ts | 5 +- src/utils/uiMode.ts | 21 +++++++ .../getShallowCopyOfObjectForLogs.ts | 2 +- 51 files changed, 282 insertions(+), 106 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/.eslintrc.yaml b/.eslintrc.yaml index fadadc99..90e94a80 100644 --- a/.eslintrc.yaml +++ b/.eslintrc.yaml @@ -207,6 +207,14 @@ rules: '@typescript-eslint/no-unsafe-unary-minus': error '@typescript-eslint/no-use-before-define': error '@typescript-eslint/no-unused-expressions': error + '@typescript-eslint/no-unused-vars': + - error + - argsIgnorePattern: ^_ + caughtErrors: all + caughtErrorsIgnorePattern: ^_ + destructuredArrayIgnorePattern: ^_ + ignoreRestSiblings: true + varsIgnorePattern: ^_ '@typescript-eslint/no-useless-empty-export': error '@typescript-eslint/parameter-properties': error '@typescript-eslint/prefer-destructuring': 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..5fd1aaa9 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,17 +21,18 @@ 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(); const columnNumber = 12; const url = ''; - await expect(consoleMessages, 'getBrowserConsoleMessages read all of messages').eql([ + const consoleMessagesWithoutDate = consoleMessages.map( + ({dateTimeInIso: _, ...messageWithoutDate}) => messageWithoutDate, + ); + + await expect(consoleMessagesWithoutDate, 'getBrowserConsoleMessages read all of messages').eql([ {args: ['error'], location: {columnNumber, lineNumber: 3, url}, text: 'error', type: 'error'}, {args: ['info'], location: {columnNumber, lineNumber: 4, url}, text: 'info', type: 'info'}, {args: ['log'], location: {columnNumber, lineNumber: 5, url}, text: 'log', type: 'log'}, @@ -47,5 +46,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]?.error), '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/actions/getBrowserConsoleMessages.ts b/src/actions/getBrowserConsoleMessages.ts index 7770d8b2..d2dd3043 100644 --- a/src/actions/getBrowserConsoleMessages.ts +++ b/src/actions/getBrowserConsoleMessages.ts @@ -4,9 +4,7 @@ import {log} from '../utils/log'; import type {ConsoleMessage} from '../types/internal'; -type Options = Readonly<{ - showMessagesInLog?: boolean; -}>; +type Options = Readonly<{showMessagesInLog?: boolean}>; const logMessage = 'Get browser console messages'; diff --git a/src/actions/getBrowserJsErrors.ts b/src/actions/getBrowserJsErrors.ts index 1dbe92bf..ffc0418d 100644 --- a/src/actions/getBrowserJsErrors.ts +++ b/src/actions/getBrowserJsErrors.ts @@ -2,14 +2,14 @@ import {LogEventStatus, LogEventType} from '../constants/internal'; import {getJsErrorsFromContext} from '../context/jsError'; import {log} from '../utils/log'; -type Options = Readonly<{ - showErrorsInLog?: boolean; -}>; +import type {JsError} from '../types/internal'; + +type Options = Readonly<{showErrorsInLog?: boolean}>; /** * Get browser JS errors. */ -export const getBrowserJsErrors = (options: Options = {}): readonly Error[] => { +export const getBrowserJsErrors = (options: Options = {}): readonly JsError[] => { const {showErrorsInLog = false} = options; const jsErrors = getJsErrorsFromContext(); 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..dea8ccf1 100644 --- a/src/context/jsError.ts +++ b/src/context/jsError.ts @@ -1,7 +1,27 @@ import {useContext} from '../useContext'; +import type {JsError} from '../types/internal'; + +/** + * 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 JsError[] => { + const maybeJsErrors = getRawJsErrorsFromContext(); + + if (maybeJsErrors !== undefined) { + return maybeJsErrors; + } + + const jsErrors: readonly JsError[] = []; + + 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/console.ts b/src/types/console.ts index a8652e2d..00071908 100644 --- a/src/types/console.ts +++ b/src/types/console.ts @@ -5,6 +5,7 @@ import type {ConsoleMessage as PlaywrightConsoleMessage} from '@playwright/test' */ export type ConsoleMessage = Readonly<{ args: readonly unknown[]; + dateTimeInIso: string; location: Readonly; text: string; type: ConsoleMessageType; 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/errors.ts b/src/types/errors.ts index 1720fdd7..30b7f65b 100644 --- a/src/types/errors.ts +++ b/src/types/errors.ts @@ -12,6 +12,14 @@ export type E2edPrintedFields = Readonly<{ stackTrace: readonly string[]; }>; +/** + * JS error from browser. + */ +export type JsError = Readonly<{ + dateTimeInIso: string; + error: Error; +}>; + /** * Maybe error params with optional field `isTestRunBroken` (or `undefined`). * The presence of such a field in a reject error results in diff --git a/src/types/index.ts b/src/types/index.ts index 7501a2f4..9b73833e 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -7,7 +7,7 @@ export type {BrowserName} from './config'; export type {ConsoleMessage, ConsoleMessageType} from './console'; export type {UtcTimeInMs} from './date'; export type {DeepMutable, DeepPartial, DeepReadonly, DeepRequired} from './deep'; -export type {E2edPrintedFields} from './errors'; +export type {E2edPrintedFields, JsError} from './errors'; export type {LogEvent, Onlog, TestRunEvent} from './events'; export type {Fn, MergeFunctions} from './fn'; export type { diff --git a/src/types/internal.ts b/src/types/internal.ts index 628a6cef..106d489d 100644 --- a/src/types/internal.ts +++ b/src/types/internal.ts @@ -20,7 +20,7 @@ export type {UtcTimeInMs} from './date'; export type {DeepMutable, DeepPartial, DeepReadonly, DeepRequired} from './deep'; /** @internal */ export type {E2edEnvironment} from './environment'; -export type {E2edPrintedFields} from './errors'; +export type {E2edPrintedFields, JsError} from './errors'; /** @internal */ export type {MaybeWithIsTestRunBroken} from './errors'; export type {LogEvent, Onlog, TestRunEvent} from './events'; 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/config/getFullPackConfig.ts b/src/utils/config/getFullPackConfig.ts index 077f881a..4396fa3c 100644 --- a/src/utils/config/getFullPackConfig.ts +++ b/src/utils/config/getFullPackConfig.ts @@ -28,7 +28,7 @@ export const getFullPackConfig = < const startInfo = readStartInfoSync(); updateConfig(updatedConfig, startInfo); - } catch (error) {} + } catch {} } return updatedConfig as unknown as FullPackConfig< diff --git a/src/utils/end/endE2ed.ts b/src/utils/end/endE2ed.ts index dd20a6ba..c4c0ad84 100644 --- a/src/utils/end/endE2ed.ts +++ b/src/utils/end/endE2ed.ts @@ -17,7 +17,7 @@ export const endE2ed = (definedEndE2edReason: EndE2edReason): void => { try { generalLog(message); - } catch (error) { + } catch { console.log(message); } @@ -28,7 +28,7 @@ export const endE2ed = (definedEndE2edReason: EndE2edReason): void => { try { generalLog(message); - } catch (error) { + } catch { console.log(message); } diff --git a/src/utils/events/registerEndE2edRunEvent.ts b/src/utils/events/registerEndE2edRunEvent.ts index a288dc57..7da261b8 100644 --- a/src/utils/events/registerEndE2edRunEvent.ts +++ b/src/utils/events/registerEndE2edRunEvent.ts @@ -28,7 +28,7 @@ export const registerEndE2edRunEvent = async (): Promise => { try { generalLog(message); - } catch (error) { + } catch { // eslint-disable-next-line no-console console.log(message); } 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/logStartE2edError.ts b/src/utils/generalLog/logStartE2edError.ts index 113f2184..3679712b 100644 --- a/src/utils/generalLog/logStartE2edError.ts +++ b/src/utils/generalLog/logStartE2edError.ts @@ -11,7 +11,7 @@ export const logStartE2edError = (error: unknown): void => { try { generalLog(message, {error}); - } catch (cause) { + } catch { // eslint-disable-next-line no-console console.log(message, error); } 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/report/collectReportData.ts b/src/utils/report/collectReportData.ts index 5a289616..d1f28fd7 100644 --- a/src/utils/report/collectReportData.ts +++ b/src/utils/report/collectReportData.ts @@ -1,5 +1,6 @@ import {getFullPackConfig} from '../config'; import {getExitCode} from '../exit'; +import {isUiMode} from '../uiMode'; import {assertThatTestNamesAndFilePathsAreUnique} from './assertThatTestNamesAndFilePathsAreUnique'; import {getFailedTestsMainParams} from './getFailedTestsMainParams'; @@ -25,7 +26,9 @@ export const collectReportData = async ({ const errors = await getReportErrors(fullTestRuns, notIncludedInPackTests); - assertThatTestNamesAndFilePathsAreUnique(fullTestRuns); + if (!isUiMode) { + assertThatTestNamesAndFilePathsAreUnique(fullTestRuns); + } unificateRunHashes(fullTestRuns); 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..bfe7e2f8 100644 --- a/src/utils/test/preparePage.ts +++ b/src/utils/test/preparePage.ts @@ -15,7 +15,7 @@ import type { Response as PlaywrightResponse, } from '@playwright/test'; -import type {ConsoleMessage, ConsoleMessageType} from '../../types/internal'; +import type {ConsoleMessage, ConsoleMessageType, JsError} from '../../types/internal'; const afterNavigationRequestsDelayInMs = 300; @@ -25,7 +25,7 @@ const afterNavigationRequestsDelayInMs = 300; */ export const preparePage = async (page: Page): Promise<() => Promise> => { const consoleMessages = getConsoleMessagesFromContext() as ConsoleMessage[]; - const jsErrors = getJsErrorsFromContext() as Error[]; + const jsErrors = getJsErrorsFromContext() as JsError[]; const navigationDelay = getNavigationDelay(); await page.route( @@ -46,11 +46,12 @@ export const preparePage = async (page: Page): Promise<() => Promise> => { const consoleListener = AsyncLocalStorage.bind(async (message: PlaywrightConsoleMessage) => { const args: unknown[] = []; + const dateTimeInIso = new Date().toISOString(); const location = message.location(); const text = message.text(); const type = message.type() as ConsoleMessageType; - consoleMessages.push({args, location, text, type}); + consoleMessages.push({args, dateTimeInIso, location, text, type}); for (const jsHandle of message.args()) { args.push(await jsHandle.jsonValue().catch(() => 'Error with getting value of argument')); @@ -58,7 +59,9 @@ export const preparePage = async (page: Page): Promise<() => Promise> => { }); const pageerrorListener = AsyncLocalStorage.bind((error: Error) => { - jsErrors.push(error); + const dateTimeInIso = new Date().toISOString(); + + jsErrors.push({dateTimeInIso, error}); }); const requestListener = AsyncLocalStorage.bind((newRequest: PlaywrightRequest) => { @@ -127,6 +130,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'; +}; diff --git a/src/utils/valueToString/getShallowCopyOfObjectForLogs.ts b/src/utils/valueToString/getShallowCopyOfObjectForLogs.ts index 3e356231..b0e88d2d 100644 --- a/src/utils/valueToString/getShallowCopyOfObjectForLogs.ts +++ b/src/utils/valueToString/getShallowCopyOfObjectForLogs.ts @@ -15,7 +15,7 @@ export const getShallowCopyOfObjectForLogs = (value: object): Return => { if (property != null && (typeof property === 'object' || typeof property === 'function')) { try { copy[key] = String(property); - } catch (error) { + } catch { const keys = Object.keys(property).join(', '); copy[key] = keys === '' ? '[object without keys]' : `[object with keys: ${keys}]`;