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.
Files changed (152) hide show
  1. package/bin.js +3 -0
  2. package/dist/aria-snapshot.d.ts +95 -0
  3. package/dist/aria-snapshot.d.ts.map +1 -0
  4. package/dist/aria-snapshot.js +490 -0
  5. package/dist/aria-snapshot.js.map +1 -0
  6. package/dist/bippy.js +971 -0
  7. package/dist/cdp-relay.d.ts +16 -0
  8. package/dist/cdp-relay.d.ts.map +1 -0
  9. package/dist/cdp-relay.js +715 -0
  10. package/dist/cdp-relay.js.map +1 -0
  11. package/dist/cdp-session.d.ts +42 -0
  12. package/dist/cdp-session.d.ts.map +1 -0
  13. package/dist/cdp-session.js +154 -0
  14. package/dist/cdp-session.js.map +1 -0
  15. package/dist/cdp-types.d.ts +63 -0
  16. package/dist/cdp-types.d.ts.map +1 -0
  17. package/dist/cdp-types.js +91 -0
  18. package/dist/cdp-types.js.map +1 -0
  19. package/dist/cli.d.ts +3 -0
  20. package/dist/cli.d.ts.map +1 -0
  21. package/dist/cli.js +213 -0
  22. package/dist/cli.js.map +1 -0
  23. package/dist/create-logger.d.ts +9 -0
  24. package/dist/create-logger.d.ts.map +1 -0
  25. package/dist/create-logger.js +25 -0
  26. package/dist/create-logger.js.map +1 -0
  27. package/dist/debugger-api.md +458 -0
  28. package/dist/debugger-examples-types.d.ts +24 -0
  29. package/dist/debugger-examples-types.d.ts.map +1 -0
  30. package/dist/debugger-examples-types.js +2 -0
  31. package/dist/debugger-examples-types.js.map +1 -0
  32. package/dist/debugger-examples.d.ts +6 -0
  33. package/dist/debugger-examples.d.ts.map +1 -0
  34. package/dist/debugger-examples.js +53 -0
  35. package/dist/debugger-examples.js.map +1 -0
  36. package/dist/debugger.d.ts +381 -0
  37. package/dist/debugger.d.ts.map +1 -0
  38. package/dist/debugger.js +633 -0
  39. package/dist/debugger.js.map +1 -0
  40. package/dist/editor-api.md +364 -0
  41. package/dist/editor-examples.d.ts +11 -0
  42. package/dist/editor-examples.d.ts.map +1 -0
  43. package/dist/editor-examples.js +124 -0
  44. package/dist/editor-examples.js.map +1 -0
  45. package/dist/editor.d.ts +203 -0
  46. package/dist/editor.d.ts.map +1 -0
  47. package/dist/editor.js +336 -0
  48. package/dist/editor.js.map +1 -0
  49. package/dist/execute.d.ts +50 -0
  50. package/dist/execute.d.ts.map +1 -0
  51. package/dist/execute.js +576 -0
  52. package/dist/execute.js.map +1 -0
  53. package/dist/index.d.ts +11 -0
  54. package/dist/index.d.ts.map +1 -0
  55. package/dist/index.js +7 -0
  56. package/dist/index.js.map +1 -0
  57. package/dist/mcp-client.d.ts +20 -0
  58. package/dist/mcp-client.d.ts.map +1 -0
  59. package/dist/mcp-client.js +56 -0
  60. package/dist/mcp-client.js.map +1 -0
  61. package/dist/mcp.d.ts +5 -0
  62. package/dist/mcp.d.ts.map +1 -0
  63. package/dist/mcp.js +720 -0
  64. package/dist/mcp.js.map +1 -0
  65. package/dist/mcp.test.d.ts +10 -0
  66. package/dist/mcp.test.d.ts.map +1 -0
  67. package/dist/mcp.test.js +2999 -0
  68. package/dist/mcp.test.js.map +1 -0
  69. package/dist/network-capture.d.ts +23 -0
  70. package/dist/network-capture.d.ts.map +1 -0
  71. package/dist/network-capture.js +98 -0
  72. package/dist/network-capture.js.map +1 -0
  73. package/dist/protocol.d.ts +54 -0
  74. package/dist/protocol.d.ts.map +1 -0
  75. package/dist/protocol.js +2 -0
  76. package/dist/protocol.js.map +1 -0
  77. package/dist/react-source.d.ts +13 -0
  78. package/dist/react-source.d.ts.map +1 -0
  79. package/dist/react-source.js +68 -0
  80. package/dist/react-source.js.map +1 -0
  81. package/dist/scoped-fs.d.ts +94 -0
  82. package/dist/scoped-fs.d.ts.map +1 -0
  83. package/dist/scoped-fs.js +356 -0
  84. package/dist/scoped-fs.js.map +1 -0
  85. package/dist/selector-generator.js +8126 -0
  86. package/dist/start-relay-server.d.ts +6 -0
  87. package/dist/start-relay-server.d.ts.map +1 -0
  88. package/dist/start-relay-server.js +33 -0
  89. package/dist/start-relay-server.js.map +1 -0
  90. package/dist/styles-api.md +117 -0
  91. package/dist/styles-examples.d.ts +8 -0
  92. package/dist/styles-examples.d.ts.map +1 -0
  93. package/dist/styles-examples.js +64 -0
  94. package/dist/styles-examples.js.map +1 -0
  95. package/dist/styles.d.ts +27 -0
  96. package/dist/styles.d.ts.map +1 -0
  97. package/dist/styles.js +234 -0
  98. package/dist/styles.js.map +1 -0
  99. package/dist/trace-utils.d.ts +14 -0
  100. package/dist/trace-utils.d.ts.map +1 -0
  101. package/dist/trace-utils.js +21 -0
  102. package/dist/trace-utils.js.map +1 -0
  103. package/dist/utils.d.ts +20 -0
  104. package/dist/utils.d.ts.map +1 -0
  105. package/dist/utils.js +75 -0
  106. package/dist/utils.js.map +1 -0
  107. package/dist/wait-for-page-load.d.ts +16 -0
  108. package/dist/wait-for-page-load.d.ts.map +1 -0
  109. package/dist/wait-for-page-load.js +127 -0
  110. package/dist/wait-for-page-load.js.map +1 -0
  111. package/package.json +67 -0
  112. package/src/aria-snapshot.ts +610 -0
  113. package/src/assets/aria-labels-github-snapshot.txt +605 -0
  114. package/src/assets/aria-labels-github.png +0 -0
  115. package/src/assets/aria-labels-google-snapshot.txt +49 -0
  116. package/src/assets/aria-labels-google.png +0 -0
  117. package/src/assets/aria-labels-hacker-news-snapshot.txt +1023 -0
  118. package/src/assets/aria-labels-hacker-news.png +0 -0
  119. package/src/cdp-relay.ts +925 -0
  120. package/src/cdp-session.ts +203 -0
  121. package/src/cdp-timing.md +128 -0
  122. package/src/cdp-types.ts +155 -0
  123. package/src/cli.ts +250 -0
  124. package/src/create-logger.ts +36 -0
  125. package/src/debugger-examples-types.ts +13 -0
  126. package/src/debugger-examples.ts +66 -0
  127. package/src/debugger.md +453 -0
  128. package/src/debugger.ts +713 -0
  129. package/src/editor-examples.ts +148 -0
  130. package/src/editor.ts +390 -0
  131. package/src/execute.ts +763 -0
  132. package/src/index.ts +10 -0
  133. package/src/mcp-client.ts +78 -0
  134. package/src/mcp.test.ts +3596 -0
  135. package/src/mcp.ts +876 -0
  136. package/src/network-capture.ts +140 -0
  137. package/src/prompt.bak.md +323 -0
  138. package/src/prompt.md +7 -0
  139. package/src/protocol.ts +63 -0
  140. package/src/react-source.ts +94 -0
  141. package/src/resource.md +436 -0
  142. package/src/scoped-fs.ts +411 -0
  143. package/src/snapshots/hacker-news-focused-accessibility.md +202 -0
  144. package/src/snapshots/hacker-news-initial-accessibility.md +11 -0
  145. package/src/snapshots/hacker-news-tabbed-accessibility.md +202 -0
  146. package/src/snapshots/shadcn-ui-accessibility.md +11 -0
  147. package/src/start-relay-server.ts +43 -0
  148. package/src/styles-examples.ts +77 -0
  149. package/src/styles.ts +345 -0
  150. package/src/trace-utils.ts +43 -0
  151. package/src/utils.ts +91 -0
  152. 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
@@ -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
+ }