Issue 1:
System info

Playwright Version: 1.31.2

Operating System: MacOS 13.2.1

Browser: Chromium

Other info:

Source code

I provided exact source code that allows reproducing the issue locally.

Config file

// playwright.config.ts
import { defineConfig } from "@playwright/test";

export default defineConfig({
timeout: 15 * 60 * 1000, // 15 minutes
use: {
acceptDownloads: true,
actionTimeout: 5 * 1000, // 5 seconds
trace: "retain-on-failure",
},
workers: 5,
fullyParallel: true,
});

Test file (self-contained)

it('should timeout after 3 seconds', async ({ page }) => {
page.goto("http://localhost:3000/");
page.setDefaultTimeout(3 * 1000); // 3 seconds
await page.waitForSelector('text="something not in the dom"')
});

Steps

Run the test

Observe that it waits until the global test timeout of 15 minutes elapses

Expected

According to the docs, the page.setDefaultTimeout value of 3 seconds should kick in here.

Also, according to this issue: #13723, the actionTimeout of 5 seconds should also apply.

Actual

The only way I've been able to get this to timeout as expected is by passing the option directly to waitForSelector.

Issue 2:
System info

Playwright Version: playwright-chromium@1.31.2

Operating System: macOS 13.1

Browser: Chrome 110.0.5481.77 (Official Build) (arm64)

Other info:

Source code

I provided exact source code that allows reproducing the issue locally.

script.ts:

import { Browser, BrowserContext, chromium, Page } from "playwright-chromium";
import * as path from "path";
import * as url from "url";

async function intercept(page: Page) {
const context = page.context();

const client = await context.newCDPSession(page);

await client.send("Fetch.enable", {
"patterns": [
{
"requestStage": "Response",
"urlPattern": "*"
}
]
});

client.on("Fetch.requestPaused", async ({ requestId, request }) => {
const { hostname, pathname } = new URL(request.url);

try {
const { body } = await client.send("Fetch.getResponseBody", {
"requestId": requestId
});

let baseName = path.basename(pathname);
const pathName = path.join(pathname.slice(0, -baseName.length));
const fileName = path.join(hostname, pathName, baseName || "index.html");
baseName ||= path.basename(fileName);

console.log("Saving " + request.url + " as " + baseName);
} catch (error) {
console.error("Failed to get " + request.url);
} finally {
await client.send("Fetch.continueRequest", {
"requestId": requestId
});
}
});
}

let browser: Browser;

let context: BrowserContext;

export async function init() {
browser ??= await chromium.launch({
"args": ["--disable-web-security"],
"devtools": true,
"headless": false //!(Boolean(process.env["CI"]) || process.platform === "win32" || Boolean(process.env["DISPLAY"]))
});

context ??= await browser.newContext();

const page = await context.newPage();

await intercept(page);

await page.goto(url.pathToFileURL(path.resolve("./index.html")).toString());
}

init();

index.html:

<!DOCTYPE html>
<html>
<head>
<meta name="description" content="" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body>
<iframe id="nodebox-iframe"></iframe>
<iframe id="preview-iframe"></iframe>
<script type="module">
import { Nodebox } from "https://cdn.jsdelivr.net/npm/@codesandbox/nodebox/build/index.min.mjs";

const nodeboxIframe = document.getElementById("nodebox-iframe");
const previewIframe = document.getElementById("preview-iframe");

const nodebox = new Nodebox({
"iframe": nodeboxIframe
});

await nodebox.connect();

await nodebox.fs.init({
"index.js": `
// server.ts
import * as http from "http";

// server.ts
var BASE_URL = "http://localhost:8000";
var router = {
"/": function(request, response) {
response.writeHead(200, { "Content-Type": "text/html" });
response.end("OK");
}
};
var server = http.createServer(function(request, response) {
const startTime = performance.now();
let status = 200;
try {
if (request.method === "GET" && router[request.url] !== void 0) {
router[request.url](request, response);
} else {
status = 404;
response.writeHead(status, { "Content-Type": "text/plain" });
response.end("File not found");
}
} catch (error) {
status = 500;
response.writeHead(status, { "Content-Type": "text/plain" });
response.end("Internal server error");
}
response.on("finish", function() {
console.log(request.method, request.url, status, (performance.now() - startTime).toFixed(3), "ms");
});
});
server.listen(new URL(BASE_URL).port, function() {
console.log("> Ready on " + BASE_URL);
});
`
});

const shell = nodebox.shell.create();

const { id } = await shell.runCommand("node", ["index.js"]);

const { url } = await nodebox.preview.getByShellId(id);
previewIframe.setAttribute("src", url);
</script>
</body>
</html>

package.json:

{
"dependencies": {
"playwright-chromium": "latest",
"ts-node": "latest",
"typescript": "latest"
},
"scripts": {
"start": "node --experimental-specifier-resolution=node --loader=ts-node/esm script.ts"
}
}

Expected

Capture all network traffic, including:

requests originating from iframes,

nested iframes,

service workers

Actual

Output:

Saving ./playwright-issue/index.html as index.html
Saving https://cdn.jsdelivr.net/npm/@codesandbox/nodebox/build/index.min.mjs as index.min.mjs
Saving ./playwright-issue/index.html as index.html
Saving https://nodebox-runtime.codesandbox.io/ as index.html

Less than what you would expect given a look at the network panel in the Chrome DevTools.

I'm going to compile some more of what I came across while researching this issue and what I tried below, but it seemed like this has been asked frequently enough with no particularly clear, working solution that I hope to at least document it better.

https://stackoverflow.com/questions/70645680/does-chromium-support-intercept-webworker-requests-via-cdp

After digging in puppeteer source code and tracing raw protocol messages, it seems that calling page.setRequestInterception(true) also intercepts WebWorker requests, but these requests never issue any Network.requestWillBeSent events, which is known as page.request events in puppeteer, then WebWorker requests hang for waiting for a request.continue() which is usually called in the page.request event handler.

Mentions using Target.attachedToTarget.

https://groups.google.com/a/chromium.org/g/chromium-extensions/c/3AnHiXwNO4o?pli=1

Someone experiencing the same issue. Notes that they tried sending Target.setAutoAttach, which didn't appear to change anything for me. Also notes that they couldn't get Target.targetCreated to fire, which I also encountered.

https://stackoverflow.com/questions/63357370/capture-requests-xhr-js-css-from-embedded-iframes-using-devtool-protocol

Issue could be worked around by using the --disable-features flag. Linked resources also talks about how setAutoAttach shouldn't be relied on.

puppeteer/puppeteer#2781 (comment)

This is what my current code is based on, although I couldn't get browser.on('targetcreated') to fire. Someone towards the bottom mentions serviceWorkerTarget.setRequestInterception.

#15684

Currently, Playwright supports an experimental feature to inspect and route Network traffic made by Service Workers (in Chrome / Chromium). To learn more and enable, please read: https://playwright.dev/docs/service-workers-experimental.

Looks like there is some experimental Service Worker-specific network events that I could enable.

#1090 (comment)

Further confirmation that I should be using the experimental Service Worker-specific network events (this section in particular).

puppeteer/puppeteer#3667 (comment)

Playwright seems to have worked around this/provided a fix: #1226

Something about iframes and auto-attaching? It sounds like with this PR, frames should auto-attach. The PR talks about frames as being popups, but surely it should apply to anything that creates a new frame.

puppeteer/puppeteer#8991

Another suggestion to use setRequestInterception, which is deprecated and suggests you use Fetch.enable instead. I don't fully understand how I would programmatically attach to all of the frames.

That's everything that seems relevant.