From f15fb0b51b796a319050ca4acdf8dc6790ec0a10 Mon Sep 17 00:00:00 2001 From: uid11 Date: Wed, 21 Aug 2024 17:59:28 +0300 Subject: [PATCH] FI-1344 fix: add action `pause` instead of `debug` fix: `getBrowserConsoleMessages` action fix: `getBrowserJsErrors` action fix: clear page callbacks in the end ot the test chore: update devDependencies (`@types/node`, `husky`) fix: rename config property `browser` to `browserName` fix: add config property `deviceScaleFactor` fix: rename config property `overriddenUserAgent` to `userAgent` --- README.md | 11 +- autotests/packs/allTests.ts | 9 +- .../tests/e2edReportExample/browserData.ts | 27 ++-- .../selectorCustomMethods.ts | 2 +- package-lock.json | 35 ++--- package.json | 4 +- src/actions/debug.ts | 12 -- src/actions/getBrowserConsoleMessages.ts | 31 ++--- src/actions/getBrowserJsErrors.ts | 17 +-- src/actions/index.ts | 2 +- src/actions/navigateToUrl.ts | 12 +- src/actions/pause.ts | 14 ++ src/actions/waitFor/waitForRequest.ts | 23 ++- src/actions/waitFor/waitForResponse.ts | 24 +++- src/config.ts | 20 ++- src/constants/childProcess.ts | 2 +- src/constants/index.ts | 4 - src/constants/internal.ts | 6 - src/constants/requestHook.ts | 22 --- src/context/consoleMessages.ts | 9 ++ src/context/jsError.ts | 7 + src/types/config/config.ts | 5 + src/types/config/index.ts | 2 +- src/types/config/ownE2edConfig.ts | 25 ++-- src/types/console.ts | 34 +++++ src/types/errors.ts | 5 - src/types/index.ts | 9 +- src/types/internal.ts | 14 +- src/types/requestHooks.ts | 86 ------------ src/types/waitForEvents.ts | 8 +- src/utils/events/registerEndTestRunEvent.ts | 2 +- src/utils/events/writeFullMocks.ts | 10 +- src/utils/test/afterErrorInTest.ts | 4 +- src/utils/test/afterTest.ts | 8 +- src/utils/test/getRunTest.ts | 5 +- src/utils/test/preparePage.ts | 131 +++++++++++------- 36 files changed, 340 insertions(+), 301 deletions(-) delete mode 100644 src/actions/debug.ts create mode 100644 src/actions/pause.ts delete mode 100644 src/constants/requestHook.ts create mode 100644 src/context/consoleMessages.ts create mode 100644 src/context/jsError.ts create mode 100644 src/types/console.ts delete mode 100644 src/types/requestHooks.ts diff --git a/README.md b/README.md index 986b7bf0..dd8598f5 100644 --- a/README.md +++ b/README.md @@ -232,17 +232,18 @@ that exports the pack's config under the name `pack`. Here are the basic fields of the pack config. -`browser: string`: browser name as a command to launch it -(like `chrome`, `chromium`, `firefox`, `webkit`, etc). - `browserFlags: string[]`: array of browser flags, like `--disable-dev-shm-usage`, with which the browser is launched to run tests. +`browserName: BrowserName`: browser name (one of `chromium`, `firefox`, `webkit`). + `concurrency: number`: the number of browser windows in which tests will run in parallel. `customPackProperties: CustomPackProperties`: a custom set of fields defined within the project. These fields allow, for example, to customize the behavior of hooks for different packs. +`deviceScaleFactor: number`: device scale factor (aka `window.devicePixelRatio`). + `doAfterPack: ((liteReport: LiteReport) => CustomReportProperties | undefined)[]`: an array of functions that will be executed, in order, after the pack completes. The functions accept a lite report object, and can return custom report properties, @@ -318,8 +319,6 @@ For example, if it is equal to three, the test will be run no more than three ti `overriddenConfigFields: PlaywrightTestConfig | null`: if not `null`, then this value will override fields of internal Playwright config. -`overriddenUserAgent: string | null`: if not `null`, then this value will override the browser's user agent in tests. - `packTimeout: number`: timeout (in millisecond) for the entire pack of tests (tasks). If the test pack takes longer than this timeout, the pack will fail with the appropriate error. @@ -362,6 +361,8 @@ This parameter can be overridden in the test-specific options. If the test run takes longer than this timeout, the test fails and rerun on the next retry. This parameter can be overridden in the test-specific options. +`userAgent: string`: `userAgent` string of browser (device) in tests. + `viewportHeight: number`: height of viewport of page in pixels. `viewportWidth: number`: width of viewport of page in pixels. diff --git a/autotests/packs/allTests.ts b/autotests/packs/allTests.ts index fdba3ad7..2faf1001 100644 --- a/autotests/packs/allTests.ts +++ b/autotests/packs/allTests.ts @@ -28,11 +28,9 @@ const browserFlags = [ '--ignore-certificate-errors', ]; -const browser = isLocalRun ? 'chrome' : 'chromium'; - const filterTestsIntoPack: FilterTestsIntoPack = ({options}) => options.meta.testId !== '13'; -const overriddenUserAgent = +const userAgent = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.35 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.35'; const packTimeoutInMinutes = 5; @@ -44,10 +42,11 @@ const msInMinute = 60_000; export const pack: Pack = { ajaxRequestTimeout: 40_000, assertionTimeout: 5_000, - browser, browserFlags, + browserName: 'chromium', concurrency: isLocalRun ? 1 : 2, customPackProperties: {internalPackRunId: 0, name: 'allTests'}, + deviceScaleFactor: 1, doAfterPack, doBeforePack, enableHeadlessMode: true, @@ -65,7 +64,6 @@ export const pack: Pack = { mapLogPayloadInReport, maxRetriesCountInDocker: 3, overriddenConfigFields: null, - overriddenUserAgent, packTimeout: packTimeoutInMinutes * msInMinute, pageRequestTimeout: 30_000, pageStabilizationInterval: 500, @@ -81,6 +79,7 @@ export const pack: Pack = { testFileGlobs: ['**/autotests/tests/**/*.ts'], testIdleTimeout: 8_000, testTimeout: 60_000, + userAgent, viewportHeight: 1080, viewportWidth: 1920, waitForAllRequestsComplete: { diff --git a/autotests/tests/e2edReportExample/browserData.ts b/autotests/tests/e2edReportExample/browserData.ts index 515c4fc5..93e1c9df 100644 --- a/autotests/tests/e2edReportExample/browserData.ts +++ b/autotests/tests/e2edReportExample/browserData.ts @@ -19,7 +19,7 @@ test('correctly read data from browser', {meta: {testId: '14'}}, async () => { console.error('error'); console.info('info'); console.log('log'); - console.warn('warn'); + console.warn('warning'); setTimeout(() => { throw new Error('foo'); @@ -29,14 +29,23 @@ test('correctly read data from browser', {meta: {testId: '14'}}, async () => { }, 32); })(); - const {error, info, log, warn} = await getBrowserConsoleMessages(); - - await expect( - error.length === 0 && info.length === 0 && log.length === 0 && warn.length === 0, - 'getBrowserConsoleMessages read all of messages', - ).eql(true); - - const jsErrors = await getBrowserJsErrors(); + const consoleMessages = getBrowserConsoleMessages(); + const columnNumber = 12; + const url = ''; + + await expect(consoleMessages, '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'}, + { + args: ['warning'], + location: {columnNumber, lineNumber: 6, url}, + text: 'warning', + type: 'warning', + }, + ]); + + const jsErrors = getBrowserJsErrors(); await expect(jsErrors.length === 0, 'getBrowserJsErrors read JS errors').eql(true); }); diff --git a/autotests/tests/e2edReportExample/selectorCustomMethods.ts b/autotests/tests/e2edReportExample/selectorCustomMethods.ts index a355f9b3..46e65414 100644 --- a/autotests/tests/e2edReportExample/selectorCustomMethods.ts +++ b/autotests/tests/e2edReportExample/selectorCustomMethods.ts @@ -56,7 +56,7 @@ test('selector custom methods', {meta: {testId: '15'}}, async () => { '[data-testid="app-navigation-retries"].findByLocatorId(app-navigation-retries-button).filterByLocatorParameter(selected, true)', ); - await click(reportPage.navigationRetriesButton); + await click(reportPage.navigationRetriesButton.nth(0)); await expect( reportPage.testRunButton.nth(2).getLocatorParameter('status'), diff --git a/package-lock.json b/package-lock.json index 1c6031ff..5a07aa05 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,7 @@ }, "devDependencies": { "@playwright/browser-chromium": "1.46.1", - "@types/node": "22.4.1", + "@types/node": "22.5.0", "@typescript-eslint/eslint-plugin": "7.18.0", "@typescript-eslint/parser": "7.18.0", "assert-modules-support-case-insensitive-fs": "1.0.1", @@ -31,7 +31,7 @@ "eslint-plugin-import": "2.29.1", "eslint-plugin-simple-import-sort": "12.1.1", "eslint-plugin-typescript-sort-keys": "3.2.0", - "husky": "9.1.4", + "husky": "9.1.5", "prettier": "3.3.3", "typescript": "5.5.4" }, @@ -215,9 +215,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "22.4.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.4.1.tgz", - "integrity": "sha512-1tbpb9325+gPnKK0dMm+/LMriX0vKxf6RnB0SZUqfyVkQ4fMgUSySqhxE/y8Jvs4NyF1yHzTfG9KlnkIODxPKg==", + "version": "22.5.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.0.tgz", + "integrity": "sha512-DkFrJOe+rfdHTqqMg0bSNlGlQ85hSoh2TPzZyhHsXnMtligRWpxUySiyw8FY14ITt24HVCiQPWxS3KO/QlGmWg==", "dev": true, "license": "MIT", "dependencies": { @@ -1900,10 +1900,11 @@ } }, "node_modules/husky": { - "version": "9.1.4", - "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.4.tgz", - "integrity": "sha512-bho94YyReb4JV7LYWRWxZ/xr6TtOTt8cMfmQ39MQYJ7f/YE268s3GdghGwi+y4zAeqewE5zYLvuhV0M0ijsDEA==", + "version": "9.1.5", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.5.tgz", + "integrity": "sha512-rowAVRUBfI0b4+niA4SJMhfQwc107VLkBUgEYYAOQAbqDCnra1nYh83hF/MDmhYs9t9n1E3DuKOrs2LYNC+0Ag==", "dev": true, + "license": "MIT", "bin": { "husky": "bin.js" }, @@ -2277,12 +2278,13 @@ } }, "node_modules/micromatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", - "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "license": "MIT", "dependencies": { - "braces": "^3.0.1", - "picomatch": "^2.2.3" + "braces": "^3.0.3", + "picomatch": "^2.3.1" }, "engines": { "node": ">=8.6" @@ -2504,9 +2506,10 @@ } }, "node_modules/picomatch": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", - "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "license": "MIT", "engines": { "node": ">=8.6" }, diff --git a/package.json b/package.json index f04070a3..2e3bafd2 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ }, "devDependencies": { "@playwright/browser-chromium": "1.46.1", - "@types/node": "22.4.1", + "@types/node": "22.5.0", "@typescript-eslint/eslint-plugin": "7.18.0", "@typescript-eslint/parser": "7.18.0", "assert-modules-support-case-insensitive-fs": "1.0.1", @@ -42,7 +42,7 @@ "eslint-plugin-import": "2.29.1", "eslint-plugin-simple-import-sort": "12.1.1", "eslint-plugin-typescript-sort-keys": "3.2.0", - "husky": "9.1.4", + "husky": "9.1.5", "prettier": "3.3.3", "typescript": "5.5.4" }, diff --git a/src/actions/debug.ts b/src/actions/debug.ts deleted file mode 100644 index 96fd4aa3..00000000 --- a/src/actions/debug.ts +++ /dev/null @@ -1,12 +0,0 @@ -import {LogEventType} from '../constants/internal'; -import {log} from '../utils/log'; - -/** - * Pauses the test and switches to the step-by-step execution mode. - */ -export const debug = (): Promise => { - log('Start debug mode', LogEventType.InternalAction); - - // TODO - return Promise.resolve(); -}; diff --git a/src/actions/getBrowserConsoleMessages.ts b/src/actions/getBrowserConsoleMessages.ts index 4c95e19f..7770d8b2 100644 --- a/src/actions/getBrowserConsoleMessages.ts +++ b/src/actions/getBrowserConsoleMessages.ts @@ -1,36 +1,31 @@ import {LogEventStatus, LogEventType} from '../constants/internal'; +import {getConsoleMessagesFromContext} from '../context/consoleMessages'; import {log} from '../utils/log'; -type ConsoleMessages = Readonly<{ - error: readonly string[]; - info: readonly string[]; - log: readonly string[]; - warn: readonly string[]; -}>; +import type {ConsoleMessage} from '../types/internal'; type Options = Readonly<{ showMessagesInLog?: boolean; }>; +const logMessage = 'Get browser console messages'; + /** * Returns an object that contains messages output to the browser console. */ -export const getBrowserConsoleMessages = (options: Options = {}): Promise => { +export const getBrowserConsoleMessages = (options: Options = {}): readonly ConsoleMessage[] => { const {showMessagesInLog = false} = options; + const consoleMessages = getConsoleMessagesFromContext(); if (showMessagesInLog === false) { - log('Get browser console messages', LogEventType.InternalAction); + log(logMessage, LogEventType.InternalAction); + } else { + const logEventStatus = consoleMessages.some(({type}) => type === 'error') + ? LogEventStatus.Failed + : LogEventStatus.Passed; - // TODO - return Promise.resolve({error: [], info: [], log: [], warn: []}); + log(logMessage, {consoleMessages, logEventStatus}, LogEventType.InternalAction); } - return Promise.resolve({error: [], info: [], log: [], warn: []}).then((messages) => { - const logEventStatus = - messages.error.length > 0 ? LogEventStatus.Failed : LogEventStatus.Passed; - - log('Got browser console messages', {logEventStatus, messages}, LogEventType.InternalAction); - - return messages; - }); + return consoleMessages; }; diff --git a/src/actions/getBrowserJsErrors.ts b/src/actions/getBrowserJsErrors.ts index 013d8199..1dbe92bf 100644 --- a/src/actions/getBrowserJsErrors.ts +++ b/src/actions/getBrowserJsErrors.ts @@ -1,8 +1,7 @@ import {LogEventStatus, LogEventType} from '../constants/internal'; +import {getJsErrorsFromContext} from '../context/jsError'; import {log} from '../utils/log'; -import type {BrowserJsError} from '../types/internal'; - type Options = Readonly<{ showErrorsInLog?: boolean; }>; @@ -10,21 +9,17 @@ type Options = Readonly<{ /** * Get browser JS errors. */ -export const getBrowserJsErrors = (options: Options = {}): Promise => { +export const getBrowserJsErrors = (options: Options = {}): readonly Error[] => { const {showErrorsInLog = false} = options; + const jsErrors = getJsErrorsFromContext(); if (showErrorsInLog === false) { log('Get browser JS errors', LogEventType.InternalAction); - - return Promise.resolve([]); - } - - // TODO - return Promise.resolve([]).then((jsErrors = []) => { + } else { const logEventStatus = jsErrors.length > 0 ? LogEventStatus.Failed : LogEventStatus.Passed; log('Got browser JS errors', {jsErrors, logEventStatus}, LogEventType.InternalAction); + } - return jsErrors; - }); + return jsErrors; }; diff --git a/src/actions/index.ts b/src/actions/index.ts index c0c83a99..8c705213 100644 --- a/src/actions/index.ts +++ b/src/actions/index.ts @@ -8,7 +8,6 @@ export { } from './asserts'; export {clearUpload} from './clearUpload'; export {click} from './click'; -export {debug} from './debug'; export {deleteCookies} from './deleteCookies'; export {dispatchEvent} from './dispatchEvent'; export {doubleClick} from './doubleClick'; @@ -28,6 +27,7 @@ export { navigateToPage, reloadPage, } from './pages'; +export {pause} from './pause'; export {pressKey} from './pressKey'; export {resizeWindow} from './resizeWindow'; export {scroll} from './scroll'; diff --git a/src/actions/navigateToUrl.ts b/src/actions/navigateToUrl.ts index 4a6bed7b..1c380d9a 100644 --- a/src/actions/navigateToUrl.ts +++ b/src/actions/navigateToUrl.ts @@ -2,11 +2,11 @@ import {LogEventType} from '../constants/internal'; import {getPlaywrightPage} from '../useContext'; import {log} from '../utils/log'; +import type {Page} from '@playwright/test'; + import type {Url} from '../types/internal'; -type Options = Readonly<{ - skipLogs?: boolean; -}>; +type Options = Readonly<{skipLogs?: boolean} & Parameters[1]>; /** * Navigate to the `url` (without waiting of interface stabilization). @@ -15,14 +15,14 @@ export const navigateToUrl = async (url: Url, options: Options = {}): Promise => { + log('Pause', LogEventType.InternalAction); + + const page = getPlaywrightPage(); + + return page.pause(); +}; diff --git a/src/actions/waitFor/waitForRequest.ts b/src/actions/waitFor/waitForRequest.ts index 69feab61..23fc8839 100644 --- a/src/actions/waitFor/waitForRequest.ts +++ b/src/actions/waitFor/waitForRequest.ts @@ -7,7 +7,12 @@ import {getDurationWithUnits} from '../../utils/getDurationWithUnits'; import {log} from '../../utils/log'; import {getRequestFromPlaywrightRequest} from '../../utils/requestHooks'; -import type {Request, RequestPredicate, RequestWithUtcTimeInMs} from '../../types/internal'; +import type { + Request, + RequestPredicate, + RequestWithUtcTimeInMs, + UtcTimeInMs, +} from '../../types/internal'; /** * Waits for some request (from browser) filtered by the request predicate. @@ -17,6 +22,8 @@ export const waitForRequest = ( predicate: RequestPredicate, {skipLogs = false, timeout}: {skipLogs?: boolean; timeout?: number} = {}, ): Promise> => { + const startTimeInMs = Date.now() as UtcTimeInMs; + setCustomInspectOnFunction(predicate); const {waitForRequestTimeout} = getFullPackConfig(); @@ -57,5 +64,17 @@ export const waitForRequest = ( ); } - return promise; + return skipLogs !== true + ? promise.then((request) => { + const waitWithUnits = getDurationWithUnits(Date.now() - startTimeInMs); + + log( + `Have waited for request for ${waitWithUnits}`, + {predicate, request, timeoutWithUnits}, + LogEventType.InternalCore, + ); + + return request; + }) + : promise; }; diff --git a/src/actions/waitFor/waitForResponse.ts b/src/actions/waitFor/waitForResponse.ts index 6a2c726e..cf15c479 100644 --- a/src/actions/waitFor/waitForResponse.ts +++ b/src/actions/waitFor/waitForResponse.ts @@ -9,7 +9,13 @@ import {log} from '../../utils/log'; import {getResponseFromPlaywrightResponse} from '../../utils/requestHooks'; import {getWaitForResponsePredicate} from '../../utils/waitForEvents'; -import type {Request, Response, ResponsePredicate, ResponseWithRequest} from '../../types/internal'; +import type { + Request, + Response, + ResponsePredicate, + ResponseWithRequest, + UtcTimeInMs, +} from '../../types/internal'; type Options = Readonly<{includeNavigationRequest?: boolean; skipLogs?: boolean; timeout?: number}>; @@ -24,6 +30,8 @@ export const waitForResponse = < predicate: ResponsePredicate, {includeNavigationRequest = false, skipLogs = false, timeout}: Options = {}, ): Promise> => { + const startTimeInMs = Date.now() as UtcTimeInMs; + setCustomInspectOnFunction(predicate); const {waitForResponseTimeout} = getFullPackConfig(); @@ -59,5 +67,17 @@ export const waitForResponse = < ); } - return promise; + return skipLogs !== true + ? promise.then((response) => { + const waitWithUnits = getDurationWithUnits(Date.now() - startTimeInMs); + + log( + `Have waited for response for ${waitWithUnits}`, + {predicate, response, timeoutWithUnits}, + LogEventType.InternalCore, + ); + + return response; + }) + : promise; }; diff --git a/src/config.ts b/src/config.ts index e37bd10d..c0f7a386 100644 --- a/src/config.ts +++ b/src/config.ts @@ -25,7 +25,7 @@ import {isLocalRun} from './configurator'; import type {FullPackConfig, Mutable, UserlandPack} from './types/internal'; -import {defineConfig, devices} from '@playwright/test'; +import {defineConfig} from '@playwright/test'; const maxTimeoutInMs = 3600_000; @@ -102,9 +102,13 @@ const playwrightConfig = defineConfig({ projects: [ { - name: 'chromium', + name: userlandPack.browserName, use: { - ...devices['Desktop Chrome'], + browserName: userlandPack.browserName, + deviceScaleFactor: userlandPack.deviceScaleFactor, + hasTouch: userlandPack.enableTouchEventEmulation, + isMobile: userlandPack.enableMobileDeviceMode, + userAgent: userlandPack.userAgent, viewport: {height: userlandPack.viewportHeight, width: userlandPack.viewportWidth}, }, }, @@ -125,15 +129,25 @@ const playwrightConfig = defineConfig({ 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, diff --git a/src/constants/childProcess.ts b/src/constants/childProcess.ts index e96b4fe9..27572353 100644 --- a/src/constants/childProcess.ts +++ b/src/constants/childProcess.ts @@ -1,5 +1,5 @@ /** - * Default options for execFile/execFileSync function from node:child_process. + * Default options for `execFile`/`execFileSync` function from `node:child_process`. * @internal */ export const EXEC_FILE_OPTIONS = {encoding: 'utf8'} as const; diff --git a/src/constants/index.ts b/src/constants/index.ts index c8d9924d..e5e443ec 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -10,9 +10,5 @@ export { OK_STATUS_CODE, } from './http'; export {LogEventStatus, LogEventType} from './log'; -export { - INCLUDE_BODY_AND_HEADERS_IN_RESPONSE_EVENT, - INCLUDE_HEADERS_IN_RESPONSE_EVENT, -} from './requestHook'; export {FAILED_TEST_RUN_STATUSES, TestRunStatus} from './testRun'; export {ANY_URL_REGEXP, SLASHES_AT_THE_END_REGEXP, SLASHES_AT_THE_START_REGEXP} from './url'; diff --git a/src/constants/internal.ts b/src/constants/internal.ts index fc5fca3f..7398051c 100644 --- a/src/constants/internal.ts +++ b/src/constants/internal.ts @@ -55,12 +55,6 @@ export { } from './paths'; /** @internal */ export {RESOLVED_PROMISE} from './promise'; -export { - INCLUDE_BODY_AND_HEADERS_IN_RESPONSE_EVENT, - INCLUDE_HEADERS_IN_RESPONSE_EVENT, - REQUEST_HOOK_CONTEXT_ID_KEY, - REQUEST_HOOK_CONTEXT_KEY, -} from './requestHook'; export {DESCRIPTION_KEY} from './selector'; /** @internal */ export {RETRY_KEY} from './selector'; diff --git a/src/constants/requestHook.ts b/src/constants/requestHook.ts deleted file mode 100644 index 94d7f4f0..00000000 --- a/src/constants/requestHook.ts +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Request hook options that includes body and headers in response event. - */ -export const INCLUDE_BODY_AND_HEADERS_IN_RESPONSE_EVENT = { - includeBody: true, - includeHeaders: true, -} as const; - -/** - * Request hook options that includes headers in response event. - */ -export const INCLUDE_HEADERS_IN_RESPONSE_EVENT = {includeHeaders: true} as const; - -/** - * Key for id of TestCafe's request hook context. - */ -export const REQUEST_HOOK_CONTEXT_ID_KEY = Symbol('e2ed:REQUEST_HOOK_CONTEXT_ID_KEY'); - -/** - * Key for TestCafe's request hook context on request hook events. - */ -export const REQUEST_HOOK_CONTEXT_KEY = Symbol('e2ed:REQUEST_HOOK_CONTEXT_KEY'); diff --git a/src/context/consoleMessages.ts b/src/context/consoleMessages.ts new file mode 100644 index 00000000..2246e46f --- /dev/null +++ b/src/context/consoleMessages.ts @@ -0,0 +1,9 @@ +import {useContext} from '../useContext'; + +import type {ConsoleMessage} from '../types/internal'; + +/** + * Get browser console messages array. + * @internal + */ +export const [getConsoleMessagesFromContext] = useContext([]); diff --git a/src/context/jsError.ts b/src/context/jsError.ts new file mode 100644 index 00000000..3ce3a967 --- /dev/null +++ b/src/context/jsError.ts @@ -0,0 +1,7 @@ +import {useContext} from '../useContext'; + +/** + * Get browser JS errors array. + * @internal + */ +export const [getJsErrorsFromContext] = useContext([]); diff --git a/src/types/config/config.ts b/src/types/config/config.ts index b50e2a7d..c4f3f285 100644 --- a/src/types/config/config.ts +++ b/src/types/config/config.ts @@ -23,6 +23,11 @@ type UserlandTestCafeConfig = Readonly<{ selectorTimeout: number; }>; +/** + * Supported browsers. + */ +export type BrowserName = 'chromium' | 'firefox' | 'webkit'; + /** * The complete pack configuration object without `doBeforePack` field. */ diff --git a/src/types/config/index.ts b/src/types/config/index.ts index 8596a347..634f88a6 100644 --- a/src/types/config/index.ts +++ b/src/types/config/index.ts @@ -1,4 +1,4 @@ -export type {FullPackConfigWithoutDoBeforePack, UserlandPack} from './config'; +export type {BrowserName, FullPackConfigWithoutDoBeforePack, UserlandPack} from './config'; export type { AnyPack, AnyPackParameters, diff --git a/src/types/config/ownE2edConfig.ts b/src/types/config/ownE2edConfig.ts index fb2d0acb..02f7e0ed 100644 --- a/src/types/config/ownE2edConfig.ts +++ b/src/types/config/ownE2edConfig.ts @@ -13,6 +13,8 @@ import type { TestMetaPlaceholder, } from '../userland'; +import type {BrowserName} from './config'; + /** * Own e2ed pack config properties without `doBeforepack` properties. */ @@ -23,20 +25,25 @@ export type OwnE2edConfig< TestMeta = TestMetaPlaceholder, > = Readonly<{ /** - * Browser name as a command to launch it (like `chrome`, `chromium`, `firefox`, `webkit`, etc). + * Array of browser flags, like `--disable-dev-shm-usage`, with which the browser is launched to run tests. */ - browser: string; + browserFlags: readonly string[]; /** - * Array of browser flags, like `--disable-dev-shm-usage`, with which the browser is launched to run tests. + * Browser name (one of `chromium`, `firefox`, `webkit`). */ - browserFlags: readonly string[]; + browserName: BrowserName; /** * Custom pack properties for using in hooks, etc. */ customPackProperties: CustomPackProperties; + /** + * Device scale factor (aka `window.devicePixelRatio`); + */ + deviceScaleFactor: number; + /** * An array of functions that will be executed, in order, after the pack completes. * The functions accept a lite report object, and can return custom report properties, @@ -150,11 +157,6 @@ export type OwnE2edConfig< */ overriddenConfigFields: PlaywrightTestConfig | null; - /** - * If not `null`, then this value will override the browser's user agent in tests. - */ - overriddenUserAgent: string | null; - /** * Timeout (in millisecond) for the entire pack of tests (tasks). * If the test pack takes longer than this timeout, the pack will fail with the appropriate error. @@ -234,6 +236,11 @@ export type OwnE2edConfig< */ testTimeout: number; + /** + * `userAgent` string of browser (device) in tests. + */ + userAgent: string; + /** * Height of viewport of page in pixels. */ diff --git a/src/types/console.ts b/src/types/console.ts new file mode 100644 index 00000000..a8652e2d --- /dev/null +++ b/src/types/console.ts @@ -0,0 +1,34 @@ +import type {ConsoleMessage as PlaywrightConsoleMessage} from '@playwright/test'; + +/** + * Object of one console message. + */ +export type ConsoleMessage = Readonly<{ + args: readonly unknown[]; + location: Readonly; + text: string; + type: ConsoleMessageType; +}>; + +/** + * Type console message. + */ +export type ConsoleMessageType = + | 'assert' + | 'clear' + | 'count' + | 'debug' + | 'dir' + | 'dirxml' + | 'endGroup' + | 'error' + | 'info' + | 'log' + | 'profile' + | 'profileEnd' + | 'startGroup' + | 'startGroupCollapsed' + | 'table' + | 'timeEnd' + | 'trace' + | 'warning'; diff --git a/src/types/errors.ts b/src/types/errors.ts index 993962e5..1720fdd7 100644 --- a/src/types/errors.ts +++ b/src/types/errors.ts @@ -1,11 +1,6 @@ import type {LogParams} from './log'; import type {RunLabel} from './runLabel'; -/** - * Browser's JS-error from TestCafe. - */ -export type BrowserJsError = Readonly<{message: string}>; - /** * Printed fields of `E2edError` instances for `toJSON`, `toString` and `inspect.custom` methods. */ diff --git a/src/types/index.ts b/src/types/index.ts index 440eb505..7501a2f4 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -3,9 +3,11 @@ export type {Brand, IsBrand} from './brand'; export type {Expect, IsEqual, IsReadonlyKey} from './checks'; export type {Class} from './class'; export type {ClientFunction} from './clientFunction'; +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 {BrowserJsError, E2edPrintedFields} from './errors'; +export type {E2edPrintedFields} from './errors'; export type {LogEvent, Onlog, TestRunEvent} from './events'; export type {Fn, MergeFunctions} from './fn'; export type { @@ -52,11 +54,6 @@ export type { PropertyKey, } from './properties'; export type {LiteReport, LiteRetry} from './report'; -export type { - RequestHookConfigureResponseEvent, - RequestHookRequestEvent, - RequestHookResponseEvent, -} from './requestHooks'; export type {ApiRouteClassType, ApiRouteClassTypeWithGetParamsFromUrl} from './routes'; export type { CreateSelector, diff --git a/src/types/internal.ts b/src/types/internal.ts index d79d22b8..628a6cef 100644 --- a/src/types/internal.ts +++ b/src/types/internal.ts @@ -9,16 +9,18 @@ export type {ClientFunction} from './clientFunction'; export type {ClientFunctionWrapperResult, MaybeTestCafeError} from './clientFunction'; export type { AnyPack, + BrowserName, FullPackConfig, FullPackConfigWithoutDoBeforePack, GetPackParameters, UserlandPack, } from './config'; +export type {ConsoleMessage, ConsoleMessageType} from './console'; export type {UtcTimeInMs} from './date'; export type {DeepMutable, DeepPartial, DeepReadonly, DeepRequired} from './deep'; /** @internal */ export type {E2edEnvironment} from './environment'; -export type {BrowserJsError, E2edPrintedFields} from './errors'; +export type {E2edPrintedFields} from './errors'; /** @internal */ export type {MaybeWithIsTestRunBroken} from './errors'; export type {LogEvent, Onlog, TestRunEvent} from './events'; @@ -103,15 +105,6 @@ export type { RetryProps, TestRunButtonProps, } from './report'; -export type { - RequestHookConfigureResponseEvent, - RequestHookContextId, - RequestHookRequestEvent, - RequestHookResponseEvent, - RequestOptions, -} from './requestHooks'; -/** @internal */ -export type {RequestHookClassWithContext, RequestHookEncoding} from './requestHooks'; /** @internal */ export type {RetriesState, RunRetryOptions, VisitedTestNamesHash} from './retries'; export type {ApiRouteClassType, ApiRouteClassTypeWithGetParamsFromUrl} from './routes'; @@ -180,6 +173,7 @@ export type { /** * @internal */ export type { AllRequestsCompletePredicateWithPromise, + RequestHookContextId, RequestOrResponsePredicateWithPromise, RequestPredicateWithPromise, ResponsePredicateWithPromise, diff --git a/src/types/requestHooks.ts b/src/types/requestHooks.ts deleted file mode 100644 index e1a45c01..00000000 --- a/src/types/requestHooks.ts +++ /dev/null @@ -1,86 +0,0 @@ -import type {REQUEST_HOOK_CONTEXT_ID_KEY, REQUEST_HOOK_CONTEXT_KEY} from '../constants/internal'; - -import type {Brand} from './brand'; -import type {Class} from './class'; -import type {DeepReadonly} from './deep'; -import type {Fn} from './fn'; -import type {HeaderEntry, Headers, StatusCode} from './http'; - -/** - * Maybe object with request hook context key. - */ -type MaybeWithContextKey = - | {readonly [REQUEST_HOOK_CONTEXT_KEY]?: RequestHookContext} - | null - | undefined; - -/** - * TestCafe internal request hook context (BaseRequestHookEventFactory). - * Here we describe only the specific fields used. - * Only the version without native automation has field `_ctx`, - * and only the version with native automation (CPD) has field `_event`. - * {@link https://github.com/DevExpress/testcafe-hammerhead/blob/master/src/request-pipeline/request-hooks/events/factory/index.ts} - * {@link https://github.com/DevExpress/testcafe/blob/master/src/native-automation/request-hooks/event-factory/request-paused-event-based.ts} - * {@link https://github.com/DevExpress/testcafe/blob/master/src/native-automation/request-hooks/event-factory/frame-navigated-event-based.ts} - */ -type RequestHookContext = Readonly<{ - [REQUEST_HOOK_CONTEXT_ID_KEY]?: RequestHookContextId; - _ctx?: Readonly<{destRes?: Readonly<{headers?: Headers}>}>; - _event?: Readonly<{requestId: string; responseHeaders?: readonly HeaderEntry[]}>; - headersModified?: boolean; -}>; - -/** - * TestCafe charset class instance for encode/decode request/response body buffers. - * @internal - */ -// eslint-disable-next-line import/no-unused-modules -export type RequestHookCharset = Brand & Readonly<{charset: string}>; - -/** - * Any internal TestCafe request hook class with request hook context. - * @internal - */ -export type RequestHookClassWithContext = Class< - unknown[], - RequestHookContext & Readonly>> ->; - -/** - * id of TestCafe's request hook context. - */ -export type RequestHookContextId = Brand; - -/** - * Encoding for encode/decode request/response body buffers. - * @internal - */ -export type RequestHookEncoding = Brand; - -/** - * TestCafe internal request event in RequestHook. - */ -export type RequestHookRequestEvent = DeepReadonly<{ - _requestInfo?: {requestId?: string}; - requestOptions: RequestOptions; -}>; - -/** - * TestCafe internal configure response event in RequestHook. - */ -export type RequestHookConfigureResponseEvent = MaybeWithContextKey & {}; - -/** - * TestCafe internal response event in RequestHook. - */ -export type RequestHookResponseEvent = Readonly<{ - body?: Buffer; - headers?: Headers; - requestId?: string; - statusCode?: StatusCode; -}>; - -/** - * TestCafe internal request options with request hook context. - */ -export type RequestOptions = MaybeWithContextKey; diff --git a/src/types/waitForEvents.ts b/src/types/waitForEvents.ts index ec24b8db..e5b1f92c 100644 --- a/src/types/waitForEvents.ts +++ b/src/types/waitForEvents.ts @@ -1,8 +1,8 @@ +import type {Brand} from './brand'; import type {UtcTimeInMs} from './date'; import type {MergeFunctions} from './fn'; import type {Request, RequestWithUtcTimeInMs, Response, ResponseWithRequest, Url} from './http'; import type {MaybePromise} from './promise'; -import type {RequestHookContextId} from './requestHooks'; /** * Request or response predicate for both event handlers. @@ -31,6 +31,12 @@ export type AllRequestsCompletePredicateWithPromise = Readonly<{ setResolveTimeout: () => void; }>; +/** + * Request hook context id. + * @internal + */ +export type RequestHookContextId = Brand; + /** * Request predicate for `waitForRequest` function. */ diff --git a/src/utils/events/registerEndTestRunEvent.ts b/src/utils/events/registerEndTestRunEvent.ts index 6429d5d6..e5e433e0 100644 --- a/src/utils/events/registerEndTestRunEvent.ts +++ b/src/utils/events/registerEndTestRunEvent.ts @@ -57,7 +57,7 @@ export const registerEndTestRunEvent = async (endTestRunEvent: EndTestRunEvent): if (fullMocksState !== undefined && fullMocksState.appliedMocks === undefined) { await getTimeoutPromise(delayForWritingFullMocksInMs); - await writeFullMocks(fullMocksState).catch((error: unknown) => { + await writeFullMocks(fullMocksState, name, filePath).catch((error: unknown) => { generalLog('Cannot write "full mocks" for test', { endTestRunEvent, error, diff --git a/src/utils/events/writeFullMocks.ts b/src/utils/events/writeFullMocks.ts index f29f3c00..2fa31927 100644 --- a/src/utils/events/writeFullMocks.ts +++ b/src/utils/events/writeFullMocks.ts @@ -2,13 +2,17 @@ import {assertValueIsNotNull} from '../asserts'; import {getFullPackConfig} from '../config'; import {generalLog} from '../generalLog'; -import type {FullMocksState} from '../../types/internal'; +import type {FullMocksState, TestFilePath} from '../../types/internal'; /** * Writes full mocks of one test. * @internal */ -export const writeFullMocks = async (fullMocksState: FullMocksState): Promise => { +export const writeFullMocks = async ( + fullMocksState: FullMocksState, + name: string, + filePath: TestFilePath, +): Promise => { const {fullMocks: fullMocksConfig} = getFullPackConfig(); assertValueIsNotNull(fullMocksConfig, 'fullMocksConfig is not null'); @@ -16,6 +20,8 @@ export const writeFullMocks = async (fullMocksState: FullMocksState): Promise [key, value.length]), ), diff --git a/src/utils/test/afterErrorInTest.ts b/src/utils/test/afterErrorInTest.ts index 082d114c..69c1e50a 100644 --- a/src/utils/test/afterErrorInTest.ts +++ b/src/utils/test/afterErrorInTest.ts @@ -18,9 +18,9 @@ const afterErrorInTestTimeoutInMs = 15_000; export const afterErrorInTest = (testStaticOptions: TestStaticOptions): Promise => addTimeoutToPromise( (async () => { - await getBrowserConsoleMessages({showMessagesInLog: true}); + getBrowserConsoleMessages({showMessagesInLog: true}); - await getBrowserJsErrors({showErrorsInLog: true}); + getBrowserJsErrors({showErrorsInLog: true}); await takeScreenshotsOnErrorIfNeeded(testStaticOptions); })(), diff --git a/src/utils/test/afterTest.ts b/src/utils/test/afterTest.ts index dbbb83b9..065d13ce 100644 --- a/src/utils/test/afterTest.ts +++ b/src/utils/test/afterTest.ts @@ -3,17 +3,21 @@ import {generalLog, writeLogsToFile} from '../generalLog'; import type {EndTestRunEvent, UtcTimeInMs} from '../../types/internal'; -type Options = Omit; +type Options = Readonly<{clearPage: (() => Promise) | undefined}> & + Omit; /** * Internal after test hook. * @internal */ export const afterTest = async (options: Options): Promise => { + const {clearPage, ...optionsWithoutClearPage} = options; const utcTimeInMs = Date.now() as UtcTimeInMs; - const endTestRunEvent: EndTestRunEvent = {...options, utcTimeInMs}; + const endTestRunEvent: EndTestRunEvent = {...optionsWithoutClearPage, utcTimeInMs}; try { + await clearPage?.(); + await registerEndTestRunEvent(endTestRunEvent); } catch (error) { generalLog('Caught an error when register end test run event', {endTestRunEvent, error}); diff --git a/src/utils/test/getRunTest.ts b/src/utils/test/getRunTest.ts index 81d954a6..0eca875a 100644 --- a/src/utils/test/getRunTest.ts +++ b/src/utils/test/getRunTest.ts @@ -28,6 +28,7 @@ export const getRunTest = const retryIndex = testInfo.retry + 1; const runId = createRunId(); + let clearPage: (() => Promise) | undefined; let hasRunError = false; let shouldRunTest = false; let testStaticOptions: TestStaticOptions | undefined; @@ -42,7 +43,7 @@ export const getRunTest = return; } - await preparePage(page); + clearPage = await preparePage(page); beforeTest({retryIndex, runId, testFn: test.testFn, testStaticOptions}); @@ -60,7 +61,7 @@ export const getRunTest = throw error; } finally { if (shouldRunTest) { - await afterTest({hasRunError, runId, unknownRunError}); + await afterTest({clearPage, hasRunError, runId, unknownRunError}); } } }; diff --git a/src/utils/test/preparePage.ts b/src/utils/test/preparePage.ts index 2d6a8a3d..dd426dd4 100644 --- a/src/utils/test/preparePage.ts +++ b/src/utils/test/preparePage.ts @@ -1,12 +1,21 @@ import {AsyncLocalStorage} from 'node:async_hooks'; +import {getConsoleMessagesFromContext} from '../../context/consoleMessages'; import {getIsPageNavigatingNow, setIsPageNavigatingNow} from '../../context/isPageNavigatingNow'; +import {getJsErrorsFromContext} from '../../context/jsError'; import {getNavigationDelay} from '../../context/navigationDelay'; import {getOnResponseCallbacks} from '../../context/onResponseCallbacks'; import {getResponseFromPlaywrightResponse} from '../requestHooks'; -import type {Page} from '@playwright/test'; +import type { + ConsoleMessage as PlaywrightConsoleMessage, + Page, + Request as PlaywrightRequest, + Response as PlaywrightResponse, +} from '@playwright/test'; + +import type {ConsoleMessage, ConsoleMessageType} from '../../types/internal'; const afterNavigationRequestsDelayInMs = 300; @@ -14,7 +23,9 @@ const afterNavigationRequestsDelayInMs = 300; * Prepares page before test. * @internal */ -export const preparePage = async (page: Page): Promise => { +export const preparePage = async (page: Page): Promise<() => Promise> => { + const consoleMessages = getConsoleMessagesFromContext() as ConsoleMessage[]; + const jsErrors = getJsErrorsFromContext() as Error[]; const navigationDelay = getNavigationDelay(); await page.route( @@ -33,65 +44,89 @@ export const preparePage = async (page: Page): Promise => { let navigationRequestsCount = 0; const skippedRequestUrls = Object.create(null) as Record; - page.on( - 'request', - AsyncLocalStorage.bind((newRequest) => { - const isNavigationRequest = newRequest.isNavigationRequest(); - const isPageNavigatingNow = getIsPageNavigatingNow(); + const consoleListener = AsyncLocalStorage.bind(async (message: PlaywrightConsoleMessage) => { + const args: unknown[] = []; + const location = message.location(); + const text = message.text(); + const type = message.type() as ConsoleMessageType; - if (isPageNavigatingNow) { - skippedRequestUrls[newRequest.url()] = true; - } + consoleMessages.push({args, location, text, type}); + + for (const jsHandle of message.args()) { + args.push(await jsHandle.jsonValue().catch(() => 'Error with getting value of argument')); + } + }); + + const pageerrorListener = AsyncLocalStorage.bind((error: Error) => { + jsErrors.push(error); + }); + + const requestListener = AsyncLocalStorage.bind((newRequest: PlaywrightRequest) => { + const isNavigationRequest = newRequest.isNavigationRequest(); + const isPageNavigatingNow = getIsPageNavigatingNow(); + + if (isPageNavigatingNow) { + skippedRequestUrls[newRequest.url()] = true; + } + + if (isNavigationRequest) { + navigationRequestsCount += 1; + + setIsPageNavigatingNow(navigationRequestsCount > 0); + } + }); - if (isNavigationRequest) { - navigationRequestsCount += 1; + const responseListener = AsyncLocalStorage.bind((newResponse: PlaywrightResponse) => { + const isNavigationRequest = newResponse.request().isNavigationRequest(); + + if (isNavigationRequest) { + setTimeout(() => { + navigationRequestsCount -= 1; setIsPageNavigatingNow(navigationRequestsCount > 0); - } - }), - ); + }, afterNavigationRequestsDelayInMs); + } + }); - page.on( - 'response', - AsyncLocalStorage.bind((newResponse) => { - const isNavigationRequest = newResponse.request().isNavigationRequest(); + const requestfinishedListener = AsyncLocalStorage.bind(async (request: PlaywrightRequest) => { + const onResponseCallbacks = getOnResponseCallbacks(); - if (isNavigationRequest) { - setTimeout(() => { - navigationRequestsCount -= 1; + if (onResponseCallbacks.length === 0) { + return; + } - setIsPageNavigatingNow(navigationRequestsCount > 0); - }, afterNavigationRequestsDelayInMs); + const playwrightResponse = await request.response().catch((error) => { + if (String(error).includes('Target page, context or browser has been closed')) { + return null; } - }), - ); - page.on( - 'requestfinished', - AsyncLocalStorage.bind(async (request) => { - const onResponseCallbacks = getOnResponseCallbacks(); + throw error; + }); - if (onResponseCallbacks.length === 0) { - return; - } + if (playwrightResponse === null) { + return; + } - const playwrightResponse = await request.response().catch((error) => { - if (String(error).includes('Target page, context or browser has been closed')) { - return null; - } + const responseWithRequest = await getResponseFromPlaywrightResponse(playwrightResponse); - throw error; - }); + for (const callback of onResponseCallbacks) { + callback(responseWithRequest); + } + }); - if (playwrightResponse === null) { - return; - } + page.on('console', consoleListener); + page.on('pageerror', pageerrorListener); + page.on('request', requestListener); + page.on('response', responseListener); + page.on('requestfinished', requestfinishedListener); - const responseWithRequest = await getResponseFromPlaywrightResponse(playwrightResponse); + return async () => { + page.removeListener('console', consoleListener); + page.removeListener('pageerror', pageerrorListener); + page.removeListener('request', requestListener); + page.removeListener('response', responseListener); + page.removeListener('requestfinished', requestfinishedListener); - for (const callback of onResponseCallbacks) { - callback(responseWithRequest); - } - }), - ); + await page.unrouteAll({behavior: 'ignoreErrors'}); + }; };