From 93ddf45c09464b69f54547151debde65816f64eb Mon Sep 17 00:00:00 2001 From: Alec Armbruster <35377827+alectrocute@users.noreply.github.com> Date: Mon, 26 Aug 2024 14:58:31 -0700 Subject: [PATCH 1/5] add support for websockets, first draft --- package-lock.json | 94 ++++++++++++++++++- package.json | 5 +- src/index.ts | 13 ++- src/lib/route-handler.ts | 6 +- src/lib/wait.ts | 8 +- src/lib/websocket-setup.ts | 59 ++++++++++++ src/types/intercept-setup.ts | 7 +- src/types/intercept-websocket-surface.ts | 7 ++ src/types/options/intercept.ts | 11 ++- src/types/options/websocket.ts | 6 ++ tests/e2e/base.ts | 2 +- tests/e2e/internals.spec.ts | 2 +- tests/e2e/mock-app/websockets.html | 68 ++++++++++++++ tests/e2e/mock-app/{index.html => xhr.html} | 0 tests/e2e/websockets.spec.ts | 44 +++++++++ ...ollection-of-intercepts-as-fixture.spec.ts | 2 +- 16 files changed, 312 insertions(+), 22 deletions(-) create mode 100644 src/lib/websocket-setup.ts create mode 100644 src/types/intercept-websocket-surface.ts create mode 100644 src/types/options/websocket.ts create mode 100644 tests/e2e/mock-app/websockets.html rename tests/e2e/mock-app/{index.html => xhr.html} (100%) create mode 100644 tests/e2e/websockets.spec.ts diff --git a/package-lock.json b/package-lock.json index b5d7345..74c5918 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,18 +1,20 @@ { "name": "playwright-intercept", - "version": "1.0.0", + "version": "1.0.10", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "playwright-intercept", - "version": "1.0.0", + "version": "1.0.10", "license": "GPL", "dependencies": { "esprima": "^4.0.1", "mime-types": "^2.1.35", "path-to-regexp": "^6.2.1", - "vue": "^3.3.8" + "portfinder": "^1.0.32", + "vue": "^3.3.8", + "ws": "^8.18.0" }, "devDependencies": { "@babel/types": "^7.25.4", @@ -20,6 +22,7 @@ "@types/esprima": "^4.0.6", "@types/mime-types": "^2.1.4", "@types/node": "^18.14.6", + "@types/ws": "^8.5.12", "@typescript-eslint/eslint-plugin": "^6.14.0", "@typescript-eslint/parser": "^6.14.0", "eslint": "^8.55.0", @@ -349,6 +352,15 @@ "integrity": "sha512-/wdoPq1QqkSj9/QOeKkFquEuPzQbHTWAMPH/PaUMB+JuR31lXhlWXRZ52IpfDYVlDOUBvX09uBrPwxGT1hjNBg==", "dev": true }, + "node_modules/@types/ws": { + "version": "8.5.12", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.12.tgz", + "integrity": "sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", @@ -824,6 +836,14 @@ "node": ">=8" } }, + "node_modules/async": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", + "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", + "dependencies": { + "lodash": "^4.17.14" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1832,6 +1852,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -2012,11 +2037,29 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/nanoid": { "version": "3.3.7", @@ -2251,6 +2294,27 @@ "node": ">=16" } }, + "node_modules/portfinder": { + "version": "1.0.32", + "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.32.tgz", + "integrity": "sha512-on2ZJVVDXRADWE6jnQaX0ioEylzgBpQk8r55NE4wjXW1ZxO+BgDlY6DXwj20i0V8eB4SenDQ00WEaxfiIQPcxg==", + "dependencies": { + "async": "^2.6.4", + "debug": "^3.2.7", + "mkdirp": "^0.5.6" + }, + "engines": { + "node": ">= 0.12.0" + } + }, + "node_modules/portfinder/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dependencies": { + "ms": "^2.1.1" + } + }, "node_modules/postcss": { "version": "8.4.35", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz", @@ -2804,6 +2868,26 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/yaml": { "version": "2.3.4", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz", diff --git a/package.json b/package.json index 55f778e..9480863 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "@types/esprima": "^4.0.6", "@types/mime-types": "^2.1.4", "@types/node": "^18.14.6", + "@types/ws": "^8.5.12", "@typescript-eslint/eslint-plugin": "^6.14.0", "@typescript-eslint/parser": "^6.14.0", "eslint": "^8.55.0", @@ -40,7 +41,9 @@ "esprima": "^4.0.1", "mime-types": "^2.1.35", "path-to-regexp": "^6.2.1", - "vue": "^3.3.8" + "portfinder": "^1.0.32", + "vue": "^3.3.8", + "ws": "^8.18.0" }, "lint-staged": { "*.ts": [ diff --git a/src/index.ts b/src/index.ts index d8c362a..b305472 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,18 +1,21 @@ +import { reactive, ref } from 'vue'; import type { InterceptOptions } from './types/options/intercept'; import type { InterceptSurface } from './types/intercept-surface'; import type { InterceptSetup } from './types/intercept-setup'; import type { GlobalOptions } from './types/options/global'; +import type { WebsocketOptions } from './types/options/websocket'; +import type { WaitOptions } from './types/options/wait'; +import type { UpdateOptions } from './types/options/update'; +import type { InterceptWebsocketSurface } from './types/intercept-websocket-surface'; import type { Page, Route, Request } from '@playwright/test'; import { routeHandler } from './lib/route-handler'; import { createMatchFunction } from './lib/create-match-function'; import { getMimeType } from './lib/get-mime-type'; import { normalizeBody } from './lib/normalize-body'; -import { reactive, ref } from 'vue'; -import type { WaitOptions } from './types/options/wait'; -import type { UpdateOptions } from './types/options/update'; import { wait } from './lib/wait'; import { update } from './lib/update'; import { getRouteRegex } from './lib/get-route-regex'; +import { websocketSetup } from './lib/websocket-setup'; export { createMatchFunction, getMimeType, normalizeBody }; @@ -110,6 +113,10 @@ export class Intercept { delete(options: InterceptOptions): InterceptSurface { return this.setup(options, 'DELETE'); } + + async wss(options: WebsocketOptions): Promise { + return await websocketSetup(this.page, options); + } } export type { InterceptOptions, InterceptSurface }; diff --git a/src/lib/route-handler.ts b/src/lib/route-handler.ts index 6612c89..614d43d 100644 --- a/src/lib/route-handler.ts +++ b/src/lib/route-handler.ts @@ -33,7 +33,7 @@ export async function routeHandler({ }: RouteHandlerParams) { const intercept = intercepts.find( (intercept) => - intercept.match(route.request().url()) && + intercept.match?.(route.request().url()) && intercept.method === route.request().method() ); @@ -46,10 +46,10 @@ export async function routeHandler({ // Ensure the response has been downloaded by client and then // as an anti-flake measure, push the request upon next tick. page.once('requestfinished', () => { - nextTick(() => intercept.requests.push(route.request())); + nextTick(() => intercept.requests?.push(route.request())); }); - const { params } = intercept.match(route.request().url()) as MatchResult; + const { params } = intercept.match?.(route.request().url()) as MatchResult; if ('body' in intercept || 'fixture' in intercept) { let body = ''; diff --git a/src/lib/wait.ts b/src/lib/wait.ts index 2b1ccd7..f74fcc6 100644 --- a/src/lib/wait.ts +++ b/src/lib/wait.ts @@ -15,20 +15,22 @@ export function wait( return expect .poll( () => { + const requests = intercept.wssPayloads ?? intercept.requests; + const hasFirstRequest = Boolean( intercept.previousRequestCount.value === 0 && - intercept.requests.length > 0 + requests!.length > 0 ); const hasNewRequests = Boolean( intercept.previousRequestCount.value > 0 && - intercept.previousRequestCount.value < intercept.requests.length + intercept.previousRequestCount.value < requests!.length ); const shouldPass = Boolean(hasFirstRequest || hasNewRequests); if (shouldPass) { - intercept.previousRequestCount.value = intercept.requests.length; + intercept.previousRequestCount.value = requests!.length; return true; } diff --git a/src/lib/websocket-setup.ts b/src/lib/websocket-setup.ts new file mode 100644 index 0000000..62133dc --- /dev/null +++ b/src/lib/websocket-setup.ts @@ -0,0 +1,59 @@ +import type { Page } from '@playwright/test'; +import type { WebsocketOptions } from '../types/options/websocket'; +import type { InterceptWebsocketSurface } from '../types/intercept-websocket-surface'; +import WebSocket, { WebSocketServer } from 'ws'; +import portfinder from 'portfinder'; +import type { InterceptSetup } from '../types/intercept-setup'; +import { reactive, ref } from 'vue'; +import { wait } from './wait'; +import type { WaitOptions } from '../types/options/wait'; + +export async function websocketSetup(page: Page, options: WebsocketOptions): Promise { + const interceptSetup = { + wssPayloads: reactive([]), + previousRequestCount: ref(0), + method: 'WSS', + ...options, + }; + + const port = await portfinder.getPortPromise(); + + const localWssUrl = `ws://localhost:${port}`; + + // @todo: ...ugh + await page.route("**", async (route) => { + try { + const response = await route.fetch(); + const frameHtml = await response.text(); + const body = frameHtml.replaceAll(options.url, localWssUrl); + const headers = response.headers(); + const csp = headers['content-security-policy']; + headers['content-security-policy'] = csp?.replace( + "connect-src 'self'", + "connect-src 'self' localhost:* ws://localhost:*" + ); + await route.fulfill({ response, body, headers }); + } catch { + route.continue(); + } + }); + + const wss = new WebSocketServer({ port }); + + let send: (message: string) => void; + + wss.on('connection', (ws: WebSocket) => { + send = ws.send.bind(ws); + + ws.on('message', (data) => { + interceptSetup.wssPayloads?.push(data.toString()); + options.handler({ message: data.toString(), send }); + }); + }); + + return { + wait: (waitOptions?: WaitOptions) => wait(interceptSetup, waitOptions), + send: (message: string) => send(message), + wssPayloads: interceptSetup.wssPayloads!, + } +} \ No newline at end of file diff --git a/src/types/intercept-setup.ts b/src/types/intercept-setup.ts index c80fa5a..3480bd9 100644 --- a/src/types/intercept-setup.ts +++ b/src/types/intercept-setup.ts @@ -8,8 +8,9 @@ import type { Ref } from 'vue'; */ export type InterceptSetup = InterceptOptions & { - method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'; - match: MatchFunction; - requests: Request[]; + method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'WSS'; + match?: MatchFunction; + requests?: Request[]; + wssPayloads?: string[]; previousRequestCount: Ref; }; diff --git a/src/types/intercept-websocket-surface.ts b/src/types/intercept-websocket-surface.ts new file mode 100644 index 0000000..9edc943 --- /dev/null +++ b/src/types/intercept-websocket-surface.ts @@ -0,0 +1,7 @@ +import type { WaitOptions } from './options/wait'; + +export interface InterceptWebsocketSurface { + wait: (options?: WaitOptions | undefined) => Promise; + send: (message: string) => void; + wssPayloads: string[]; +} diff --git a/src/types/options/intercept.ts b/src/types/options/intercept.ts index 1a5efed..e2d5995 100644 --- a/src/types/options/intercept.ts +++ b/src/types/options/intercept.ts @@ -33,13 +33,22 @@ type FixtureOption = { | ((args: { route: Route; params: Record }) => string); }; +export type WebsocketHandlerArgs = { + message?: string; + send?: (message: string) => void; +}; + +type WebsocketHandlerOption = { + handler: (args: WebsocketHandlerArgs) => void; +}; + type HandlerOption = { handler: (args: { route: Route; params: Record; request: Request; }) => void; -}; +} | WebsocketHandlerOption; export type InterceptOptions = BaseOptions & ( diff --git a/src/types/options/websocket.ts b/src/types/options/websocket.ts new file mode 100644 index 0000000..ec40040 --- /dev/null +++ b/src/types/options/websocket.ts @@ -0,0 +1,6 @@ +import type { WebsocketHandlerArgs } from "./intercept"; + +export interface WebsocketOptions { + url: string; + handler: (args: WebsocketHandlerArgs) => void; +} diff --git a/tests/e2e/base.ts b/tests/e2e/base.ts index 3f58bae..3dc2d0f 100644 --- a/tests/e2e/base.ts +++ b/tests/e2e/base.ts @@ -21,7 +21,7 @@ const test = base.extend({ const mockAppGet = intercept.get({ url: 'https://example.com', - fixture: 'mock-app/index.html', + fixture: 'mock-app/xhr.html', }); await page.goto('https://example.com'); diff --git a/tests/e2e/internals.spec.ts b/tests/e2e/internals.spec.ts index fc18448..f8d0fa5 100644 --- a/tests/e2e/internals.spec.ts +++ b/tests/e2e/internals.spec.ts @@ -178,7 +178,7 @@ test.describe('Internals', () => { } test('HTML', () => { - runTest('index.html', 'text/html'); + runTest('xhr.html', 'text/html'); }); test('CSS', () => { diff --git a/tests/e2e/mock-app/websockets.html b/tests/e2e/mock-app/websockets.html new file mode 100644 index 0000000..a34366e --- /dev/null +++ b/tests/e2e/mock-app/websockets.html @@ -0,0 +1,68 @@ + + + + Mock App + + + + +
+ + Message + + + + + + +
+ + + + + \ No newline at end of file diff --git a/tests/e2e/mock-app/index.html b/tests/e2e/mock-app/xhr.html similarity index 100% rename from tests/e2e/mock-app/index.html rename to tests/e2e/mock-app/xhr.html diff --git a/tests/e2e/websockets.spec.ts b/tests/e2e/websockets.spec.ts new file mode 100644 index 0000000..79e816a --- /dev/null +++ b/tests/e2e/websockets.spec.ts @@ -0,0 +1,44 @@ +import { Intercept } from '../../dist'; +import { test as base, expect } from '@playwright/test'; +import path from 'path'; + +type BaseFixtures = { + intercept: Intercept; +}; + +const test = base.extend({ + intercept: async ({ page }, use) => { + await use( + new Intercept(page, { + fixturePathPrefix: path.join(process.cwd(), 'tests/e2e'), + }) + ); + }, +}); + +test.describe('Websockets', () => { + test('Intercept websockets', async ({ page, intercept }) => { + const mockWebsocketServer = await intercept.wss({ + url: 'wss://demo.piesocket.com/v3/channel_123?api_key=VCXCEuvhGcBDP7XhiJJUDvR1e1D3eiVjgZ9VRiaV¬ify_self', + handler: ({ message, send }) => { + if (message === "Hello PieSocket!") { + send!("Hi there"); + } + } + }); + + await page.goto('https://piehost.com/websocket-tester'); + + await page.getByRole('button', { name: 'Connect' }).click(); + + await expect(page.getByText('Connection Established')).toBeVisible(); + + await page.getByRole('button', { name: 'Send' }).click(); + + await mockWebsocketServer.wait(); + + await expect(page.getByText('Hi there')).toBeVisible(); + + expect(mockWebsocketServer.wssPayloads?.length).toBe(1); + }); +}); \ No newline at end of file diff --git a/tests/examples/collection-of-intercepts-as-fixture.spec.ts b/tests/examples/collection-of-intercepts-as-fixture.spec.ts index 8c2c0b1..a9c092f 100644 --- a/tests/examples/collection-of-intercepts-as-fixture.spec.ts +++ b/tests/examples/collection-of-intercepts-as-fixture.spec.ts @@ -55,7 +55,7 @@ test('Example using collection of intercepts', async ({ }) => { const mockDemoAppGet = intercept.get({ url: 'https://example.com', - fixture: 'mock-app/index.html', // Yes, you can even mock HTML responses! + fixture: 'mock-app/xhr.html', // Yes, you can even mock HTML responses! }); await page.goto('https://example.com'); From cefaccb077e4f3669b68d9d9b583f38f50bc0ae5 Mon Sep 17 00:00:00 2001 From: Alec Armbruster <35377827+alectrocute@users.noreply.github.com> Date: Mon, 26 Aug 2024 15:12:06 -0700 Subject: [PATCH 2/5] remove associated demo app --- src/lib/websocket-setup.ts | 1 + tests/e2e/base.ts | 2 +- tests/e2e/internals.spec.ts | 2 +- tests/e2e/mock-app/{xhr.html => index.html} | 0 tests/e2e/mock-app/websockets.html | 68 ------------------- ...ollection-of-intercepts-as-fixture.spec.ts | 2 +- 6 files changed, 4 insertions(+), 71 deletions(-) rename tests/e2e/mock-app/{xhr.html => index.html} (100%) delete mode 100644 tests/e2e/mock-app/websockets.html diff --git a/src/lib/websocket-setup.ts b/src/lib/websocket-setup.ts index 62133dc..8b1076e 100644 --- a/src/lib/websocket-setup.ts +++ b/src/lib/websocket-setup.ts @@ -22,6 +22,7 @@ export async function websocketSetup(page: Page, options: WebsocketOptions): Pro // @todo: ...ugh await page.route("**", async (route) => { + console.log("route", route.request().url()); try { const response = await route.fetch(); const frameHtml = await response.text(); diff --git a/tests/e2e/base.ts b/tests/e2e/base.ts index 3dc2d0f..3f58bae 100644 --- a/tests/e2e/base.ts +++ b/tests/e2e/base.ts @@ -21,7 +21,7 @@ const test = base.extend({ const mockAppGet = intercept.get({ url: 'https://example.com', - fixture: 'mock-app/xhr.html', + fixture: 'mock-app/index.html', }); await page.goto('https://example.com'); diff --git a/tests/e2e/internals.spec.ts b/tests/e2e/internals.spec.ts index f8d0fa5..fc18448 100644 --- a/tests/e2e/internals.spec.ts +++ b/tests/e2e/internals.spec.ts @@ -178,7 +178,7 @@ test.describe('Internals', () => { } test('HTML', () => { - runTest('xhr.html', 'text/html'); + runTest('index.html', 'text/html'); }); test('CSS', () => { diff --git a/tests/e2e/mock-app/xhr.html b/tests/e2e/mock-app/index.html similarity index 100% rename from tests/e2e/mock-app/xhr.html rename to tests/e2e/mock-app/index.html diff --git a/tests/e2e/mock-app/websockets.html b/tests/e2e/mock-app/websockets.html deleted file mode 100644 index a34366e..0000000 --- a/tests/e2e/mock-app/websockets.html +++ /dev/null @@ -1,68 +0,0 @@ - - - - Mock App - - - - -
- - Message - - - - - - -
- - - - - \ No newline at end of file diff --git a/tests/examples/collection-of-intercepts-as-fixture.spec.ts b/tests/examples/collection-of-intercepts-as-fixture.spec.ts index a9c092f..8c2c0b1 100644 --- a/tests/examples/collection-of-intercepts-as-fixture.spec.ts +++ b/tests/examples/collection-of-intercepts-as-fixture.spec.ts @@ -55,7 +55,7 @@ test('Example using collection of intercepts', async ({ }) => { const mockDemoAppGet = intercept.get({ url: 'https://example.com', - fixture: 'mock-app/xhr.html', // Yes, you can even mock HTML responses! + fixture: 'mock-app/index.html', // Yes, you can even mock HTML responses! }); await page.goto('https://example.com'); From b130f762bd4823f8c8ba9c50514829dcb03b02f9 Mon Sep 17 00:00:00 2001 From: Alec Armbruster <35377827+alectrocute@users.noreply.github.com> Date: Mon, 26 Aug 2024 15:13:40 -0700 Subject: [PATCH 3/5] emit message when handshake occurs --- src/lib/websocket-setup.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/lib/websocket-setup.ts b/src/lib/websocket-setup.ts index 8b1076e..ac1303a 100644 --- a/src/lib/websocket-setup.ts +++ b/src/lib/websocket-setup.ts @@ -46,6 +46,8 @@ export async function websocketSetup(page: Page, options: WebsocketOptions): Pro wss.on('connection', (ws: WebSocket) => { send = ws.send.bind(ws); + options.handler({ message: 'intercept:handshake', send }); + ws.on('message', (data) => { interceptSetup.wssPayloads?.push(data.toString()); options.handler({ message: data.toString(), send }); From 74342fa86d81ac9d21a96a24417599d2528b04f1 Mon Sep 17 00:00:00 2001 From: Alec Armbruster <35377827+alectrocute@users.noreply.github.com> Date: Mon, 26 Aug 2024 15:14:18 -0700 Subject: [PATCH 4/5] remove console.log --- src/lib/websocket-setup.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/lib/websocket-setup.ts b/src/lib/websocket-setup.ts index ac1303a..cac9262 100644 --- a/src/lib/websocket-setup.ts +++ b/src/lib/websocket-setup.ts @@ -22,7 +22,6 @@ export async function websocketSetup(page: Page, options: WebsocketOptions): Pro // @todo: ...ugh await page.route("**", async (route) => { - console.log("route", route.request().url()); try { const response = await route.fetch(); const frameHtml = await response.text(); From 2fc717f8b4de4e9aba023082722541cf13c1ccec Mon Sep 17 00:00:00 2001 From: Alec Armbruster <35377827+alectrocute@users.noreply.github.com> Date: Mon, 26 Aug 2024 15:23:44 -0700 Subject: [PATCH 5/5] clean up websockets.spec.ts --- src/lib/websocket-setup.ts | 91 ++++++++++++++++++++---------------- tests/e2e/websockets.spec.ts | 15 ++++-- 2 files changed, 62 insertions(+), 44 deletions(-) diff --git a/src/lib/websocket-setup.ts b/src/lib/websocket-setup.ts index cac9262..05eadb6 100644 --- a/src/lib/websocket-setup.ts +++ b/src/lib/websocket-setup.ts @@ -9,53 +9,66 @@ import { wait } from './wait'; import type { WaitOptions } from '../types/options/wait'; export async function websocketSetup(page: Page, options: WebsocketOptions): Promise { - const interceptSetup = { - wssPayloads: reactive([]), - previousRequestCount: ref(0), - method: 'WSS', - ...options, - }; - - const port = await portfinder.getPortPromise(); - - const localWssUrl = `ws://localhost:${port}`; - - // @todo: ...ugh - await page.route("**", async (route) => { + const interceptSetup = { + wssPayloads: reactive([]), + previousRequestCount: ref(0), + method: 'WSS', + ...options, + }; + + const port = await portfinder.getPortPromise(); + + const localWssUrl = `ws://localhost:${port}`; + + // @todo: ...ugh + await page.route("**", async (route) => { + try { + const response = await route.fetch(); + const pageHtml = await response.text(); + + if (!pageHtml.includes(options.url)) { + route.continue(); + return; + } + + const body = pageHtml.replaceAll(options.url, localWssUrl); + const headers = response.headers(); + const csp = headers['content-security-policy']; + headers['content-security-policy'] = csp?.replace( + "connect-src 'self'", + "connect-src 'self' localhost:* ws://localhost:*" + ); + + await route.fulfill({ response, body, headers }); + } catch (e) { try { - const response = await route.fetch(); - const frameHtml = await response.text(); - const body = frameHtml.replaceAll(options.url, localWssUrl); - const headers = response.headers(); - const csp = headers['content-security-policy']; - headers['content-security-policy'] = csp?.replace( - "connect-src 'self'", - "connect-src 'self' localhost:* ws://localhost:*" - ); - await route.fulfill({ response, body, headers }); + route?.continue(); } catch { - route.continue(); + // } - }); + } + }); - const wss = new WebSocketServer({ port }); + const wss = new WebSocketServer({ port }); - let send: (message: string) => void; + let send: (message: string) => void; - wss.on('connection', (ws: WebSocket) => { - send = ws.send.bind(ws); + wss.on('connection', (ws: WebSocket) => { + send = ws.send.bind(ws); - options.handler({ message: 'intercept:handshake', send }); + const handshakeMessage = 'intercept:handshake'; + interceptSetup.wssPayloads?.push(handshakeMessage); + options.handler({ message: handshakeMessage, send }); - ws.on('message', (data) => { - interceptSetup.wssPayloads?.push(data.toString()); - options.handler({ message: data.toString(), send }); - }); + ws.on('message', (data) => { + interceptSetup.wssPayloads?.push(data.toString()); + options.handler({ message: data.toString(), send }); }); + }); - return { - wait: (waitOptions?: WaitOptions) => wait(interceptSetup, waitOptions), - send: (message: string) => send(message), - wssPayloads: interceptSetup.wssPayloads!, - } + return { + wait: (waitOptions?: WaitOptions) => wait(interceptSetup, waitOptions), + send: (message: string) => send(message), + wssPayloads: interceptSetup.wssPayloads!, + } } \ No newline at end of file diff --git a/tests/e2e/websockets.spec.ts b/tests/e2e/websockets.spec.ts index 79e816a..579e4fe 100644 --- a/tests/e2e/websockets.spec.ts +++ b/tests/e2e/websockets.spec.ts @@ -1,6 +1,5 @@ import { Intercept } from '../../dist'; import { test as base, expect } from '@playwright/test'; -import path from 'path'; type BaseFixtures = { intercept: Intercept; @@ -9,9 +8,7 @@ type BaseFixtures = { const test = base.extend({ intercept: async ({ page }, use) => { await use( - new Intercept(page, { - fixturePathPrefix: path.join(process.cwd(), 'tests/e2e'), - }) + new Intercept(page) ); }, }); @@ -24,6 +21,10 @@ test.describe('Websockets', () => { if (message === "Hello PieSocket!") { send!("Hi there"); } + + if (message === "intercept:handshake") { + send!("I have intercepted the handshake!"); + } } }); @@ -31,6 +32,8 @@ test.describe('Websockets', () => { await page.getByRole('button', { name: 'Connect' }).click(); + await mockWebsocketServer.wait(); + await expect(page.getByText('Connection Established')).toBeVisible(); await page.getByRole('button', { name: 'Send' }).click(); @@ -39,6 +42,8 @@ test.describe('Websockets', () => { await expect(page.getByText('Hi there')).toBeVisible(); - expect(mockWebsocketServer.wssPayloads?.length).toBe(1); + expect(mockWebsocketServer.wssPayloads?.length).toBe(2); + + expect(mockWebsocketServer.wssPayloads?.[1]).toBe("Hello PieSocket!"); }); }); \ No newline at end of file