e2e-pilot 0.0.69
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/bin.js +3 -0
- package/dist/aria-snapshot.d.ts +95 -0
- package/dist/aria-snapshot.d.ts.map +1 -0
- package/dist/aria-snapshot.js +490 -0
- package/dist/aria-snapshot.js.map +1 -0
- package/dist/bippy.js +971 -0
- package/dist/cdp-relay.d.ts +16 -0
- package/dist/cdp-relay.d.ts.map +1 -0
- package/dist/cdp-relay.js +715 -0
- package/dist/cdp-relay.js.map +1 -0
- package/dist/cdp-session.d.ts +42 -0
- package/dist/cdp-session.d.ts.map +1 -0
- package/dist/cdp-session.js +154 -0
- package/dist/cdp-session.js.map +1 -0
- package/dist/cdp-types.d.ts +63 -0
- package/dist/cdp-types.d.ts.map +1 -0
- package/dist/cdp-types.js +91 -0
- package/dist/cdp-types.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +213 -0
- package/dist/cli.js.map +1 -0
- package/dist/create-logger.d.ts +9 -0
- package/dist/create-logger.d.ts.map +1 -0
- package/dist/create-logger.js +25 -0
- package/dist/create-logger.js.map +1 -0
- package/dist/debugger-api.md +458 -0
- package/dist/debugger-examples-types.d.ts +24 -0
- package/dist/debugger-examples-types.d.ts.map +1 -0
- package/dist/debugger-examples-types.js +2 -0
- package/dist/debugger-examples-types.js.map +1 -0
- package/dist/debugger-examples.d.ts +6 -0
- package/dist/debugger-examples.d.ts.map +1 -0
- package/dist/debugger-examples.js +53 -0
- package/dist/debugger-examples.js.map +1 -0
- package/dist/debugger.d.ts +381 -0
- package/dist/debugger.d.ts.map +1 -0
- package/dist/debugger.js +633 -0
- package/dist/debugger.js.map +1 -0
- package/dist/editor-api.md +364 -0
- package/dist/editor-examples.d.ts +11 -0
- package/dist/editor-examples.d.ts.map +1 -0
- package/dist/editor-examples.js +124 -0
- package/dist/editor-examples.js.map +1 -0
- package/dist/editor.d.ts +203 -0
- package/dist/editor.d.ts.map +1 -0
- package/dist/editor.js +336 -0
- package/dist/editor.js.map +1 -0
- package/dist/execute.d.ts +50 -0
- package/dist/execute.d.ts.map +1 -0
- package/dist/execute.js +576 -0
- package/dist/execute.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp-client.d.ts +20 -0
- package/dist/mcp-client.d.ts.map +1 -0
- package/dist/mcp-client.js +56 -0
- package/dist/mcp-client.js.map +1 -0
- package/dist/mcp.d.ts +5 -0
- package/dist/mcp.d.ts.map +1 -0
- package/dist/mcp.js +720 -0
- package/dist/mcp.js.map +1 -0
- package/dist/mcp.test.d.ts +10 -0
- package/dist/mcp.test.d.ts.map +1 -0
- package/dist/mcp.test.js +2999 -0
- package/dist/mcp.test.js.map +1 -0
- package/dist/network-capture.d.ts +23 -0
- package/dist/network-capture.d.ts.map +1 -0
- package/dist/network-capture.js +98 -0
- package/dist/network-capture.js.map +1 -0
- package/dist/protocol.d.ts +54 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +2 -0
- package/dist/protocol.js.map +1 -0
- package/dist/react-source.d.ts +13 -0
- package/dist/react-source.d.ts.map +1 -0
- package/dist/react-source.js +68 -0
- package/dist/react-source.js.map +1 -0
- package/dist/scoped-fs.d.ts +94 -0
- package/dist/scoped-fs.d.ts.map +1 -0
- package/dist/scoped-fs.js +356 -0
- package/dist/scoped-fs.js.map +1 -0
- package/dist/selector-generator.js +8126 -0
- package/dist/start-relay-server.d.ts +6 -0
- package/dist/start-relay-server.d.ts.map +1 -0
- package/dist/start-relay-server.js +33 -0
- package/dist/start-relay-server.js.map +1 -0
- package/dist/styles-api.md +117 -0
- package/dist/styles-examples.d.ts +8 -0
- package/dist/styles-examples.d.ts.map +1 -0
- package/dist/styles-examples.js +64 -0
- package/dist/styles-examples.js.map +1 -0
- package/dist/styles.d.ts +27 -0
- package/dist/styles.d.ts.map +1 -0
- package/dist/styles.js +234 -0
- package/dist/styles.js.map +1 -0
- package/dist/trace-utils.d.ts +14 -0
- package/dist/trace-utils.d.ts.map +1 -0
- package/dist/trace-utils.js +21 -0
- package/dist/trace-utils.js.map +1 -0
- package/dist/utils.d.ts +20 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +75 -0
- package/dist/utils.js.map +1 -0
- package/dist/wait-for-page-load.d.ts +16 -0
- package/dist/wait-for-page-load.d.ts.map +1 -0
- package/dist/wait-for-page-load.js +127 -0
- package/dist/wait-for-page-load.js.map +1 -0
- package/package.json +67 -0
- package/src/aria-snapshot.ts +610 -0
- package/src/assets/aria-labels-github-snapshot.txt +605 -0
- package/src/assets/aria-labels-github.png +0 -0
- package/src/assets/aria-labels-google-snapshot.txt +49 -0
- package/src/assets/aria-labels-google.png +0 -0
- package/src/assets/aria-labels-hacker-news-snapshot.txt +1023 -0
- package/src/assets/aria-labels-hacker-news.png +0 -0
- package/src/cdp-relay.ts +925 -0
- package/src/cdp-session.ts +203 -0
- package/src/cdp-timing.md +128 -0
- package/src/cdp-types.ts +155 -0
- package/src/cli.ts +250 -0
- package/src/create-logger.ts +36 -0
- package/src/debugger-examples-types.ts +13 -0
- package/src/debugger-examples.ts +66 -0
- package/src/debugger.md +453 -0
- package/src/debugger.ts +713 -0
- package/src/editor-examples.ts +148 -0
- package/src/editor.ts +390 -0
- package/src/execute.ts +763 -0
- package/src/index.ts +10 -0
- package/src/mcp-client.ts +78 -0
- package/src/mcp.test.ts +3596 -0
- package/src/mcp.ts +876 -0
- package/src/network-capture.ts +140 -0
- package/src/prompt.bak.md +323 -0
- package/src/prompt.md +7 -0
- package/src/protocol.ts +63 -0
- package/src/react-source.ts +94 -0
- package/src/resource.md +436 -0
- package/src/scoped-fs.ts +411 -0
- package/src/snapshots/hacker-news-focused-accessibility.md +202 -0
- package/src/snapshots/hacker-news-initial-accessibility.md +11 -0
- package/src/snapshots/hacker-news-tabbed-accessibility.md +202 -0
- package/src/snapshots/shadcn-ui-accessibility.md +11 -0
- package/src/start-relay-server.ts +43 -0
- package/src/styles-examples.ts +77 -0
- package/src/styles.ts +345 -0
- package/src/trace-utils.ts +43 -0
- package/src/utils.ts +91 -0
- package/src/wait-for-page-load.ts +174 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import type { Page, BrowserContext, Response } from 'playwright-core'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
|
|
5
|
+
export interface NetworkRequest {
|
|
6
|
+
ts: number
|
|
7
|
+
pageIndex: number
|
|
8
|
+
pageTitle: string
|
|
9
|
+
method: string
|
|
10
|
+
url: string
|
|
11
|
+
status: number
|
|
12
|
+
reqBody: string | Record<string, unknown> | null
|
|
13
|
+
resBody: string | Record<string, unknown> | null
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function tryParseJson(text: string): string | Record<string, unknown> {
|
|
17
|
+
try {
|
|
18
|
+
const parsed = JSON.parse(text)
|
|
19
|
+
if (typeof parsed === 'object' && parsed !== null) {
|
|
20
|
+
return parsed
|
|
21
|
+
}
|
|
22
|
+
return text
|
|
23
|
+
} catch {
|
|
24
|
+
return text
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const MAX_REQUEST_BODY_SIZE = 1024 * 1024 // 1mb
|
|
29
|
+
|
|
30
|
+
async function captureNetworkRequest({
|
|
31
|
+
response,
|
|
32
|
+
pageIndex,
|
|
33
|
+
pageTitle,
|
|
34
|
+
}: {
|
|
35
|
+
response: Response
|
|
36
|
+
pageIndex: number
|
|
37
|
+
pageTitle: string
|
|
38
|
+
}): Promise<NetworkRequest | null> {
|
|
39
|
+
const request = response.request()
|
|
40
|
+
const resourceType = request.resourceType()
|
|
41
|
+
|
|
42
|
+
// Only capture XHR/fetch (API calls)
|
|
43
|
+
if (resourceType !== 'xhr' && resourceType !== 'fetch') {
|
|
44
|
+
return null
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let reqBody: string | Record<string, unknown> | null = null
|
|
48
|
+
let resBody: string | Record<string, unknown> | null = null
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const postData = request.postData()
|
|
52
|
+
if (postData) {
|
|
53
|
+
const truncated =
|
|
54
|
+
postData.length > MAX_REQUEST_BODY_SIZE ? postData.slice(0, MAX_REQUEST_BODY_SIZE) + '...' : postData
|
|
55
|
+
reqBody = tryParseJson(truncated)
|
|
56
|
+
}
|
|
57
|
+
} catch {
|
|
58
|
+
// Ignore errors getting request body
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
const body = await response.text()
|
|
63
|
+
if (body) {
|
|
64
|
+
const truncated = body.length > MAX_REQUEST_BODY_SIZE ? body.slice(0, MAX_REQUEST_BODY_SIZE) + '...' : body
|
|
65
|
+
resBody = tryParseJson(truncated)
|
|
66
|
+
}
|
|
67
|
+
} catch {
|
|
68
|
+
// Ignore errors getting response body
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
ts: Date.now(),
|
|
73
|
+
pageIndex,
|
|
74
|
+
pageTitle,
|
|
75
|
+
method: request.method(),
|
|
76
|
+
url: request.url(),
|
|
77
|
+
status: response.status(),
|
|
78
|
+
reqBody,
|
|
79
|
+
resBody,
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface NetworkCapture {
|
|
84
|
+
requests: NetworkRequest[]
|
|
85
|
+
cleanup: () => void
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function setupNetworkCapture({ context }: { context: BrowserContext }): NetworkCapture {
|
|
89
|
+
const requests: NetworkRequest[] = []
|
|
90
|
+
const allPages = context.pages()
|
|
91
|
+
|
|
92
|
+
const createHandler = (page: Page, pageIndex: number) => {
|
|
93
|
+
return async (response: Response) => {
|
|
94
|
+
let pageTitle = ''
|
|
95
|
+
try {
|
|
96
|
+
pageTitle = await page.title()
|
|
97
|
+
} catch {
|
|
98
|
+
// Page might be closed or navigating
|
|
99
|
+
}
|
|
100
|
+
const captured = await captureNetworkRequest({ response, pageIndex, pageTitle })
|
|
101
|
+
if (captured) {
|
|
102
|
+
requests.push(captured)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const handlers = new Map<Page, (response: Response) => Promise<void>>()
|
|
108
|
+
|
|
109
|
+
for (let i = 0; i < allPages.length; i++) {
|
|
110
|
+
const page = allPages[i]
|
|
111
|
+
const handler = createHandler(page, i)
|
|
112
|
+
handlers.set(page, handler)
|
|
113
|
+
page.on('response', handler)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const cleanup = () => {
|
|
117
|
+
for (const [page, handler] of handlers) {
|
|
118
|
+
page.off('response', handler)
|
|
119
|
+
}
|
|
120
|
+
handlers.clear()
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return { requests, cleanup }
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function saveNetworkRequests({
|
|
127
|
+
requests,
|
|
128
|
+
snapshotDir,
|
|
129
|
+
}: {
|
|
130
|
+
requests: NetworkRequest[]
|
|
131
|
+
snapshotDir: string
|
|
132
|
+
}): string | null {
|
|
133
|
+
if (requests.length === 0) {
|
|
134
|
+
return null
|
|
135
|
+
}
|
|
136
|
+
const networksPath = path.join(snapshotDir, 'networks.jsonl')
|
|
137
|
+
const jsonlContent = requests.map((req) => JSON.stringify(req)).join('\n')
|
|
138
|
+
fs.writeFileSync(networksPath, jsonlContent, 'utf-8')
|
|
139
|
+
return networksPath
|
|
140
|
+
}
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
# e2e-pilot execute
|
|
2
|
+
|
|
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 E2E Pilot extension icon on the tab they want to control.
|
|
4
|
+
|
|
5
|
+
You can collaborate with the user - they can help with captchas, difficult elements, or reproducing bugs.
|
|
6
|
+
|
|
7
|
+
## context variables
|
|
8
|
+
|
|
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.js modules like fs
|
|
13
|
+
- Node.js globals: `setTimeout`, `setInterval`, `fetch`, `URL`, `Buffer`, `crypto`, etc.
|
|
14
|
+
|
|
15
|
+
## rules
|
|
16
|
+
|
|
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 e2e-pilot 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
|
|
25
|
+
|
|
26
|
+
## checking page state
|
|
27
|
+
|
|
28
|
+
After any action (click, submit, navigate), verify what happened:
|
|
29
|
+
|
|
30
|
+
```js
|
|
31
|
+
console.log('url:', page.url()); console.log(await accessibilitySnapshot({ page }).then(x => x.split('\n').slice(0, 30).join('\n')));
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
For visually complex pages (grids, galleries, dashboards), use `screenshotWithAccessibilityLabels({ page })` instead to understand spatial layout.
|
|
35
|
+
|
|
36
|
+
If nothing changed, try `await page.waitForLoadState('networkidle', {timeout: 3000})` or you may have clicked the wrong element.
|
|
37
|
+
|
|
38
|
+
## accessibility snapshots
|
|
39
|
+
|
|
40
|
+
```js
|
|
41
|
+
await accessibilitySnapshot({ page, search?, contextLines?, showDiffSinceLastCall? })
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
- `search` - string/regex to filter results (returns first 10 matches with context)
|
|
45
|
+
- `contextLines` - lines of context around matches (default: 10)
|
|
46
|
+
- `showDiffSinceLastCall` - returns diff since last snapshot (useful after actions)
|
|
47
|
+
|
|
48
|
+
Example output:
|
|
49
|
+
|
|
50
|
+
```md
|
|
51
|
+
- banner [ref=e3]:
|
|
52
|
+
- link "Home" [ref=e5] [cursor=pointer]:
|
|
53
|
+
- /url: /
|
|
54
|
+
- navigation [ref=e12]:
|
|
55
|
+
- link "Docs" [ref=e13] [cursor=pointer]:
|
|
56
|
+
- /url: /docs
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Use `aria-ref` to interact - **no quotes around the ref value**:
|
|
60
|
+
|
|
61
|
+
```js
|
|
62
|
+
await page.locator('aria-ref=e13').click()
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Search for specific elements:
|
|
66
|
+
|
|
67
|
+
```js
|
|
68
|
+
const snapshot = await accessibilitySnapshot({ page, search: /button|submit/i })
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## choosing between snapshot methods
|
|
72
|
+
|
|
73
|
+
Both `accessibilitySnapshot` and `screenshotWithAccessibilityLabels` use the same `aria-ref` system, so you can combine them effectively.
|
|
74
|
+
|
|
75
|
+
**Use `accessibilitySnapshot` when:**
|
|
76
|
+
- Page has simple, semantic structure (articles, forms, lists)
|
|
77
|
+
- You need to search for specific text or patterns
|
|
78
|
+
- Token usage matters (text is smaller than images)
|
|
79
|
+
- You need to process the output programmatically
|
|
80
|
+
|
|
81
|
+
**Use `screenshotWithAccessibilityLabels` when:**
|
|
82
|
+
- Page has complex visual layout (grids, galleries, dashboards, maps)
|
|
83
|
+
- Spatial position matters (e.g., "first image", "top-left button")
|
|
84
|
+
- DOM order doesn't match visual order
|
|
85
|
+
- You need to understand the visual hierarchy
|
|
86
|
+
|
|
87
|
+
**Combining both:** Use screenshot first to understand layout and identify target elements visually, then use `accessibilitySnapshot({ search: /pattern/ })` for efficient searching in subsequent calls.
|
|
88
|
+
|
|
89
|
+
## selector best practices
|
|
90
|
+
|
|
91
|
+
**For unknown websites**: use `accessibilitySnapshot()` with `aria-ref` - it shows what's actually interactive.
|
|
92
|
+
|
|
93
|
+
**For development** (when you have source code access), prefer stable selectors in this order:
|
|
94
|
+
|
|
95
|
+
1. **Best**: `[data-testid="submit"]` - explicit test attributes, never change accidentally
|
|
96
|
+
2. **Good**: `getByRole('button', { name: 'Save' })` - accessible, semantic
|
|
97
|
+
3. **Good**: `getByText('Sign in')`, `getByLabel('Email')` - readable, user-facing
|
|
98
|
+
4. **OK**: `input[name="email"]`, `button[type="submit"]` - semantic HTML
|
|
99
|
+
5. **Avoid**: `.btn-primary`, `#submit` - classes/IDs change frequently
|
|
100
|
+
6. **Last resort**: `div.container > form > button` - fragile, breaks easily
|
|
101
|
+
|
|
102
|
+
Combine locators for precision:
|
|
103
|
+
|
|
104
|
+
```js
|
|
105
|
+
page.locator('tr').filter({ hasText: 'John' }).locator('button').click()
|
|
106
|
+
page.locator('button').nth(2).click()
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
If a locator matches multiple elements, Playwright throws "strict mode violation". Use `.first()`, `.last()`, or `.nth(n)`:
|
|
110
|
+
|
|
111
|
+
```js
|
|
112
|
+
await page.locator('button').first().click() // first match
|
|
113
|
+
await page.locator('.item').last().click() // last match
|
|
114
|
+
await page.locator('li').nth(3).click() // 4th item (0-indexed)
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## working with pages
|
|
118
|
+
|
|
119
|
+
Find a specific page:
|
|
120
|
+
|
|
121
|
+
```js
|
|
122
|
+
const pages = context.pages().filter(x => x.url().includes('localhost'));
|
|
123
|
+
if (pages.length !== 1) throw new Error(`Expected 1 page, found ${pages.length}`);
|
|
124
|
+
state.targetPage = pages[0];
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Create new page:
|
|
128
|
+
|
|
129
|
+
```js
|
|
130
|
+
state.newPage = await context.newPage();
|
|
131
|
+
await state.newPage.goto('https://example.com');
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## common patterns
|
|
135
|
+
|
|
136
|
+
**Popups** - capture before triggering:
|
|
137
|
+
|
|
138
|
+
```js
|
|
139
|
+
const [popup] = await Promise.all([page.waitForEvent('popup'), page.click('a[target=_blank]')]);
|
|
140
|
+
await popup.waitForLoadState(); console.log('Popup URL:', popup.url());
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
**Downloads** - capture and save:
|
|
144
|
+
|
|
145
|
+
```js
|
|
146
|
+
const [download] = await Promise.all([page.waitForEvent('download'), page.click('button.download')]);
|
|
147
|
+
await download.saveAs(`/tmp/${download.suggestedFilename()}`);
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
**iFrames** - use frameLocator:
|
|
151
|
+
|
|
152
|
+
```js
|
|
153
|
+
const frame = page.frameLocator('#my-iframe');
|
|
154
|
+
await frame.locator('button').click();
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
**Dialogs** - handle alerts/confirms/prompts:
|
|
158
|
+
|
|
159
|
+
```js
|
|
160
|
+
page.on('dialog', async dialog => { console.log(dialog.message()); await dialog.accept(); });
|
|
161
|
+
await page.click('button.trigger-alert');
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## utility functions
|
|
165
|
+
|
|
166
|
+
**getLatestLogs** - retrieve captured browser console logs (up to 5000 per page, cleared on navigation):
|
|
167
|
+
|
|
168
|
+
```js
|
|
169
|
+
await getLatestLogs({ page?, count?, search? })
|
|
170
|
+
// Examples:
|
|
171
|
+
const errors = await getLatestLogs({ search: /error/i, count: 50 })
|
|
172
|
+
const pageLogs = await getLatestLogs({ page })
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
For custom log collection across runs, store in state: `state.logs = []; page.on('console', m => state.logs.push(m.text()))`
|
|
176
|
+
|
|
177
|
+
**waitForPageLoad** - smart load detection that ignores analytics/ads:
|
|
178
|
+
|
|
179
|
+
```js
|
|
180
|
+
await waitForPageLoad({ page, timeout?, pollInterval?, minWait? })
|
|
181
|
+
// Returns: { success, readyState, pendingRequests, waitTimeMs, timedOut }
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
**getCDPSession** - send raw CDP commands:
|
|
185
|
+
|
|
186
|
+
```js
|
|
187
|
+
const cdp = await getCDPSession({ page });
|
|
188
|
+
const metrics = await cdp.send('Page.getLayoutMetrics');
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
**getLocatorStringForElement** - get stable selector from ephemeral aria-ref:
|
|
192
|
+
|
|
193
|
+
```js
|
|
194
|
+
const selector = await getLocatorStringForElement(page.locator('aria-ref=e14'));
|
|
195
|
+
// => "getByRole('button', { name: 'Save' })"
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
**getReactSource** - get React component source location (dev mode only):
|
|
199
|
+
|
|
200
|
+
```js
|
|
201
|
+
const source = await getReactSource({ locator: page.locator('aria-ref=e5') });
|
|
202
|
+
// => { fileName, lineNumber, columnNumber, componentName }
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
**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.
|
|
206
|
+
|
|
207
|
+
```js
|
|
208
|
+
const styles = await getStylesForLocator({ locator: page.locator('.btn'), cdp: await getCDPSession({ page }) });
|
|
209
|
+
console.log(formatStylesAsText(styles));
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
**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.
|
|
213
|
+
|
|
214
|
+
```js
|
|
215
|
+
const cdp = await getCDPSession({ page }); const dbg = createDebugger({ cdp }); await dbg.enable();
|
|
216
|
+
const scripts = await dbg.listScripts({ search: 'app' });
|
|
217
|
+
await dbg.setBreakpoint({ file: scripts[0].url, line: 42 });
|
|
218
|
+
// when paused: dbg.inspectLocalVariables(), dbg.stepOver(), dbg.resume()
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
**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.
|
|
222
|
+
|
|
223
|
+
```js
|
|
224
|
+
const cdp = await getCDPSession({ page }); const editor = createEditor({ cdp }); await editor.enable();
|
|
225
|
+
const matches = await editor.grep({ regex: /console\.log/ });
|
|
226
|
+
await editor.edit({ url: matches[0].url, oldString: 'DEBUG = false', newString: 'DEBUG = true' });
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
**screenshotWithAccessibilityLabels** - take a screenshot with Vimium-style visual labels overlaid on interactive elements. Shows labels, captures screenshot, then removes labels. The image and accessibility snapshot are automatically included in the response. Can be called multiple times to capture multiple screenshots. Use a timeout of **20 seconds** for complex pages.
|
|
230
|
+
|
|
231
|
+
Prefer this for pages with grids, image galleries, maps, or complex visual layouts where spatial position matters. For simple text-heavy pages, `accessibilitySnapshot` with search is faster and uses fewer tokens.
|
|
232
|
+
|
|
233
|
+
```js
|
|
234
|
+
await screenshotWithAccessibilityLabels({ page });
|
|
235
|
+
// Image and accessibility snapshot are automatically included in response
|
|
236
|
+
// Use aria-ref from snapshot to interact with elements
|
|
237
|
+
await page.locator('aria-ref=e5').click();
|
|
238
|
+
|
|
239
|
+
// Can take multiple screenshots in one execution
|
|
240
|
+
await screenshotWithAccessibilityLabels({ page });
|
|
241
|
+
await page.click('button');
|
|
242
|
+
await screenshotWithAccessibilityLabels({ page });
|
|
243
|
+
// Both images are included in the response
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
Labels are color-coded: yellow=links, orange=buttons, coral=inputs, pink=checkboxes, peach=sliders, salmon=menus, amber=tabs.
|
|
247
|
+
|
|
248
|
+
## pinned elements
|
|
249
|
+
|
|
250
|
+
Users can right-click → "Copy E2E Pilot Element Reference" to store elements in `globalThis.e2ePilotPinnedElem1` (increments for each pin). The reference is copied to clipboard:
|
|
251
|
+
|
|
252
|
+
```js
|
|
253
|
+
const el = await page.evaluateHandle(() => globalThis.e2ePilotPinnedElem1);
|
|
254
|
+
await el.click();
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
## page.evaluate
|
|
258
|
+
|
|
259
|
+
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):
|
|
260
|
+
|
|
261
|
+
```js
|
|
262
|
+
const title = await page.evaluate(() => document.title);
|
|
263
|
+
console.log('Title:', title);
|
|
264
|
+
|
|
265
|
+
const info = await page.evaluate(() => ({
|
|
266
|
+
url: location.href,
|
|
267
|
+
buttons: document.querySelectorAll('button').length,
|
|
268
|
+
}));
|
|
269
|
+
console.log(info);
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
## loading files
|
|
273
|
+
|
|
274
|
+
Fill inputs with file content:
|
|
275
|
+
|
|
276
|
+
```js
|
|
277
|
+
const fs = require('node:fs'); const content = fs.readFileSync('./data.txt', 'utf-8'); await page.locator('textarea').fill(content);
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
## network interception
|
|
281
|
+
|
|
282
|
+
For scraping or reverse-engineering APIs, intercept network requests instead of scrolling DOM. Store in `state` to analyze across calls:
|
|
283
|
+
|
|
284
|
+
```js
|
|
285
|
+
state.requests = []; state.responses = [];
|
|
286
|
+
page.on('request', req => { if (req.url().includes('/api/')) state.requests.push({ url: req.url(), method: req.method(), headers: req.headers() }); });
|
|
287
|
+
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 {} } });
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
Then trigger actions (scroll, click, navigate) and analyze captured data:
|
|
291
|
+
|
|
292
|
+
```js
|
|
293
|
+
console.log('Captured', state.responses.length, 'API calls');
|
|
294
|
+
state.responses.forEach(r => console.log(r.status, r.url.slice(0, 80)));
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
Inspect a specific response to understand schema:
|
|
298
|
+
|
|
299
|
+
```js
|
|
300
|
+
const resp = state.responses.find(r => r.url.includes('users'));
|
|
301
|
+
console.log(JSON.stringify(resp.body, null, 2).slice(0, 2000));
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
Replay API directly (useful for pagination):
|
|
305
|
+
|
|
306
|
+
```js
|
|
307
|
+
const { url, headers } = state.requests.find(r => r.url.includes('feed'));
|
|
308
|
+
const data = await page.evaluate(async ({ url, headers }) => { const res = await fetch(url, { headers }); return res.json(); }, { url, headers });
|
|
309
|
+
console.log(data);
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
Clean up listeners when done: `page.removeAllListeners('request'); page.removeAllListeners('response');`
|
|
313
|
+
|
|
314
|
+
## capabilities
|
|
315
|
+
|
|
316
|
+
Examples of what e2e-pilot can do:
|
|
317
|
+
- Monitor console logs while user reproduces a bug
|
|
318
|
+
- Intercept network requests to reverse-engineer APIs and build SDKs
|
|
319
|
+
- Scrape data by replaying paginated API calls instead of scrolling DOM
|
|
320
|
+
- Get accessibility snapshot to find elements, then automate interactions
|
|
321
|
+
- Use visual screenshots to understand complex layouts like image grids, dashboards, or maps
|
|
322
|
+
- Debug issues by collecting logs and controlling the page simultaneously
|
|
323
|
+
- Handle popups, downloads, iframes, and dialog boxes
|
package/src/prompt.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Control user's Chrome browser via playwright code snippets. If you get "Extension not running" error, tell user to click the E2E Pilot extension icon on the tab they want to control.
|
|
2
|
+
|
|
3
|
+
Each `execute` call saves accessibility snapshots to `.e2e-pilot/snapshots/`. Read these files to:
|
|
4
|
+
|
|
5
|
+
- Understand page structure before acting
|
|
6
|
+
- Find element locators (look for `aria-ref` values)
|
|
7
|
+
- Debug when actions fail
|
package/src/protocol.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { CDPEventFor, ProtocolMapping } from './cdp-types.js'
|
|
2
|
+
|
|
3
|
+
export const VERSION = 1
|
|
4
|
+
|
|
5
|
+
type ForwardCDPCommand =
|
|
6
|
+
{
|
|
7
|
+
[K in keyof ProtocolMapping.Commands]: {
|
|
8
|
+
id: number
|
|
9
|
+
method: 'forwardCDPCommand'
|
|
10
|
+
params: {
|
|
11
|
+
method: K
|
|
12
|
+
sessionId?: string
|
|
13
|
+
params?: ProtocolMapping.Commands[K]['paramsType'][0]
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}[keyof ProtocolMapping.Commands]
|
|
17
|
+
|
|
18
|
+
export type ExtensionCommandMessage = ForwardCDPCommand
|
|
19
|
+
|
|
20
|
+
export type ExtensionResponseMessage = {
|
|
21
|
+
id: number
|
|
22
|
+
method?: undefined
|
|
23
|
+
result?: any
|
|
24
|
+
error?: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* This produces a discriminated union for narrowing, similar to ForwardCDPCommand,
|
|
29
|
+
* but for forwarded CDP events. Uses CDPEvent to maintain proper type extraction.
|
|
30
|
+
*/
|
|
31
|
+
export type ExtensionEventMessage =
|
|
32
|
+
{
|
|
33
|
+
[K in keyof ProtocolMapping.Events]: {
|
|
34
|
+
id?: undefined
|
|
35
|
+
method: 'forwardCDPEvent'
|
|
36
|
+
params: {
|
|
37
|
+
method: CDPEventFor<K>['method']
|
|
38
|
+
sessionId?: string
|
|
39
|
+
params?: CDPEventFor<K>['params']
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}[keyof ProtocolMapping.Events]
|
|
43
|
+
|
|
44
|
+
export type ExtensionLogMessage = {
|
|
45
|
+
id?: undefined
|
|
46
|
+
method: 'log'
|
|
47
|
+
params: {
|
|
48
|
+
level: 'log' | 'debug' | 'info' | 'warn' | 'error'
|
|
49
|
+
args: string[]
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
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
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
import type { Page, Locator, ElementHandle } from 'playwright-core'
|
|
5
|
+
import type { ICDPSession, CDPSession } from './cdp-session.js'
|
|
6
|
+
|
|
7
|
+
export interface ReactSourceLocation {
|
|
8
|
+
fileName: string | null
|
|
9
|
+
lineNumber: number | null
|
|
10
|
+
columnNumber: number | null
|
|
11
|
+
componentName: string | null
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let bippyCode: string | null = null
|
|
15
|
+
|
|
16
|
+
function getBippyCode(): string {
|
|
17
|
+
if (bippyCode) {
|
|
18
|
+
return bippyCode
|
|
19
|
+
}
|
|
20
|
+
const currentDir = path.dirname(fileURLToPath(import.meta.url))
|
|
21
|
+
const bippyPath = path.join(currentDir, '..', 'dist', 'bippy.js')
|
|
22
|
+
bippyCode = fs.readFileSync(bippyPath, 'utf-8')
|
|
23
|
+
return bippyCode
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function getReactSource({
|
|
27
|
+
locator,
|
|
28
|
+
cdp: cdpSession,
|
|
29
|
+
}: {
|
|
30
|
+
locator: Locator | ElementHandle
|
|
31
|
+
cdp: ICDPSession
|
|
32
|
+
}): Promise<ReactSourceLocation | null> {
|
|
33
|
+
// Cast to CDPSession for internal type safety - at runtime both are compatible
|
|
34
|
+
const cdp = cdpSession as CDPSession
|
|
35
|
+
const page: Page = 'page' in locator && typeof locator.page === 'function' ? locator.page() : (locator as any)._page
|
|
36
|
+
|
|
37
|
+
if (!page) {
|
|
38
|
+
throw new Error('Could not get page from locator')
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const hasBippy = await page.evaluate(() => !!(globalThis as any).__bippy)
|
|
42
|
+
|
|
43
|
+
if (!hasBippy) {
|
|
44
|
+
const code = getBippyCode()
|
|
45
|
+
await cdp.send('Runtime.evaluate', { expression: code })
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const result = await (locator as any).evaluate(async (el: any) => {
|
|
49
|
+
const bippy = (globalThis as any).__bippy
|
|
50
|
+
if (!bippy) {
|
|
51
|
+
throw new Error('bippy not loaded')
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const fiber = bippy.getFiberFromHostInstance(el)
|
|
55
|
+
if (!fiber) {
|
|
56
|
+
return { _notFound: 'fiber' as const }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const source = await bippy.getSource(fiber)
|
|
60
|
+
if (source) {
|
|
61
|
+
return {
|
|
62
|
+
fileName: source.fileName ? bippy.normalizeFileName(source.fileName) : null,
|
|
63
|
+
lineNumber: source.lineNumber ?? null,
|
|
64
|
+
columnNumber: source.columnNumber ?? null,
|
|
65
|
+
componentName: source.functionName ?? bippy.getDisplayName(fiber.type) ?? null,
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const ownerStack = await bippy.getOwnerStack(fiber)
|
|
70
|
+
for (const frame of ownerStack) {
|
|
71
|
+
if (frame.fileName && bippy.isSourceFile(frame.fileName)) {
|
|
72
|
+
return {
|
|
73
|
+
fileName: bippy.normalizeFileName(frame.fileName),
|
|
74
|
+
lineNumber: frame.lineNumber ?? null,
|
|
75
|
+
columnNumber: frame.columnNumber ?? null,
|
|
76
|
+
componentName: frame.functionName ?? null,
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return { _notFound: 'source' as const }
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
if (result && '_notFound' in result) {
|
|
85
|
+
if (result._notFound === 'fiber') {
|
|
86
|
+
console.warn('[getReactSource] no fiber found - is this a React element?')
|
|
87
|
+
} else {
|
|
88
|
+
console.warn('[getReactSource] no source location found - is this a React dev build?')
|
|
89
|
+
}
|
|
90
|
+
return null
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return result
|
|
94
|
+
}
|