Issue 1:
System info

Playwright Version: v1.31.0

Operating System: macOS 13.2.1 Chip Apple M2

Browser: Chromium

Other info:

Source code

Playwright Server:

import * as playwright from "playwright";

export type BrowserTypes = "chromium" | "firefox" | "webkit";

export const launchOptions: playwright.LaunchOptions = {};
const chromiumDefaultArgs = [
"--disable-gpu",
"--disable-dev-shm-usage",
"--disable-setuid-sandbox",
"--no-sandbox",
"--no-zygote",
"--single-process",
];

const getLaunchOptions = (browser: BrowserTypes): playwright.LaunchOptions => {
let newLaunchOptions = {
...launchOptions,
};
if (browser === "chromium") {
newLaunchOptions = {
...newLaunchOptions,
args: [
...(launchOptions.args ? launchOptions.args : []),
...chromiumDefaultArgs,
],
};
}

return newLaunchOptions;
};

interface BrowserInstance {
[endPoint: string]: {
server: playwright.BrowserServer;
timer?: any;
browser: string;
guid: string;
};
}

class BrowserServer {
instances: BrowserInstance = {};

async launchServer(
browser: BrowserTypes,
socket: net.Socket
): Promise<playwright.BrowserServer> {
const server = await playwright[browser].launchServer(
getLaunchOptions(browser)
);

if (!server) {
return null;
}

const endPoint = server.wsEndpoint();

const guid = /((\w{4,12}-?)){5}/.exec(endPoint)[0];

this.instances[endPoint] = {
server,
browser,
guid,
};

socket.on("error", (err: any) => {
console.log(`ERROR: ${browser} - ${endPoint}`);
console.error(err);
});

socket.on("end", () => {
console.log("Socket ended successfully");
});

socket.on("close", async () => {
await this.kill(server);
});

const timeout =
process.env[BROWSER_SERVER_TIMEOUT] &&
Number.parseInt(process.env[BROWSER_SERVER_TIMEOUT]);

if (timeout) {
console.log(`Browser will close in ${timeout} seconds`);
}

return server;
}

getWsEndPoint(server: playwright.BrowserServer): string {
return server.wsEndpoint();
}

async killAll(): Promise<void> {
const { instances } = this;

for (const key in instances) {
const info = instances[key];
await this.kill(info.server);
}
}

async kill(server: playwright.BrowserServer): Promise<void> {
const endPoint = server.wsEndpoint();

// If instance is undefined then its already in the process of terminating.
if (!this.instances[endPoint]) {
return;
}

const { browser, guid } = this.instances[endPoint];
clearTimeout(this.instances[endPoint].timer);
console.log(`Terminating ${browser} (${guid})...`);
delete this.instances[endPoint];
await server.close();
console.log(`${browser} terminiated (${guid}).`);
}
}

export default BrowserServer;

Docker Server: some-registry/playwright-server:latest

FROM mcr.microsoft.com/playwright:v1.31.0-focal

ENV NODE_ENV=production

# Run everything after as non-privileged user.
USER pwuser

ENV PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1

ARG NPM_LOGLEVEL=info

# Create app directory
WORKDIR /home/pwuser

COPY package.json .

RUN npm install --loglevel ${NPM_LOGLEVEL} --force

COPY . .

CMD ["node", "build/src/main.js"]

EXPOSE 3000/tcp
EXPOSE 5000/tcp

Config file

// playwright.config.ts
import { Page } from "playwright-core";
import { expect, PlaywrightTestConfig, devices } from "@playwright/test";

const config: PlaywrightTestConfig = {
globalSetup: require.resolve("./playwright.global-setup"),
forbidOnly: !!process.env.CI,
timeout: 50000,
testMatch: /.*\.spec\.ts/,
testDir: "../../packages",
outputDir: "./test-results",
retries: process.env.CI ? 2 : 0,
reporter: [["html", { open: "never" }], process.env.CI ? ["dot"] : ["list"]],
webServer: [
{
command: "npm run serve",
port: 5000,
timeout: 50000,
reuseExistingServer: !process.env.CI
},
{
command:
"podman run --rm -p 3000:3000 some-registry/playwright-server:latest",
port: 3000,
timeout: 50000,
reuseExistingServer: !process.env.CI
}
],
use: {
baseURL: process.env.IP_ADDRESS || "http://localhost:5000/",
locale: "en-GB",
trace: "on-first-retry",
screenshot: "only-on-failure",
connectOptions: {
wsEndpoint: "ws://127.0.0.1:3000/"
}
},
expect: {
toHaveScreenshot: { maxDiffPixels: 10 }
},
projects: [
{
name: "chromium",
use: {
...devices["Desktop Chrome"],
viewport: { width: 1280, height: 1024 }
}
}
]
};

export default config;

Test file (self-contained)

import { test, expect } from "@playwright/test";

test.describe("@component", () => {
test("should render my component correctly", async ({ page }) => {
await page.goto(`/path/my-component/`);
await page.waitForSelector("#test-container");
await expect(page).toHaveScreenshot();
});
});

Steps

Run the test playwright test --update-snapshots --grep @component

Expected

No error while running chromium

Actual

I've been trying different chromium args but no luck. Before the Macbook M1 device, I was able to run the same setup with the following chromium args.

const chromiumDefaultArgs = ["--disable-dev-shm-usage", "--no-sandbox"];

After changing to Macbook M1 device, the same setup failed and I've tried different args (see below). I was able to run tests in Firefox successfully but not in Chromium.

const chromiumDefaultArgs = [
"--disable-gpu",
"--disable-dev-shm-usage",
"--disable-setuid-sandbox",
"--no-sandbox",
"--no-zygote",
"--single-process",
];

Issue 2:
System info

Playwright Version: [v1.32.2]

Operating System: [Windows 10]

Browser: [Firefox]

Source code

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

Config file

// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'], },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
});

Test file (self-contained)

import * as sysPath from "node:path";
import { test, expect } from "@playwright/test";

test('should support exposeBinding/addInitScript/addScriptTag in dynamic iframes', async function({ page, browser }) {
await page.exposeBinding("intercept", ({ page, frame }, value) => {
console.log("intercepted", value);
});

await page.context().addInitScript(() => window['bar'] = 17);
await page.addInitScript(() => window['foo'] = 42);
await page.addInitScript(() => window['intercepted'] = typeof window.intercept === 'function');

let ready = () => {};
let iframes = 0;

console.log("start");

page.on("frameattached", frame => {
// TEST : url is always empty
console.log("frameattached %i, url [%s]", ++iframes, frame.url());

frame.addScriptTag({ content: `window['baz'] = 54` }).then(async script => {
// TEST : url is good on chromium
// TEST : url is good on firefox when the page is a local server
// TEST : url is empty on firefox when the page is a local file or a private website
console.log("addScriptTag, url [%s]", frame.url());

expect(await frame.evaluate(() => window['foo'])).toBe(42);
expect(await frame.evaluate(() => window['bar'])).toBe(17);
expect(await frame.evaluate(() => window['intercepted'])).toBe(true);
expect(await frame.evaluate(() => window['baz'])).toBe(54);
}, err => {
// iframes are purposely detached
// ignore detached frames
});
});

page.on("framedetached", frame => {
// TEST : url is always empty
console.log("framedetached %i, url [%s]", --iframes, frame.url());
if (iframes === 1) {
ready();
}
});

await page.goto(sysPath.join(__dirname, 'dynamic-oopif.html'));
// await page.goto('http://127.0.0.1:3000/dynamic-oopif.html');

console.log("wait for ready");

await new Promise((resolve, reject) => {
ready = resolve;
});

await page.waitForTimeout(1000);

console.log("ready");

expect(page.frames().length).toBe(2);

// TEST : url is good on chromium
// TEST : url is good on firefox when the page is a local server
// TEST : url is empty on firefox when the page is a local file or a private website
console.log("iframe, url [%s]", page.frames()[1].url());

expect(await page.frames()[0].evaluate(() => window['foo'])).toBe(42);
expect(await page.frames()[1].evaluate(() => window['foo'])).toBe(42);
expect(await page.frames()[0].evaluate(() => window['bar'])).toBe(17);
expect(await page.frames()[1].evaluate(() => window['bar'])).toBe(17);
expect(await page.frames()[0].evaluate(() => window['intercepted'])).toBe(true);
expect(await page.frames()[1].evaluate(() => window['intercepted'])).toBe(true);

// TEST : pass on chromium
// TEST : pass on firefox if the server is a local server
// TEST : fail on firefox if the server is a local file or a private website
expect(await page.frames()[1].evaluate(() => window['baz'])).toBe(54);
});

dynamic-oopif.html

<script>
const promiseTimeout = ms => {
return new Promise((resolve, reject) => {
setTimeout(resolve, ms);
});
};

window.addEventListener('DOMContentLoaded', async () => {
const iframes = [];

function frameattach() {
const iframe = document.createElement('iframe');
iframe.src = "https://fr.wikipedia.org/wiki/Main_Page";
document.body.appendChild(iframe);
iframes.push(iframe);
}

function framedetach() {
const iframe = iframes.pop();
document.body.removeChild(iframe);
}

await promiseTimeout(2000);

frameattach();

await promiseTimeout(1000);

frameattach();
framedetach();
}, false);
</script>

Steps

[Run the test]

node node_modules/playwright/cli.js test --project=firefox --headed oopif.spec.ts

Expected

frame.evaluate to return the correct value

Actual

Error: frame.evaluate: Execution context was destroyed, most likely because of a navigation

Running 1 test using 1 worker
[firefox] › oopif.spec.ts:4:5 › should support exposeBinding/addInitScript/addScriptTag in dynamic iframes
start
wait for ready
frameattached 1, url []
addScriptTag, url []
frameattached 2, url []
framedetached 1, url []
ready
iframe, url []
1) [firefox] › oopif.spec.ts:4:5 › should support exposeBinding/addInitScript/addScriptTag in dynamic iframes

Error: frame.evaluate: Execution context was destroyed, most likely because of a navigation.

65 |
66 | expect(await page.frames()[0].evaluate(() => window['foo'])).toBe(42);
> 67 | expect(await page.frames()[1].evaluate(() => window['foo'])).toBe(42);
| ^
68 | expect(await page.frames()[0].evaluate(() => window['bar'])).toBe(17);
69 | expect(await page.frames()[1].evaluate(() => window['bar'])).toBe(17);
70 | expect(await page.frames()[0].evaluate(() => window['intercepted'])).toBe(true);

Running 1 test using 1 worker
[chromium] › oopif.spec.ts:4:5 › should support exposeBinding/addInitScript/addScriptTag in dynamic iframes
start
wait for ready
frameattached 1, url []
addScriptTag, url [https://fr.wikipedia.org/wiki/Main_Page]
frameattached 2, url []
framedetached 1, url []
ready
iframe, url [https://fr.wikipedia.org/wiki/Wikip%C3%A9dia:Accueil_principal]
1 passed (7.3s)