11import { cliFixture } from './cli.js'
22import { BROWSER_TIMEOUT } from './constants.js'
3- import { chromium , type Page } from '@playwright/test'
3+ import { chromium , type Locator , type Page } from '@playwright/test'
44import * as fs from 'fs'
55
66// ---------------------------------------------------------------------------
@@ -11,6 +11,33 @@ export interface BrowserContext {
1111 browserPage : Page
1212}
1313
14+ // ---------------------------------------------------------------------------
15+ // Main-frame response status tracking
16+ // ---------------------------------------------------------------------------
17+
18+ /**
19+ * Records the HTTP status of the most recent main-frame document response per
20+ * page. Populated by a `response` listener attached when the page is created
21+ * (see fixture below). Read via `getLastPageStatus(page)`.
22+ *
23+ * A WeakMap lets the entry be garbage-collected with the page — no manual
24+ * cleanup required.
25+ */
26+ const lastMainFrameStatus = new WeakMap < Page , number > ( )
27+
28+ export function trackMainFrameStatus ( page : Page ) : void {
29+ page . on ( 'response' , ( response ) => {
30+ if ( response . frame ( ) !== page . mainFrame ( ) ) return
31+ if ( response . request ( ) . resourceType ( ) !== 'document' ) return
32+ lastMainFrameStatus . set ( page , response . status ( ) )
33+ } )
34+ }
35+
36+ /** Get the HTTP status of the last main-frame document response on `page`. */
37+ export function getLastPageStatus ( page : Page ) : number | undefined {
38+ return lastMainFrameStatus . get ( page )
39+ }
40+
1441// ---------------------------------------------------------------------------
1542// Fixture
1643// ---------------------------------------------------------------------------
@@ -40,6 +67,7 @@ export const browserFixture = cliFixture.extend<{}, {browserPage: Page}>({
4067 context . setDefaultTimeout ( BROWSER_TIMEOUT . max )
4168 context . setDefaultNavigationTimeout ( BROWSER_TIMEOUT . max )
4269 const page = await context . newPage ( )
70+ trackMainFrameStatus ( page )
4371 await use ( page )
4472 await browser . close ( )
4573 } ,
@@ -52,13 +80,32 @@ export const browserFixture = cliFixture.extend<{}, {browserPage: Page}>({
5280// ---------------------------------------------------------------------------
5381
5482/**
55- * Check if the current page shows a server error (500, 502). If so, refresh and return true.
56- * Call this in retry loops when a selector isn't found — the page might be an error page.
83+ * Wait up to `timeoutMs` for `locator` to become visible. Returns `true` if it
84+ * appears in time, `false` on timeout or any other locator error (detached
85+ * element, closed context, etc.).
86+ *
87+ * Use this instead of `locator.isVisible({timeout})` — that API does not
88+ * actually wait in modern Playwright; it returns the current visibility state.
89+ * This helper uses `waitFor({state: 'visible', timeout})` for true polling.
90+ */
91+ export async function isVisibleWithin ( locator : Locator , timeoutMs : number ) : Promise < boolean > {
92+ return locator
93+ . waitFor ( { state : 'visible' , timeout : timeoutMs } )
94+ . then ( ( ) => true )
95+ . catch ( ( ) => false )
96+ }
97+
98+ /**
99+ * If the most recent main-frame response was a 5xx server error, reload the
100+ * page and return true. Otherwise return false. Call this in retry loops when
101+ * a selector isn't found — the page might be an error page.
102+ *
103+ * Uses the HTTP status captured by `trackMainFrameStatus` rather than scraping
104+ * body text, so it works regardless of how an error page is rendered.
57105 */
58106export async function refreshIfPageError ( page : Page ) : Promise < boolean > {
59- const pageText = ( await page . textContent ( 'body' ) ) ?? ''
60- if ( ! pageText . includes ( 'Internal Server Error' ) && ! pageText . includes ( '502 Bad Gateway' ) ) return false
61- // if (process.env.DEBUG === '1') process.stdout.write(' page refreshing...\n')
107+ const status = getLastPageStatus ( page )
108+ if ( status === undefined || status < 500 ) return false
62109 await page . reload ( { waitUntil : 'domcontentloaded' } )
63110 await page . waitForTimeout ( BROWSER_TIMEOUT . medium )
64111 return true
@@ -83,7 +130,7 @@ export async function navigateToDashboard(
83130 // Handle account picker (skip if email not provided)
84131 if ( ctx . email ) {
85132 const accountButton = browserPage . locator ( `text=${ ctx . email } ` ) . first ( )
86- if ( await accountButton . isVisible ( { timeout : BROWSER_TIMEOUT . medium } ) . catch ( ( ) => false ) ) {
133+ if ( await isVisibleWithin ( accountButton , BROWSER_TIMEOUT . medium ) ) {
87134 await accountButton . click ( )
88135 await browserPage . waitForTimeout ( BROWSER_TIMEOUT . medium )
89136 }
0 commit comments