From 18553b3f539aa362fa0d924fd821ba9518ee7aa5 Mon Sep 17 00:00:00 2001 From: uid11 Date: Sun, 1 Dec 2024 07:16:55 +0300 Subject: [PATCH] FI-1557 fix: ingore all re-requests when `waitForAllRequestsComplete` tests: add tests of types for `mockWebSocketRoute` tests: add tests for `mockWebSocketRoute` fix: protocol in `WebSocketRoute.getUrl` method fix: types of request and response for `mockWebSocketRoute` fix: context in `onMessage` handler for `mockWebSocketRoute` --- autotests/entities/index.ts | 1 + autotests/entities/score.ts | 25 +++++++++ autotests/routes/webSocketRoutes/Base.ts | 12 ++++ autotests/routes/webSocketRoutes/Score.ts | 33 +++++++++++ autotests/routes/webSocketRoutes/index.ts | 2 + .../e2edReportExample/mockWebSocketRoute.ts | 45 +++++++++++++++ .../mockWebSocketRoute.skip.ts | 55 +++++++++++++++++++ autotests/tests/mockApiRoute.ts | 2 +- autotests/types/api/Base.ts | 9 +++ autotests/types/api/Score.ts | 9 +++ autotests/types/api/index.ts | 2 + autotests/types/index.ts | 4 ++ src/ApiRoute.ts | 3 + src/PageRoute.ts | 3 + src/WebSocketRoute.ts | 29 ++++++++++ src/actions/mock/mockWebSocketRoute.ts | 2 +- .../mockWebSocketRoute/getSetResponse.ts | 54 +++++++++--------- src/utils/waitForEvents/isReRequest.ts | 17 +----- 18 files changed, 263 insertions(+), 44 deletions(-) create mode 100644 autotests/entities/score.ts create mode 100644 autotests/routes/webSocketRoutes/Base.ts create mode 100644 autotests/routes/webSocketRoutes/Score.ts create mode 100644 autotests/routes/webSocketRoutes/index.ts create mode 100644 autotests/tests/e2edReportExample/mockWebSocketRoute.ts create mode 100644 autotests/tests/internalTypeTests/mockWebSocketRoute.skip.ts create mode 100644 autotests/types/api/Base.ts create mode 100644 autotests/types/api/Score.ts diff --git a/autotests/entities/index.ts b/autotests/entities/index.ts index ace9be88..113975f9 100644 --- a/autotests/entities/index.ts +++ b/autotests/entities/index.ts @@ -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'; diff --git a/autotests/entities/score.ts b/autotests/entities/score.ts new file mode 100644 index 00000000..25bbbc16 --- /dev/null +++ b/autotests/entities/score.ts @@ -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> = createClientFunction( + (pageState, url) => { + const socket = new WebSocket(url); + const data = JSON.stringify({pageState}); + const promise = new Promise((resolve) => { + socket.onmessage = (event) => { + resolve(event.data as string); + }; + }); + + socket.onopen = () => { + socket.send(data); + }; + + return promise; + }, + {name: 'sendScore', timeout: 1_000}, +); diff --git a/autotests/routes/webSocketRoutes/Base.ts b/autotests/routes/webSocketRoutes/Base.ts new file mode 100644 index 00000000..f21ac53c --- /dev/null +++ b/autotests/routes/webSocketRoutes/Base.ts @@ -0,0 +1,12 @@ +import {WebSocketRoute} from 'e2ed'; + +import type {WebSocketBaseRequest, WebSocketBaseResponse} from 'autotests/types'; + +/** + * Base WebSocket. + */ +export class Base extends WebSocketRoute { + getPath(): string { + return '/base'; + } +} diff --git a/autotests/routes/webSocketRoutes/Score.ts b/autotests/routes/webSocketRoutes/Score.ts new file mode 100644 index 00000000..8fe8e08d --- /dev/null +++ b/autotests/routes/webSocketRoutes/Score.ts @@ -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 { + 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}`; + } +} diff --git a/autotests/routes/webSocketRoutes/index.ts b/autotests/routes/webSocketRoutes/index.ts new file mode 100644 index 00000000..3424082d --- /dev/null +++ b/autotests/routes/webSocketRoutes/index.ts @@ -0,0 +1,2 @@ +export {Base} from './Base'; +export {Score} from './Score'; diff --git a/autotests/tests/e2edReportExample/mockWebSocketRoute.ts b/autotests/tests/e2edReportExample/mockWebSocketRoute.ts new file mode 100644 index 00000000..04769ad3 --- /dev/null +++ b/autotests/tests/e2edReportExample/mockWebSocketRoute.ts @@ -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'); + }, +); diff --git a/autotests/tests/internalTypeTests/mockWebSocketRoute.skip.ts b/autotests/tests/internalTypeTests/mockWebSocketRoute.skip.ts new file mode 100644 index 00000000..8ba1f06c --- /dev/null +++ b/autotests/tests/internalTypeTests/mockWebSocketRoute.skip.ts @@ -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}; + }, +); diff --git a/autotests/tests/mockApiRoute.ts b/autotests/tests/mockApiRoute.ts index a5c3c149..a7204de9 100644 --- a/autotests/tests/mockApiRoute.ts +++ b/autotests/tests/mockApiRoute.ts @@ -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 = { diff --git a/autotests/types/api/Base.ts b/autotests/types/api/Base.ts new file mode 100644 index 00000000..44a56a6f --- /dev/null +++ b/autotests/types/api/Base.ts @@ -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[]}>; diff --git a/autotests/types/api/Score.ts b/autotests/types/api/Score.ts new file mode 100644 index 00000000..5219555f --- /dev/null +++ b/autotests/types/api/Score.ts @@ -0,0 +1,9 @@ +/** + * Request for score WebSocket. + */ +export type WebSocketScoreRequest = Readonly<{pageState: string}>; + +/** + * Response for score WebSocket. + */ +export type WebSocketScoreResponse = Readonly<{score: number}>; diff --git a/autotests/types/api/index.ts b/autotests/types/api/index.ts index 927e7ddf..f61374dd 100644 --- a/autotests/types/api/index.ts +++ b/autotests/types/api/index.ts @@ -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'; diff --git a/autotests/types/index.ts b/autotests/types/index.ts index 881f7874..9fb1a17f 100644 --- a/autotests/types/index.ts +++ b/autotests/types/index.ts @@ -11,6 +11,10 @@ export type { ApiGetUsersResponse, ApiUserSignUpRequest, ApiUserSignUpResponse, + WebSocketBaseRequest, + WebSocketBaseResponse, + WebSocketScoreRequest, + WebSocketScoreResponse, } from './api'; export type { ApiDevice, diff --git a/src/ApiRoute.ts b/src/ApiRoute.ts index 2178a3b1..e1d98fca 100644 --- a/src/ApiRoute.ts +++ b/src/ApiRoute.ts @@ -36,6 +36,9 @@ export abstract class ApiRoute< return true; } + /** + * Returns the origin of the route. + */ getOrigin(): Url { return 'http://localhost' as Url; } diff --git a/src/PageRoute.ts b/src/PageRoute.ts index a7724416..ac488268 100644 --- a/src/PageRoute.ts +++ b/src/PageRoute.ts @@ -7,6 +7,9 @@ import type {Url} from './types/internal'; * Abstract route for page. */ export abstract class PageRoute extends Route { + /** + * Returns the origin of the route. + */ getOrigin(): Url { const {E2ED_ORIGIN} = process.env; diff --git a/src/WebSocketRoute.ts b/src/WebSocketRoute.ts index d9abba96..4312abe6 100644 --- a/src/WebSocketRoute.ts +++ b/src/WebSocketRoute.ts @@ -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". */ @@ -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; + } } diff --git a/src/actions/mock/mockWebSocketRoute.ts b/src/actions/mock/mockWebSocketRoute.ts index be341cf2..551bfa3a 100644 --- a/src/actions/mock/mockWebSocketRoute.ts +++ b/src/actions/mock/mockWebSocketRoute.ts @@ -20,7 +20,7 @@ import type { * (by methods `getParamsFromUrlOrThrow` and `isMatchUrl`). */ export const mockWebSocketRoute = async ( - Route: WebSocketRouteClassTypeWithGetParamsFromUrl, + Route: WebSocketRouteClassTypeWithGetParamsFromUrl, webSocketMockFunction: WebSocketMockFunction, {skipLogs = false}: {skipLogs?: boolean} = {}, ): Promise => { diff --git a/src/utils/mockWebSocketRoute/getSetResponse.ts b/src/utils/mockWebSocketRoute/getSetResponse.ts index 6974dc66..2bff1b25 100644 --- a/src/utils/mockWebSocketRoute/getSetResponse.ts +++ b/src/utils/mockWebSocketRoute/getSetResponse.ts @@ -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, + ); + } + }), + ); }); diff --git a/src/utils/waitForEvents/isReRequest.ts b/src/utils/waitForEvents/isReRequest.ts index 882446ad..0a182b3b 100644 --- a/src/utils/waitForEvents/isReRequest.ts +++ b/src/utils/waitForEvents/isReRequest.ts @@ -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; };