This commit is contained in:
parent
b66873dfdb
commit
eae7bde3ac
|
|
@ -871,3 +871,13 @@ page.WebSocket += (_, ws) =>
|
|||
- [`event: WebSocket.close`]
|
||||
|
||||
<br/>
|
||||
|
||||
## Missing Network Events and Service Workers
|
||||
|
||||
Playwright's built-in [`method: BrowserContext.route`] and [`method: Page.route`] allow your tests to natively route requests and perform mocking and interception.
|
||||
|
||||
1. If you're using Playwright's native [`method: BrowserContext.route`] and [`method: Page.route`], and it appears network events are missing, disable Service Workers by setting [`option: Browser.newContext.serviceWorkers`] to `'block'`.
|
||||
1. It might be that you are using a mock tool such as Mock Service Worker (MSW). While this tool works out of the box for mocking responses, it adds its own Service Worker that takes over the network requests, hence making them invisible to [`method: BrowserContext.route`] and [`method: Page.route`]. If you are interested in both network testing and mocking, consider using built-in [`method: BrowserContext.route`] and [`method: Page.route`] for [response mocking](#handle-requests).
|
||||
1. If you're interested in not solely using Service Workers for testing and network mocking, but in routing and listening for requests made by Service Workers themselves, please see [this experimental feature](https://github.com/microsoft/playwright/issues/15684).
|
||||
|
||||
<br/>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
---
|
||||
id: service-workers
|
||||
title: "Service Workers Guide"
|
||||
id: service-workers-experimental
|
||||
title: "(Experimental) Service Worker Network Events"
|
||||
---
|
||||
|
||||
:::warning
|
||||
If you're looking to do general network mocking, routing, and interception, please see the [Network Guide](./network.md) first. Playwright provides built-in APIs for this use case that don't require the information below. However, if you're interested in requests made by Service Workers themselves, please read below.
|
||||
:::
|
||||
|
||||
[Service Workers](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API) provide a browser-native method of handling requests made by a page with the native [Fetch API (`fetch`)](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) along with other network-requested assets (like scripts, css, and images).
|
||||
|
||||
|
|
@ -10,35 +13,31 @@ They can act as a **network proxy** between the page and the external network to
|
|||
|
||||
Many sites that use Service Workers simply use them as a transparent optimization technique. While users might notice a faster experience, the app's implementation is unaware of their existence. Running the app with or without Service Workers enabled appears functionally equivalent.
|
||||
|
||||
**If your app uses Service Workers**, here's the scenarios that Playwright supports:
|
||||
## How to Enable
|
||||
|
||||
1. Testing the page exactly like a user would experience it. This works out of the box in all supported browsers.
|
||||
1. Test your page without a Service Worker. Set [`option: Browser.newContext.serviceWorkers`] to `'block'`. You can test your page as if no Service Worker was registered.
|
||||
1. Listen for and route network traffic via Playwright, whether it comes from a Service Worker or not. In Firefox and WebKit, set [`option: Browser.newContext.serviceWorkers`] to `'block'` to avoid Service Worker network traffic entirely. In Chromium, either block Service Workers or use [`method: BrowserContext.route`].
|
||||
1. (Chromium-only) Test your Service Worker implementation itself. Use [`method: BrowserContext.serviceWorkers`] to get access to the Service Worker and evaluate there.
|
||||
Playwright's inspection and routing of requests made by Service Workers are **experimental** and disabled by default.
|
||||
|
||||
Set the `PW_EXPERIMENTAL_SERVICE_WORKER_NETWORK_EVENTS` environment variable to `1` (or any other value) to enable the feature. Only Chrome/Chromium are currently supported.
|
||||
|
||||
If you're using (or are interested in using this this feature), please comment on [this issue](https://github.com/microsoft/playwright/issues/15684) letting us know your use case.
|
||||
|
||||
## Service Worker Fetch
|
||||
|
||||
:::note
|
||||
The next sections are only currently supported when using Playwright with Chrome/Chromium. In Firefox and WebKit, if a Service Worker has a FetchEvent handler, Playwright will **not** emit Network events for all network traffic.
|
||||
:::
|
||||
|
||||
### Accessing Service Workers and Waiting for Activation
|
||||
|
||||
You can use [`method: BrowserContext.serviceWorkers`] to list the Service [Worker]s, or specifically watch for the Service [Worker] if you anticipate a page will trigger its [registration](https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerContainer/register):
|
||||
|
||||
```js tab=js-ts
|
||||
const [ serviceworker ] = await Promise.all([
|
||||
context.waitForEvent('serviceworker'),
|
||||
page.goto('/example-with-a-service-worker.html'),
|
||||
const [serviceworker] = await Promise.all([
|
||||
context.waitForEvent("serviceworker"),
|
||||
page.goto("/example-with-a-service-worker.html"),
|
||||
]);
|
||||
```
|
||||
|
||||
```js tab=js-js
|
||||
const [ serviceworker ] = await Promise.all([
|
||||
context.waitForEvent('serviceworker'),
|
||||
page.goto('/example-with-a-service-worker.html'),
|
||||
const [serviceworker] = await Promise.all([
|
||||
context.waitForEvent("serviceworker"),
|
||||
page.goto("/example-with-a-service-worker.html"),
|
||||
]);
|
||||
```
|
||||
|
||||
|
|
@ -66,25 +65,27 @@ Worker serviceWorker = page.waitForRequest(() -> {
|
|||
});
|
||||
```
|
||||
|
||||
[`event: BrowserContext.serviceWorker`] is fired ***before*** the Service Worker's main script has been evaluated, so ***before*** calling service[`method: Worker.evaluate`] you should wait on its activation.
|
||||
[`event: BrowserContext.serviceWorker`] is fired **_before_** the Service Worker's main script has been evaluated, so **_before_** calling service[`method: Worker.evaluate`] you should wait on its activation.
|
||||
|
||||
There are more iodiomatic methods of waiting for a Service Worker to be activated, but the following is an implementation agnostic method:
|
||||
|
||||
```js tab=js-ts
|
||||
await page.evaluate(async () => {
|
||||
const registration = await window.navigator.serviceWorker.getRegistration();
|
||||
if (registration.active?.state === 'activated')
|
||||
return;
|
||||
await new Promise(res => window.navigator.serviceWorker.addEventListener('controllerchange', res));
|
||||
if (registration.active?.state === "activated") return;
|
||||
await new Promise((res) =>
|
||||
window.navigator.serviceWorker.addEventListener("controllerchange", res)
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
```js tab=js-js
|
||||
await page.evaluate(async () => {
|
||||
const registration = await window.navigator.serviceWorker.getRegistration();
|
||||
if (registration.active?.state === 'activated')
|
||||
return;
|
||||
await new Promise(res => window.navigator.serviceWorker.addEventListener('controllerchange', res));
|
||||
if (registration.active?.state === "activated") return;
|
||||
await new Promise((res) =>
|
||||
window.navigator.serviceWorker.addEventListener("controllerchange", res)
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
|
|
@ -130,18 +131,18 @@ page.evaluate(
|
|||
|
||||
Any network request made by the **Service Worker** will have:
|
||||
|
||||
* [`event: BrowserContext.request`] and its correponding events ([`event: BrowserContext.requestFinished`] and [`event: BrowserContext.response`], or [`event: BrowserContext.requestFailed`])
|
||||
* [`method: BrowserContext.route`] will see the request
|
||||
* [`method: Request.serviceWorker`] will be set to the Service [Worker] instance, and [`method: Request.frame`] will **throw**
|
||||
* [`method: Response.fromServiceWorker`] will return `false`
|
||||
- [`event: BrowserContext.request`] and its correponding events ([`event: BrowserContext.requestFinished`] and [`event: BrowserContext.response`], or [`event: BrowserContext.requestFailed`])
|
||||
- [`method: BrowserContext.route`] will see the request
|
||||
- [`method: Request.serviceWorker`] will be set to the Service [Worker] instance, and [`method: Request.frame`] will **throw**
|
||||
- [`method: Response.fromServiceWorker`] will return `false`
|
||||
|
||||
Additionally, any network request made by the **Page** (including its sub-[Frame]s) will have:
|
||||
|
||||
* [`event: BrowserContext.request`] and its correponding events ([`event: BrowserContext.requestFinished`] and [`event: BrowserContext.response`], or [`event: BrowserContext.requestFailed`])
|
||||
* [`event: Page.request`] and its correponding events ([`event: Page.requestFinished`] and [`event: Page.response`], or [`event: Page.requestFailed`])
|
||||
* [`method: Page.route`] and [`method: Page.route`] will **not** see the request (if a Service Worker's fetch handler was registered)
|
||||
* [`method: Request.serviceWorker`] will be set to `null`, and [`method: Request.frame`] will return the [Frame]
|
||||
* [`method: Response.fromServiceWorker`] will return `true` (if a Service Worker's fetch handler was registered)
|
||||
- [`event: BrowserContext.request`] and its correponding events ([`event: BrowserContext.requestFinished`] and [`event: BrowserContext.response`], or [`event: BrowserContext.requestFailed`])
|
||||
- [`event: Page.request`] and its correponding events ([`event: Page.requestFinished`] and [`event: Page.response`], or [`event: Page.requestFailed`])
|
||||
- [`method: Page.route`] and [`method: Page.route`] will **not** see the request (if a Service Worker's fetch handler was registered)
|
||||
- [`method: Request.serviceWorker`] will be set to `null`, and [`method: Request.frame`] will return the [Frame]
|
||||
- [`method: Response.fromServiceWorker`] will return `true` (if a Service Worker's fetch handler was registered)
|
||||
|
||||
Many Service Worker implementations simply execute the request from the page (possibly with some custom caching/offline logic omitted for simplicity):
|
||||
|
||||
|
|
@ -164,14 +165,16 @@ If a page registers the above Service Worker:
|
|||
```html
|
||||
<!-- filename: index.html -->
|
||||
<script>
|
||||
window.registrationPromise = navigator.serviceWorker.register('/transparent-service-worker.js');
|
||||
window.registrationPromise = navigator.serviceWorker.register(
|
||||
"/transparent-service-worker.js"
|
||||
);
|
||||
</script>
|
||||
```
|
||||
|
||||
On the first visit to the page via [`method: Page.goto`], the following Request/Response events would be emitted (along with the corresponding network lifecycle events):
|
||||
|
||||
| Event | Owner | URL | Routed | [`method: Response.fromServiceWorker`] |
|
||||
| - | - | - | - | - |
|
||||
| --------------------------------- | ---------------- | ----------------------------- | ------ | -------------------------------------- |
|
||||
| [`event: BrowserContext.request`] | [Frame] | index.html | Yes | |
|
||||
| [`event: Page.request`] | [Frame] | index.html | Yes | |
|
||||
| [`event: BrowserContext.request`] | Service [Worker] | transparent-service-worker.js | Yes | |
|
||||
|
|
@ -179,25 +182,21 @@ On the first visit to the page via [`method: Page.goto`], the following Request/
|
|||
| [`event: BrowserContext.request`] | [Frame] | data.json | | Yes |
|
||||
| [`event: Page.request`] | [Frame] | data.json | | Yes |
|
||||
|
||||
|
||||
|
||||
Since the example Service Worker just acts a basic transparent "proxy":
|
||||
|
||||
* There's 2 [`event: BrowserContext.request`] events for `data.json`; one [Frame]-owned, the other Service [Worker]-owned.
|
||||
* Only the Service [Worker]-owned request for the resource was routable via [`method: BrowserContext.route`]; the [Frame]-owned events for `data.json` are not routeable, as they would not have even had the possibility to hit the external network since the Service Worker has a fetch handler registered.
|
||||
- There's 2 [`event: BrowserContext.request`] events for `data.json`; one [Frame]-owned, the other Service [Worker]-owned.
|
||||
- Only the Service [Worker]-owned request for the resource was routable via [`method: BrowserContext.route`]; the [Frame]-owned events for `data.json` are not routeable, as they would not have even had the possibility to hit the external network since the Service Worker has a fetch handler registered.
|
||||
|
||||
:::caution
|
||||
It's important to note: calling [`method: Request.frame`] or [`method: Response.frame`] will **throw** an exception, if called on a [Request]/[Response] that has a non-null [`method: Request.serviceWorker`].
|
||||
:::
|
||||
|
||||
|
||||
#### Advanced Example
|
||||
|
||||
When a Service Worker handles a page's request, the Service Worker can make 0 to n requests to the external network. The Service Worker might respond directly from a cache, generate a reponse in memory, rewrite the request, make two requests and then combine into 1, etc.
|
||||
|
||||
Consider the code snippets below to understand Playwright's view into the Request/Responses and how it impacts routing in some of these cases.
|
||||
|
||||
|
||||
```js
|
||||
// filename: complex-service-worker.js
|
||||
self.addEventListener("install", function (event) {
|
||||
|
|
@ -243,14 +242,16 @@ And a page that simply registers the Service Worker:
|
|||
```html
|
||||
<!-- filename: index.html -->
|
||||
<script>
|
||||
window.registrationPromise = navigator.serviceWorker.register('/complex-service-worker.js');
|
||||
window.registrationPromise = navigator.serviceWorker.register(
|
||||
"/complex-service-worker.js"
|
||||
);
|
||||
</script>
|
||||
```
|
||||
|
||||
On the first visit to the page via [`method: Page.goto`], the following Request/Response events would be emitted:
|
||||
|
||||
| Event | Owner | URL | Routed | [`method: Response.fromServiceWorker`] |
|
||||
| - | - | - | - | - |
|
||||
| --------------------------------- | ---------------- | ------------------------- | ------ | -------------------------------------- |
|
||||
| [`event: BrowserContext.request`] | [Frame] | index.html | Yes | |
|
||||
| [`event: Page.request`] | [Frame] | index.html | Yes | |
|
||||
| [`event: BrowserContext.request`] | Service [Worker] | complex-service-worker.js | Yes | |
|
||||
|
|
@ -261,17 +262,17 @@ It's important to note that [`cache.add`](https://developer.mozilla.org/en-US/do
|
|||
Once the Service Worker is activated and handling FetchEvents, if the page makes the following requests:
|
||||
|
||||
```js tab=js-ts
|
||||
await page.evaluate(() => fetch('/addressbook.json'));
|
||||
await page.evaluate(() => fetch('/foo'));
|
||||
await page.evaluate(() => fetch('/tracker.js'));
|
||||
await page.evaluate(() => fetch('/fallthrough.txt'));
|
||||
await page.evaluate(() => fetch("/addressbook.json"));
|
||||
await page.evaluate(() => fetch("/foo"));
|
||||
await page.evaluate(() => fetch("/tracker.js"));
|
||||
await page.evaluate(() => fetch("/fallthrough.txt"));
|
||||
```
|
||||
|
||||
```js tab=js-js
|
||||
await page.evaluate(() => fetch('/addressbook.json'));
|
||||
await page.evaluate(() => fetch('/foo'));
|
||||
await page.evaluate(() => fetch('/tracker.js'));
|
||||
await page.evaluate(() => fetch('/fallthrough.txt'));
|
||||
await page.evaluate(() => fetch("/addressbook.json"));
|
||||
await page.evaluate(() => fetch("/foo"));
|
||||
await page.evaluate(() => fetch("/tracker.js"));
|
||||
await page.evaluate(() => fetch("/fallthrough.txt"));
|
||||
```
|
||||
|
||||
```python async
|
||||
|
|
@ -305,7 +306,7 @@ page.evaluate("fetch('/fallthrough.txt')")
|
|||
The following Request/Response events would be emitted:
|
||||
|
||||
| Event | Owner | URL | Routed | [`method: Response.fromServiceWorker`] |
|
||||
| - | - | - | - | - |
|
||||
| --------------------------------- | ---------------- | ---------------- | ------ | -------------------------------------- |
|
||||
| [`event: BrowserContext.request`] | [Frame] | addressbook.json | | Yes |
|
||||
| [`event: Page.request`] | [Frame] | addressbook.json | | Yes |
|
||||
| [`event: BrowserContext.request`] | Service [Worker] | bar | Yes | |
|
||||
|
|
@ -319,19 +320,19 @@ The following Request/Response events would be emitted:
|
|||
|
||||
It's important to note:
|
||||
|
||||
* The page requested `/foo`, but the Service Worker requested `/bar`, so there are only [Frame]-owned events for `/foo`, but not `/bar`.
|
||||
* Likewise, the Service Worker never hit the network for `tracker.js`, so ony [Frame]-owned events were emitted for that request.
|
||||
- The page requested `/foo`, but the Service Worker requested `/bar`, so there are only [Frame]-owned events for `/foo`, but not `/bar`.
|
||||
- Likewise, the Service Worker never hit the network for `tracker.js`, so ony [Frame]-owned events were emitted for that request.
|
||||
|
||||
## Routing Service Worker Requests Only
|
||||
|
||||
```js tab=js-ts
|
||||
await context.route('**', async route => {
|
||||
await context.route("**", async (route) => {
|
||||
if (route.request().serviceWorker()) {
|
||||
// NB: calling route.request().frame() here would THROW
|
||||
return route.fulfill({
|
||||
contentType: 'text/plain',
|
||||
contentType: "text/plain",
|
||||
status: 200,
|
||||
body: 'from sw',
|
||||
body: "from sw",
|
||||
});
|
||||
} else {
|
||||
return route.continue();
|
||||
|
|
@ -340,13 +341,13 @@ await context.route('**', async route => {
|
|||
```
|
||||
|
||||
```js tab=js-js
|
||||
await context.route('**', async route => {
|
||||
await context.route("**", async (route) => {
|
||||
if (route.request().serviceWorker()) {
|
||||
// NB: calling route.request().frame() here would THROW
|
||||
return route.fulfill({
|
||||
contentType: 'text/plain',
|
||||
contentType: "text/plain",
|
||||
status: 200,
|
||||
body: 'from sw',
|
||||
body: "from sw",
|
||||
});
|
||||
} else {
|
||||
return route.continue();
|
||||
|
|
@ -406,4 +407,3 @@ browserContext.route("**", route -> {
|
|||
## Known Limitations
|
||||
|
||||
Requests for updated Service Worker main script code currently cannot be routed (https://github.com/microsoft/playwright/issues/14711).
|
||||
|
||||
|
|
@ -367,7 +367,10 @@ export class CRNetworkManager {
|
|||
// For frame-level Requests that are handled by a Service Worker's fetch handler, we'll never get a requestPaused event, so we need to
|
||||
// manually create the request. In an ideal world, crNetworkManager would be able to know this on Network.requestWillBeSent, but there
|
||||
// is not enough metadata there.
|
||||
if (!request && event.response.fromServiceWorker) {
|
||||
//
|
||||
// PW_EXPERIMENTAL_SERVICE_WORKER_NETWORK_EVENTS we guard with, since this would fix an old bug where, when using routing,
|
||||
// request would not be emitted to the user for requests made by a page with a SW (and fetch handler) registered
|
||||
if (!!process.env.PW_EXPERIMENTAL_SERVICE_WORKER_NETWORK_EVENTS && !request && event.response.fromServiceWorker) {
|
||||
const requestWillBeSentEvent = this._requestIdToRequestWillBeSentEvent.get(event.requestId);
|
||||
const frame = requestWillBeSentEvent?.frameId ? this._page?._frameManager.frame(requestWillBeSentEvent.frameId) : null;
|
||||
if (requestWillBeSentEvent && frame) {
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ import { headersArrayToObject } from '../../utils';
|
|||
|
||||
export class CRServiceWorker extends Worker {
|
||||
readonly _browserContext: CRBrowserContext;
|
||||
readonly _networkManager: CRNetworkManager;
|
||||
readonly _networkManager?: CRNetworkManager;
|
||||
private _session: CRSession;
|
||||
private _extraHTTPHeaders: types.HeadersArray | null = null;
|
||||
|
||||
|
|
@ -33,12 +33,13 @@ export class CRServiceWorker extends Worker {
|
|||
super(browserContext, url);
|
||||
this._session = session;
|
||||
this._browserContext = browserContext;
|
||||
if (!!process.env.PW_EXPERIMENTAL_SERVICE_WORKER_NETWORK_EVENTS)
|
||||
this._networkManager = new CRNetworkManager(session, null, this, null);
|
||||
session.once('Runtime.executionContextCreated', event => {
|
||||
this._createExecutionContext(new CRExecutionContext(session, event.context));
|
||||
});
|
||||
|
||||
if (this._isNetworkInspectionEnabled()) {
|
||||
if (this._networkManager && this._isNetworkInspectionEnabled()) {
|
||||
this._networkManager.initialize().catch(() => {});
|
||||
this.updateRequestInterception();
|
||||
this.updateExtraHTTPHeaders(true);
|
||||
|
|
@ -56,7 +57,7 @@ export class CRServiceWorker extends Worker {
|
|||
|
||||
const offline = !!this._browserContext._options.offline;
|
||||
if (!initial || offline)
|
||||
await this._networkManager.setOffline(offline);
|
||||
await this._networkManager?.setOffline(offline);
|
||||
}
|
||||
|
||||
async updateHttpCredentials(initial: boolean): Promise<void> {
|
||||
|
|
@ -65,7 +66,7 @@ export class CRServiceWorker extends Worker {
|
|||
|
||||
const credentials = this._browserContext._options.httpCredentials || null;
|
||||
if (!initial || credentials)
|
||||
await this._networkManager.authenticate(credentials);
|
||||
await this._networkManager?.authenticate(credentials);
|
||||
}
|
||||
|
||||
async updateExtraHTTPHeaders(initial: boolean): Promise<void> {
|
||||
|
|
@ -81,7 +82,7 @@ export class CRServiceWorker extends Worker {
|
|||
}
|
||||
|
||||
updateRequestInterception(): Promise<void> {
|
||||
if (!this._isNetworkInspectionEnabled())
|
||||
if (!this._networkManager || !this._isNetworkInspectionEnabled())
|
||||
return Promise.resolve();
|
||||
|
||||
return this._networkManager.setRequestInterception(this.needsRequestInterception()).catch(e => { });
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue