Merge c322af9ed9 into 4bc8cf0d47
This commit is contained in:
commit
ea480a0d08
261
docs/src/mock.md
261
docs/src/mock.md
|
|
@ -554,3 +554,264 @@ await page.RouteWebSocketAsync("wss://example.com/ws", ws => {
|
|||
```
|
||||
|
||||
For more details, see [WebSocketRoute].
|
||||
|
||||
## Mock Server
|
||||
* langs: js
|
||||
|
||||
By default, Playwright only has access to the network traffic made by the browser.
|
||||
To mock and intercept traffic made by the application server, use Playwright's **experimental** mocking proxy. Note this feature is **experimental** and subject to change.
|
||||
|
||||
The mocking proxy is a HTTP proxy server that's connected to the currently running test.
|
||||
If you send it a request, it will apply the network routes configured via `page.route` and `context.route`, reusing your existing browser routes.
|
||||
|
||||
To get started, enable the `mockingProxy` option in your Playwright config:
|
||||
|
||||
```js
|
||||
export default defineConfig({
|
||||
use: { mockingProxy: 'inject-via-header' }
|
||||
});
|
||||
```
|
||||
|
||||
Playwright will now inject the proxy URL into all browser requests under the `x-playwright-proxy` header.
|
||||
On your server, read the URL in this header and prepend it to all outgoing traffic you want to intercept:
|
||||
|
||||
```js
|
||||
const headers = getCurrentRequestHeaders(); // this looks different for each application
|
||||
const proxyURL = decodeURIComponent(headers.get('x-playwright-proxy') ?? '');
|
||||
await fetch(proxyURL + 'https://api.example.com/users');
|
||||
```
|
||||
|
||||
Prepending the URL will direct the request through the proxy. You can now intercept it with [`method: BrowserContext.route`] and [`method: Page.route`], just like browser requests:
|
||||
```js
|
||||
// shopping-cart.spec.ts
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test('checkout applies customer loyalty bonus points', async ({ page }) => {
|
||||
await page.route('https://users.internal.example.com/loyalty/balance*', (route, request) => {
|
||||
await route.fulfill({ json: { userId: 'jane@doe.com', balance: 100 } });
|
||||
});
|
||||
|
||||
await page.goto('http://localhost:3000/checkout');
|
||||
|
||||
await expect(page.getByRole('list')).toMatchAriaSnapshot(`
|
||||
- list "Cart":
|
||||
- listitem: Super Duper Hammer
|
||||
- listitem: Nails
|
||||
- listitem: 16mm Birch Plywood
|
||||
- text: "Price after applying 10$ loyalty discount: 79.99$"
|
||||
- button "Buy now"
|
||||
`);
|
||||
});
|
||||
```
|
||||
|
||||
Now, prepending the proxy URL manually can be cumbersome. If your HTTP client supports it, consider setting up a global interceptor:
|
||||
|
||||
```js
|
||||
import { axios } from 'axios';
|
||||
|
||||
axios.interceptors.request.use(async config => {
|
||||
const headers = getCurrentRequestHeaders(); // this line looks different for each application
|
||||
const proxy = decodeURIComponent(headers.get('x-playwright-proxy') ?? '');
|
||||
config.url = new URL(proxy + config.url, config.baseURL).toString();
|
||||
return config;
|
||||
});
|
||||
```
|
||||
|
||||
```js
|
||||
import { setGlobalDispatcher, getGlobalDispatcher } from 'undici';
|
||||
|
||||
const proxyingDispatcher = getGlobalDispatcher().compose(dispatch => (opts, handler) => {
|
||||
const headers = getCurrentRequestHeaders(); // this line looks different for each application
|
||||
const proxy = decodeURIComponent(headers.get('x-playwright-proxy') ?? '');
|
||||
const newURL = new URL(proxy + opts.origin + opts.path);
|
||||
opts.origin = newURL.origin;
|
||||
opts.path = newURL.pathname;
|
||||
return dispatch(opts, handler);
|
||||
});
|
||||
setGlobalDispatcher(proxyingDispatcher); // this will also apply to global fetch
|
||||
```
|
||||
|
||||
:::note
|
||||
Note that this style of proxying, where the proxy URL is prepended to the request URL, does *not* use [`CONNECT`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/CONNECT), which is the common way of establishing a proxy connection.
|
||||
This is because for HTTPS requests, a `CONNECT` proxy does not have access to the proxied traffic. That's great behaviour for a production proxy, but counteracts network interception!
|
||||
:::
|
||||
|
||||
:::note
|
||||
Known Limitations:
|
||||
|
||||
1. The mocking proxy is experimental and subject to change.
|
||||
2. The injected `x-playwright-proxy` header affects CORS and might turn [simple requests](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#simple_requests) into requests that require a preflight.
|
||||
3. Requests on the server that were not made in response to a browser request, like those triggered by CRON job, won't be routed because they don't have access to the `x-playwright-proxy` header.
|
||||
4. On Firefox, the first requests after page open might not be intercepted by the mocking proxy.
|
||||
5. `defaultContextOptions` aren't applied when using `route.fetch`.
|
||||
:::
|
||||
|
||||
|
||||
### Recipes
|
||||
* langs: js
|
||||
|
||||
#### Next.js
|
||||
* langs: js
|
||||
|
||||
Monkey-patch `globalThis.fetch` in your `instrumentation.ts` file:
|
||||
|
||||
```js
|
||||
// instrumentation.ts
|
||||
|
||||
import { headers } from 'next/headers';
|
||||
|
||||
export function register() {
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
const originalFetch = globalThis.fetch;
|
||||
globalThis.fetch = async (input, init) => {
|
||||
const proxy = (await headers()).get('x-playwright-proxy');
|
||||
if (!proxy)
|
||||
return originalFetch(input, init);
|
||||
const request = new Request(input, init);
|
||||
return originalFetch(decodeURIComponent(proxy) + request.url, request);
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Remix
|
||||
* langs: js
|
||||
|
||||
|
||||
Monkey-patch `globalThis.fetch` in your `entry.server.ts` file, and use `AsyncLocalStorage` to make current request headers available:
|
||||
|
||||
```js
|
||||
import { setGlobalDispatcher, getGlobalDispatcher } from 'undici';
|
||||
import { AsyncLocalStorage } from 'node:async_hooks';
|
||||
|
||||
const headersStore = new AsyncLocalStorage<Headers>();
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
const originalFetch = globalThis.fetch;
|
||||
globalThis.fetch = async (input, init) => {
|
||||
const proxy = headersStore.getStore()?.get('x-playwright-proxy');
|
||||
if (!proxy)
|
||||
return originalFetch(input, init);
|
||||
const request = new Request(input, init);
|
||||
return originalFetch(decodeURIComponent(proxy) + request.url, request);
|
||||
};
|
||||
}
|
||||
|
||||
export default function handleRequest(request: Request, /* ... */) {
|
||||
return headersStore.run(request.headers, () => {
|
||||
// ...
|
||||
return handleBrowserRequest(request, /* ... */);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
#### Angular
|
||||
* langs: js
|
||||
|
||||
Configure your `HttpClient` with an [interceptor](https://angular.dev/guide/http/setup#withinterceptors):
|
||||
|
||||
```js
|
||||
// app.config.server.ts
|
||||
|
||||
import { inject, REQUEST } from '@angular/core';
|
||||
import { provideHttpClient, withInterceptors } from '@angular/common/http';
|
||||
|
||||
const serverConfig = {
|
||||
providers: [
|
||||
/* ... */
|
||||
provideHttpClient(
|
||||
/* ... */
|
||||
withInterceptors([
|
||||
(req, next) => {
|
||||
const proxy = inject(REQUEST)?.headers.get('x-playwright-proxy');
|
||||
if (proxy)
|
||||
req = req.clone({ url: decodeURIComponent(proxy) + req.url });
|
||||
return next(req);
|
||||
},
|
||||
]),
|
||||
)
|
||||
]
|
||||
};
|
||||
|
||||
/* ... */
|
||||
```
|
||||
|
||||
#### Astro
|
||||
* langs: js
|
||||
|
||||
Set up a server-side fetch override in an Astro integration:
|
||||
|
||||
```js
|
||||
// astro.config.mjs
|
||||
import { defineConfig } from 'astro/config';
|
||||
import type { AstroIntegration } from 'astro';
|
||||
import { AsyncLocalStorage } from 'async_hooks';
|
||||
|
||||
const playwrightMockingProxy: AstroIntegration = {
|
||||
name: 'playwrightMockingProxy',
|
||||
hooks: {
|
||||
'astro:server:setup': async astro => {
|
||||
if (process.env.NODE_ENV !== 'test')
|
||||
return;
|
||||
|
||||
const proxyStorage = new AsyncLocalStorage<string>();
|
||||
const originalFetch = globalThis.fetch;
|
||||
globalThis.fetch = async (input, init) => {
|
||||
const proxy = proxyStorage.getStore();
|
||||
if (!proxy)
|
||||
return originalFetch(input, init);
|
||||
const request = new Request(input, init);
|
||||
return originalFetch(proxy + request.url, request);
|
||||
};
|
||||
astro.server.middlewares.use((req, res, next) => {
|
||||
const header = req.headers['x-playwright-proxy'] as string;
|
||||
if (typeof header !== 'string')
|
||||
return next();
|
||||
proxyStorage.run(decodeURIComponent(header), next);
|
||||
});
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
export default defineConfig({
|
||||
integrations: [
|
||||
playwrightMockingProxy
|
||||
]
|
||||
});
|
||||
```
|
||||
|
||||
#### Nuxt
|
||||
|
||||
```js
|
||||
// server/plugins/playwright-mocking-proxy.ts
|
||||
|
||||
import { getGlobalDispatcher, setGlobalDispatcher } from 'undici';
|
||||
import { useEvent, getRequestHeader } from '#imports';
|
||||
|
||||
export default defineNitroPlugin(() => {
|
||||
if (process.env.NODE_ENV !== 'test')
|
||||
return;
|
||||
|
||||
const proxiedDispatcher = getGlobalDispatcher().compose(dispatch => (opts, handler) => {
|
||||
const isInternal = opts.path.startsWith('/__nuxt');
|
||||
const proxy = getRequestHeader(useEvent(), 'x-playwright-proxy');
|
||||
if (proxy && !isInternal) {
|
||||
const newURL = new URL(decodeURIComponent(proxy) + opts.origin + opts.path);
|
||||
opts.origin = newURL.origin;
|
||||
opts.path = newURL.pathname;
|
||||
}
|
||||
return dispatch(opts, handler);
|
||||
});
|
||||
setGlobalDispatcher(proxiedDispatcher);
|
||||
});
|
||||
```
|
||||
|
||||
```js
|
||||
// nuxt.config.ts
|
||||
export default defineNuxtConfig({
|
||||
nitro: {
|
||||
experimental: {
|
||||
asyncContext: true,
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
|
|
|||
|
|
@ -676,3 +676,19 @@ export default defineConfig({
|
|||
},
|
||||
});
|
||||
```
|
||||
|
||||
## property: TestOptions.mockingProxy
|
||||
* since: v1.51
|
||||
- type: <[MockingProxyMode]<"off"|"inject-via-header">> Enables the mocking proxy. Playwright will inject the proxy URL into all outgoing requests under the `x-playwright-proxy` header.
|
||||
|
||||
**Usage**
|
||||
|
||||
```js title="playwright.config.ts"
|
||||
import { defineConfig } from '@playwright/test';
|
||||
|
||||
export default defineConfig({
|
||||
use: {
|
||||
mockingProxy: 'inject-via-header'
|
||||
},
|
||||
});
|
||||
```
|
||||
|
|
@ -81,7 +81,7 @@ export class Browser extends ChannelOwner<channels.BrowserChannel> implements ap
|
|||
|
||||
async _innerNewContext(options: BrowserContextOptions = {}, forReuse: boolean): Promise<BrowserContext> {
|
||||
options = { ...this._browserType._playwright._defaultContextOptions, ...options };
|
||||
const contextOptions = await prepareBrowserContextParams(options);
|
||||
const contextOptions = await prepareBrowserContextParams(options, this._browserType);
|
||||
const response = forReuse ? await this._channel.newContextForReuse(contextOptions) : await this._channel.newContext(contextOptions);
|
||||
const context = BrowserContext.from(response.context);
|
||||
await this._browserType._didCreateContext(context, contextOptions, this._options, options.logger || this._logger);
|
||||
|
|
|
|||
|
|
@ -90,7 +90,11 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
|
|||
this._channel.on('bindingCall', ({ binding }) => this._onBinding(BindingCall.from(binding)));
|
||||
this._channel.on('close', () => this._onClose());
|
||||
this._channel.on('page', ({ page }) => this._onPage(Page.from(page)));
|
||||
this._channel.on('route', ({ route }) => this._onRoute(network.Route.from(route)));
|
||||
this._channel.on('route', params => {
|
||||
const route = network.Route.from(params.route);
|
||||
route._apiRequestContext = this.request;
|
||||
this._onRoute(route);
|
||||
});
|
||||
this._channel.on('webSocketRoute', ({ webSocketRoute }) => this._onWebSocketRoute(network.WebSocketRoute.from(webSocketRoute)));
|
||||
this._channel.on('backgroundPage', ({ page }) => {
|
||||
const backgroundPage = Page.from(page);
|
||||
|
|
@ -136,7 +140,7 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
|
|||
});
|
||||
this._channel.on('request', ({ request, page }) => this._onRequest(network.Request.from(request), Page.fromNullable(page)));
|
||||
this._channel.on('requestFailed', ({ request, failureText, responseEndTiming, page }) => this._onRequestFailed(network.Request.from(request), responseEndTiming, failureText, Page.fromNullable(page)));
|
||||
this._channel.on('requestFinished', params => this._onRequestFinished(params));
|
||||
this._channel.on('requestFinished', ({ request, response, page, responseEndTiming }) => this._onRequestFinished(network.Request.from(request), network.Response.fromNullable(response), Page.fromNullable(page), responseEndTiming));
|
||||
this._channel.on('response', ({ response, page }) => this._onResponse(network.Response.from(response), Page.fromNullable(page)));
|
||||
this._closedPromise = new Promise(f => this.once(Events.BrowserContext.Close, f));
|
||||
|
||||
|
|
@ -164,19 +168,19 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
|
|||
page._opener.emit(Events.Page.Popup, page);
|
||||
}
|
||||
|
||||
private _onRequest(request: network.Request, page: Page | null) {
|
||||
_onRequest(request: network.Request, page: Page | null) {
|
||||
this.emit(Events.BrowserContext.Request, request);
|
||||
if (page)
|
||||
page.emit(Events.Page.Request, request);
|
||||
}
|
||||
|
||||
private _onResponse(response: network.Response, page: Page | null) {
|
||||
_onResponse(response: network.Response, page: Page | null) {
|
||||
this.emit(Events.BrowserContext.Response, response);
|
||||
if (page)
|
||||
page.emit(Events.Page.Response, response);
|
||||
}
|
||||
|
||||
private _onRequestFailed(request: network.Request, responseEndTiming: number, failureText: string | undefined, page: Page | null) {
|
||||
_onRequestFailed(request: network.Request, responseEndTiming: number, failureText: string | undefined, page: Page | null) {
|
||||
request._failureText = failureText || null;
|
||||
request._setResponseEndTiming(responseEndTiming);
|
||||
this.emit(Events.BrowserContext.RequestFailed, request);
|
||||
|
|
@ -184,11 +188,7 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
|
|||
page.emit(Events.Page.RequestFailed, request);
|
||||
}
|
||||
|
||||
private _onRequestFinished(params: channels.BrowserContextRequestFinishedEvent) {
|
||||
const { responseEndTiming } = params;
|
||||
const request = network.Request.from(params.request);
|
||||
const response = network.Response.fromNullable(params.response);
|
||||
const page = Page.fromNullable(params.page);
|
||||
_onRequestFinished(request: network.Request, response: network.Response | null, page: Page | null, responseEndTiming: number) {
|
||||
request._setResponseEndTiming(responseEndTiming);
|
||||
this.emit(Events.BrowserContext.RequestFinished, request);
|
||||
if (page)
|
||||
|
|
@ -198,7 +198,6 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
|
|||
}
|
||||
|
||||
async _onRoute(route: network.Route) {
|
||||
route._context = this;
|
||||
const page = route.request()._safePage();
|
||||
const routeHandlers = this._routes.slice();
|
||||
for (const routeHandler of routeHandlers) {
|
||||
|
|
@ -521,7 +520,7 @@ function prepareRecordHarOptions(options: BrowserContextOptions['recordHar']): c
|
|||
};
|
||||
}
|
||||
|
||||
export async function prepareBrowserContextParams(options: BrowserContextOptions): Promise<channels.BrowserNewContextParams> {
|
||||
export async function prepareBrowserContextParams(options: BrowserContextOptions, type?: BrowserType): Promise<channels.BrowserNewContextParams> {
|
||||
if (options.videoSize && !options.videosPath)
|
||||
throw new Error(`"videoSize" option requires "videosPath" to be specified`);
|
||||
if (options.extraHTTPHeaders)
|
||||
|
|
@ -540,6 +539,7 @@ export async function prepareBrowserContextParams(options: BrowserContextOptions
|
|||
contrast: options.contrast === null ? 'no-override' : options.contrast,
|
||||
acceptDownloads: toAcceptDownloadsProtocol(options.acceptDownloads),
|
||||
clientCertificates: await toClientCertificatesProtocol(options.clientCertificates),
|
||||
mockingProxyBaseURL: type?._playwright._mockingProxy?.baseURL(),
|
||||
};
|
||||
if (!contextParams.recordVideo && options.videosPath) {
|
||||
contextParams.recordVideo = {
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel> imple
|
|||
const logger = options.logger || this._playwright._defaultLaunchOptions?.logger;
|
||||
assert(!(options as any).port, 'Cannot specify a port without launching as a server.');
|
||||
options = { ...this._playwright._defaultLaunchOptions, ...this._playwright._defaultContextOptions, ...options };
|
||||
const contextParams = await prepareBrowserContextParams(options);
|
||||
const contextParams = await prepareBrowserContextParams(options, this);
|
||||
const persistentParams: channels.BrowserTypeLaunchPersistentContextParams = {
|
||||
...contextParams,
|
||||
ignoreDefaultArgs: Array.isArray(options.ignoreDefaultArgs) ? options.ignoreDefaultArgs : undefined,
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ import { findValidator, ValidationError, type ValidatorContext } from '../protoc
|
|||
import { createInstrumentation } from './clientInstrumentation';
|
||||
import type { ClientInstrumentation } from './clientInstrumentation';
|
||||
import { formatCallLog, rewriteErrorMessage, zones } from '../utils';
|
||||
import { MockingProxy } from './mockingProxy';
|
||||
|
||||
class Root extends ChannelOwner<channels.RootChannel> {
|
||||
constructor(connection: Connection) {
|
||||
|
|
@ -279,6 +280,9 @@ export class Connection extends EventEmitter {
|
|||
if (!this._localUtils)
|
||||
this._localUtils = result as LocalUtils;
|
||||
break;
|
||||
case 'MockingProxy':
|
||||
result = new MockingProxy(parent, type, guid, initializer);
|
||||
break;
|
||||
case 'Page':
|
||||
result = new Page(parent, type, guid, initializer);
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ export type FetchOptions = {
|
|||
maxRetries?: number,
|
||||
};
|
||||
|
||||
type NewContextOptions = Omit<channels.PlaywrightNewRequestOptions, 'extraHTTPHeaders' | 'clientCertificates' | 'storageState' | 'tracesDir'> & {
|
||||
export type NewContextOptions = Omit<channels.PlaywrightNewRequestOptions, 'extraHTTPHeaders' | 'clientCertificates' | 'storageState' | 'tracesDir'> & {
|
||||
extraHTTPHeaders?: Headers,
|
||||
storageState?: string | SetStorageState,
|
||||
clientCertificates?: ClientCertificate[];
|
||||
|
|
@ -62,6 +62,10 @@ export class APIRequest implements api.APIRequest {
|
|||
}
|
||||
|
||||
async newContext(options: NewContextOptions = {}): Promise<APIRequestContext> {
|
||||
return await this._newContext(options, this._playwright._channel);
|
||||
}
|
||||
|
||||
async _newContext(options: NewContextOptions = {}, channel: channels.PlaywrightChannel | channels.LocalUtilsChannel): Promise<APIRequestContext> {
|
||||
options = {
|
||||
...this._playwright._defaultContextOptions,
|
||||
timeout: this._playwright._defaultContextTimeout,
|
||||
|
|
@ -70,7 +74,7 @@ export class APIRequest implements api.APIRequest {
|
|||
const storageState = typeof options.storageState === 'string' ?
|
||||
JSON.parse(await fs.promises.readFile(options.storageState, 'utf8')) :
|
||||
options.storageState;
|
||||
const context = APIRequestContext.from((await this._playwright._channel.newRequest({
|
||||
const context = APIRequestContext.from((await channel.newRequest({
|
||||
...options,
|
||||
extraHTTPHeaders: options.extraHTTPHeaders ? headersObjectToArray(options.extraHTTPHeaders) : undefined,
|
||||
storageState,
|
||||
|
|
|
|||
79
packages/playwright-core/src/client/mockingProxy.ts
Normal file
79
packages/playwright-core/src/client/mockingProxy.ts
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import * as network from './network';
|
||||
import type * as channels from '@protocol/channels';
|
||||
import { ChannelOwner } from './channelOwner';
|
||||
import type { APIRequestContext } from './fetch';
|
||||
import { assert } from '../utils';
|
||||
import type { Page } from './page';
|
||||
import type { Playwright } from './playwright';
|
||||
|
||||
export class MockingProxy extends ChannelOwner<channels.MockingProxyChannel> {
|
||||
_requestContext!: APIRequestContext;
|
||||
_playwright!: Playwright;
|
||||
|
||||
constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.MockingProxyInitializer) {
|
||||
super(parent, type, guid, initializer);
|
||||
|
||||
this._channel.on('route', async (params: channels.MockingProxyRouteEvent) => {
|
||||
const route = network.Route.from(params.route);
|
||||
route._apiRequestContext = this._requestContext;
|
||||
const page = route.request()._pageForMockingProxy!;
|
||||
await page._onRoute(route);
|
||||
});
|
||||
|
||||
this._channel.on('request', async (params: channels.MockingProxyRequestEvent) => {
|
||||
const page = this.findPage(params.correlation);
|
||||
assert(page);
|
||||
const request = network.Request.from(params.request);
|
||||
request._pageForMockingProxy = page;
|
||||
page.context()._onRequest(request, page);
|
||||
});
|
||||
|
||||
this._channel.on('requestFailed', async (params: channels.MockingProxyRequestFailedEvent) => {
|
||||
const request = network.Request.from(params.request);
|
||||
const page = request._pageForMockingProxy!;
|
||||
page.context()._onRequestFailed(request, params.responseEndTiming, params.failureText, page);
|
||||
});
|
||||
|
||||
this._channel.on('requestFinished', async (params: channels.MockingProxyRequestFinishedEvent) => {
|
||||
const { responseEndTiming } = params;
|
||||
const request = network.Request.from(params.request);
|
||||
const response = network.Response.fromNullable(params.response);
|
||||
const page = request._pageForMockingProxy!;
|
||||
page.context()._onRequestFinished(request, response, page, responseEndTiming);
|
||||
});
|
||||
|
||||
this._channel.on('response', async (params: channels.MockingProxyResponseEvent) => {
|
||||
const response = network.Response.from(params.response);
|
||||
const page = response.request()._pageForMockingProxy!;
|
||||
page.context()._onResponse(response, page);
|
||||
});
|
||||
}
|
||||
|
||||
static from(channel: channels.MockingProxyChannel): MockingProxy {
|
||||
return (channel as any)._object;
|
||||
}
|
||||
|
||||
findPage(correlation: string): Page | undefined {
|
||||
const guid = `page@${correlation}`;
|
||||
return this._playwright._allPages().find(page => page._guid === guid);
|
||||
}
|
||||
|
||||
baseURL() {
|
||||
return this._initializer.baseURL;
|
||||
}
|
||||
}
|
||||
|
|
@ -30,9 +30,9 @@ import type { Page } from './page';
|
|||
import { Waiter } from './waiter';
|
||||
import type * as api from '../../types/types';
|
||||
import type { HeadersArray } from '../common/types';
|
||||
import type { APIRequestContext } from './fetch';
|
||||
import { APIResponse } from './fetch';
|
||||
import type { Serializable } from '../../types/structs';
|
||||
import type { BrowserContext } from './browserContext';
|
||||
import { isTargetClosedError } from './errors';
|
||||
|
||||
export type NetworkCookie = {
|
||||
|
|
@ -86,6 +86,7 @@ export class Request extends ChannelOwner<channels.RequestChannel> implements ap
|
|||
private _actualHeadersPromise: Promise<RawHeaders> | undefined;
|
||||
_timing: ResourceTiming;
|
||||
private _fallbackOverrides: SerializedFallbackOverrides = {};
|
||||
_pageForMockingProxy: Page | null = null;
|
||||
|
||||
static from(request: channels.RequestChannel): Request {
|
||||
return (request as any)._object;
|
||||
|
|
@ -200,6 +201,8 @@ export class Request extends ChannelOwner<channels.RequestChannel> implements ap
|
|||
}
|
||||
|
||||
frame(): Frame {
|
||||
if (this._pageForMockingProxy)
|
||||
throw new Error('Frame for this request is not available, because the request was issued on the server.');
|
||||
if (!this._initializer.frame) {
|
||||
assert(this.serviceWorker());
|
||||
throw new Error('Service Worker requests do not have an associated frame.');
|
||||
|
|
@ -216,7 +219,7 @@ export class Request extends ChannelOwner<channels.RequestChannel> implements ap
|
|||
}
|
||||
|
||||
_safePage(): Page | null {
|
||||
return Frame.fromNullable(this._initializer.frame)?._page || null;
|
||||
return Frame.fromNullable(this._initializer.frame)?._page ?? null;
|
||||
}
|
||||
|
||||
serviceWorker(): Worker | null {
|
||||
|
|
@ -291,7 +294,7 @@ export class Request extends ChannelOwner<channels.RequestChannel> implements ap
|
|||
|
||||
export class Route extends ChannelOwner<channels.RouteChannel> implements api.Route {
|
||||
private _handlingPromise: ManualPromise<boolean> | null = null;
|
||||
_context!: BrowserContext;
|
||||
_apiRequestContext!: APIRequestContext;
|
||||
_didThrow: boolean = false;
|
||||
|
||||
static from(route: channels.RouteChannel): Route {
|
||||
|
|
@ -339,7 +342,7 @@ export class Route extends ChannelOwner<channels.RouteChannel> implements api.Ro
|
|||
|
||||
async fetch(options: FallbackOverrides & { maxRedirects?: number, maxRetries?: number, timeout?: number } = {}): Promise<APIResponse> {
|
||||
return await this._wrapApiCall(async () => {
|
||||
return await this._context.request._innerFetch({ request: this.request(), data: options.postData, ...options });
|
||||
return await this._apiRequestContext._innerFetch({ request: this.request(), data: options.postData, ...options });
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -138,7 +138,11 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
|
|||
this._channel.on('frameAttached', ({ frame }) => this._onFrameAttached(Frame.from(frame)));
|
||||
this._channel.on('frameDetached', ({ frame }) => this._onFrameDetached(Frame.from(frame)));
|
||||
this._channel.on('locatorHandlerTriggered', ({ uid }) => this._onLocatorHandlerTriggered(uid));
|
||||
this._channel.on('route', ({ route }) => this._onRoute(Route.from(route)));
|
||||
this._channel.on('route', params => {
|
||||
const route = Route.from(params.route);
|
||||
route._apiRequestContext = this.context().request;
|
||||
this._onRoute(route);
|
||||
});
|
||||
this._channel.on('webSocketRoute', ({ webSocketRoute }) => this._onWebSocketRoute(WebSocketRoute.from(webSocketRoute)));
|
||||
this._channel.on('video', ({ artifact }) => {
|
||||
const artifactObject = Artifact.from(artifact);
|
||||
|
|
@ -179,8 +183,7 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
|
|||
this.emit(Events.Page.FrameDetached, frame);
|
||||
}
|
||||
|
||||
private async _onRoute(route: Route) {
|
||||
route._context = this.context();
|
||||
async _onRoute(route: Route) {
|
||||
const routeHandlers = this._routes.slice();
|
||||
for (const routeHandler of routeHandlers) {
|
||||
// If the page was closed we stall all requests right away.
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import { ChannelOwner } from './channelOwner';
|
|||
import { Electron } from './electron';
|
||||
import { APIRequest } from './fetch';
|
||||
import { Selectors, SelectorsOwner } from './selectors';
|
||||
import { MockingProxy } from './mockingProxy';
|
||||
import type { BrowserContextOptions, LaunchOptions } from 'playwright-core';
|
||||
|
||||
export class Playwright extends ChannelOwner<channels.PlaywrightChannel> {
|
||||
|
|
@ -36,6 +37,7 @@ export class Playwright extends ChannelOwner<channels.PlaywrightChannel> {
|
|||
selectors: Selectors;
|
||||
readonly request: APIRequest;
|
||||
readonly errors: { TimeoutError: typeof TimeoutError };
|
||||
_mockingProxy?: MockingProxy;
|
||||
|
||||
// Instrumentation.
|
||||
_defaultLaunchOptions?: LaunchOptions;
|
||||
|
|
@ -92,4 +94,14 @@ export class Playwright extends ChannelOwner<channels.PlaywrightChannel> {
|
|||
_allPages() {
|
||||
return this._allContexts().flatMap(context => context.pages());
|
||||
}
|
||||
|
||||
|
||||
async _startMockingProxy() {
|
||||
const requestContext = await this.request._newContext(undefined, this._connection.localUtils()._channel);
|
||||
const result = await this._connection.localUtils()._channel.newMockingProxy({ requestContext: requestContext._channel });
|
||||
this._mockingProxy = MockingProxy.from(result.mockingProxy);
|
||||
this._mockingProxy._requestContext = requestContext;
|
||||
this._mockingProxy._playwright = this;
|
||||
return this._mockingProxy;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ export const slowMoActions = new Set([
|
|||
|
||||
export const commandsWithTracingSnapshots = new Set([
|
||||
'EventTarget.waitForEventInfo',
|
||||
'MockingProxy.waitForEventInfo',
|
||||
'BrowserContext.waitForEventInfo',
|
||||
'Page.waitForEventInfo',
|
||||
'WebSocket.waitForEventInfo',
|
||||
|
|
|
|||
|
|
@ -257,6 +257,29 @@ scheme.APIResponse = tObject({
|
|||
headers: tArray(tType('NameValue')),
|
||||
});
|
||||
scheme.LifecycleEvent = tEnum(['load', 'domcontentloaded', 'networkidle', 'commit']);
|
||||
scheme.EventTargetInitializer = tOptional(tObject({}));
|
||||
scheme.EventTargetWaitForEventInfoParams = tObject({
|
||||
info: tObject({
|
||||
waitId: tString,
|
||||
phase: tEnum(['before', 'after', 'log']),
|
||||
event: tOptional(tString),
|
||||
message: tOptional(tString),
|
||||
error: tOptional(tString),
|
||||
}),
|
||||
});
|
||||
scheme.MockingProxyWaitForEventInfoParams = tType('EventTargetWaitForEventInfoParams');
|
||||
scheme.BrowserContextWaitForEventInfoParams = tType('EventTargetWaitForEventInfoParams');
|
||||
scheme.PageWaitForEventInfoParams = tType('EventTargetWaitForEventInfoParams');
|
||||
scheme.WebSocketWaitForEventInfoParams = tType('EventTargetWaitForEventInfoParams');
|
||||
scheme.ElectronApplicationWaitForEventInfoParams = tType('EventTargetWaitForEventInfoParams');
|
||||
scheme.AndroidDeviceWaitForEventInfoParams = tType('EventTargetWaitForEventInfoParams');
|
||||
scheme.EventTargetWaitForEventInfoResult = tOptional(tObject({}));
|
||||
scheme.MockingProxyWaitForEventInfoResult = tType('EventTargetWaitForEventInfoResult');
|
||||
scheme.BrowserContextWaitForEventInfoResult = tType('EventTargetWaitForEventInfoResult');
|
||||
scheme.PageWaitForEventInfoResult = tType('EventTargetWaitForEventInfoResult');
|
||||
scheme.WebSocketWaitForEventInfoResult = tType('EventTargetWaitForEventInfoResult');
|
||||
scheme.ElectronApplicationWaitForEventInfoResult = tType('EventTargetWaitForEventInfoResult');
|
||||
scheme.AndroidDeviceWaitForEventInfoResult = tType('EventTargetWaitForEventInfoResult');
|
||||
scheme.LocalUtilsInitializer = tObject({
|
||||
deviceDescriptors: tArray(tObject({
|
||||
name: tString,
|
||||
|
|
@ -344,6 +367,69 @@ scheme.LocalUtilsTraceDiscardedParams = tObject({
|
|||
stacksId: tString,
|
||||
});
|
||||
scheme.LocalUtilsTraceDiscardedResult = tOptional(tObject({}));
|
||||
scheme.LocalUtilsNewRequestParams = tObject({
|
||||
baseURL: tOptional(tString),
|
||||
userAgent: tOptional(tString),
|
||||
ignoreHTTPSErrors: tOptional(tBoolean),
|
||||
extraHTTPHeaders: tOptional(tArray(tType('NameValue'))),
|
||||
clientCertificates: tOptional(tArray(tObject({
|
||||
origin: tString,
|
||||
cert: tOptional(tBinary),
|
||||
key: tOptional(tBinary),
|
||||
passphrase: tOptional(tString),
|
||||
pfx: tOptional(tBinary),
|
||||
}))),
|
||||
httpCredentials: tOptional(tObject({
|
||||
username: tString,
|
||||
password: tString,
|
||||
origin: tOptional(tString),
|
||||
send: tOptional(tEnum(['always', 'unauthorized'])),
|
||||
})),
|
||||
proxy: tOptional(tObject({
|
||||
server: tString,
|
||||
bypass: tOptional(tString),
|
||||
username: tOptional(tString),
|
||||
password: tOptional(tString),
|
||||
})),
|
||||
timeout: tOptional(tNumber),
|
||||
storageState: tOptional(tObject({
|
||||
cookies: tOptional(tArray(tType('NetworkCookie'))),
|
||||
origins: tOptional(tArray(tType('SetOriginStorage'))),
|
||||
})),
|
||||
tracesDir: tOptional(tString),
|
||||
});
|
||||
scheme.LocalUtilsNewRequestResult = tObject({
|
||||
request: tChannel(['APIRequestContext']),
|
||||
});
|
||||
scheme.LocalUtilsNewMockingProxyParams = tObject({
|
||||
requestContext: tChannel(['APIRequestContext']),
|
||||
});
|
||||
scheme.LocalUtilsNewMockingProxyResult = tObject({
|
||||
mockingProxy: tChannel(['MockingProxy']),
|
||||
});
|
||||
scheme.MockingProxyInitializer = tObject({
|
||||
baseURL: tString,
|
||||
});
|
||||
scheme.MockingProxyRouteEvent = tObject({
|
||||
route: tChannel(['Route']),
|
||||
});
|
||||
scheme.MockingProxyRequestEvent = tObject({
|
||||
request: tChannel(['Request']),
|
||||
correlation: tString,
|
||||
});
|
||||
scheme.MockingProxyRequestFailedEvent = tObject({
|
||||
request: tChannel(['Request']),
|
||||
failureText: tOptional(tString),
|
||||
responseEndTiming: tNumber,
|
||||
});
|
||||
scheme.MockingProxyRequestFinishedEvent = tObject({
|
||||
request: tChannel(['Request']),
|
||||
response: tOptional(tChannel(['Response'])),
|
||||
responseEndTiming: tNumber,
|
||||
});
|
||||
scheme.MockingProxyResponseEvent = tObject({
|
||||
response: tChannel(['Response']),
|
||||
});
|
||||
scheme.RootInitializer = tOptional(tObject({}));
|
||||
scheme.RootInitializeParams = tObject({
|
||||
sdkLanguage: tEnum(['javascript', 'python', 'java', 'csharp']),
|
||||
|
|
@ -722,6 +808,7 @@ scheme.BrowserNewContextParams = tObject({
|
|||
cookies: tOptional(tArray(tType('SetNetworkCookie'))),
|
||||
origins: tOptional(tArray(tType('SetOriginStorage'))),
|
||||
})),
|
||||
mockingProxyBaseURL: tOptional(tString),
|
||||
});
|
||||
scheme.BrowserNewContextResult = tObject({
|
||||
context: tChannel(['BrowserContext']),
|
||||
|
|
@ -814,27 +901,6 @@ scheme.BrowserStopTracingParams = tOptional(tObject({}));
|
|||
scheme.BrowserStopTracingResult = tObject({
|
||||
artifact: tChannel(['Artifact']),
|
||||
});
|
||||
scheme.EventTargetInitializer = tOptional(tObject({}));
|
||||
scheme.EventTargetWaitForEventInfoParams = tObject({
|
||||
info: tObject({
|
||||
waitId: tString,
|
||||
phase: tEnum(['before', 'after', 'log']),
|
||||
event: tOptional(tString),
|
||||
message: tOptional(tString),
|
||||
error: tOptional(tString),
|
||||
}),
|
||||
});
|
||||
scheme.BrowserContextWaitForEventInfoParams = tType('EventTargetWaitForEventInfoParams');
|
||||
scheme.PageWaitForEventInfoParams = tType('EventTargetWaitForEventInfoParams');
|
||||
scheme.WebSocketWaitForEventInfoParams = tType('EventTargetWaitForEventInfoParams');
|
||||
scheme.ElectronApplicationWaitForEventInfoParams = tType('EventTargetWaitForEventInfoParams');
|
||||
scheme.AndroidDeviceWaitForEventInfoParams = tType('EventTargetWaitForEventInfoParams');
|
||||
scheme.EventTargetWaitForEventInfoResult = tOptional(tObject({}));
|
||||
scheme.BrowserContextWaitForEventInfoResult = tType('EventTargetWaitForEventInfoResult');
|
||||
scheme.PageWaitForEventInfoResult = tType('EventTargetWaitForEventInfoResult');
|
||||
scheme.WebSocketWaitForEventInfoResult = tType('EventTargetWaitForEventInfoResult');
|
||||
scheme.ElectronApplicationWaitForEventInfoResult = tType('EventTargetWaitForEventInfoResult');
|
||||
scheme.AndroidDeviceWaitForEventInfoResult = tType('EventTargetWaitForEventInfoResult');
|
||||
scheme.BrowserContextInitializer = tObject({
|
||||
isChromium: tBoolean,
|
||||
requestContext: tChannel(['APIRequestContext']),
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ import { RecorderApp } from './recorder/recorderApp';
|
|||
import * as storageScript from './storageScript';
|
||||
import * as utilityScriptSerializers from './isomorphic/utilityScriptSerializers';
|
||||
|
||||
export abstract class BrowserContext extends SdkObject {
|
||||
export abstract class BrowserContext extends SdkObject implements network.RequestContext {
|
||||
static Events = {
|
||||
Console: 'console',
|
||||
Close: 'close',
|
||||
|
|
|
|||
|
|
@ -41,8 +41,12 @@ import type { Playwright } from '../playwright';
|
|||
import { SdkObject } from '../../server/instrumentation';
|
||||
import { serializeClientSideCallMetadata } from '../../utils';
|
||||
import { deviceDescriptors as descriptors } from '../deviceDescriptors';
|
||||
import { MockingProxy } from '../mockingProxy';
|
||||
import { MockingProxyDispatcher } from './mockingProxyDispatcher';
|
||||
import { APIRequestContextDispatcher } from './networkDispatchers';
|
||||
import { GlobalAPIRequestContext } from '../fetch';
|
||||
|
||||
export class LocalUtilsDispatcher extends Dispatcher<{ guid: string }, channels.LocalUtilsChannel, RootDispatcher> implements channels.LocalUtilsChannel {
|
||||
export class LocalUtilsDispatcher extends Dispatcher<SdkObject, channels.LocalUtilsChannel, RootDispatcher> implements channels.LocalUtilsChannel {
|
||||
_type_LocalUtils: boolean;
|
||||
private _harBackends = new Map<string, HarBackend>();
|
||||
private _stackSessions = new Map<string, {
|
||||
|
|
@ -51,6 +55,7 @@ export class LocalUtilsDispatcher extends Dispatcher<{ guid: string }, channels.
|
|||
tmpDir: string | undefined,
|
||||
callStacks: channels.ClientSideCallMetadata[]
|
||||
}>();
|
||||
private _playwright: Playwright;
|
||||
|
||||
constructor(scope: RootDispatcher, playwright: Playwright) {
|
||||
const localUtils = new SdkObject(playwright, 'localUtils', 'localUtils');
|
||||
|
|
@ -59,6 +64,7 @@ export class LocalUtilsDispatcher extends Dispatcher<{ guid: string }, channels.
|
|||
super(scope, localUtils, 'LocalUtils', {
|
||||
deviceDescriptors,
|
||||
});
|
||||
this._playwright = playwright;
|
||||
this._type_LocalUtils = true;
|
||||
}
|
||||
|
||||
|
|
@ -273,6 +279,18 @@ export class LocalUtilsDispatcher extends Dispatcher<{ guid: string }, channels.
|
|||
await removeFolders([session.tmpDir]);
|
||||
this._stackSessions.delete(stacksId!);
|
||||
}
|
||||
|
||||
async newRequest(params: channels.LocalUtilsNewRequestParams, metadata?: CallMetadata): Promise<channels.LocalUtilsNewRequestResult> {
|
||||
const requestContext = new GlobalAPIRequestContext(this._playwright, params);
|
||||
return { request: APIRequestContextDispatcher.from(this.parentScope(), requestContext) };
|
||||
}
|
||||
|
||||
async newMockingProxy(params: channels.LocalUtilsNewMockingProxyParams, metadata?: CallMetadata): Promise<channels.LocalUtilsNewMockingProxyResult> {
|
||||
const requestContext = (params.requestContext as APIRequestContextDispatcher)._object;
|
||||
const mockingProxy = new MockingProxy(this._object, requestContext);
|
||||
await mockingProxy.start();
|
||||
return { mockingProxy: new MockingProxyDispatcher(this, mockingProxy) };
|
||||
}
|
||||
}
|
||||
|
||||
const redirectStatus = [301, 302, 303, 307, 308];
|
||||
|
|
|
|||
|
|
@ -0,0 +1,58 @@
|
|||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { MockingProxy } from '../mockingProxy';
|
||||
import { Dispatcher } from './dispatcher';
|
||||
import type * as channels from '@protocol/channels';
|
||||
import { RequestDispatcher, ResponseDispatcher, RouteDispatcher } from './networkDispatchers';
|
||||
import type { Request } from '../network';
|
||||
import type { LocalUtilsDispatcher } from './localUtilsDispatcher';
|
||||
|
||||
export class MockingProxyDispatcher extends Dispatcher<MockingProxy, channels.MockingProxyChannel, LocalUtilsDispatcher> implements channels.MockingProxyChannel {
|
||||
_type_MockingProxy = true;
|
||||
_type_EventTarget = true;
|
||||
|
||||
constructor(scope: LocalUtilsDispatcher, mockingProxy: MockingProxy) {
|
||||
super(scope, mockingProxy, 'MockingProxy', {
|
||||
baseURL: mockingProxy.baseURL(),
|
||||
});
|
||||
|
||||
mockingProxy.onRoute = async route => {
|
||||
const requestDispatcher = RequestDispatcher.from(this, route.request());
|
||||
this._dispatchEvent('route', { route: RouteDispatcher.from(requestDispatcher, route) });
|
||||
};
|
||||
this.addObjectListener(MockingProxy.Events.Request, ({ request, correlation }: { request: Request, correlation: string }) => {
|
||||
this._dispatchEvent('request', { request: RequestDispatcher.from(this, request), correlation });
|
||||
});
|
||||
this.addObjectListener(MockingProxy.Events.RequestFailed, (request: Request) => {
|
||||
this._dispatchEvent('requestFailed', {
|
||||
request: RequestDispatcher.from(this, request),
|
||||
failureText: request._failureText ?? undefined,
|
||||
responseEndTiming: request._responseEndTiming,
|
||||
});
|
||||
});
|
||||
this.addObjectListener(MockingProxy.Events.RequestFinished, (request: Request) => {
|
||||
this._dispatchEvent('requestFinished', {
|
||||
request: RequestDispatcher.from(this, request),
|
||||
response: ResponseDispatcher.fromNullable(this, request._existingResponse()),
|
||||
responseEndTiming: request._responseEndTiming,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
override _onDispose(): void {
|
||||
this._object.stop();
|
||||
}
|
||||
}
|
||||
|
|
@ -26,30 +26,33 @@ import type { BrowserContextDispatcher } from './browserContextDispatcher';
|
|||
import type { PageDispatcher } from './pageDispatcher';
|
||||
import { FrameDispatcher } from './frameDispatcher';
|
||||
import { WorkerDispatcher } from './pageDispatcher';
|
||||
import type { MockingProxyDispatcher } from './mockingProxyDispatcher';
|
||||
|
||||
export class RequestDispatcher extends Dispatcher<Request, channels.RequestChannel, BrowserContextDispatcher | PageDispatcher | FrameDispatcher> implements channels.RequestChannel {
|
||||
type NetworkScope = BrowserContextDispatcher | MockingProxyDispatcher;
|
||||
|
||||
export class RequestDispatcher extends Dispatcher<Request, channels.RequestChannel, NetworkScope | PageDispatcher | FrameDispatcher> implements channels.RequestChannel {
|
||||
_type_Request: boolean;
|
||||
private _browserContextDispatcher: BrowserContextDispatcher;
|
||||
private _networkScope: NetworkScope;
|
||||
|
||||
static from(scope: BrowserContextDispatcher, request: Request): RequestDispatcher {
|
||||
static from(scope: NetworkScope, request: Request): RequestDispatcher {
|
||||
const result = existingDispatcher<RequestDispatcher>(request);
|
||||
return result || new RequestDispatcher(scope, request);
|
||||
}
|
||||
|
||||
static fromNullable(scope: BrowserContextDispatcher, request: Request | null): RequestDispatcher | undefined {
|
||||
static fromNullable(scope: NetworkScope, request: Request | null): RequestDispatcher | undefined {
|
||||
return request ? RequestDispatcher.from(scope, request) : undefined;
|
||||
}
|
||||
|
||||
private constructor(scope: BrowserContextDispatcher, request: Request) {
|
||||
private constructor(scope: NetworkScope, request: Request) {
|
||||
const postData = request.postDataBuffer();
|
||||
// Always try to attach request to the page, if not, frame.
|
||||
const frame = request.frame();
|
||||
const page = request.frame()?._page;
|
||||
const pageDispatcher = page ? existingDispatcher<PageDispatcher>(page) : null;
|
||||
const frameDispatcher = frame ? FrameDispatcher.from(scope, frame) : null;
|
||||
const frameDispatcher = frame ? FrameDispatcher.from(scope as BrowserContextDispatcher, frame) : null;
|
||||
super(pageDispatcher || frameDispatcher || scope, request, 'Request', {
|
||||
frame: FrameDispatcher.fromNullable(scope, request.frame()),
|
||||
serviceWorker: WorkerDispatcher.fromNullable(scope, request.serviceWorker()),
|
||||
frame: FrameDispatcher.fromNullable(scope as BrowserContextDispatcher, request.frame()),
|
||||
serviceWorker: WorkerDispatcher.fromNullable(scope as BrowserContextDispatcher, request.serviceWorker()),
|
||||
url: request.url(),
|
||||
resourceType: request.resourceType(),
|
||||
method: request.method(),
|
||||
|
|
@ -59,7 +62,7 @@ export class RequestDispatcher extends Dispatcher<Request, channels.RequestChann
|
|||
redirectedFrom: RequestDispatcher.fromNullable(scope, request.redirectedFrom()),
|
||||
});
|
||||
this._type_Request = true;
|
||||
this._browserContextDispatcher = scope;
|
||||
this._networkScope = scope;
|
||||
}
|
||||
|
||||
async rawRequestHeaders(params?: channels.RequestRawRequestHeadersParams): Promise<channels.RequestRawRequestHeadersResult> {
|
||||
|
|
@ -67,20 +70,20 @@ export class RequestDispatcher extends Dispatcher<Request, channels.RequestChann
|
|||
}
|
||||
|
||||
async response(): Promise<channels.RequestResponseResult> {
|
||||
return { response: ResponseDispatcher.fromNullable(this._browserContextDispatcher, await this._object.response()) };
|
||||
return { response: ResponseDispatcher.fromNullable(this._networkScope, await this._object.response()) };
|
||||
}
|
||||
}
|
||||
|
||||
export class ResponseDispatcher extends Dispatcher<Response, channels.ResponseChannel, RequestDispatcher> implements channels.ResponseChannel {
|
||||
_type_Response = true;
|
||||
|
||||
static from(scope: BrowserContextDispatcher, response: Response): ResponseDispatcher {
|
||||
static from(scope: NetworkScope, response: Response): ResponseDispatcher {
|
||||
const result = existingDispatcher<ResponseDispatcher>(response);
|
||||
const requestDispatcher = RequestDispatcher.from(scope, response.request());
|
||||
return result || new ResponseDispatcher(requestDispatcher, response);
|
||||
}
|
||||
|
||||
static fromNullable(scope: BrowserContextDispatcher, response: Response | null): ResponseDispatcher | undefined {
|
||||
static fromNullable(scope: NetworkScope, response: Response | null): ResponseDispatcher | undefined {
|
||||
return response ? ResponseDispatcher.from(scope, response) : undefined;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -330,7 +330,7 @@ export class FFPage implements PageDelegate {
|
|||
}
|
||||
|
||||
async updateExtraHTTPHeaders(): Promise<void> {
|
||||
await this._session.send('Network.setExtraHTTPHeaders', { headers: this._page.extraHTTPHeaders() || [] });
|
||||
await this._session.send('Network.setExtraHTTPHeaders', { headers: this._page.extraHTTPHeaders() });
|
||||
}
|
||||
|
||||
async updateEmulatedViewportSize(): Promise<void> {
|
||||
|
|
|
|||
|
|
@ -654,7 +654,7 @@ export class Frame extends SdkObject {
|
|||
private async _gotoAction(progress: Progress, url: string, options: types.GotoOptions): Promise<network.Response | null> {
|
||||
const waitUntil = verifyLifecycle('waitUntil', options.waitUntil === undefined ? 'load' : options.waitUntil);
|
||||
progress.log(`navigating to "${url}", waiting until "${waitUntil}"`);
|
||||
const headers = this._page.extraHTTPHeaders() || [];
|
||||
const headers = this._page.extraHTTPHeaders();
|
||||
const refererHeader = headers.find(h => h.name.toLowerCase() === 'referer');
|
||||
let referer = refererHeader ? refererHeader.value : undefined;
|
||||
if (options.referer !== undefined) {
|
||||
|
|
|
|||
260
packages/playwright-core/src/server/mockingProxy.ts
Normal file
260
packages/playwright-core/src/server/mockingProxy.ts
Normal file
|
|
@ -0,0 +1,260 @@
|
|||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import http from 'http';
|
||||
import https from 'https';
|
||||
import url from 'url';
|
||||
import type { APIRequestContext } from './fetch';
|
||||
import { SdkObject } from './instrumentation';
|
||||
import type { RequestContext, ResourceTiming, SecurityDetails } from './network';
|
||||
import { Request, Response, Route } from './network';
|
||||
import type { HeadersArray, } from './types';
|
||||
import { HttpServer, ManualPromise, monotonicTime } from '../utils';
|
||||
import { TLSSocket } from 'tls';
|
||||
import type { AddressInfo } from 'net';
|
||||
import { pipeline } from 'stream/promises';
|
||||
import { Transform } from 'stream';
|
||||
|
||||
export class MockingProxy extends SdkObject implements RequestContext {
|
||||
static Events = {
|
||||
Request: 'request',
|
||||
Response: 'response',
|
||||
RequestFailed: 'requestfailed',
|
||||
RequestFinished: 'requestfinished',
|
||||
};
|
||||
|
||||
fetchRequest: APIRequestContext;
|
||||
private _httpServer = new WorkerHttpServer();
|
||||
onRoute = (route: Route) => route.continue({ isFallback: true });
|
||||
|
||||
constructor(parent: SdkObject, requestContext: APIRequestContext) {
|
||||
super(parent, 'MockingProxy');
|
||||
this.fetchRequest = requestContext;
|
||||
|
||||
this._httpServer.routePrefix('/', (req, res) => {
|
||||
this._proxy(req, res);
|
||||
return true;
|
||||
});
|
||||
this._httpServer.server().on('connect', (req, socket, head) => {
|
||||
// TODO: improve error message
|
||||
socket.end('HTTP/1.1 405 Method Not Allowed\r\n\r\n');
|
||||
});
|
||||
}
|
||||
|
||||
async start(): Promise<void> {
|
||||
await this._httpServer.start();
|
||||
}
|
||||
|
||||
async stop() {
|
||||
await this._httpServer.stop();
|
||||
}
|
||||
|
||||
port() {
|
||||
return this._httpServer.port();
|
||||
}
|
||||
|
||||
baseURL() {
|
||||
return `http://localhost:${this.port()}/`;
|
||||
}
|
||||
|
||||
private async _proxy(req: http.IncomingMessage, res: http.ServerResponse) {
|
||||
if (req.url?.startsWith('/'))
|
||||
req.url = req.url.substring(1);
|
||||
|
||||
if (!req.url?.startsWith('pw_meta:')) {
|
||||
res.statusCode = 400;
|
||||
res.end('Playwright mocking proxy received invalid URL, must start with "pw_meta:"');
|
||||
return;
|
||||
}
|
||||
|
||||
const correlation = req.url.substring('pw_meta:'.length, req.url.indexOf('/'));
|
||||
req.url = req.url.substring(req.url.indexOf('/') + 1);
|
||||
|
||||
// Java URL likes removing double slashes from the pathname.
|
||||
if (req.url?.startsWith('http:/') && !req.url?.startsWith('http://'))
|
||||
req.url = req.url.replace('http:/', 'http://');
|
||||
if (req.url?.startsWith('https:/') && !req.url?.startsWith('https://'))
|
||||
req.url = req.url.replace('https:/', 'https://');
|
||||
|
||||
delete req.headersDistinct.host;
|
||||
const headers = headersArray(req);
|
||||
const body = await collectBody(req);
|
||||
const request = new Request(this, null, null, null, undefined, req.url!, '', req.method!, body, headers);
|
||||
request.setRawRequestHeaders(headers);
|
||||
this.emit(MockingProxy.Events.Request, { request, correlation });
|
||||
|
||||
const route = new Route(request, {
|
||||
abort: async errorCode => {
|
||||
req.destroy(errorCode ? new Error(errorCode) : undefined);
|
||||
},
|
||||
continue: async overrides => {
|
||||
const proxyUrl = url.parse(overrides?.url ?? req.url!);
|
||||
const httpLib = proxyUrl.protocol === 'https:' ? https : http;
|
||||
const proxyHeaders = overrides?.headers ?? headers;
|
||||
const proxyMethod = overrides?.method ?? req.method;
|
||||
const proxyBody = overrides?.postData ?? body;
|
||||
|
||||
const startAt = monotonicTime();
|
||||
let connectEnd: number | undefined;
|
||||
let connectStart: number | undefined;
|
||||
let dnsLookupAt: number | undefined;
|
||||
let tlsHandshakeAt: number | undefined;
|
||||
let socketBytesReadStart = 0;
|
||||
|
||||
return new Promise<void>(resolve => {
|
||||
const proxyReq = httpLib.request({
|
||||
...proxyUrl,
|
||||
headers: headersArrayToOutgoingHeaders(proxyHeaders),
|
||||
method: proxyMethod,
|
||||
}, async proxyRes => {
|
||||
const responseStart = monotonicTime();
|
||||
const timings: ResourceTiming = {
|
||||
startTime: startAt / 1000,
|
||||
connectStart: connectStart ? (connectStart - startAt) : -1,
|
||||
connectEnd: connectEnd ? (connectEnd - startAt) : -1,
|
||||
domainLookupStart: -1,
|
||||
domainLookupEnd: dnsLookupAt ? (dnsLookupAt - startAt) : -1,
|
||||
requestStart: -1,
|
||||
responseStart: (responseStart - startAt),
|
||||
secureConnectionStart: tlsHandshakeAt ? (tlsHandshakeAt - startAt) : -1,
|
||||
};
|
||||
|
||||
const socket = proxyRes.socket;
|
||||
|
||||
let securityDetails: SecurityDetails | undefined;
|
||||
if (socket instanceof TLSSocket) {
|
||||
const peerCertificate = socket.getPeerCertificate();
|
||||
securityDetails = {
|
||||
protocol: socket.getProtocol() ?? undefined,
|
||||
subjectName: peerCertificate.subject.CN,
|
||||
validFrom: new Date(peerCertificate.valid_from).getTime() / 1000,
|
||||
validTo: new Date(peerCertificate.valid_to).getTime() / 1000,
|
||||
issuer: peerCertificate.issuer.CN
|
||||
};
|
||||
}
|
||||
|
||||
const address = socket.address() as AddressInfo;
|
||||
const responseBodyPromise = new ManualPromise<Buffer>();
|
||||
const response = new Response(request, proxyRes.statusCode!, proxyRes.statusMessage!, headersArray(proxyRes), timings, () => responseBodyPromise, false, proxyRes.httpVersion);
|
||||
response.setRawResponseHeaders(headersArray(proxyRes));
|
||||
response._securityDetailsFinished(securityDetails);
|
||||
response._serverAddrFinished({ ipAddress: address.family === 'IPv6' ? `[${address.address}]` : address.address, port: address.port });
|
||||
this.emit(MockingProxy.Events.Response, response);
|
||||
|
||||
try {
|
||||
res.writeHead(proxyRes.statusCode!, proxyRes.headers);
|
||||
|
||||
const chunks: Buffer[] = [];
|
||||
await pipeline(
|
||||
proxyRes,
|
||||
new Transform({
|
||||
transform(chunk, encoding, callback) {
|
||||
chunks.push(chunk);
|
||||
callback(undefined, chunk);
|
||||
},
|
||||
}),
|
||||
res
|
||||
);
|
||||
const body = Buffer.concat(chunks);
|
||||
responseBodyPromise.resolve(body);
|
||||
|
||||
const transferSize = socket.bytesRead - socketBytesReadStart;
|
||||
const encodedBodySize = body.byteLength;
|
||||
response._requestFinished(monotonicTime() - startAt);
|
||||
response.setTransferSize(transferSize);
|
||||
response.setEncodedBodySize(encodedBodySize);
|
||||
response.setResponseHeadersSize(transferSize - encodedBodySize);
|
||||
this.emit(MockingProxy.Events.RequestFinished, request);
|
||||
resolve();
|
||||
} catch (error) {
|
||||
request._setFailureText('' + error);
|
||||
this.emit(MockingProxy.Events.RequestFailed, request);
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
|
||||
proxyReq.on('error', error => {
|
||||
request._setFailureText('' + error);
|
||||
this.emit(MockingProxy.Events.RequestFailed, request);
|
||||
res.statusCode = 502;
|
||||
res.end(resolve);
|
||||
});
|
||||
proxyReq.once('socket', socket => {
|
||||
if (proxyReq.reusedSocket)
|
||||
return;
|
||||
|
||||
socketBytesReadStart = socket.bytesRead;
|
||||
|
||||
socket.once('lookup', () => { dnsLookupAt = monotonicTime(); });
|
||||
socket.once('connectionAttempt', () => { connectStart = monotonicTime(); });
|
||||
socket.once('connect', () => { connectEnd = monotonicTime(); });
|
||||
socket.once('secureConnect', () => { tlsHandshakeAt = monotonicTime(); });
|
||||
});
|
||||
proxyReq.end(proxyBody);
|
||||
});
|
||||
},
|
||||
fulfill: async ({ status, headers, body, isBase64 }) => {
|
||||
res.statusCode = status;
|
||||
for (const { name, value } of headers)
|
||||
res.appendHeader(name, value);
|
||||
res.sendDate = false;
|
||||
res.end(Buffer.from(body, isBase64 ? 'base64' : 'utf-8'));
|
||||
},
|
||||
});
|
||||
|
||||
await this.onRoute(route);
|
||||
}
|
||||
|
||||
addRouteInFlight(route: Route): void {
|
||||
// no-op, might be useful for warnings
|
||||
}
|
||||
|
||||
removeRouteInFlight(route: Route): void {
|
||||
// no-op, might be useful for warnings
|
||||
}
|
||||
}
|
||||
|
||||
function headersArray(req: Pick<http.IncomingMessage, 'headersDistinct'>): HeadersArray {
|
||||
return Object.entries(req.headersDistinct).flatMap(([name, values = []]) => values.map(value => ({ name, value })));
|
||||
}
|
||||
|
||||
function headersArrayToOutgoingHeaders(headers: HeadersArray) {
|
||||
const result: http.OutgoingHttpHeaders = {};
|
||||
for (const { name, value } of headers) {
|
||||
if (result[name] === undefined)
|
||||
result[name] = value;
|
||||
else if (Array.isArray(result[name]))
|
||||
result[name].push(value);
|
||||
else
|
||||
result[name] = [result[name] as string, value];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async function collectBody(req: http.IncomingMessage) {
|
||||
return await new Promise<Buffer>((resolve, reject) => {
|
||||
const chunks: Buffer[] = [];
|
||||
req.on('data', chunk => chunks.push(chunk));
|
||||
req.on('end', () => resolve(Buffer.concat(chunks)));
|
||||
req.on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
class WorkerHttpServer extends HttpServer {
|
||||
override handleCORS(request: http.IncomingMessage, response: http.ServerResponse): boolean {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -14,7 +14,6 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type * as contexts from './browserContext';
|
||||
import type * as pages from './page';
|
||||
import type * as frames from './frames';
|
||||
import type * as types from './types';
|
||||
|
|
@ -88,6 +87,13 @@ export function stripFragmentFromUrl(url: string): string {
|
|||
return url.substring(0, url.indexOf('#'));
|
||||
}
|
||||
|
||||
export interface RequestContext extends SdkObject {
|
||||
readonly fetchRequest: APIRequestContext;
|
||||
|
||||
addRouteInFlight(route: Route): void;
|
||||
removeRouteInFlight(route: Route): void;
|
||||
}
|
||||
|
||||
export class Request extends SdkObject {
|
||||
private _response: Response | null = null;
|
||||
private _redirectedFrom: Request | null;
|
||||
|
|
@ -103,14 +109,14 @@ export class Request extends SdkObject {
|
|||
private _headersMap = new Map<string, string>();
|
||||
readonly _frame: frames.Frame | null = null;
|
||||
readonly _serviceWorker: pages.Worker | null = null;
|
||||
readonly _context: contexts.BrowserContext;
|
||||
readonly _context: RequestContext;
|
||||
private _rawRequestHeadersPromise = new ManualPromise<HeadersArray>();
|
||||
private _waitForResponsePromise = new ManualPromise<Response | null>();
|
||||
_responseEndTiming = -1;
|
||||
private _overrides: NormalizedContinueOverrides | undefined;
|
||||
private _bodySize: number | undefined;
|
||||
|
||||
constructor(context: contexts.BrowserContext, frame: frames.Frame | null, serviceWorker: pages.Worker | null, redirectedFrom: Request | null, documentId: string | undefined,
|
||||
constructor(context: RequestContext, frame: frames.Frame | null, serviceWorker: pages.Worker | null, redirectedFrom: Request | null, documentId: string | undefined,
|
||||
url: string, resourceType: string, method: string, postData: Buffer | null, headers: HeadersArray) {
|
||||
super(frame || context, 'request');
|
||||
assert(!url.startsWith('data:'), 'Data urls should not fire requests');
|
||||
|
|
@ -346,7 +352,7 @@ export class Route extends SdkObject {
|
|||
|
||||
export type RouteHandler = (route: Route, request: Request) => boolean;
|
||||
|
||||
type GetResponseBodyCallback = () => Promise<Buffer>;
|
||||
export type GetResponseBodyCallback = () => Promise<Buffer>;
|
||||
|
||||
export type ResourceTiming = {
|
||||
startTime: number;
|
||||
|
|
|
|||
|
|
@ -366,8 +366,20 @@ export class Page extends SdkObject {
|
|||
return this._delegate.updateExtraHTTPHeaders();
|
||||
}
|
||||
|
||||
extraHTTPHeaders(): types.HeadersArray | undefined {
|
||||
return this._extraHTTPHeaders;
|
||||
extraHTTPHeaders(): types.HeadersArray {
|
||||
return this.instrumentationHeaders().concat(this._extraHTTPHeaders ?? []);
|
||||
}
|
||||
|
||||
instrumentationHeaders() {
|
||||
const headers: channels.NameValue[] = [];
|
||||
|
||||
const mockingProxyBaseURL = this.context()._options.mockingProxyBaseURL;
|
||||
if (mockingProxyBaseURL) {
|
||||
const correlation = this.guid.substring('Page@'.length);
|
||||
headers.push({ name: 'x-playwright-proxy', value: encodeURIComponent(mockingProxyBaseURL + `pw_meta:${correlation}/`) });
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
async _onBindingCalled(payload: string, context: dom.FrameExecutionContext) {
|
||||
|
|
|
|||
|
|
@ -213,13 +213,20 @@ export class HttpServer {
|
|||
readable.pipe(response);
|
||||
}
|
||||
|
||||
private _onRequest(request: http.IncomingMessage, response: http.ServerResponse) {
|
||||
handleCORS(request: http.IncomingMessage, response: http.ServerResponse): boolean {
|
||||
if (request.method === 'OPTIONS') {
|
||||
response.writeHead(200);
|
||||
response.end();
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private _onRequest(request: http.IncomingMessage, response: http.ServerResponse) {
|
||||
if (this.handleCORS(request, response))
|
||||
return;
|
||||
|
||||
request.on('error', () => response.end());
|
||||
try {
|
||||
if (!request.url) {
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import type { TestInfoImpl, TestStepInternal } from './worker/testInfo';
|
|||
import { rootTestType } from './common/testType';
|
||||
import type { ContextReuseMode } from './common/config';
|
||||
import type { ApiCallData, ClientInstrumentation, ClientInstrumentationListener } from '../../playwright-core/src/client/clientInstrumentation';
|
||||
import type { BrowserContext as BrowserContextImpl } from '../../playwright-core/src/client/browserContext';
|
||||
import type { Playwright as PlaywrightImpl } from '../../playwright-core/src/client/playwright';
|
||||
import { currentTestInfo } from './common/globals';
|
||||
export { expect } from './matchers/expect';
|
||||
|
|
@ -56,6 +57,7 @@ type WorkerFixtures = PlaywrightWorkerArgs & PlaywrightWorkerOptions & {
|
|||
_optionContextReuseMode: ContextReuseMode,
|
||||
_optionConnectOptions: PlaywrightWorkerOptions['connectOptions'],
|
||||
_reuseContext: boolean,
|
||||
_mockingProxy?: void,
|
||||
_pageSnapshot: PageSnapshotOption,
|
||||
};
|
||||
|
||||
|
|
@ -74,6 +76,7 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
|
|||
screenshot: ['off', { scope: 'worker', option: true }],
|
||||
video: ['off', { scope: 'worker', option: true }],
|
||||
trace: ['off', { scope: 'worker', option: true }],
|
||||
mockingProxy: [undefined, { scope: 'worker', option: true }],
|
||||
_pageSnapshot: ['off', { scope: 'worker', option: true }],
|
||||
|
||||
_browserOptions: [async ({ playwright, headless, channel, launchOptions }, use) => {
|
||||
|
|
@ -121,6 +124,12 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
|
|||
}, true);
|
||||
}, { scope: 'worker', timeout: 0 }],
|
||||
|
||||
_mockingProxy: [async ({ mockingProxy, playwright }, use) => {
|
||||
if (mockingProxy === 'inject-via-header')
|
||||
await (playwright as PlaywrightImpl)._startMockingProxy();
|
||||
await use();
|
||||
}, { scope: 'worker', box: true, auto: true }],
|
||||
|
||||
acceptDownloads: [({ contextOptions }, use) => use(contextOptions.acceptDownloads ?? true), { option: true }],
|
||||
bypassCSP: [({ contextOptions }, use) => use(contextOptions.bypassCSP ?? false), { option: true }],
|
||||
colorScheme: [({ contextOptions }, use) => use(contextOptions.colorScheme === undefined ? 'light' : contextOptions.colorScheme), { option: true }],
|
||||
|
|
@ -295,7 +304,7 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
|
|||
if (!keepTestTimeout)
|
||||
currentTestInfo()?._setDebugMode();
|
||||
},
|
||||
runAfterCreateBrowserContext: async (context: BrowserContext) => {
|
||||
runAfterCreateBrowserContext: async (context: BrowserContextImpl) => {
|
||||
await artifactsRecorder?.didCreateBrowserContext(context);
|
||||
const testInfo = currentTestInfo();
|
||||
if (testInfo)
|
||||
|
|
@ -343,7 +352,7 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
|
|||
size: typeof video === 'string' ? undefined : video.size,
|
||||
}
|
||||
} : {};
|
||||
const context = await browser.newContext({ ...videoOptions, ...options });
|
||||
const context = await browser.newContext({ ...videoOptions, ...options }) as BrowserContextImpl;
|
||||
const contextData: { pagesWithVideo: Page[] } = { pagesWithVideo: [] };
|
||||
contexts.set(context, contextData);
|
||||
if (captureVideo)
|
||||
|
|
|
|||
17
packages/playwright/types/test.d.ts
vendored
17
packages/playwright/types/test.d.ts
vendored
|
|
@ -6243,11 +6243,28 @@ export interface PlaywrightWorkerOptions {
|
|||
* Learn more about [recording video](https://playwright.dev/docs/test-use-options#recording-options).
|
||||
*/
|
||||
video: VideoMode | /** deprecated */ 'retry-with-video' | { mode: VideoMode, size?: ViewportSize };
|
||||
/**
|
||||
* **Usage**
|
||||
*
|
||||
* ```js
|
||||
* // playwright.config.ts
|
||||
* import { defineConfig } from '@playwright/test';
|
||||
*
|
||||
* export default defineConfig({
|
||||
* use: {
|
||||
* mockingProxy: 'inject-via-header'
|
||||
* },
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
*/
|
||||
mockingProxy: MockingProxyMode | undefined;
|
||||
}
|
||||
|
||||
export type ScreenshotMode = 'off' | 'on' | 'only-on-failure' | 'on-first-failure';
|
||||
export type TraceMode = 'off' | 'on' | 'retain-on-failure' | 'on-first-retry' | 'on-all-retries' | 'retain-on-first-failure';
|
||||
export type VideoMode = 'off' | 'on' | 'retain-on-failure' | 'on-first-retry';
|
||||
export type MockingProxyMode = 'off' | 'inject-via-header';
|
||||
|
||||
/**
|
||||
* Playwright Test provides many options to configure test environment,
|
||||
|
|
|
|||
180
packages/protocol/src/channels.d.ts
vendored
180
packages/protocol/src/channels.d.ts
vendored
|
|
@ -49,7 +49,6 @@ export type InitializerTraits<T> =
|
|||
T extends FrameChannel ? FrameInitializer :
|
||||
T extends PageChannel ? PageInitializer :
|
||||
T extends BrowserContextChannel ? BrowserContextInitializer :
|
||||
T extends EventTargetChannel ? EventTargetInitializer :
|
||||
T extends BrowserChannel ? BrowserInitializer :
|
||||
T extends BrowserTypeChannel ? BrowserTypeInitializer :
|
||||
T extends SelectorsChannel ? SelectorsInitializer :
|
||||
|
|
@ -57,7 +56,9 @@ export type InitializerTraits<T> =
|
|||
T extends DebugControllerChannel ? DebugControllerInitializer :
|
||||
T extends PlaywrightChannel ? PlaywrightInitializer :
|
||||
T extends RootChannel ? RootInitializer :
|
||||
T extends MockingProxyChannel ? MockingProxyInitializer :
|
||||
T extends LocalUtilsChannel ? LocalUtilsInitializer :
|
||||
T extends EventTargetChannel ? EventTargetInitializer :
|
||||
T extends APIRequestContextChannel ? APIRequestContextInitializer :
|
||||
object;
|
||||
|
||||
|
|
@ -87,7 +88,6 @@ export type EventsTraits<T> =
|
|||
T extends FrameChannel ? FrameEvents :
|
||||
T extends PageChannel ? PageEvents :
|
||||
T extends BrowserContextChannel ? BrowserContextEvents :
|
||||
T extends EventTargetChannel ? EventTargetEvents :
|
||||
T extends BrowserChannel ? BrowserEvents :
|
||||
T extends BrowserTypeChannel ? BrowserTypeEvents :
|
||||
T extends SelectorsChannel ? SelectorsEvents :
|
||||
|
|
@ -95,7 +95,9 @@ export type EventsTraits<T> =
|
|||
T extends DebugControllerChannel ? DebugControllerEvents :
|
||||
T extends PlaywrightChannel ? PlaywrightEvents :
|
||||
T extends RootChannel ? RootEvents :
|
||||
T extends MockingProxyChannel ? MockingProxyEvents :
|
||||
T extends LocalUtilsChannel ? LocalUtilsEvents :
|
||||
T extends EventTargetChannel ? EventTargetEvents :
|
||||
T extends APIRequestContextChannel ? APIRequestContextEvents :
|
||||
undefined;
|
||||
|
||||
|
|
@ -125,7 +127,6 @@ export type EventTargetTraits<T> =
|
|||
T extends FrameChannel ? FrameEventTarget :
|
||||
T extends PageChannel ? PageEventTarget :
|
||||
T extends BrowserContextChannel ? BrowserContextEventTarget :
|
||||
T extends EventTargetChannel ? EventTargetEventTarget :
|
||||
T extends BrowserChannel ? BrowserEventTarget :
|
||||
T extends BrowserTypeChannel ? BrowserTypeEventTarget :
|
||||
T extends SelectorsChannel ? SelectorsEventTarget :
|
||||
|
|
@ -133,7 +134,9 @@ export type EventTargetTraits<T> =
|
|||
T extends DebugControllerChannel ? DebugControllerEventTarget :
|
||||
T extends PlaywrightChannel ? PlaywrightEventTarget :
|
||||
T extends RootChannel ? RootEventTarget :
|
||||
T extends MockingProxyChannel ? MockingProxyEventTarget :
|
||||
T extends LocalUtilsChannel ? LocalUtilsEventTarget :
|
||||
T extends EventTargetChannel ? EventTargetEventTarget :
|
||||
T extends APIRequestContextChannel ? APIRequestContextEventTarget :
|
||||
undefined;
|
||||
|
||||
|
|
@ -439,6 +442,31 @@ export type APIResponse = {
|
|||
};
|
||||
|
||||
export type LifecycleEvent = 'load' | 'domcontentloaded' | 'networkidle' | 'commit';
|
||||
// ----------- EventTarget -----------
|
||||
export type EventTargetInitializer = {};
|
||||
export interface EventTargetEventTarget {
|
||||
}
|
||||
export interface EventTargetChannel extends EventTargetEventTarget, Channel {
|
||||
_type_EventTarget: boolean;
|
||||
waitForEventInfo(params: EventTargetWaitForEventInfoParams, metadata?: CallMetadata): Promise<EventTargetWaitForEventInfoResult>;
|
||||
}
|
||||
export type EventTargetWaitForEventInfoParams = {
|
||||
info: {
|
||||
waitId: string,
|
||||
phase: 'before' | 'after' | 'log',
|
||||
event?: string,
|
||||
message?: string,
|
||||
error?: string,
|
||||
},
|
||||
};
|
||||
export type EventTargetWaitForEventInfoOptions = {
|
||||
|
||||
};
|
||||
export type EventTargetWaitForEventInfoResult = void;
|
||||
|
||||
export interface EventTargetEvents {
|
||||
}
|
||||
|
||||
// ----------- LocalUtils -----------
|
||||
export type LocalUtilsInitializer = {
|
||||
deviceDescriptors: {
|
||||
|
|
@ -473,6 +501,8 @@ export interface LocalUtilsChannel extends LocalUtilsEventTarget, Channel {
|
|||
tracingStarted(params: LocalUtilsTracingStartedParams, metadata?: CallMetadata): Promise<LocalUtilsTracingStartedResult>;
|
||||
addStackToTracingNoReply(params: LocalUtilsAddStackToTracingNoReplyParams, metadata?: CallMetadata): Promise<LocalUtilsAddStackToTracingNoReplyResult>;
|
||||
traceDiscarded(params: LocalUtilsTraceDiscardedParams, metadata?: CallMetadata): Promise<LocalUtilsTraceDiscardedResult>;
|
||||
newRequest(params: LocalUtilsNewRequestParams, metadata?: CallMetadata): Promise<LocalUtilsNewRequestResult>;
|
||||
newMockingProxy(params: LocalUtilsNewMockingProxyParams, metadata?: CallMetadata): Promise<LocalUtilsNewMockingProxyResult>;
|
||||
}
|
||||
export type LocalUtilsZipParams = {
|
||||
zipFile: string,
|
||||
|
|
@ -572,10 +602,127 @@ export type LocalUtilsTraceDiscardedOptions = {
|
|||
|
||||
};
|
||||
export type LocalUtilsTraceDiscardedResult = void;
|
||||
export type LocalUtilsNewRequestParams = {
|
||||
baseURL?: string,
|
||||
userAgent?: string,
|
||||
ignoreHTTPSErrors?: boolean,
|
||||
extraHTTPHeaders?: NameValue[],
|
||||
clientCertificates?: {
|
||||
origin: string,
|
||||
cert?: Binary,
|
||||
key?: Binary,
|
||||
passphrase?: string,
|
||||
pfx?: Binary,
|
||||
}[],
|
||||
httpCredentials?: {
|
||||
username: string,
|
||||
password: string,
|
||||
origin?: string,
|
||||
send?: 'always' | 'unauthorized',
|
||||
},
|
||||
proxy?: {
|
||||
server: string,
|
||||
bypass?: string,
|
||||
username?: string,
|
||||
password?: string,
|
||||
},
|
||||
timeout?: number,
|
||||
storageState?: {
|
||||
cookies?: NetworkCookie[],
|
||||
origins?: SetOriginStorage[],
|
||||
},
|
||||
tracesDir?: string,
|
||||
};
|
||||
export type LocalUtilsNewRequestOptions = {
|
||||
baseURL?: string,
|
||||
userAgent?: string,
|
||||
ignoreHTTPSErrors?: boolean,
|
||||
extraHTTPHeaders?: NameValue[],
|
||||
clientCertificates?: {
|
||||
origin: string,
|
||||
cert?: Binary,
|
||||
key?: Binary,
|
||||
passphrase?: string,
|
||||
pfx?: Binary,
|
||||
}[],
|
||||
httpCredentials?: {
|
||||
username: string,
|
||||
password: string,
|
||||
origin?: string,
|
||||
send?: 'always' | 'unauthorized',
|
||||
},
|
||||
proxy?: {
|
||||
server: string,
|
||||
bypass?: string,
|
||||
username?: string,
|
||||
password?: string,
|
||||
},
|
||||
timeout?: number,
|
||||
storageState?: {
|
||||
cookies?: NetworkCookie[],
|
||||
origins?: SetOriginStorage[],
|
||||
},
|
||||
tracesDir?: string,
|
||||
};
|
||||
export type LocalUtilsNewRequestResult = {
|
||||
request: APIRequestContextChannel,
|
||||
};
|
||||
export type LocalUtilsNewMockingProxyParams = {
|
||||
requestContext: APIRequestContextChannel,
|
||||
};
|
||||
export type LocalUtilsNewMockingProxyOptions = {
|
||||
|
||||
};
|
||||
export type LocalUtilsNewMockingProxyResult = {
|
||||
mockingProxy: MockingProxyChannel,
|
||||
};
|
||||
|
||||
export interface LocalUtilsEvents {
|
||||
}
|
||||
|
||||
// ----------- MockingProxy -----------
|
||||
export type MockingProxyInitializer = {
|
||||
baseURL: string,
|
||||
};
|
||||
export interface MockingProxyEventTarget {
|
||||
on(event: 'route', callback: (params: MockingProxyRouteEvent) => void): this;
|
||||
on(event: 'request', callback: (params: MockingProxyRequestEvent) => void): this;
|
||||
on(event: 'requestFailed', callback: (params: MockingProxyRequestFailedEvent) => void): this;
|
||||
on(event: 'requestFinished', callback: (params: MockingProxyRequestFinishedEvent) => void): this;
|
||||
on(event: 'response', callback: (params: MockingProxyResponseEvent) => void): this;
|
||||
}
|
||||
export interface MockingProxyChannel extends MockingProxyEventTarget, EventTargetChannel {
|
||||
_type_MockingProxy: boolean;
|
||||
}
|
||||
export type MockingProxyRouteEvent = {
|
||||
route: RouteChannel,
|
||||
};
|
||||
export type MockingProxyRequestEvent = {
|
||||
request: RequestChannel,
|
||||
correlation: string,
|
||||
};
|
||||
export type MockingProxyRequestFailedEvent = {
|
||||
request: RequestChannel,
|
||||
failureText?: string,
|
||||
responseEndTiming: number,
|
||||
};
|
||||
export type MockingProxyRequestFinishedEvent = {
|
||||
request: RequestChannel,
|
||||
response?: ResponseChannel,
|
||||
responseEndTiming: number,
|
||||
};
|
||||
export type MockingProxyResponseEvent = {
|
||||
response: ResponseChannel,
|
||||
};
|
||||
|
||||
export interface MockingProxyEvents {
|
||||
'route': MockingProxyRouteEvent;
|
||||
'request': MockingProxyRequestEvent;
|
||||
'requestFailed': MockingProxyRequestFailedEvent;
|
||||
'requestFinished': MockingProxyRequestFinishedEvent;
|
||||
'response': MockingProxyResponseEvent;
|
||||
}
|
||||
|
||||
// ----------- Root -----------
|
||||
export type RootInitializer = {};
|
||||
export interface RootEventTarget {
|
||||
|
|
@ -1260,6 +1407,7 @@ export type BrowserNewContextParams = {
|
|||
cookies?: SetNetworkCookie[],
|
||||
origins?: SetOriginStorage[],
|
||||
},
|
||||
mockingProxyBaseURL?: string,
|
||||
};
|
||||
export type BrowserNewContextOptions = {
|
||||
noDefaultViewport?: boolean,
|
||||
|
|
@ -1327,6 +1475,7 @@ export type BrowserNewContextOptions = {
|
|||
cookies?: SetNetworkCookie[],
|
||||
origins?: SetOriginStorage[],
|
||||
},
|
||||
mockingProxyBaseURL?: string,
|
||||
};
|
||||
export type BrowserNewContextResult = {
|
||||
context: BrowserContextChannel,
|
||||
|
|
@ -1501,31 +1650,6 @@ export interface BrowserEvents {
|
|||
'close': BrowserCloseEvent;
|
||||
}
|
||||
|
||||
// ----------- EventTarget -----------
|
||||
export type EventTargetInitializer = {};
|
||||
export interface EventTargetEventTarget {
|
||||
}
|
||||
export interface EventTargetChannel extends EventTargetEventTarget, Channel {
|
||||
_type_EventTarget: boolean;
|
||||
waitForEventInfo(params: EventTargetWaitForEventInfoParams, metadata?: CallMetadata): Promise<EventTargetWaitForEventInfoResult>;
|
||||
}
|
||||
export type EventTargetWaitForEventInfoParams = {
|
||||
info: {
|
||||
waitId: string,
|
||||
phase: 'before' | 'after' | 'log',
|
||||
event?: string,
|
||||
message?: string,
|
||||
error?: string,
|
||||
},
|
||||
};
|
||||
export type EventTargetWaitForEventInfoOptions = {
|
||||
|
||||
};
|
||||
export type EventTargetWaitForEventInfoResult = void;
|
||||
|
||||
export interface EventTargetEvents {
|
||||
}
|
||||
|
||||
// ----------- BrowserContext -----------
|
||||
export type BrowserContextInitializer = {
|
||||
isChromium: boolean,
|
||||
|
|
|
|||
|
|
@ -584,6 +584,78 @@ ContextOptions:
|
|||
- allow
|
||||
- block
|
||||
|
||||
NewRequestParameters:
|
||||
type: mixin
|
||||
properties:
|
||||
baseURL: string?
|
||||
userAgent: string?
|
||||
ignoreHTTPSErrors: boolean?
|
||||
extraHTTPHeaders:
|
||||
type: array?
|
||||
items: NameValue
|
||||
clientCertificates:
|
||||
type: array?
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
origin: string
|
||||
cert: binary?
|
||||
key: binary?
|
||||
passphrase: string?
|
||||
pfx: binary?
|
||||
httpCredentials:
|
||||
type: object?
|
||||
properties:
|
||||
username: string
|
||||
password: string
|
||||
origin: string?
|
||||
send:
|
||||
type: enum?
|
||||
literals:
|
||||
- always
|
||||
- unauthorized
|
||||
proxy:
|
||||
type: object?
|
||||
properties:
|
||||
server: string
|
||||
bypass: string?
|
||||
username: string?
|
||||
password: string?
|
||||
timeout: number?
|
||||
storageState:
|
||||
type: object?
|
||||
properties:
|
||||
cookies:
|
||||
type: array?
|
||||
items: NetworkCookie
|
||||
origins:
|
||||
type: array?
|
||||
items: SetOriginStorage
|
||||
tracesDir: string?
|
||||
|
||||
|
||||
EventTarget:
|
||||
type: interface
|
||||
|
||||
commands:
|
||||
waitForEventInfo:
|
||||
parameters:
|
||||
info:
|
||||
type: object
|
||||
properties:
|
||||
waitId: string
|
||||
phase:
|
||||
type: enum
|
||||
literals:
|
||||
- before
|
||||
- after
|
||||
- log
|
||||
event: string?
|
||||
message: string?
|
||||
error: string?
|
||||
flags:
|
||||
snapshot: true
|
||||
|
||||
LocalUtils:
|
||||
type: interface
|
||||
|
||||
|
|
@ -705,6 +777,52 @@ LocalUtils:
|
|||
parameters:
|
||||
stacksId: string
|
||||
|
||||
newRequest:
|
||||
parameters:
|
||||
$mixin: NewRequestParameters
|
||||
returns:
|
||||
request: APIRequestContext
|
||||
|
||||
newMockingProxy:
|
||||
parameters:
|
||||
requestContext: APIRequestContext
|
||||
returns:
|
||||
mockingProxy: MockingProxy
|
||||
|
||||
MockingProxy:
|
||||
type: interface
|
||||
|
||||
extends: EventTarget
|
||||
|
||||
initializer:
|
||||
baseURL: string
|
||||
|
||||
events:
|
||||
route:
|
||||
parameters:
|
||||
route: Route
|
||||
|
||||
request:
|
||||
parameters:
|
||||
request: Request
|
||||
correlation: string
|
||||
|
||||
requestFailed:
|
||||
parameters:
|
||||
request: Request
|
||||
failureText: string?
|
||||
responseEndTiming: number
|
||||
|
||||
requestFinished:
|
||||
parameters:
|
||||
request: Request
|
||||
response: Response?
|
||||
responseEndTiming: number
|
||||
|
||||
response:
|
||||
parameters:
|
||||
response: Response
|
||||
|
||||
Root:
|
||||
type: interface
|
||||
|
||||
|
|
@ -745,52 +863,7 @@ Playwright:
|
|||
commands:
|
||||
newRequest:
|
||||
parameters:
|
||||
baseURL: string?
|
||||
userAgent: string?
|
||||
ignoreHTTPSErrors: boolean?
|
||||
extraHTTPHeaders:
|
||||
type: array?
|
||||
items: NameValue
|
||||
clientCertificates:
|
||||
type: array?
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
origin: string
|
||||
cert: binary?
|
||||
key: binary?
|
||||
passphrase: string?
|
||||
pfx: binary?
|
||||
httpCredentials:
|
||||
type: object?
|
||||
properties:
|
||||
username: string
|
||||
password: string
|
||||
origin: string?
|
||||
send:
|
||||
type: enum?
|
||||
literals:
|
||||
- always
|
||||
- unauthorized
|
||||
proxy:
|
||||
type: object?
|
||||
properties:
|
||||
server: string
|
||||
bypass: string?
|
||||
username: string?
|
||||
password: string?
|
||||
timeout: number?
|
||||
storageState:
|
||||
type: object?
|
||||
properties:
|
||||
cookies:
|
||||
type: array?
|
||||
items: NetworkCookie
|
||||
origins:
|
||||
type: array?
|
||||
items: SetOriginStorage
|
||||
tracesDir: string?
|
||||
|
||||
$mixin: NewRequestParameters
|
||||
returns:
|
||||
request: APIRequestContext
|
||||
|
||||
|
|
@ -1023,6 +1096,7 @@ Browser:
|
|||
origins:
|
||||
type: array?
|
||||
items: SetOriginStorage
|
||||
mockingProxyBaseURL: string?
|
||||
returns:
|
||||
context: BrowserContext
|
||||
|
||||
|
|
@ -1088,29 +1162,6 @@ ConsoleMessage:
|
|||
lineNumber: number
|
||||
columnNumber: number
|
||||
|
||||
|
||||
EventTarget:
|
||||
type: interface
|
||||
|
||||
commands:
|
||||
waitForEventInfo:
|
||||
parameters:
|
||||
info:
|
||||
type: object
|
||||
properties:
|
||||
waitId: string
|
||||
phase:
|
||||
type: enum
|
||||
literals:
|
||||
- before
|
||||
- after
|
||||
- log
|
||||
event: string?
|
||||
message: string?
|
||||
error: string?
|
||||
flags:
|
||||
snapshot: true
|
||||
|
||||
BrowserContext:
|
||||
type: interface
|
||||
|
||||
|
|
|
|||
298
tests/playwright-test/playwright.mockingproxy.spec.ts
Normal file
298
tests/playwright-test/playwright.mockingproxy.spec.ts
Normal file
|
|
@ -0,0 +1,298 @@
|
|||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { test, expect } from './playwright-test-fixtures';
|
||||
import http from 'http';
|
||||
|
||||
const config = {
|
||||
'playwright.config.ts': `
|
||||
module.exports = {
|
||||
use: {
|
||||
mockingProxy: "inject-via-header",
|
||||
ignoreHTTPSErrors: true,
|
||||
}
|
||||
};
|
||||
`,
|
||||
};
|
||||
|
||||
test('inject mode', async ({ runInlineTest, server }) => {
|
||||
server.setRoute('/page', (req, res) => {
|
||||
res.end(req.headers['x-playwright-proxy'] ? 'proxy url injected' : 'proxy url missing');
|
||||
});
|
||||
const result = await runInlineTest({
|
||||
...config,
|
||||
'a.test.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('foo', async ({ page }) => {
|
||||
await page.goto('${server.PREFIX}/page');
|
||||
expect(await page.textContent('body')).toEqual('proxy url injected');
|
||||
});
|
||||
`
|
||||
}, { workers: 1 });
|
||||
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.passed).toBe(1);
|
||||
});
|
||||
|
||||
test('routes are reset between tests', async ({ runInlineTest, server, request }) => {
|
||||
server.setRoute('/fallback', async (req, res) => {
|
||||
res.end('fallback');
|
||||
});
|
||||
server.setRoute('/page', async (req, res) => {
|
||||
const proxyURL = decodeURIComponent((req.headers['x-playwright-proxy'] as string) ?? '');
|
||||
const response = await request.get(proxyURL + server.PREFIX + '/fallback');
|
||||
res.end(await response.body());
|
||||
});
|
||||
const result = await runInlineTest({
|
||||
...config,
|
||||
'a.test.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('first', async ({ page, request, context }) => {
|
||||
await context.route('${server.PREFIX}/fallback', route => route.fulfill({ body: 'first' }));
|
||||
await page.goto('${server.PREFIX}/page');
|
||||
expect(await page.textContent('body')).toEqual('first');
|
||||
});
|
||||
test('second', async ({ page, request, context }) => {
|
||||
await context.route('${server.PREFIX}/fallback', route => route.fallback());
|
||||
await page.goto('${server.PREFIX}/page');
|
||||
expect(await page.textContent('body')).toEqual('fallback');
|
||||
});
|
||||
`
|
||||
}, { workers: 1 });
|
||||
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.passed).toBe(2);
|
||||
});
|
||||
|
||||
test('all properties are populated', async ({ runInlineTest, server, request }) => {
|
||||
server.setRoute('/fallback', async (req, res) => {
|
||||
res.statusCode = 201;
|
||||
res.setHeader('foo', 'bar');
|
||||
res.end('fallback');
|
||||
});
|
||||
server.setRoute('/page', async (req, res) => {
|
||||
const proxyURL = decodeURIComponent((req.headers['x-playwright-proxy'] as string) ?? '');
|
||||
const response = await request.get(proxyURL + server.PREFIX + '/fallback');
|
||||
res.end(await response.body());
|
||||
});
|
||||
const result = await runInlineTest({
|
||||
...config,
|
||||
'a.test.js': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('test', async ({ page, context }) => {
|
||||
let request;
|
||||
await context.route('${server.PREFIX}/fallback', route => {
|
||||
request = route.request();
|
||||
route.continue();
|
||||
});
|
||||
await page.goto('${server.PREFIX}/page');
|
||||
expect(await page.textContent('body')).toEqual('fallback');
|
||||
|
||||
const response = await request.response();
|
||||
expect(request.url()).toBe('${server.PREFIX}/fallback');
|
||||
expect(response.url()).toBe('${server.PREFIX}/fallback');
|
||||
expect(response.status()).toBe(201);
|
||||
expect(await response.headersArray()).toContainEqual({ name: 'foo', value: 'bar' });
|
||||
expect(await response.body()).toEqual(Buffer.from('fallback'));
|
||||
|
||||
expect(await response.finished()).toBe(null);
|
||||
expect(request.serviceWorker()).toBe(null);
|
||||
expect(() => request.frame()).toThrowError("Frame for this request is not available, because the request was issued on the server.");
|
||||
|
||||
expect(request.failure()).toBe(null);
|
||||
expect(request.isNavigationRequest()).toBe(false);
|
||||
expect(request.redirectedFrom()).toBe(null);
|
||||
expect(request.redirectedTo()).toBe(null);
|
||||
expect(request.resourceType()).toBe(''); // TODO: should this be different?
|
||||
expect(request.method()).toBe('GET');
|
||||
|
||||
expect(await request.sizes()).toEqual({
|
||||
requestBodySize: 0,
|
||||
requestHeadersSize: expect.any(Number),
|
||||
responseBodySize: 8,
|
||||
responseHeadersSize: 137,
|
||||
});
|
||||
|
||||
expect(request.timing()).toEqual({
|
||||
'connectEnd': expect.any(Number),
|
||||
'connectStart': expect.any(Number),
|
||||
'domainLookupEnd': expect.any(Number),
|
||||
'domainLookupStart': -1,
|
||||
'requestStart': expect.any(Number),
|
||||
'responseEnd': expect.any(Number),
|
||||
'responseStart': expect.any(Number),
|
||||
'secureConnectionStart': -1,
|
||||
'startTime': expect.any(Number),
|
||||
});
|
||||
|
||||
expect(await response.securityDetails()).toBe(null);
|
||||
expect(await response.serverAddr()).toEqual({
|
||||
ipAddress: expect.any(String),
|
||||
port: expect.any(Number),
|
||||
});
|
||||
});
|
||||
`
|
||||
}, { workers: 1 });
|
||||
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.passed).toBe(1);
|
||||
});
|
||||
|
||||
test('securityDetails', async ({ httpsServer, request, runInlineTest }) => {
|
||||
httpsServer.setRoute('/fallback', async (req, res) => {
|
||||
res.statusCode = 201;
|
||||
res.setHeader('foo', 'bar');
|
||||
res.end('fallback');
|
||||
});
|
||||
httpsServer.setRoute('/page', async (req, res) => {
|
||||
const proxyURL = decodeURIComponent((req.headers['x-playwright-proxy'] as string) ?? '');
|
||||
const response = await request.get(proxyURL + httpsServer.PREFIX + '/fallback', { ignoreHTTPSErrors: true });
|
||||
res.end(await response.body());
|
||||
});
|
||||
const result = await runInlineTest({
|
||||
...config,
|
||||
'a.test.js': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('test', async ({ page, context }) => {
|
||||
let request;
|
||||
await context.route('${httpsServer.PREFIX}/fallback', route => {
|
||||
request = route.request();
|
||||
route.continue();
|
||||
});
|
||||
await page.goto('${httpsServer.PREFIX}/page');
|
||||
expect(await page.textContent('body')).toEqual('fallback');
|
||||
const response = await request.response();
|
||||
expect(await response.securityDetails()).toEqual({
|
||||
"issuer": "playwright-test",
|
||||
"protocol": expect.any(String),
|
||||
"subjectName": "playwright-test",
|
||||
"validFrom": expect.any(Number),
|
||||
"validTo": expect.any(Number)
|
||||
});
|
||||
});
|
||||
`
|
||||
}, { workers: 1 }, { NODE_TLS_REJECT_UNAUTHORIZED: '0' });
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.passed).toBe(1);
|
||||
});
|
||||
|
||||
test('aborting', async ({ runInlineTest, server }) => {
|
||||
server.setRoute('/page', async (req, res) => {
|
||||
const proxyURL = decodeURIComponent((req.headers['x-playwright-proxy'] as string) ?? '');
|
||||
const request = http.get(proxyURL + server.PREFIX + '/fallback');
|
||||
request.on('error', () => res.end('aborted'));
|
||||
request.pipe(res);
|
||||
});
|
||||
const result = await runInlineTest({
|
||||
...config,
|
||||
'a.test.js': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('test', async ({ page, context, request }) => {
|
||||
await context.route('${server.PREFIX}/fallback', route => route.abort());
|
||||
const response = await request.get('${server.PREFIX}/page')
|
||||
expect(await response.text()).toEqual('aborted');
|
||||
});
|
||||
`
|
||||
}, { workers: 1 });
|
||||
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.passed).toBe(1);
|
||||
});
|
||||
|
||||
test('fetch', async ({ runInlineTest, server, request }) => {
|
||||
server.setRoute('/fallback', async (req, res) => {
|
||||
res.statusCode = 201;
|
||||
res.setHeader('foo', 'bar');
|
||||
res.end('fallback');
|
||||
});
|
||||
server.setRoute('/page', async (req, res) => {
|
||||
const proxyURL = decodeURIComponent((req.headers['x-playwright-proxy'] as string) ?? '');
|
||||
const response = await request.get(proxyURL + server.PREFIX + '/fallback');
|
||||
res.end(await response.body());
|
||||
});
|
||||
const result = await runInlineTest({
|
||||
...config,
|
||||
'a.test.js': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('test', async ({ page, context }) => {
|
||||
let request;
|
||||
await context.route('${server.PREFIX}/fallback', async route => {
|
||||
route.fulfill({ response: await route.fetch() });
|
||||
});
|
||||
await page.goto('${server.PREFIX}/page');
|
||||
expect(await page.textContent('body')).toEqual('fallback');
|
||||
});
|
||||
`
|
||||
}, { workers: 1 });
|
||||
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.passed).toBe(1);
|
||||
});
|
||||
|
||||
test('inject mode knows originating page', async ({ runInlineTest, server, request }) => {
|
||||
server.setRoute('/fallback', async (req, res) => {
|
||||
res.end('fallback');
|
||||
});
|
||||
server.setRoute('/page', async (req, res) => {
|
||||
const proxyURL = decodeURIComponent((req.headers['x-playwright-proxy'] as string) ?? '');
|
||||
const response = await request.get(proxyURL + server.PREFIX + '/fallback');
|
||||
res.end(await response.body());
|
||||
});
|
||||
const result = await runInlineTest({
|
||||
...config,
|
||||
'a.test.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('first', async ({ page, context }) => {
|
||||
await page.route('${server.PREFIX}/fallback', route => route.fulfill({ body: 'first' }));
|
||||
await page.goto('${server.PREFIX}/page');
|
||||
expect(await page.textContent('body')).toEqual('first');
|
||||
});
|
||||
`
|
||||
}, { workers: 1 });
|
||||
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.passed).toBe(1);
|
||||
});
|
||||
|
||||
test('failure', async ({ runInlineTest, server, request }) => {
|
||||
server.setRoute('/fallback', async (req, res) => {
|
||||
res.socket.destroy();
|
||||
});
|
||||
server.setRoute('/page', async (req, res) => {
|
||||
const proxyURL = decodeURIComponent((req.headers['x-playwright-proxy'] as string) ?? '');
|
||||
const response = await request.get(proxyURL + server.PREFIX + '/fallback');
|
||||
res.end(await response.body());
|
||||
});
|
||||
const result = await runInlineTest({
|
||||
...config,
|
||||
'a.test.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('first', async ({ page, context }) => {
|
||||
let request;
|
||||
await page.route('${server.PREFIX}/fallback', route => {
|
||||
request = route.request();
|
||||
route.continue();
|
||||
});
|
||||
await page.goto('${server.PREFIX}/page');
|
||||
|
||||
expect(request.failure()).toEqual({ errorText: expect.any(String) });
|
||||
});
|
||||
`
|
||||
}, { workers: 1 });
|
||||
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.passed).toBe(1);
|
||||
});
|
||||
2
utils/generate_types/overrides-test.d.ts
vendored
2
utils/generate_types/overrides-test.d.ts
vendored
|
|
@ -236,11 +236,13 @@ export interface PlaywrightWorkerOptions {
|
|||
screenshot: ScreenshotMode | { mode: ScreenshotMode } & Pick<PageScreenshotOptions, 'fullPage' | 'omitBackground'>;
|
||||
trace: TraceMode | /** deprecated */ 'retry-with-trace' | { mode: TraceMode, snapshots?: boolean, screenshots?: boolean, sources?: boolean, attachments?: boolean };
|
||||
video: VideoMode | /** deprecated */ 'retry-with-video' | { mode: VideoMode, size?: ViewportSize };
|
||||
mockingProxy: MockingProxyMode | undefined;
|
||||
}
|
||||
|
||||
export type ScreenshotMode = 'off' | 'on' | 'only-on-failure' | 'on-first-failure';
|
||||
export type TraceMode = 'off' | 'on' | 'retain-on-failure' | 'on-first-retry' | 'on-all-retries' | 'retain-on-first-failure';
|
||||
export type VideoMode = 'off' | 'on' | 'retain-on-failure' | 'on-first-retry';
|
||||
export type MockingProxyMode = 'off' | 'inject-via-header';
|
||||
|
||||
export interface PlaywrightTestOptions {
|
||||
acceptDownloads: boolean;
|
||||
|
|
|
|||
Loading…
Reference in a new issue