From 9ad8428be1a5d30a82cdb94ae95ca04febd98cf3 Mon Sep 17 00:00:00 2001 From: "Tommy D. Rossi" Date: Wed, 31 Dec 2025 10:55:03 +0100 Subject: [PATCH] Update prompt.md --- playwriter/src/prompt.md | 352 +++++++++++++++++---------------------- 1 file changed, 149 insertions(+), 203 deletions(-) diff --git a/playwriter/src/prompt.md b/playwriter/src/prompt.md index fbfc746..5b5b2ed 100644 --- a/playwriter/src/prompt.md +++ b/playwriter/src/prompt.md @@ -1,243 +1,189 @@ -playwriter execute is a tool to control the user browser instance via extension also called playwriter MCP. +# playwriter execute -if you get an error Extension not running tell user to install and enable the playwriter extension first, clicking on the extension icon on the tab the user wants to control +Control user's Chrome browser via playwright code snippets. Prefer single-line code with semicolons between statements. If you get "Extension not running" error, tell user to click the playwriter extension icon on the tab they want to control. -execute tool let you run playwright js code snippets to control user Chrome window, these js code snippets are preferred to be in a single line to make them more readable in agent interface. separating statements with semicolons +You can collaborate with the user - they can help with captchas, difficult elements, or reproducing bugs. -you can extract data from your script using `console.log`. But remember that console.log in `page.evaluate` callbacks are run in the browser, so you will not see them. Instead log the evaluate result +## context variables -to keep some variables between calls, you can use `state` global object. constants and variables are reset between runs. Instead use code like `state.newPage = await browser.newPage();` to reuse the created page in later calls - -you MUST use multiple execute tool calls for running complex logic. this ensures -- you have clear understanding of intermediate state between interactions -- you can split finding an element from interacting with it. making it simpler to understand what is the issue when an action is not successful - -it will control an existing user Chrome window. The js code will be run in a sandbox with some variables in context: - -- state: an object shared between runs that you can mutate to persist functions and objects. for example `state.requests = []` to monitor network requests between runs -- context: the playwright browser context. you can do things like `await context.pages()` to see user connected pages -- page, the first page the user opened and made it accessible to this MCP. do things like `page.url()` to see current url. assume the user wants you to use this page for your playwright code -- require: node's require function to load modules -- all standard Node.js globals: setTimeout, setInterval, clearTimeout, clearInterval, URL, URLSearchParams, fetch, Buffer, TextEncoder, TextDecoder, crypto, AbortController, AbortSignal, structuredClone - -the chrome window can have more than one page. you can see other pages with `context.pages().find((p) => p.url().includes('localhost'))`. you can also open and close pages: `state.newPage = await context.newPage()`. store the page in state so that you can reuse it later - -you can control the browser in collaboration with the user. the user can help you get unstuck from captchas or difficult to find elements or reproducing a bug - -## capabilities - -examples of things playwriter MCP can do: -- monitor logs for a page while the user reproduces a but to let you understand what is causing a bug -- monitor logs while also controlling the page, then read collected logs and debug an issue -- monitor xhr network requests while scrolling an infinite scroll page to extract data from a website -- get accessibility snapshot to see clickable elements on the page, then click or interact with them to automate a task like ordering pizza - -## finding the page to execute code in - -if you plan to control a specific page for an url you can store it in `state` so you can reuse it later on: - -```js -const pages = context.pages().filter(x => x.url().includes('localhost')); -if (pages.length === 0) throw new Error('No page with URL matching localhost found'); -if (pages.length > 1) throw new Error('Multiple pages with URL matching localhost found'); -state.localhostPage = pages[0]; -// do things with the page -await state.localhostPage.bringToFront(); -``` - -IMPORTANT! never call bringToFront unless specifically asked by the user. It is very bothering to the user otherwise! you don't need to call bringToFront before being able to interact. you can very well interact without calling it first. on any page in the background you have access to. +- `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 modules (e.g., `require('node:fs')`) +- Node.js globals: `setTimeout`, `setInterval`, `fetch`, `URL`, `Buffer`, `crypto`, etc. ## rules -- only call `page.close()` if the user asks you so or if you previously created this page yourself with `newPage`. do not close user created pages unless asked -- try to never sleep or run `page.waitForTimeout` unless you have to. there are better ways to wait for an element -- use `page.waitForLoadState('load')` instead of `page.waitForEvent('load')`. `waitForEvent` waits for a future event and will timeout if the page is already loaded, while `waitForLoadState` resolves immediately if already in that state -- never close browser or context. NEVER call `browser.close()` -- NEVER use `page.context().newCDPSession()` or `browser.newCDPSession()` - these do not work through the playwriter relay. If you need to send raw CDP commands, use the `getCDPSession` utility function instead. +- **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()` - the latter doesn't work through playwriter relay +- **Wait for load**: use `page.waitForLoadState('load')` 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 -## always check the current page state after an action +After any action (click, submit, navigate), verify what happened: -after you click a button or submit a form you ALWAYS have to then check what is the current state of the page. you cannot assume what happened after doing an action. instead run the following code to know what happened after the action: +```js +console.log('url:', page.url()); console.log(await accessibilitySnapshot({ page }).then(x => x.split('\n').slice(0, 30).join('\n'))); +``` -`console.log('url:', page.url()); console.log(await accessibilitySnapshot({ page }).then(x => x.split('\n').slice(0, 30).join('\n')));` +If nothing changed, try `await page.waitForLoadState('networkidle', {timeout: 3000})` or you may have clicked the wrong element. -if nothing happened you may need to wait before the action completes, using something like `page.waitForNavigation({timeout: 3000})` or `await page.waitForLoadState('networkidle', {timeout: 3000})` +## accessibility snapshots -if nothing happens it could also means that you clicked the wrong button or link. try to search for other appropriate elements to click or submit +```js +await accessibilitySnapshot({ page, search?, contextLines?, showDiffSinceLastCall? }) +``` +- `search` - string/regex to filter results (returns first 10 matches with context) +- `contextLines` - lines of context around matches (default: 10) +- `showDiffSinceLastCall` - returns diff since last snapshot (useful after actions) -## event listeners +Example output: -always detach event listener you create at the end of a message using `page.removeAllListeners()` or similar so that you never leak them in future messages +```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 }) +``` + +## 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'); +``` ## utility functions -you have access to some functions in addition to playwright methods: - -- `async accessibilitySnapshot({ page, search, contextLines, showDiffSinceLastCall })`: gets a human readable snapshot of clickable elements on the page. useful to see the overall structure of the page and what elements you can interact with. - - `page`: the page object to snapshot - - `search`: (optional) a string or regex to filter the snapshot. If provided, returns the first 10 matches with surrounding context - - `contextLines`: (optional) number of lines of context to show around each match (default: 10). Also controls context lines in diff output. - - `showDiffSinceLastCall`: (optional) if true, returns a unified diff patch showing only changes since the last non-diff snapshot call for this page. Disables search when enabled. Useful to see what changed after an action. Note: diff calls do not update the stored snapshot, so you can call diff multiple times and always compare against the same baseline. -- `getLatestLogs({ page, count, search })`: retrieves browser console logs. The system automatically captures and stores up to 5000 logs per page. Logs are cleared when a page reloads or navigates. - - `page`: (optional) filter logs by a specific page instance. Only returns logs from that page - - `count`: (optional) limit number of logs to return. If not specified, returns all available logs - - `search`: (optional) string or regex to filter logs. Only returns logs that match -- `waitForPageLoad({ page, timeout, pollInterval, minWait })`: smart network-aware page load detection. Playwright's `networkidle` waits for ALL requests to finish, which often times out on sites with analytics/ads. This function ignores those and returns when meaningful content is loaded. - - `page`: the page object to wait on - - `timeout`: (optional) max wait time in ms (default: 30000) - - `pollInterval`: (optional) how often to check in ms (default: 100) - - `minWait`: (optional) minimum wait before checking in ms (default: 500) - - Returns: `{ success, readyState, pendingRequests, waitTimeMs, timedOut }` - - Filters out: ad networks (doubleclick, googlesyndication), analytics (google-analytics, mixpanel, segment), social (facebook.net, twitter), support widgets (intercom, zendesk), and slow fonts/images -- `getCDPSession({ page })`: creates a CDP session to send raw Chrome DevTools Protocol commands. Use this instead of `page.context().newCDPSession()` which does not work through the playwriter relay. Sessions are cached per page. - - `page`: the page object to create the session for - - Returns: `{ send(method, params?), on(event, callback), off(event, callback) }` - - Example: `const cdp = await getCDPSession({ page }); const metrics = await cdp.send('Page.getLayoutMetrics');` -- `createDebugger({ cdp })`: creates a Debugger instance for setting breakpoints, stepping, and inspecting variables. ALWAYS read `https://playwriter.dev/resources/debugger-api.md` before using. -- `createEditor({ cdp })`: creates an Editor instance for viewing and live-editing page scripts and CSS stylesheets. ALWAYS read `https://playwriter.dev/resources/editor-api.md` before using -- `getStylesForLocator({ locator })`: gets the CSS styles applied to an element, similar to browser DevTools "Styles" panel. ALWAYS read `https://playwriter.dev/resources/styles-api.md` before using. -- `getReactSource({ locator })`: gets the React component source location (file, line, column) for an element. - - `locator`: a Playwright Locator or ElementHandle for the element to inspect - - Returns: `{ fileName, lineNumber, columnNumber, componentName }` or `null` if not found - - **Important**: Only works on **local dev servers** (localhost with Vite, Next.js, CRA in dev mode). Production builds strip source info. - -example: - -```md -- generic [active] [ref=e1]: - - generic [ref=e2]: - - banner [ref=e3]: - - generic [ref=e5]: - - link "shadcn/ui" [ref=e6] [cursor=pointer]: - - /url: / - - img - - generic [ref=e11] [cursor=pointer]: shadcn/ui - - navigation [ref=e12]: - - link "Docs" [ref=e13] [cursor=pointer]: - - /url: /docs/installation - - link "Components" [ref=e14] [cursor=pointer]: - - /url: /docs/components -``` - -Then you can use `page.locator(`aria-ref=${ref}`)` to get an element with a specific `ref` and interact with it. - -`const componentsLink = page.locator('aria-ref=e14').click()` - -IMPORTANT: notice that we do not add any quotes in `aria-ref`! it MUST be called without quotes - -## getting a stable selector for an element (getLocatorStringForElement) - -The `aria-ref` values from accessibility snapshots are ephemeral - they change on page reload and when components remount. Use `getLocatorStringForElement(element)` to get a stable Playwright locator string that you can reuse programmatically. - -This is useful for: -- Getting a selector you can store and reuse across page reloads -- Finding similar elements in a list (modify the selector pattern) -- Debugging which selector Playwright would use for an element +**getLatestLogs** - retrieve captured browser console logs (up to 5000 per page, cleared on navigation): ```js -const loc = page.locator('aria-ref=e14'); -const selector = await getLocatorStringForElement(loc); -console.log(selector); -// => "getByRole('button', { name: 'Save' })" - -// use the selector programmatically with eval: -const stableLocator = page.getByRole('button', { name: 'Save' }) -await stableLocator.click(); +await getLatestLogs({ page?, count?, search? }) +// Examples: +const errors = await getLatestLogs({ search: /error/i, count: 50 }) +const pageLogs = await getLatestLogs({ page }) ``` -## pinned elements (user right-click to pin) +For custom log collection across runs, store in state: `state.logs = []; page.on('console', m => state.logs.push(m.text()))` -Users can right-click an element and select "Pin to Playwriter" to store it in `globalThis.playwriterPinnedElem1` (increments for each pin). The variable name is copied to clipboard. +**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' }); +``` + +## 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(); -const selector = await getLocatorStringForElement(el); ``` -## finding specific elements with snapshot +## page.evaluate -You can use `search` to find specific elements in the snapshot without reading the whole page structure. This is useful for finding forms, textareas, or specific text. - -Example: find a textarea or form using case-insensitive regex: +Use `console.log()` to output values to the tool result. For `page.evaluate()`, return values and log outside (console.log inside evaluate runs in browser, not visible): ```js -const snapshot = await accessibilitySnapshot({ page, search: /textarea|form/i }) -console.log(snapshot) +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); ``` -Example: find elements containing "Login": +## loading files + +Fill inputs with file content: ```js -const snapshot = await accessibilitySnapshot({ page, search: "Login" }) -console.log(snapshot) +const fs = require('node:fs'); const content = fs.readFileSync('./README.md', 'utf-8'); await page.locator('textarea').fill(content); ``` -## getting outputs of code execution +## capabilities -You can use `console.log` to print values you want to see in the tool call result. For seeing logs across runs you can store strings in `state.logs` and then print them later, filtering and paginating them too. - -## using page.evaluate - -you can execute client side JavaScript code using `page.evaluate()` - -When executing code with `page.evaluate()`, return values directly from the evaluate function. Use `console.log()` outside of evaluate to display results: - -```js -// Get data from the page by returning it -const title = await page.evaluate(() => document.title) -console.log('Page title:', title) - -// Return multiple values as an object -const pageInfo = await page.evaluate(() => ({ - url: window.location.href, - buttonCount: document.querySelectorAll('button').length, - readyState: document.readyState, -})) -console.log(pageInfo) -``` - -## using getLatestLogs to read browser console logs - -The system automatically captures and stores up to 5000 browser console logs per page. Logs are automatically cleared when a page reloads or navigates to a new URL. You can retrieve logs using the `getLatestLogs` function: - -```js -// Get all browser console logs from all pages (up to 5000 per page) -const allLogs = await getLatestLogs() -console.log(allLogs) - -// Get last 50 browser error logs -const errorLogs = await getLatestLogs({ count: 50, search: /\[error\]/ }) -console.log(errorLogs) - -// Get all browser logs from the current page only -const pageLogs = await getLatestLogs({ page }) -console.log(pageLogs) - -// Find browser logs containing specific text -const authLogs = await getLatestLogs({ search: 'authentication failed' }) -console.log(authLogs) - -// Example output format: -// [log] User clicked login button -// [error] Failed to fetch /api/auth -// [warn] Session expiring soon -``` - -## loading file content into inputs - -you can use the `require` function to read files and fill inputs with their content: - -```js -const fs = require('node:fs'); const content = fs.readFileSync('/path/to/file.txt', 'utf-8'); await page.locator('textarea').fill(content) -``` - -for example, to fill a textarea with the content of a markdown file: - -```js -const fs = require('node:fs'); const readme = fs.readFileSync('./README.md', 'utf-8'); await page.locator('#description').fill(readme); console.log('Filled textarea with README content') -``` - -the chrome extension has id jfeammnjpkecdekppnclgkkffahnhfhe in case user wants to create a review (if he's happy and compliments you) redirect them to the chrome web store url +Examples of what playwriter can do: +- Monitor console logs while user reproduces a bug +- Monitor XHR requests while scrolling infinite scroll to extract data +- Get accessibility snapshot to find elements, then automate interactions +- Debug issues by collecting logs and controlling the page simultaneously