This commit is contained in:
Tommy D. Rossi
2026-02-25 19:01:08 +01:00
parent 5173526475
commit 43ad91e439
5 changed files with 842 additions and 698 deletions
+3 -1
View File
@@ -3,7 +3,9 @@
{ {
"name": "playwriter", "name": "playwriter",
"description": "Control the user own Chrome browser via Playwriter extension with Playwright code snippets in a stateful local js sandbox via playwriter cli. Use this over other Playwright MCPs to automate the browser — it connects to the user's existing Chrome instead of launching a new one. Use this for JS-heavy websites (Instagram, Twitter, cookie/login walls, lazy-loaded UIs) instead of webfetch/curl. Run `playwriter skill` command to read the complete up to date skill", "description": "Control the user own Chrome browser via Playwriter extension with Playwright code snippets in a stateful local js sandbox via playwriter cli. Use this over other Playwright MCPs to automate the browser — it connects to the user's existing Chrome instead of launching a new one. Use this for JS-heavy websites (Instagram, Twitter, cookie/login walls, lazy-loaded UIs) instead of webfetch/curl. Run `playwriter skill` command to read the complete up to date skill",
"files": ["SKILL.md"] "files": [
"SKILL.md"
]
} }
] ]
} }
+288 -179
View File
@@ -61,19 +61,19 @@ Default timeout is 10 seconds. you can increase the timeout with `--timeout <ms>
playwriter -s 1 -e "state.page = await context.newPage(); await state.page.goto('https://example.com')" playwriter -s 1 -e "state.page = await context.newPage(); await state.page.goto('https://example.com')"
# Click a button # Click a button
playwriter -s 1 -e "await page.click('button')" playwriter -s 1 -e "await state.page.click('button')"
# Get page title # Get page title
playwriter -s 1 -e "await page.title()" playwriter -s 1 -e "await state.page.title()"
# Take a screenshot # Take a screenshot
playwriter -s 1 -e "await page.screenshot({ path: 'screenshot.png', scale: 'css' })" playwriter -s 1 -e "await state.page.screenshot({ path: 'screenshot.png', scale: 'css' })"
# Get accessibility snapshot # Get accessibility snapshot
playwriter -s 1 -e "await snapshot({ page })" playwriter -s 1 -e "await snapshot({ page: state.page })"
# Get accessibility snapshot for a specific iframe # Get accessibility snapshot for a specific iframe
const frame = await page.locator('iframe').contentFrame() const frame = await state.page.locator('iframe').contentFrame()
await snapshot({ frame }) await snapshot({ frame })
``` ```
@@ -82,14 +82,14 @@ await snapshot({ frame })
```bash ```bash
# Using $'...' syntax for multiline code # Using $'...' syntax for multiline code
playwriter -s 1 -e $' playwriter -s 1 -e $'
const title = await page.title(); const title = await state.page.title();
const url = page.url(); const url = state.page.url();
console.log({ title, url }); console.log({ title, url });
' '
# Or use heredoc # Or use heredoc
playwriter -s 1 -e "$(cat <<'EOF' playwriter -s 1 -e "$(cat <<'EOF'
const links = await page.$$eval('a', els => els.map(e => e.href)); const links = await state.page.$$eval('a', els => els.map(e => e.href));
console.log('Found', links.length, 'links'); console.log('Found', links.length, 'links');
EOF EOF
)" )"
@@ -120,40 +120,42 @@ If you find a bug, you can create a gh issue using `gh issue create -R remorses/
Control user's Chrome browser via playwright code snippets. Prefer single-line code with semicolons between statements. Use playwriter immediately without waiting for user actions; only if you get "extension is not connected" or "no browser tabs have Playwriter enabled" should you ask the user to click the playwriter extension icon on the target tab. Control user's Chrome browser via playwright code snippets. Prefer single-line code with semicolons between statements. Use playwriter immediately without waiting for user actions; only if you get "extension is not connected" or "no browser tabs have Playwriter enabled" should you ask the user to click the playwriter extension icon on the target tab.
**When to use playwriter instead of webfetch/curl:** If a website is JS-heavy (SPAs like Instagram, Twitter, Facebook, etc.), has cookie consent modals, login walls, lazy-loaded content, carousels, or infinite scroll — **always use playwriter**. Simple fetch/webfetch will return an empty HTML shell with no content. Do NOT waste time trying curl, webfetch, or parsing raw HTML from JS-rendered sites. Go straight to playwriter: navigate with a real browser, dismiss modals, then extract what you need via `page.evaluate()` or network interception.
**If Chrome is not running**, the extension can't connect. Start Chrome from the command line before retrying: **If Chrome is not running**, the extension can't connect. Start Chrome from the command line before retrying:
```bash ```bash
# macOS # macOS
open -a "Google Chrome" open -a "Google Chrome" --args --profile-directory=Default
# Linux # Linux
google-chrome & google-chrome --profile-directory=Default &
# Windows (cmd) # Windows (cmd)
start chrome.exe start chrome.exe --profile-directory=Default
# Windows (PowerShell) # Windows (PowerShell)
Start-Process chrome.exe Start-Process chrome.exe -ArgumentList '--profile-directory=Default'
``` ```
To also enable automatic tab capture for screen recording (no manual extension click needed), add the `--allowlisted-extension-id` and `--auto-accept-this-tab-capture` flags: To also enable automatic tab capture for screen recording (no manual extension click needed), add the `--allowlisted-extension-id` and `--auto-accept-this-tab-capture` flags:
```bash ```bash
# macOS # macOS
open -a "Google Chrome" --args --allowlisted-extension-id=jfeammnjpkecdekppnclgkkffahnhfhe --auto-accept-this-tab-capture open -a "Google Chrome" --args --profile-directory=Default --allowlisted-extension-id=jfeammnjpkecdekppnclgkkffahnhfhe --auto-accept-this-tab-capture
# Linux # Linux
google-chrome --allowlisted-extension-id=jfeammnjpkecdekppnclgkkffahnhfhe --auto-accept-this-tab-capture & google-chrome --profile-directory=Default --allowlisted-extension-id=jfeammnjpkecdekppnclgkkffahnhfhe --auto-accept-this-tab-capture &
# Windows # Windows
start chrome.exe --allowlisted-extension-id=jfeammnjpkecdekppnclgkkffahnhfhe --auto-accept-this-tab-capture start chrome.exe --profile-directory=Default --allowlisted-extension-id=jfeammnjpkecdekppnclgkkffahnhfhe --auto-accept-this-tab-capture
``` ```
You can collaborate with the user - they can help with captchas, difficult elements, or reproducing bugs. You can collaborate with the user - they can help with captchas, difficult elements, or reproducing bugs.
## context variables ## context variables
- `state` - object persisted between calls **within your session**. Each session has its own isolated state. Use to store pages, data, listeners (e.g., `state.myPage = await context.newPage()`) - `state` - object persisted between calls **within your session**. Each session has its own isolated state. Use to store pages, data, listeners (e.g., `state.page = await context.newPage()`)
- `page` - a default page (may be shared with other agents). Prefer creating your own page and storing it in `state` (see "working with pages") - `page` - a default page (may be shared with other agents). Prefer creating your own page and storing it in `state` (see "working with pages")
- `context` - browser context, access all pages via `context.pages()` - `context` - browser context, access all pages via `context.pages()`
- `require` - load Node.js modules (e.g., `const fs = require('node:fs')`). ESM `import` is not available in the sandbox - `require` - load Node.js modules (e.g., `const fs = require('node:fs')`). ESM `import` is not available in the sandbox
@@ -163,15 +165,16 @@ You can collaborate with the user - they can help with captchas, difficult eleme
## rules ## rules
- **Create your own page**: see "working with pages" — always create and store your own page in `state`, never use the default `page` for automation. Examples below use bare `page` for brevity, but in real automation always use `state.myPage` - **Initialize state.page first**: see "working with pages" — at the start of a task, assign `state.page` (reuse `about:blank` or create one) and use `state.page` for all automation steps.
- **Multiple calls**: use multiple execute calls for complex logic - helps understand intermediate state and isolate which action failed - **Multiple calls**: use multiple execute calls for complex logic - helps understand intermediate state and isolate which action failed
- **Never close**: never call `browser.close()` or `context.close()`. Only close pages you created or if user asks - **Never close**: never call `browser.close()` or `context.close()`. Only close pages you created or if user asks
- **No bringToFront**: never call unless user asks - it's disruptive and unnecessary, you can interact with background pages - **No bringToFront**: never call unless user asks - it's disruptive and unnecessary, you can interact with background pages
- **Check state after actions**: always verify page state after clicking/submitting (see next section) - **Check state after actions**: always verify page state after clicking/submitting (see next section)
- **Clean up listeners**: call `page.removeAllListeners()` at end of message to prevent leaks - **Clean up listeners**: call `state.page.removeAllListeners()` at end of message to prevent leaks
- **CDP sessions**: use `getCDPSession({ page })` not `page.context().newCDPSession()` - NEVER use `newCDPSession()` method, it doesn't work through playwriter relay - **CDP sessions**: use `getCDPSession({ page: state.page })` not `state.page.context().newCDPSession()` - NEVER use `newCDPSession()` method, it doesn't work through playwriter relay
- **Wait for load**: use `page.waitForLoadState('domcontentloaded')` not `page.waitForEvent('load')` - waitForEvent times out if already loaded - **Wait for load**: use `state.page.waitForLoadState('domcontentloaded')` not `state.page.waitForEvent('load')` - waitForEvent times out if already loaded
- **Minimize timeouts**: prefer proper waits (`waitForSelector`, `waitForPageLoad`) over `page.waitForTimeout()`. Short timeouts (1-2s) are acceptable for non-deterministic events like popups, animations, or tab opens where no specific selector is available - **Minimize timeouts**: prefer proper waits (`waitForSelector`, `waitForPageLoad`) over `state.page.waitForTimeout()`. Short timeouts (1-2s) are acceptable for non-deterministic events like popups, animations, or tab opens where no specific selector is available
- **Snapshot before screenshot**: always use `snapshot()` first to understand page state (text-based, fast, cheap). Only use `screenshot` or `screenshotWithAccessibilityLabels` when you specifically need visual/spatial information. Never take a screenshot just to check if a page loaded or to read text content — snapshot gives you that instantly without burning image tokens
## interaction feedback loop ## interaction feedback loop
@@ -180,7 +183,7 @@ Every browser interaction should follow a **observe → act → observe** loop.
**Core loop:** **Core loop:**
1. **Open page** — get or create your page and navigate to the target URL 1. **Open page** — get or create your page and navigate to the target URL
2. **Observe** — print `page.url()` and take an accessibility snapshot. Always print the URL so you know where you are — pages can redirect, and actions can trigger unexpected navigation. 2. **Observe** — print `state.page.url()` and take an accessibility snapshot. Always print the URL so you know where you are — pages can redirect, and actions can trigger unexpected navigation.
3. **Check** — read the snapshot and URL. If the page isn't ready (still loading, expected content missing, wrong URL), **wait and observe again** — don't act on stale or incomplete state. Only proceed when you can identify the element to interact with. 3. **Check** — read the snapshot and URL. If the page isn't ready (still loading, expected content missing, wrong URL), **wait and observe again** — don't act on stale or incomplete state. Only proceed when you can identify the element to interact with.
4. **Act** — perform one action (click, type, submit) 4. **Act** — perform one action (click, type, submit)
5. **Observe again** — print URL + snapshot to verify the action's effect. If the action didn't take effect (nothing changed, page still loading), wait and observe again before proceeding. 5. **Observe again** — print URL + snapshot to verify the action's effect. If the action didn't take effect (nothing changed, page still loading), wait and observe again before proceeding.
@@ -214,34 +217,34 @@ Each step is a separate execute call. Notice how every action is followed by a s
```js ```js
// 1. Open page and observe — always print URL first // 1. Open page and observe — always print URL first
state.myPage = context.pages().find((p) => p.url() === 'about:blank') ?? (await context.newPage()) state.page = context.pages().find((p) => p.url() === 'about:blank') ?? (await context.newPage())
await state.myPage.goto('https://framer.com/projects/my-project', { waitUntil: 'domcontentloaded' }) await state.page.goto('https://framer.com/projects/my-project', { waitUntil: 'domcontentloaded' })
console.log('URL:', state.myPage.url()) console.log('URL:', state.page.url())
await snapshot({ page: state.myPage }).then(console.log) await snapshot({ page: state.page }).then(console.log)
``` ```
```js ```js
// 2. Act: open command palette → observe result // 2. Act: open command palette → observe result
await state.myPage.keyboard.press('Meta+k') await state.page.keyboard.press('Meta+k')
console.log('URL:', state.myPage.url()) console.log('URL:', state.page.url())
await snapshot({ page: state.myPage, search: /dialog|Search/ }).then(console.log) await snapshot({ page: state.page, search: /dialog|Search/ }).then(console.log)
// If dialog didn't appear, observe again before retrying // If dialog didn't appear, observe again before retrying
``` ```
```js ```js
// 3. Act: type search query → observe result // 3. Act: type search query → observe result
await state.myPage.keyboard.type('MCP') await state.page.keyboard.type('MCP')
console.log('URL:', state.myPage.url()) console.log('URL:', state.page.url())
await snapshot({ page: state.myPage, search: /MCP/ }).then(console.log) await snapshot({ page: state.page, search: /MCP/ }).then(console.log)
``` ```
```js ```js
// 4. Act: press Enter → observe plugin loaded // 4. Act: press Enter → observe plugin loaded
await state.myPage.keyboard.press('Enter') await state.page.keyboard.press('Enter')
await state.myPage.waitForTimeout(1000) await state.page.waitForTimeout(1000)
console.log('URL:', state.myPage.url()) console.log('URL:', state.page.url())
const frame = state.myPage.frames().find((f) => f.url().includes('plugins.framercdn.com')) const frame = state.page.frames().find((f) => f.url().includes('plugins.framercdn.com'))
await snapshot({ page: state.myPage, frame: frame || undefined }).then(console.log) await snapshot({ page: state.page, frame: frame || undefined }).then(console.log)
// If frame not found, wait and observe again — plugin may still be loading // If frame not found, wait and observe again — plugin may still be loading
``` ```
@@ -251,11 +254,11 @@ Snapshots are the primary feedback mechanism, but some actions have side effects
- **Console logs** — check for errors or app state after an action: - **Console logs** — check for errors or app state after an action:
```js ```js
await getLatestLogs({ page, search: /error|fail/i, count: 20 }) await getLatestLogs({ page: state.page, search: /error|fail/i, count: 20 })
``` ```
- **Network requests** — verify API calls were made after a form submit or button click: - **Network requests** — verify API calls were made after a form submit or button click:
```js ```js
page.on('response', async (res) => { state.page.on('response', async (res) => {
if (res.url().includes('/api/')) { if (res.url().includes('/api/')) {
console.log(res.status(), res.url()) console.log(res.status(), res.url())
} }
@@ -263,7 +266,7 @@ Snapshots are the primary feedback mechanism, but some actions have side effects
``` ```
- **URL changes** — confirm navigation happened: - **URL changes** — confirm navigation happened:
```js ```js
console.log(page.url()) console.log(state.page.url())
``` ```
- **Screenshots** — only for visual layout issues (see "choosing between snapshot methods" below). - **Screenshots** — only for visual layout issues (see "choosing between snapshot methods" below).
@@ -273,8 +276,8 @@ Snapshots are the primary feedback mechanism, but some actions have side effects
Always check page state after important actions (form submissions, uploads, typing). Your mental model can diverge from actual browser state: Always check page state after important actions (form submissions, uploads, typing). Your mental model can diverge from actual browser state:
```js ```js
await page.keyboard.type('my text') await state.page.keyboard.type('my text')
await snapshot({ page, search: /my text/ }) await snapshot({ page: state.page, search: /my text/ })
// If verifying visual layout specifically, use screenshotWithAccessibilityLabels instead // If verifying visual layout specifically, use screenshotWithAccessibilityLabels instead
``` ```
@@ -283,11 +286,11 @@ Clipboard paste (`Meta+v`) can silently fail. For file uploads, prefer file inpu
```js ```js
// Reliable: use file input // Reliable: use file input
const fileInput = page.locator('input[type="file"]').first() const fileInput = state.page.locator('input[type="file"]').first()
await fileInput.setInputFiles('/path/to/image.png') await fileInput.setInputFiles('/path/to/image.png')
// Unreliable: clipboard paste may silently fail, need to focus textarea first for example // Unreliable: clipboard paste may silently fail, need to focus textarea first for example
await page.keyboard.press('Meta+v') // always verify with screenshot! await state.page.keyboard.press('Meta+v') // always verify with screenshot!
``` ```
**3. Using stale locators from old snapshots** **3. Using stale locators from old snapshots**
@@ -295,10 +298,10 @@ Locators (especially ones with `>> nth=`) can change when the page updates. Alwa
```js ```js
// BAD: using ref from minutes ago // BAD: using ref from minutes ago
await page.locator('[id="old-id"]').click() // element may have changed await state.page.locator('[id="old-id"]').click() // element may have changed
// GOOD: get fresh snapshot, then immediately use locators from it // GOOD: get fresh snapshot, then immediately use locators from it
await snapshot({ page, showDiffSinceLastCall: true }) await snapshot({ page: state.page, showDiffSinceLastCall: true })
// Now use the NEW locators from this output // Now use the NEW locators from this output
``` ```
@@ -307,7 +310,7 @@ Before destructive actions (delete, submit), verify you're targeting the right t
```js ```js
// Before deleting, verify it's the right item // Before deleting, verify it's the right item
await screenshotWithAccessibilityLabels({ page }) await screenshotWithAccessibilityLabels({ page: state.page })
// READ the screenshot to confirm, THEN proceed with delete // READ the screenshot to confirm, THEN proceed with delete
``` ```
@@ -316,12 +319,12 @@ await screenshotWithAccessibilityLabels({ page })
```js ```js
// BAD: newlines in string don't create line breaks // BAD: newlines in string don't create line breaks
await page.keyboard.type('Line 1\nLine 2') // becomes "Line 1Line 2" await state.page.keyboard.type('Line 1\nLine 2') // becomes "Line 1Line 2"
// GOOD: use Enter key for line breaks // GOOD: use Enter key for line breaks
await page.keyboard.type('Line 1') await state.page.keyboard.type('Line 1')
await page.keyboard.press('Enter') await state.page.keyboard.press('Enter')
await page.keyboard.type('Line 2') await state.page.keyboard.type('Line 2')
``` ```
**6. Quote escaping in $'...' syntax** **6. Quote escaping in $'...' syntax**
@@ -329,14 +332,14 @@ When using `$'...'` for multiline code, nested quotes break parsing. Use differe
```bash ```bash
# BAD: nested double quotes break $'...' # BAD: nested double quotes break $'...'
playwriter -s 1 -e $'await page.locator("[id=\"_r_a_\"]").click()' playwriter -s 1 -e $'await state.page.locator("[id=\"_r_a_\"]").click()'
# GOOD: use single quotes inside, or template strings # GOOD: use single quotes inside, or template strings
playwriter -s 1 -e $'await page.locator(\'[id="_r_a_"]\').click()' playwriter -s 1 -e $'await state.page.locator(\'[id="_r_a_"]\').click()'
# GOOD: use heredoc for complex quoting # GOOD: use heredoc for complex quoting
playwriter -s 1 -e "$(cat <<'EOF' playwriter -s 1 -e "$(cat <<'EOF'
await page.locator('[id="_r_a_"]').click() await state.page.locator('[id="_r_a_"]').click()
EOF EOF
)" )"
``` ```
@@ -346,41 +349,56 @@ Screenshots + image analysis is expensive and slow. Only use screenshots for vis
```js ```js
// BAD: screenshot to check if text appeared (wastes tokens on image analysis) // BAD: screenshot to check if text appeared (wastes tokens on image analysis)
await page.screenshot({ path: 'check.png', scale: 'css' }) await state.page.screenshot({ path: 'check.png', scale: 'css' })
// GOOD: snapshot is text — fast, cheap, searchable // GOOD: snapshot is text — fast, cheap, searchable
await snapshot({ page, search: /expected text/i }) await snapshot({ page: state.page, search: /expected text/i })
// GOOD: evaluate DOM directly for content checks // GOOD: evaluate DOM directly for content checks
const text = await page.evaluate(() => document.querySelector('.message')?.textContent) const text = await state.page.evaluate(() => document.querySelector('.message')?.textContent)
``` ```
**8. Assuming page content loaded** **8. Assuming page content loaded**
Even after `goto()`, dynamic content may not be ready: Even after `goto()`, dynamic content may not be ready:
```js ```js
await page.goto('https://example.com') await state.page.goto('https://example.com')
// Content may still be loading via JavaScript! // Content may still be loading via JavaScript!
await page.waitForSelector('article', { timeout: 10000 }) await state.page.waitForSelector('article', { timeout: 10000 })
// Or use waitForPageLoad utility // Or use waitForPageLoad utility
await waitForPageLoad({ page, timeout: 5000 }) await waitForPageLoad({ page: state.page, timeout: 5000 })
``` ```
**9. Login buttons that open popups** **9. Not using playwriter for JS-rendered sites**
Do NOT waste context trying webfetch, curl, or Playwright CLI screenshots on SPAs (Instagram, Twitter, etc.). These sites return empty HTML shells — the real content is rendered by JavaScript. Use playwriter with a real browser session instead:
```js
// BAD: webfetch/curl on Instagram returns empty HTML, grep finds nothing, huge context wasted
// BAD: Playwright CLI screenshot needs browser install, produces blank/modal-blocked images
// GOOD: use playwriter — real browser, full JS rendering, interactive
state.page = context.pages().find((p) => p.url() === 'about:blank') ?? (await context.newPage())
await state.page.goto('https://www.instagram.com/p/ABC123/', { waitUntil: 'domcontentloaded' })
await waitForPageLoad({ page: state.page, timeout: 8000 })
await snapshot({ page: state.page, search: /cookie|consent|accept/i }).then(console.log)
// Now you can see modals, dismiss them, navigate carousels, extract content
```
**10. Login buttons that open popups**
Playwriter extension cannot control popup windows. If a login button opens a popup (common with OAuth/SSO), use cmd+click to open in a new tab instead: Playwriter extension cannot control popup windows. If a login button opens a popup (common with OAuth/SSO), use cmd+click to open in a new tab instead:
```js ```js
// BAD: popup window is not controllable by playwriter // BAD: popup window is not controllable by playwriter
await page.click('button:has-text("Login with Google")') await state.page.click('button:has-text("Login with Google")')
// GOOD: cmd+click opens in new tab that playwriter can control // GOOD: cmd+click opens in new tab that playwriter can control
await page.locator('button:has-text("Login with Google")').click({ modifiers: ['Meta'] }) await state.page.locator('button:has-text("Login with Google")').click({ modifiers: ['Meta'] })
await page.waitForTimeout(2000) await state.page.waitForTimeout(2000)
// Verify new tab opened - last page should be the login page // Verify new tab opened - last page should be the login page
const pages = context.pages() const pages = context.pages()
const loginPage = pages[pages.length - 1] const loginPage = pages[pages.length - 1]
if (loginPage.url() === page.url()) { if (loginPage.url() === state.page.url()) {
throw new Error('Cmd+click did not open new tab - login may have opened as popup') throw new Error('Cmd+click did not open new tab - login may have opened as popup')
} }
@@ -396,20 +414,20 @@ After any action (click, submit, navigate), verify what happened. Always print U
```js ```js
// Always print URL first, then snapshot // Always print URL first, then snapshot
console.log('URL:', page.url()) console.log('URL:', state.page.url())
await snapshot({ page }).then(console.log) await snapshot({ page: state.page }).then(console.log)
// Filter for specific content when snapshot is large // Filter for specific content when snapshot is large
console.log('URL:', page.url()) console.log('URL:', state.page.url())
await snapshot({ page, search: /dialog|button|error/i }).then(console.log) await snapshot({ page: state.page, search: /dialog|button|error/i }).then(console.log)
``` ```
If nothing changed, try `await waitForPageLoad({ page, timeout: 3000 })` or you may have clicked the wrong element. If nothing changed, try `await waitForPageLoad({ page: state.page, timeout: 3000 })` or you may have clicked the wrong element.
## accessibility snapshots ## accessibility snapshots
```js ```js
await snapshot({ page, search?, showDiffSinceLastCall? }) await snapshot({ page: state.page, search?, showDiffSinceLastCall? })
``` ```
`accessibilitySnapshot` is still available as an alias for backward compatibility. `accessibilitySnapshot` is still available as an alias for backward compatibility.
@@ -429,34 +447,34 @@ Example output:
- link "Blog" role=link[name="Blog"] - link "Blog" role=link[name="Blog"]
``` ```
Each interactive line ends with a Playwright locator you can pass to `page.locator()`. Each interactive line ends with a Playwright locator you can pass to `state.page.locator()`.
If multiple elements share the same locator, a `>> nth=N` suffix is added (0-based) If multiple elements share the same locator, a `>> nth=N` suffix is added (0-based)
to make it unique. to make it unique.
If a screenshot shows ref labels like `e3`, resolve them using the last snapshot: If a screenshot shows ref labels like `e3`, resolve them using the last snapshot:
```js ```js
const snap = await snapshot({ page }) const snap = await snapshot({ page: state.page })
const locator = refToLocator({ ref: 'e3' }) const locator = refToLocator({ ref: 'e3' })
await page.locator(locator!).click() await state.page.locator(locator!).click()
``` ```
```js ```js
await page.locator('[id="nav-home"]').click() await state.page.locator('[id="nav-home"]').click()
await page.locator('[data-testid="docs-link"]').click() await state.page.locator('[data-testid="docs-link"]').click()
await page.locator('role=link[name="Blog"]').click() await state.page.locator('role=link[name="Blog"]').click()
``` ```
Search for specific elements: Search for specific elements:
```js ```js
const snap = await snapshot({ page, search: /button|submit/i }) const snap = await snapshot({ page: state.page, search: /button|submit/i })
``` ```
**Filtering large snapshots in JS** — when the built-in `search` isn't enough (e.g., you need multiple patterns or custom logic), filter the snapshot string directly: **Filtering large snapshots in JS** — when the built-in `search` isn't enough (e.g., you need multiple patterns or custom logic), filter the snapshot string directly:
```js ```js
const snap = await snapshot({ page, showDiffSinceLastCall: false }) const snap = await snapshot({ page: state.page, showDiffSinceLastCall: false })
const relevant = snap const relevant = snap
.split('\n') .split('\n')
.filter((l) => l.includes('dialog') || l.includes('error') || l.includes('button')) .filter((l) => l.includes('dialog') || l.includes('error') || l.includes('button'))
@@ -502,16 +520,16 @@ Both `snapshot` and `screenshotWithAccessibilityLabels` use the same ref system,
Combine locators for precision: Combine locators for precision:
```js ```js
page.locator('tr').filter({ hasText: 'John' }).locator('button').click() state.page.locator('tr').filter({ hasText: 'John' }).locator('button').click()
page.locator('button').nth(2).click() state.page.locator('button').nth(2).click()
``` ```
If a locator matches multiple elements, Playwright throws "strict mode violation". Use `.first()`, `.last()`, or `.nth(n)`: If a locator matches multiple elements, Playwright throws "strict mode violation". Use `.first()`, `.last()`, or `.nth(n)`:
```js ```js
await page.locator('button').first().click() // first match await state.page.locator('button').first().click() // first match
await page.locator('.item').last().click() // last match await state.page.locator('.item').last().click() // last match
await page.locator('li').nth(3).click() // 4th item (0-indexed) await state.page.locator('li').nth(3).click() // 4th item (0-indexed)
``` ```
## working with pages ## working with pages
@@ -520,15 +538,15 @@ await page.locator('li').nth(3).click() // 4th item (0-indexed)
**Get or create your page (first call):** **Get or create your page (first call):**
On your very first execute call, reuse an existing empty tab or create a new one, and navigate it **in the same execute call**. Store it in `state` and use `state.myPage` for all subsequent operations instead of the default `page` variable: On your very first execute call, reuse an existing empty tab or create a new one, and navigate it **in the same execute call**. Store it in `state` and use `state.page` for all subsequent operations instead of the default `page` variable:
```js ```js
// Reuse an empty about:blank tab if available, otherwise create a new one. // Reuse an empty about:blank tab if available, otherwise create a new one.
// IMPORTANT: always navigate immediately in the same call to avoid another // IMPORTANT: always navigate immediately in the same call to avoid another
// agent grabbing the same about:blank tab between execute calls. // agent grabbing the same about:blank tab between execute calls.
state.myPage = context.pages().find((p) => p.url() === 'about:blank') ?? (await context.newPage()) state.page = context.pages().find((p) => p.url() === 'about:blank') ?? (await context.newPage())
await state.myPage.goto('https://example.com') await state.page.goto('https://example.com')
// Use state.myPage for ALL subsequent operations // Use state.page for ALL subsequent operations
``` ```
**Handle page closures gracefully:** **Handle page closures gracefully:**
@@ -536,10 +554,10 @@ await state.myPage.goto('https://example.com')
The user may close your page by accident (e.g., closing a tab in Chrome). Always check before using it and recreate if needed: The user may close your page by accident (e.g., closing a tab in Chrome). Always check before using it and recreate if needed:
```js ```js
if (!state.myPage || state.myPage.isClosed()) { if (!state.page || state.page.isClosed()) {
state.myPage = context.pages().find((p) => p.url() === 'about:blank') ?? (await context.newPage()) state.page = context.pages().find((p) => p.url() === 'about:blank') ?? (await context.newPage())
} }
await state.myPage.goto('https://example.com') await state.page.goto('https://example.com')
``` ```
**Use an existing page only when the user asks:** **Use an existing page only when the user asks:**
@@ -564,8 +582,8 @@ context.pages().map((p) => p.url())
**Use `domcontentloaded`** for `page.goto()`: **Use `domcontentloaded`** for `page.goto()`:
```js ```js
await page.goto('https://example.com', { waitUntil: 'domcontentloaded' }) await state.page.goto('https://example.com', { waitUntil: 'domcontentloaded' })
await waitForPageLoad({ page, timeout: 5000 }) await waitForPageLoad({ page: state.page, timeout: 5000 })
``` ```
## common patterns ## common patterns
@@ -576,8 +594,8 @@ await waitForPageLoad({ page, timeout: 5000 })
// BAD: curl/external requests don't have session cookies // BAD: curl/external requests don't have session cookies
// curl -H "Cookie: ..." often fails due to missing cookies or CSRF // curl -H "Cookie: ..." often fails due to missing cookies or CSRF
// GOOD: fetch inside page.evaluate uses browser's full session // GOOD: fetch inside state.page.evaluate uses browser's full session
const data = await page.evaluate(async (url) => { const data = await state.page.evaluate(async (url) => {
const resp = await fetch(url) const resp = await fetch(url)
return await resp.text() return await resp.text()
}, 'https://example.com/protected/resource') }, 'https://example.com/protected/resource')
@@ -587,7 +605,7 @@ const data = await page.evaluate(async (url) => {
```js ```js
// Fetch protected data and trigger download to user's Downloads folder // Fetch protected data and trigger download to user's Downloads folder
await page.evaluate(async (url) => { await state.page.evaluate(async (url) => {
const resp = await fetch(url) const resp = await fetch(url)
const data = await resp.text() const data = await resp.text()
const blob = new Blob([data], { type: 'application/octet-stream' }) const blob = new Blob([data], { type: 'application/octet-stream' })
@@ -611,8 +629,8 @@ Instead, use simpler alternatives (single download via `a.click()`, store data i
**Links that open new tabs** - playwriter cannot control popup windows opened via `window.open`. Use cmd+click to open in a controllable new tab instead (see mistake #9 above for a full example): **Links that open new tabs** - playwriter cannot control popup windows opened via `window.open`. Use cmd+click to open in a controllable new tab instead (see mistake #9 above for a full example):
```js ```js
await page.locator('a[target=_blank]').click({ modifiers: ['Meta'] }) await state.page.locator('a[target=_blank]').click({ modifiers: ['Meta'] })
await page.waitForTimeout(1000) await state.page.waitForTimeout(1000)
const pages = context.pages() const pages = context.pages()
const newTab = pages[pages.length - 1] const newTab = pages[pages.length - 1]
console.log('New tab URL:', newTab.url()) console.log('New tab URL:', newTab.url())
@@ -621,32 +639,73 @@ console.log('New tab URL:', newTab.url())
**Downloads** - capture and save: **Downloads** - capture and save:
```js ```js
const [download] = await Promise.all([page.waitForEvent('download'), page.click('button.download')]) const [download] = await Promise.all([state.page.waitForEvent('download'), state.page.click('button.download')])
await download.saveAs(`./${download.suggestedFilename()}`) await download.saveAs(`/tmp/${download.suggestedFilename()}`)
``` ```
**iFrames** - two approaches depending on what you need: **iFrames** - two approaches depending on what you need:
```js ```js
// frameLocator: for chaining locator operations (click, fill, etc.) // frameLocator: for chaining locator operations (click, fill, etc.)
const frame = page.frameLocator('#my-iframe') const frame = state.page.frameLocator('#my-iframe')
await frame.locator('button').click() await frame.locator('button').click()
// contentFrame: returns a Frame object, needed for snapshot({ frame }) // contentFrame: returns a Frame object, needed for snapshot({ frame })
const frame2 = await page.locator('iframe').contentFrame() const frame2 = await state.page.locator('iframe').contentFrame()
await snapshot({ frame: frame2 }) await snapshot({ frame: frame2 })
``` ```
**Dialogs** - handle alerts/confirms/prompts: **Dialogs** - handle alerts/confirms/prompts:
```js ```js
page.on('dialog', async (dialog) => { state.page.on('dialog', async (dialog) => {
console.log(dialog.message()) console.log(dialog.message())
await dialog.accept() await dialog.accept()
}) })
await page.click('button.trigger-alert') await state.page.click('button.trigger-alert')
``` ```
**Handling page obstacles (cookie modals, login walls, age gates)** - most major websites show blocking overlays. Always check for these with `snapshot()` right after navigation and dismiss them before doing anything else:
```js
// After navigating, check for common obstacles
await waitForPageLoad({ page: state.page, timeout: 5000 })
const snap = await snapshot({
page: state.page,
search: /cookie|consent|accept|reject|decline|allow|age|verify|login|sign.in/i,
})
console.log(snap)
// Look for dismiss/accept/decline buttons in the snapshot, then click them:
// await state.page.locator('button:has-text("Accept")').click();
// await state.page.locator('button:has-text("Decline optional")').click();
// Then re-snapshot to confirm the modal is gone before proceeding
```
If the page requires login and the user is already logged into Chrome, their session cookies are available — just navigate and the page should load authenticated. If not, ask the user for help or use their existing logged-in tab via `context.pages()`.
**Extracting and downloading media (images, videos)** - use `page.evaluate()` to extract URLs from the rendered DOM, then download via Node.js in the sandbox. This is far more reliable than parsing raw HTML:
```js
// Extract all image URLs from rendered DOM
const images = await state.page.evaluate(() =>
Array.from(document.querySelectorAll('img[src]')).map((img) => ({
src: img.src,
alt: img.alt,
width: img.naturalWidth,
})),
)
console.log(JSON.stringify(images, null, 2))
// Download a specific image to disk
const fs = require('node:fs')
const resp = await fetch(images[0].src)
const buf = Buffer.from(await resp.arrayBuffer())
fs.writeFileSync('./downloaded-image.jpg', buf)
console.log('Saved', buf.length, 'bytes')
```
For carousels or lazy-loaded galleries, you may need to click navigation arrows or scroll first, then re-extract. Use network interception (see "network interception" section) to capture high-resolution CDN URLs that may differ from the `img.src` thumbnails.
## utility functions ## utility functions
**getLatestLogs** - retrieve captured browser console logs (up to 5000 per page, cleared on navigation): **getLatestLogs** - retrieve captured browser console logs (up to 5000 per page, cleared on navigation):
@@ -655,19 +714,19 @@ await page.click('button.trigger-alert')
await getLatestLogs({ page?, count?, search? }) await getLatestLogs({ page?, count?, search? })
// Examples: // Examples:
const errors = await getLatestLogs({ search: /error/i, count: 50 }) const errors = await getLatestLogs({ search: /error/i, count: 50 })
const pageLogs = await getLatestLogs({ page }) const pageLogs = await getLatestLogs({ page: state.page })
``` ```
For custom log collection across runs, store in state: `state.logs = []; page.on('console', m => state.logs.push(m.text()))` For custom log collection across runs, store in state: `state.logs = []; state.page.on('console', m => state.logs.push(m.text()))`
**getCleanHTML** - get cleaned HTML from a locator or page, with search and diffing: **getCleanHTML** - get cleaned HTML from a locator or page, with search and diffing:
```js ```js
await getCleanHTML({ locator, search?, showDiffSinceLastCall?, includeStyles? }) await getCleanHTML({ locator, search?, showDiffSinceLastCall?, includeStyles? })
// Examples: // Examples:
const html = await getCleanHTML({ locator: page.locator('body') }) const html = await getCleanHTML({ locator: state.page.locator('body') })
const html = await getCleanHTML({ locator: page, search: /button/i }) const html = await getCleanHTML({ locator: state.page, search: /button/i })
const fullHtml = await getCleanHTML({ locator: page, showDiffSinceLastCall: false }) // disable diff const fullHtml = await getCleanHTML({ locator: state.page, showDiffSinceLastCall: false }) // disable diff
``` ```
**Parameters:** **Parameters:**
@@ -694,10 +753,10 @@ The function cleans HTML for compact, readable output:
**getPageMarkdown** - extract main page content as plain text using Mozilla Readability (same algorithm as Firefox Reader View). Strips navigation, ads, sidebars, and other clutter. Returns formatted text with title, author, and content: **getPageMarkdown** - extract main page content as plain text using Mozilla Readability (same algorithm as Firefox Reader View). Strips navigation, ads, sidebars, and other clutter. Returns formatted text with title, author, and content:
```js ```js
await getPageMarkdown({ page, search?, showDiffSinceLastCall? }) await getPageMarkdown({ page: state.page, search?, showDiffSinceLastCall? })
// Examples: // Examples:
const content = await getPageMarkdown({ page, showDiffSinceLastCall: false }) // full article const content = await getPageMarkdown({ page: state.page, showDiffSinceLastCall: false }) // full article
const matches = await getPageMarkdown({ page, search: /API/i }) // search within content const matches = await getPageMarkdown({ page: state.page, search: /API/i }) // search within content
``` ```
**Output format:** **Output format:**
@@ -727,42 +786,45 @@ The main article content as plain text, with paragraphs preserved...
**waitForPageLoad** - smart load detection that ignores analytics/ads: **waitForPageLoad** - smart load detection that ignores analytics/ads:
```js ```js
await waitForPageLoad({ page, timeout?, pollInterval?, minWait? }) await waitForPageLoad({ page: state.page, timeout?, pollInterval?, minWait? })
// Returns: { success, readyState, pendingRequests, waitTimeMs, timedOut } // Returns: { success, readyState, pendingRequests, waitTimeMs, timedOut }
``` ```
**getCDPSession** - send raw CDP commands: **getCDPSession** - send raw CDP commands:
```js ```js
const cdp = await getCDPSession({ page }) const cdp = await getCDPSession({ page: state.page })
const metrics = await cdp.send('Page.getLayoutMetrics') const metrics = await cdp.send('Page.getLayoutMetrics')
``` ```
**getLocatorStringForElement** - get stable Playwright selector from an element: **getLocatorStringForElement** - get stable Playwright selector from an element:
```js ```js
const selector = await getLocatorStringForElement(page.locator('[id="submit-btn"]')) const selector = await getLocatorStringForElement(state.page.locator('[id="submit-btn"]'))
// => "getByRole('button', { name: 'Save' })" // => "getByRole('button', { name: 'Save' })"
``` ```
**getReactSource** - get React component source location (dev mode only): **getReactSource** - get React component source location (dev mode only):
```js ```js
const source = await getReactSource({ locator: page.locator('[data-testid="submit-btn"]') }) const source = await getReactSource({ locator: state.page.locator('[data-testid="submit-btn"]') })
// => { fileName, lineNumber, columnNumber, componentName } // => { fileName, lineNumber, columnNumber, componentName }
``` ```
**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 fetch `https://playwriter.dev/resources/styles-api.md` first with curl or webfetch tool. **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 fetch `https://playwriter.dev/resources/styles-api.md` first with curl or webfetch tool.
```js ```js
const styles = await getStylesForLocator({ locator: page.locator('.btn'), cdp: await getCDPSession({ page }) }) const styles = await getStylesForLocator({
locator: state.page.locator('.btn'),
cdp: await getCDPSession({ page: state.page }),
})
console.log(formatStylesAsText(styles)) console.log(formatStylesAsText(styles))
``` ```
**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 fetch `https://playwriter.dev/resources/debugger-api.md` first. **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 fetch `https://playwriter.dev/resources/debugger-api.md` first.
```js ```js
const cdp = await getCDPSession({ page }) const cdp = await getCDPSession({ page: state.page })
const dbg = createDebugger({ cdp }) const dbg = createDebugger({ cdp })
await dbg.enable() await dbg.enable()
const scripts = await dbg.listScripts({ search: 'app' }) const scripts = await dbg.listScripts({ search: 'app' })
@@ -773,7 +835,7 @@ await dbg.setBreakpoint({ file: scripts[0].url, line: 42 })
**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. **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.
```js ```js
const cdp = await getCDPSession({ page }) const cdp = await getCDPSession({ page: state.page })
const editor = createEditor({ cdp }) const editor = createEditor({ cdp })
await editor.enable() await editor.enable()
const matches = await editor.grep({ regex: /console\.log/ }) const matches = await editor.grep({ regex: /console\.log/ })
@@ -785,28 +847,30 @@ await editor.edit({ url: matches[0].url, oldString: 'DEBUG = false', newString:
Prefer this for pages with grids, image galleries, maps, or complex visual layouts where spatial position matters. For simple text-heavy pages, `snapshot` with search is faster and uses fewer tokens. Prefer this for pages with grids, image galleries, maps, or complex visual layouts where spatial position matters. For simple text-heavy pages, `snapshot` with search is faster and uses fewer tokens.
```js ```js
await screenshotWithAccessibilityLabels({ page }) await screenshotWithAccessibilityLabels({ page: state.page })
// Image and accessibility snapshot are automatically included in response // Image and accessibility snapshot are automatically included in response
// Use refs from snapshot to interact with elements // Use refs from snapshot to interact with elements
await page.locator('[id="submit-btn"]').click() await state.page.locator('[id="submit-btn"]').click()
// Can take multiple screenshots in one execution // Can take multiple screenshots in one execution
await screenshotWithAccessibilityLabels({ page }) await screenshotWithAccessibilityLabels({ page: state.page })
await page.click('button') await state.page.click('button')
await screenshotWithAccessibilityLabels({ page }) await screenshotWithAccessibilityLabels({ page: state.page })
// Both images are included in the response // Both images are included in the response
``` ```
Labels are color-coded: yellow=links, orange=buttons, coral=inputs, pink=checkboxes, peach=sliders, salmon=menus, amber=tabs. Labels are color-coded: yellow=links, orange=buttons, coral=inputs, pink=checkboxes, peach=sliders, salmon=menus, amber=tabs.
**startRecording / stopRecording** - record the page as a video at native FPS (30-60fps). Uses `chrome.tabCapture` in the extension context, so **recording survives page navigation**. Video is saved as mp4. **recording.start / recording.stop** - record the page as a video at native FPS (30-60fps). Uses `chrome.tabCapture` in the extension context, so **recording survives page navigation**. Video is saved as mp4.
While recording is active, Playwriter automatically overlays a smooth ghost cursor that follows automated mouse actions (`page.mouse.*`, `locator.click()`, hover flows) using `page.onMouseAction` from the Playwright fork.
**Note**: Recording requires the user to have clicked the Playwriter extension icon on the tab. This grants `activeTab` permission needed for `chrome.tabCapture`. Recording works on tabs where the icon was clicked - if you need to record a new tab, ask the user to click the icon on it first. **Note**: Recording requires the user to have clicked the Playwriter extension icon on the tab. This grants `activeTab` permission needed for `chrome.tabCapture`. Recording works on tabs where the icon was clicked - if you need to record a new tab, ask the user to click the icon on it first.
```js ```js
// Start recording - outputPath must be specified upfront // Start recording - outputPath must be specified upfront
await startRecording({ await recording.start({
page, page: state.page,
outputPath: './recording.mp4', outputPath: './recording.mp4',
frameRate: 30, // default: 30 frameRate: 30, // default: 30
audio: false, // default: false (tab audio) audio: false, // default: false (tab audio)
@@ -814,12 +878,12 @@ await startRecording({
}) })
// Navigate around - recording continues! // Navigate around - recording continues!
await page.click('a') await state.page.click('a')
await page.waitForLoadState('domcontentloaded') await state.page.waitForLoadState('domcontentloaded')
await page.goBack() await state.page.goBack()
// Stop and get result // Stop and get result
const { path, duration, size } = await stopRecording({ page }) const { path, duration, size } = await recording.stop({ page: state.page })
console.log(`Saved ${size} bytes, duration: ${duration}ms`) console.log(`Saved ${size} bytes, duration: ${duration}ms`)
``` ```
@@ -827,20 +891,65 @@ Additional recording utilities:
```js ```js
// Check if recording is active // Check if recording is active
const { isRecording, startedAt } = await isRecording({ page }) const { isRecording, startedAt } = await recording.isRecording({ page: state.page })
// Cancel recording without saving // Cancel recording without saving
await cancelRecording({ page }) await recording.cancel({ page: state.page })
``` ```
**ghostCursor.show / ghostCursor.hide** - manually show or hide the in-page cursor overlay. Useful for screenshots and demos even when recording is not running.
```js
// Show cursor in the center (or keep current position if already visible)
await ghostCursor.show({ page: state.page })
// Hide cursor overlay
await ghostCursor.hide({ page: state.page })
```
`startRecording`, `stopRecording`, `isRecording`, and `cancelRecording` remain available as backward-compatible aliases.
**Key difference from getDisplayMedia**: This approach uses `chrome.tabCapture` which runs in the extension context, not the page. The recording persists across navigations because the extension holds the `MediaRecorder`, not the page's JavaScript context. **Key difference from getDisplayMedia**: This approach uses `chrome.tabCapture` which runs in the extension context, not the page. The recording persists across navigations because the extension holds the `MediaRecorder`, not the page's JavaScript context.
**createDemoVideo** - create a polished demo video from a recording by automatically speeding up idle sections (time between execute() calls) while keeping interactions at normal speed. Useful for creating demo videos of agent workflows without long pauses.
While recording is active, playwriter tracks when each `execute()` call starts and ends. `recording.stop()` returns these timestamps alongside the video file. `createDemoVideo` uses this data to identify idle gaps and speed them up with ffmpeg in a single pass.
A 1-second buffer is preserved around each interaction so viewers see context before and after each action.
Requires `ffmpeg` and `ffprobe` installed on the system.
```js
// Start recording
await recording.start({ page: state.page, outputPath: './recording.mp4' })
```
```js
// ... multiple execute() calls with browser interactions ...
// Each call's timing is tracked automatically while recording is active
```
```js
// Stop recording — executionTimestamps is included in the result
const recordingResult = await recording.stop({ page: state.page })
// Create demo video — idle gaps are sped up 4x (default)
const demoPath = await createDemoVideo({
recordingPath: recordingResult.path,
durationMs: recordingResult.duration,
executionTimestamps: recordingResult.executionTimestamps,
speed: 5, // optional, default 5x for idle sections
// outputFile: './demo.mp4', // optional, defaults to recording-demo.mp4
})
console.log('Demo video:', demoPath)
```
## pinned elements ## pinned elements
Users can right-click → "Copy Playwriter Element Reference" to store elements in `globalThis.playwriterPinnedElem1` (increments for each pin). The reference is copied to clipboard: Users can right-click → "Copy Playwriter Element Reference" to store elements in `globalThis.playwriterPinnedElem1` (increments for each pin). The reference is copied to clipboard:
```js ```js
const el = await page.evaluateHandle(() => globalThis.playwriterPinnedElem1) const el = await state.page.evaluateHandle(() => globalThis.playwriterPinnedElem1)
await el.click() await el.click()
``` ```
@@ -849,7 +958,7 @@ await el.click()
Always use `scale: 'css'` to avoid 2-4x larger images on high-DPI displays: Always use `scale: 'css'` to avoid 2-4x larger images on high-DPI displays:
```js ```js
await page.screenshot({ path: 'shot.png', scale: 'css' }) await state.page.screenshot({ path: 'shot.png', scale: 'css' })
``` ```
If you want to read back the image file into context make sure to resize it first, scaling down the image to make sure max size is 1500px. for example with `sips --resampleHeightWidthMax 1500 input.png --out output.png` on macOS. If you want to read back the image file into context make sure to resize it first, scaling down the image to make sure max size is 1500px. for example with `sips --resampleHeightWidthMax 1500 input.png --out output.png` on macOS.
@@ -859,10 +968,10 @@ If you want to read back the image file into context make sure to resize it firs
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): 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):
```js ```js
const title = await page.evaluate(() => document.title) const title = await state.page.evaluate(() => document.title)
console.log('Title:', title) console.log('Title:', title)
const info = await page.evaluate(() => ({ const info = await state.page.evaluate(() => ({
url: location.href, url: location.href,
buttons: document.querySelectorAll('button').length, buttons: document.querySelectorAll('button').length,
})) }))
@@ -876,7 +985,7 @@ Fill inputs with file content:
```js ```js
const fs = require('node:fs') const fs = require('node:fs')
const content = fs.readFileSync('./data.txt', 'utf-8') const content = fs.readFileSync('./data.txt', 'utf-8')
await page.locator('textarea').fill(content) await state.page.locator('textarea').fill(content)
``` ```
## network interception ## network interception
@@ -886,10 +995,10 @@ For scraping or reverse-engineering APIs, intercept network requests instead of
```js ```js
state.requests = [] state.requests = []
state.responses = [] state.responses = []
page.on('request', (req) => { state.page.on('request', (req) => {
if (req.url().includes('/api/')) state.requests.push({ url: req.url(), method: req.method(), headers: req.headers() }) if (req.url().includes('/api/')) state.requests.push({ url: req.url(), method: req.method(), headers: req.headers() })
}) })
page.on('response', async (res) => { state.page.on('response', async (res) => {
if (res.url().includes('/api/')) { if (res.url().includes('/api/')) {
try { try {
state.responses.push({ url: res.url(), status: res.status(), body: await res.json() }) state.responses.push({ url: res.url(), status: res.status(), body: await res.json() })
@@ -916,7 +1025,7 @@ Replay API directly (useful for pagination):
```js ```js
const { url, headers } = state.requests.find((r) => r.url.includes('feed')) const { url, headers } = state.requests.find((r) => r.url.includes('feed'))
const data = await page.evaluate( const data = await state.page.evaluate(
async ({ url, headers }) => { async ({ url, headers }) => {
const res = await fetch(url, { headers }) const res = await fetch(url, { headers })
return res.json() return res.json()
@@ -926,7 +1035,7 @@ const data = await page.evaluate(
console.log(data) console.log(data)
``` ```
Clean up listeners when done: `page.removeAllListeners('request'); page.removeAllListeners('response');` Clean up listeners when done: `state.page.removeAllListeners('request'); state.page.removeAllListeners('response');`
## debugging web apps ## debugging web apps
@@ -935,14 +1044,14 @@ When debugging why a web app isn't working (e.g., content not rendering, API err
**1. Console logs** — use `getLatestLogs` to check for errors: **1. Console logs** — use `getLatestLogs` to check for errors:
```js ```js
const errors = await getLatestLogs({ page, search: /error|fail/i, count: 20 }) const errors = await getLatestLogs({ page: state.page, search: /error|fail/i, count: 20 })
const appLogs = await getLatestLogs({ page, search: /myComponent|state/i }) const appLogs = await getLatestLogs({ page: state.page, search: /myComponent|state/i })
``` ```
**2. DOM inspection via evaluate** — check content directly without screenshots: **2. DOM inspection via evaluate** — check content directly without screenshots:
```js ```js
const info = await page.evaluate(() => { const info = await state.page.evaluate(() => {
const msgs = document.querySelectorAll('.message') const msgs = document.querySelectorAll('.message')
return Array.from(msgs).map((m) => ({ return Array.from(msgs).map((m) => ({
text: m.textContent?.slice(0, 200), text: m.textContent?.slice(0, 200),
@@ -955,11 +1064,11 @@ console.log(JSON.stringify(info, null, 2))
**3. Combine snapshot + logs for full picture:** **3. Combine snapshot + logs for full picture:**
```js ```js
await page.keyboard.press('Enter') await state.page.keyboard.press('Enter')
await page.waitForTimeout(2000) await state.page.waitForTimeout(2000)
const snap = await snapshot({ page, search: /dialog|error|message/ }) const snap = await snapshot({ page: state.page, search: /dialog|error|message/ })
const logs = await getLatestLogs({ page, search: /error/i, count: 10 }) const logs = await getLatestLogs({ page: state.page, search: /error/i, count: 10 })
console.log('UI:', snap) console.log('UI:', snap)
console.log('Logs:', logs) console.log('Logs:', logs)
``` ```
@@ -987,47 +1096,47 @@ This section covers low-level mouse/keyboard APIs not documented elsewhere. For
```js ```js
// Preferred: by locator (stable, auto-waits, no coordinates needed) // Preferred: by locator (stable, auto-waits, no coordinates needed)
await page.locator('button[name="Submit"]').click() await state.page.locator('button[name="Submit"]').click()
await page.locator('text=Login').click({ button: 'right' }) await state.page.locator('text=Login').click({ button: 'right' })
await page.locator('text=Login').dblclick() await state.page.locator('text=Login').dblclick()
await page await state.page
.locator('a') .locator('a')
.first() .first()
.click({ modifiers: ['Meta'] }) // cmd+click opens new tab .click({ modifiers: ['Meta'] }) // cmd+click opens new tab
// By coordinates (when locators aren't available, e.g. canvas, maps, custom widgets) // By coordinates (when locators aren't available, e.g. canvas, maps, custom widgets)
await page.mouse.click(450, 320) // left click await state.page.mouse.click(450, 320) // left click
await page.mouse.click(450, 320, { button: 'right' }) // right click await state.page.mouse.click(450, 320, { button: 'right' }) // right click
await page.mouse.dblclick(450, 320) // double click await state.page.mouse.dblclick(450, 320) // double click
await page.mouse.click(450, 320, { clickCount: 3 }) // triple click await state.page.mouse.click(450, 320, { clickCount: 3 }) // triple click
await page.mouse.click(450, 320, { modifiers: ['Shift'] }) // shift+click await state.page.mouse.click(450, 320, { modifiers: ['Shift'] }) // shift+click
``` ```
### hover ### hover
```js ```js
await page.locator('.tooltip-trigger').hover() // by locator (preferred) await state.page.locator('.tooltip-trigger').hover() // by locator (preferred)
await page.mouse.move(450, 320) // by coordinates await state.page.mouse.move(450, 320) // by coordinates
``` ```
### scroll ### scroll
```js ```js
// By locator (preferred) // By locator (preferred)
await page.locator('#footer').scrollIntoViewIfNeeded() await state.page.locator('#footer').scrollIntoViewIfNeeded()
// By pixel (for canvas, maps, infinite scroll) // By pixel (for canvas, maps, infinite scroll)
await page.mouse.wheel(0, 300) // scroll down 300px await state.page.mouse.wheel(0, 300) // scroll down 300px
await page.mouse.wheel(0, -300) // scroll up await state.page.mouse.wheel(0, -300) // scroll up
await page.mouse.wheel(300, 0) // scroll right await state.page.mouse.wheel(300, 0) // scroll right
await page.mouse.wheel(-300, 0) // scroll left await state.page.mouse.wheel(-300, 0) // scroll left
// Scroll at a specific position // Scroll at a specific position
await page.mouse.move(450, 320) await state.page.mouse.move(450, 320)
await page.mouse.wheel(0, 500) await state.page.mouse.wheel(0, 500)
// Scroll inside a container // Scroll inside a container
await page.locator('.scrollable-list').evaluate((el) => { await state.page.locator('.scrollable-list').evaluate((el) => {
el.scrollTop += 500 el.scrollTop += 500
}) })
``` ```
@@ -1036,37 +1145,37 @@ await page.locator('.scrollable-list').evaluate((el) => {
```js ```js
// By locator (preferred) // By locator (preferred)
await page.locator('#item').dragTo(page.locator('#target')) await state.page.locator('#item').dragTo(state.page.locator('#target'))
// By coordinates (for canvas, sliders, custom drag targets) // By coordinates (for canvas, sliders, custom drag targets)
await page.mouse.move(100, 200) await state.page.mouse.move(100, 200)
await page.mouse.down() await state.page.mouse.down()
await page.mouse.move(400, 500, { steps: 10 }) // steps for smooth drag await state.page.mouse.move(400, 500, { steps: 10 }) // steps for smooth drag
await page.mouse.up() await state.page.mouse.up()
``` ```
### key hold / release / repeat ### key hold / release / repeat
```js ```js
// Hold modifier while pressing another key // Hold modifier while pressing another key
await page.keyboard.down('Shift') await state.page.keyboard.down('Shift')
await page.keyboard.press('ArrowDown') await state.page.keyboard.press('ArrowDown')
await page.keyboard.up('Shift') await state.page.keyboard.up('Shift')
// Repeat a key // Repeat a key
for (let i = 0; i < 5; i++) await page.keyboard.press('ArrowDown') for (let i = 0; i < 5; i++) await state.page.keyboard.press('ArrowDown')
``` ```
### resize viewport ### resize viewport
```js ```js
await page.setViewportSize({ width: 1280, height: 720 }) await state.page.setViewportSize({ width: 1280, height: 720 })
``` ```
### region screenshot (zoom equivalent) ### region screenshot (zoom equivalent)
```js ```js
await page.screenshot({ path: 'region.png', scale: 'css', clip: { x: 100, y: 200, width: 400, height: 300 } }) await state.page.screenshot({ path: 'region.png', scale: 'css', clip: { x: 100, y: 200, width: 400, height: 300 } })
``` ```
Prefer locator-based actions over coordinates — locators are stable across scroll/resize, auto-wait for elements, and don't require screenshot round-trips that burn ~800 image tokens per cycle. Prefer locator-based actions over coordinates — locators are stable across scroll/resize, auto-wait for elements, and don't require screenshot round-trips that burn ~800 image tokens per cycle.
+78 -53
View File
@@ -2,31 +2,31 @@
## Types ## Types
````ts ```ts
import type { ICDPSession } from './cdp-session.js' import type { ICDPSession } from './cdp-session.js';
export interface BreakpointInfo { export interface BreakpointInfo {
id: string id: string;
file: string file: string;
line: number line: number;
} }
export interface LocationInfo { export interface LocationInfo {
url: string url: string;
lineNumber: number lineNumber: number;
columnNumber: number columnNumber: number;
callstack: Array<{ callstack: Array<{
functionName: string functionName: string;
url: string url: string;
lineNumber: number lineNumber: number;
columnNumber: number columnNumber: number;
}> }>;
sourceContext: string sourceContext: string;
} }
export interface EvaluateResult { export interface EvaluateResult {
value: unknown value: unknown;
} }
export interface ScriptInfo { export interface ScriptInfo {
scriptId: string scriptId: string;
url: string url: string;
} }
/** /**
* A class for debugging JavaScript code via Chrome DevTools Protocol. * A class for debugging JavaScript code via Chrome DevTools Protocol.
@@ -45,14 +45,14 @@ export interface ScriptInfo {
* ``` * ```
*/ */
export declare class Debugger { export declare class Debugger {
private cdp private cdp;
private debuggerEnabled private debuggerEnabled;
private paused private paused;
private currentCallFrames private currentCallFrames;
private breakpoints private breakpoints;
private scripts private scripts;
private xhrBreakpoints private xhrBreakpoints;
private blackboxPatterns private blackboxPatterns;
/** /**
* Creates a new Debugger instance. * Creates a new Debugger instance.
* *
@@ -66,8 +66,10 @@ export declare class Debugger {
* const dbg = new Debugger({ cdp }) * const dbg = new Debugger({ cdp })
* ``` * ```
*/ */
constructor({ cdp }: { cdp: ICDPSession }) constructor({ cdp }: {
private setupEventListeners cdp: ICDPSession;
});
private setupEventListeners;
/** /**
* Enables the debugger and runtime domains. Called automatically by other methods. * Enables the debugger and runtime domains. Called automatically by other methods.
* Also resumes execution if the target was started with --inspect-brk. * Also resumes execution if the target was started with --inspect-brk.
@@ -77,7 +79,7 @@ export declare class Debugger {
* await dbg.enable() * await dbg.enable()
* ``` * ```
*/ */
enable(): Promise<void> enable(): Promise<void>;
/** /**
* Sets a breakpoint at a specified URL and line number. * Sets a breakpoint at a specified URL and line number.
* Use the URL from listScripts() to find available scripts. * Use the URL from listScripts() to find available scripts.
@@ -102,7 +104,11 @@ export declare class Debugger {
* }) * })
* ``` * ```
*/ */
setBreakpoint({ file, line, condition }: { file: string; line: number; condition?: string }): Promise<string> setBreakpoint({ file, line, condition }: {
file: string;
line: number;
condition?: string;
}): Promise<string>;
/** /**
* Removes a breakpoint by its ID. * Removes a breakpoint by its ID.
* *
@@ -114,7 +120,9 @@ export declare class Debugger {
* await dbg.deleteBreakpoint({ breakpointId: 'bp-123' }) * await dbg.deleteBreakpoint({ breakpointId: 'bp-123' })
* ``` * ```
*/ */
deleteBreakpoint({ breakpointId }: { breakpointId: string }): Promise<void> deleteBreakpoint({ breakpointId }: {
breakpointId: string;
}): Promise<void>;
/** /**
* Returns a list of all active breakpoints set by this debugger instance. * Returns a list of all active breakpoints set by this debugger instance.
* *
@@ -126,7 +134,7 @@ export declare class Debugger {
* // [{ id: 'bp-123', file: 'https://example.com/index.js', line: 42 }] * // [{ id: 'bp-123', file: 'https://example.com/index.js', line: 42 }]
* ``` * ```
*/ */
listBreakpoints(): BreakpointInfo[] listBreakpoints(): BreakpointInfo[];
/** /**
* Inspects local variables in the current call frame. * Inspects local variables in the current call frame.
* Must be paused at a breakpoint. String values over 1000 chars are truncated. * Must be paused at a breakpoint. String values over 1000 chars are truncated.
@@ -141,7 +149,7 @@ export declare class Debugger {
* // { myVar: 'hello', count: 42 } * // { myVar: 'hello', count: 42 }
* ``` * ```
*/ */
inspectLocalVariables(): Promise<Record<string, unknown>> inspectLocalVariables(): Promise<Record<string, unknown>>;
/** /**
* Returns global lexical scope variable names. * Returns global lexical scope variable names.
* *
@@ -153,7 +161,7 @@ export declare class Debugger {
* // ['myGlobal', 'CONFIG'] * // ['myGlobal', 'CONFIG']
* ``` * ```
*/ */
inspectGlobalVariables(): Promise<string[]> inspectGlobalVariables(): Promise<string[]>;
/** /**
* Evaluates a JavaScript expression and returns the result. * Evaluates a JavaScript expression and returns the result.
* When paused at a breakpoint, evaluates in the current stack frame scope, * When paused at a breakpoint, evaluates in the current stack frame scope,
@@ -173,7 +181,9 @@ export declare class Debugger {
* const full = await dbg.evaluate({ expression: 'largeStringVar' }) * const full = await dbg.evaluate({ expression: 'largeStringVar' })
* ``` * ```
*/ */
evaluate({ expression }: { expression: string }): Promise<EvaluateResult> evaluate({ expression }: {
expression: string;
}): Promise<EvaluateResult>;
/** /**
* Gets the current execution location when paused at a breakpoint. * Gets the current execution location when paused at a breakpoint.
* Includes the call stack and surrounding source code for context. * Includes the call stack and surrounding source code for context.
@@ -194,7 +204,7 @@ export declare class Debugger {
* // 43: }' * // 43: }'
* ``` * ```
*/ */
getLocation(): Promise<LocationInfo> getLocation(): Promise<LocationInfo>;
/** /**
* Steps over to the next line of code, not entering function calls. * Steps over to the next line of code, not entering function calls.
* *
@@ -206,7 +216,7 @@ export declare class Debugger {
* const newLocation = await dbg.getLocation() * const newLocation = await dbg.getLocation()
* ``` * ```
*/ */
stepOver(): Promise<void> stepOver(): Promise<void>;
/** /**
* Steps into a function call on the current line. * Steps into a function call on the current line.
* *
@@ -219,7 +229,7 @@ export declare class Debugger {
* // now inside the called function * // now inside the called function
* ``` * ```
*/ */
stepInto(): Promise<void> stepInto(): Promise<void>;
/** /**
* Steps out of the current function, returning to the caller. * Steps out of the current function, returning to the caller.
* *
@@ -232,7 +242,7 @@ export declare class Debugger {
* // back in the calling function * // back in the calling function
* ``` * ```
*/ */
stepOut(): Promise<void> stepOut(): Promise<void>;
/** /**
* Resumes code execution until the next breakpoint or completion. * Resumes code execution until the next breakpoint or completion.
* *
@@ -244,7 +254,7 @@ export declare class Debugger {
* // execution continues * // execution continues
* ``` * ```
*/ */
resume(): Promise<void> resume(): Promise<void>;
/** /**
* Returns whether the debugger is currently paused at a breakpoint. * Returns whether the debugger is currently paused at a breakpoint.
* *
@@ -257,7 +267,7 @@ export declare class Debugger {
* } * }
* ``` * ```
*/ */
isPaused(): boolean isPaused(): boolean;
/** /**
* Configures the debugger to pause on exceptions. * Configures the debugger to pause on exceptions.
* *
@@ -276,7 +286,9 @@ export declare class Debugger {
* await dbg.setPauseOnExceptions({ state: 'none' }) * await dbg.setPauseOnExceptions({ state: 'none' })
* ``` * ```
*/ */
setPauseOnExceptions({ state }: { state: 'none' | 'uncaught' | 'all' }): Promise<void> setPauseOnExceptions({ state }: {
state: 'none' | 'uncaught' | 'all';
}): Promise<void>;
/** /**
* Lists available scripts where breakpoints can be set. * Lists available scripts where breakpoints can be set.
* Automatically enables the debugger if not already enabled. * Automatically enables the debugger if not already enabled.
@@ -296,10 +308,16 @@ export declare class Debugger {
* // [{ scriptId: '5', url: 'https://example.com/handlers.js' }] * // [{ scriptId: '5', url: 'https://example.com/handlers.js' }]
* ``` * ```
*/ */
listScripts({ search }?: { search?: string }): Promise<ScriptInfo[]> listScripts({ search }?: {
setXHRBreakpoint({ url }: { url: string }): Promise<void> search?: string;
removeXHRBreakpoint({ url }: { url: string }): Promise<void> }): Promise<ScriptInfo[]>;
listXHRBreakpoints(): string[] setXHRBreakpoint({ url }: {
url: string;
}): Promise<void>;
removeXHRBreakpoint({ url }: {
url: string;
}): Promise<void>;
listXHRBreakpoints(): string[];
/** /**
* Sets regex patterns for scripts to blackbox (skip when stepping). * Sets regex patterns for scripts to blackbox (skip when stepping).
* Blackboxed scripts are hidden from the call stack and stepped over automatically. * Blackboxed scripts are hidden from the call stack and stepped over automatically.
@@ -330,7 +348,9 @@ export declare class Debugger {
* await dbg.setBlackboxPatterns({ patterns: [] }) * await dbg.setBlackboxPatterns({ patterns: [] })
* ``` * ```
*/ */
setBlackboxPatterns({ patterns }: { patterns: string[] }): Promise<void> setBlackboxPatterns({ patterns }: {
patterns: string[];
}): Promise<void>;
/** /**
* Adds a single regex pattern to the blackbox list. * Adds a single regex pattern to the blackbox list.
* *
@@ -343,23 +363,27 @@ export declare class Debugger {
* await dbg.addBlackboxPattern({ pattern: 'node_modules/axios' }) * await dbg.addBlackboxPattern({ pattern: 'node_modules/axios' })
* ``` * ```
*/ */
addBlackboxPattern({ pattern }: { pattern: string }): Promise<void> addBlackboxPattern({ pattern }: {
pattern: string;
}): Promise<void>;
/** /**
* Removes a pattern from the blackbox list. * Removes a pattern from the blackbox list.
* *
* @param options - Options * @param options - Options
* @param options.pattern - The exact pattern string to remove * @param options.pattern - The exact pattern string to remove
*/ */
removeBlackboxPattern({ pattern }: { pattern: string }): Promise<void> removeBlackboxPattern({ pattern }: {
pattern: string;
}): Promise<void>;
/** /**
* Returns the current list of blackbox patterns. * Returns the current list of blackbox patterns.
*/ */
listBlackboxPatterns(): string[] listBlackboxPatterns(): string[];
private truncateValue private truncateValue;
private formatPropertyValue private formatPropertyValue;
private processRemoteObject private processRemoteObject;
} }
```` ```
## Examples ## Examples
@@ -430,4 +454,5 @@ async function cleanupBreakpoints() {
} }
export { listScriptsAndSetBreakpoint, inspectWhenPaused, stepThroughCode, cleanupBreakpoints } export { listScriptsAndSetBreakpoint, inspectWhenPaused, stepThroughCode, cleanupBreakpoints }
``` ```
+49 -38
View File
@@ -4,22 +4,22 @@ The Editor class provides a Claude Code-like interface for viewing and editing w
## Types ## Types
````ts ```ts
import type { ICDPSession } from './cdp-session.js' import type { ICDPSession } from './cdp-session.js';
export interface ReadResult { export interface ReadResult {
content: string content: string;
totalLines: number totalLines: number;
startLine: number startLine: number;
endLine: number endLine: number;
} }
export interface SearchMatch { export interface SearchMatch {
url: string url: string;
lineNumber: number lineNumber: number;
lineContent: string lineContent: string;
} }
export interface EditResult { export interface EditResult {
success: boolean success: boolean;
stackChanged?: boolean stackChanged?: boolean;
} }
/** /**
* A class for viewing and editing web page scripts via Chrome DevTools Protocol. * A class for viewing and editing web page scripts via Chrome DevTools Protocol.
@@ -49,20 +49,22 @@ export interface EditResult {
* ``` * ```
*/ */
export declare class Editor { export declare class Editor {
private cdp private cdp;
private enabled private enabled;
private scripts private scripts;
private stylesheets private stylesheets;
private sourceCache private sourceCache;
constructor({ cdp }: { cdp: ICDPSession }) constructor({ cdp }: {
private setupEventListeners cdp: ICDPSession;
});
private setupEventListeners;
/** /**
* Enables the editor. Must be called before other methods. * Enables the editor. Must be called before other methods.
* Scripts are collected from Debugger.scriptParsed events. * Scripts are collected from Debugger.scriptParsed events.
* Reload the page after enabling to capture all scripts. * Reload the page after enabling to capture all scripts.
*/ */
enable(): Promise<void> enable(): Promise<void>;
private getIdByUrl private getIdByUrl;
/** /**
* Lists available script and stylesheet URLs. Use pattern to filter by regex. * Lists available script and stylesheet URLs. Use pattern to filter by regex.
* Automatically enables the editor if not already enabled. * Automatically enables the editor if not already enabled.
@@ -86,7 +88,9 @@ export declare class Editor {
* const appScripts = await editor.list({ pattern: /app/ }) * const appScripts = await editor.list({ pattern: /app/ })
* ``` * ```
*/ */
list({ pattern }?: { pattern?: RegExp }): Promise<string[]> list({ pattern }?: {
pattern?: RegExp;
}): Promise<string[]>;
/** /**
* Reads a script or stylesheet's source code by URL. * Reads a script or stylesheet's source code by URL.
* Returns line-numbered content like Claude Code's Read tool. * Returns line-numbered content like Claude Code's Read tool.
@@ -116,8 +120,12 @@ export declare class Editor {
* }) * })
* ``` * ```
*/ */
read({ url, offset, limit }: { url: string; offset?: number; limit?: number }): Promise<ReadResult> read({ url, offset, limit }: {
private getSource url: string;
offset?: number;
limit?: number;
}): Promise<ReadResult>;
private getSource;
/** /**
* Edits a script or stylesheet by replacing oldString with newString. * Edits a script or stylesheet by replacing oldString with newString.
* Like Claude Code's Edit tool - performs exact string replacement. * Like Claude Code's Edit tool - performs exact string replacement.
@@ -146,18 +154,13 @@ export declare class Editor {
* }) * })
* ``` * ```
*/ */
edit({ edit({ url, oldString, newString, dryRun, }: {
url, url: string;
oldString, oldString: string;
newString, newString: string;
dryRun, dryRun?: boolean;
}: { }): Promise<EditResult>;
url: string private setSource;
oldString: string
newString: string
dryRun?: boolean
}): Promise<EditResult>
private setSource
/** /**
* Searches for a regex across all scripts and stylesheets. * Searches for a regex across all scripts and stylesheets.
* Like Claude Code's Grep tool - returns matching lines with context. * Like Claude Code's Grep tool - returns matching lines with context.
@@ -185,7 +188,10 @@ export declare class Editor {
* }) * })
* ``` * ```
*/ */
grep({ regex, pattern }: { regex: RegExp; pattern?: RegExp }): Promise<SearchMatch[]> grep({ regex, pattern }: {
regex: RegExp;
pattern?: RegExp;
}): Promise<SearchMatch[]>;
/** /**
* Writes entire content to a script or stylesheet, replacing all existing code. * Writes entire content to a script or stylesheet, replacing all existing code.
* Use with caution - prefer edit() for targeted changes. * Use with caution - prefer edit() for targeted changes.
@@ -195,9 +201,13 @@ export declare class Editor {
* @param options.content - New content * @param options.content - New content
* @param options.dryRun - If true, validate without applying (default false, only works for JS) * @param options.dryRun - If true, validate without applying (default false, only works for JS)
*/ */
write({ url, content, dryRun }: { url: string; content: string; dryRun?: boolean }): Promise<EditResult> write({ url, content, dryRun, }: {
url: string;
content: string;
dryRun?: boolean;
}): Promise<EditResult>;
} }
```` ```
## Examples ## Examples
@@ -360,4 +370,5 @@ export {
editStylesheet, editStylesheet,
searchStyles, searchStyles,
} }
``` ```
+21 -24
View File
@@ -5,36 +5,32 @@ The getStylesForLocator function inspects CSS styles applied to an element, simi
## Types ## Types
```ts ```ts
import type { ICDPSession } from './cdp-session.js' import type { ICDPSession } from './cdp-session.js';
import type { Locator } from '@xmorse/playwright-core' import type { Locator } from '@xmorse/playwright-core';
export interface StyleSource { export interface StyleSource {
url: string url: string;
line: number line: number;
column: number column: number;
} }
export type StyleDeclarations = Record<string, string> export type StyleDeclarations = Record<string, string>;
export interface StyleRule { export interface StyleRule {
selector: string selector: string;
source: StyleSource | null source: StyleSource | null;
origin: 'regular' | 'user-agent' | 'injected' | 'inspector' origin: 'regular' | 'user-agent' | 'injected' | 'inspector';
declarations: StyleDeclarations declarations: StyleDeclarations;
inheritedFrom: string | null inheritedFrom: string | null;
} }
export interface StylesResult { export interface StylesResult {
element: string element: string;
inlineStyle: StyleDeclarations | null inlineStyle: StyleDeclarations | null;
rules: StyleRule[] rules: StyleRule[];
} }
export declare function getStylesForLocator({ export declare function getStylesForLocator({ locator, cdp: cdpSession, includeUserAgentStyles, }: {
locator, locator: Locator;
cdp: cdpSession, cdp: ICDPSession;
includeUserAgentStyles, includeUserAgentStyles?: boolean;
}: { }): Promise<StylesResult>;
locator: Locator export declare function formatStylesAsText(styles: StylesResult): string;
cdp: ICDPSession
includeUserAgentStyles?: boolean
}): Promise<StylesResult>
export declare function formatStylesAsText(styles: StylesResult): string
``` ```
## Examples ## Examples
@@ -124,4 +120,5 @@ export {
checkInheritedStyles, checkInheritedStyles,
compareStyles, compareStyles,
} }
``` ```