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.
- package/dist/cdp-relay.d.ts.map +1 -1
- package/dist/cdp-relay.js +22 -2
- package/dist/cdp-relay.js.map +1 -1
- package/dist/cdp-session.d.ts +24 -3
- package/dist/cdp-session.d.ts.map +1 -1
- package/dist/cdp-session.js +23 -0
- package/dist/cdp-session.js.map +1 -1
- package/dist/debugger-api.md +4 -3
- package/dist/debugger.d.ts +4 -3
- package/dist/debugger.d.ts.map +1 -1
- package/dist/debugger.js +3 -1
- package/dist/debugger.js.map +1 -1
- package/dist/editor-api.md +2 -2
- package/dist/editor.d.ts +2 -2
- package/dist/editor.d.ts.map +1 -1
- package/dist/editor.js +1 -0
- package/dist/editor.js.map +1 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +24 -0
- package/dist/mcp.js.map +1 -1
- package/dist/protocol.d.ts +12 -1
- package/dist/protocol.d.ts.map +1 -1
- package/dist/react-source.d.ts +3 -3
- package/dist/react-source.d.ts.map +1 -1
- package/dist/react-source.js +3 -1
- package/dist/react-source.js.map +1 -1
- package/dist/styles-api.md +3 -3
- package/dist/styles.d.ts +3 -3
- package/dist/styles.d.ts.map +1 -1
- package/dist/styles.js +3 -1
- package/dist/styles.js.map +1 -1
- package/package.json +1 -1
- package/src/cdp-relay.ts +23 -2
- package/src/cdp-session.ts +50 -3
- package/src/debugger.ts +6 -4
- package/src/editor.ts +4 -3
- package/src/index.ts +6 -0
- package/src/mcp.ts +32 -5
- package/src/prompt.md +192 -168
- package/src/protocol.ts +14 -1
- package/src/react-source.ts +5 -3
- package/src/styles.ts +5 -3
package/src/prompt.md
CHANGED
|
@@ -1,259 +1,283 @@
|
|
|
1
|
-
playwriter execute
|
|
1
|
+
# playwriter execute
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
5
|
+
You can collaborate with the user - they can help with captchas, difficult elements, or reproducing bugs.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
## context variables
|
|
8
8
|
|
|
9
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
28
|
+
After any action (click, submit, navigate), verify what happened:
|
|
24
29
|
|
|
25
|
-
|
|
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
|
-
|
|
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
|
-
|
|
30
|
-
-
|
|
31
|
-
-
|
|
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
|
-
|
|
46
|
+
Example output:
|
|
36
47
|
|
|
37
|
-
|
|
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
|
-
|
|
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
|
-
|
|
63
|
+
Search for specific elements:
|
|
49
64
|
|
|
50
|
-
|
|
65
|
+
```js
|
|
66
|
+
const snapshot = await accessibilitySnapshot({ page, search: /button|submit/i })
|
|
67
|
+
```
|
|
51
68
|
|
|
52
|
-
|
|
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
|
-
|
|
73
|
+
**For development** (when you have source code access), prefer stable selectors in this order:
|
|
60
74
|
|
|
61
|
-
|
|
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
|
-
|
|
82
|
+
Combine locators for precision:
|
|
64
83
|
|
|
65
|
-
|
|
84
|
+
```js
|
|
85
|
+
page.locator('tr').filter({ hasText: 'John' }).locator('button').click()
|
|
86
|
+
page.locator('button').nth(2).click()
|
|
87
|
+
```
|
|
66
88
|
|
|
67
|
-
|
|
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
|
-
##
|
|
97
|
+
## working with pages
|
|
71
98
|
|
|
72
|
-
|
|
99
|
+
Find a specific page:
|
|
73
100
|
|
|
74
|
-
|
|
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
|
-
|
|
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
|
-
```
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
114
|
+
## common patterns
|
|
125
115
|
|
|
126
|
-
|
|
116
|
+
**Popups** - capture before triggering:
|
|
127
117
|
|
|
128
|
-
|
|
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
|
-
|
|
123
|
+
**Downloads** - capture and save:
|
|
131
124
|
|
|
132
|
-
|
|
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
|
-
|
|
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
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
133
|
+
const frame = page.frameLocator('#my-iframe');
|
|
134
|
+
await frame.locator('button').click();
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
**Dialogs** - handle alerts/confirms/prompts:
|
|
144
138
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
await
|
|
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
|
-
##
|
|
144
|
+
## utility functions
|
|
151
145
|
|
|
152
|
-
|
|
146
|
+
**getLatestLogs** - retrieve captured browser console logs (up to 5000 per page, cleared on navigation):
|
|
153
147
|
|
|
154
148
|
```js
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
159
|
+
```js
|
|
160
|
+
await waitForPageLoad({ page, timeout?, pollInterval?, minWait? })
|
|
161
|
+
// Returns: { success, readyState, pendingRequests, waitTimeMs, timedOut }
|
|
162
|
+
```
|
|
163
163
|
|
|
164
|
-
|
|
164
|
+
**getCDPSession** - send raw CDP commands:
|
|
165
165
|
|
|
166
166
|
```js
|
|
167
|
-
const
|
|
168
|
-
|
|
167
|
+
const cdp = await getCDPSession({ page });
|
|
168
|
+
const metrics = await cdp.send('Page.getLayoutMetrics');
|
|
169
169
|
```
|
|
170
170
|
|
|
171
|
-
|
|
171
|
+
**getLocatorStringForElement** - get stable selector from ephemeral aria-ref:
|
|
172
172
|
|
|
173
173
|
```js
|
|
174
|
-
const
|
|
175
|
-
|
|
174
|
+
const selector = await getLocatorStringForElement(page.locator('aria-ref=e14'));
|
|
175
|
+
// => "getByRole('button', { name: 'Save' })"
|
|
176
176
|
```
|
|
177
177
|
|
|
178
|
-
|
|
178
|
+
**getReactSource** - get React component source location (dev mode only):
|
|
179
179
|
|
|
180
|
-
|
|
180
|
+
```js
|
|
181
|
+
const source = await getReactSource({ locator: page.locator('aria-ref=e5') });
|
|
182
|
+
// => { fileName, lineNumber, columnNumber, componentName }
|
|
183
|
+
```
|
|
181
184
|
|
|
182
|
-
|
|
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
|
-
|
|
187
|
+
```js
|
|
188
|
+
const styles = await getStylesForLocator({ locator: page.locator('.btn'), cdp: await getCDPSession({ page }) });
|
|
189
|
+
console.log(formatStylesAsText(styles));
|
|
190
|
+
```
|
|
185
191
|
|
|
186
|
-
|
|
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
|
-
|
|
190
|
-
const
|
|
191
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
209
|
+
## pinned elements
|
|
207
210
|
|
|
208
|
-
|
|
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
|
-
|
|
213
|
+
```js
|
|
214
|
+
const el = await page.evaluateHandle(() => globalThis.playwriterPinnedElem1);
|
|
215
|
+
await el.click();
|
|
216
|
+
```
|
|
211
217
|
|
|
212
|
-
|
|
218
|
+
## page.evaluate
|
|
213
219
|
|
|
214
|
-
`
|
|
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
|
-
|
|
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
|
-
##
|
|
233
|
+
## loading files
|
|
219
234
|
|
|
220
|
-
|
|
235
|
+
Fill inputs with file content:
|
|
221
236
|
|
|
222
237
|
```js
|
|
223
|
-
|
|
224
|
-
|
|
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
|
-
|
|
232
|
-
const pageLogs = await getLatestLogs({ page })
|
|
233
|
-
console.log(pageLogs)
|
|
241
|
+
## network interception
|
|
234
242
|
|
|
235
|
-
|
|
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
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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
|
-
|
|
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
|
-
|
|
258
|
+
Inspect a specific response to understand schema:
|
|
248
259
|
|
|
249
260
|
```js
|
|
250
|
-
const
|
|
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
|
-
|
|
265
|
+
Replay API directly (useful for pagination):
|
|
254
266
|
|
|
255
267
|
```js
|
|
256
|
-
const
|
|
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
|
-
|
|
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
|
|
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
|
package/src/react-source.ts
CHANGED
|
@@ -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:
|
|
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:
|
|
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
|
|