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].
|
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> {
|
async _innerNewContext(options: BrowserContextOptions = {}, forReuse: boolean): Promise<BrowserContext> {
|
||||||
options = { ...this._browserType._playwright._defaultContextOptions, ...options };
|
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 response = forReuse ? await this._channel.newContextForReuse(contextOptions) : await this._channel.newContext(contextOptions);
|
||||||
const context = BrowserContext.from(response.context);
|
const context = BrowserContext.from(response.context);
|
||||||
await this._browserType._didCreateContext(context, contextOptions, this._options, options.logger || this._logger);
|
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('bindingCall', ({ binding }) => this._onBinding(BindingCall.from(binding)));
|
||||||
this._channel.on('close', () => this._onClose());
|
this._channel.on('close', () => this._onClose());
|
||||||
this._channel.on('page', ({ page }) => this._onPage(Page.from(page)));
|
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('webSocketRoute', ({ webSocketRoute }) => this._onWebSocketRoute(network.WebSocketRoute.from(webSocketRoute)));
|
||||||
this._channel.on('backgroundPage', ({ page }) => {
|
this._channel.on('backgroundPage', ({ page }) => {
|
||||||
const backgroundPage = Page.from(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('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('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._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));
|
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);
|
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);
|
this.emit(Events.BrowserContext.Request, request);
|
||||||
if (page)
|
if (page)
|
||||||
page.emit(Events.Page.Request, request);
|
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);
|
this.emit(Events.BrowserContext.Response, response);
|
||||||
if (page)
|
if (page)
|
||||||
page.emit(Events.Page.Response, response);
|
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._failureText = failureText || null;
|
||||||
request._setResponseEndTiming(responseEndTiming);
|
request._setResponseEndTiming(responseEndTiming);
|
||||||
this.emit(Events.BrowserContext.RequestFailed, request);
|
this.emit(Events.BrowserContext.RequestFailed, request);
|
||||||
|
|
@ -184,11 +188,7 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
|
||||||
page.emit(Events.Page.RequestFailed, request);
|
page.emit(Events.Page.RequestFailed, request);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _onRequestFinished(params: channels.BrowserContextRequestFinishedEvent) {
|
_onRequestFinished(request: network.Request, response: network.Response | null, page: Page | null, responseEndTiming: number) {
|
||||||
const { responseEndTiming } = params;
|
|
||||||
const request = network.Request.from(params.request);
|
|
||||||
const response = network.Response.fromNullable(params.response);
|
|
||||||
const page = Page.fromNullable(params.page);
|
|
||||||
request._setResponseEndTiming(responseEndTiming);
|
request._setResponseEndTiming(responseEndTiming);
|
||||||
this.emit(Events.BrowserContext.RequestFinished, request);
|
this.emit(Events.BrowserContext.RequestFinished, request);
|
||||||
if (page)
|
if (page)
|
||||||
|
|
@ -198,7 +198,6 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
|
||||||
}
|
}
|
||||||
|
|
||||||
async _onRoute(route: network.Route) {
|
async _onRoute(route: network.Route) {
|
||||||
route._context = this;
|
|
||||||
const page = route.request()._safePage();
|
const page = route.request()._safePage();
|
||||||
const routeHandlers = this._routes.slice();
|
const routeHandlers = this._routes.slice();
|
||||||
for (const routeHandler of routeHandlers) {
|
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)
|
if (options.videoSize && !options.videosPath)
|
||||||
throw new Error(`"videoSize" option requires "videosPath" to be specified`);
|
throw new Error(`"videoSize" option requires "videosPath" to be specified`);
|
||||||
if (options.extraHTTPHeaders)
|
if (options.extraHTTPHeaders)
|
||||||
|
|
@ -540,6 +539,7 @@ export async function prepareBrowserContextParams(options: BrowserContextOptions
|
||||||
contrast: options.contrast === null ? 'no-override' : options.contrast,
|
contrast: options.contrast === null ? 'no-override' : options.contrast,
|
||||||
acceptDownloads: toAcceptDownloadsProtocol(options.acceptDownloads),
|
acceptDownloads: toAcceptDownloadsProtocol(options.acceptDownloads),
|
||||||
clientCertificates: await toClientCertificatesProtocol(options.clientCertificates),
|
clientCertificates: await toClientCertificatesProtocol(options.clientCertificates),
|
||||||
|
mockingProxyBaseURL: type?._playwright._mockingProxy?.baseURL(),
|
||||||
};
|
};
|
||||||
if (!contextParams.recordVideo && options.videosPath) {
|
if (!contextParams.recordVideo && options.videosPath) {
|
||||||
contextParams.recordVideo = {
|
contextParams.recordVideo = {
|
||||||
|
|
|
||||||
|
|
@ -89,7 +89,7 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel> imple
|
||||||
const logger = options.logger || this._playwright._defaultLaunchOptions?.logger;
|
const logger = options.logger || this._playwright._defaultLaunchOptions?.logger;
|
||||||
assert(!(options as any).port, 'Cannot specify a port without launching as a server.');
|
assert(!(options as any).port, 'Cannot specify a port without launching as a server.');
|
||||||
options = { ...this._playwright._defaultLaunchOptions, ...this._playwright._defaultContextOptions, ...options };
|
options = { ...this._playwright._defaultLaunchOptions, ...this._playwright._defaultContextOptions, ...options };
|
||||||
const contextParams = await prepareBrowserContextParams(options);
|
const contextParams = await prepareBrowserContextParams(options, this);
|
||||||
const persistentParams: channels.BrowserTypeLaunchPersistentContextParams = {
|
const persistentParams: channels.BrowserTypeLaunchPersistentContextParams = {
|
||||||
...contextParams,
|
...contextParams,
|
||||||
ignoreDefaultArgs: Array.isArray(options.ignoreDefaultArgs) ? options.ignoreDefaultArgs : undefined,
|
ignoreDefaultArgs: Array.isArray(options.ignoreDefaultArgs) ? options.ignoreDefaultArgs : undefined,
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,7 @@ import { findValidator, ValidationError, type ValidatorContext } from '../protoc
|
||||||
import { createInstrumentation } from './clientInstrumentation';
|
import { createInstrumentation } from './clientInstrumentation';
|
||||||
import type { ClientInstrumentation } from './clientInstrumentation';
|
import type { ClientInstrumentation } from './clientInstrumentation';
|
||||||
import { formatCallLog, rewriteErrorMessage, zones } from '../utils';
|
import { formatCallLog, rewriteErrorMessage, zones } from '../utils';
|
||||||
|
import { MockingProxy } from './mockingProxy';
|
||||||
|
|
||||||
class Root extends ChannelOwner<channels.RootChannel> {
|
class Root extends ChannelOwner<channels.RootChannel> {
|
||||||
constructor(connection: Connection) {
|
constructor(connection: Connection) {
|
||||||
|
|
@ -279,6 +280,9 @@ export class Connection extends EventEmitter {
|
||||||
if (!this._localUtils)
|
if (!this._localUtils)
|
||||||
this._localUtils = result as LocalUtils;
|
this._localUtils = result as LocalUtils;
|
||||||
break;
|
break;
|
||||||
|
case 'MockingProxy':
|
||||||
|
result = new MockingProxy(parent, type, guid, initializer);
|
||||||
|
break;
|
||||||
case 'Page':
|
case 'Page':
|
||||||
result = new Page(parent, type, guid, initializer);
|
result = new Page(parent, type, guid, initializer);
|
||||||
break;
|
break;
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@ export type FetchOptions = {
|
||||||
maxRetries?: number,
|
maxRetries?: number,
|
||||||
};
|
};
|
||||||
|
|
||||||
type NewContextOptions = Omit<channels.PlaywrightNewRequestOptions, 'extraHTTPHeaders' | 'clientCertificates' | 'storageState' | 'tracesDir'> & {
|
export type NewContextOptions = Omit<channels.PlaywrightNewRequestOptions, 'extraHTTPHeaders' | 'clientCertificates' | 'storageState' | 'tracesDir'> & {
|
||||||
extraHTTPHeaders?: Headers,
|
extraHTTPHeaders?: Headers,
|
||||||
storageState?: string | SetStorageState,
|
storageState?: string | SetStorageState,
|
||||||
clientCertificates?: ClientCertificate[];
|
clientCertificates?: ClientCertificate[];
|
||||||
|
|
@ -62,6 +62,10 @@ export class APIRequest implements api.APIRequest {
|
||||||
}
|
}
|
||||||
|
|
||||||
async newContext(options: NewContextOptions = {}): Promise<APIRequestContext> {
|
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 = {
|
options = {
|
||||||
...this._playwright._defaultContextOptions,
|
...this._playwright._defaultContextOptions,
|
||||||
timeout: this._playwright._defaultContextTimeout,
|
timeout: this._playwright._defaultContextTimeout,
|
||||||
|
|
@ -70,7 +74,7 @@ export class APIRequest implements api.APIRequest {
|
||||||
const storageState = typeof options.storageState === 'string' ?
|
const storageState = typeof options.storageState === 'string' ?
|
||||||
JSON.parse(await fs.promises.readFile(options.storageState, 'utf8')) :
|
JSON.parse(await fs.promises.readFile(options.storageState, 'utf8')) :
|
||||||
options.storageState;
|
options.storageState;
|
||||||
const context = APIRequestContext.from((await this._playwright._channel.newRequest({
|
const context = APIRequestContext.from((await channel.newRequest({
|
||||||
...options,
|
...options,
|
||||||
extraHTTPHeaders: options.extraHTTPHeaders ? headersObjectToArray(options.extraHTTPHeaders) : undefined,
|
extraHTTPHeaders: options.extraHTTPHeaders ? headersObjectToArray(options.extraHTTPHeaders) : undefined,
|
||||||
storageState,
|
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 { Waiter } from './waiter';
|
||||||
import type * as api from '../../types/types';
|
import type * as api from '../../types/types';
|
||||||
import type { HeadersArray } from '../common/types';
|
import type { HeadersArray } from '../common/types';
|
||||||
|
import type { APIRequestContext } from './fetch';
|
||||||
import { APIResponse } from './fetch';
|
import { APIResponse } from './fetch';
|
||||||
import type { Serializable } from '../../types/structs';
|
import type { Serializable } from '../../types/structs';
|
||||||
import type { BrowserContext } from './browserContext';
|
|
||||||
import { isTargetClosedError } from './errors';
|
import { isTargetClosedError } from './errors';
|
||||||
|
|
||||||
export type NetworkCookie = {
|
export type NetworkCookie = {
|
||||||
|
|
@ -86,6 +86,7 @@ export class Request extends ChannelOwner<channels.RequestChannel> implements ap
|
||||||
private _actualHeadersPromise: Promise<RawHeaders> | undefined;
|
private _actualHeadersPromise: Promise<RawHeaders> | undefined;
|
||||||
_timing: ResourceTiming;
|
_timing: ResourceTiming;
|
||||||
private _fallbackOverrides: SerializedFallbackOverrides = {};
|
private _fallbackOverrides: SerializedFallbackOverrides = {};
|
||||||
|
_pageForMockingProxy: Page | null = null;
|
||||||
|
|
||||||
static from(request: channels.RequestChannel): Request {
|
static from(request: channels.RequestChannel): Request {
|
||||||
return (request as any)._object;
|
return (request as any)._object;
|
||||||
|
|
@ -200,6 +201,8 @@ export class Request extends ChannelOwner<channels.RequestChannel> implements ap
|
||||||
}
|
}
|
||||||
|
|
||||||
frame(): Frame {
|
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) {
|
if (!this._initializer.frame) {
|
||||||
assert(this.serviceWorker());
|
assert(this.serviceWorker());
|
||||||
throw new Error('Service Worker requests do not have an associated frame.');
|
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 {
|
_safePage(): Page | null {
|
||||||
return Frame.fromNullable(this._initializer.frame)?._page || null;
|
return Frame.fromNullable(this._initializer.frame)?._page ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
serviceWorker(): Worker | 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 {
|
export class Route extends ChannelOwner<channels.RouteChannel> implements api.Route {
|
||||||
private _handlingPromise: ManualPromise<boolean> | null = null;
|
private _handlingPromise: ManualPromise<boolean> | null = null;
|
||||||
_context!: BrowserContext;
|
_apiRequestContext!: APIRequestContext;
|
||||||
_didThrow: boolean = false;
|
_didThrow: boolean = false;
|
||||||
|
|
||||||
static from(route: channels.RouteChannel): Route {
|
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> {
|
async fetch(options: FallbackOverrides & { maxRedirects?: number, maxRetries?: number, timeout?: number } = {}): Promise<APIResponse> {
|
||||||
return await this._wrapApiCall(async () => {
|
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('frameAttached', ({ frame }) => this._onFrameAttached(Frame.from(frame)));
|
||||||
this._channel.on('frameDetached', ({ frame }) => this._onFrameDetached(Frame.from(frame)));
|
this._channel.on('frameDetached', ({ frame }) => this._onFrameDetached(Frame.from(frame)));
|
||||||
this._channel.on('locatorHandlerTriggered', ({ uid }) => this._onLocatorHandlerTriggered(uid));
|
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('webSocketRoute', ({ webSocketRoute }) => this._onWebSocketRoute(WebSocketRoute.from(webSocketRoute)));
|
||||||
this._channel.on('video', ({ artifact }) => {
|
this._channel.on('video', ({ artifact }) => {
|
||||||
const artifactObject = Artifact.from(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);
|
this.emit(Events.Page.FrameDetached, frame);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _onRoute(route: Route) {
|
async _onRoute(route: Route) {
|
||||||
route._context = this.context();
|
|
||||||
const routeHandlers = this._routes.slice();
|
const routeHandlers = this._routes.slice();
|
||||||
for (const routeHandler of routeHandlers) {
|
for (const routeHandler of routeHandlers) {
|
||||||
// If the page was closed we stall all requests right away.
|
// If the page was closed we stall all requests right away.
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ import { ChannelOwner } from './channelOwner';
|
||||||
import { Electron } from './electron';
|
import { Electron } from './electron';
|
||||||
import { APIRequest } from './fetch';
|
import { APIRequest } from './fetch';
|
||||||
import { Selectors, SelectorsOwner } from './selectors';
|
import { Selectors, SelectorsOwner } from './selectors';
|
||||||
|
import { MockingProxy } from './mockingProxy';
|
||||||
import type { BrowserContextOptions, LaunchOptions } from 'playwright-core';
|
import type { BrowserContextOptions, LaunchOptions } from 'playwright-core';
|
||||||
|
|
||||||
export class Playwright extends ChannelOwner<channels.PlaywrightChannel> {
|
export class Playwright extends ChannelOwner<channels.PlaywrightChannel> {
|
||||||
|
|
@ -36,6 +37,7 @@ export class Playwright extends ChannelOwner<channels.PlaywrightChannel> {
|
||||||
selectors: Selectors;
|
selectors: Selectors;
|
||||||
readonly request: APIRequest;
|
readonly request: APIRequest;
|
||||||
readonly errors: { TimeoutError: typeof TimeoutError };
|
readonly errors: { TimeoutError: typeof TimeoutError };
|
||||||
|
_mockingProxy?: MockingProxy;
|
||||||
|
|
||||||
// Instrumentation.
|
// Instrumentation.
|
||||||
_defaultLaunchOptions?: LaunchOptions;
|
_defaultLaunchOptions?: LaunchOptions;
|
||||||
|
|
@ -92,4 +94,14 @@ export class Playwright extends ChannelOwner<channels.PlaywrightChannel> {
|
||||||
_allPages() {
|
_allPages() {
|
||||||
return this._allContexts().flatMap(context => context.pages());
|
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([
|
export const commandsWithTracingSnapshots = new Set([
|
||||||
'EventTarget.waitForEventInfo',
|
'EventTarget.waitForEventInfo',
|
||||||
|
'MockingProxy.waitForEventInfo',
|
||||||
'BrowserContext.waitForEventInfo',
|
'BrowserContext.waitForEventInfo',
|
||||||
'Page.waitForEventInfo',
|
'Page.waitForEventInfo',
|
||||||
'WebSocket.waitForEventInfo',
|
'WebSocket.waitForEventInfo',
|
||||||
|
|
|
||||||
|
|
@ -257,6 +257,29 @@ scheme.APIResponse = tObject({
|
||||||
headers: tArray(tType('NameValue')),
|
headers: tArray(tType('NameValue')),
|
||||||
});
|
});
|
||||||
scheme.LifecycleEvent = tEnum(['load', 'domcontentloaded', 'networkidle', 'commit']);
|
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({
|
scheme.LocalUtilsInitializer = tObject({
|
||||||
deviceDescriptors: tArray(tObject({
|
deviceDescriptors: tArray(tObject({
|
||||||
name: tString,
|
name: tString,
|
||||||
|
|
@ -344,6 +367,69 @@ scheme.LocalUtilsTraceDiscardedParams = tObject({
|
||||||
stacksId: tString,
|
stacksId: tString,
|
||||||
});
|
});
|
||||||
scheme.LocalUtilsTraceDiscardedResult = tOptional(tObject({}));
|
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.RootInitializer = tOptional(tObject({}));
|
||||||
scheme.RootInitializeParams = tObject({
|
scheme.RootInitializeParams = tObject({
|
||||||
sdkLanguage: tEnum(['javascript', 'python', 'java', 'csharp']),
|
sdkLanguage: tEnum(['javascript', 'python', 'java', 'csharp']),
|
||||||
|
|
@ -722,6 +808,7 @@ scheme.BrowserNewContextParams = tObject({
|
||||||
cookies: tOptional(tArray(tType('SetNetworkCookie'))),
|
cookies: tOptional(tArray(tType('SetNetworkCookie'))),
|
||||||
origins: tOptional(tArray(tType('SetOriginStorage'))),
|
origins: tOptional(tArray(tType('SetOriginStorage'))),
|
||||||
})),
|
})),
|
||||||
|
mockingProxyBaseURL: tOptional(tString),
|
||||||
});
|
});
|
||||||
scheme.BrowserNewContextResult = tObject({
|
scheme.BrowserNewContextResult = tObject({
|
||||||
context: tChannel(['BrowserContext']),
|
context: tChannel(['BrowserContext']),
|
||||||
|
|
@ -814,27 +901,6 @@ scheme.BrowserStopTracingParams = tOptional(tObject({}));
|
||||||
scheme.BrowserStopTracingResult = tObject({
|
scheme.BrowserStopTracingResult = tObject({
|
||||||
artifact: tChannel(['Artifact']),
|
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({
|
scheme.BrowserContextInitializer = tObject({
|
||||||
isChromium: tBoolean,
|
isChromium: tBoolean,
|
||||||
requestContext: tChannel(['APIRequestContext']),
|
requestContext: tChannel(['APIRequestContext']),
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ import { RecorderApp } from './recorder/recorderApp';
|
||||||
import * as storageScript from './storageScript';
|
import * as storageScript from './storageScript';
|
||||||
import * as utilityScriptSerializers from './isomorphic/utilityScriptSerializers';
|
import * as utilityScriptSerializers from './isomorphic/utilityScriptSerializers';
|
||||||
|
|
||||||
export abstract class BrowserContext extends SdkObject {
|
export abstract class BrowserContext extends SdkObject implements network.RequestContext {
|
||||||
static Events = {
|
static Events = {
|
||||||
Console: 'console',
|
Console: 'console',
|
||||||
Close: 'close',
|
Close: 'close',
|
||||||
|
|
|
||||||
|
|
@ -41,8 +41,12 @@ import type { Playwright } from '../playwright';
|
||||||
import { SdkObject } from '../../server/instrumentation';
|
import { SdkObject } from '../../server/instrumentation';
|
||||||
import { serializeClientSideCallMetadata } from '../../utils';
|
import { serializeClientSideCallMetadata } from '../../utils';
|
||||||
import { deviceDescriptors as descriptors } from '../deviceDescriptors';
|
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;
|
_type_LocalUtils: boolean;
|
||||||
private _harBackends = new Map<string, HarBackend>();
|
private _harBackends = new Map<string, HarBackend>();
|
||||||
private _stackSessions = new Map<string, {
|
private _stackSessions = new Map<string, {
|
||||||
|
|
@ -51,6 +55,7 @@ export class LocalUtilsDispatcher extends Dispatcher<{ guid: string }, channels.
|
||||||
tmpDir: string | undefined,
|
tmpDir: string | undefined,
|
||||||
callStacks: channels.ClientSideCallMetadata[]
|
callStacks: channels.ClientSideCallMetadata[]
|
||||||
}>();
|
}>();
|
||||||
|
private _playwright: Playwright;
|
||||||
|
|
||||||
constructor(scope: RootDispatcher, playwright: Playwright) {
|
constructor(scope: RootDispatcher, playwright: Playwright) {
|
||||||
const localUtils = new SdkObject(playwright, 'localUtils', 'localUtils');
|
const localUtils = new SdkObject(playwright, 'localUtils', 'localUtils');
|
||||||
|
|
@ -59,6 +64,7 @@ export class LocalUtilsDispatcher extends Dispatcher<{ guid: string }, channels.
|
||||||
super(scope, localUtils, 'LocalUtils', {
|
super(scope, localUtils, 'LocalUtils', {
|
||||||
deviceDescriptors,
|
deviceDescriptors,
|
||||||
});
|
});
|
||||||
|
this._playwright = playwright;
|
||||||
this._type_LocalUtils = true;
|
this._type_LocalUtils = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -273,6 +279,18 @@ export class LocalUtilsDispatcher extends Dispatcher<{ guid: string }, channels.
|
||||||
await removeFolders([session.tmpDir]);
|
await removeFolders([session.tmpDir]);
|
||||||
this._stackSessions.delete(stacksId!);
|
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];
|
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 type { PageDispatcher } from './pageDispatcher';
|
||||||
import { FrameDispatcher } from './frameDispatcher';
|
import { FrameDispatcher } from './frameDispatcher';
|
||||||
import { WorkerDispatcher } from './pageDispatcher';
|
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;
|
_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);
|
const result = existingDispatcher<RequestDispatcher>(request);
|
||||||
return result || new RequestDispatcher(scope, 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;
|
return request ? RequestDispatcher.from(scope, request) : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
private constructor(scope: BrowserContextDispatcher, request: Request) {
|
private constructor(scope: NetworkScope, request: Request) {
|
||||||
const postData = request.postDataBuffer();
|
const postData = request.postDataBuffer();
|
||||||
// Always try to attach request to the page, if not, frame.
|
// Always try to attach request to the page, if not, frame.
|
||||||
const frame = request.frame();
|
const frame = request.frame();
|
||||||
const page = request.frame()?._page;
|
const page = request.frame()?._page;
|
||||||
const pageDispatcher = page ? existingDispatcher<PageDispatcher>(page) : null;
|
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', {
|
super(pageDispatcher || frameDispatcher || scope, request, 'Request', {
|
||||||
frame: FrameDispatcher.fromNullable(scope, request.frame()),
|
frame: FrameDispatcher.fromNullable(scope as BrowserContextDispatcher, request.frame()),
|
||||||
serviceWorker: WorkerDispatcher.fromNullable(scope, request.serviceWorker()),
|
serviceWorker: WorkerDispatcher.fromNullable(scope as BrowserContextDispatcher, request.serviceWorker()),
|
||||||
url: request.url(),
|
url: request.url(),
|
||||||
resourceType: request.resourceType(),
|
resourceType: request.resourceType(),
|
||||||
method: request.method(),
|
method: request.method(),
|
||||||
|
|
@ -59,7 +62,7 @@ export class RequestDispatcher extends Dispatcher<Request, channels.RequestChann
|
||||||
redirectedFrom: RequestDispatcher.fromNullable(scope, request.redirectedFrom()),
|
redirectedFrom: RequestDispatcher.fromNullable(scope, request.redirectedFrom()),
|
||||||
});
|
});
|
||||||
this._type_Request = true;
|
this._type_Request = true;
|
||||||
this._browserContextDispatcher = scope;
|
this._networkScope = scope;
|
||||||
}
|
}
|
||||||
|
|
||||||
async rawRequestHeaders(params?: channels.RequestRawRequestHeadersParams): Promise<channels.RequestRawRequestHeadersResult> {
|
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> {
|
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 {
|
export class ResponseDispatcher extends Dispatcher<Response, channels.ResponseChannel, RequestDispatcher> implements channels.ResponseChannel {
|
||||||
_type_Response = true;
|
_type_Response = true;
|
||||||
|
|
||||||
static from(scope: BrowserContextDispatcher, response: Response): ResponseDispatcher {
|
static from(scope: NetworkScope, response: Response): ResponseDispatcher {
|
||||||
const result = existingDispatcher<ResponseDispatcher>(response);
|
const result = existingDispatcher<ResponseDispatcher>(response);
|
||||||
const requestDispatcher = RequestDispatcher.from(scope, response.request());
|
const requestDispatcher = RequestDispatcher.from(scope, response.request());
|
||||||
return result || new ResponseDispatcher(requestDispatcher, response);
|
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;
|
return response ? ResponseDispatcher.from(scope, response) : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -330,7 +330,7 @@ export class FFPage implements PageDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateExtraHTTPHeaders(): Promise<void> {
|
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> {
|
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> {
|
private async _gotoAction(progress: Progress, url: string, options: types.GotoOptions): Promise<network.Response | null> {
|
||||||
const waitUntil = verifyLifecycle('waitUntil', options.waitUntil === undefined ? 'load' : options.waitUntil);
|
const waitUntil = verifyLifecycle('waitUntil', options.waitUntil === undefined ? 'load' : options.waitUntil);
|
||||||
progress.log(`navigating to "${url}", waiting until "${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');
|
const refererHeader = headers.find(h => h.name.toLowerCase() === 'referer');
|
||||||
let referer = refererHeader ? refererHeader.value : undefined;
|
let referer = refererHeader ? refererHeader.value : undefined;
|
||||||
if (options.referer !== 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.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type * as contexts from './browserContext';
|
|
||||||
import type * as pages from './page';
|
import type * as pages from './page';
|
||||||
import type * as frames from './frames';
|
import type * as frames from './frames';
|
||||||
import type * as types from './types';
|
import type * as types from './types';
|
||||||
|
|
@ -88,6 +87,13 @@ export function stripFragmentFromUrl(url: string): string {
|
||||||
return url.substring(0, url.indexOf('#'));
|
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 {
|
export class Request extends SdkObject {
|
||||||
private _response: Response | null = null;
|
private _response: Response | null = null;
|
||||||
private _redirectedFrom: Request | null;
|
private _redirectedFrom: Request | null;
|
||||||
|
|
@ -103,14 +109,14 @@ export class Request extends SdkObject {
|
||||||
private _headersMap = new Map<string, string>();
|
private _headersMap = new Map<string, string>();
|
||||||
readonly _frame: frames.Frame | null = null;
|
readonly _frame: frames.Frame | null = null;
|
||||||
readonly _serviceWorker: pages.Worker | null = null;
|
readonly _serviceWorker: pages.Worker | null = null;
|
||||||
readonly _context: contexts.BrowserContext;
|
readonly _context: RequestContext;
|
||||||
private _rawRequestHeadersPromise = new ManualPromise<HeadersArray>();
|
private _rawRequestHeadersPromise = new ManualPromise<HeadersArray>();
|
||||||
private _waitForResponsePromise = new ManualPromise<Response | null>();
|
private _waitForResponsePromise = new ManualPromise<Response | null>();
|
||||||
_responseEndTiming = -1;
|
_responseEndTiming = -1;
|
||||||
private _overrides: NormalizedContinueOverrides | undefined;
|
private _overrides: NormalizedContinueOverrides | undefined;
|
||||||
private _bodySize: number | 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) {
|
url: string, resourceType: string, method: string, postData: Buffer | null, headers: HeadersArray) {
|
||||||
super(frame || context, 'request');
|
super(frame || context, 'request');
|
||||||
assert(!url.startsWith('data:'), 'Data urls should not fire requests');
|
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;
|
export type RouteHandler = (route: Route, request: Request) => boolean;
|
||||||
|
|
||||||
type GetResponseBodyCallback = () => Promise<Buffer>;
|
export type GetResponseBodyCallback = () => Promise<Buffer>;
|
||||||
|
|
||||||
export type ResourceTiming = {
|
export type ResourceTiming = {
|
||||||
startTime: number;
|
startTime: number;
|
||||||
|
|
|
||||||
|
|
@ -366,8 +366,20 @@ export class Page extends SdkObject {
|
||||||
return this._delegate.updateExtraHTTPHeaders();
|
return this._delegate.updateExtraHTTPHeaders();
|
||||||
}
|
}
|
||||||
|
|
||||||
extraHTTPHeaders(): types.HeadersArray | undefined {
|
extraHTTPHeaders(): types.HeadersArray {
|
||||||
return this._extraHTTPHeaders;
|
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) {
|
async _onBindingCalled(payload: string, context: dom.FrameExecutionContext) {
|
||||||
|
|
|
||||||
|
|
@ -213,13 +213,20 @@ export class HttpServer {
|
||||||
readable.pipe(response);
|
readable.pipe(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _onRequest(request: http.IncomingMessage, response: http.ServerResponse) {
|
handleCORS(request: http.IncomingMessage, response: http.ServerResponse): boolean {
|
||||||
if (request.method === 'OPTIONS') {
|
if (request.method === 'OPTIONS') {
|
||||||
response.writeHead(200);
|
response.writeHead(200);
|
||||||
response.end();
|
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());
|
request.on('error', () => response.end());
|
||||||
try {
|
try {
|
||||||
if (!request.url) {
|
if (!request.url) {
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ import type { TestInfoImpl, TestStepInternal } from './worker/testInfo';
|
||||||
import { rootTestType } from './common/testType';
|
import { rootTestType } from './common/testType';
|
||||||
import type { ContextReuseMode } from './common/config';
|
import type { ContextReuseMode } from './common/config';
|
||||||
import type { ApiCallData, ClientInstrumentation, ClientInstrumentationListener } from '../../playwright-core/src/client/clientInstrumentation';
|
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 type { Playwright as PlaywrightImpl } from '../../playwright-core/src/client/playwright';
|
||||||
import { currentTestInfo } from './common/globals';
|
import { currentTestInfo } from './common/globals';
|
||||||
export { expect } from './matchers/expect';
|
export { expect } from './matchers/expect';
|
||||||
|
|
@ -56,6 +57,7 @@ type WorkerFixtures = PlaywrightWorkerArgs & PlaywrightWorkerOptions & {
|
||||||
_optionContextReuseMode: ContextReuseMode,
|
_optionContextReuseMode: ContextReuseMode,
|
||||||
_optionConnectOptions: PlaywrightWorkerOptions['connectOptions'],
|
_optionConnectOptions: PlaywrightWorkerOptions['connectOptions'],
|
||||||
_reuseContext: boolean,
|
_reuseContext: boolean,
|
||||||
|
_mockingProxy?: void,
|
||||||
_pageSnapshot: PageSnapshotOption,
|
_pageSnapshot: PageSnapshotOption,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -74,6 +76,7 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
|
||||||
screenshot: ['off', { scope: 'worker', option: true }],
|
screenshot: ['off', { scope: 'worker', option: true }],
|
||||||
video: ['off', { scope: 'worker', option: true }],
|
video: ['off', { scope: 'worker', option: true }],
|
||||||
trace: ['off', { scope: 'worker', option: true }],
|
trace: ['off', { scope: 'worker', option: true }],
|
||||||
|
mockingProxy: [undefined, { scope: 'worker', option: true }],
|
||||||
_pageSnapshot: ['off', { scope: 'worker', option: true }],
|
_pageSnapshot: ['off', { scope: 'worker', option: true }],
|
||||||
|
|
||||||
_browserOptions: [async ({ playwright, headless, channel, launchOptions }, use) => {
|
_browserOptions: [async ({ playwright, headless, channel, launchOptions }, use) => {
|
||||||
|
|
@ -121,6 +124,12 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
|
||||||
}, true);
|
}, true);
|
||||||
}, { scope: 'worker', timeout: 0 }],
|
}, { 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 }],
|
acceptDownloads: [({ contextOptions }, use) => use(contextOptions.acceptDownloads ?? true), { option: true }],
|
||||||
bypassCSP: [({ contextOptions }, use) => use(contextOptions.bypassCSP ?? false), { option: true }],
|
bypassCSP: [({ contextOptions }, use) => use(contextOptions.bypassCSP ?? false), { option: true }],
|
||||||
colorScheme: [({ contextOptions }, use) => use(contextOptions.colorScheme === undefined ? 'light' : contextOptions.colorScheme), { 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)
|
if (!keepTestTimeout)
|
||||||
currentTestInfo()?._setDebugMode();
|
currentTestInfo()?._setDebugMode();
|
||||||
},
|
},
|
||||||
runAfterCreateBrowserContext: async (context: BrowserContext) => {
|
runAfterCreateBrowserContext: async (context: BrowserContextImpl) => {
|
||||||
await artifactsRecorder?.didCreateBrowserContext(context);
|
await artifactsRecorder?.didCreateBrowserContext(context);
|
||||||
const testInfo = currentTestInfo();
|
const testInfo = currentTestInfo();
|
||||||
if (testInfo)
|
if (testInfo)
|
||||||
|
|
@ -343,7 +352,7 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
|
||||||
size: typeof video === 'string' ? undefined : video.size,
|
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: [] };
|
const contextData: { pagesWithVideo: Page[] } = { pagesWithVideo: [] };
|
||||||
contexts.set(context, contextData);
|
contexts.set(context, contextData);
|
||||||
if (captureVideo)
|
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).
|
* Learn more about [recording video](https://playwright.dev/docs/test-use-options#recording-options).
|
||||||
*/
|
*/
|
||||||
video: VideoMode | /** deprecated */ 'retry-with-video' | { mode: VideoMode, size?: ViewportSize };
|
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 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 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 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,
|
* 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 FrameChannel ? FrameInitializer :
|
||||||
T extends PageChannel ? PageInitializer :
|
T extends PageChannel ? PageInitializer :
|
||||||
T extends BrowserContextChannel ? BrowserContextInitializer :
|
T extends BrowserContextChannel ? BrowserContextInitializer :
|
||||||
T extends EventTargetChannel ? EventTargetInitializer :
|
|
||||||
T extends BrowserChannel ? BrowserInitializer :
|
T extends BrowserChannel ? BrowserInitializer :
|
||||||
T extends BrowserTypeChannel ? BrowserTypeInitializer :
|
T extends BrowserTypeChannel ? BrowserTypeInitializer :
|
||||||
T extends SelectorsChannel ? SelectorsInitializer :
|
T extends SelectorsChannel ? SelectorsInitializer :
|
||||||
|
|
@ -57,7 +56,9 @@ export type InitializerTraits<T> =
|
||||||
T extends DebugControllerChannel ? DebugControllerInitializer :
|
T extends DebugControllerChannel ? DebugControllerInitializer :
|
||||||
T extends PlaywrightChannel ? PlaywrightInitializer :
|
T extends PlaywrightChannel ? PlaywrightInitializer :
|
||||||
T extends RootChannel ? RootInitializer :
|
T extends RootChannel ? RootInitializer :
|
||||||
|
T extends MockingProxyChannel ? MockingProxyInitializer :
|
||||||
T extends LocalUtilsChannel ? LocalUtilsInitializer :
|
T extends LocalUtilsChannel ? LocalUtilsInitializer :
|
||||||
|
T extends EventTargetChannel ? EventTargetInitializer :
|
||||||
T extends APIRequestContextChannel ? APIRequestContextInitializer :
|
T extends APIRequestContextChannel ? APIRequestContextInitializer :
|
||||||
object;
|
object;
|
||||||
|
|
||||||
|
|
@ -87,7 +88,6 @@ export type EventsTraits<T> =
|
||||||
T extends FrameChannel ? FrameEvents :
|
T extends FrameChannel ? FrameEvents :
|
||||||
T extends PageChannel ? PageEvents :
|
T extends PageChannel ? PageEvents :
|
||||||
T extends BrowserContextChannel ? BrowserContextEvents :
|
T extends BrowserContextChannel ? BrowserContextEvents :
|
||||||
T extends EventTargetChannel ? EventTargetEvents :
|
|
||||||
T extends BrowserChannel ? BrowserEvents :
|
T extends BrowserChannel ? BrowserEvents :
|
||||||
T extends BrowserTypeChannel ? BrowserTypeEvents :
|
T extends BrowserTypeChannel ? BrowserTypeEvents :
|
||||||
T extends SelectorsChannel ? SelectorsEvents :
|
T extends SelectorsChannel ? SelectorsEvents :
|
||||||
|
|
@ -95,7 +95,9 @@ export type EventsTraits<T> =
|
||||||
T extends DebugControllerChannel ? DebugControllerEvents :
|
T extends DebugControllerChannel ? DebugControllerEvents :
|
||||||
T extends PlaywrightChannel ? PlaywrightEvents :
|
T extends PlaywrightChannel ? PlaywrightEvents :
|
||||||
T extends RootChannel ? RootEvents :
|
T extends RootChannel ? RootEvents :
|
||||||
|
T extends MockingProxyChannel ? MockingProxyEvents :
|
||||||
T extends LocalUtilsChannel ? LocalUtilsEvents :
|
T extends LocalUtilsChannel ? LocalUtilsEvents :
|
||||||
|
T extends EventTargetChannel ? EventTargetEvents :
|
||||||
T extends APIRequestContextChannel ? APIRequestContextEvents :
|
T extends APIRequestContextChannel ? APIRequestContextEvents :
|
||||||
undefined;
|
undefined;
|
||||||
|
|
||||||
|
|
@ -125,7 +127,6 @@ export type EventTargetTraits<T> =
|
||||||
T extends FrameChannel ? FrameEventTarget :
|
T extends FrameChannel ? FrameEventTarget :
|
||||||
T extends PageChannel ? PageEventTarget :
|
T extends PageChannel ? PageEventTarget :
|
||||||
T extends BrowserContextChannel ? BrowserContextEventTarget :
|
T extends BrowserContextChannel ? BrowserContextEventTarget :
|
||||||
T extends EventTargetChannel ? EventTargetEventTarget :
|
|
||||||
T extends BrowserChannel ? BrowserEventTarget :
|
T extends BrowserChannel ? BrowserEventTarget :
|
||||||
T extends BrowserTypeChannel ? BrowserTypeEventTarget :
|
T extends BrowserTypeChannel ? BrowserTypeEventTarget :
|
||||||
T extends SelectorsChannel ? SelectorsEventTarget :
|
T extends SelectorsChannel ? SelectorsEventTarget :
|
||||||
|
|
@ -133,7 +134,9 @@ export type EventTargetTraits<T> =
|
||||||
T extends DebugControllerChannel ? DebugControllerEventTarget :
|
T extends DebugControllerChannel ? DebugControllerEventTarget :
|
||||||
T extends PlaywrightChannel ? PlaywrightEventTarget :
|
T extends PlaywrightChannel ? PlaywrightEventTarget :
|
||||||
T extends RootChannel ? RootEventTarget :
|
T extends RootChannel ? RootEventTarget :
|
||||||
|
T extends MockingProxyChannel ? MockingProxyEventTarget :
|
||||||
T extends LocalUtilsChannel ? LocalUtilsEventTarget :
|
T extends LocalUtilsChannel ? LocalUtilsEventTarget :
|
||||||
|
T extends EventTargetChannel ? EventTargetEventTarget :
|
||||||
T extends APIRequestContextChannel ? APIRequestContextEventTarget :
|
T extends APIRequestContextChannel ? APIRequestContextEventTarget :
|
||||||
undefined;
|
undefined;
|
||||||
|
|
||||||
|
|
@ -439,6 +442,31 @@ export type APIResponse = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export type LifecycleEvent = 'load' | 'domcontentloaded' | 'networkidle' | 'commit';
|
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 -----------
|
// ----------- LocalUtils -----------
|
||||||
export type LocalUtilsInitializer = {
|
export type LocalUtilsInitializer = {
|
||||||
deviceDescriptors: {
|
deviceDescriptors: {
|
||||||
|
|
@ -473,6 +501,8 @@ export interface LocalUtilsChannel extends LocalUtilsEventTarget, Channel {
|
||||||
tracingStarted(params: LocalUtilsTracingStartedParams, metadata?: CallMetadata): Promise<LocalUtilsTracingStartedResult>;
|
tracingStarted(params: LocalUtilsTracingStartedParams, metadata?: CallMetadata): Promise<LocalUtilsTracingStartedResult>;
|
||||||
addStackToTracingNoReply(params: LocalUtilsAddStackToTracingNoReplyParams, metadata?: CallMetadata): Promise<LocalUtilsAddStackToTracingNoReplyResult>;
|
addStackToTracingNoReply(params: LocalUtilsAddStackToTracingNoReplyParams, metadata?: CallMetadata): Promise<LocalUtilsAddStackToTracingNoReplyResult>;
|
||||||
traceDiscarded(params: LocalUtilsTraceDiscardedParams, metadata?: CallMetadata): Promise<LocalUtilsTraceDiscardedResult>;
|
traceDiscarded(params: LocalUtilsTraceDiscardedParams, metadata?: CallMetadata): Promise<LocalUtilsTraceDiscardedResult>;
|
||||||
|
newRequest(params: LocalUtilsNewRequestParams, metadata?: CallMetadata): Promise<LocalUtilsNewRequestResult>;
|
||||||
|
newMockingProxy(params: LocalUtilsNewMockingProxyParams, metadata?: CallMetadata): Promise<LocalUtilsNewMockingProxyResult>;
|
||||||
}
|
}
|
||||||
export type LocalUtilsZipParams = {
|
export type LocalUtilsZipParams = {
|
||||||
zipFile: string,
|
zipFile: string,
|
||||||
|
|
@ -572,10 +602,127 @@ export type LocalUtilsTraceDiscardedOptions = {
|
||||||
|
|
||||||
};
|
};
|
||||||
export type LocalUtilsTraceDiscardedResult = void;
|
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 {
|
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 -----------
|
// ----------- Root -----------
|
||||||
export type RootInitializer = {};
|
export type RootInitializer = {};
|
||||||
export interface RootEventTarget {
|
export interface RootEventTarget {
|
||||||
|
|
@ -1260,6 +1407,7 @@ export type BrowserNewContextParams = {
|
||||||
cookies?: SetNetworkCookie[],
|
cookies?: SetNetworkCookie[],
|
||||||
origins?: SetOriginStorage[],
|
origins?: SetOriginStorage[],
|
||||||
},
|
},
|
||||||
|
mockingProxyBaseURL?: string,
|
||||||
};
|
};
|
||||||
export type BrowserNewContextOptions = {
|
export type BrowserNewContextOptions = {
|
||||||
noDefaultViewport?: boolean,
|
noDefaultViewport?: boolean,
|
||||||
|
|
@ -1327,6 +1475,7 @@ export type BrowserNewContextOptions = {
|
||||||
cookies?: SetNetworkCookie[],
|
cookies?: SetNetworkCookie[],
|
||||||
origins?: SetOriginStorage[],
|
origins?: SetOriginStorage[],
|
||||||
},
|
},
|
||||||
|
mockingProxyBaseURL?: string,
|
||||||
};
|
};
|
||||||
export type BrowserNewContextResult = {
|
export type BrowserNewContextResult = {
|
||||||
context: BrowserContextChannel,
|
context: BrowserContextChannel,
|
||||||
|
|
@ -1501,31 +1650,6 @@ export interface BrowserEvents {
|
||||||
'close': BrowserCloseEvent;
|
'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 -----------
|
// ----------- BrowserContext -----------
|
||||||
export type BrowserContextInitializer = {
|
export type BrowserContextInitializer = {
|
||||||
isChromium: boolean,
|
isChromium: boolean,
|
||||||
|
|
|
||||||
|
|
@ -584,6 +584,78 @@ ContextOptions:
|
||||||
- allow
|
- allow
|
||||||
- block
|
- 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:
|
LocalUtils:
|
||||||
type: interface
|
type: interface
|
||||||
|
|
||||||
|
|
@ -705,6 +777,52 @@ LocalUtils:
|
||||||
parameters:
|
parameters:
|
||||||
stacksId: string
|
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:
|
Root:
|
||||||
type: interface
|
type: interface
|
||||||
|
|
||||||
|
|
@ -745,52 +863,7 @@ Playwright:
|
||||||
commands:
|
commands:
|
||||||
newRequest:
|
newRequest:
|
||||||
parameters:
|
parameters:
|
||||||
baseURL: string?
|
$mixin: NewRequestParameters
|
||||||
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?
|
|
||||||
|
|
||||||
returns:
|
returns:
|
||||||
request: APIRequestContext
|
request: APIRequestContext
|
||||||
|
|
||||||
|
|
@ -1023,6 +1096,7 @@ Browser:
|
||||||
origins:
|
origins:
|
||||||
type: array?
|
type: array?
|
||||||
items: SetOriginStorage
|
items: SetOriginStorage
|
||||||
|
mockingProxyBaseURL: string?
|
||||||
returns:
|
returns:
|
||||||
context: BrowserContext
|
context: BrowserContext
|
||||||
|
|
||||||
|
|
@ -1088,29 +1162,6 @@ ConsoleMessage:
|
||||||
lineNumber: number
|
lineNumber: number
|
||||||
columnNumber: 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:
|
BrowserContext:
|
||||||
type: interface
|
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'>;
|
screenshot: ScreenshotMode | { mode: ScreenshotMode } & Pick<PageScreenshotOptions, 'fullPage' | 'omitBackground'>;
|
||||||
trace: TraceMode | /** deprecated */ 'retry-with-trace' | { mode: TraceMode, snapshots?: boolean, screenshots?: boolean, sources?: boolean, attachments?: boolean };
|
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 };
|
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 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 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 VideoMode = 'off' | 'on' | 'retain-on-failure' | 'on-first-retry';
|
||||||
|
export type MockingProxyMode = 'off' | 'inject-via-header';
|
||||||
|
|
||||||
export interface PlaywrightTestOptions {
|
export interface PlaywrightTestOptions {
|
||||||
acceptDownloads: boolean;
|
acceptDownloads: boolean;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue