playwriter 0.0.20 → 0.0.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/mcp.ts CHANGED
@@ -15,6 +15,13 @@ import { getCdpUrl, LOG_FILE_PATH } from './utils.js'
15
15
  import { waitForPageLoad, WaitForPageLoadOptions, WaitForPageLoadResult } from './wait-for-page-load.js'
16
16
  import { getCDPSessionForPage, CDPSession } from './cdp-session.js'
17
17
 
18
+ class CodeExecutionTimeoutError extends Error {
19
+ constructor(timeout: number) {
20
+ super(`Code execution timed out after ${timeout}ms`)
21
+ this.name = 'CodeExecutionTimeoutError'
22
+ }
23
+ }
24
+
18
25
  const require = createRequire(import.meta.url)
19
26
 
20
27
  const usefulGlobals = {
@@ -94,8 +101,6 @@ const cdpSessionCache: WeakMap<Page, CDPSession> = new WeakMap()
94
101
  const RELAY_PORT = 19988
95
102
  const NO_TABS_ERROR = `No browser tabs are connected. Please install and enable the Playwriter extension on at least one tab: https://chromewebstore.google.com/detail/playwriter-mcp/jfeammnjpkecdekppnclgkkffahnhfhe`
96
103
 
97
-
98
-
99
104
  async function setDeviceScaleFactorForMacOS(context: BrowserContext): Promise<void> {
100
105
  if (os.platform() !== 'darwin') {
101
106
  return
@@ -113,8 +118,25 @@ async function setDeviceScaleFactorForMacOS(context: BrowserContext): Promise<vo
113
118
  }
114
119
  }
115
120
 
121
+ async function preserveSystemColorScheme(context: BrowserContext): Promise<void> {
122
+ const options = (context as any)._options
123
+ if (!options) {
124
+ return
125
+ }
126
+ options.colorScheme = 'no-override'
127
+ options.reducedMotion = 'no-override'
128
+ options.forcedColors = 'no-override'
129
+ await Promise.all(
130
+ context.pages().map((page) => {
131
+ return page.emulateMedia({ colorScheme: null, reducedMotion: null, forcedColors: null }).catch(() => {})
132
+ }),
133
+ )
134
+ }
135
+
116
136
  function isRegExp(value: any): value is RegExp {
117
- return typeof value === 'object' && value !== null && typeof value.test === 'function' && typeof value.exec === 'function'
137
+ return (
138
+ typeof value === 'object' && value !== null && typeof value.test === 'function' && typeof value.exec === 'function'
139
+ )
118
140
  }
119
141
 
120
142
  function clearUserState() {
@@ -209,6 +231,7 @@ async function ensureConnection(): Promise<{ browser: Browser; page: Page }> {
209
231
  // Set up console listener for all existing pages
210
232
  context.pages().forEach((p) => setupPageConsoleListener(p))
211
233
 
234
+ await preserveSystemColorScheme(context)
212
235
  await setDeviceScaleFactorForMacOS(context)
213
236
 
214
237
  state.browser = browser
@@ -246,31 +269,32 @@ function setupPageConsoleListener(page: Page) {
246
269
  browserLogs.set(targetId, [])
247
270
  }
248
271
 
249
- // Clear logs on navigation/reload
250
272
  page.on('framenavigated', (frame) => {
251
- // Only clear if it's the main frame navigating (page reload/navigation)
252
273
  if (frame === page.mainFrame()) {
253
274
  browserLogs.set(targetId, [])
254
275
  }
255
276
  })
256
277
 
257
- // Delete logs when page is closed
258
278
  page.on('close', () => {
259
279
  browserLogs.delete(targetId)
260
280
  })
261
281
 
262
282
  page.on('console', (msg) => {
263
- const logEntry = `[${msg.type()}] ${msg.text()}`
283
+ try {
284
+ let logEntry = `[${msg.type()}] ${msg.text()}`
264
285
 
265
- // Get or create logs array for this page targetId
266
- if (!browserLogs.has(targetId)) {
267
- browserLogs.set(targetId, [])
268
- }
269
- const pageLogs = browserLogs.get(targetId)!
286
+ if (!browserLogs.has(targetId)) {
287
+ browserLogs.set(targetId, [])
288
+ }
289
+ const pageLogs = browserLogs.get(targetId)!
270
290
 
271
- pageLogs.push(logEntry)
272
- if (pageLogs.length > MAX_LOGS_PER_PAGE) {
273
- pageLogs.shift()
291
+ pageLogs.push(logEntry)
292
+ if (pageLogs.length > MAX_LOGS_PER_PAGE) {
293
+ pageLogs.shift()
294
+ }
295
+ } catch (e) {
296
+ console.error('[MCP] Failed to get console message text:', e)
297
+ return
274
298
  }
275
299
  })
276
300
  }
@@ -287,8 +311,7 @@ async function getCurrentPage(timeout = 5000) {
287
311
 
288
312
  if (pages.length > 0) {
289
313
  const page = pages[0]
290
- page.waitForEvent('load', { timeout })
291
- await page.emulateMedia({ colorScheme: null })
314
+ await page.waitForLoadState('load', { timeout }).catch(() => {})
292
315
  return page
293
316
  }
294
317
  }
@@ -334,6 +357,7 @@ async function resetConnection(): Promise<{ browser: Browser; page: Page; contex
334
357
  // Set up console listener for all existing pages
335
358
  context.pages().forEach((p) => setupPageConsoleListener(p))
336
359
 
360
+ await preserveSystemColorScheme(context)
337
361
  await setDeviceScaleFactorForMacOS(context)
338
362
 
339
363
  state.browser = browser
@@ -350,7 +374,9 @@ const server = new McpServer({
350
374
  version: '1.0.0',
351
375
  })
352
376
 
353
- const promptContent = fs.readFileSync(path.join(path.dirname(fileURLToPath(import.meta.url)), 'prompt.md'), 'utf-8') + `\n\nfor debugging errors, check relay server logs at: ${LOG_FILE_PATH}`
377
+ const promptContent =
378
+ fs.readFileSync(path.join(path.dirname(fileURLToPath(import.meta.url)), 'prompt.md'), 'utf-8') +
379
+ `\n\nfor debugging internal playwriter errors, check playwriter relay server logs at: ${LOG_FILE_PATH}`
354
380
 
355
381
  server.tool(
356
382
  'execute',
@@ -597,7 +623,7 @@ server.tool(
597
623
  displayErrors: true,
598
624
  }),
599
625
  new Promise((_, reject) =>
600
- setTimeout(() => reject(new Error(`Code execution timed out after ${timeout}ms`)), timeout),
626
+ setTimeout(() => reject(new CodeExecutionTimeoutError(timeout)), timeout),
601
627
  ),
602
628
  ])
603
629
 
@@ -636,7 +662,7 @@ server.tool(
636
662
 
637
663
  const logsText = formatConsoleLogs(consoleLogs, 'Console output (before error)')
638
664
 
639
- const isTimeoutError = error.name === 'TimeoutError' || error.message.includes('Timeout')
665
+ const isTimeoutError = error instanceof CodeExecutionTimeoutError || error.name === 'TimeoutError'
640
666
  if (!isTimeoutError) {
641
667
  sendLogToRelayServer('error', '[MCP] Error:', errorStack)
642
668
  }
@@ -694,12 +720,13 @@ server.tool(
694
720
  },
695
721
  )
696
722
 
697
- // Start the server
698
723
  async function main() {
699
724
  await ensureRelayServer()
700
725
  const transport = new StdioServerTransport()
701
726
  await server.connect(transport)
702
- // console.error('Playwright MCP server running on stdio')
703
727
  }
704
728
 
705
- main().catch(console.error)
729
+ main().catch((error) => {
730
+ console.error('Fatal error starting MCP server:', error)
731
+ process.exit(1)
732
+ })
package/src/prompt.md CHANGED
@@ -51,9 +51,11 @@ IMPORTANT! never call bringToFront unless specifically asked by the user. It is
51
51
 
52
52
  - only call `page.close()` if the user asks you so or if you previously created this page yourself with `newPage`. do not close user created pages unless asked
53
53
  - try to never sleep or run `page.waitForTimeout` unless you have to. there are better ways to wait for an element
54
+ - use `page.waitForLoadState('load')` instead of `page.waitForEvent('load')`. `waitForEvent` waits for a future event and will timeout if the page is already loaded, while `waitForLoadState` resolves immediately if already in that state
54
55
  - never close browser or context. NEVER call `browser.close()`
55
56
  - NEVER use `page.context().newCDPSession()` or `browser.newCDPSession()` - these do not work through the playwriter relay. If you need to send raw CDP commands, use the `getCDPSession` utility function instead.
56
57
 
58
+
57
59
  ## always check the current page state after an action
58
60
 
59
61
  after you click a button or submit a form you ALWAYS have to then check what is the current state of the page. you cannot assume what happened after doing an action. instead run the following code to know what happened after the action:
package/src/utils.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import fs from 'node:fs'
2
+ import os from 'node:os'
2
3
  import path from 'node:path'
3
4
  import { xdgData } from 'xdg-basedir'
4
5
 
@@ -8,7 +9,8 @@ export function getCdpUrl({ port = 19988, host = '127.0.0.1' }: { port?: number;
8
9
  }
9
10
 
10
11
  export function getDataDir(): string {
11
- return path.join(xdgData!, 'playwriter')
12
+ const dataDir = xdgData || path.join(os.homedir(), '.local', 'share')
13
+ return path.join(dataDir, 'playwriter')
12
14
  }
13
15
 
14
16
  export function ensureDataDir(): string {
@@ -133,15 +133,26 @@ export async function waitForPageLoad(options: WaitForPageLoadOptions): Promise<
133
133
  await new Promise((resolve) => setTimeout(resolve, minWait))
134
134
 
135
135
  while (Date.now() - startTime < timeout) {
136
- const { ready, readyState, pendingRequests } = await checkPageReady()
137
- lastReadyState = readyState
138
- lastPendingRequests = pendingRequests
136
+ try {
137
+ const { ready, readyState, pendingRequests } = await checkPageReady()
138
+ lastReadyState = readyState
139
+ lastPendingRequests = pendingRequests
139
140
 
140
- if (ready) {
141
+ if (ready) {
142
+ return {
143
+ success: true,
144
+ readyState,
145
+ pendingRequests: [],
146
+ waitTimeMs: Date.now() - startTime,
147
+ timedOut: false,
148
+ }
149
+ }
150
+ } catch (e) {
151
+ console.error('[waitForPageLoad] page.evaluate failed:', e)
141
152
  return {
142
- success: true,
143
- readyState,
144
- pendingRequests: [],
153
+ success: false,
154
+ readyState: 'error',
155
+ pendingRequests: ['page.evaluate failed - page may have closed or navigated'],
145
156
  waitTimeMs: Date.now() - startTime,
146
157
  timedOut: false,
147
158
  }