diff --git a/.eslintrc.yaml b/.eslintrc.yaml index cd1a5283..e4a94bbf 100644 --- a/.eslintrc.yaml +++ b/.eslintrc.yaml @@ -163,7 +163,7 @@ rules: '@typescript-eslint/class-literal-property-style': error '@typescript-eslint/consistent-generic-constructors': error '@typescript-eslint/consistent-indexed-object-style': error - '@typescript-eslint/consistent-return': error + '@typescript-eslint/consistent-return': off '@typescript-eslint/consistent-type-assertions': [error, {assertionStyle: as, objectLiteralTypeAssertions: never}] '@typescript-eslint/consistent-type-definitions': [error, type] diff --git a/README.md b/README.md index 2261834c..b3fbf68b 100644 --- a/README.md +++ b/README.md @@ -369,6 +369,9 @@ This parameter can be overridden in the test-specific options. `viewportWidth: number`: width of viewport of page in pixels. +`waitBeforeRetry: (options: Options) => number`: returns how many milliseconds `e2ed` +should wait before running test (for retries). + `waitForAllRequestsComplete.maxIntervalBetweenRequestsInMs: number`: default maximum interval (in milliseconds) between requests for `waitForAllRequestsComplete` function. If there are no new requests for more than this interval, then the promise diff --git a/autotests/configurator/index.ts b/autotests/configurator/index.ts index dbdbcc52..376e5756 100644 --- a/autotests/configurator/index.ts +++ b/autotests/configurator/index.ts @@ -27,4 +27,5 @@ export type { SkipTests, TestFunction, TestMeta, + WaitBeforeRetry, } from './types'; diff --git a/autotests/configurator/mapLogPayloadInConsole.ts b/autotests/configurator/mapLogPayloadInConsole.ts index b9bbce55..fce15437 100644 --- a/autotests/configurator/mapLogPayloadInConsole.ts +++ b/autotests/configurator/mapLogPayloadInConsole.ts @@ -19,7 +19,6 @@ export const mapLogPayloadInConsole: MapLogPayloadInConsole = (message, payload) if ( message.startsWith('Caught an error when running tests in retry') || - message.startsWith('Warning from TestCafe:') || message.startsWith('Usage:') ) { return payload; diff --git a/autotests/configurator/types/index.ts b/autotests/configurator/types/index.ts index 29aecba9..2d0dbdc3 100644 --- a/autotests/configurator/types/index.ts +++ b/autotests/configurator/types/index.ts @@ -16,6 +16,7 @@ export type { MapLogPayloadInReport, Pack, TestFunction, + WaitBeforeRetry, } from './packSpecific'; export type {SkipTests} from './skipTests'; export type {TestMeta} from './testMeta'; diff --git a/autotests/configurator/types/packSpecific.ts b/autotests/configurator/types/packSpecific.ts index c4ccc859..4ed96353 100644 --- a/autotests/configurator/types/packSpecific.ts +++ b/autotests/configurator/types/packSpecific.ts @@ -23,4 +23,5 @@ export type MapLogPayloadInConsole = PackSpecificTypes['MapLogPayloadInConsole'] export type MapLogPayloadInLogFile = PackSpecificTypes['MapLogPayloadInLogFile']; export type MapLogPayloadInReport = PackSpecificTypes['MapLogPayloadInReport']; export type TestFunction = PackSpecificTypes['TestFunction']; +export type WaitBeforeRetry = PackSpecificTypes['WaitBeforeRetry']; export type {Pack}; diff --git a/autotests/fixtures/fullMocks/jKDXNUZ75U.json b/autotests/fixtures/fullMocks/jKDXNUZ75U.json deleted file mode 100644 index 184ef5ee..00000000 --- a/autotests/fixtures/fullMocks/jKDXNUZ75U.json +++ /dev/null @@ -1 +0,0 @@ -{"/api/product/135865":[{"completionTimeInMs":1721837017221,"duration":"4ms","request":{"method":"POST","query":{"size":"13"},"requestBody":{"cookies":[],"input":17,"model":"samsung","version":"12"},"requestHeaders":{"accept":"*/*","accept-language":"en-US","content-type":"application/json; charset=UTF-8","origin":"https://joomcode.github.io","referer":"https://joomcode.github.io/","user-agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.17 Safari/537.36","sec-ch-ua":"\"Not)A;Brand\";v=\"99\", \"HeadlessChrome\";v=\"127\", \"Chromium\";v=\"127\"","sec-ch-ua-mobile":"?0","sec-ch-ua-platform":"\"Windows\""},"url":"https://reqres.in/api/product/135865?size=13","utcTimeInMs":1721837017217},"responseBody":{"id":135865,"method":"POST","output":"17","payload":{"id":"135865","cookies":[],"input":17,"model":"samsung","version":"12"},"query":{"size":"13"},"url":"https://reqres.in/api/product/135865?size=13"},"responseHeaders":{"access-control-allow-origin":"https://joomcode.github.io","access-control-allow-credentials":"true","content-length":"201","vary":"Origin","content-type":"application/json; charset=UTF-8"},"statusCode":200},{"completionTimeInMs":1721837017604,"duration":"2ms","request":{"method":"POST","query":{"size":"13"},"requestBody":{"cookies":[],"input":17,"model":"samsung","version":"12"},"requestHeaders":{"accept":"*/*","accept-language":"en-US","content-type":"application/json; charset=UTF-8","origin":"https://joomcode.github.io","referer":"https://joomcode.github.io/","user-agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.17 Safari/537.36","sec-ch-ua":"\"Not)A;Brand\";v=\"99\", \"HeadlessChrome\";v=\"127\", \"Chromium\";v=\"127\"","sec-ch-ua-mobile":"?0","sec-ch-ua-platform":"\"Windows\""},"url":"https://reqres.in/api/product/135865?size=13","utcTimeInMs":1721837017602},"responseBody":{"cookies":[],"input":17,"model":"samsung","version":"12","id":"246","createdAt":"2024-07-24T16:03:37.444Z"},"responseHeaders":{"date":"Wed, 24 Jul 2024 16:03:37 GMT","via":"1.1 vegur","cf-cache-status":"DYNAMIC","nel":"{\"report_to\":\"heroku-nel\",\"max_age\":3600,\"success_fraction\":0.005,\"failure_fraction\":0.05,\"response_headers\":[\"Via\"]}","server":"cloudflare","x-powered-by":"Express","etag":"W/\"6c-u6X0/XoH52vavslyTQkfjMGy8UI\"","report-to":"{\"group\":\"heroku-nel\",\"max_age\":3600,\"endpoints\":[{\"url\":\"https://nel.heroku.com/reports?ts=1721837017&sid=c4c9725f-1ab0-44d8-820f-430df2718e11&s=QtldDuafqeEmbMdmRCPQibdEu5m%2BGzC8BP8Dd%2F%2FFv0A%3D\"}]}","content-type":"application/json; charset=utf-8","access-control-allow-origin":"*","cf-ray":"8a85242e4d4c0d52-ARN","content-length":"108","reporting-endpoints":"heroku-nel=https://nel.heroku.com/reports?ts=1721837017&sid=c4c9725f-1ab0-44d8-820f-430df2718e11&s=QtldDuafqeEmbMdmRCPQibdEu5m%2BGzC8BP8Dd%2F%2FFv0A%3D"},"statusCode":201}]} \ No newline at end of file diff --git a/autotests/fixtures/fullMocks/mr-iHTD7Lp.json b/autotests/fixtures/fullMocks/mr-iHTD7Lp.json new file mode 100644 index 00000000..a197eb8a --- /dev/null +++ b/autotests/fixtures/fullMocks/mr-iHTD7Lp.json @@ -0,0 +1 @@ +{"/api/product/135865":[{"completionTimeInMs":1729850053899,"duration":"4ms","request":{"method":"POST","query":{"size":"13"},"requestBody":{"cookies":[],"input":17,"model":"samsung","version":"12"},"requestHeaders":{"accept":"*/*","accept-language":"en-US","content-type":"application/json; charset=UTF-8","referer":"https://joomcode.github.io/","user-agent":"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.35 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.35","sec-ch-ua":"\"Chromium\";v=\"130\", \"HeadlessChrome\";v=\"130\", \"Not?A_Brand\";v=\"99\"","sec-ch-ua-mobile":"?0","sec-ch-ua-platform":"\"Linux\""},"url":"https://reqres.in/api/product/135865?size=13","utcTimeInMs":1729850053895},"responseBody":{"id":135865,"method":"POST","output":"17","payload":{"id":"135865","cookies":[],"input":17,"model":"samsung","version":"12"},"query":{"size":"13"},"url":"https://reqres.in/api/product/135865?size=13"},"responseHeaders":{"content-type":"application/json; charset=UTF-8","content-length":"201"},"statusCode":200},{"completionTimeInMs":1729850054453,"duration":"4ms","request":{"method":"POST","query":{"size":"13"},"requestBody":{"cookies":[],"input":17,"model":"samsung","version":"12"},"requestHeaders":{"accept":"*/*","accept-language":"en-US","content-type":"application/json; charset=UTF-8","referer":"https://joomcode.github.io/","user-agent":"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.35 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.35","sec-ch-ua":"\"Chromium\";v=\"130\", \"HeadlessChrome\";v=\"130\", \"Not?A_Brand\";v=\"99\"","sec-ch-ua-mobile":"?0","sec-ch-ua-platform":"\"Linux\""},"url":"https://reqres.in/api/product/135865?size=13","utcTimeInMs":1729850054449},"responseBody":{"cookies":[],"input":17,"model":"samsung","version":"12","id":"816","createdAt":"2024-10-25T09:54:14.388Z"},"responseHeaders":{"reporting-endpoints":"heroku-nel=https://nel.heroku.com/reports?ts=1729850054&sid=c4c9725f-1ab0-44d8-820f-430df2718e11&s=LZBdsiG2rgFml4T6awFYmhftTlXLyvdfcMob39wiK2I%3D","nel":"{\"report_to\":\"heroku-nel\",\"max_age\":3600,\"success_fraction\":0.005,\"failure_fraction\":0.05,\"response_headers\":[\"Via\"]}","cf-cache-status":"DYNAMIC","etag":"W/\"6c-uS8VtSQALUKVvQbVlIe2ZwBwLRE\"","report-to":"{\"group\":\"heroku-nel\",\"max_age\":3600,\"endpoints\":[{\"url\":\"https://nel.heroku.com/reports?ts=1729850054&sid=c4c9725f-1ab0-44d8-820f-430df2718e11&s=LZBdsiG2rgFml4T6awFYmhftTlXLyvdfcMob39wiK2I%3D\"}]}","via":"1.1 vegur","cf-ray":"8d8152f7a8d43cff-CDG","access-control-allow-origin":"*","content-length":"108","date":"Fri, 25 Oct 2024 09:54:14 GMT","content-type":"application/json; charset=utf-8","x-powered-by":"Express","server":"cloudflare"},"statusCode":201}]} \ No newline at end of file diff --git a/autotests/packs/allTests.ts b/autotests/packs/allTests.ts index b926c23b..eefbc47e 100644 --- a/autotests/packs/allTests.ts +++ b/autotests/packs/allTests.ts @@ -78,10 +78,11 @@ export const pack: Pack = { takeViewportScreenshotOnError: true, testFileGlobs: ['**/autotests/tests/**/*.ts'], testIdleTimeout: 8_000, - testTimeout: 60_000, + testTimeout: 15_000, userAgent, viewportHeight: 1080, viewportWidth: 1920, + waitBeforeRetry: () => 0, waitForAllRequestsComplete: { maxIntervalBetweenRequestsInMs: 500, timeout: 30_000, diff --git a/package.json b/package.json index 7a182a66..103d11de 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,10 @@ { "name": "e2ed", "version": "0.18.15", - "description": "E2E testing framework over TestCafe", + "description": "E2E testing framework over Playwright", "keywords": [ "E2E", - "TestCafe", + "Playwright", "testing" ], "author": "uid11", diff --git a/src/Page.ts b/src/Page.ts index 9c072029..3c60f64a 100644 --- a/src/Page.ts +++ b/src/Page.ts @@ -80,8 +80,9 @@ export abstract class Page { * Asserts that we are on the expected page by `isMatch` flage. * `isMatch` equals `true`, if url matches the page with given parameters, and `false` otherwise. */ - assertPage(isMatch: boolean): AsyncVoid { + assertPage(isMatch: boolean, documentUrl: Url): AsyncVoid { assertValueIsTrue(isMatch, `the document url matches the page "${this.constructor.name}"`, { + documentUrl, page: this, }); } diff --git a/src/actions/pages/assertPage.ts b/src/actions/pages/assertPage.ts index db9903b8..9e1999c6 100644 --- a/src/actions/pages/assertPage.ts +++ b/src/actions/pages/assertPage.ts @@ -34,7 +34,7 @@ export const assertPage = async ( LogEventType.InternalAction, ); - await page.assertPage(isMatch); + await page.assertPage(isMatch, documentUrl); await page.afterAssertPage?.(); diff --git a/src/actions/pages/navigateToPage.ts b/src/actions/pages/navigateToPage.ts index 02f02b7c..06aa1159 100644 --- a/src/actions/pages/navigateToPage.ts +++ b/src/actions/pages/navigateToPage.ts @@ -46,7 +46,7 @@ export const navigateToPage = async ( const documentUrl = await getDocumentUrl(); const isMatch = route.isMatchUrl(documentUrl); - await page.assertPage(isMatch); + await page.assertPage(isMatch, documentUrl); await page.afterAssertPage?.(); diff --git a/src/actions/pressKey.ts b/src/actions/pressKey.ts index ef4ea94e..4e9c694c 100644 --- a/src/actions/pressKey.ts +++ b/src/actions/pressKey.ts @@ -1,20 +1,73 @@ import {LogEventType} from '../constants/internal'; import {getPlaywrightPage} from '../useContext'; +import {E2edError} from '../utils/error'; import {log} from '../utils/log'; +import {getDescriptionFromSelector} from '../utils/selectors'; -import type {Keyboard} from '@playwright/test'; +import type {KeyboardPressKey, Selector} from '../types/internal'; -import type {KeyboardPressKey} from '../types/internal'; +type Options = Readonly<{delay?: number; timeout?: number}>; -type Options = Parameters[1]; +type PressKey = (( + this: void, + selector: Selector, + key: KeyboardPressKey, + options?: Options, +) => Promise) & + ((this: void, key: KeyboardPressKey, options?: Options) => Promise); /** * Presses the specified keyboard keys. */ -export const pressKey = async (key: KeyboardPressKey, options: Options = {}): Promise => { - log(`Press keyboard key: "${key}"`, options, LogEventType.InternalAction); +export const pressKey: PressKey = async ( + keyOrSelector: KeyboardPressKey | Selector, + keyOrOptions?: KeyboardPressKey | Options, + maybeOptions?: Options, +): Promise => { + let key: KeyboardPressKey; + let selector: Selector | undefined; + let options: Options; + + if (typeof keyOrSelector === 'string') { + key = keyOrSelector; + + if (typeof keyOrOptions === 'string') { + throw new E2edError('keyOrOptions is string', { + keyOrOptions, + keyOrSelector, + maybeOptions, + }); + } + + options = keyOrOptions ?? {}; + } else { + selector = keyOrSelector; + + if (typeof keyOrOptions !== 'string') { + throw new E2edError('keyOrOptions is not string', { + keyOrOptions, + keyOrSelector, + maybeOptions, + }); + } + + key = keyOrOptions; + + options = maybeOptions ?? {}; + } + + const withDescription = + selector !== undefined + ? ` on element with description ${getDescriptionFromSelector(selector)}` + : ''; + + log(`Press keyboard key${withDescription}: "${key}"`, options, LogEventType.InternalAction); const page = getPlaywrightPage(); - await page.keyboard.press(key, options); + if (selector !== undefined) { + await selector.getPlaywrightLocator().press(key, options); + } else { + await page.keyboard.press(key, options); + } }; diff --git a/src/constants/testRun.ts b/src/constants/testRun.ts index 4f867d47..25ba3cbf 100644 --- a/src/constants/testRun.ts +++ b/src/constants/testRun.ts @@ -13,8 +13,8 @@ declare type TestRunTypesChecks = [ /** * Main status of test run. - * Failed if it have run error and passed if not. - * Broken if the test failed and TestCafe restarted it themself. + * `Failed` if it have run error and passed if not. + * Probably should never be `Broken`. */ export const enum TestRunStatus { Broken = 'broken', diff --git a/src/generators/createRunId.ts b/src/generators/createRunId.ts index c8b4df76..d879fe90 100644 --- a/src/generators/createRunId.ts +++ b/src/generators/createRunId.ts @@ -1,9 +1,21 @@ -import {getRandomId} from './getRandomId'; +import {createHash} from 'node:crypto'; -import type {RunId} from '../types/internal'; +import type {RunId, Test} from '../types/internal'; + +const runIdBaseLength = 10; /** * Creates new RunId for TestRun. * @internal */ -export const createRunId = (): RunId => getRandomId().replace(/:/g, '-') as RunId; +export const createRunId = (test: Test, retryIndex: number): RunId => { + const data = {...test, testFn: test.testFn.toString()}; + const text = JSON.stringify(data); + const hash = createHash('sha1'); + + hash.update(text); + + const base = hash.digest('base64url').slice(0, runIdBaseLength); + + return `${base}-${retryIndex}` as RunId; +}; diff --git a/src/types/clientFunction.ts b/src/types/clientFunction.ts index 48570d3c..2dbb9d66 100644 --- a/src/types/clientFunction.ts +++ b/src/types/clientFunction.ts @@ -7,24 +7,3 @@ export type ClientFunction this: void, ...args: Args ) => Promise; - -/** - * Result of the internal client function wrapper (object with error as string or with value). - * @internal - */ -export type ClientFunctionWrapperResult = Readonly< - | { - errorMessage: string; - result: undefined; - } - | { - errorMessage: undefined; - result: Result; - } ->; - -/** - * Internal TestCafe error object or undefined. - * @internal - */ -export type MaybeTestCafeError = {code?: string} | undefined; diff --git a/src/types/config/config.ts b/src/types/config/config.ts index 2b425375..10592056 100644 --- a/src/types/config/config.ts +++ b/src/types/config/config.ts @@ -10,18 +10,6 @@ import type { import type {WithDoBeforePack} from './doBeforePack'; import type {OwnE2edConfig} from './ownE2edConfig'; -/** - * Userland part of TestCafe config. - */ -type UserlandTestCafeConfig = Readonly<{ - assertionTimeout: number; - concurrency: number; - pageRequestTimeout: number; - port1: number; - port2: number; - selectorTimeout: number; -}>; - /** * Supported browsers. */ @@ -69,5 +57,12 @@ export type UserlandPackWithoutDoBeforePack< CustomReportProperties = CustomReportPropertiesPlaceholder, SkipTests = SkipTestsPlaceholder, TestMeta = TestMetaPlaceholder, -> = UserlandTestCafeConfig & +> = Readonly<{ + assertionTimeout: number; + concurrency: number; + pageRequestTimeout: number; + port1: number; + port2: number; + selectorTimeout: number; +}> & OwnE2edConfig; diff --git a/src/types/config/ownE2edConfig.ts b/src/types/config/ownE2edConfig.ts index ab9e7dfc..98e6786d 100644 --- a/src/types/config/ownE2edConfig.ts +++ b/src/types/config/ownE2edConfig.ts @@ -1,5 +1,7 @@ import type {PlaywrightTestConfig} from '@playwright/test'; +import type {TestRunStatus} from '../../constants/internal'; + import type {FullMocksConfig} from '../fullMocks'; import type {MapBackendResponseToLog, MapLogPayload, MapLogPayloadInReport} from '../log'; import type {MaybePromise} from '../promise'; @@ -255,6 +257,19 @@ export type OwnE2edConfig< */ viewportWidth: number; + /** + * Returns how many milliseconds `e2ed` should wait before running test (for retries). + */ + waitBeforeRetry: ( + this: void, + options: Readonly<{ + previousError: string | undefined; + retryIndex: number; + status: TestRunStatus; + testStaticOptions: TestStaticOptions; + }>, + ) => MaybePromise; + /** * Group of settings for the `waitForAllRequestsComplete` function. */ diff --git a/src/types/internal.ts b/src/types/internal.ts index 9bc167c9..844c7805 100644 --- a/src/types/internal.ts +++ b/src/types/internal.ts @@ -5,8 +5,6 @@ export type {Brand, IsBrand} from './brand'; export type {Expect, IsEqual, IsReadonlyKey} from './checks'; export type {Class} from './class'; export type {ClientFunction} from './clientFunction'; -/** @internal */ -export type {ClientFunctionWrapperResult, MaybeTestCafeError} from './clientFunction'; export type { AnyPack, BrowserName, diff --git a/src/types/userland/createPackSpecificTypes.ts b/src/types/userland/createPackSpecificTypes.ts index 3fc8383a..f9f8a231 100644 --- a/src/types/userland/createPackSpecificTypes.ts +++ b/src/types/userland/createPackSpecificTypes.ts @@ -36,4 +36,5 @@ export type CreatePackSpecificTypes< MapLogPayloadInLogFile: MapLogPayload; MapLogPayloadInReport: MapLogPayloadInReport; TestFunction: TestFunction; + WaitBeforeRetry: FullPackConfigByPack['waitBeforeRetry']; }>; diff --git a/src/utils/fs/index.ts b/src/utils/fs/index.ts index 56d5ffae..ee1cd2ab 100644 --- a/src/utils/fs/index.ts +++ b/src/utils/fs/index.ts @@ -7,6 +7,8 @@ export {getTestRunEventFileName} from './getTestRunEventFileName'; /** @internal */ export {getLastLogEventTimeInMs, writeLogEventTime} from './logIsoString'; /** @internal */ +export {readEventFromFile} from './readEventFromFile'; +/** @internal */ export {readEventsFromFiles} from './readEventsFromFiles'; /** @internal */ export {readStartInfo} from './readStartInfo'; diff --git a/src/utils/fs/readEventFromFile.ts b/src/utils/fs/readEventFromFile.ts new file mode 100644 index 00000000..618b2006 --- /dev/null +++ b/src/utils/fs/readEventFromFile.ts @@ -0,0 +1,23 @@ +import {readFile} from 'node:fs/promises'; +import {join} from 'node:path'; + +import {EVENTS_DIRECTORY_PATH, READ_FILE_OPTIONS} from '../../constants/internal'; + +import {generalLog} from '../generalLog'; + +/** + * Read event object with test run from temporary directory. + * @internal + */ +export const readEventFromFile = (fileName: string): Promise => { + const filePath = join(EVENTS_DIRECTORY_PATH, fileName); + + return readFile(filePath, READ_FILE_OPTIONS).catch((error: unknown) => { + generalLog(`Caught an error on reading text of test run event from file "${fileName}"`, { + error, + filePath, + }); + + return undefined; + }); +}; diff --git a/src/utils/fs/readEventsFromFiles.ts b/src/utils/fs/readEventsFromFiles.ts index a9b7c50f..3e7c7f53 100644 --- a/src/utils/fs/readEventsFromFiles.ts +++ b/src/utils/fs/readEventsFromFiles.ts @@ -1,18 +1,19 @@ import {execFile} from 'node:child_process'; -import {readdir, readFile} from 'node:fs/promises'; +import {readdir} from 'node:fs/promises'; import {join} from 'node:path'; import { AMOUNT_OF_PARALLEL_OPEN_FILES, EVENTS_DIRECTORY_PATH, INTERNAL_REPORTS_DIRECTORY_PATH, - READ_FILE_OPTIONS, } from '../../constants/internal'; import {assertValueIsDefined, assertValueIsTrue} from '../asserts'; import {generalLog} from '../generalLog'; import {getDurationWithUnits} from '../getDurationWithUnits'; +import {readEventFromFile} from './readEventFromFile'; + import type {FullTestRun, UtcTimeInMs} from '../../types/internal'; /** @@ -43,7 +44,7 @@ export const readEventsFromFiles = async ( fileIndex < newEventFiles.length; fileIndex += AMOUNT_OF_PARALLEL_OPEN_FILES ) { - const readPromises: Promise>[] = []; + const readPromises: Promise | undefined>[] = []; for ( let index = fileIndex; @@ -58,13 +59,14 @@ export const readEventsFromFiles = async ( newEventFilesLength: newEventFiles.length, }); - const filePath = join(EVENTS_DIRECTORY_PATH, fileName); - const promise = readFile(filePath, READ_FILE_OPTIONS).then((text) => ({fileName, text})); + const promise = readEventFromFile(fileName).then((maybeText) => + maybeText === undefined ? undefined : {fileName, text: maybeText}, + ); readPromises.push(promise); } - const filesWithNames = await Promise.all(readPromises); + const filesWithNames = (await Promise.all(readPromises)).filter((value) => value !== undefined); for (const {fileName, text} of filesWithNames) { try { diff --git a/src/utils/selectors/Selector.ts b/src/utils/selectors/Selector.ts index 88326f64..134dac2f 100644 --- a/src/utils/selectors/Selector.ts +++ b/src/utils/selectors/Selector.ts @@ -170,7 +170,6 @@ export class Selector { return result; } - // eslint-disable-next-line @typescript-eslint/consistent-return getPlaywrightLocator(): PlaywrightLocator { const args = this.args!; const selector = this.parentSelector!; diff --git a/src/utils/selectors/createCustomMethods.ts b/src/utils/selectors/createCustomMethods.ts index 88ccae43..cff42316 100644 --- a/src/utils/selectors/createCustomMethods.ts +++ b/src/utils/selectors/createCustomMethods.ts @@ -7,7 +7,7 @@ import type { } from '../../types/internal'; /** - * Creates custom `e2ed` methods of selector (additional to selector's own methods from TestCafe). + * Creates custom `e2ed` methods of selector (additional to selector's own methods). * @internal */ export const createCustomMethods = ( diff --git a/src/utils/test/beforeTest.ts b/src/utils/test/beforeTest.ts index 1c75285a..0ad23c2e 100644 --- a/src/utils/test/beforeTest.ts +++ b/src/utils/test/beforeTest.ts @@ -22,18 +22,29 @@ import type { UtcTimeInMs, } from '../../types/internal'; +import {test} from '@playwright/test'; + type Options = Readonly<{ + beforeRetryTimeout: number | undefined; retryIndex: number; runId: RunId; testFn: TestFn; testStaticOptions: TestStaticOptions; }>; +const additionalDurationToPlaywrightTestTimeoutInMs = 500; + /** * Internal before test hook. * @internal */ -export const beforeTest = ({retryIndex, runId, testFn, testStaticOptions}: Options): void => { +export const beforeTest = ({ + beforeRetryTimeout, + retryIndex, + runId, + testFn, + testStaticOptions, +}: Options): void => { const {options} = testStaticOptions; setMeta(options.meta); @@ -49,6 +60,10 @@ export const beforeTest = ({retryIndex, runId, testFn, testStaticOptions}: Optio const testIdleTimeout = options.testIdleTimeout ?? testIdleTimeoutFromConfig; const testTimeout = options.testTimeout ?? testTimeoutFromConfig; + test.setTimeout( + testTimeout + additionalDurationToPlaywrightTestTimeoutInMs + (beforeRetryTimeout ?? 0), + ); + setTestIdleTimeout(testIdleTimeout); setTestTimeout(testTimeout); diff --git a/src/utils/test/getRunTest.ts b/src/utils/test/getRunTest.ts index 0eca875a..207cd6eb 100644 --- a/src/utils/test/getRunTest.ts +++ b/src/utils/test/getRunTest.ts @@ -10,6 +10,7 @@ import {getShouldRunTest} from './getShouldRunTest'; import {getTestStaticOptions} from './getTestStaticOptions'; import {preparePage} from './preparePage'; import {runTestFn} from './runTestFn'; +import {waitBeforeRetry} from './waitBeforeRetry'; import type {PlaywrightTestArgs, TestInfo} from '@playwright/test'; @@ -26,7 +27,7 @@ export const getRunTest = ({context, page, request}: PlaywrightTestArgs, testInfo: TestInfo): Promise => { const runTest = async (): Promise => { const retryIndex = testInfo.retry + 1; - const runId = createRunId(); + const runId = createRunId(test, retryIndex); let clearPage: (() => Promise) | undefined; let hasRunError = false; @@ -43,13 +44,15 @@ export const getRunTest = return; } + const beforeRetryTimeout = await waitBeforeRetry(runId, testStaticOptions); + clearPage = await preparePage(page); - beforeTest({retryIndex, runId, testFn: test.testFn, testStaticOptions}); + beforeTest({beforeRetryTimeout, retryIndex, runId, testFn: test.testFn, testStaticOptions}); const testController = {context, page, request}; - await runTestFn({retryIndex, runId, testController, testStaticOptions}); + await runTestFn({beforeRetryTimeout, retryIndex, runId, testController, testStaticOptions}); } catch (error) { hasRunError = true; unknownRunError = error; diff --git a/src/utils/test/runTestFn.ts b/src/utils/test/runTestFn.ts index 60539315..e83f8061 100644 --- a/src/utils/test/runTestFn.ts +++ b/src/utils/test/runTestFn.ts @@ -1,10 +1,11 @@ -import {TestRunStatus} from '../../constants/internal'; +import {LogEventType, TestRunStatus} from '../../constants/internal'; import {setTestRunPromise} from '../../context/testRunPromise'; import {getTestTimeout} from '../../context/testTimeout'; import {getFullPackConfig} from '../config'; import {getTestRunEvent} from '../events'; import {enableFullMocks} from '../fullMocks'; +import {log} from '../log'; import {getPromiseWithResolveAndReject} from '../promise'; import type {PlaywrightTestArgs} from '@playwright/test'; @@ -14,6 +15,7 @@ import type {RunId, TestStaticOptions} from '../../types/internal'; const delayForTestRunPromiseResolutionAfterTestTimeoutInMs = 100; type Options = Readonly<{ + beforeRetryTimeout: number | undefined; retryIndex: number; runId: RunId; testController: PlaywrightTestArgs; @@ -25,6 +27,7 @@ type Options = Readonly<{ * @internal */ export const runTestFn = async ({ + beforeRetryTimeout, retryIndex, runId, testController, @@ -40,6 +43,10 @@ export const runTestFn = async ({ setTestRunPromise(testRunPromise); + if (beforeRetryTimeout !== undefined) { + log(`Waited for ${beforeRetryTimeout}ms before running this retry`, LogEventType.InternalUtil); + } + const {fullMocks} = getFullPackConfig(); if (status !== TestRunStatus.Skipped && fullMocks?.filterTests(testStaticOptions)) { diff --git a/src/utils/test/waitBeforeRetry.ts b/src/utils/test/waitBeforeRetry.ts new file mode 100644 index 00000000..ef618ac3 --- /dev/null +++ b/src/utils/test/waitBeforeRetry.ts @@ -0,0 +1,85 @@ +import {assertValueIsTrue} from '../asserts'; +import {getFullPackConfig} from '../config'; +import {getTestRunEventFileName, readEventFromFile} from '../fs'; +import {generalLog} from '../generalLog'; +import {getTimeoutPromise} from '../promise'; + +import type {FullTestRun, RunId, TestStaticOptions} from '../../types/internal'; + +/** + * Waits before running test for some time from pack config (for retries). + * @internal + */ +export const waitBeforeRetry = async ( + runId: RunId, + testStaticOptions: TestStaticOptions, +): Promise => { + const indexOfRetryIndex = runId.lastIndexOf('-'); + + assertValueIsTrue( + indexOfRetryIndex > 0 && indexOfRetryIndex < runId.length - 1, + 'runId has dash', + {runId, testStaticOptions}, + ); + + const retryIndex = Number(runId.slice(indexOfRetryIndex + 1)); + + assertValueIsTrue( + Number.isInteger(retryIndex) && retryIndex > 0, + 'retryIndex from runId is correct', + {runId, testStaticOptions}, + ); + + const previousRetryIndex = retryIndex - 1; + + if (previousRetryIndex < 1) { + return; + } + + const previousRunId = `${runId.slice(0, indexOfRetryIndex)}-${previousRetryIndex}` as RunId; + + const fileName = getTestRunEventFileName(previousRunId); + const fileText = await readEventFromFile(fileName); + + if (fileText === undefined) { + generalLog('Cannot find JSON file of previous test run', { + previousRunId, + runId, + testStaticOptions, + }); + + return; + } + + try { + const fullTestRun = JSON.parse(fileText) as FullTestRun; + + const {runError, status} = fullTestRun; + const {waitBeforeRetry: waitBeforeRetryFromConfig} = getFullPackConfig(); + + const previousError = runError === undefined ? undefined : String(runError); + + const timeoutInMs = await waitBeforeRetryFromConfig({ + previousError, + retryIndex, + status, + testStaticOptions, + }); + + if (timeoutInMs === 0) { + return; + } + + await getTimeoutPromise(timeoutInMs); + + return timeoutInMs; + } catch (error) { + generalLog('Caught an error on getting timeout for "before retry" waiting', { + error, + runId, + testStaticOptions, + }); + + return undefined; + } +}; diff --git a/tsconfig.json b/tsconfig.json index 9a484543..35d28100 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,6 +14,7 @@ "noImplicitReturns": true, "noPropertyAccessFromIndexSignature": true, "noUncheckedIndexedAccess": true, + "noUncheckedSideEffectImports": true, "noUnusedLocals": true, "outDir": "build", "paths": {