Skip to content

Commit

Permalink
Merge pull request #95 from joomcode/feat/add-mocks-for-web-sockets
Browse files Browse the repository at this point in the history
feat: add mocks for web sockets
  • Loading branch information
uid11 authored Nov 28, 2024
2 parents 07719bb + 549ae91 commit 0f8b255
Show file tree
Hide file tree
Showing 48 changed files with 605 additions and 137 deletions.
7 changes: 3 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -325,16 +325,15 @@ If the mapping returns `undefined`, the log entry is not skipped, but is printed
`your-project/autotests/bin/runDocker.sh` (until the test passes).
For example, if it is equal to three, the test will be run no more than three times.

`navigationTimeout: number`: default timeout for navigation to url
(`navigateToPage`, `navigateToUrl` actions) in milliseconds.

`overriddenConfigFields: PlaywrightTestConfig | null`: if not `null`, then this value will override
fields of internal Playwright config.

`packTimeout: number`: timeout (in millisecond) for the entire pack of tests (tasks).
If the test pack takes longer than this timeout, the pack will fail with the appropriate error.

`pageStabilizationInterval: number`: after navigating to the page, `e2ed` will wait until
the page is stable for the specified time in millisecond, and only after that it will consider the page loaded.
This parameter can be overridden on a specific page instance.

`pathToScreenshotsDirectoryForReport: string | null`: path to the directory where screenshots
will be stored for displaying them in the HTML report.
This path must be either relative (from the HTML report file) or absolute (i.e. with http/https protocol).
Expand Down
3 changes: 1 addition & 2 deletions autotests/packs/allTests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,9 @@ export const pack: Pack = {
mapLogPayloadInLogFile,
mapLogPayloadInReport,
maxRetriesCountInDocker: 3,
navigationTimeout: 6_000,
overriddenConfigFields: null,
packTimeout: packTimeoutInMinutes * msInMinute,
pageRequestTimeout: 7_000,
pageStabilizationInterval: 500,
pathToScreenshotsDirectoryForReport: './screenshots',
port1: 1337,
port2: 1338,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ export class E2edReportExample extends Page<CustomPageParams> {
readonly navigationRetriesButtonSelected: Selector =
this.navigationRetriesButton.filterByLocatorParameter('selected', 'true');

override readonly navigationTimeout = 5_000;

/**
* Cookies that we set (additionally) on a page before navigating to it.
*/
Expand All @@ -47,8 +49,6 @@ export class E2edReportExample extends Page<CustomPageParams> {
*/
readonly pageRequestHeaders: StringHeaders | undefined;

override readonly pageStabilizationInterval = 600;

/**
* Test run button.
*/
Expand Down
33 changes: 17 additions & 16 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
},
"devDependencies": {
"@playwright/browser-chromium": "1.49.0",
"@types/node": "22.9.0",
"@types/node": "22.10.1",
"@typescript-eslint/eslint-plugin": "7.18.0",
"@typescript-eslint/parser": "7.18.0",
"assert-modules-support-case-insensitive-fs": "1.0.1",
Expand All @@ -44,8 +44,8 @@
"eslint-plugin-simple-import-sort": "12.1.1",
"eslint-plugin-typescript-sort-keys": "3.3.0",
"husky": "9.1.7",
"prettier": "3.3.3",
"typescript": "5.6.3"
"prettier": "3.4.1",
"typescript": "5.7.2"
},
"peerDependencies": {
"@types/node": ">=20",
Expand Down
22 changes: 10 additions & 12 deletions src/Page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {reloadDocument} from './utils/document';
import {getPlaywrightPage} from './useContext';

import type {PageRoute} from './PageRoute';
import type {AsyncVoid, PageClassTypeArgs, Url} from './types/internal';
import type {AsyncVoid, NavigateToUrlOptions, PageClassTypeArgs, Url} from './types/internal';

/**
* Abstract page with base methods.
Expand All @@ -33,17 +33,15 @@ export abstract class Page<PageParams = undefined> {
readonly maxIntervalBetweenRequestsInMs: number;

/**
* Immutable page parameters.
* Default timeout for navigation to url (`navigateToPage`, `navigateToUrl` actions) in milliseconds.
* The default value is taken from the corresponding field of the pack config.
*/
readonly pageParams: PageParams;
readonly navigationTimeout: number;

/**
* After navigating to the page, `e2ed` will wait until
* the page is stable for the specified time in millisecond,
* and only after that it will consider the page loaded.
* The default value is taken from the corresponding field of the pack config.
* Immutable page parameters.
*/
readonly pageStabilizationInterval: number;
readonly pageParams: PageParams;

constructor(...args: PageClassTypeArgs<PageParams>) {
const [createPageToken, pageParams] = args;
Expand All @@ -56,12 +54,12 @@ export abstract class Page<PageParams = undefined> {
this.pageParams = pageParams as PageParams;

const {
pageStabilizationInterval,
navigationTimeout,
waitForAllRequestsComplete: {maxIntervalBetweenRequestsInMs},
} = getFullPackConfig();

this.maxIntervalBetweenRequestsInMs = maxIntervalBetweenRequestsInMs;
this.pageStabilizationInterval = pageStabilizationInterval;
this.navigationTimeout = navigationTimeout;
}

/**
Expand Down Expand Up @@ -114,8 +112,8 @@ export abstract class Page<PageParams = undefined> {
/**
* Navigates to the page by url.
*/
navigateToPage(url: Url): Promise<void> {
return navigateToUrl(url, {skipLogs: true});
navigateToPage(url: Url, options?: NavigateToUrlOptions): Promise<void> {
return navigateToUrl(url, {skipLogs: true, timeout: this.navigationTimeout, ...options});
}

/**
Expand Down
19 changes: 10 additions & 9 deletions src/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Modules in the dependency graph should only import the modules above them:
2. `constants`
3. `configurator`
4. `generators`
5. `utils/browser`
5. `utils/parse`
6. `utils/getDurationWithUnits`
7. `utils/setReadonlyProperty`
8. `utils/selectors`
Expand Down Expand Up @@ -40,11 +40,12 @@ Modules in the dependency graph should only import the modules above them:
33. `Route`
34. `ApiRoute`
35. `PageRoute`
36. `testController`
37. `useContext`
38. `context`
39. `utils/log`
40. `utils/waitForEvents`
41. `utils/expect`
42. `expect`
43. ...
36. `WebSocketRoute`
37. `testController`
38. `useContext`
39. `context`
40. `utils/log`
41. `utils/waitForEvents`
42. `utils/expect`
43. `expect`
44. ...
4 changes: 2 additions & 2 deletions src/Route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {SLASHES_AT_THE_END_REGEXP, SLASHES_AT_THE_START_REGEXP} from './constants/internal';

import type {Method, Url, ZeroOrOneArg} from './types/internal';
import type {Url, ZeroOrOneArg} from './types/internal';

/**
* Abstract route with base methods.
Expand All @@ -25,7 +25,7 @@ export abstract class Route<RouteParams> {
* Returns route params from the passed url.
* @throws {Error} If the route does not match on the url.
*/
static getParamsFromUrlOrThrow?(url: Url, method?: Method): unknown;
static getParamsFromUrlOrThrow?(url: Url): unknown;

/**
* Returns the url of the route.
Expand Down
36 changes: 36 additions & 0 deletions src/WebSocketRoute.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import {Route} from './Route';

/**
* Abstract route for WebSocket "requests".
*/
export abstract class WebSocketRoute<
Params = undefined,
SomeRequest = unknown,
SomeResponse = unknown,
> extends Route<Params> {
/**
* Request type of WebSocket route.
*/
// eslint-disable-next-line @typescript-eslint/naming-convention
declare readonly __REQUEST_KEY: SomeRequest;

/**
* Response type of WebSocket route.
*/
// eslint-disable-next-line @typescript-eslint/naming-convention
declare readonly __RESPONSE_KEY: SomeResponse;

/**
* Returns `true`, if the request body is in JSON format.
*/
getIsRequestBodyInJsonFormat(): boolean {
return true;
}

/**
* Returns `true`, if the response body is in JSON format.
*/
getIsResponseBodyInJsonFormat(): boolean {
return true;
}
}
2 changes: 1 addition & 1 deletion src/actions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export {getBrowserConsoleMessages} from './getBrowserConsoleMessages';
export {getBrowserJsErrors} from './getBrowserJsErrors';
export {getCookies} from './getCookies';
export {hover} from './hover';
export {mockApiRoute, unmockApiRoute} from './mock';
export {mockApiRoute, mockWebSocketRoute, unmockApiRoute, unmockWebSocketRoute} from './mock';
export {navigateToUrl} from './navigateToUrl';
export {
assertPage,
Expand Down
2 changes: 2 additions & 0 deletions src/actions/mock/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
export {mockApiRoute} from './mockApiRoute';
export {mockWebSocketRoute} from './mockWebSocketRoute';
export {unmockApiRoute} from './unmockApiRoute';
export {unmockWebSocketRoute} from './unmockWebSocketRoute';
2 changes: 1 addition & 1 deletion src/actions/mock/mockApiRoute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import type {
* Mock API for some API route.
* Applicable only for routes with the `getParamsFromUrlOrThrow` method.
* The mock is applied to a request that matches the route by url
* (by methods `getParamsFromUrlOrThrow` and `isMatchUrl`) and by HTTP method (by `getMethod`).
* (by methods `getParamsFromUrlOrThrow` and `isMatchUrl`).
*/
export const mockApiRoute = async <
RouteParams,
Expand Down
80 changes: 80 additions & 0 deletions src/actions/mock/mockWebSocketRoute.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import {LogEventType} from '../../constants/internal';
import {getFullMocksState} from '../../context/fullMocks';
import {getWebSocketMockState} from '../../context/webSocketMockState';
import {getPlaywrightPage} from '../../useContext';
import {assertValueIsDefined} from '../../utils/asserts';
import {setCustomInspectOnFunction} from '../../utils/fn';
import {log} from '../../utils/log';
import {getRequestsFilter, getSetResponse} from '../../utils/mockWebSocketRoute';
import {setReadonlyProperty} from '../../utils/setReadonlyProperty';

import type {
WebSocketMockFunction,
WebSocketRouteClassTypeWithGetParamsFromUrl,
} from '../../types/internal';

/**
* Mock WebSocket for some API route.
* Applicable only for routes with the `getParamsFromUrlOrThrow` method.
* The mock is applied to a WebSocket that matches the route by url
* (by methods `getParamsFromUrlOrThrow` and `isMatchUrl`).
*/
export const mockWebSocketRoute = async <RouteParams, SomeRequest, SomeResponse>(
Route: WebSocketRouteClassTypeWithGetParamsFromUrl<RouteParams>,
webSocketMockFunction: WebSocketMockFunction<RouteParams, SomeRequest, SomeResponse>,
{skipLogs = false}: {skipLogs?: boolean} = {},
): Promise<void> => {
setCustomInspectOnFunction(webSocketMockFunction);

const webSocketMockState = getWebSocketMockState();

if (!webSocketMockState.isMocksEnabled) {
return;
}

const fullMocksState = getFullMocksState();

if (fullMocksState?.appliedMocks !== undefined) {
setReadonlyProperty(webSocketMockState, 'isMocksEnabled', false);
}

let {optionsByRoute} = webSocketMockState;

if (optionsByRoute === undefined) {
optionsByRoute = new Map();

setReadonlyProperty(webSocketMockState, 'optionsByRoute', optionsByRoute);

const requestsFilter = getRequestsFilter(webSocketMockState);

setReadonlyProperty(webSocketMockState, 'requestsFilter', requestsFilter);
}

if (optionsByRoute.size === 0) {
const {requestsFilter} = webSocketMockState;

assertValueIsDefined(requestsFilter, 'requestsFilter is defined', {
routeName: Route.name,
webSocketMockState,
});

const page = getPlaywrightPage();

const setResponse = getSetResponse(webSocketMockState);

await page.routeWebSocket(requestsFilter, setResponse);
}

optionsByRoute.set(Route, {
skipLogs,
webSocketMockFunction: webSocketMockFunction as WebSocketMockFunction,
});

if (skipLogs !== true) {
log(
`Mock WebSocket for route "${Route.name}"`,
{webSocketMockFunction},
LogEventType.InternalAction,
);
}
};
Loading

0 comments on commit 0f8b255

Please sign in to comment.