Skip to content

Commit

Permalink
FI-1534 feat: add mockWebSocketRoute/unmockWebSocketRoute actions
Browse files Browse the repository at this point in the history
  • Loading branch information
uid11 committed Nov 28, 2024
1 parent c41b348 commit 28abc13
Show file tree
Hide file tree
Showing 28 changed files with 470 additions and 53 deletions.
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,
);
}
};
57 changes: 57 additions & 0 deletions src/actions/mock/unmockWebSocketRoute.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import {LogEventType} from '../../constants/internal';
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 type {
WebSocketMockFunction,
WebSocketRouteClassTypeWithGetParamsFromUrl,
} from '../../types/internal';

/**
* Unmock WebSocket (remove mock, if any) for some WebSocket route.
*/
export const unmockWebSocketRoute = async <RouteParams, SomeRequest, SomeResponse>(
Route: WebSocketRouteClassTypeWithGetParamsFromUrl<RouteParams, SomeRequest, SomeResponse>,
): Promise<void> => {
const webSocketMockState = getWebSocketMockState();
const {optionsByRoute, requestsFilter} = webSocketMockState;
let webSocketMockFunction: WebSocketMockFunction | undefined;
let routeWasMocked = false;
let skipLogs: boolean | undefined;

if (optionsByRoute?.has(Route)) {
const options = optionsByRoute.get(Route);

webSocketMockFunction = options?.webSocketMockFunction;
skipLogs = options?.skipLogs;

routeWasMocked = true;
optionsByRoute.delete(Route);
}

if (optionsByRoute?.size === 0) {
assertValueIsDefined(requestsFilter, 'requestsFilter is defined', {
routeName: Route.name,
routeWasMocked,
});

const page = getPlaywrightPage();

await page.unroute(requestsFilter);
}

if (webSocketMockFunction) {
setCustomInspectOnFunction(webSocketMockFunction);
}

if (skipLogs !== true) {
log(
`Unmock WebSocket for route "${Route.name}"`,
{routeWasMocked, webSocketMockFunction},
LogEventType.InternalAction,
);
}
};
32 changes: 32 additions & 0 deletions src/context/webSocketMockState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import {useContext} from '../useContext';

import type {WebSocketMockState} from '../types/internal';

/**
* Raw get and set internal (maybe `undefined`) WebSocket mock state.
* @internal
*/
const [getRawWebSocketMockState, setRawWebSocketMockState] = useContext<WebSocketMockState>();

/**
* Get internal always defined WebSocket mock state (for `mockWebSocketRoute`).
* @internal
*/
export const getWebSocketMockState = (): WebSocketMockState => {
const maybeWebSocketMockState = getRawWebSocketMockState();

if (maybeWebSocketMockState !== undefined) {
return maybeWebSocketMockState;
}

const webSocketMockState: WebSocketMockState = {
isMocksEnabled: true,
optionsByRoute: undefined,
optionsWithRouteByUrl: Object.create(null) as {},
requestsFilter: undefined,
};

setRawWebSocketMockState(webSocketMockState);

return webSocketMockState;
};
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export {PageRoute} from './PageRoute';
export {devices} from './playwright';
export {Route} from './Route';
export {getPlaywrightPage, useContext} from './useContext';
export {WebSocketRoute} from './WebSocketRoute';

/**
* Public modules, dependent on internal utils.
Expand Down
8 changes: 7 additions & 1 deletion src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export type {
} from './http';
export type {KeyboardPressKey} from './keyboard';
export type {ApiMockFunction} from './mockApiRoute';
export type {WebSocketMockFunction} from './mockWebSocketRoute';
export type {NavigateToUrlOptions} from './navigation';
export type {
AnyPageClassType,
Expand All @@ -55,7 +56,12 @@ export type {
PropertyKey,
} from './properties';
export type {LiteReport, LiteRetry} from './report';
export type {ApiRouteClassType, ApiRouteClassTypeWithGetParamsFromUrl} from './routes';
export type {
ApiRouteClassType,
ApiRouteClassTypeWithGetParamsFromUrl,
WebSocketRouteClassType,
WebSocketRouteClassTypeWithGetParamsFromUrl,
} from './routes';
export type {
CreateSelector,
CreateSelectorFunctionOptions,
Expand Down
10 changes: 9 additions & 1 deletion src/types/internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ export type {
export type {ApiMockFunction} from './mockApiRoute';
/** @internal */
export type {ApiMockState} from './mockApiRoute';
export type {WebSocketMockFunction} from './mockWebSocketRoute';
/** @internal */
export type {WebSocketMockState} from './mockWebSocketRoute';
export type {NavigateToUrlOptions} from './navigation';
/** @internal */
export type {NavigationDelay} from './navigation';
Expand Down Expand Up @@ -105,7 +108,12 @@ export type {
} from './report';
/** @internal */
export type {RetriesState, RunRetryOptions, VisitedTestNamesHash} from './retries';
export type {ApiRouteClassType, ApiRouteClassTypeWithGetParamsFromUrl} from './routes';
export type {
ApiRouteClassType,
ApiRouteClassTypeWithGetParamsFromUrl,
WebSocketRouteClassType,
WebSocketRouteClassTypeWithGetParamsFromUrl,
} from './routes';
export type {RunLabel, RunLabelObject} from './runLabel';
/** @internal */
export type {RawRunLabelObject} from './runLabel';
Expand Down
6 changes: 2 additions & 4 deletions src/types/mockApiRoute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type {URL} from 'node:url';
import type {ApiRoute} from '../ApiRoute';

import type {Request, Response, Url} from './http';
import type {MaybePromise} from './promise';
import type {ApiRouteClassTypeWithGetParamsFromUrl} from './routes';

/**
Expand All @@ -23,10 +24,7 @@ export type ApiMockFunction<
RouteParams = unknown,
SomeRequest extends Request = Request,
SomeResponse extends Response = Response,
> = (
routeParams: RouteParams,
request: SomeRequest,
) => Partial<SomeResponse> | Promise<Partial<SomeResponse>>;
> = (routeParams: RouteParams, request: SomeRequest) => MaybePromise<Partial<SomeResponse>>;

/**
* Internal state of `mockApiRoute`/`unmockApiRoute`.
Expand Down
38 changes: 38 additions & 0 deletions src/types/mockWebSocketRoute.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import type {URL} from 'node:url';

import type {WebSocketRoute} from '../WebSocketRoute';

import type {Url} from './http';
import type {MaybePromise} from './promise';
import type {WebSocketRouteClassTypeWithGetParamsFromUrl} from './routes';

/**
* Mock option with mocked route.
* @internal
*/
type MockOptionsWithRoute = MockOptions & Readonly<{route: WebSocketRoute<unknown>}>;

/**
* Mock option (`skipLogs` and `webSocketMockFunction`).
*/
type MockOptions = Readonly<{skipLogs: boolean; webSocketMockFunction: WebSocketMockFunction}>;

/**
* WebSocket mock function, that map request to mocked response.
*/
export type WebSocketMockFunction<
RouteParams = unknown,
SomeRequest = unknown,
SomeResponse = unknown,
> = (routeParams: RouteParams, request: SomeRequest) => MaybePromise<SomeResponse>;

/**
* Internal state of `mockWebSocketRoute`/`unmockWebSocketRoute`.
* @internal
*/
export type WebSocketMockState = Readonly<{
isMocksEnabled: boolean;
optionsByRoute: Map<WebSocketRouteClassTypeWithGetParamsFromUrl, MockOptions> | undefined;
optionsWithRouteByUrl: Record<Url, MockOptionsWithRoute | undefined>;
requestsFilter: ((urlObject: URL) => boolean) | undefined;
}>;
Loading

0 comments on commit 28abc13

Please sign in to comment.