diff --git a/README.md b/README.md index e196627f..e87346de 100644 --- a/README.md +++ b/README.md @@ -286,6 +286,8 @@ If `true`, page fires `touch` events when test interact with the page (instead o filters tests (tasks) by their static options — only those tests for which the function returned `true` get into the pack. +`fullMocks: FullMocksConfig | null`: functions that specify the "full mocks" functionality. + `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/configurator/index.ts b/autotests/configurator/index.ts index 2b5074f5..b603d755 100644 --- a/autotests/configurator/index.ts +++ b/autotests/configurator/index.ts @@ -10,6 +10,7 @@ export type { DoAfterPack, DoBeforePack, FilterTestsIntoPack, + FullMocksConfig, GetFullPackConfig, GetLogContext, GetMainTestRunParams, diff --git a/autotests/configurator/types/index.ts b/autotests/configurator/types/index.ts index dd5ae5f3..a193b75f 100644 --- a/autotests/configurator/types/index.ts +++ b/autotests/configurator/types/index.ts @@ -2,6 +2,7 @@ export type { DoAfterPack, DoBeforePack, FilterTestsIntoPack, + FullMocksConfig, GetFullPackConfig, GetLogContext, GetMainTestRunParams, diff --git a/autotests/configurator/types/packSpecific.ts b/autotests/configurator/types/packSpecific.ts index f17c76c2..dd2d0417 100644 --- a/autotests/configurator/types/packSpecific.ts +++ b/autotests/configurator/types/packSpecific.ts @@ -14,6 +14,7 @@ export type GetLogContext = PackSpecificTypes['GetLogContext']; export type GetMainTestRunParams = PackSpecificTypes['GetMainTestRunParams']; export type GetTestRunHash = PackSpecificTypes['GetTestRunHash']; export type FilterTestsIntoPack = PackSpecificTypes['FilterTestsIntoPack']; +export type FullMocksConfig = PackSpecificTypes['FullMocksConfig']; export type IsTestSkipped = PackSpecificTypes['IsTestSkipped']; export type LiteReport = PackSpecificTypes['LiteReport']; export type MapBackendResponseErrorToLog = PackSpecificTypes['MapBackendResponseErrorToLog']; diff --git a/autotests/packs/allTests.ts b/autotests/packs/allTests.ts index f55313fe..d73c3973 100644 --- a/autotests/packs/allTests.ts +++ b/autotests/packs/allTests.ts @@ -56,6 +56,7 @@ export const pack: Pack = { enableMobileDeviceMode: false, enableTouchEventEmulation: false, filterTestsIntoPack, + fullMocks: null, liteReportFileName: 'lite-report.json', logFileName: 'pack-logs.log', mapBackendResponseErrorToLog, diff --git a/package.json b/package.json index a01d28fa..2472c112 100644 --- a/package.json +++ b/package.json @@ -75,7 +75,7 @@ "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 lint test", - "clear:lint:cache": "rm -f ./node_modules/.cache/lint-*", + "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 .", "lint:prettier": "prettier --cache --cache-location=./node_modules/.cache/lint-prettier --cache-strategy=content --check --ignore-path=.gitignore . !docs/index.html", diff --git a/src/actions/mock/mockApiRoute.ts b/src/actions/mock/mockApiRoute.ts index ac858ad4..2f9ead74 100644 --- a/src/actions/mock/mockApiRoute.ts +++ b/src/actions/mock/mockApiRoute.ts @@ -2,6 +2,7 @@ import {RequestMock} from 'testcafe-without-typecheck'; import {LogEventType} from '../../constants/internal'; import {getApiMockState} from '../../context/apiMockState'; +import {getFullMocksState} from '../../context/fullMocks'; import {testController} from '../../testController'; import {assertValueIsDefined} from '../../utils/asserts'; import {setCustomInspectOnFunction} from '../../utils/fn'; @@ -35,6 +36,17 @@ export const mockApiRoute = async < setCustomInspectOnFunction(apiMockFunction); const apiMockState = getApiMockState(); + + if (!apiMockState.isMocksEnabled) { + return; + } + + const fullMocksState = getFullMocksState(); + + if (fullMocksState?.appliedMocks !== undefined) { + setReadonlyProperty(apiMockState, 'isMocksEnabled', false); + } + let {optionsByRoute} = apiMockState; if (optionsByRoute === undefined) { diff --git a/src/constants/testRun.ts b/src/constants/testRun.ts index d5fcaeaa..1c18ee3e 100644 --- a/src/constants/testRun.ts +++ b/src/constants/testRun.ts @@ -60,9 +60,9 @@ export const TEST_RUN_STATUS_SYMBOLS = { [TestRunStatus.Failed]: '×', [TestRunStatus.Unknown]: '?', [TestRunStatus.Passed]: '✓', - [TestRunStatus.Skipped]: '⊘', + [TestRunStatus.Skipped]: '−', [TestRunStatus.Manual]: '⚒', - [TestRunStatus.Broken]: '!', + [TestRunStatus.Broken]: '⊘', }; /** diff --git a/src/context/apiMockState.ts b/src/context/apiMockState.ts index 6a28033a..c80de437 100644 --- a/src/context/apiMockState.ts +++ b/src/context/apiMockState.ts @@ -21,6 +21,7 @@ export const getApiMockState = (): ApiMockState => { const apiMockState: ApiMockState = { apiMock: undefined, + isMocksEnabled: true, optionsByRoute: undefined, optionsWithRouteByUrl: Object.create(null) as {}, }; diff --git a/src/context/fullMocks.ts b/src/context/fullMocks.ts new file mode 100644 index 00000000..e07085ba --- /dev/null +++ b/src/context/fullMocks.ts @@ -0,0 +1,30 @@ +import {useContext} from '../useContext'; +import {assertValueIsUndefined} from '../utils/asserts'; + +import type {FullMocksState} from '../types/internal'; + +/** + * Raw versions of `getFullMocksState` and `setFullMocksState`. + * @internal + */ +const [getFullMocksState, setRawFullMocksState] = useContext(); + +/** + * Get state of full mocks. + * @internal + */ +export {getFullMocksState}; + +/** + * Set state of full mocks (can only be called once). + * @internal + */ +export const setFullMocksState: typeof setRawFullMocksState = (fullMocksState) => { + const currentFullMocksState = getFullMocksState(); + + assertValueIsUndefined(currentFullMocksState, 'currentFullMocksState is not defined', { + fullMocksState, + }); + + return setRawFullMocksState(fullMocksState); +}; diff --git a/src/types/config/ownE2edConfig.ts b/src/types/config/ownE2edConfig.ts index 34607bb6..abbc91cd 100644 --- a/src/types/config/ownE2edConfig.ts +++ b/src/types/config/ownE2edConfig.ts @@ -1,3 +1,4 @@ +import type {FullMocksConfig} from '../fullMocks'; import type {MapBackendResponseToLog, MapLogPayload, MapLogPayloadInReport} from '../log'; import type {MaybePromise} from '../promise'; import type {LiteReport} from '../report'; @@ -80,6 +81,11 @@ export type OwnE2edConfig< */ filterTestsIntoPack: (this: void, testStaticOptions: TestStaticOptions) => boolean; + /** + * Functions that specify the "full mocks" functionality. + */ + fullMocks: FullMocksConfig | null; + /** * 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/fullMocks.ts b/src/types/fullMocks.ts new file mode 100644 index 00000000..4c24cc08 --- /dev/null +++ b/src/types/fullMocks.ts @@ -0,0 +1,105 @@ +import type {URL} from 'node:url'; + +import type {Brand} from './brand'; +import type {Method, Request, Response, ResponseWithRequest, StatusCode} from './http'; +import type {TestStaticOptions} from './testRun'; +import type {TestMetaPlaceholder} from './userland'; + +/** + * Options of `getResponseFromFullMocks` function. + */ +type ResponseFromFullMocksOptions = Readonly<{ + request: Request; + requestKind: RequestKind; + responseWithRequest: ResponseWithRequest | undefined; + testFullMocks: TestFullMocks; +}>; + +/** + * Functions that specify the "full mocks" functionality. + */ +export type FullMocksConfig = Readonly<{ + /** + * Filters tests by their static options — + * full mocks will only be applied to tests for which the function returned `true`. + */ + filterTests: (this: void, testStaticOptions: TestStaticOptions) => boolean; + + /** + * Get `RequestKind` of request by `method` and `urlObject`. + */ + getRequestKind: (this: void, method: Method, urlObject: URL) => RequestKind; + + /** + * Get `response` on `request` by `requestKind` and by test full mocks. + */ + getResponseFromFullMocks: ( + this: void, + options: ResponseFromFullMocksOptions, + ) => FullMocksResponse; + + /** + * Get `responseWithRequest` of API request to write to full mocks. + * If it returns `undefined`, the response is not written to full mocks. + */ + getResponseToWriteToFullMocks: ( + this: void, + requestKind: RequestKind, + responseWithRequest: ResponseWithRequest, + ) => ResponseWithRequest | undefined; + + /** + * Reads full mocks of one test by `testId`. + */ + readTestFullMocks: (this: void, testId: FullMocksTestId) => Promise; + + /** + * Writes full mocks of one test by `testId`. + */ + writeTestFullMocks: ( + this: void, + testId: FullMocksTestId, + testFullMocks: TestFullMocks, + ) => Promise; +}>; + +/** + * Mocked (generated) `response` for full mocks. + */ +export type FullMocksResponse = Partial & Readonly<{statusCode: StatusCode}>; + +/** + * Parameters of special `FullMocksRoute`. + * @internal + */ +export type FullMocksRouteParams = Readonly<{ + fullMocksState: FullMocksState; + method: Method; + requestKind: RequestKind; + urlObject: URL; +}>; + +/** + * State of full mocks during concrete test. + * @internal + */ +export type FullMocksState = Readonly<{ + appliedMocks: Record | undefined; + testFullMocks: TestFullMocks; + testId: FullMocksTestId; +}>; + +/** + * Identifier of test (usually the hash of test file content). + */ +export type FullMocksTestId = Brand; + +/** + * Identifier of request in set of requests for one test (usually just `path` of request url). + */ +export type RequestKind = Brand; + +/** + * Full mocks of one test. + */ +export type TestFullMocks = Readonly>; diff --git a/src/types/index.ts b/src/types/index.ts index 948e5f3a..4ab52587 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -8,6 +8,13 @@ export type {DeepMutable, DeepPartial, DeepReadonly, DeepRequired} from './deep' export type {BrowserJsError, E2edPrintedFields} from './errors'; export type {LogEvent, Onlog, TestRunEvent} from './events'; export type {Fn, MergeFunctions} from './fn'; +export type { + FullMocksConfig, + FullMocksResponse, + FullMocksTestId, + RequestKind, + TestFullMocks, +} from './fullMocks'; export type { Cookie, CookieHeaderString, diff --git a/src/types/internal.ts b/src/types/internal.ts index 1f68faa1..a359ef3c 100644 --- a/src/types/internal.ts +++ b/src/types/internal.ts @@ -32,6 +32,15 @@ export type {LogEvent, Onlog, TestRunEvent} from './events'; /** @internal */ export type {EndTestRunEvent, FullEventsData} from './events'; export type {Fn, MergeFunctions} from './fn'; +export type { + FullMocksConfig, + FullMocksResponse, + FullMocksTestId, + RequestKind, + TestFullMocks, +} from './fullMocks'; +/** @internal */ +export type {FullMocksRouteParams, FullMocksState} from './fullMocks'; /** @internal */ export type {SafeHtml} from './html'; export type { @@ -109,7 +118,7 @@ export type { /** @internal */ export type {RequestHookClassWithContext, RequestHookEncoding} from './requestHooks'; /** @internal */ -export type {RetriesState, RunRetryOptions} from './retries'; +export type {RetriesState, RunRetryOptions, VisitedTestNamesHash} from './retries'; export type {ApiRouteClassType, ApiRouteClassTypeWithGetParamsFromUrl} from './routes'; export type {RunLabel, RunLabelObject} from './runLabel'; /** @internal */ diff --git a/src/types/mockApiRoute.ts b/src/types/mockApiRoute.ts index ebd4c903..3b870083 100644 --- a/src/types/mockApiRoute.ts +++ b/src/types/mockApiRoute.ts @@ -34,6 +34,7 @@ export type ApiMockFunction< */ export type ApiMockState = Readonly<{ apiMock: Inner.RequestMock | undefined; + isMocksEnabled: boolean; optionsByRoute: Map | undefined; optionsWithRouteByUrl: Record; }>; diff --git a/src/types/retries.ts b/src/types/retries.ts index f658f92a..0aae9032 100644 --- a/src/types/retries.ts +++ b/src/types/retries.ts @@ -14,6 +14,7 @@ export type RetriesState = Readonly<{ retryIndex: number; startLastRetryTimeInMs: UtcTimeInMs; successfulTestRunNamesHash: Record; + visitedTestNamesHash: Record; visitedTestRunEventsFileName: readonly string[]; }>; @@ -24,5 +25,12 @@ export type RetriesState = Readonly<{ export type RunRetryOptions = Readonly<{ concurrency: number; runLabel: RunLabel; - successfulTestRunNamesHash: Record; + successfulTestRunNamesHash: VisitedTestNamesHash; + visitedTestNamesHash: VisitedTestNamesHash; }>; + +/** + * Hash of names of already visited tests (maybe, in previous retries). + * @internal + */ +export type VisitedTestNamesHash = Readonly>; diff --git a/src/types/userland/createPackSpecificTypes.ts b/src/types/userland/createPackSpecificTypes.ts index 022ea263..fdae1063 100644 --- a/src/types/userland/createPackSpecificTypes.ts +++ b/src/types/userland/createPackSpecificTypes.ts @@ -1,4 +1,5 @@ import type {AnyPack, AnyPackParameters, FullPackConfigByPack, GetPackParameters} from '../config'; +import type {FullMocksConfig} from '../fullMocks'; import type {MapBackendResponseToLog, MapLogPayload, MapLogPayloadInReport} from '../log'; import type {LiteReport} from '../report'; @@ -16,6 +17,7 @@ export type CreatePackSpecificTypes< DoAfterPack: FullPackConfigByPack['doAfterPack'][number]; DoBeforePack: FullPackConfigByPack['doBeforePack'][number]; FilterTestsIntoPack: Pack['filterTestsIntoPack']; + FullMocksConfig: FullMocksConfig; GetFullPackConfig: () => FullPackConfigByPack; GetLogContext: Hooks['getLogContext']; GetMainTestRunParams: Hooks['getMainTestRunParams']; diff --git a/src/utils/events/registerEndTestRunEvent.ts b/src/utils/events/registerEndTestRunEvent.ts index fb6c978d..735886bc 100644 --- a/src/utils/events/registerEndTestRunEvent.ts +++ b/src/utils/events/registerEndTestRunEvent.ts @@ -1,4 +1,5 @@ import {TestRunStatus} from '../../constants/internal'; +import {getFullMocksState} from '../../context/fullMocks'; import {cloneWithoutLogEvents} from '../clone'; import {getRunErrorFromError} from '../error'; @@ -8,6 +9,7 @@ import {getUserlandHooks} from '../userland'; import {calculateTestRunStatus} from './calculateTestRunStatus'; import {getTestRunEvent} from './getTestRunEvent'; +import {writeFullMocks} from './writeFullMocks'; import type {EndTestRunEvent, FullTestRun, TestRun} from '../../types/internal'; @@ -43,6 +45,20 @@ export const registerEndTestRunEvent = async (endTestRunEvent: EndTestRunEvent): const status = calculateTestRunStatus({endTestRunEvent, testRunEvent}); + if (status === TestRunStatus.Passed) { + const fullMocksState = getFullMocksState(); + + if (fullMocksState !== undefined && fullMocksState.appliedMocks === undefined) { + await writeFullMocks(fullMocksState).catch((error: unknown) => { + generalLog('Cannot write "full mocks" for test', { + endTestRunEvent, + error, + testRunEvent: cloneWithoutLogEvents(testRunEvent), + }); + }); + } + } + (testRunEvent as {status: TestRunStatus}).status = status; const runError = hasRunError ? getRunErrorFromError(unknownRunError) : undefined; diff --git a/src/utils/events/writeFullMocks.ts b/src/utils/events/writeFullMocks.ts new file mode 100644 index 00000000..f29f3c00 --- /dev/null +++ b/src/utils/events/writeFullMocks.ts @@ -0,0 +1,24 @@ +import {assertValueIsNotNull} from '../asserts'; +import {getFullPackConfig} from '../config'; +import {generalLog} from '../generalLog'; + +import type {FullMocksState} from '../../types/internal'; + +/** + * Writes full mocks of one test. + * @internal + */ +export const writeFullMocks = async (fullMocksState: FullMocksState): Promise => { + const {fullMocks: fullMocksConfig} = getFullPackConfig(); + + assertValueIsNotNull(fullMocksConfig, 'fullMocksConfig is not null'); + + await fullMocksConfig.writeTestFullMocks(fullMocksState.testId, fullMocksState.testFullMocks); + + generalLog('Full mocks have been written', { + requestKinds: Object.fromEntries( + Object.entries(fullMocksState.testFullMocks).map(([key, value]) => [key, value.length]), + ), + testId: fullMocksState.testId, + }); +}; diff --git a/src/utils/fullMocks/FullMocksRoute.ts b/src/utils/fullMocks/FullMocksRoute.ts new file mode 100644 index 00000000..ae5b2b51 --- /dev/null +++ b/src/utils/fullMocks/FullMocksRoute.ts @@ -0,0 +1,45 @@ +import {URL} from 'node:url'; + +import {ApiRoute} from '../../ApiRoute'; +import {getFullMocksState} from '../../context/fullMocks'; + +import {assertValueIsDefined, assertValueIsNotNull} from '../asserts'; +import {getFullPackConfig} from '../config'; +import {E2edError} from '../error'; + +import type {FullMocksRouteParams, Method, Url} from '../../types/internal'; + +/** + * Special route for mocking all requests in "full mocks" mode. + * @internal + */ +export class FullMocksRoute extends ApiRoute { + static override getParamsFromUrl(url: Url, method: Method): FullMocksRouteParams { + const {fullMocks: fullMocksConfig} = getFullPackConfig(); + const fullMocksState = getFullMocksState(); + + assertValueIsDefined(fullMocksState, 'fullMocksState is defined', {method, url}); + assertValueIsNotNull(fullMocksConfig, 'fullMocksConfig is not null', {method, url}); + + const urlObject = new URL(url); + const requestKind = fullMocksConfig.getRequestKind(method, urlObject); + + if (fullMocksState.testFullMocks[requestKind]) { + return {fullMocksState, method, requestKind, urlObject}; + } + + throw new E2edError('Request should not be mocked', {method, requestKind, url}); + } + + getMethod(): Method { + return this.routeParams.method; + } + + getPath(): string { + return this.routeParams.urlObject.pathname; + } + + override isMatchUrl(): true { + return true; + } +} diff --git a/src/utils/fullMocks/enableFullMocks.ts b/src/utils/fullMocks/enableFullMocks.ts new file mode 100644 index 00000000..e344c3ae --- /dev/null +++ b/src/utils/fullMocks/enableFullMocks.ts @@ -0,0 +1,73 @@ +// eslint-disable-next-line import/no-internal-modules +import {mockApiRoute} from '../../actions/mock'; +// eslint-disable-next-line import/no-internal-modules +import {waitForResponse} from '../../actions/waitFor'; +import {LogEventType} from '../../constants/internal'; +import {setFullMocksState} from '../../context/fullMocks'; + +import {log} from '../log'; +import {setReadonlyProperty} from '../setReadonlyProperty'; + +import {FullMocksRoute} from './FullMocksRoute'; +import {getResponseFromFullMocks} from './getResponseFromFullMocks'; +import {getTestIdByTestFilePath} from './getTestIdByTestFilePath'; +import {writeResposneToFullMocks} from './writeResposneToFullMocks'; + +import type { + FullMocksConfig, + FullMocksState, + TestFilePath, + TestFullMocks, +} from '../../types/internal'; + +const maxTestRunDurationInMs = 3600_000_000; + +/** + * Enables full mocks for concrete test. + * @internal + */ +export const enableFullMocks = async ( + fullMocksConfig: FullMocksConfig, + shouldApplyMocks: boolean, + testFilePath: TestFilePath, +): Promise => { + const fullMocksState: FullMocksState = { + appliedMocks: undefined, + testFullMocks: Object.create(null) as {}, + testId: await getTestIdByTestFilePath(testFilePath), + }; + + setFullMocksState(fullMocksState); + + let testFullMocks: TestFullMocks | undefined; + + if (shouldApplyMocks) { + testFullMocks = await fullMocksConfig.readTestFullMocks(fullMocksState.testId); + } + + if (testFullMocks !== undefined) { + setReadonlyProperty(fullMocksState, 'appliedMocks', Object.create(null) as {}); + setReadonlyProperty(fullMocksState, 'testFullMocks', testFullMocks); + + log( + 'Full mocks have been read and enabled', + { + requestKinds: Object.fromEntries( + Object.entries(testFullMocks).map(([key, value]) => [key, value.length]), + ), + }, + LogEventType.InternalUtil, + ); + + await mockApiRoute(FullMocksRoute, getResponseFromFullMocks, {skipLogs: true}); + } else { + void waitForResponse( + (responseWithRequest) => { + writeResposneToFullMocks(responseWithRequest); + + return false; + }, + {skipLogs: true, timeout: maxTestRunDurationInMs}, + ); + } +}; diff --git a/src/utils/fullMocks/getResponseFromFullMocks.ts b/src/utils/fullMocks/getResponseFromFullMocks.ts new file mode 100644 index 00000000..5fd7a981 --- /dev/null +++ b/src/utils/fullMocks/getResponseFromFullMocks.ts @@ -0,0 +1,51 @@ +import {assertValueIsDefined, assertValueIsNotNull} from '../asserts'; +import {getFullPackConfig} from '../config'; +import {getContentJsonHeaders} from '../http'; + +import type {FullMocksRouteParams, Request, Response} from '../../types/internal'; + +/** + * Get `Response` for mocking API requests in "full mocks" mode. + * @internal + */ +export const getResponseFromFullMocks = ( + {fullMocksState, requestKind}: FullMocksRouteParams, + request: Request, +): Response => { + const {appliedMocks, testFullMocks} = fullMocksState; + + assertValueIsDefined(appliedMocks, 'appliedMocks is defined', {request, requestKind}); + + const appliedCount = appliedMocks[requestKind] ?? 0; + + appliedMocks[requestKind] = appliedCount + 1; + + const {fullMocks: fullMocksConfig} = getFullPackConfig(); + + assertValueIsNotNull(fullMocksConfig, 'fullMocksConfig is not null', {request, requestKind}); + + const responseWithRequest = testFullMocks[requestKind]?.[appliedCount]; + + const fullMocksResponse = fullMocksConfig.getResponseFromFullMocks({ + request, + requestKind, + responseWithRequest, + testFullMocks, + }); + + const {responseBody} = fullMocksResponse; + const responseBodyAsString = responseBody === undefined ? '' : JSON.stringify(responseBody); + const contentJsonHeaders = getContentJsonHeaders(responseBodyAsString); + + const response: Response = { + responseBody: undefined, + ...fullMocksResponse, + responseHeaders: { + ...fullMocksResponse.responseHeaders, + 'content-encoding': 'identity', + ...contentJsonHeaders, + }, + }; + + return response; +}; diff --git a/src/utils/fullMocks/getShouldApplyMocks.ts b/src/utils/fullMocks/getShouldApplyMocks.ts new file mode 100644 index 00000000..6624f8ab --- /dev/null +++ b/src/utils/fullMocks/getShouldApplyMocks.ts @@ -0,0 +1,11 @@ +import {getVisitedTestNamesHash} from '../globalState'; + +/** + * Returns `true`, if we should apply full mocks for test (by test name). + * @internal + */ +export const getShouldApplyMocks = (testName: string): boolean => { + const visitedTestNamesHash = getVisitedTestNamesHash(); + + return !visitedTestNamesHash?.[testName]; +}; diff --git a/src/utils/fullMocks/getTestIdByTestFilePath.ts b/src/utils/fullMocks/getTestIdByTestFilePath.ts new file mode 100644 index 00000000..451000c1 --- /dev/null +++ b/src/utils/fullMocks/getTestIdByTestFilePath.ts @@ -0,0 +1,25 @@ +import {createHash} from 'node:crypto'; +import {readFile} from 'node:fs/promises'; + +import {READ_FILE_OPTIONS} from '../../constants/internal'; + +import type {FullMocksTestId, TestFilePath} from '../../types/internal'; + +const fullMocksTestIdLength = 10; + +/** + * Get `testId` by `testFilePath`. + * @internal + */ +export const getTestIdByTestFilePath = async ( + testFilePath: TestFilePath, +): Promise => { + const testFileContent = await readFile(testFilePath, READ_FILE_OPTIONS); + const hash = createHash('sha1'); + + hash.update(testFileContent); + + const testId = hash.digest('base64url').slice(0, fullMocksTestIdLength) as FullMocksTestId; + + return testId; +}; diff --git a/src/utils/fullMocks/index.ts b/src/utils/fullMocks/index.ts new file mode 100644 index 00000000..17821acc --- /dev/null +++ b/src/utils/fullMocks/index.ts @@ -0,0 +1,8 @@ +/** @internal */ +export {enableFullMocks} from './enableFullMocks'; +/** @internal */ +export {getResponseFromFullMocks} from './getResponseFromFullMocks'; +/** @internal */ +export {getShouldApplyMocks} from './getShouldApplyMocks'; +/** @internal */ +export {writeResposneToFullMocks} from './writeResposneToFullMocks'; diff --git a/src/utils/fullMocks/writeResposneToFullMocks.ts b/src/utils/fullMocks/writeResposneToFullMocks.ts new file mode 100644 index 00000000..1e01dd41 --- /dev/null +++ b/src/utils/fullMocks/writeResposneToFullMocks.ts @@ -0,0 +1,54 @@ +import {getFullMocksState} from '../../context/fullMocks'; + +import {assertValueIsDefined, assertValueIsNotNull} from '../asserts'; +import {getFullPackConfig} from '../config'; +import {getHeaderValue} from '../requestHooks'; +import {setReadonlyProperty} from '../setReadonlyProperty'; + +import type {Mutable, ResponseWithRequest} from '../../types/internal'; + +/** + * Writes `ResponseWithRequest` to full mocks of test. + * @internal + */ +export const writeResposneToFullMocks = (responseWithRequest: ResponseWithRequest): void => { + const {fullMocks: fullMocksConfig} = getFullPackConfig(); + const fullMocksState = getFullMocksState(); + + assertValueIsDefined(fullMocksState, 'fullMocksState is defined', {responseWithRequest}); + assertValueIsNotNull(fullMocksConfig, 'fullMocksConfig is not null', {responseWithRequest}); + + const contentType = getHeaderValue(responseWithRequest.responseHeaders, 'content-type'); + const contentTypeString = (Array.isArray(contentType) ? contentType : [contentType]) + .join(',') + .toLowerCase(); + + if (!contentTypeString.includes('application/json')) { + return; + } + + const {url} = responseWithRequest.request; + const urlObject = new URL(url); + const requestKind = fullMocksConfig.getRequestKind(responseWithRequest.request.method, urlObject); + + const responseToWrite = fullMocksConfig.getResponseToWriteToFullMocks( + requestKind, + responseWithRequest, + ); + + if (responseToWrite === undefined) { + return; + } + + const {testFullMocks} = fullMocksState; + + if (!testFullMocks[requestKind]) { + setReadonlyProperty(testFullMocks, requestKind, []); + } + + const listOfResponses = testFullMocks[requestKind]; + + assertValueIsDefined(listOfResponses, 'listOfResponses is defined', {responseWithRequest}); + + (listOfResponses as Mutable).push(responseToWrite); +}; diff --git a/src/utils/globalState/index.ts b/src/utils/globalState/index.ts new file mode 100644 index 00000000..f35ec4bd --- /dev/null +++ b/src/utils/globalState/index.ts @@ -0,0 +1,2 @@ +/** @internal */ +export {getVisitedTestNamesHash, setVisitedTestNamesHash} from './visitedTestNamesHash'; diff --git a/src/utils/globalState/visitedTestNamesHash.ts b/src/utils/globalState/visitedTestNamesHash.ts new file mode 100644 index 00000000..1cafbbc1 --- /dev/null +++ b/src/utils/globalState/visitedTestNamesHash.ts @@ -0,0 +1,23 @@ +import {assertValueIsUndefined} from '../asserts'; + +import type {VisitedTestNamesHash} from '../../types/internal'; + +let visitedTestNamesHash: VisitedTestNamesHash | undefined; + +/** + * Get hash of names of already visited tests (maybe, in previous retries). + * @internal + */ +export const getVisitedTestNamesHash = (): VisitedTestNamesHash | undefined => visitedTestNamesHash; + +/** + * Set hash of names of already visited tests (can only be called once). + * @internal + */ +export const setVisitedTestNamesHash = (newVisitedTestNamesHash: VisitedTestNamesHash): void => { + assertValueIsUndefined(visitedTestNamesHash, 'visitedTestNamesHash is not defined', { + newVisitedTestNamesHash, + }); + + visitedTestNamesHash = newVisitedTestNamesHash; +}; diff --git a/src/utils/request/getBodyAsString.ts b/src/utils/http/getBodyAsString.ts similarity index 100% rename from src/utils/request/getBodyAsString.ts rename to src/utils/http/getBodyAsString.ts diff --git a/src/utils/request/getContentJsonHeaders.ts b/src/utils/http/getContentJsonHeaders.ts similarity index 100% rename from src/utils/request/getContentJsonHeaders.ts rename to src/utils/http/getContentJsonHeaders.ts diff --git a/src/utils/http/index.ts b/src/utils/http/index.ts new file mode 100644 index 00000000..19e87237 --- /dev/null +++ b/src/utils/http/index.ts @@ -0,0 +1,3 @@ +/** @internal */ +export {getBodyAsString} from './getBodyAsString'; +export {getContentJsonHeaders} from './getContentJsonHeaders'; diff --git a/src/utils/index.ts b/src/utils/index.ts index 72e0747d..48fe3017 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -36,6 +36,7 @@ export {writeFile} from './fs'; export {removeStyleFromString} from './generalLog'; export {getDurationWithUnits} from './getDurationWithUnits'; export {getKeysCounter} from './getKeysCounter'; +export {getContentJsonHeaders} from './http'; export {log} from './log'; export {parseMaybeEmptyValueAsJson} from './parseMaybeEmptyValueAsJson'; export { @@ -44,7 +45,7 @@ export { getTimeoutPromise, waitForAllProperties, } from './promise'; -export {getContentJsonHeaders, request} from './request'; +export {request} from './request'; export { getEquivalentHeadersNames, getHeadersFromHeaderEntries, diff --git a/src/utils/mockApiRoute/getSetResponse.ts b/src/utils/mockApiRoute/getSetResponse.ts index b9d2aca6..1a59d872 100644 --- a/src/utils/mockApiRoute/getSetResponse.ts +++ b/src/utils/mockApiRoute/getSetResponse.ts @@ -2,8 +2,8 @@ import {LogEventType, OK_STATUS_CODE} from '../../constants/internal'; import {assertValueIsDefined} from '../asserts'; import {cloneWithoutUndefinedProperties} from '../clone'; +import {getBodyAsString, getContentJsonHeaders} from '../http'; import {log} from '../log'; -import {getBodyAsString, getContentJsonHeaders} from '../request'; import {getMainRequestOptions, getRequestFromRequestOptions} from '../requestHooks'; import type {Inner} from 'testcafe-without-typecheck'; diff --git a/src/utils/packCompiler/compilePack.ts b/src/utils/packCompiler/compilePack.ts index 9a2c3c93..3232d990 100644 --- a/src/utils/packCompiler/compilePack.ts +++ b/src/utils/packCompiler/compilePack.ts @@ -17,6 +17,7 @@ type Return = Readonly<{ configCompileTimeWithUnits: string; }>; +const inNotUnderRootDir = "is not under 'rootDir'"; const unusedTsExceptErrorMessage = "Unused '@ts-expect-error' directive."; /** @@ -43,7 +44,7 @@ export const compilePack = (): Return => { allDiagnostics.forEach((diagnostic) => { const message = flattenDiagnosticMessageText(diagnostic.messageText, '\n'); - if (message === unusedTsExceptErrorMessage) { + if (message === unusedTsExceptErrorMessage || message.includes(inNotUnderRootDir)) { return; } diff --git a/src/utils/request/getFullMocksResponse.ts b/src/utils/request/getFullMocksResponse.ts new file mode 100644 index 00000000..e54fc1e1 --- /dev/null +++ b/src/utils/request/getFullMocksResponse.ts @@ -0,0 +1,57 @@ +import {assertValueIsDefined, assertValueIsNotNull} from '../asserts'; +import {getFullPackConfig} from '../config'; +import {getResponseFromFullMocks} from '../fullMocks'; + +import {getQuery} from './getQuery'; + +import type {URL} from 'node:url'; + +import type { + FullMocksState, + RequestWithUtcTimeInMs, + ResponseWithRequest, + UtcTimeInMs, +} from '../../types/internal'; + +import type {LogParams} from './types'; + +/** + * Get mocked response in "full mocks" mode. + * @internal + */ +export const getFullMocksResponse = ( + fullMocksState: FullMocksState, + logParams: LogParams, + urlObject: URL, +): ResponseWithRequest => { + const {fullMocks: fullMocksConfig} = getFullPackConfig(); + + assertValueIsNotNull(fullMocksConfig, 'fullMocksConfig is not null', logParams); + + const {method, requestBody, requestHeaders, url} = logParams; + + assertValueIsDefined(requestHeaders, 'requestHeaders is defined', logParams); + + const requestKind = fullMocksConfig.getRequestKind(method, urlObject); + + const requestWithUtcTimeInMs: RequestWithUtcTimeInMs = { + method, + query: getQuery(urlObject.search), + requestBody, + requestHeaders, + url, + utcTimeInMs: Date.now() as UtcTimeInMs, + }; + + const response: ResponseWithRequest = { + completionTimeInMs: requestWithUtcTimeInMs.utcTimeInMs, + duration: '0ms', + request: requestWithUtcTimeInMs, + ...getResponseFromFullMocks( + {fullMocksState, method, requestKind, urlObject}, + requestWithUtcTimeInMs, + ), + }; + + return response; +}; diff --git a/src/utils/request/getPreparedOptions.ts b/src/utils/request/getPreparedOptions.ts new file mode 100644 index 00000000..e457940e --- /dev/null +++ b/src/utils/request/getPreparedOptions.ts @@ -0,0 +1,75 @@ +import {URL} from 'node:url'; + +import {getDurationWithUnits} from '../getDurationWithUnits'; +import {getBodyAsString, getContentJsonHeaders} from '../http'; +import {setReadonlyProperty} from '../setReadonlyProperty'; + +import type { + ApiRouteClassType, + Headers, + Method, + Request, + Response, + Url, + ZeroOrOneArg, +} from '../../types/internal'; + +import type {LogParams} from './types'; + +type Options = Readonly<{ + requestBody: unknown; + requestHeaders: Headers | undefined; + routeParams: RouteParams | undefined; + timeout: number; +}>; + +type PreparedOptions = Readonly<{ + isResponseBodyInJsonFormat: boolean; + logParams: LogParams; + options: Readonly<{method: Method; requestHeaders: Headers}>; + requestBodyAsString: string; + url: Url; + urlObject: URL; +}>; + +/** + * Get prepared `request` options by it's arguments. + * @internal + */ +export const getPreparedOptions = ( + Route: ApiRouteClassType, + {requestHeaders, requestBody, routeParams, timeout}: Options, +): PreparedOptions => { + const route = new Route(...([routeParams] as ZeroOrOneArg)); + + const method = route.getMethod(); + const isRequestBodyInJsonFormat = route.getIsRequestBodyInJsonFormat(); + const isResponseBodyInJsonFormat = route.getIsResponseBodyInJsonFormat(); + const url = route.getUrl(); + + const urlObject = new URL(url); + + const timeoutWithUnits = getDurationWithUnits(timeout); + const logParams: LogParams = { + cause: undefined, + method, + requestBody, + requestHeaders, + retry: undefined, + timeoutWithUnits, + url, + }; + + const requestBodyAsString = getBodyAsString(requestBody, isRequestBodyInJsonFormat); + const options = { + method, + requestHeaders: { + ...getContentJsonHeaders(requestBodyAsString), + ...requestHeaders, + }, + }; + + setReadonlyProperty(logParams, 'requestHeaders', options.requestHeaders); + + return {isResponseBodyInJsonFormat, logParams, options, requestBodyAsString, url, urlObject}; +}; diff --git a/src/utils/request/getQuery.ts b/src/utils/request/getQuery.ts new file mode 100644 index 00000000..6fb1b584 --- /dev/null +++ b/src/utils/request/getQuery.ts @@ -0,0 +1,10 @@ +import {parse} from 'node:querystring'; + +import type {Query} from '../../types/internal'; + +/** + * Get `query` of request by url search string. + * @internal + */ +export const getQuery = (searchString: string): Query => + parse(searchString ? searchString.slice(1) : ''); diff --git a/src/utils/request/getResponse.ts b/src/utils/request/getResponse.ts new file mode 100644 index 00000000..d84002db --- /dev/null +++ b/src/utils/request/getResponse.ts @@ -0,0 +1,57 @@ +import {LogEventStatus, LogEventType} from '../../constants/internal'; +import {getFullMocksState} from '../../context/fullMocks'; + +import {writeResposneToFullMocks} from '../fullMocks'; +import {log} from '../log'; + +import {oneTryOfRequest} from './oneTryOfRequest'; + +import type {Request, Response, ResponseWithRequest, Url} from '../../types/internal'; + +import type {OneTryOfRequestOptions} from './types'; + +type Options = Readonly<{ + isNeedRetry: ( + response: ResponseWithRequest, + ) => Promise | boolean; + url: Url; +}> & + OneTryOfRequestOptions; + +/** + * Get `ResponseWithRequest` from one try of request. + * @internal + */ +export const getResponse = async ( + options: Options, +): Promise | undefined> => { + const {isNeedRetry, url, ...oneTryOfRequestOptions} = options; + + const {fullLogParams, response} = await oneTryOfRequest( + oneTryOfRequestOptions, + ); + const needRetry = await isNeedRetry(response); + + log( + `Got a response to the request to ${url}`, + { + ...fullLogParams, + logEventStatus: needRetry ? LogEventStatus.Failed : LogEventStatus.Passed, + needRetry, + response, + }, + LogEventType.InternalUtil, + ); + + if (needRetry === false) { + const fullMocksState = getFullMocksState(); + + if (fullMocksState !== undefined) { + writeResposneToFullMocks(response); + } + + return response; + } + + return undefined; +}; diff --git a/src/utils/request/index.ts b/src/utils/request/index.ts index 22211418..fc4ed3da 100644 --- a/src/utils/request/index.ts +++ b/src/utils/request/index.ts @@ -1,4 +1 @@ -/** @internal */ -export {getBodyAsString} from './getBodyAsString'; -export {getContentJsonHeaders} from './getContentJsonHeaders'; export {request} from './request'; diff --git a/src/utils/request/oneTryOfRequest.ts b/src/utils/request/oneTryOfRequest.ts index 70d0bb67..4dad04d9 100644 --- a/src/utils/request/oneTryOfRequest.ts +++ b/src/utils/request/oneTryOfRequest.ts @@ -1,5 +1,3 @@ -import {parse} from 'node:querystring'; - import {BAD_REQUEST_STATUS_CODE, LogEventType} from '../../constants/internal'; import {getRandomId} from '../../generators/internal'; @@ -10,6 +8,8 @@ import {log} from '../log'; import {parseMaybeEmptyValueAsJson} from '../parseMaybeEmptyValueAsJson'; import {wrapInTestRunTracker} from '../testRun'; +import {getQuery} from './getQuery'; + import type { Request, RequestWithUtcTimeInMs, @@ -80,12 +80,14 @@ export const oneTryOfRequest = ; diff --git a/src/utils/request/request.ts b/src/utils/request/request.ts index 7b9f6518..6d8790c9 100644 --- a/src/utils/request/request.ts +++ b/src/utils/request/request.ts @@ -1,28 +1,21 @@ import {request as httpRequest} from 'node:http'; import {request as httpsRequest} from 'node:https'; -import {URL} from 'node:url'; import {BAD_REQUEST_STATUS_CODE, LogEventStatus, LogEventType} from '../../constants/internal'; +import {getFullMocksState} from '../../context/fullMocks'; import {E2edError} from '../error'; -import {getDurationWithUnits} from '../getDurationWithUnits'; import {log} from '../log'; import {setReadonlyProperty} from '../setReadonlyProperty'; import {wrapInTestRunTracker} from '../testRun'; -import {getBodyAsString} from './getBodyAsString'; -import {getContentJsonHeaders} from './getContentJsonHeaders'; -import {oneTryOfRequest} from './oneTryOfRequest'; +import {getFullMocksResponse} from './getFullMocksResponse'; +import {getPreparedOptions} from './getPreparedOptions'; +import {getResponse} from './getResponse'; -import type { - ApiRouteClassType, - Request, - Response, - ResponseWithRequest, - ZeroOrOneArg, -} from '../../types/internal'; +import type {ApiRouteClassType, Request, Response, ResponseWithRequest} from '../../types/internal'; -import type {LogParams, Options} from './types'; +import type {Options} from './types'; const defaultIsNeedRetry = ({statusCode}: SomeResponse): boolean => statusCode >= BAD_REQUEST_STATUS_CODE; @@ -44,53 +37,36 @@ export const request = async < requestBody, routeParams, timeout = 30_000, - }: Options = {} as unknown as Options< + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + }: Options = {} as Options< RouteParams, SomeRequest, SomeResponse >, ): Promise> => { - const route = new Route(...([routeParams] as ZeroOrOneArg)); + const {isResponseBodyInJsonFormat, logParams, options, requestBodyAsString, url, urlObject} = + getPreparedOptions(Route, {requestBody, requestHeaders, routeParams, timeout}); - const method = route.getMethod(); - const isRequestBodyInJsonFormat = route.getIsRequestBodyInJsonFormat(); - const isResponseBodyInJsonFormat = route.getIsResponseBodyInJsonFormat(); - const url = route.getUrl(); + const fullMocksState = getFullMocksState(); - const urlObject = new URL(url); + if (fullMocksState?.appliedMocks !== undefined) { + const response = getFullMocksResponse(fullMocksState, logParams, urlObject); + + return response as ResponseWithRequest; + } - const timeoutWithUnits = getDurationWithUnits(timeout); - const logParams: LogParams = { - cause: undefined, - method, - requestBody, - requestHeaders, - retry: undefined, - timeoutWithUnits, - url, - }; - - const requestBodyAsString = getBodyAsString(requestBody, isRequestBodyInJsonFormat); - const options = { - method, - requestHeaders: { - ...getContentJsonHeaders(requestBodyAsString), - ...requestHeaders, - }, - }; const libRequest = wrapInTestRunTracker( urlObject.protocol === 'http:' ? httpRequest : httpsRequest, ); - setReadonlyProperty(logParams, 'requestHeaders', requestHeaders); - for (let retryIndex = 1; retryIndex <= maxRetriesCount; retryIndex += 1) { const retry = `${retryIndex}/${maxRetriesCount}`; setReadonlyProperty(logParams, 'retry', retry); try { - const {fullLogParams, response} = await oneTryOfRequest({ + const response = await getResponse({ + isNeedRetry, isResponseBodyInJsonFormat, libRequest, logParams, @@ -98,22 +74,11 @@ export const request = async < requestBody, requestBodyAsString, timeout, + url, urlObject, }); - const needRetry = await isNeedRetry(response); - - log( - `Got a response to the request to ${url}`, - { - ...fullLogParams, - logEventStatus: needRetry ? LogEventStatus.Failed : LogEventStatus.Passed, - needRetry, - response, - }, - LogEventType.InternalUtil, - ); - if (needRetry === false) { + if (response !== undefined) { return response; } diff --git a/src/utils/retry/getNewFullTestRunsByStatuses.ts b/src/utils/retry/getNewFullTestRunsByStatuses.ts new file mode 100644 index 00000000..db78c90d --- /dev/null +++ b/src/utils/retry/getNewFullTestRunsByStatuses.ts @@ -0,0 +1,37 @@ +import {TestRunStatus} from '../../constants/internal'; + +import {getNewFullTestRuns} from './getNewFullTestRuns'; + +import type {FullTestRun, RetriesState} from '../../types/internal'; + +type Return = Readonly<{ + failedNewFullTestRuns: readonly FullTestRun[]; + newFullTestRuns: readonly FullTestRun[]; + successfulNewFullTestRuns: readonly FullTestRun[]; + unbrokenNewFullTestRuns: readonly FullTestRun[]; +}>; + +/** + * Get new full test runs for different statuses by retries state. + * @internal + */ +export const getNewFullTestRunsByStatuses = async (retriesState: RetriesState): Promise => { + const newFullTestRuns = await getNewFullTestRuns(retriesState); + + const unbrokenNewFullTestRuns = newFullTestRuns.filter( + ({status}) => status !== TestRunStatus.Broken, + ); + const failedNewFullTestRuns = unbrokenNewFullTestRuns.filter( + ({status}) => status === TestRunStatus.Failed, + ); + const successfulNewFullTestRuns = unbrokenNewFullTestRuns.filter( + ({status}) => status !== TestRunStatus.Failed, + ); + + return { + failedNewFullTestRuns, + newFullTestRuns, + successfulNewFullTestRuns, + unbrokenNewFullTestRuns, + }; +}; diff --git a/src/utils/retry/processRetry.ts b/src/utils/retry/processRetry.ts index ab18d27f..a298889b 100644 --- a/src/utils/retry/processRetry.ts +++ b/src/utils/retry/processRetry.ts @@ -14,7 +14,13 @@ import type {RetriesState, UtcTimeInMs} from '../../types/internal'; * @internal */ export const processRetry = async (retriesState: RetriesState): Promise => { - const {concurrency, maxRetriesCount, retryIndex, successfulTestRunNamesHash} = retriesState; + const { + concurrency, + maxRetriesCount, + retryIndex, + successfulTestRunNamesHash, + visitedTestNamesHash, + } = retriesState; const runLabel = createRunLabel({ concurrency, disconnectedBrowsersCount: 0, @@ -34,7 +40,7 @@ export const processRetry = async (retriesState: RetriesState): Promise => try { await writeLogsToFile(); - await runRetry({concurrency, runLabel, successfulTestRunNamesHash}); + await runRetry({concurrency, runLabel, successfulTestRunNamesHash, visitedTestNamesHash}); setReadonlyProperty(retriesState, 'isLastRetrySuccessful', true); } catch (error) { diff --git a/src/utils/retry/runPackWithRetries.ts b/src/utils/retry/runPackWithRetries.ts index 354e2160..6a2cce33 100644 --- a/src/utils/retry/runPackWithRetries.ts +++ b/src/utils/retry/runPackWithRetries.ts @@ -20,7 +20,8 @@ export const runPackWithRetries = async (): Promise => { maxRetriesCount: 1, retryIndex: 1, startLastRetryTimeInMs: 0 as UtcTimeInMs, - successfulTestRunNamesHash: {}, + successfulTestRunNamesHash: Object.create(null) as {}, + visitedTestNamesHash: Object.create(null) as {}, visitedTestRunEventsFileName: [], }; diff --git a/src/utils/retry/updateRetriesStateAfterRetry.ts b/src/utils/retry/updateRetriesStateAfterRetry.ts index 33df2ce2..bab2501d 100644 --- a/src/utils/retry/updateRetriesStateAfterRetry.ts +++ b/src/utils/retry/updateRetriesStateAfterRetry.ts @@ -1,12 +1,10 @@ -import {TestRunStatus} from '../../constants/internal'; - import {assertValueIsFalse, assertValueIsTrue} from '../asserts'; import {cloneWithoutLogEvents} from '../clone'; import {getTestRunEventFileName} from '../fs'; import {setReadonlyProperty} from '../setReadonlyProperty'; import {getConcurrencyForNextRetry} from './getConcurrencyForNextRetry'; -import {getNewFullTestRuns} from './getNewFullTestRuns'; +import {getNewFullTestRunsByStatuses} from './getNewFullTestRunsByStatuses'; import {getPrintedRetry} from './getPrintedRetry'; import {logRetryResult} from './logRetryResult'; import {truncateRetriesStateForLogs} from './truncateRetriesStateForLogs'; @@ -23,19 +21,15 @@ export const updateRetriesStateAfterRetry = async (retriesState: RetriesState): maxRetriesCount, retryIndex, successfulTestRunNamesHash, + visitedTestNamesHash, visitedTestRunEventsFileName, } = retriesState; - const newFullTestRuns = await getNewFullTestRuns(retriesState); - - const unbrokenNewFullTestRuns = newFullTestRuns.filter( - ({status}) => status !== TestRunStatus.Broken, - ); - const failedNewFullTestRuns = unbrokenNewFullTestRuns.filter( - ({status}) => status === TestRunStatus.Failed, - ); - const successfulNewFullTestRuns = unbrokenNewFullTestRuns.filter( - ({status}) => status !== TestRunStatus.Failed, - ); + const { + failedNewFullTestRuns, + newFullTestRuns, + successfulNewFullTestRuns, + unbrokenNewFullTestRuns, + } = await getNewFullTestRunsByStatuses(retriesState); const printedRetry = getPrintedRetry({maxRetriesCount, retryIndex}); const successfulTotalInPreviousRetries = Object.keys(successfulTestRunNamesHash).length; @@ -50,6 +44,7 @@ export const updateRetriesStateAfterRetry = async (retriesState: RetriesState): ); successfulTestRunNamesHash[name] = true; + visitedTestNamesHash[name] = true; } for (const {runId} of newFullTestRuns) { @@ -85,6 +80,10 @@ export const updateRetriesStateAfterRetry = async (retriesState: RetriesState): const failedTestNamesInLastRetry = failedNewFullTestRuns.map(({name}) => name); + for (const name of failedTestNamesInLastRetry) { + visitedTestNamesHash[name] = true; + } + const retriesStateUpdate: Partial> = { concurrency: concurrencyForNextRetry, failedTestNamesInLastRetry, diff --git a/src/utils/test/getRunTest.ts b/src/utils/test/getRunTest.ts index cfb7c5a5..db2297a5 100644 --- a/src/utils/test/getRunTest.ts +++ b/src/utils/test/getRunTest.ts @@ -49,7 +49,7 @@ export const getRunTest = (test: Test): RunTest => { previousRunId = runId; - await runTestFn(runId, testController); + await runTestFn(runId, testController, testStaticOptions); } catch (error) { hasRunError = true; unknownRunError = error; diff --git a/src/utils/test/runTestFn.ts b/src/utils/test/runTestFn.ts index 9c6bc87b..428ea11a 100644 --- a/src/utils/test/runTestFn.ts +++ b/src/utils/test/runTestFn.ts @@ -2,11 +2,13 @@ import {setTestRunPromise} from '../../context/testRunPromise'; import {getTestTimeout} from '../../context/testTimeout'; import {getWaitForEventsState} from '../../context/waitForEventsState'; +import {getFullPackConfig} from '../config'; import {getTestRunEvent} from '../events'; +import {enableFullMocks, getShouldApplyMocks} from '../fullMocks'; import {getPromiseWithResolveAndReject} from '../promise'; import {RequestHookToWaitForEvents} from '../requestHooks'; -import type {RunId, TestController} from '../../types/internal'; +import type {RunId, TestController, TestStaticOptions} from '../../types/internal'; const delayForTestRunPromiseResolutionAfterTestTimeoutInMs = 100; @@ -14,7 +16,11 @@ const delayForTestRunPromiseResolutionAfterTestTimeoutInMs = 100; * Runs test function with reject in test run event. * @internal */ -export const runTestFn = async (runId: RunId, testController: TestController): Promise => { +export const runTestFn = async ( + runId: RunId, + testController: TestController, + testStaticOptions: TestStaticOptions, +): Promise => { const testRunEvent = getTestRunEvent(runId); const testTimeout = getTestTimeout(); @@ -29,5 +35,13 @@ export const runTestFn = async (runId: RunId, testController: TestController): P await testController.addRequestHooks(waitForEventsState.hook); + const {fullMocks} = getFullPackConfig(); + + if (fullMocks?.filterTests(testStaticOptions)) { + const shouldApplyMocks = getShouldApplyMocks(testStaticOptions.name); + + await enableFullMocks(fullMocks, shouldApplyMocks, testStaticOptions.filePath); + } + await testRunEvent.testFnWithReject().finally(() => resolveTestRunPromise(undefined)); }; diff --git a/src/utils/tests/runTests.ts b/src/utils/tests/runTests.ts index 2a13641e..738c5317 100644 --- a/src/utils/tests/runTests.ts +++ b/src/utils/tests/runTests.ts @@ -7,6 +7,7 @@ import {getFullPackConfig} from '../config'; import {getRunLabel, setRunLabel} from '../environment'; import {E2edError} from '../error'; import {generalLog, setSuccessfulTotalInPreviousRetries} from '../generalLog'; +import {setVisitedTestNamesHash} from '../globalState'; import {getNotIncludedInPackTests} from '../notIncludedInPackTests'; import {startResourceUsageReading} from '../resourceUsage'; import {setTestCafeInstance} from '../testCafe'; @@ -24,8 +25,10 @@ export const runTests = async ({ concurrency, runLabel, successfulTestRunNamesHash, + visitedTestNamesHash, }: RunRetryOptions): Promise => { setRunLabel(runLabel); + setVisitedTestNamesHash(visitedTestNamesHash); try { const successfulTotalInPreviousRetries = Object.keys(successfulTestRunNamesHash).length;