Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: ingore re-requests with the same urls #96

Merged
merged 1 commit into from
Dec 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions autotests/entities/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export {createDevice} from './device';
export {addProduct} from './product';
export {sendScore} from './score';
export {createUser} from './user';
export {addUser, getUsers} from './worker';
25 changes: 25 additions & 0 deletions autotests/entities/score.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import {createClientFunction} from 'e2ed';

import type {ClientFunction, Url} from 'e2ed/types';

/**
* Sends page score.
*/
export const sendScore: ClientFunction<[string, Url], Promise<string>> = createClientFunction(
(pageState, url) => {
const socket = new WebSocket(url);
const data = JSON.stringify({pageState});
const promise = new Promise<string>((resolve) => {
socket.onmessage = (event) => {
resolve(event.data as string);
};
});

socket.onopen = () => {
socket.send(data);
};

return promise;
},
{name: 'sendScore', timeout: 1_000},
);
12 changes: 12 additions & 0 deletions autotests/routes/webSocketRoutes/Base.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import {WebSocketRoute} from 'e2ed';

import type {WebSocketBaseRequest, WebSocketBaseResponse} from 'autotests/types';

/**
* Base WebSocket.
*/
export class Base extends WebSocketRoute<undefined, WebSocketBaseRequest, WebSocketBaseResponse> {
getPath(): string {
return '/base';
}
}
33 changes: 33 additions & 0 deletions autotests/routes/webSocketRoutes/Score.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import {stringify} from 'node:querystring';
import {URL} from 'node:url';

import {WebSocketRoute} from 'e2ed';
import {assertValueIsTrue} from 'e2ed/utils';

import type {WebSocketScoreRequest, WebSocketScoreResponse} from 'autotests/types';
import type {Url} from 'e2ed/types';

type Params = Readonly<{size: number}>;

const pathname = '/score';

/**
* Score WebSocket.
*/
export class Score extends WebSocketRoute<Params, WebSocketScoreRequest, WebSocketScoreResponse> {
static override getParamsFromUrlOrThrow(url: Url): Params {
const urlObject = new URL(url);
const size = urlObject.searchParams.get('size');

assertValueIsTrue(urlObject.pathname === pathname, 'pathname is correct', {urlObject});

return {size: Number(size)};
}

getPath(): string {
const {size} = this.routeParams;
const query = stringify({size});

return `${pathname}?${query}`;
}
}
2 changes: 2 additions & 0 deletions autotests/routes/webSocketRoutes/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export {Base} from './Base';
export {Score} from './Score';
45 changes: 45 additions & 0 deletions autotests/tests/e2edReportExample/mockWebSocketRoute.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import {test} from 'autotests';
import {sendScore} from 'autotests/entities';
import {E2edReportExample} from 'autotests/pageObjects/pages';
import {Score as ScoreRoute} from 'autotests/routes/webSocketRoutes';
import {expect} from 'e2ed';
import {mockWebSocketRoute, navigateToPage, unmockWebSocketRoute} from 'e2ed/actions';
import {assertFunctionThrows} from 'e2ed/utils';

import type {Url} from 'e2ed/types';

test(
'mockWebSocketRoute correctly intercepts requests, and unmockWebSocketRoute cancels the interception',
{meta: {testId: '19'}, testIdleTimeout: 3_000},
async () => {
await mockWebSocketRoute(ScoreRoute, ({size}, {pageState}) => {
const stateScore = Number(pageState);

return {score: size * stateScore};
});

await navigateToPage(E2edReportExample);

const pageState = '5';
const size = 3;
const webSocketUrl = `wss://localhost/score?size=${size}` as Url;

const result = await sendScore(pageState, webSocketUrl);

const scoreRouteParams = ScoreRoute.getParamsFromUrlOrThrow(webSocketUrl);

const scoreRouteFromUrl = new ScoreRoute(scoreRouteParams);

await expect(scoreRouteFromUrl.routeParams.size, 'route has correct params').eql(size);

await expect(JSON.parse(result), 'mocked WebSocket returns correct result').eql({
score: size * Number(pageState),
});

await unmockWebSocketRoute(ScoreRoute);

await assertFunctionThrows(async () => {
await sendScore(pageState, webSocketUrl);
}, 'throws an error after unmocking WebSocket route');
},
);
55 changes: 55 additions & 0 deletions autotests/tests/internalTypeTests/mockWebSocketRoute.skip.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import {Main} from 'autotests/routes/pageRoutes';
import {Base, Score} from 'autotests/routes/webSocketRoutes';
import {mockWebSocketRoute, unmockWebSocketRoute} from 'e2ed/actions';

import type {WebSocketScoreRequest, WebSocketScoreResponse} from 'autotests/types';
import type {Any} from 'e2ed/types';

const anyMockFunction = (..._args: Any[]): Any => {};

const webSocketMockFunction = (
{size}: {size: number},
{pageState}: WebSocketScoreRequest,
): WebSocketScoreResponse => {
if (pageState !== '') {
return {score: 8};
}

return {score: size > 2 ? size : 2};
};

// @ts-expect-error: mockWebSocketRoute require WebSocket route as first argument
void mockWebSocketRoute(Main, anyMockFunction);

// @ts-expect-error: unmockWebSocketRoute require WebSocket route as first argument
void unmockWebSocketRoute(Main);

// @ts-expect-error: mockWebSocketRoute require WebSocket route with static method getParamsFromUrlOrThrow
void mockWebSocketRoute(Base, anyMockFunction);

// ok
void mockWebSocketRoute(Score, anyMockFunction);

// @ts-expect-error: unmockWebSocketRoute require WebSocket route with static method getParamsFromUrlOrThrow
void unmockWebSocketRoute(Base);

// ok
void mockWebSocketRoute(Score, webSocketMockFunction);

// ok
void unmockWebSocketRoute(Score);

// ok
void mockWebSocketRoute(
Score,
async (
{size},
{pageState}, // eslint-disable-next-line @typescript-eslint/require-await
) => {
if (pageState !== '') {
return {score: 10};
}

return {score: size > 1 ? size : 1};
},
);
2 changes: 1 addition & 1 deletion autotests/tests/mockApiRoute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import type {Url} from 'e2ed/types';

test(
'mockApiRoute correctly intercepts requests, and unmockApiRoute cancels the interception',
{meta: {testId: '6'}, testIdleTimeout: 15_000},
{meta: {testId: '6'}, testIdleTimeout: 4_000},
async () => {
await mockApiRoute(CreateProductRoute, (routeParams, {method, query, requestBody, url}) => {
const responseBody = {
Expand Down
9 changes: 9 additions & 0 deletions autotests/types/api/Base.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/**
* Request for base WebSocket.
*/
export type WebSocketBaseRequest = Readonly<{pageState: string}>;

/**
* Response for base WebSocket.
*/
export type WebSocketBaseResponse = Readonly<{tags: readonly string[]}>;
9 changes: 9 additions & 0 deletions autotests/types/api/Score.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/**
* Request for score WebSocket.
*/
export type WebSocketScoreRequest = Readonly<{pageState: string}>;

/**
* Response for score WebSocket.
*/
export type WebSocketScoreResponse = Readonly<{score: number}>;
2 changes: 2 additions & 0 deletions autotests/types/api/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
export type {ApiAddUserRequest, ApiAddUserResponse} from './AddUser';
export type {WebSocketBaseRequest, WebSocketBaseResponse} from './Base';
export type {ApiCreateDeviceRequest, ApiCreateDeviceResponse} from './CreateDevice';
export type {ApiCreateProductRequest, ApiCreateProductResponse} from './CreateProduct';
export type {ApiGetUserRequest, ApiGetUserResponse} from './GetUser';
export type {ApiGetUsersRequest, ApiGetUsersResponse} from './GetUsers';
export type {WebSocketScoreRequest, WebSocketScoreResponse} from './Score';
export type {ApiUserSignUpRequest, ApiUserSignUpResponse} from './UserSignUp';
4 changes: 4 additions & 0 deletions autotests/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ export type {
ApiGetUsersResponse,
ApiUserSignUpRequest,
ApiUserSignUpResponse,
WebSocketBaseRequest,
WebSocketBaseResponse,
WebSocketScoreRequest,
WebSocketScoreResponse,
} from './api';
export type {
ApiDevice,
Expand Down
3 changes: 3 additions & 0 deletions src/ApiRoute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ export abstract class ApiRoute<
return true;
}

/**
* Returns the origin of the route.
*/
getOrigin(): Url {
return 'http://localhost' as Url;
}
Expand Down
3 changes: 3 additions & 0 deletions src/PageRoute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import type {Url} from './types/internal';
* Abstract route for page.
*/
export abstract class PageRoute<Params = undefined> extends Route<Params> {
/**
* Returns the origin of the route.
*/
getOrigin(): Url {
const {E2ED_ORIGIN} = process.env;

Expand Down
29 changes: 29 additions & 0 deletions src/WebSocketRoute.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import {Route} from './Route';

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

const http = 'http:';
const https = 'https:';

/**
* Abstract route for WebSocket "requests".
*/
Expand Down Expand Up @@ -33,4 +38,28 @@ export abstract class WebSocketRoute<
getIsResponseBodyInJsonFormat(): boolean {
return true;
}

/**
* Returns the origin of the route.
*/
getOrigin(): Url {
return 'http://localhost' as Url;
}

/**
* Returns the url of the route.
*/
override getUrl(): Url {
const url = super.getUrl();

if (url.startsWith(https)) {
return `wss:${url.slice(https.length)}` as Url;
}

if (url.startsWith(http)) {
return `ws:${url.slice(http.length)}` as Url;
}

return url;
}
}
2 changes: 1 addition & 1 deletion src/actions/mock/mockWebSocketRoute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import type {
* (by methods `getParamsFromUrlOrThrow` and `isMatchUrl`).
*/
export const mockWebSocketRoute = async <RouteParams, SomeRequest, SomeResponse>(
Route: WebSocketRouteClassTypeWithGetParamsFromUrl<RouteParams>,
Route: WebSocketRouteClassTypeWithGetParamsFromUrl<RouteParams, SomeRequest, SomeResponse>,
webSocketMockFunction: WebSocketMockFunction<RouteParams, SomeRequest, SomeResponse>,
{skipLogs = false}: {skipLogs?: boolean} = {},
): Promise<void> => {
Expand Down
54 changes: 28 additions & 26 deletions src/utils/mockWebSocketRoute/getSetResponse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,32 +28,34 @@ export const getSetResponse = ({
const isRequestBodyInJsonFormat = route.getIsRequestBodyInJsonFormat();
const isResponseBodyInJsonFormat = route.getIsResponseBodyInJsonFormat();

playwrightRoute.onMessage(async (message) => {
const {value: request, hasParseError} = parseValueAsJsonIfNeeded(
String(message),
isRequestBodyInJsonFormat,
);

if (hasParseError && skipLogs !== true) {
log(
'WebSocket message is not in JSON format',
{logEventStatus: LogEventStatus.Failed, message, url},
LogEventType.InternalUtil,
playwrightRoute.onMessage(
AsyncLocalStorage.bind(async (message) => {
const {value: request, hasParseError} = parseValueAsJsonIfNeeded(
String(message),
isRequestBodyInJsonFormat,
);
}

const response = await webSocketMockFunction(route.routeParams, request);

const responseAsString = getBodyAsString(response, isResponseBodyInJsonFormat);

playwrightRoute.send(responseAsString);

if (skipLogs !== true) {
log(
`A mock was applied to the WebSocket route "${route.constructor.name}"`,
{request, response, route, webSocketMockFunction},
LogEventType.InternalUtil,
);
}
});
if (hasParseError && skipLogs !== true) {
log(
'WebSocket message is not in JSON format',
{logEventStatus: LogEventStatus.Failed, message, url},
LogEventType.InternalUtil,
);
}

const response = await webSocketMockFunction(route.routeParams, request);

const responseAsString = getBodyAsString(response, isResponseBodyInJsonFormat);

playwrightRoute.send(responseAsString);

if (skipLogs !== true) {
log(
`A mock was applied to the WebSocket route "${route.constructor.name}"`,
{request, response, route, webSocketMockFunction},
LogEventType.InternalUtil,
);
}
}),
);
});
17 changes: 1 addition & 16 deletions src/utils/waitForEvents/isReRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,24 +13,9 @@ export const isReRequest = (
return false;
}

const baseHeaders = baseRequest.requestHeaders;
const headers = reRequest.requestHeaders;

const headersNames = Object.keys(headers);
const baseHeadersNames = Object.keys(baseHeaders);

if (headersNames.length < baseHeadersNames.length) {
if (reRequest.method !== baseRequest.method) {
return false;
}

for (const headerName of baseHeadersNames) {
if (
!(headerName in headers) ||
String(baseHeaders[headerName]) !== String(headers[headerName])
) {
return false;
}
}

return true;
};