Create wait-for-page-load.ts
This commit is contained in:
@@ -12,6 +12,7 @@ import vm from 'node:vm'
|
|||||||
import dedent from 'string-dedent'
|
import dedent from 'string-dedent'
|
||||||
import { createPatch } from 'diff'
|
import { createPatch } from 'diff'
|
||||||
import { getCdpUrl } from './utils.js'
|
import { getCdpUrl } from './utils.js'
|
||||||
|
import { waitForPageLoad, WaitForPageLoadOptions, WaitForPageLoadResult } from './wait-for-page-load.js'
|
||||||
|
|
||||||
const require = createRequire(import.meta.url)
|
const require = createRequire(import.meta.url)
|
||||||
|
|
||||||
@@ -60,6 +61,7 @@ interface VMContext {
|
|||||||
resetPlaywright: () => Promise<{ page: Page; context: BrowserContext }>
|
resetPlaywright: () => Promise<{ page: Page; context: BrowserContext }>
|
||||||
getLatestLogs: (options?: { page?: Page; count?: number; search?: string | RegExp }) => Promise<string[]>
|
getLatestLogs: (options?: { page?: Page; count?: number; search?: string | RegExp }) => Promise<string[]>
|
||||||
clearAllLogs: () => void
|
clearAllLogs: () => void
|
||||||
|
waitForPageLoad: (options: WaitForPageLoadOptions) => Promise<WaitForPageLoadResult>
|
||||||
require: NodeRequire
|
require: NodeRequire
|
||||||
import: (specifier: string) => Promise<any>
|
import: (specifier: string) => Promise<any>
|
||||||
}
|
}
|
||||||
@@ -527,6 +529,7 @@ server.tool(
|
|||||||
getLocatorStringForElement,
|
getLocatorStringForElement,
|
||||||
getLatestLogs,
|
getLatestLogs,
|
||||||
clearAllLogs,
|
clearAllLogs,
|
||||||
|
waitForPageLoad,
|
||||||
resetPlaywright: async () => {
|
resetPlaywright: async () => {
|
||||||
const { page: newPage, context: newContext } = await resetConnection()
|
const { page: newPage, context: newContext } = await resetConnection()
|
||||||
|
|
||||||
@@ -539,6 +542,7 @@ server.tool(
|
|||||||
getLocatorStringForElement,
|
getLocatorStringForElement,
|
||||||
getLatestLogs,
|
getLatestLogs,
|
||||||
clearAllLogs,
|
clearAllLogs,
|
||||||
|
waitForPageLoad,
|
||||||
resetPlaywright: vmContextObj.resetPlaywright,
|
resetPlaywright: vmContextObj.resetPlaywright,
|
||||||
require,
|
require,
|
||||||
// TODO --experimental-vm-modules is needed to make import work in vm
|
// TODO --experimental-vm-modules is needed to make import work in vm
|
||||||
|
|||||||
@@ -81,6 +81,13 @@ you have access to some functions in addition to playwright methods:
|
|||||||
- `page`: (optional) filter logs by a specific page instance. Only returns logs from that page
|
- `page`: (optional) filter logs by a specific page instance. Only returns logs from that page
|
||||||
- `count`: (optional) limit number of logs to return. If not specified, returns all available logs
|
- `count`: (optional) limit number of logs to return. If not specified, returns all available logs
|
||||||
- `search`: (optional) string or regex to filter logs. Only returns logs that match
|
- `search`: (optional) string or regex to filter logs. Only returns logs that match
|
||||||
|
- `waitForPageLoad({ page, timeout, pollInterval, minWait })`: smart network-aware page load detection. Playwright's `networkidle` waits for ALL requests to finish, which often times out on sites with analytics/ads. This function ignores those and returns when meaningful content is loaded.
|
||||||
|
- `page`: the page object to wait on
|
||||||
|
- `timeout`: (optional) max wait time in ms (default: 30000)
|
||||||
|
- `pollInterval`: (optional) how often to check in ms (default: 100)
|
||||||
|
- `minWait`: (optional) minimum wait before checking in ms (default: 500)
|
||||||
|
- Returns: `{ success, readyState, pendingRequests, waitTimeMs, timedOut }`
|
||||||
|
- Filters out: ad networks (doubleclick, googlesyndication), analytics (google-analytics, mixpanel, segment), social (facebook.net, twitter), support widgets (intercom, zendesk), and slow fonts/images
|
||||||
|
|
||||||
To bring a tab to front and focus it, use the standard Playwright method `await page.bringToFront()`
|
To bring a tab to front and focus it, use the standard Playwright method `await page.bringToFront()`
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,162 @@
|
|||||||
|
import type { Page } from 'playwright-core'
|
||||||
|
|
||||||
|
const FILTERED_DOMAINS = [
|
||||||
|
'doubleclick',
|
||||||
|
'googlesyndication',
|
||||||
|
'googleadservices',
|
||||||
|
'google-analytics',
|
||||||
|
'googletagmanager',
|
||||||
|
'facebook.net',
|
||||||
|
'fbcdn.net',
|
||||||
|
'twitter.com',
|
||||||
|
'linkedin.com',
|
||||||
|
'hotjar',
|
||||||
|
'mixpanel',
|
||||||
|
'segment.io',
|
||||||
|
'segment.com',
|
||||||
|
'newrelic',
|
||||||
|
'datadoghq',
|
||||||
|
'sentry.io',
|
||||||
|
'fullstory',
|
||||||
|
'amplitude',
|
||||||
|
'intercom',
|
||||||
|
'crisp.chat',
|
||||||
|
'zdassets.com',
|
||||||
|
'zendesk',
|
||||||
|
'tawk.to',
|
||||||
|
'hubspot',
|
||||||
|
'marketo',
|
||||||
|
'pardot',
|
||||||
|
'optimizely',
|
||||||
|
'crazyegg',
|
||||||
|
'mouseflow',
|
||||||
|
'clarity.ms',
|
||||||
|
'bing.com/bat',
|
||||||
|
'ads.',
|
||||||
|
'analytics.',
|
||||||
|
'tracking.',
|
||||||
|
'pixel.',
|
||||||
|
]
|
||||||
|
|
||||||
|
const FILTERED_EXTENSIONS = ['.gif', '.ico', '.cur', '.woff', '.woff2', '.ttf', '.otf', '.eot']
|
||||||
|
|
||||||
|
export interface WaitForPageLoadOptions {
|
||||||
|
page: Page
|
||||||
|
timeout?: number
|
||||||
|
pollInterval?: number
|
||||||
|
minWait?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WaitForPageLoadResult {
|
||||||
|
success: boolean
|
||||||
|
readyState: string
|
||||||
|
pendingRequests: string[]
|
||||||
|
waitTimeMs: number
|
||||||
|
timedOut: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function waitForPageLoad(options: WaitForPageLoadOptions): Promise<WaitForPageLoadResult> {
|
||||||
|
const { page, timeout = 30000, pollInterval = 100, minWait = 500 } = options
|
||||||
|
|
||||||
|
const startTime = Date.now()
|
||||||
|
let timedOut = false
|
||||||
|
let lastReadyState = ''
|
||||||
|
let lastPendingRequests: string[] = []
|
||||||
|
|
||||||
|
const checkPageReady = async (): Promise<{ ready: boolean; readyState: string; pendingRequests: string[] }> => {
|
||||||
|
const result = await page.evaluate(
|
||||||
|
({ filteredDomains, filteredExtensions, stuckThreshold, slowResourceThreshold }): {
|
||||||
|
ready: boolean
|
||||||
|
readyState: string
|
||||||
|
pendingRequests: string[]
|
||||||
|
} => {
|
||||||
|
const doc = globalThis.document as { readyState: string }
|
||||||
|
const readyState = doc.readyState
|
||||||
|
|
||||||
|
if (readyState !== 'complete') {
|
||||||
|
return { ready: false, readyState, pendingRequests: [`document.readyState: ${readyState}`] }
|
||||||
|
}
|
||||||
|
|
||||||
|
const resources = (performance as any).getEntriesByType('resource') as Array<{
|
||||||
|
name: string
|
||||||
|
startTime: number
|
||||||
|
responseEnd: number
|
||||||
|
}>
|
||||||
|
const now = (performance as any).now() as number
|
||||||
|
|
||||||
|
const pendingRequests = resources
|
||||||
|
.filter((r) => {
|
||||||
|
if (r.responseEnd > 0) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const elapsed = now - r.startTime
|
||||||
|
const url = r.name.toLowerCase()
|
||||||
|
|
||||||
|
if (url.startsWith('data:')) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filteredDomains.some((domain: string) => url.includes(domain))) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (elapsed > stuckThreshold) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (elapsed > slowResourceThreshold && filteredExtensions.some((ext: string) => url.includes(ext))) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
.map((r) => r.name)
|
||||||
|
|
||||||
|
return {
|
||||||
|
ready: pendingRequests.length === 0,
|
||||||
|
readyState,
|
||||||
|
pendingRequests,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
filteredDomains: FILTERED_DOMAINS,
|
||||||
|
filteredExtensions: FILTERED_EXTENSIONS,
|
||||||
|
stuckThreshold: 10000,
|
||||||
|
slowResourceThreshold: 3000,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, minWait))
|
||||||
|
|
||||||
|
while (Date.now() - startTime < timeout) {
|
||||||
|
const { ready, readyState, pendingRequests } = await checkPageReady()
|
||||||
|
lastReadyState = readyState
|
||||||
|
lastPendingRequests = pendingRequests
|
||||||
|
|
||||||
|
if (ready) {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
readyState,
|
||||||
|
pendingRequests: [],
|
||||||
|
waitTimeMs: Date.now() - startTime,
|
||||||
|
timedOut: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, pollInterval))
|
||||||
|
}
|
||||||
|
|
||||||
|
timedOut = true
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
readyState: lastReadyState,
|
||||||
|
pendingRequests: lastPendingRequests.slice(0, 10),
|
||||||
|
waitTimeMs: Date.now() - startTime,
|
||||||
|
timedOut,
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user