refactor: make SKILL.md source of truth, generate prompt.md from it
This commit is contained in:
@@ -19,3 +19,6 @@ opensrc/
|
||||
|
||||
# snapshots
|
||||
playwriter/src/snapshots/shadcn-ui-accessibility.md
|
||||
|
||||
# generated from skills/playwriter/SKILL.md
|
||||
playwriter/src/prompt.md
|
||||
|
||||
@@ -5,7 +5,7 @@ this is the playwriter codebase
|
||||
the extension uses chrome.debugger to manage the user browser
|
||||
|
||||
read ./README.md for an overview of how this extension and mcp work
|
||||
read playwriter/src/prompt.md to understand how the MCP works
|
||||
read skills/playwriter/SKILL.md to understand the MCP docs (source of truth)
|
||||
|
||||
## backward compatibility
|
||||
|
||||
@@ -34,11 +34,11 @@ extension/ contains the chrome extension code. you need to run `pnpm build` to m
|
||||
|
||||
when I ask you to release extension run package.json release script
|
||||
|
||||
playwriter contains the ws server and MCP code. also the tests for the mcp are there. playwriter/src/prompt.md contains the docs for the MCP the agent will use. you should add there important sections that help the agent control the browser well with the MCP interface
|
||||
playwriter contains the ws server and MCP code. also the tests for the mcp are there. skills/playwriter/SKILL.md is the source of truth for MCP docs - edit that file to update agent instructions. the build script generates playwriter/src/prompt.md (gitignored) from SKILL.md, stripping CLI-only sections.
|
||||
|
||||
playwriter/src/resource.md is for more generic knowledge about playwright that the agent can use when necessary, for things like best practices for selecting locators on the page
|
||||
|
||||
website/public/resources/ is auto-generated by `playwriter/scripts/build-resources.ts` during `pnpm build`. DO NOT edit these files manually - edit the source files in `playwriter/src/` instead (e.g. `debugger-examples.ts`, `editor-examples.ts`, `styles-examples.ts`)
|
||||
website/public/resources/ and website/public/SKILL.md are auto-generated by `playwriter/scripts/build-resources.ts` during `pnpm build`. DO NOT edit these files manually - edit the source files instead (e.g. `debugger-examples.ts`, `editor-examples.ts`, `styles-examples.ts`, `skills/playwriter/SKILL.md`)
|
||||
|
||||
## CDP docs
|
||||
|
||||
@@ -69,7 +69,7 @@ remember that every time the extension is activated in a tab that tab gets added
|
||||
|
||||
to debug server or extension issues you can also inspect the file @playwriter/relay-server.log to see both extension and server logs. with all cdp events sent. to see if there are events missing or something broken. this file is recreated every time the server is started and appended in real time. use rg to only read relevant lines and parts because it can get quite long
|
||||
|
||||
IMPORTANT: `pnpm test` will take about 30 seconds so set a timeout of at least 60000ms when running the pnpm test bash command
|
||||
IMPORTANT: `pnpm test` will take about 1 minute or more so set a timeout of at least 90000ms when running the pnpm test bash command
|
||||
|
||||
## changelogs
|
||||
|
||||
@@ -93,6 +93,10 @@ the playwright source code is cloned at `./tmp/playwright` (gitignored). use Tas
|
||||
- `packages/playwright-core/src/server/chromium/crBrowser.ts` - browser and page discovery
|
||||
- `packages/playwright-core/src/server/chromium/chromium.ts` - connectOverCDP implementation
|
||||
|
||||
## ./claude-extension
|
||||
|
||||
ignore ./claude-extension. this is the source code of the Claude Chrome extension. used to reverse engineer new methods and tools to extract and control the page
|
||||
|
||||
# core guidelines
|
||||
|
||||
when summarizing changes at the end of the message, be super short, a few words and in bullet points, use bold text to highlight important keywords. use markdown.
|
||||
@@ -449,25 +453,28 @@ gh pr-review comments reply 42 -R owner/repo \
|
||||
gh pr-review threads resolve 42 -R owner/repo --thread-id PRRT_kwDOAAABbcdEFG12
|
||||
```
|
||||
|
||||
## listing, searching, reading github repos files with gitchamber
|
||||
## reading github repos source code
|
||||
|
||||
you MUST use gitchamber.com to read repo files. first ALWAYS run `curl https://gitchamber.com` to read detailed usage docs. always use curl to fetch the responses of gitchamber.com
|
||||
```sh
|
||||
opensrc zod # npm package name
|
||||
|
||||
for example when working with the vercel ai sdk, you can fetch the latest docs using:
|
||||
# Using github: prefix
|
||||
opensrc github:owner/repo
|
||||
|
||||
https://gitchamber.com/repos/facebook/react/main/files
|
||||
# Using owner/repo shorthand
|
||||
opensrc facebook/react
|
||||
|
||||
https://gitchamber.com/repos/remorses/fumabase/main/files?glob=**/*.ts
|
||||
# Using full GitHub URL
|
||||
opensrc https://github.com/colinhacks/zod
|
||||
|
||||
https://gitchamber.com/repos/facebook/react/main/files/README.md?start=10&end=50
|
||||
# Fetch a specific branch or tag
|
||||
opensrc owner/repo@v1.0.0
|
||||
opensrc owner/repo#main
|
||||
|
||||
https://gitchamber.com/repos/facebook/react/main/search/useState
|
||||
# Mix packages and repos
|
||||
```
|
||||
|
||||
gitchamber allows you to list, search and read files in a repo. you MUST use it over alternatives likes raw.github.com, because
|
||||
- it allows you to use context usage better via limit and offset pagination
|
||||
- it can list files, even filtering by a specific glob (default is *.md and *.mdx)
|
||||
- it can search a repo for a specific substring
|
||||
- it can show the code with line numbers for each line, letting you find a specific line number
|
||||
This will download the source code in ./opensrc. which should be put in .gitignore
|
||||
|
||||
# playwright
|
||||
|
||||
@@ -497,26 +504,3 @@ const jsonSchema = toJSONSchema(mySchema, {
|
||||
```
|
||||
|
||||
github.md
|
||||
|
||||
<!-- opensrc:start -->
|
||||
|
||||
## Source Code Reference
|
||||
|
||||
Source code for dependencies is available in `opensrc/` for deeper understanding of implementation details.
|
||||
|
||||
See `opensrc/sources.json` for the list of available packages and their versions.
|
||||
|
||||
Use this source code when you need to understand how a package works internally, not just its types/interface.
|
||||
|
||||
### Fetching Additional Source Code
|
||||
|
||||
To fetch source code for a package or repository you need to understand, run:
|
||||
|
||||
```bash
|
||||
npx opensrc <package> # npm package (e.g., npx opensrc zod)
|
||||
npx opensrc pypi:<package> # Python package (e.g., npx opensrc pypi:requests)
|
||||
npx opensrc crates:<package> # Rust crate (e.g., npx opensrc crates:serde)
|
||||
npx opensrc <owner>/<repo> # GitHub repo (e.g., npx opensrc vercel/ai)
|
||||
```
|
||||
|
||||
<!-- opensrc:end -->
|
||||
@@ -3,7 +3,7 @@ this is the playwriter codebase
|
||||
the extension uses chrome.debugger to manage the user browser
|
||||
|
||||
read ./README.md for an overview of how this extension and mcp work
|
||||
read playwriter/src/prompt.md to understand how the MCP works
|
||||
read skills/playwriter/SKILL.md to understand the MCP docs (source of truth)
|
||||
|
||||
## backward compatibility
|
||||
|
||||
@@ -32,11 +32,11 @@ extension/ contains the chrome extension code. you need to run `pnpm build` to m
|
||||
|
||||
when I ask you to release extension run package.json release script
|
||||
|
||||
playwriter contains the ws server and MCP code. also the tests for the mcp are there. playwriter/src/prompt.md contains the docs for the MCP the agent will use. you should add there important sections that help the agent control the browser well with the MCP interface
|
||||
playwriter contains the ws server and MCP code. also the tests for the mcp are there. skills/playwriter/SKILL.md is the source of truth for MCP docs - edit that file to update agent instructions. the build script generates playwriter/src/prompt.md (gitignored) from SKILL.md, stripping CLI-only sections.
|
||||
|
||||
playwriter/src/resource.md is for more generic knowledge about playwright that the agent can use when necessary, for things like best practices for selecting locators on the page
|
||||
|
||||
website/public/resources/ is auto-generated by `playwriter/scripts/build-resources.ts` during `pnpm build`. DO NOT edit these files manually - edit the source files in `playwriter/src/` instead (e.g. `debugger-examples.ts`, `editor-examples.ts`, `styles-examples.ts`)
|
||||
website/public/resources/ and website/public/SKILL.md are auto-generated by `playwriter/scripts/build-resources.ts` during `pnpm build`. DO NOT edit these files manually - edit the source files instead (e.g. `debugger-examples.ts`, `editor-examples.ts`, `styles-examples.ts`, `skills/playwriter/SKILL.md`)
|
||||
|
||||
## CDP docs
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
"bippy": "^0.5.27",
|
||||
"devtools-protocol": "^0.0.1568893",
|
||||
"image-size": "^2.0.2",
|
||||
"marked": "^17.0.1",
|
||||
"mcp-extension": "workspace:*",
|
||||
"vite-node": "^5.0.0",
|
||||
"vitest": "^4.0.8"
|
||||
|
||||
@@ -4,12 +4,20 @@
|
||||
* These files are written to:
|
||||
* - playwriter/dist/ - for the MCP to read at runtime
|
||||
* - website/public/ - for hosting on playwriter.dev
|
||||
*
|
||||
* Source of truth:
|
||||
* - skills/playwriter/SKILL.md - manually edited, contains full docs including CLI usage
|
||||
*
|
||||
* Generated files:
|
||||
* - playwriter/src/prompt.md - MCP prompt (SKILL.md minus frontmatter and CLI sections)
|
||||
* - website/public/SKILL.md - full copy for playwriter.dev/SKILL.md
|
||||
*/
|
||||
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import dedent from 'string-dedent'
|
||||
import { Lexer, type Token, type Tokens } from 'marked'
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
const playwriterDir = path.join(__dirname, '..')
|
||||
@@ -118,84 +126,69 @@ function buildStylesApi() {
|
||||
writeToDestinations('styles-api.md', content)
|
||||
}
|
||||
|
||||
function buildSkill() {
|
||||
const promptContent = readFile('src/prompt.md')
|
||||
/**
|
||||
* Removes frontmatter and CLI-related sections from SKILL.md to create prompt.md for the MCP.
|
||||
*
|
||||
* Sections removed:
|
||||
* - Frontmatter (--- block at top)
|
||||
* - "## CLI Usage" section and all its subsections (### Execute code, ### Reset connection)
|
||||
*/
|
||||
function stripCliSectionsFromSkill(skillContent: string): string {
|
||||
// Remove frontmatter using regex
|
||||
const withoutFrontmatter = skillContent.replace(/^---\n[\s\S]*?\n---\n*/, '')
|
||||
|
||||
const frontmatter = dedent`
|
||||
---
|
||||
name: playwriter
|
||||
description: Control Chrome browser via Playwright code snippets. Automate web interactions, take screenshots, inspect accessibility trees, and debug web applications.
|
||||
---
|
||||
`
|
||||
// Parse markdown tokens
|
||||
const tokens = Lexer.lex(withoutFrontmatter)
|
||||
|
||||
const cliUsage = dedent`
|
||||
## CLI Usage
|
||||
|
||||
If \`playwriter\` command is not found, install globally or use npx/bunx:
|
||||
|
||||
\`\`\`bash
|
||||
npm install -g playwriter
|
||||
# or use without installing:
|
||||
npx playwriter -e "..." -s 1
|
||||
bunx playwriter -e "..." -s 1
|
||||
\`\`\`
|
||||
|
||||
### Execute code
|
||||
|
||||
\`\`\`bash
|
||||
playwriter -e "<code>" -s <session>
|
||||
\`\`\`
|
||||
|
||||
The \`-s\` flag specifies a session name (required). Use the same session to persist state across commands.
|
||||
|
||||
**Examples:**
|
||||
|
||||
\`\`\`bash
|
||||
# Navigate to a page
|
||||
playwriter -e "await page.goto('https://example.com')" -s 1
|
||||
|
||||
# Click a button
|
||||
playwriter -e "await page.click('button')" -s 1
|
||||
|
||||
# Get page title
|
||||
playwriter -e "console.log(await page.title())" -s 1
|
||||
|
||||
# Take a screenshot
|
||||
playwriter -e "await page.screenshot({ path: 'screenshot.png', scale: 'css' })" -s 1
|
||||
|
||||
# Get accessibility snapshot
|
||||
playwriter -e "console.log(await accessibilitySnapshot({ page }))" -s 1
|
||||
\`\`\`
|
||||
|
||||
### Reset connection
|
||||
|
||||
If the browser connection is stale or broken:
|
||||
|
||||
\`\`\`bash
|
||||
playwriter reset -s <session>
|
||||
\`\`\`
|
||||
|
||||
`
|
||||
// Filter out CLI Usage section and its subsections
|
||||
const filteredTokens: Token[] = []
|
||||
let skipUntilLevel: number | null = null
|
||||
|
||||
const content = frontmatter + '\n\n' + cliUsage + '\n' + promptContent
|
||||
for (const token of tokens) {
|
||||
if (token.type === 'heading') {
|
||||
const heading = token as Tokens.Heading
|
||||
// Check if we should start skipping (CLI Usage section)
|
||||
if (heading.depth === 2 && heading.text === 'CLI Usage') {
|
||||
skipUntilLevel = 2
|
||||
continue
|
||||
}
|
||||
// Check if we should stop skipping (next h2 section)
|
||||
if (skipUntilLevel !== null && heading.depth <= skipUntilLevel) {
|
||||
skipUntilLevel = null
|
||||
}
|
||||
}
|
||||
|
||||
if (skipUntilLevel === null) {
|
||||
filteredTokens.push(token)
|
||||
}
|
||||
}
|
||||
|
||||
// Write to repo root skills/ folder for add-skill discovery
|
||||
const skillsDir = path.join(playwriterDir, '..', 'skills', 'playwriter')
|
||||
ensureDir(skillsDir)
|
||||
fs.writeFileSync(path.join(skillsDir, 'SKILL.md'), content, 'utf-8')
|
||||
console.log('Generated skills/playwriter/SKILL.md')
|
||||
// Reconstruct markdown from tokens
|
||||
return filteredTokens.map((token) => { return token.raw }).join('').trim() + '\n'
|
||||
}
|
||||
|
||||
function buildPromptFromSkill() {
|
||||
// Read SKILL.md as source of truth
|
||||
const skillPath = path.join(playwriterDir, '..', 'skills', 'playwriter', 'SKILL.md')
|
||||
const skillContent = fs.readFileSync(skillPath, 'utf-8')
|
||||
|
||||
// Write to website/public/ for hosting at playwriter.dev/prompt.md
|
||||
// Generate prompt.md for MCP (without frontmatter and CLI sections)
|
||||
const promptContent = stripCliSectionsFromSkill(skillContent)
|
||||
const srcPromptPath = path.join(playwriterDir, 'src', 'prompt.md')
|
||||
fs.writeFileSync(srcPromptPath, promptContent, 'utf-8')
|
||||
console.log('Generated playwriter/src/prompt.md (from SKILL.md)')
|
||||
|
||||
// Copy full SKILL.md to website/public/ for hosting at playwriter.dev/SKILL.md
|
||||
const websitePublicRoot = path.join(playwriterDir, '..', 'website', 'public')
|
||||
ensureDir(websitePublicRoot)
|
||||
fs.writeFileSync(path.join(websitePublicRoot, 'prompt.md'), content, 'utf-8')
|
||||
console.log('Generated website/public/prompt.md')
|
||||
fs.writeFileSync(path.join(websitePublicRoot, 'SKILL.md'), skillContent, 'utf-8')
|
||||
console.log('Generated website/public/SKILL.md')
|
||||
}
|
||||
|
||||
// Run all builds
|
||||
buildDebuggerApi()
|
||||
buildEditorApi()
|
||||
buildStylesApi()
|
||||
buildSkill()
|
||||
buildPromptFromSkill()
|
||||
|
||||
console.log('Resource files generated successfully')
|
||||
|
||||
@@ -10,7 +10,7 @@ cli
|
||||
.command('', 'Start the MCP server or controls the browser with -e')
|
||||
.option('--host <host>', 'Remote relay server host to connect to (or use PLAYWRITER_HOST env var)')
|
||||
.option('--token <token>', 'Authentication token (or use PLAYWRITER_TOKEN env var)')
|
||||
.option('-e, --eval <code>', 'Execute JavaScript code and exit, read https://playwriter.dev/prompt.md for usage')
|
||||
.option('-e, --eval <code>', 'Execute JavaScript code and exit, read https://playwriter.dev/SKILL.md for usage')
|
||||
.option('--timeout <ms>', 'Execution timeout in milliseconds', { default: 5000 })
|
||||
.option('-s, --session <name>', 'Session name (required for -e)')
|
||||
.action(async (options: { host?: string; token?: string; eval?: string; timeout?: number; session?: string }) => {
|
||||
|
||||
Generated
+20
-2
@@ -131,6 +131,9 @@ importers:
|
||||
image-size:
|
||||
specifier: ^2.0.2
|
||||
version: 2.0.2
|
||||
marked:
|
||||
specifier: ^17.0.1
|
||||
version: 17.0.1
|
||||
mcp-extension:
|
||||
specifier: workspace:*
|
||||
version: link:../extension
|
||||
@@ -3762,6 +3765,11 @@ packages:
|
||||
magic-string@0.30.21:
|
||||
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
||||
|
||||
marked@17.0.1:
|
||||
resolution: {integrity: sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg==}
|
||||
engines: {node: '>= 20'}
|
||||
hasBin: true
|
||||
|
||||
marky@1.3.0:
|
||||
resolution: {integrity: sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==}
|
||||
|
||||
@@ -7612,6 +7620,14 @@ snapshots:
|
||||
optionalDependencies:
|
||||
vite: 7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)
|
||||
|
||||
'@vitest/mocker@4.0.8(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6))':
|
||||
dependencies:
|
||||
'@vitest/spy': 4.0.8
|
||||
estree-walker: 3.0.3
|
||||
magic-string: 0.30.21
|
||||
optionalDependencies:
|
||||
vite: 7.2.2(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)
|
||||
|
||||
'@vitest/mocker@4.0.8(vite@7.2.2(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6))':
|
||||
dependencies:
|
||||
'@vitest/spy': 4.0.8
|
||||
@@ -7663,7 +7679,7 @@ snapshots:
|
||||
sirv: 3.0.2
|
||||
tinyglobby: 0.2.15
|
||||
tinyrainbow: 3.0.3
|
||||
vitest: 4.0.8(@types/node@25.0.3)(@vitest/ui@4.0.8)(jiti@2.6.1)(jsdom@27.2.0(bufferutil@4.0.9))(lightningcss@1.30.2)(tsx@4.20.6)
|
||||
vitest: 4.0.8(@types/node@24.10.1)(@vitest/ui@4.0.8)(jiti@2.6.1)(jsdom@27.2.0(bufferutil@4.0.9))(lightningcss@1.30.2)(tsx@4.20.6)
|
||||
|
||||
'@vitest/utils@4.0.16':
|
||||
dependencies:
|
||||
@@ -9181,6 +9197,8 @@ snapshots:
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
|
||||
marked@17.0.1: {}
|
||||
|
||||
marky@1.3.0:
|
||||
optional: true
|
||||
|
||||
@@ -10491,7 +10509,7 @@ snapshots:
|
||||
vitest@4.0.8(@types/node@24.10.1)(@vitest/ui@4.0.8)(jiti@2.6.1)(jsdom@27.2.0(bufferutil@4.0.9))(lightningcss@1.30.2)(tsx@4.20.6):
|
||||
dependencies:
|
||||
'@vitest/expect': 4.0.8
|
||||
'@vitest/mocker': 4.0.8(vite@7.2.2(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6))
|
||||
'@vitest/mocker': 4.0.8(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6))
|
||||
'@vitest/pretty-format': 4.0.8
|
||||
'@vitest/runner': 4.0.8
|
||||
'@vitest/snapshot': 4.0.8
|
||||
|
||||
+1
-1
@@ -4,4 +4,4 @@ node_modules
|
||||
.env
|
||||
.react-router
|
||||
.DS_Store
|
||||
public/prompt.md
|
||||
public/SKILL.md
|
||||
|
||||
@@ -1,449 +0,0 @@
|
||||
---
|
||||
name: playwriter
|
||||
description: Control Chrome browser via Playwright code snippets. Automate web interactions, take screenshots, inspect accessibility trees, and debug web applications.
|
||||
---
|
||||
|
||||
## CLI Usage
|
||||
|
||||
If `playwriter` command is not found, install globally or use npx/bunx:
|
||||
|
||||
```bash
|
||||
npm install -g playwriter
|
||||
# or use without installing:
|
||||
npx playwriter -e "..." -s 1
|
||||
bunx playwriter -e "..." -s 1
|
||||
```
|
||||
|
||||
### Execute code
|
||||
|
||||
```bash
|
||||
playwriter -e "<code>" -s <session>
|
||||
```
|
||||
|
||||
The `-s` flag specifies a session name (required). Use the same session to persist state across commands.
|
||||
|
||||
**Examples:**
|
||||
|
||||
```bash
|
||||
# Navigate to a page
|
||||
playwriter -e "await page.goto('https://example.com')" -s 1
|
||||
|
||||
# Click a button
|
||||
playwriter -e "await page.click('button')" -s 1
|
||||
|
||||
# Get page title
|
||||
playwriter -e "console.log(await page.title())" -s 1
|
||||
|
||||
# Take a screenshot
|
||||
playwriter -e "await page.screenshot({ path: 'screenshot.png', scale: 'css' })" -s 1
|
||||
|
||||
# Get accessibility snapshot
|
||||
playwriter -e "console.log(await accessibilitySnapshot({ page }))" -s 1
|
||||
```
|
||||
|
||||
### Reset connection
|
||||
|
||||
If the browser connection is stale or broken:
|
||||
|
||||
```bash
|
||||
playwriter reset -s <session>
|
||||
```
|
||||
|
||||
# playwriter execute
|
||||
|
||||
Control user's Chrome browser via playwright code snippets. Prefer single-line code with semicolons between statements. If you get "extension is not connected" or "no browser tabs have Playwriter enabled" error, tell user to click the playwriter extension icon on the tab they want to control.
|
||||
|
||||
You can collaborate with the user - they can help with captchas, difficult elements, or reproducing bugs.
|
||||
|
||||
## context variables
|
||||
|
||||
- `state` - object persisted between calls, use to store data/pages (e.g., `state.myPage = await context.newPage()`)
|
||||
- `page` - default page the user activated, use this unless working with multiple pages
|
||||
- `context` - browser context, access all pages via `context.pages()`
|
||||
- `require` - load Node.js modules like fs
|
||||
- Node.js globals: `setTimeout`, `setInterval`, `fetch`, `URL`, `Buffer`, `crypto`, etc.
|
||||
|
||||
## rules
|
||||
|
||||
- **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
|
||||
- **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)
|
||||
- **Clean up listeners**: call `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
|
||||
- **Wait for load**: use `page.waitForLoadState('domcontentloaded')` not `page.waitForEvent('load')` - waitForEvent times out if already loaded
|
||||
- **Avoid timeouts**: prefer proper waits over `page.waitForTimeout()` - there are better ways to wait for elements
|
||||
|
||||
## checking page state
|
||||
|
||||
After any action (click, submit, navigate), verify what happened:
|
||||
|
||||
```js
|
||||
console.log('url:', page.url()); console.log(await accessibilitySnapshot({ page }).then(x => x.split('\n').slice(0, 30).join('\n')));
|
||||
```
|
||||
|
||||
For visually complex pages (grids, galleries, dashboards), use `screenshotWithAccessibilityLabels({ page })` instead to understand spatial layout.
|
||||
|
||||
If nothing changed, try `await page.waitForLoadState('networkidle', {timeout: 3000})` or you may have clicked the wrong element.
|
||||
|
||||
## accessibility snapshots
|
||||
|
||||
```js
|
||||
await accessibilitySnapshot({ page, search?, showDiffSinceLastCall? })
|
||||
```
|
||||
|
||||
- `search` - string/regex to filter results (returns first 10 matching lines)
|
||||
- `showDiffSinceLastCall` - returns diff since last snapshot (useful after actions)
|
||||
|
||||
For pagination, use `.split('\n').slice(offset, offset + limit).join('\n')`:
|
||||
```js
|
||||
console.log((await accessibilitySnapshot({ page })).split('\n').slice(0, 50).join('\n')); // first 50 lines
|
||||
console.log((await accessibilitySnapshot({ page })).split('\n').slice(50, 100).join('\n')); // next 50 lines
|
||||
```
|
||||
|
||||
Example output:
|
||||
|
||||
```md
|
||||
- banner [ref=e3]:
|
||||
- link "Home" [ref=e5] [cursor=pointer]:
|
||||
- /url: /
|
||||
- navigation [ref=e12]:
|
||||
- link "Docs" [ref=e13] [cursor=pointer]:
|
||||
- /url: /docs
|
||||
```
|
||||
|
||||
Use `aria-ref` to interact - **no quotes around the ref value**:
|
||||
|
||||
```js
|
||||
await page.locator('aria-ref=e13').click()
|
||||
```
|
||||
|
||||
Search for specific elements:
|
||||
|
||||
```js
|
||||
const snapshot = await accessibilitySnapshot({ page, search: /button|submit/i })
|
||||
```
|
||||
|
||||
## choosing between snapshot methods
|
||||
|
||||
Both `accessibilitySnapshot` and `screenshotWithAccessibilityLabels` use the same `aria-ref` system, so you can combine them effectively.
|
||||
|
||||
**Use `accessibilitySnapshot` when:**
|
||||
- Page has simple, semantic structure (articles, forms, lists)
|
||||
- You need to search for specific text or patterns
|
||||
- Token usage matters (text is smaller than images)
|
||||
- You need to process the output programmatically
|
||||
|
||||
**Use `screenshotWithAccessibilityLabels` when:**
|
||||
- Page has complex visual layout (grids, galleries, dashboards, maps)
|
||||
- Spatial position matters (e.g., "first image", "top-left button")
|
||||
- DOM order doesn't match visual order
|
||||
- You need to understand the visual hierarchy
|
||||
|
||||
**Combining both:** Use screenshot first to understand layout and identify target elements visually, then use `accessibilitySnapshot({ search: /pattern/ })` for efficient searching in subsequent calls.
|
||||
|
||||
## selector best practices
|
||||
|
||||
**For unknown websites**: use `accessibilitySnapshot()` with `aria-ref` - it shows what's actually interactive.
|
||||
|
||||
**For development** (when you have source code access), prefer stable selectors in this order:
|
||||
|
||||
1. **Best**: `[data-testid="submit"]` - explicit test attributes, never change accidentally
|
||||
2. **Good**: `getByRole('button', { name: 'Save' })` - accessible, semantic
|
||||
3. **Good**: `getByText('Sign in')`, `getByLabel('Email')` - readable, user-facing
|
||||
4. **OK**: `input[name="email"]`, `button[type="submit"]` - semantic HTML
|
||||
5. **Avoid**: `.btn-primary`, `#submit` - classes/IDs change frequently
|
||||
6. **Last resort**: `div.container > form > button` - fragile, breaks easily
|
||||
|
||||
Combine locators for precision:
|
||||
|
||||
```js
|
||||
page.locator('tr').filter({ hasText: 'John' }).locator('button').click()
|
||||
page.locator('button').nth(2).click()
|
||||
```
|
||||
|
||||
If a locator matches multiple elements, Playwright throws "strict mode violation". Use `.first()`, `.last()`, or `.nth(n)`:
|
||||
|
||||
```js
|
||||
await page.locator('button').first().click() // first match
|
||||
await page.locator('.item').last().click() // last match
|
||||
await page.locator('li').nth(3).click() // 4th item (0-indexed)
|
||||
```
|
||||
|
||||
## working with pages
|
||||
|
||||
Find a specific page:
|
||||
|
||||
```js
|
||||
const pages = context.pages().filter(x => x.url().includes('localhost'));
|
||||
if (pages.length !== 1) throw new Error(`Expected 1 page, found ${pages.length}`);
|
||||
state.targetPage = pages[0];
|
||||
```
|
||||
|
||||
Create new page:
|
||||
|
||||
```js
|
||||
state.newPage = await context.newPage();
|
||||
await state.newPage.goto('https://example.com');
|
||||
```
|
||||
|
||||
## navigation
|
||||
|
||||
**Use `domcontentloaded`** for `page.goto()`:
|
||||
|
||||
```js
|
||||
await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
|
||||
await waitForPageLoad({ page, timeout: 5000 });
|
||||
```
|
||||
|
||||
## common patterns
|
||||
|
||||
**Popups** - capture before triggering:
|
||||
|
||||
```js
|
||||
const [popup] = await Promise.all([page.waitForEvent('popup'), page.click('a[target=_blank]')]);
|
||||
await popup.waitForLoadState(); console.log('Popup URL:', popup.url());
|
||||
```
|
||||
|
||||
**Downloads** - capture and save:
|
||||
|
||||
```js
|
||||
const [download] = await Promise.all([page.waitForEvent('download'), page.click('button.download')]);
|
||||
await download.saveAs(`/tmp/${download.suggestedFilename()}`);
|
||||
```
|
||||
|
||||
**iFrames** - use frameLocator:
|
||||
|
||||
```js
|
||||
const frame = page.frameLocator('#my-iframe');
|
||||
await frame.locator('button').click();
|
||||
```
|
||||
|
||||
**Dialogs** - handle alerts/confirms/prompts:
|
||||
|
||||
```js
|
||||
page.on('dialog', async dialog => { console.log(dialog.message()); await dialog.accept(); });
|
||||
await page.click('button.trigger-alert');
|
||||
```
|
||||
|
||||
## utility functions
|
||||
|
||||
**getLatestLogs** - retrieve captured browser console logs (up to 5000 per page, cleared on navigation):
|
||||
|
||||
```js
|
||||
await getLatestLogs({ page?, count?, search? })
|
||||
// Examples:
|
||||
const errors = await getLatestLogs({ search: /error/i, count: 50 })
|
||||
const pageLogs = await getLatestLogs({ page })
|
||||
```
|
||||
|
||||
For custom log collection across runs, store in state: `state.logs = []; page.on('console', m => state.logs.push(m.text()))`
|
||||
|
||||
**getCleanHTML** - get cleaned HTML from a locator or page, with search and diffing:
|
||||
|
||||
```js
|
||||
await getCleanHTML({ locator, search?, showDiffSinceLastCall?, includeStyles? })
|
||||
// Examples:
|
||||
const html = await getCleanHTML({ locator: page.locator('body') })
|
||||
const html = await getCleanHTML({ locator: page, search: /button/i })
|
||||
const diff = await getCleanHTML({ locator: page, showDiffSinceLastCall: true })
|
||||
```
|
||||
|
||||
- `locator` - Playwright Locator or Page to get HTML from
|
||||
- `search` - string/regex to filter results (returns first 10 matching lines)
|
||||
- `showDiffSinceLastCall` - returns diff since last snapshot
|
||||
- `includeStyles` - keep style and class attributes (default: false)
|
||||
|
||||
Returns cleaned HTML with only essential attributes (aria-*, data-*, href, role, title, alt, etc.). Removes script, style, svg, head tags.
|
||||
|
||||
For pagination, use `.split('\n').slice(offset, offset + limit).join('\n')`:
|
||||
```js
|
||||
console.log((await getCleanHTML({ locator: page })).split('\n').slice(0, 50).join('\n')); // first 50 lines
|
||||
console.log((await getCleanHTML({ locator: page })).split('\n').slice(50, 100).join('\n')); // next 50 lines
|
||||
```
|
||||
|
||||
**waitForPageLoad** - smart load detection that ignores analytics/ads:
|
||||
|
||||
```js
|
||||
await waitForPageLoad({ page, timeout?, pollInterval?, minWait? })
|
||||
// Returns: { success, readyState, pendingRequests, waitTimeMs, timedOut }
|
||||
```
|
||||
|
||||
**getCDPSession** - send raw CDP commands:
|
||||
|
||||
```js
|
||||
const cdp = await getCDPSession({ page });
|
||||
const metrics = await cdp.send('Page.getLayoutMetrics');
|
||||
```
|
||||
|
||||
**getLocatorStringForElement** - get stable selector from ephemeral aria-ref:
|
||||
|
||||
```js
|
||||
const selector = await getLocatorStringForElement(page.locator('aria-ref=e14'));
|
||||
// => "getByRole('button', { name: 'Save' })"
|
||||
```
|
||||
|
||||
**getReactSource** - get React component source location (dev mode only):
|
||||
|
||||
```js
|
||||
const source = await getReactSource({ locator: page.locator('aria-ref=e5') });
|
||||
// => { 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 read `https://playwriter.dev/resources/styles-api.md` first.
|
||||
|
||||
```js
|
||||
const styles = await getStylesForLocator({ locator: page.locator('.btn'), cdp: await getCDPSession({ page }) });
|
||||
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 read `https://playwriter.dev/resources/debugger-api.md` first.
|
||||
|
||||
```js
|
||||
const cdp = await getCDPSession({ page }); const dbg = createDebugger({ cdp }); await dbg.enable();
|
||||
const scripts = await dbg.listScripts({ search: 'app' });
|
||||
await dbg.setBreakpoint({ file: scripts[0].url, line: 42 });
|
||||
// when paused: dbg.inspectLocalVariables(), dbg.stepOver(), dbg.resume()
|
||||
```
|
||||
|
||||
**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
|
||||
const cdp = await getCDPSession({ page }); const editor = createEditor({ cdp }); await editor.enable();
|
||||
const matches = await editor.grep({ regex: /console\.log/ });
|
||||
await editor.edit({ url: matches[0].url, oldString: 'DEBUG = false', newString: 'DEBUG = true' });
|
||||
```
|
||||
|
||||
**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.
|
||||
|
||||
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.
|
||||
|
||||
```js
|
||||
await screenshotWithAccessibilityLabels({ page });
|
||||
// Image and accessibility snapshot are automatically included in response
|
||||
// Use aria-ref from snapshot to interact with elements
|
||||
await page.locator('aria-ref=e5').click();
|
||||
|
||||
// Can take multiple screenshots in one execution
|
||||
await screenshotWithAccessibilityLabels({ page });
|
||||
await page.click('button');
|
||||
await screenshotWithAccessibilityLabels({ page });
|
||||
// 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.
|
||||
|
||||
## 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:
|
||||
|
||||
```js
|
||||
const el = await page.evaluateHandle(() => globalThis.playwriterPinnedElem1);
|
||||
await el.click();
|
||||
```
|
||||
|
||||
## taking screenshots
|
||||
|
||||
Always use `scale: 'css'` to avoid 2-4x larger images on high-DPI displays:
|
||||
|
||||
```js
|
||||
await page.screenshot({ path: 'shot.png', scale: 'css' });
|
||||
```
|
||||
|
||||
## page.evaluate
|
||||
|
||||
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
|
||||
const title = await page.evaluate(() => document.title);
|
||||
console.log('Title:', title);
|
||||
|
||||
const info = await page.evaluate(() => ({
|
||||
url: location.href,
|
||||
buttons: document.querySelectorAll('button').length,
|
||||
}));
|
||||
console.log(info);
|
||||
```
|
||||
|
||||
## loading files
|
||||
|
||||
Fill inputs with file content:
|
||||
|
||||
```js
|
||||
const fs = require('node:fs'); const content = fs.readFileSync('./data.txt', 'utf-8'); await page.locator('textarea').fill(content);
|
||||
```
|
||||
|
||||
## network interception
|
||||
|
||||
For scraping or reverse-engineering APIs, intercept network requests instead of scrolling DOM. Store in `state` to analyze across calls:
|
||||
|
||||
```js
|
||||
state.requests = []; state.responses = [];
|
||||
page.on('request', req => { if (req.url().includes('/api/')) state.requests.push({ url: req.url(), method: req.method(), headers: req.headers() }); });
|
||||
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 {} } });
|
||||
```
|
||||
|
||||
Then trigger actions (scroll, click, navigate) and analyze captured data:
|
||||
|
||||
```js
|
||||
console.log('Captured', state.responses.length, 'API calls');
|
||||
state.responses.forEach(r => console.log(r.status, r.url.slice(0, 80)));
|
||||
```
|
||||
|
||||
Inspect a specific response to understand schema:
|
||||
|
||||
```js
|
||||
const resp = state.responses.find(r => r.url.includes('users'));
|
||||
console.log(JSON.stringify(resp.body, null, 2).slice(0, 2000));
|
||||
```
|
||||
|
||||
Replay API directly (useful for pagination):
|
||||
|
||||
```js
|
||||
const { url, headers } = state.requests.find(r => r.url.includes('feed'));
|
||||
const data = await page.evaluate(async ({ url, headers }) => { const res = await fetch(url, { headers }); return res.json(); }, { url, headers });
|
||||
console.log(data);
|
||||
```
|
||||
|
||||
Clean up listeners when done: `page.removeAllListeners('request'); page.removeAllListeners('response');`
|
||||
|
||||
## reading response bodies
|
||||
|
||||
By default, playwriter disables CDP response body buffering to ensure SSE streaming works properly. If you need to read response bodies (e.g., for HAR recording or inspecting API responses), re-enable buffering first:
|
||||
|
||||
```js
|
||||
// Get CDP session and re-enable Network buffering
|
||||
const cdp = await getCDPSession({ page });
|
||||
await cdp.send('Network.disable');
|
||||
await cdp.send('Network.enable', {
|
||||
maxTotalBufferSize: 10000000, // 10MB total buffer
|
||||
maxResourceBufferSize: 5000000 // 5MB per resource
|
||||
});
|
||||
|
||||
// Now use Playwright's response API - it will work because buffering is enabled
|
||||
const [response] = await Promise.all([
|
||||
page.waitForResponse(resp => resp.url().includes('/api/data')),
|
||||
page.click('button.load-data')
|
||||
]);
|
||||
|
||||
const body = await response.text(); // or response.json(), response.body()
|
||||
console.log(body);
|
||||
```
|
||||
|
||||
**Note**: This may cause SSE/streaming responses to buffer. Only enable when you specifically need response bodies.
|
||||
|
||||
## capabilities
|
||||
|
||||
Examples of what playwriter can do:
|
||||
- Monitor console logs while user reproduces a bug
|
||||
- Intercept network requests to reverse-engineer APIs and build SDKs
|
||||
- Scrape data by replaying paginated API calls instead of scrolling DOM
|
||||
- Get accessibility snapshot to find elements, then automate interactions
|
||||
- Use visual screenshots to understand complex layouts like image grids, dashboards, or maps
|
||||
- Debug issues by collecting logs and controlling the page simultaneously
|
||||
- Handle popups, downloads, iframes, and dialog boxes
|
||||
|
||||
|
||||
## debugging playwriter issues
|
||||
|
||||
if some internal critical error happens you can read your own relay ws logs to understand the issue, it will show logs from extension, mcp and ws server together. then you can create a gh issue using `gh issue create -R remorses/playwriter --title title --body body`. ask for user confirmation before doing this.
|
||||
Reference in New Issue
Block a user