Issue 1:
System info

Playwright Version: [v1.32]

Operating System: [macOS 13.2]

Browser: [All]

Other info: Storage State / Authentication state enabled through globalSetup

Config file

// playwright.config.ts
globalSetup: require.resolve("./src/e2e/tests/global-setup"),
storageState: './src/e2e/tests/state.json',
actionTimeout: 0,
screenshot: 'only-on-failure',
trace: 'on-first-retry',
},

Test file (self-contained)

test.beforeEach(async ({ page }) => {
// Runs before each test and signs in each page.
const assetsPage = new AnyPage(page);
const loginPage = new LoginPage(page);
await loginPage.visit()
await assetsPage.menuTopNavComponent.clickAnyMenuItemNav();
});

test('Creating an Element - Any Type', async ({ page }) => {
const anyPage = new AnyPage(page);
await anyPage.anyGridLoads()
await anyPage.createElementAnyType();
});

Pre-condition

Having Storage state enabled and being used to sign in during the beforeEach hook

Steps

Run the test with the new flag --ui

npx playwright test --ui

Expected

UI Should be displayed as per the new feature is intended to work

Actual

After executing the command with the new flag --ui, terminal shows the following error:

/Users/clundstedt/Documents/zf-playwright/node_modules/@playwright/test/lib/runner/uiMode.js:159
this._page.mainFrame().evaluateExpression(dispatchFuncSource, true, message).catch(e => this._originalStderr(String(e)));
^

TypeError: Cannot read properties of undefined (reading 'mainFrame')
at UIMode._dispatchEvent (/Users/clundstedt/Documents/zf-playwright/node_modules/@playwright/test/lib/runner/uiMode.js:159:16)
at Timeout._onTimeout (/Users/clundstedt/Documents/zf-playwright/node_modules/@playwright/test/lib/runner/uiMode.js:69:14)
at listOnTimeout (node:internal/timers:564:17)
at processTimers (node:internal/timers:507:7)

Node.js v19.4.0

Issue 2:
System info

Playwright Version: [v1.33.0]

Operating System: Ubuntu 22.04.2 LTS

Browser: Chromium

Other info: Confirmed on Chromium but I think it likely affects all browsers

Note: OP has been edited for clarity.

Source code

implementation details

// open browser, inject needed Carted window object, and wait for graphQL responses
async open(waitForApi = true) {
const graphQlResponses = waitForResponses(this.page, 4);

await this.injectCarted();
// note no await
this.page.goto(ClientData.baseUrl + '/index.html');

// workarounds that fix the issue:
// 1. await this.page.goto(ClientData.baseUrl + '/index.html');
// 2. this.page.goto(ClientData.baseUrl + '/index.html', { waitUntil: 'commit' });

if (waitForApi) {
await graphQlResponses;
}
}

// example page object class getter - `this.self` is itself a locator that scopes
// the CSS query, while OrderStatusCardSelectors.confirmationHeader is a CSS string
get confirmationHeader() {
return this.self.locator(OrderStatusCardSelectors.confirmationHeader);
}

test.spec.ts

import { Page, test } from '@playwright/test';
import { Browser, Page } from '@playwright/test';

// graphql helper
import CartedSession from '../../graphql';
// extended test and expect
import { expect, test } from '../fixtures';
// page objects
import {
CreditCard,
FloatingCartButton,
OrderStatusCard,
ShoppingCart,
} from '../page-objects';
// support
import { cleanUp, getState, sendResults, setUp } from '../support';
// test data
import {
adidasBackpack,
ccDetailsTestCard,
plushToy,
staticUSShippingDetails,
} from '../test-data';

test.describe.configure({ mode: 'serial' });

test.describe('Order Confirmation', () => {
const cartedSession = new CartedSession('order-confirmation');
let page: Page;
let testBrowser: Browser;
let cart: ShoppingCart;
let floatingCartButton: FloatingCartButton;
let payment: CreditCard;
let orderStatus: OrderStatusCard;

test.beforeAll(async ({ browser }, testInfo) => {
// use the GraphAPI directly set session state, loading needed fixtures (if any)
// all these requests happens before the browser even opens, so we can seed some state
await cartedSession.loadFixture();
await cartedSession.fetchProduct(plushToy);
await cartedSession.fetchProduct(adidasBackpack);
await cartedSession.addToCart(plushToy);
await cartedSession.addToCart(adidasBackpack, 2);
await cartedSession.startCheckout();
await cartedSession.setAddress(staticUSShippingDetails);
await cartedSession.setShippingOptions([
{ vendorName: plushToy.vendor, label: 'Economy' },
{ vendorName: adidasBackpack.vendor, label: 'Standard' },
]);

// fetch local storage state object
const state = getState([cartedSession.localStorageObject]);
const mockOptions = {
fixtureName: 'order-confirmation',
storageState: state,
sessionId: cartedSession.sessionId,
};

// now we create a page and setup calls
// browser.newPage({ storageState }) at some point
[page, testBrowser] = await setUp(browser, testInfo, mockOptions);

// init page objects
cart = new ShoppingCart(page);
floatingCartButton = new FloatingCartButton(page);
payment = new CreditCard(page);
orderStatus = new OrderStatusCard(page);

// launch the browser, open the cart, confirm payment
await payment.open();
await floatingCartButton.openCart();
await payment.enterCreditCardDetails(ccDetailsTestCard);
await cart.confirmPurchase();
});

test.afterEach(async ({}, testInfo) => {
await sendResults(page, testInfo);
});

test.afterAll(async ({}) => {
await cleanUp(page, testBrowser, { fixtureName: 'order-confirmation' });
});

test('shows the order confrimation header', () => {
expect(orderStatus.confirmationHeader).toBeVisible();
});

test('displays correct email in thank you summary', () => {
expect(orderStatus.emailSummary).toContainText(
staticUSShippingDetails.email
);
});

test('displays Order Placed for the first item', () => {
// set order status card to equal plushtoy vendor
orderStatus.merchantName = plushToy.vendor;
expect(orderStatus.badge).toHaveText('Order Placed');
});

test('displays correct title for first item', () => {
expect(orderStatus.title).toHaveText(plushToy.title);
});

test.fixme('displays correct image for first item ', () => {
expect(orderStatus.image).toHaveScreenshot();
});

test('displays correct vendor name for first item ', () => {
expect(orderStatus.vendor).toContainText(plushToy.vendor);
});

test('displays Order Placed for the second item', () => {
// set order status card to equal backpack vendor
orderStatus.merchantName = adidasBackpack.vendor;
expect(orderStatus.badge).toHaveText('Order Placed');
});

test('displays correct title for second item', () => {
expect(orderStatus.title).toHaveText(adidasBackpack.title);
});

test('displays correct vendor name for second item ', () => {
expect(orderStatus.vendor).toContainText(adidasBackpack.vendor);
});

test.fixme('displays correct image for second item ', () => {
expect(orderStatus.image).toHaveScreenshot();
});
});

index.html

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="Test File" />
<title>Publisher Page</title>
<script>
// if thing defined don't overwrite it
if (!window['thing']) {
window['thing'] = {
environment: 'sandbox',
apiKey: 'default',
onMessage: console.debug,
displayName: 'PlayWright Tester',
};
}
// actual index test file imports a module using scripts src='url' defer
// this module registers a custom component which calls our graphQL api
</script>
<style>
* { box-sizing: border-box; }
body { margin: 0; padding: 2rem; }
main { max-width: min(100%, 58rem); margin: 0 auto; }
carted-product-card { contain: size layout paint; }
</style>
</head>
<body>
<main>
<article>

<h2>Hello world!</h2>
<p>This is me, life can be: fun for everyone</p>
<custom-component src='url'></custom-component>
</article>
</main>
</body>
</html>

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

Config file

// playwright.config.ts
import type { PlaywrightTestConfig } from '@playwright/test';
import dotenv from 'dotenv';

// read .env file values
dotenv.config();

// this config sets the browserstack env var to false
process.env.BROWSERSTACK = 'false';

const config: PlaywrightTestConfig = {
name: 'Local Config',
globalSetup: './playwright/global-setup.ts', // only thing this does is remove all files in the dist directory
testDir: './playwright/integration',
timeout: 100_000,
reporter: [['html', { outputFolder: 'dist/playwright/reports' }]],
outputDir: 'dist/playwright/test-results/',
retries: 0,
expect: {
timeout: 10_000,
toHaveScreenshot: {
threshold: 0.2,
maxDiffPixelRatio: 0.1,
maxDiffPixels: 5,
},
},
use: {
actionTimeout: 0,
screenshot: 'only-on-failure',
trace: 'on',
video: 'off',
contextOptions: {
recordVideo: {
dir: 'dist/playwright/videos',
},
},
},
projects: [
{
name: 'chromium',
use: {
browserName: 'chromium',
channel: 'chromium',
},
},
],
};

export default config;
});

Steps

Run the test

I rarely see this bug occur when running a single test, create a bunch of dummy test files and run them in parallel to boost the chances of seeing the bug

Expected

No "page goto failed: page was closed error" is thrown

Actual

Fails 33% of the time on my machine. I don't know for sure but after verifying the valid load state here it seems like eventually the private server _goToAction method will be called, which on line 693 awaits an extra LifecycleEvent event promise.

My suspicion is that the file protocol is behaving differently (due to how the browser treats local html files) - or that it is perhaps just faster than the typical http/https file that, under normal circumstances, is served up by a server which is therefore exposing some raciness.

Mileage may vary, I believe the above code reproduces the issue, I had to take a whole lot of IP and other work related stuff out that over complicates things, but wanted to leave stuff like dotenv to mirror our code as close as possible.

What I can confirm is that setting the wait until option to commit fixes the issue for us.