Skip to content

Commit

Permalink
FI-1236 feat: add "full mocks" functionality
Browse files Browse the repository at this point in the history
fix: pack compilation errors `is not under 'rootDir'`
  • Loading branch information
uid11 committed May 18, 2024
1 parent 154f699 commit b82fa50
Show file tree
Hide file tree
Showing 48 changed files with 837 additions and 90 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions autotests/configurator/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export type {
DoAfterPack,
DoBeforePack,
FilterTestsIntoPack,
FullMocksConfig,
GetFullPackConfig,
GetLogContext,
GetMainTestRunParams,
Expand Down
1 change: 1 addition & 0 deletions autotests/configurator/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export type {
DoAfterPack,
DoBeforePack,
FilterTestsIntoPack,
FullMocksConfig,
GetFullPackConfig,
GetLogContext,
GetMainTestRunParams,
Expand Down
1 change: 1 addition & 0 deletions autotests/configurator/types/packSpecific.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'];
Expand Down
1 change: 1 addition & 0 deletions autotests/packs/allTests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export const pack: Pack = {
enableMobileDeviceMode: false,
enableTouchEventEmulation: false,
filterTestsIntoPack,
fullMocks: null,
liteReportFileName: 'lite-report.json',
logFileName: 'pack-logs.log',
mapBackendResponseErrorToLog,
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
12 changes: 12 additions & 0 deletions src/actions/mock/mockApiRoute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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) {
Expand Down
4 changes: 2 additions & 2 deletions src/constants/testRun.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]: '',
};

/**
Expand Down
1 change: 1 addition & 0 deletions src/context/apiMockState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export const getApiMockState = (): ApiMockState => {

const apiMockState: ApiMockState = {
apiMock: undefined,
isMocksEnabled: true,
optionsByRoute: undefined,
optionsWithRouteByUrl: Object.create(null) as {},
};
Expand Down
30 changes: 30 additions & 0 deletions src/context/fullMocks.ts
Original file line number Diff line number Diff line change
@@ -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<FullMocksState>();

/**
* 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);
};
6 changes: 6 additions & 0 deletions src/types/config/ownE2edConfig.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -80,6 +81,11 @@ export type OwnE2edConfig<
*/
filterTestsIntoPack: (this: void, testStaticOptions: TestStaticOptions<TestMeta>) => 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,
Expand Down
105 changes: 105 additions & 0 deletions src/types/fullMocks.ts
Original file line number Diff line number Diff line change
@@ -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<TestMeta = TestMetaPlaceholder> = 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<TestMeta>) => 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<TestFullMocks | undefined>;

/**
* Writes full mocks of one test by `testId`.
*/
writeTestFullMocks: (
this: void,
testId: FullMocksTestId,
testFullMocks: TestFullMocks,
) => Promise<void>;
}>;

/**
* Mocked (generated) `response` for full mocks.
*/
export type FullMocksResponse = Partial<Response> & 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<RequestKind, number> | undefined;
testFullMocks: TestFullMocks;
testId: FullMocksTestId;
}>;

/**
* Identifier of test (usually the hash of test file content).
*/
export type FullMocksTestId = Brand<string, 'FullMocksTestId'>;

/**
* Identifier of request in set of requests for one test (usually just `path` of request url).
*/
export type RequestKind = Brand<string, 'RequestKind'>;

/**
* Full mocks of one test.
*/
export type TestFullMocks = Readonly<Record<RequestKind, readonly ResponseWithRequest[]>>;
7 changes: 7 additions & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
11 changes: 10 additions & 1 deletion src/types/internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 */
Expand Down
1 change: 1 addition & 0 deletions src/types/mockApiRoute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export type ApiMockFunction<
*/
export type ApiMockState = Readonly<{
apiMock: Inner.RequestMock | undefined;
isMocksEnabled: boolean;
optionsByRoute: Map<ApiRouteClassTypeWithGetParamsFromUrl, MockOptions> | undefined;
optionsWithRouteByUrl: Record<Url, MockOptionsWithRoute | undefined>;
}>;
10 changes: 9 additions & 1 deletion src/types/retries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export type RetriesState = Readonly<{
retryIndex: number;
startLastRetryTimeInMs: UtcTimeInMs;
successfulTestRunNamesHash: Record<string, true>;
visitedTestNamesHash: Record<string, true>;
visitedTestRunEventsFileName: readonly string[];
}>;

Expand All @@ -24,5 +25,12 @@ export type RetriesState = Readonly<{
export type RunRetryOptions = Readonly<{
concurrency: number;
runLabel: RunLabel;
successfulTestRunNamesHash: Record<string, true>;
successfulTestRunNamesHash: VisitedTestNamesHash;
visitedTestNamesHash: VisitedTestNamesHash;
}>;

/**
* Hash of names of already visited tests (maybe, in previous retries).
* @internal
*/
export type VisitedTestNamesHash = Readonly<Record<string, true>>;
2 changes: 2 additions & 0 deletions src/types/userland/createPackSpecificTypes.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -16,6 +17,7 @@ export type CreatePackSpecificTypes<
DoAfterPack: FullPackConfigByPack<Pack>['doAfterPack'][number];
DoBeforePack: FullPackConfigByPack<Pack>['doBeforePack'][number];
FilterTestsIntoPack: Pack['filterTestsIntoPack'];
FullMocksConfig: FullMocksConfig<PackParameters['TestMeta']>;
GetFullPackConfig: () => FullPackConfigByPack<Pack>;
GetLogContext: Hooks['getLogContext'];
GetMainTestRunParams: Hooks['getMainTestRunParams'];
Expand Down
16 changes: 16 additions & 0 deletions src/utils/events/registerEndTestRunEvent.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {TestRunStatus} from '../../constants/internal';
import {getFullMocksState} from '../../context/fullMocks';

import {cloneWithoutLogEvents} from '../clone';
import {getRunErrorFromError} from '../error';
Expand All @@ -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';

Expand Down Expand Up @@ -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;
Expand Down
24 changes: 24 additions & 0 deletions src/utils/events/writeFullMocks.ts
Original file line number Diff line number Diff line change
@@ -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<void> => {
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,
});
};
Loading

0 comments on commit b82fa50

Please sign in to comment.