diff --git a/README.md b/README.md index e196627f..dde374a0 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: FullMocks | 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/fullMocks.ts b/autotests/configurator/fullMocks.ts new file mode 100644 index 00000000..e8d39b39 --- /dev/null +++ b/autotests/configurator/fullMocks.ts @@ -0,0 +1,36 @@ +import {readFile, writeFile} from 'node:fs/promises'; +import {join} from 'node:path'; + +import {BAD_REQUEST_STATUS_CODE, READ_FILE_OPTIONS} from 'e2ed/constants'; + +import type {FullMocks} from 'autotests/configurator'; +import type {FilePathFromRoot, FullMocksTestId, RequestKind, TestFullMocks} from 'e2ed/types'; + +const fullMocksStoragePath = join('autotests', 'fixtures', 'fullMocks'); + +const getTestFullMocksPath = (testId: FullMocksTestId): FilePathFromRoot => + join(fullMocksStoragePath, `${testId}.json`) as FilePathFromRoot; + +export const fullMocks: FullMocks = { + filterTests: ({options: {meta}}) => meta.testId === '18', + getRequestKind: (method, {pathname}) => pathname as RequestKind, + getResponseFromFullMocks: ({responseWithRequest}) => + responseWithRequest ?? {statusCode: BAD_REQUEST_STATUS_CODE}, + getResponseToWriteToFullMocks: (requestKind, responseWithRequest) => responseWithRequest, + readTestFullMocks: async (testId) => { + const testFullMocksJson = await readFile(getTestFullMocksPath(testId), READ_FILE_OPTIONS).catch( + () => undefined, + ); + + if (testFullMocksJson === undefined) { + return undefined; + } + + return JSON.parse(testFullMocksJson) as TestFullMocks; + }, + writeTestFullMocks: async (testId, testFullMocks) => { + const testFullMocksJson = JSON.stringify(testFullMocks); + + await writeFile(getTestFullMocksPath(testId), testFullMocksJson); + }, +}; diff --git a/autotests/configurator/index.ts b/autotests/configurator/index.ts index 2b5074f5..5be1fda1 100644 --- a/autotests/configurator/index.ts +++ b/autotests/configurator/index.ts @@ -1,5 +1,6 @@ export {doAfterPack} from './doAfterPack'; export {doBeforePack} from './doBeforePack'; +export {fullMocks} from './fullMocks'; export {mapBackendResponseErrorToLog} from './mapBackendResponseErrorToLog'; export {mapBackendResponseToLog} from './mapBackendResponseToLog'; export {mapLogPayloadInConsole} from './mapLogPayloadInConsole'; @@ -10,6 +11,7 @@ export type { DoAfterPack, DoBeforePack, FilterTestsIntoPack, + FullMocks, GetFullPackConfig, GetLogContext, GetMainTestRunParams, diff --git a/autotests/configurator/types/index.ts b/autotests/configurator/types/index.ts index dd5ae5f3..8360652d 100644 --- a/autotests/configurator/types/index.ts +++ b/autotests/configurator/types/index.ts @@ -2,6 +2,7 @@ export type { DoAfterPack, DoBeforePack, FilterTestsIntoPack, + FullMocks, GetFullPackConfig, GetLogContext, GetMainTestRunParams, diff --git a/autotests/configurator/types/packSpecific.ts b/autotests/configurator/types/packSpecific.ts index f17c76c2..90c6d6da 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 FullMocks = PackSpecificTypes['FullMocks']; export type IsTestSkipped = PackSpecificTypes['IsTestSkipped']; export type LiteReport = PackSpecificTypes['LiteReport']; export type MapBackendResponseErrorToLog = PackSpecificTypes['MapBackendResponseErrorToLog']; diff --git a/autotests/entities/index.ts b/autotests/entities/index.ts index 8673dc78..ace9be88 100644 --- a/autotests/entities/index.ts +++ b/autotests/entities/index.ts @@ -1,3 +1,4 @@ export {createDevice} from './device'; +export {addProduct} from './product'; export {createUser} from './user'; export {addUser, getUsers} from './worker'; diff --git a/autotests/entities/product.ts b/autotests/entities/product.ts new file mode 100644 index 00000000..5ecda081 --- /dev/null +++ b/autotests/entities/product.ts @@ -0,0 +1,21 @@ +import {createClientFunction} from 'e2ed'; + +import type {ApiProduct, Product} from 'autotests/types'; + +/** + * Adds product. + */ +export const addProduct = createClientFunction( + (product: Product) => + fetch(`https://reqres.in/api/product/${product.id}?size=${product.size}`, { + body: JSON.stringify({ + cookies: [], + input: product.input, + model: product.model, + version: product.version, + }), + headers: {'Content-Type': 'application/json; charset=UTF-8'}, + method: 'POST', + }).then((res) => res.json() as Promise), + {name: 'addProduct', timeout: 2_000}, +); diff --git a/autotests/fixtures/fullMocks/gitkeep b/autotests/fixtures/fullMocks/gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/autotests/fixtures/fullMocks/jKDXNUZ75U.json b/autotests/fixtures/fullMocks/jKDXNUZ75U.json new file mode 100644 index 00000000..4be152c1 --- /dev/null +++ b/autotests/fixtures/fullMocks/jKDXNUZ75U.json @@ -0,0 +1 @@ +{"/api/product/135865":[{"completionTimeInMs":1716205312921,"duration":"4ms","request":{"method":"POST","query":{"size":"13"},"requestBody":{"cookies":[],"input":17,"model":"samsung","version":"12"},"requestHeaders":{"accept":"*/*","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"},"url":"https://reqres.in/api/product/135865?size=13","utcTimeInMs":1716205312917},"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-length":"201","content-type":"application/json; charset=UTF-8"},"statusCode":200},{"completionTimeInMs":1716205313198,"duration":"255ms","request":{"method":"POST","query":{"size":"13"},"requestBody":{"cookies":[],"input":17,"model":"samsung","version":"12"},"requestHeaders":{"accept":"*/*","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"},"url":"https://reqres.in/api/product/135865?size=13","utcTimeInMs":1716205312943},"responseBody":{"cookies":[],"input":17,"model":"samsung","version":"12","id":"713","createdAt":"2024-05-20T11:41:53.144Z"},"responseHeaders":{"date":"Mon, 20 May 2024 11:41:53 GMT","content-type":"application/json; charset=utf-8","content-length":"108","report-to":"{\"group\":\"heroku-nel\",\"max_age\":3600,\"endpoints\":[{\"url\":\"https://nel.heroku.com/reports?ts=1716205313&sid=c4c9725f-1ab0-44d8-820f-430df2718e11&s=mbk9hukht3cdEhvMVZlrntLk5LPKjfRJrh277niKK2Q%3D\"}]}","reporting-endpoints":"heroku-nel=https://nel.heroku.com/reports?ts=1716205313&sid=c4c9725f-1ab0-44d8-820f-430df2718e11&s=mbk9hukht3cdEhvMVZlrntLk5LPKjfRJrh277niKK2Q%3D","nel":"{\"report_to\":\"heroku-nel\",\"max_age\":3600,\"success_fraction\":0.005,\"failure_fraction\":0.05,\"response_headers\":[\"Via\"]}","x-powered-by":"Express","access-control-allow-origin":"*","etag":"W/\"6c-640Odu4IHdHX5uCM17XShtUIvGU\"","via":"1.1 vegur","cf-cache-status":"DYNAMIC","server":"cloudflare","cf-ray":"886c0f66bc3265cc-FRA"},"statusCode":201}]} \ No newline at end of file diff --git a/autotests/packs/allTests.ts b/autotests/packs/allTests.ts index f55313fe..26dd0291 100644 --- a/autotests/packs/allTests.ts +++ b/autotests/packs/allTests.ts @@ -1,8 +1,8 @@ /** * @file Pack file (file with configuration of pack). * Do not import anything (from `utils`, etc) into this file other than - * the types and values from `../configurator`, `e2ed/configurator` or other packs - * (because the pack is compiled separately from the tests themselves + * the types and values from `../configurator`, `e2ed/configurator`, `e2ed/constants` + * or other packs (because the pack is compiled separately from the tests themselves * and has separate TypeScript scope). */ @@ -11,6 +11,7 @@ import {isLocalRun} from 'e2ed/configurator'; import { doAfterPack, doBeforePack, + fullMocks, mapBackendResponseErrorToLog, mapBackendResponseToLog, mapLogPayloadInConsole, @@ -56,6 +57,7 @@ export const pack: Pack = { enableMobileDeviceMode: false, enableTouchEventEmulation: false, filterTestsIntoPack, + fullMocks, liteReportFileName: 'lite-report.json', logFileName: 'pack-logs.log', mapBackendResponseErrorToLog, @@ -77,7 +79,7 @@ export const pack: Pack = { skipTests, takeFullPageScreenshotOnError: false, takeViewportScreenshotOnError: true, - testFileGlobs: ['./autotests/tests/**/*.ts', '!**/*.skip.ts'], + testFileGlobs: ['./autotests/tests/**/fullMocks.ts', '!**/*.skip.ts'], testIdleTimeout: 20_000, testTimeout: 60_000, viewportHeight: 1080, diff --git a/autotests/pageObjects/MobilePage.ts b/autotests/pageObjects/MobilePage.ts index 651764a7..939e78f3 100644 --- a/autotests/pageObjects/MobilePage.ts +++ b/autotests/pageObjects/MobilePage.ts @@ -1,8 +1,8 @@ import {Page} from 'e2ed'; -import type {MobileDevice} from 'autotests/types'; +import type {MobileDeviceModel} from 'autotests/types'; -type PageParams = CustomPageParams & Readonly<{mobileDevice?: MobileDevice}>; +type PageParams = CustomPageParams & Readonly<{mobileDevice?: MobileDeviceModel}>; /** * Abstract mobile page. @@ -11,5 +11,5 @@ export abstract class MobilePage extends Page; +type Params = Readonly<{model: MobileDeviceModel}>; /** * Test API route for creating a device. diff --git a/autotests/routes/apiRoutes/CreateProduct.ts b/autotests/routes/apiRoutes/CreateProduct.ts index cde1607c..9dd9889d 100644 --- a/autotests/routes/apiRoutes/CreateProduct.ts +++ b/autotests/routes/apiRoutes/CreateProduct.ts @@ -3,10 +3,10 @@ import {URL} from 'node:url'; import {ApiRoute} from 'autotests/routes'; import {assertValueIsTrue} from 'e2ed/utils'; -import type {ApiCreateProductRequest, ApiCreateProductResponse} from 'autotests/types'; +import type {ApiCreateProductRequest, ApiCreateProductResponse, ProductId} from 'autotests/types'; import type {Url} from 'e2ed/types'; -type Params = Readonly<{id: number; size: number}>; +type Params = Readonly<{id: ProductId; size: number}>; const pathStart = '/api/product/'; @@ -27,7 +27,7 @@ export class CreateProduct extends ApiRoute< {urlObject}, ); - const id = Number(urlObject.pathname.slice(pathStart.length)); + const id = Number(urlObject.pathname.slice(pathStart.length)) as ProductId; const size = Number(urlObject.searchParams.get('size')); assertValueIsTrue(Number.isInteger(id), 'url has correct id', {id, size, urlObject}); diff --git a/autotests/tests/e2edReportExample/fullMocks.ts b/autotests/tests/e2edReportExample/fullMocks.ts new file mode 100644 index 00000000..c966d951 --- /dev/null +++ b/autotests/tests/e2edReportExample/fullMocks.ts @@ -0,0 +1,60 @@ +import {test} from 'autotests'; +import {addProduct} from 'autotests/entities'; +import {E2edReportExample} from 'autotests/pageObjects/pages'; +import {CreateProduct as CreateProductRoute} from 'autotests/routes/apiRoutes'; +import {expect} from 'e2ed'; +import {mockApiRoute, navigateToPage, unmockApiRoute} from 'e2ed/actions'; + +import type {DeviceId, Product, ProductId} from 'autotests/types'; +import type {Url} from 'e2ed/types'; + +test('full mocks works correctly', {meta: {testId: '18'}}, async () => { + await navigateToPage(E2edReportExample); + + await mockApiRoute(CreateProductRoute, (routeParams, {method, query, requestBody, url}) => { + const responseBody = { + id: routeParams.id, + method, + output: String(requestBody.input), + payload: {id: String(routeParams.id) as DeviceId, ...requestBody}, + query, + url, + }; + + return {responseBody}; + }); + + const productId = Number('135865') as ProductId; + const product: Product = { + id: productId, + input: 17, + model: 'samsung', + size: '13', + version: '12', + }; + + const mockedProduct = await addProduct(product); + + const fetchUrl = `https://reqres.in/api/product/${productId}?size=${product.size}` as Url; + + await expect(mockedProduct, 'mocked API returns correct result').eql({ + id: productId, + method: 'POST', + output: String(product.input), + payload: { + cookies: [], + id: String(productId) as DeviceId, + input: product.input, + model: product.model, + version: product.version, + }, + query: {size: product.size}, + url: fetchUrl, + }); + + await unmockApiRoute(CreateProductRoute); + + const newMockedProduct = await addProduct(product); + + await expect('createdAt' in newMockedProduct, 'API mock on CreateProductRoute was umocked').ok(); +}); diff --git a/autotests/tests/internalTypeTests/mockApiRoute.skip.ts b/autotests/tests/internalTypeTests/mockApiRoute.skip.ts index 17f7859a..7abe5762 100644 --- a/autotests/tests/internalTypeTests/mockApiRoute.skip.ts +++ b/autotests/tests/internalTypeTests/mockApiRoute.skip.ts @@ -3,16 +3,22 @@ import {Main} from 'autotests/routes/pageRoutes'; import {mockApiRoute, unmockApiRoute} from 'e2ed/actions'; import {CREATED_STATUS_CODE, OK_STATUS_CODE} from 'e2ed/constants'; -import type {ApiCreateDeviceRequest, ApiCreateDeviceResponse, DeviceId} from 'autotests/types'; +import type { + ApiCreateDeviceRequest, + ApiCreateDeviceResponse, + DeviceId, + ProductId, +} from 'autotests/types'; const apiMockFunction = ( routeParams: object, {method, query, requestBody, url}: ApiCreateDeviceRequest, ): Partial => { const {input} = requestBody; + const productId = 12; const responseBody = { - id: 12, + id: productId as ProductId, method, output: String(input), payload: {id: '12' as DeviceId, ...requestBody}, @@ -49,9 +55,10 @@ void mockApiRoute( {method, requestBody, query, url}, // eslint-disable-next-line @typescript-eslint/require-await ) => { const {input} = requestBody; + const productId = 7; const responseBody = { - id: 7, + id: productId as ProductId, method, output: `${input}${routeParams.id}`, payload: {id: '7' as DeviceId, ...requestBody}, diff --git a/autotests/tests/internalTypeTests/pages.skip.ts b/autotests/tests/internalTypeTests/pages.skip.ts index 1272cc2c..f042f1b4 100644 --- a/autotests/tests/internalTypeTests/pages.skip.ts +++ b/autotests/tests/internalTypeTests/pages.skip.ts @@ -6,7 +6,7 @@ import {Main, Search, Services} from 'autotests/pageObjects/pages'; import {navigateToPage} from 'e2ed/actions'; /** - * PageParams = Readonly<{mobileDevice?: MobileDevice, query?: string}> + * PageParams = Readonly<{mobileDevice?: MobileDeviceModel, query?: string}> */ // ok diff --git a/autotests/tests/internalTypeTests/request.skip.ts b/autotests/tests/internalTypeTests/request.skip.ts index c2039e31..1a5598c6 100644 --- a/autotests/tests/internalTypeTests/request.skip.ts +++ b/autotests/tests/internalTypeTests/request.skip.ts @@ -3,10 +3,10 @@ import {Main} from 'autotests/routes/pageRoutes'; import {getRandomId} from 'e2ed/generators'; import {request} from 'e2ed/utils'; -import type {ApiDevice, ApiDeviceParams, ApiUserParams, MobileDevice} from 'autotests/types'; +import type {ApiDevice, ApiDeviceParams, ApiUserParams, MobileDeviceModel} from 'autotests/types'; declare const apiUserParams: ApiUserParams; -declare const model: MobileDevice; +declare const model: MobileDeviceModel; declare const apiDeviceParams: ApiDeviceParams; // @ts-expect-error: request require API route as first argument diff --git a/autotests/tests/mockApiRoute.ts b/autotests/tests/mockApiRoute.ts index e0c53e75..7d985e5b 100644 --- a/autotests/tests/mockApiRoute.ts +++ b/autotests/tests/mockApiRoute.ts @@ -1,9 +1,10 @@ import {test} from 'autotests'; +import {addProduct} from 'autotests/entities'; import {CreateProduct as CreateProductRoute} from 'autotests/routes/apiRoutes'; -import {createClientFunction, expect} from 'e2ed'; +import {expect} from 'e2ed'; import {mockApiRoute, unmockApiRoute} from 'e2ed/actions'; -import type {ApiCreateDeviceResponse, DeviceId} from 'autotests/types'; +import type {DeviceId, Product, ProductId} from 'autotests/types'; import type {Url} from 'e2ed/types'; test( @@ -23,42 +24,45 @@ test( return {responseBody}; }); - const getMockedProduct = createClientFunction( - () => - fetch('https://reqres.in/api/product/135865?size=13', { - body: JSON.stringify({cookies: [], input: 17, model: 'samsung', version: '12'}), - headers: {'Content-Type': 'application/json; charset=UTF-8'}, - method: 'POST', - }).then((res) => res.json() as Promise), - {name: 'getMockedProduct', timeout: 2_000}, - ); + const productId = 135865; + const product: Product = { + id: productId as ProductId, + input: 17, + model: 'samsung', + size: '13', + version: '12', + }; - const mockedProduct = await getMockedProduct(); + const mockedProduct = await addProduct(product); - const fetchUrl = 'https://reqres.in/api/product/135865?size=13' as Url; + const fetchUrl = `https://reqres.in/api/product/${productId}?size=${product.size}` as Url; const productRouteParams = CreateProductRoute.getParamsFromUrl(fetchUrl); const productRouteFromUrl = new CreateProductRoute(productRouteParams); + await expect(productRouteFromUrl.routeParams.id, 'route has correct params').eql( + productId as ProductId, + ); + await expect(mockedProduct, 'mocked API returns correct result').eql({ id: productRouteFromUrl.routeParams.id, method: productRouteFromUrl.getMethod(), - output: '17', + output: String(product.input), payload: { cookies: [], id: String(productRouteFromUrl.routeParams.id) as DeviceId, - input: 17, - model: 'samsung', - version: '12', + input: product.input, + model: product.model, + version: product.version, }, - query: {size: '13'}, + query: {size: product.size}, url: fetchUrl, }); await unmockApiRoute(CreateProductRoute); - const newMockedProduct = (await getMockedProduct().catch(() => undefined)) ?? {createdAt: ''}; + const newMockedProduct = await addProduct(product); await expect( 'createdAt' in newMockedProduct, diff --git a/autotests/types/api/CreateDevice.ts b/autotests/types/api/CreateDevice.ts index 9a32fdce..cf0bafcd 100644 --- a/autotests/types/api/CreateDevice.ts +++ b/autotests/types/api/CreateDevice.ts @@ -1,17 +1,9 @@ -import type {ApiDevice, ApiDeviceParams} from 'autotests/types'; -import type {Method, Query, Request, Response, Url} from 'e2ed/types'; +import type {ApiDeviceParams, ApiProduct} from 'autotests/types'; +import type {Query, Request, Response} from 'e2ed/types'; type RequestBody = ApiDeviceParams; -type ResponseBody = Readonly<{ - id: number; - method: Method; - output: string; - payload: ApiDevice; - query: Query; - url: Url; -}>; - +type ResponseBody = ApiProduct; /** * API request for create device endpoint. */ diff --git a/autotests/types/entities/device.ts b/autotests/types/entities/device.ts index 4dadfba1..9c364757 100644 --- a/autotests/types/entities/device.ts +++ b/autotests/types/entities/device.ts @@ -11,7 +11,7 @@ export type DeviceId = Brand; export type ApiDeviceParams = Readonly<{ cookies: readonly string[]; input: number; - model: MobileDevice; + model: MobileDeviceModel; version: string; }>; @@ -33,4 +33,4 @@ export type ApiDevice = Device; /** * Mobile device type. */ -export type MobileDevice = 'iphone' | 'samsung'; +export type MobileDeviceModel = 'iphone' | 'samsung'; diff --git a/autotests/types/entities/index.ts b/autotests/types/entities/index.ts index 5e248072..b2deffd7 100644 --- a/autotests/types/entities/index.ts +++ b/autotests/types/entities/index.ts @@ -4,8 +4,9 @@ export type { Device, DeviceId, DeviceParams, - MobileDevice, + MobileDeviceModel, } from './device'; +export type {ApiProduct, Product, ProductId} from './product'; export type { ApiUser, ApiUserParams, diff --git a/autotests/types/entities/product.ts b/autotests/types/entities/product.ts new file mode 100644 index 00000000..9b60f302 --- /dev/null +++ b/autotests/types/entities/product.ts @@ -0,0 +1,30 @@ +import type {ApiDevice, MobileDeviceModel} from 'autotests/types'; +import type {Brand, Method, Query, Url} from 'e2ed/types'; + +/** + * Product id. + */ +export type ProductId = Brand; + +/** + * Product object. + */ +export type Product = Readonly<{ + id: ProductId; + input: number; + model: MobileDeviceModel; + size: string; + version: string; +}>; + +/** + * Product object returned by API. + */ +export type ApiProduct = Readonly<{ + id: ProductId; + method: Method; + output: string; + payload: ApiDevice; + query: Query; + url: Url; +}>; diff --git a/autotests/types/index.ts b/autotests/types/index.ts index 1335a383..881f7874 100644 --- a/autotests/types/index.ts +++ b/autotests/types/index.ts @@ -15,6 +15,7 @@ export type { export type { ApiDevice, ApiDeviceParams, + ApiProduct, ApiUser, ApiUserParams, Device, @@ -22,8 +23,10 @@ export type { DeviceParams, Email, Language, - MobileDevice, + MobileDeviceModel, Password, + Product, + ProductId, User, UserId, UserParams, diff --git a/package.json b/package.json index a01d28fa..4e34f09b 100644 --- a/package.json +++ b/package.json @@ -75,10 +75,10 @@ "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", + "lint:prettier": "prettier --cache --cache-location=./node_modules/.cache/lint-prettier --cache-strategy=content --check --ignore-path=.gitignore . !autotests/fixtures/fullMocks !docs/index.html", "lint:types": "npm run parallel lint:types:base lint:types:build", "lint:types:base": "tsc --noEmit", "lint:types:build": "if [ -f ./build/tsconfig.json ]; then tsc --noEmit --project ./build; else echo 'No build directory'; fi", 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/fs.ts b/src/constants/fs.ts index 657c7960..5171e31b 100644 --- a/src/constants/fs.ts +++ b/src/constants/fs.ts @@ -5,13 +5,12 @@ export const AMOUNT_OF_PARALLEL_OPEN_FILES = 40; /** - * Default options for `readFile`/`readFileSync` function from `node:fs`. + * Default file chunk length (for `writeFile`). * @internal */ -export const READ_FILE_OPTIONS = {encoding: 'utf8'} as const; +export const DEFAULT_FILE_CHUNK_LENGTH = 16_384; /** - * Default file chunk length (for `writeFile`). - * @internal + * Default options for `readFile`/`readFileSync` function from `node:fs`. */ -export const DEFAULT_FILE_CHUNK_LENGTH = 16_384; +export const READ_FILE_OPTIONS = {encoding: 'utf8'} as const; diff --git a/src/constants/index.ts b/src/constants/index.ts index e756fd2c..1c4a5446 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -1,4 +1,5 @@ export {EndE2edReason, ExitCode} from './end'; +export {READ_FILE_OPTIONS} from './fs'; export { BAD_REQUEST_STATUS_CODE, CREATED_STATUS_CODE, diff --git a/src/constants/internal.ts b/src/constants/internal.ts index aa63be84..91dc77bb 100644 --- a/src/constants/internal.ts +++ b/src/constants/internal.ts @@ -12,8 +12,9 @@ export { RUN_LABEL_VARIABLE_NAME, START_TIME_IN_MS_VARIABLE_NAME, } from './environment'; +export {READ_FILE_OPTIONS} from './fs'; /** @internal */ -export {AMOUNT_OF_PARALLEL_OPEN_FILES, DEFAULT_FILE_CHUNK_LENGTH, READ_FILE_OPTIONS} from './fs'; +export {AMOUNT_OF_PARALLEL_OPEN_FILES, DEFAULT_FILE_CHUNK_LENGTH} from './fs'; export { BAD_REQUEST_STATUS_CODE, CREATED_STATUS_CODE, 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..9ea3c527 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..6cab5beb 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']; + FullMocks: 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..ae90e416 --- /dev/null +++ b/src/utils/fullMocks/enableFullMocks.ts @@ -0,0 +1,79 @@ +// 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 {LogEventStatus, 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; + +/** + * 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}, + ).catch((cause: unknown) => { + log( + 'Caught an error in "waitForResponse" for full mocks', + {cause, fullMocksState, logEventStatus: LogEventStatus.Failed}, + LogEventType.InternalUtil, + ); + }); + } +}; 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;