playwriter 0.0.33 → 0.0.34

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.
Files changed (46) hide show
  1. package/dist/cdp-relay.d.ts.map +1 -1
  2. package/dist/cdp-relay.js +22 -2
  3. package/dist/cdp-relay.js.map +1 -1
  4. package/dist/cdp-session.d.ts +24 -3
  5. package/dist/cdp-session.d.ts.map +1 -1
  6. package/dist/cdp-session.js +23 -0
  7. package/dist/cdp-session.js.map +1 -1
  8. package/dist/debugger-api.md +4 -3
  9. package/dist/debugger.d.ts +4 -3
  10. package/dist/debugger.d.ts.map +1 -1
  11. package/dist/debugger.js +3 -1
  12. package/dist/debugger.js.map +1 -1
  13. package/dist/editor-api.md +2 -2
  14. package/dist/editor.d.ts +2 -2
  15. package/dist/editor.d.ts.map +1 -1
  16. package/dist/editor.js +1 -0
  17. package/dist/editor.js.map +1 -1
  18. package/dist/index.d.ts +6 -0
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +3 -0
  21. package/dist/index.js.map +1 -1
  22. package/dist/mcp.d.ts.map +1 -1
  23. package/dist/mcp.js +24 -0
  24. package/dist/mcp.js.map +1 -1
  25. package/dist/protocol.d.ts +12 -1
  26. package/dist/protocol.d.ts.map +1 -1
  27. package/dist/react-source.d.ts +3 -3
  28. package/dist/react-source.d.ts.map +1 -1
  29. package/dist/react-source.js +3 -1
  30. package/dist/react-source.js.map +1 -1
  31. package/dist/styles-api.md +3 -3
  32. package/dist/styles.d.ts +3 -3
  33. package/dist/styles.d.ts.map +1 -1
  34. package/dist/styles.js +3 -1
  35. package/dist/styles.js.map +1 -1
  36. package/package.json +1 -1
  37. package/src/cdp-relay.ts +23 -2
  38. package/src/cdp-session.ts +50 -3
  39. package/src/debugger.ts +6 -4
  40. package/src/editor.ts +4 -3
  41. package/src/index.ts +6 -0
  42. package/src/mcp.ts +32 -5
  43. package/src/prompt.md +192 -168
  44. package/src/protocol.ts +14 -1
  45. package/src/react-source.ts +5 -3
  46. package/src/styles.ts +5 -3
package/src/prompt.md CHANGED
@@ -1,259 +1,283 @@
1
- playwriter execute is a tool to control the user browser instance via extension also called playwriter MCP.
1
+ # playwriter execute
2
2
 
3
- if you get an error Extension not running tell user to install and enable the playwriter extension first, clicking on the extension icon on the tab the user wants to control
3
+ Control user's Chrome browser via playwright code snippets. Prefer single-line code with semicolons between statements. If you get "Extension not running" error, tell user to click the playwriter extension icon on the tab they want to control.
4
4
 
5
- execute tool let you run playwright js code snippets to control user Chrome window, these js code snippets are preferred to be in a single line to make them more readable in agent interface. separating statements with semicolons
5
+ You can collaborate with the user - they can help with captchas, difficult elements, or reproducing bugs.
6
6
 
7
- you can extract data from your script using `console.log`. But remember that console.log in `page.evaluate` callbacks are run in the browser, so you will not see them. Instead log the evaluate result
7
+ ## context variables
8
8
 
9
- to keep some variables between calls, you can use `state` global object. constants and variables are reset between runs. Instead use code like `state.newPage = await browser.newPage();` to reuse the created page in later calls
9
+ - `state` - object persisted between calls, use to store data/pages (e.g., `state.myPage = await context.newPage()`)
10
+ - `page` - default page the user activated, use this unless working with multiple pages
11
+ - `context` - browser context, access all pages via `context.pages()`
12
+ - `require` - load node modules (e.g., `require('node:fs')`)
13
+ - Node.js globals: `setTimeout`, `setInterval`, `fetch`, `URL`, `Buffer`, `crypto`, etc.
10
14
 
11
- you MUST use multiple execute tool calls for running complex logic. this ensures
12
- - you have clear understanding of intermediate state between interactions
13
- - you can split finding an element from interacting with it. making it simpler to understand what is the issue when an action is not successful
15
+ ## rules
14
16
 
15
- it will control an existing user Chrome window. The js code will be run in a sandbox with some variables in context:
17
+ - **Multiple calls**: use multiple execute calls for complex logic - helps understand intermediate state and isolate which action failed
18
+ - **Never close**: never call `browser.close()` or `context.close()`. Only close pages you created or if user asks
19
+ - **No bringToFront**: never call unless user asks - it's disruptive and unnecessary, you can interact with background pages
20
+ - **Check state after actions**: always verify page state after clicking/submitting (see next section)
21
+ - **Clean up listeners**: call `page.removeAllListeners()` at end of message to prevent leaks
22
+ - **CDP sessions**: use `getCDPSession({ page })` not `page.context().newCDPSession()` - the latter doesn't work through playwriter relay
23
+ - **Wait for load**: use `page.waitForLoadState('load')` not `page.waitForEvent('load')` - waitForEvent times out if already loaded
24
+ - **Avoid timeouts**: prefer proper waits over `page.waitForTimeout()` - there are better ways to wait for elements
16
25
 
17
- - state: an object shared between runs that you can mutate to persist functions and objects. for example `state.requests = []` to monitor network requests between runs
18
- - context: the playwright browser context. you can do things like `await context.pages()` to see user connected pages
19
- - page, the first page the user opened and made it accessible to this MCP. do things like `page.url()` to see current url. assume the user wants you to use this page for your playwright code
20
- - require: node's require function to load modules
21
- - all standard Node.js globals: setTimeout, setInterval, clearTimeout, clearInterval, URL, URLSearchParams, fetch, Buffer, TextEncoder, TextDecoder, crypto, AbortController, AbortSignal, structuredClone
26
+ ## checking page state
22
27
 
23
- the chrome window can have more than one page. you can see other pages with `context.pages().find((p) => p.url().includes('localhost'))`. you can also open and close pages: `state.newPage = await context.newPage()`. store the page in state so that you can reuse it later
28
+ After any action (click, submit, navigate), verify what happened:
24
29
 
25
- you can control the browser in collaboration with the user. the user can help you get unstuck from captchas or difficult to find elements or reproducing a bug
30
+ ```js
31
+ console.log('url:', page.url()); console.log(await accessibilitySnapshot({ page }).then(x => x.split('\n').slice(0, 30).join('\n')));
32
+ ```
26
33
 
27
- ## capabilities
34
+ If nothing changed, try `await page.waitForLoadState('networkidle', {timeout: 3000})` or you may have clicked the wrong element.
35
+
36
+ ## accessibility snapshots
37
+
38
+ ```js
39
+ await accessibilitySnapshot({ page, search?, contextLines?, showDiffSinceLastCall? })
40
+ ```
28
41
 
29
- examples of things playwriter MCP can do:
30
- - monitor logs for a page while the user reproduces a but to let you understand what is causing a bug
31
- - monitor logs while also controlling the page, then read collected logs and debug an issue
32
- - monitor xhr network requests while scrolling an infinite scroll page to extract data from a website
33
- - get accessibility snapshot to see clickable elements on the page, then click or interact with them to automate a task like ordering pizza
42
+ - `search` - string/regex to filter results (returns first 10 matches with context)
43
+ - `contextLines` - lines of context around matches (default: 10)
44
+ - `showDiffSinceLastCall` - returns diff since last snapshot (useful after actions)
34
45
 
35
- ## finding the page to execute code in
46
+ Example output:
36
47
 
37
- if you plan to control a specific page for an url you can store it in `state` so you can reuse it later on:
48
+ ```md
49
+ - banner [ref=e3]:
50
+ - link "Home" [ref=e5] [cursor=pointer]:
51
+ - /url: /
52
+ - navigation [ref=e12]:
53
+ - link "Docs" [ref=e13] [cursor=pointer]:
54
+ - /url: /docs
55
+ ```
56
+
57
+ Use `aria-ref` to interact - **no quotes around the ref value**:
38
58
 
39
59
  ```js
40
- const pages = context.pages().filter(x => x.url().includes('localhost'));
41
- if (pages.length === 0) throw new Error('No page with URL matching localhost found');
42
- if (pages.length > 1) throw new Error('Multiple pages with URL matching localhost found');
43
- state.localhostPage = pages[0];
44
- // do things with the page
45
- await state.localhostPage.bringToFront();
60
+ await page.locator('aria-ref=e13').click()
46
61
  ```
47
62
 
48
- IMPORTANT! never call bringToFront unless specifically asked by the user. It is very bothering to the user otherwise! you don't need to call bringToFront before being able to interact. you can very well interact without calling it first. on any page in the background you have access to.
63
+ Search for specific elements:
49
64
 
50
- ## rules
65
+ ```js
66
+ const snapshot = await accessibilitySnapshot({ page, search: /button|submit/i })
67
+ ```
51
68
 
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
- - 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
55
- - never close browser or context. NEVER call `browser.close()`
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.
69
+ ## selector best practices
57
70
 
71
+ **For unknown websites**: use `accessibilitySnapshot()` with `aria-ref` - it shows what's actually interactive.
58
72
 
59
- ## always check the current page state after an action
73
+ **For development** (when you have source code access), prefer stable selectors in this order:
60
74
 
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:
75
+ 1. **Best**: `[data-testid="submit"]` - explicit test attributes, never change accidentally
76
+ 2. **Good**: `getByRole('button', { name: 'Save' })` - accessible, semantic
77
+ 3. **Good**: `getByText('Sign in')`, `getByLabel('Email')` - readable, user-facing
78
+ 4. **OK**: `input[name="email"]`, `button[type="submit"]` - semantic HTML
79
+ 5. **Avoid**: `.btn-primary`, `#submit` - classes/IDs change frequently
80
+ 6. **Last resort**: `div.container > form > button` - fragile, breaks easily
62
81
 
63
- `console.log('url:', page.url()); console.log(await accessibilitySnapshot({ page }).then(x => x.split('\n').slice(0, 30).join('\n')));`
82
+ Combine locators for precision:
64
83
 
65
- if nothing happened you may need to wait before the action completes, using something like `page.waitForNavigation({timeout: 3000})` or `await page.waitForLoadState('networkidle', {timeout: 3000})`
84
+ ```js
85
+ page.locator('tr').filter({ hasText: 'John' }).locator('button').click()
86
+ page.locator('button').nth(2).click()
87
+ ```
66
88
 
67
- if nothing happens it could also means that you clicked the wrong button or link. try to search for other appropriate elements to click or submit
89
+ If a locator matches multiple elements, Playwright throws "strict mode violation". Use `.first()`, `.last()`, or `.nth(n)`:
68
90
 
91
+ ```js
92
+ await page.locator('button').first().click() // first match
93
+ await page.locator('.item').last().click() // last match
94
+ await page.locator('li').nth(3).click() // 4th item (0-indexed)
95
+ ```
69
96
 
70
- ## event listeners
97
+ ## working with pages
71
98
 
72
- always detach event listener you create at the end of a message using `page.removeAllListeners()` or similar so that you never leak them in future messages
99
+ Find a specific page:
73
100
 
74
- ## utility functions
101
+ ```js
102
+ const pages = context.pages().filter(x => x.url().includes('localhost'));
103
+ if (pages.length !== 1) throw new Error(`Expected 1 page, found ${pages.length}`);
104
+ state.targetPage = pages[0];
105
+ ```
75
106
 
76
- you have access to some functions in addition to playwright methods:
77
-
78
- - `async accessibilitySnapshot({ page, search, contextLines, showDiffSinceLastCall })`: gets a human readable snapshot of clickable elements on the page. useful to see the overall structure of the page and what elements you can interact with.
79
- - `page`: the page object to snapshot
80
- - `search`: (optional) a string or regex to filter the snapshot. If provided, returns the first 10 matches with surrounding context
81
- - `contextLines`: (optional) number of lines of context to show around each match (default: 10). Also controls context lines in diff output.
82
- - `showDiffSinceLastCall`: (optional) if true, returns a unified diff patch showing only changes since the last non-diff snapshot call for this page. Disables search when enabled. Useful to see what changed after an action. Note: diff calls do not update the stored snapshot, so you can call diff multiple times and always compare against the same baseline.
83
- - `getLatestLogs({ page, count, search })`: retrieves browser console logs. The system automatically captures and stores up to 5000 logs per page. Logs are cleared when a page reloads or navigates.
84
- - `page`: (optional) filter logs by a specific page instance. Only returns logs from that page
85
- - `count`: (optional) limit number of logs to return. If not specified, returns all available logs
86
- - `search`: (optional) string or regex to filter logs. Only returns logs that match
87
- - `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.
88
- - `page`: the page object to wait on
89
- - `timeout`: (optional) max wait time in ms (default: 30000)
90
- - `pollInterval`: (optional) how often to check in ms (default: 100)
91
- - `minWait`: (optional) minimum wait before checking in ms (default: 500)
92
- - Returns: `{ success, readyState, pendingRequests, waitTimeMs, timedOut }`
93
- - Filters out: ad networks (doubleclick, googlesyndication), analytics (google-analytics, mixpanel, segment), social (facebook.net, twitter), support widgets (intercom, zendesk), and slow fonts/images
94
- - `getCDPSession({ page })`: creates a CDP session to send raw Chrome DevTools Protocol commands. Use this instead of `page.context().newCDPSession()` which does not work through the playwriter relay. Sessions are cached per page.
95
- - `page`: the page object to create the session for
96
- - Returns: `{ send(method, params?), on(event, callback), off(event, callback) }`
97
- - Example: `const cdp = await getCDPSession({ page }); const metrics = await cdp.send('Page.getLayoutMetrics');`
98
- - `createDebugger({ cdp })`: creates a Debugger instance for setting breakpoints, stepping, and inspecting variables. Read the `https://playwriter.dev/resources/debugger-api.md` resource for full API docs and examples.
99
- - `createEditor({ cdp })`: creates an Editor instance for viewing and live-editing page scripts and CSS stylesheets. Read the `https://playwriter.dev/resources/editor-api.md` resource for full API docs and examples.
100
- - `getStylesForLocator({ locator })`: gets the CSS styles applied to an element, similar to browser DevTools "Styles" panel. Read the `https://playwriter.dev/resources/styles-api.md` resource for full API docs and examples.
101
- - `getReactSource({ locator })`: gets the React component source location (file, line, column) for an element.
102
- - `locator`: a Playwright Locator or ElementHandle for the element to inspect
103
- - Returns: `{ fileName, lineNumber, columnNumber, componentName }` or `null` if not found
104
- - **Important**: Only works on **local dev servers** (localhost with Vite, Next.js, CRA in dev mode). Production builds strip source info.
105
-
106
- example:
107
+ Create new page:
107
108
 
108
- ```md
109
- - generic [active] [ref=e1]:
110
- - generic [ref=e2]:
111
- - banner [ref=e3]:
112
- - generic [ref=e5]:
113
- - link "shadcn/ui" [ref=e6] [cursor=pointer]:
114
- - /url: /
115
- - img
116
- - generic [ref=e11] [cursor=pointer]: shadcn/ui
117
- - navigation [ref=e12]:
118
- - link "Docs" [ref=e13] [cursor=pointer]:
119
- - /url: /docs/installation
120
- - link "Components" [ref=e14] [cursor=pointer]:
121
- - /url: /docs/components
109
+ ```js
110
+ state.newPage = await context.newPage();
111
+ await state.newPage.goto('https://example.com');
122
112
  ```
123
113
 
124
- Then you can use `page.locator(`aria-ref=${ref}`)` to get an element with a specific `ref` and interact with it.
114
+ ## common patterns
125
115
 
126
- `const componentsLink = page.locator('aria-ref=e14').click()`
116
+ **Popups** - capture before triggering:
127
117
 
128
- IMPORTANT: notice that we do not add any quotes in `aria-ref`! it MUST be called without quotes
118
+ ```js
119
+ const [popup] = await Promise.all([page.waitForEvent('popup'), page.click('a[target=_blank]')]);
120
+ await popup.waitForLoadState(); console.log('Popup URL:', popup.url());
121
+ ```
129
122
 
130
- ## getting a stable selector for an element (getLocatorStringForElement)
123
+ **Downloads** - capture and save:
131
124
 
132
- The `aria-ref` values from accessibility snapshots are ephemeral - they change on page reload and when components remount. Use `getLocatorStringForElement(element)` to get a stable Playwright locator string that you can reuse programmatically.
125
+ ```js
126
+ const [download] = await Promise.all([page.waitForEvent('download'), page.click('button.download')]);
127
+ await download.saveAs(`/tmp/${download.suggestedFilename()}`);
128
+ ```
133
129
 
134
- This is useful for:
135
- - Getting a selector you can store and reuse across page reloads
136
- - Finding similar elements in a list (modify the selector pattern)
137
- - Debugging which selector Playwright would use for an element
130
+ **iFrames** - use frameLocator:
138
131
 
139
132
  ```js
140
- const loc = page.locator('aria-ref=e14');
141
- const selector = await getLocatorStringForElement(loc);
142
- console.log(selector);
143
- // => "getByRole('button', { name: 'Save' })"
133
+ const frame = page.frameLocator('#my-iframe');
134
+ await frame.locator('button').click();
135
+ ```
136
+
137
+ **Dialogs** - handle alerts/confirms/prompts:
144
138
 
145
- // use the selector programmatically with eval:
146
- const stableLocator = page.getByRole('button', { name: 'Save' })
147
- await stableLocator.click();
139
+ ```js
140
+ page.on('dialog', async dialog => { console.log(dialog.message()); await dialog.accept(); });
141
+ await page.click('button.trigger-alert');
148
142
  ```
149
143
 
150
- ## pinned elements (user right-click to pin)
144
+ ## utility functions
151
145
 
152
- Users can right-click an element and select "Pin to Playwriter" to store it in `globalThis.playwriterPinnedElem1` (increments for each pin). The variable name is copied to clipboard.
146
+ **getLatestLogs** - retrieve captured browser console logs (up to 5000 per page, cleared on navigation):
153
147
 
154
148
  ```js
155
- const el = await page.evaluateHandle(() => globalThis.playwriterPinnedElem1);
156
- await el.click();
157
- const selector = await getLocatorStringForElement(el);
149
+ await getLatestLogs({ page?, count?, search? })
150
+ // Examples:
151
+ const errors = await getLatestLogs({ search: /error/i, count: 50 })
152
+ const pageLogs = await getLatestLogs({ page })
158
153
  ```
159
154
 
160
- ## finding specific elements with snapshot
155
+ For custom log collection across runs, store in state: `state.logs = []; page.on('console', m => state.logs.push(m.text()))`
156
+
157
+ **waitForPageLoad** - smart load detection that ignores analytics/ads:
161
158
 
162
- You can use `search` to find specific elements in the snapshot without reading the whole page structure. This is useful for finding forms, textareas, or specific text.
159
+ ```js
160
+ await waitForPageLoad({ page, timeout?, pollInterval?, minWait? })
161
+ // Returns: { success, readyState, pendingRequests, waitTimeMs, timedOut }
162
+ ```
163
163
 
164
- Example: find a textarea or form using case-insensitive regex:
164
+ **getCDPSession** - send raw CDP commands:
165
165
 
166
166
  ```js
167
- const snapshot = await accessibilitySnapshot({ page, search: /textarea|form/i })
168
- console.log(snapshot)
167
+ const cdp = await getCDPSession({ page });
168
+ const metrics = await cdp.send('Page.getLayoutMetrics');
169
169
  ```
170
170
 
171
- Example: find elements containing "Login":
171
+ **getLocatorStringForElement** - get stable selector from ephemeral aria-ref:
172
172
 
173
173
  ```js
174
- const snapshot = await accessibilitySnapshot({ page, search: "Login" })
175
- console.log(snapshot)
174
+ const selector = await getLocatorStringForElement(page.locator('aria-ref=e14'));
175
+ // => "getByRole('button', { name: 'Save' })"
176
176
  ```
177
177
 
178
- ## getting outputs of code execution
178
+ **getReactSource** - get React component source location (dev mode only):
179
179
 
180
- You can use `console.log` to print values you want to see in the tool call result. For seeing logs across runs you can store then in `state.logs` and then print them later, filtering and paginating them too.
180
+ ```js
181
+ const source = await getReactSource({ locator: page.locator('aria-ref=e5') });
182
+ // => { fileName, lineNumber, columnNumber, componentName }
183
+ ```
181
184
 
182
- ## using page.evaluate
185
+ **getStylesForLocator** - inspect CSS styles applied to an element, like browser DevTools "Styles" panel. Useful for debugging styling issues, finding where a CSS property is defined (file:line), and checking inherited styles. Returns selector, source location, and declarations for each matching rule. ALWAYS read `https://playwriter.dev/resources/styles-api.md` first.
183
186
 
184
- you can execute client side JavaScript code using `page.evaluate()`
187
+ ```js
188
+ const styles = await getStylesForLocator({ locator: page.locator('.btn'), cdp: await getCDPSession({ page }) });
189
+ console.log(formatStylesAsText(styles));
190
+ ```
185
191
 
186
- When executing code with `page.evaluate()`, return values directly from the evaluate function. Use `console.log()` outside of evaluate to display results:
192
+ **createDebugger** - set breakpoints, step through code, inspect variables at runtime. Useful for debugging issues that only reproduce in browser, understanding code flow, and inspecting state at specific points. Can pause on exceptions, evaluate expressions in scope, and blackbox framework code. ALWAYS read `https://playwriter.dev/resources/debugger-api.md` first.
187
193
 
188
194
  ```js
189
- // Get data from the page by returning it
190
- const title = await page.evaluate(() => document.title)
191
- console.log('Page title:', title)
192
-
193
- // Return multiple values as an object
194
- const pageInfo = await page.evaluate(() => ({
195
- url: window.location.href,
196
- buttonCount: document.querySelectorAll('button').length,
197
- readyState: document.readyState,
198
- }))
199
- console.log(pageInfo)
195
+ const cdp = await getCDPSession({ page }); const dbg = createDebugger({ cdp }); await dbg.enable();
196
+ const scripts = await dbg.listScripts({ search: 'app' });
197
+ await dbg.setBreakpoint({ file: scripts[0].url, line: 42 });
198
+ // when paused: dbg.inspectLocalVariables(), dbg.stepOver(), dbg.resume()
200
199
  ```
201
200
 
202
- ## read logs during interactions
201
+ **createEditor** - view and live-edit page scripts and CSS at runtime. Edits are in-memory (persist until reload). Useful for testing quick fixes, searching page scripts with grep, and toggling debug flags. ALWAYS read `https://playwriter.dev/resources/editor-api.md` first.
203
202
 
204
- you can see logs during interactions with `page.on('console', msg => console.log(`Browser log: [${msg.type()}] ${msg.text()}`))`
203
+ ```js
204
+ const cdp = await getCDPSession({ page }); const editor = createEditor({ cdp }); await editor.enable();
205
+ const matches = await editor.grep({ regex: /console\.log/ });
206
+ await editor.edit({ url: matches[0].url, oldString: 'DEBUG = false', newString: 'DEBUG = true' });
207
+ ```
205
208
 
206
- then remember to call `context.removeAllListeners()` or `page.removeAllListeners('console')` to not see logs in next execute calls.
209
+ ## pinned elements
207
210
 
208
- ## reading past logs
211
+ Users can right-click → "Copy Playwriter Element Reference" to store elements in `globalThis.playwriterPinnedElem1` (increments for each pin). The reference is copied to clipboard:
209
212
 
210
- you can keep track of logs using `state.logs = []; page.on('console', msg => state.logs.push({ type: msg.type(), text: msg.text() }))`
213
+ ```js
214
+ const el = await page.evaluateHandle(() => globalThis.playwriterPinnedElem1);
215
+ await el.click();
216
+ ```
211
217
 
212
- later, you can read logs that you care about. For example, to get the last 100 logs that contain the word "error":
218
+ ## page.evaluate
213
219
 
214
- `console.log('errors:'); state.logs.filter(log => log.type === 'error').slice(-100).forEach(x => console.log(x))`
220
+ Code inside `page.evaluate()` runs in the browser - use plain JavaScript only, no TypeScript syntax. Return values and log outside (console.log inside evaluate runs in browser, not visible):
215
221
 
216
- then to reset logs: `state.logs = []` and to stop listening: `page.removeAllListeners('console')`
222
+ ```js
223
+ const title = await page.evaluate(() => document.title);
224
+ console.log('Title:', title);
225
+
226
+ const info = await page.evaluate(() => ({
227
+ url: location.href,
228
+ buttons: document.querySelectorAll('button').length,
229
+ }));
230
+ console.log(info);
231
+ ```
217
232
 
218
- ## using getLatestLogs to read browser console logs
233
+ ## loading files
219
234
 
220
- The system automatically captures and stores up to 5000 browser console logs per page. Logs are automatically cleared when a page reloads or navigates to a new URL. You can retrieve logs using the `getLatestLogs` function:
235
+ Fill inputs with file content:
221
236
 
222
237
  ```js
223
- // Get all browser console logs from all pages (up to 5000 per page)
224
- const allLogs = await getLatestLogs()
225
- console.log(allLogs)
226
-
227
- // Get last 50 browser error logs
228
- const errorLogs = await getLatestLogs({ count: 50, search: /\[error\]/ })
229
- console.log(errorLogs)
238
+ const fs = require('node:fs'); const content = fs.readFileSync('./README.md', 'utf-8'); await page.locator('textarea').fill(content);
239
+ ```
230
240
 
231
- // Get all browser logs from the current page only
232
- const pageLogs = await getLatestLogs({ page })
233
- console.log(pageLogs)
241
+ ## network interception
234
242
 
235
- // Find browser logs containing specific text
236
- const authLogs = await getLatestLogs({ search: 'authentication failed' })
237
- console.log(authLogs)
243
+ For scraping or reverse-engineering APIs, intercept network requests instead of scrolling DOM. Store in `state` to analyze across calls:
238
244
 
239
- // Example output format:
240
- // [log] User clicked login button
241
- // [error] Failed to fetch /api/auth
242
- // [warn] Session expiring soon
245
+ ```js
246
+ state.requests = []; state.responses = [];
247
+ page.on('request', req => { if (req.url().includes('/api/')) state.requests.push({ url: req.url(), method: req.method(), headers: req.headers() }); });
248
+ page.on('response', async res => { if (res.url().includes('/api/')) { try { state.responses.push({ url: res.url(), status: res.status(), body: await res.json() }); } catch {} } });
243
249
  ```
244
250
 
245
- ## loading file content into inputs
251
+ Then trigger actions (scroll, click, navigate) and analyze captured data:
252
+
253
+ ```js
254
+ console.log('Captured', state.responses.length, 'API calls');
255
+ state.responses.forEach(r => console.log(r.status, r.url.slice(0, 80)));
256
+ ```
246
257
 
247
- you can use the `require` function to read files and fill inputs with their content:
258
+ Inspect a specific response to understand schema:
248
259
 
249
260
  ```js
250
- const fs = require('node:fs'); const content = fs.readFileSync('/path/to/file.txt', 'utf-8'); await page.locator('textarea').fill(content)
261
+ const resp = state.responses.find(r => r.url.includes('users'));
262
+ console.log(JSON.stringify(resp.body, null, 2).slice(0, 2000));
251
263
  ```
252
264
 
253
- for example, to fill a textarea with the content of a markdown file:
265
+ Replay API directly (useful for pagination):
254
266
 
255
267
  ```js
256
- const fs = require('node:fs'); const readme = fs.readFileSync('./README.md', 'utf-8'); await page.locator('#description').fill(readme); console.log('Filled textarea with README content')
268
+ const { url, headers } = state.requests.find(r => r.url.includes('feed'));
269
+ const data = await page.evaluate(async ({ url, headers }) => { const res = await fetch(url, { headers }); return res.json(); }, { url, headers });
270
+ console.log(data);
257
271
  ```
258
272
 
259
- if user seems happy or compliments playwriter ask them to review in chrome web store extension jfeammnjpkecdekppnclgkkffahnhfhe
273
+ Clean up listeners when done: `page.removeAllListeners('request'); page.removeAllListeners('response');`
274
+
275
+ ## capabilities
276
+
277
+ Examples of what playwriter can do:
278
+ - Monitor console logs while user reproduces a bug
279
+ - Intercept network requests to reverse-engineer APIs and build SDKs
280
+ - Scrape data by replaying paginated API calls instead of scrolling DOM
281
+ - Get accessibility snapshot to find elements, then automate interactions
282
+ - Debug issues by collecting logs and controlling the page simultaneously
283
+ - Handle popups, downloads, iframes, and dialog boxes
package/src/protocol.ts CHANGED
@@ -19,6 +19,7 @@ export type ExtensionCommandMessage = ForwardCDPCommand
19
19
 
20
20
  export type ExtensionResponseMessage = {
21
21
  id: number
22
+ method?: undefined
22
23
  result?: any
23
24
  error?: string
24
25
  }
@@ -30,6 +31,7 @@ export type ExtensionResponseMessage = {
30
31
  export type ExtensionEventMessage =
31
32
  {
32
33
  [K in keyof ProtocolMapping.Events]: {
34
+ id?: undefined
33
35
  method: 'forwardCDPEvent'
34
36
  params: {
35
37
  method: CDPEventFor<K>['method']
@@ -40,6 +42,7 @@ export type ExtensionEventMessage =
40
42
  }[keyof ProtocolMapping.Events]
41
43
 
42
44
  export type ExtensionLogMessage = {
45
+ id?: undefined
43
46
  method: 'log'
44
47
  params: {
45
48
  level: 'log' | 'debug' | 'info' | 'warn' | 'error'
@@ -47,4 +50,14 @@ export type ExtensionLogMessage = {
47
50
  }
48
51
  }
49
52
 
50
- export type ExtensionMessage = ExtensionResponseMessage | ExtensionEventMessage | ExtensionLogMessage
53
+ export type ExtensionPongMessage = {
54
+ id?: undefined
55
+ method: 'pong'
56
+ }
57
+
58
+ export type ServerPingMessage = {
59
+ method: 'ping'
60
+ id?: undefined
61
+ }
62
+
63
+ export type ExtensionMessage = ExtensionResponseMessage | ExtensionEventMessage | ExtensionLogMessage | ExtensionPongMessage
@@ -2,7 +2,7 @@ import fs from 'node:fs'
2
2
  import path from 'node:path'
3
3
  import { fileURLToPath } from 'node:url'
4
4
  import type { Page, Locator, ElementHandle } from 'playwright-core'
5
- import type { CDPSession } from './cdp-session.js'
5
+ import type { ICDPSession, CDPSession } from './cdp-session.js'
6
6
 
7
7
  export interface ReactSourceLocation {
8
8
  fileName: string | null
@@ -25,11 +25,13 @@ function getBippyCode(): string {
25
25
 
26
26
  export async function getReactSource({
27
27
  locator,
28
- cdp,
28
+ cdp: cdpSession,
29
29
  }: {
30
30
  locator: Locator | ElementHandle
31
- cdp: CDPSession
31
+ cdp: ICDPSession
32
32
  }): Promise<ReactSourceLocation | null> {
33
+ // Cast to CDPSession for internal type safety - at runtime both are compatible
34
+ const cdp = cdpSession as CDPSession
33
35
  const page: Page = 'page' in locator && typeof locator.page === 'function' ? locator.page() : (locator as any)._page
34
36
 
35
37
  if (!page) {
package/src/styles.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { CDPSession } from './cdp-session.js'
1
+ import type { ICDPSession, CDPSession } from './cdp-session.js'
2
2
  import type { Locator } from 'playwright-core'
3
3
 
4
4
  export interface StyleSource {
@@ -66,13 +66,15 @@ interface CSSStyleSheetHeader {
66
66
 
67
67
  export async function getStylesForLocator({
68
68
  locator,
69
- cdp,
69
+ cdp: cdpSession,
70
70
  includeUserAgentStyles = false,
71
71
  }: {
72
72
  locator: Locator
73
- cdp: CDPSession
73
+ cdp: ICDPSession
74
74
  includeUserAgentStyles?: boolean
75
75
  }): Promise<StylesResult> {
76
+ // Cast to CDPSession for internal type safety - at runtime both are compatible
77
+ const cdp = cdpSession as CDPSession
76
78
  await cdp.send('DOM.enable')
77
79
  await cdp.send('CSS.enable')
78
80