From e86c8af5994122d99c4e5debb47f94a6c8bf62f3 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Tue, 23 Jul 2024 07:43:28 -0700 Subject: [PATCH] chore: rename `route` fixture in ct (#31817) Addresses review feedback. --- docs/src/test-components-js.md | 51 ++++--------------- packages/playwright-ct-core/index.d.ts | 9 ++-- packages/playwright-ct-core/src/mount.ts | 10 ++-- .../src/{route.ts => router.ts} | 25 +++------ .../ct-react-vite/tests/route.spec.tsx | 49 +++++++++--------- 5 files changed, 48 insertions(+), 96 deletions(-) rename packages/playwright-ct-core/src/{route.ts => router.ts} (89%) diff --git a/docs/src/test-components-js.md b/docs/src/test-components-js.md index 60d920ee73..dc448eff00 100644 --- a/docs/src/test-components-js.md +++ b/docs/src/test-components-js.md @@ -726,51 +726,18 @@ test('update', async ({ mount }) => { ### Handling network requests -Playwright provides a `route` fixture to intercept and handle network requests. +Playwright provides an **experimental** `router` fixture to intercept and handle network requests. There are two ways to use the `router` fixture: +* Call `router.route(url, handler)` that behaves similarly to [`method: Page.route`]. See the [network mocking guide](./mock.md) for more details. +* Call `router.use(handlers)` and pass [MSW library](https://mswjs.io/) request handlers to it. -```ts -test.beforeEach(async ({ route }) => { - // install common routes before each test - await route('*/**/api/v1/fruits', async route => { - const json = [{ name: 'Strawberry', id: 21 }]; - await route.fulfill({ json }); - }); -}); - -test('example test', async ({ mount }) => { - // test as usual, your routes are active - // ... -}); -``` - -You can also introduce test-specific routes. - -```ts -import { http, HttpResponse } from 'msw'; - -test('example test', async ({ mount, route }) => { - await route('*/**/api/v1/fruits', async route => { - const json = [{ name: 'fruit for this single test', id: 42 }]; - await route.fulfill({ json }); - }); - - // test as usual, your route is active - // ... -}); -``` - -The `route` fixture works in the same way as [`method: Page.route`]. See the [network mocking guide](./mock.md) for more details. - -**Re-using MSW handlers** - -If you are using the [MSW library](https://mswjs.io/) to handle network requests during development or testing, you can pass them directly to the `route` fixture. +Here is an example of reusing your existing MSW handlers in the test. ```ts import { handlers } from '@src/mocks/handlers'; -test.beforeEach(async ({ route }) => { +test.beforeEach(async ({ router }) => { // install common handlers before each test - await route(handlers); + await router.use(...handlers); }); test('example test', async ({ mount }) => { @@ -779,13 +746,13 @@ test('example test', async ({ mount }) => { }); ``` -You can also introduce test-specific handlers. +You can also introduce a one-off handler for a specific test. ```ts import { http, HttpResponse } from 'msw'; -test('example test', async ({ mount, route }) => { - await route(http.get('/data', async ({ request }) => { +test('example test', async ({ mount, router }) => { + await router.use(http.get('/data', async ({ request }) => { return HttpResponse.json({ value: 'mocked' }); })); diff --git a/packages/playwright-ct-core/index.d.ts b/packages/playwright-ct-core/index.d.ts index 1006bd59ec..9c323ef6df 100644 --- a/packages/playwright-ct-core/index.d.ts +++ b/packages/playwright-ct-core/index.d.ts @@ -38,14 +38,13 @@ interface RequestHandler { run(args: { request: Request, requestId?: string, resolutionContext?: { baseUrl?: string } }): Promise<{ response?: Response } | null>; } -export interface RouteFixture { - (...args: Parameters): Promise; - (handlers: RequestHandler[]): Promise; - (handler: RequestHandler): Promise; +export interface RouterFixture { + route(...args: Parameters): Promise; + use(...handlers: RequestHandler[]): Promise; } export type TestType = BaseTestType< - PlaywrightTestArgs & PlaywrightTestOptions & ComponentFixtures & { route: RouteFixture }, + PlaywrightTestArgs & PlaywrightTestOptions & ComponentFixtures & { router: RouterFixture }, PlaywrightWorkerArgs & PlaywrightWorkerOptions >; diff --git a/packages/playwright-ct-core/src/mount.ts b/packages/playwright-ct-core/src/mount.ts index 3fa56a3a5b..a3bcfe0641 100644 --- a/packages/playwright-ct-core/src/mount.ts +++ b/packages/playwright-ct-core/src/mount.ts @@ -19,8 +19,8 @@ import type { Component, JsxComponent, MountOptions, ObjectComponentOptions } fr import type { ContextReuseMode, FullConfigInternal } from '../../playwright/src/common/config'; import type { ImportRef } from './injected/importRegistry'; import { wrapObject } from './injected/serializers'; -import { Router } from './route'; -import type { RouteFixture } from '../index'; +import { Router } from './router'; +import type { RouterFixture } from '../index'; let boundCallbacksForMount: Function[] = []; @@ -31,7 +31,7 @@ interface MountResult extends Locator { type TestFixtures = PlaywrightTestArgs & PlaywrightTestOptions & { mount: (component: any, options: any) => Promise; - route: RouteFixture; + router: RouterFixture; }; type WorkerFixtures = PlaywrightWorkerArgs & PlaywrightWorkerOptions; type BaseTestFixtures = { @@ -80,9 +80,9 @@ export const fixtures: Fixtures boundCallbacksForMount = []; }, - route: async ({ context, baseURL }, use) => { + router: async ({ context, baseURL }, use) => { const router = new Router(context, baseURL); - await use((...args) => router.handle(...args)); + await use(router); await router.dispose(); }, }; diff --git a/packages/playwright-ct-core/src/route.ts b/packages/playwright-ct-core/src/router.ts similarity index 89% rename from packages/playwright-ct-core/src/route.ts rename to packages/playwright-ct-core/src/router.ts index 92aca8be30..50adf8c573 100644 --- a/packages/playwright-ct-core/src/route.ts +++ b/packages/playwright-ct-core/src/router.ts @@ -134,25 +134,14 @@ export class Router { }; } - async handle(...args: any[]) { - // Multiple RequestHandlers. - if (Array.isArray(args[0])) { - const handlers = args[0] as RequestHandler[]; - this._requestHandlers = handlers.concat(this._requestHandlers); - await this._updateRequestHandlersRoute(); - return; - } - // Single RequestHandler. - if (args.length === 1 && typeof args[0] === 'object') { - const handlers = [args[0] as RequestHandler]; - this._requestHandlers = handlers.concat(this._requestHandlers); - await this._updateRequestHandlersRoute(); - return; - } - // Arguments of BrowserContext.route(url, handler, options?). - const routeArgs = args as RouteArgs; + async route(...routeArgs: RouteArgs) { this._routes.push(routeArgs); - await this._context.route(...routeArgs); + return await this._context.route(...routeArgs); + } + + async use(...handlers: RequestHandler[]) { + this._requestHandlers = handlers.concat(this._requestHandlers); + await this._updateRequestHandlersRoute(); } async dispose() { diff --git a/tests/components/ct-react-vite/tests/route.spec.tsx b/tests/components/ct-react-vite/tests/route.spec.tsx index 62a8680a1b..5e17d00dfc 100644 --- a/tests/components/ct-react-vite/tests/route.spec.tsx +++ b/tests/components/ct-react-vite/tests/route.spec.tsx @@ -26,14 +26,14 @@ test('should load font with routes', async ({ mount, page }) => { }); test.describe('request handlers', () => { - test('should handle requests', async ({ page, mount, route }) => { + test('should handle requests', async ({ page, mount, router }) => { let respond: (() => void) = () => {}; const promise = new Promise(f => respond = f); let postReceived: ((body: string) => void) = () => {}; const postBody = new Promise(f => postReceived = f); - await route([ + await router.use( http.get('/data.json', async () => { await promise; return HttpResponse.json({ name: 'John Doe' }); @@ -42,7 +42,7 @@ test.describe('request handlers', () => { postReceived(await request.text()); return HttpResponse.text('ok'); }), - ]); + ); const component = await mount(); await expect(component.getByTestId('name')).toHaveText(''); @@ -54,15 +54,15 @@ test.describe('request handlers', () => { expect(await postBody).toBe('hello from the page'); }); - test('should add dynamically', async ({ page, mount, route }) => { - await route('**/data.json', async route => { + test('should add dynamically', async ({ page, mount, router }) => { + await router.route('**/data.json', async route => { await route.fulfill({ body: JSON.stringify({ name: '' }) }); }); const component = await mount(); await expect(component.getByTestId('name')).toHaveText(''); - await route( + await router.use( http.get('/data.json', async () => { return HttpResponse.json({ name: 'John Doe' }); }), @@ -72,12 +72,12 @@ test.describe('request handlers', () => { await expect(component.getByTestId('name')).toHaveText('John Doe'); }); - test('should passthrough', async ({ page, mount, route }) => { - await route('**/data.json', async route => { + test('should passthrough', async ({ page, mount, router }) => { + await router.route('**/data.json', async route => { await route.fulfill({ body: JSON.stringify({ name: '' }) }); }); - await route( + await router.use( http.get('/data.json', async () => { return passthrough(); }), @@ -87,13 +87,13 @@ test.describe('request handlers', () => { await expect(component.getByTestId('name')).toHaveText(''); }); - test('should fallback when nothing is returned', async ({ page, mount, route }) => { - await route('**/data.json', async route => { + test('should fallback when nothing is returned', async ({ page, mount, router }) => { + await router.route('**/data.json', async route => { await route.fulfill({ body: JSON.stringify({ name: '' }) }); }); let called = false; - await route( + await router.use( http.get('/data.json', async () => { called = true; }), @@ -104,12 +104,12 @@ test.describe('request handlers', () => { expect(called).toBe(true); }); - test('should bypass(request)', async ({ page, mount, route }) => { - await route('**/data.json', async route => { + test('should bypass(request)', async ({ page, mount, router }) => { + await router.route('**/data.json', async route => { await route.fulfill({ body: JSON.stringify({ name: `` }) }); }); - await route( + await router.use( http.get('/data.json', async ({ request }) => { return await fetch(bypass(request)); }), @@ -119,7 +119,7 @@ test.describe('request handlers', () => { await expect(component.getByTestId('name')).toHaveText(''); }); - test('should bypass(url) and get cookies', async ({ page, mount, route, browserName }) => { + test('should bypass(url) and get cookies', async ({ page, mount, router, browserName }) => { let cookie = ''; const server = new httpServer.Server(); server.on('request', (req, res) => { @@ -129,7 +129,7 @@ test.describe('request handlers', () => { await new Promise(f => server.listen(0, f)); const port = (server.address() as net.AddressInfo).port; - await route('**/data.json', async route => { + await router.route('**/data.json', async route => { await route.fulfill({ body: JSON.stringify({ name: `` }) }); }); @@ -137,7 +137,7 @@ test.describe('request handlers', () => { await expect(component.getByTestId('name')).toHaveText(''); await page.evaluate(() => document.cookie = 'foo=bar'); - await route( + await router.use( http.get('/data.json', async ({ request }) => { if (browserName !== 'webkit') { // WebKit does not have cookies while intercepting. @@ -153,12 +153,12 @@ test.describe('request handlers', () => { await new Promise(f => server.close(f)); }); - test('should ignore navigation requests', async ({ page, mount, route }) => { - await route('**/newpage', async route => { + test('should ignore navigation requests', async ({ page, mount, router }) => { + await router.route('**/newpage', async route => { await route.fulfill({ body: `
original
`, contentType: 'text/html' }); }); - await route( + await router.use( http.get('/newpage', async ({ request }) => { return new Response(`
intercepted
`, { headers: new Headers({ 'Content-Type': 'text/html' }), @@ -171,11 +171,8 @@ test.describe('request handlers', () => { await expect(page.locator('div')).toHaveText('original'); }); - test('should throw when calling fetch(bypass) outside of a handler', async ({ page, route, baseURL }) => { - await route( - http.get('/data.json', async () => { - }), - ); + test('should throw when calling fetch(bypass) outside of a handler', async ({ page, router, baseURL }) => { + await router.use(http.get('/data.json', async () => {})); const error = await fetch(bypass(baseURL + '/hello')).catch(e => e); expect(error.message).toContain(`Cannot call fetch(bypass()) outside of a request handler`);