From 69d2d2481568a63d8677ab6e932868401c646771 Mon Sep 17 00:00:00 2001 From: "Tommy D. Rossi" Date: Sun, 11 Jan 2026 22:24:40 +0100 Subject: [PATCH] release: playwriter@0.0.43 --- playwriter/CHANGELOG.md | 15 ++++ playwriter/package.json | 2 +- playwriter/src/clean-html.ts | 131 +++++++++++++++++++++++++++++++++++ playwriter/src/mcp.test.ts | 90 ++++++++++++++++++++++++ playwriter/src/mcp.ts | 25 +++---- playwriter/src/prompt.md | 34 ++++++++- 6 files changed, 278 insertions(+), 19 deletions(-) create mode 100644 playwriter/src/clean-html.ts diff --git a/playwriter/CHANGELOG.md b/playwriter/CHANGELOG.md index 4a61a5c..0fdbb4e 100644 --- a/playwriter/CHANGELOG.md +++ b/playwriter/CHANGELOG.md @@ -1,5 +1,20 @@ # Changelog +## 0.0.43 + +### Features + +- **`getCleanHTML` utility**: New function to get cleaned HTML from a locator or page + - Removes script, style, svg, head tags + - Keeps only essential attributes (aria-*, data-*, href, role, title, alt, etc.) + - Supports `search` option to filter results (returns first 10 matching lines) + - Supports `showDiffSinceLastCall` to see changes since last snapshot + - Supports `includeStyles` to optionally keep style/class attributes + +### Changes + +- **Simplified `accessibilitySnapshot` search**: Removed `contextLines` parameter, search now returns just matching lines instead of context around matches. Use `.split('\n').slice()` for pagination instead. + ## 0.0.42 ### Bug Fixes diff --git a/playwriter/package.json b/playwriter/package.json index baed79e..4f734c3 100644 --- a/playwriter/package.json +++ b/playwriter/package.json @@ -1,7 +1,7 @@ { "name": "playwriter", "description": "", - "version": "0.0.42", + "version": "0.0.43", "type": "module", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/playwriter/src/clean-html.ts b/playwriter/src/clean-html.ts new file mode 100644 index 0000000..baddbc7 --- /dev/null +++ b/playwriter/src/clean-html.ts @@ -0,0 +1,131 @@ +import { Page, Locator } from 'playwright-core' +import { createPatch } from 'diff' +import { formatHtmlForPrompt } from './htmlrewrite.js' + +export interface GetCleanHTMLOptions { + locator: Locator | Page + search?: string | RegExp + showDiffSinceLastCall?: boolean + includeStyles?: boolean + maxAttrLen?: number + maxContentLen?: number +} + +// Store last HTML snapshots per locator/page for diffing +const lastHtmlSnapshots: WeakMap> = new WeakMap() + +function isPage(obj: any): obj is Page { + return obj && typeof obj.content === 'function' && typeof obj.goto === 'function' +} + +function isRegExp(value: any): value is RegExp { + return ( + typeof value === 'object' && value !== null && typeof value.test === 'function' && typeof value.exec === 'function' + ) +} + +function getSnapshotKey(locator: Locator | Page): string { + if (isPage(locator)) { + return '__page__' + } + // For locators, use a string representation + return (locator as any)._selector || '__locator__' +} + +export async function getCleanHTML(options: GetCleanHTMLOptions): Promise { + const { + locator, + search, + showDiffSinceLastCall = false, + includeStyles = false, + maxAttrLen = 200, + maxContentLen = 500, + } = options + + // Get raw HTML + let rawHtml: string + let page: Page + + if (isPage(locator)) { + page = locator + rawHtml = await locator.content() + } else { + page = locator.page() + rawHtml = await locator.innerHTML() + } + + // Clean the HTML using formatHtmlForPrompt + const cleanedHtml = await formatHtmlForPrompt({ + html: rawHtml, + keepStyles: includeStyles, + maxAttrLen, + maxContentLen, + }) + + // Sanitize to remove unpaired surrogates that break JSON encoding + let htmlStr = cleanedHtml.toWellFormed?.() ?? cleanedHtml + + // Handle diffing + if (showDiffSinceLastCall) { + let pageSnapshots = lastHtmlSnapshots.get(page) + if (!pageSnapshots) { + pageSnapshots = new Map() + lastHtmlSnapshots.set(page, pageSnapshots) + } + + const snapshotKey = getSnapshotKey(locator) + const previousSnapshot = pageSnapshots.get(snapshotKey) + + if (!previousSnapshot) { + pageSnapshots.set(snapshotKey, htmlStr) + return 'No previous snapshot available. This is the first call for this locator. Full snapshot stored for next diff.' + } + + const patch = createPatch('html', previousSnapshot, htmlStr, 'previous', 'current', { + context: 3, + }) + + pageSnapshots.set(snapshotKey, htmlStr) + + if (patch.split('\n').length <= 4) { + return 'No changes detected since last snapshot' + } + return patch + } + + // Store snapshot for future diffs + let pageSnapshots = lastHtmlSnapshots.get(page) + if (!pageSnapshots) { + pageSnapshots = new Map() + lastHtmlSnapshots.set(page, pageSnapshots) + } + pageSnapshots.set(getSnapshotKey(locator), htmlStr) + + // Handle search + if (search) { + const lines = htmlStr.split('\n') + const matches: string[] = [] + + for (const line of lines) { + let isMatch = false + if (isRegExp(search)) { + isMatch = search.test(line) + } else { + isMatch = line.includes(search) + } + + if (isMatch) { + matches.push(line) + if (matches.length >= 10) break + } + } + + if (matches.length === 0) { + return 'No matches found' + } + + return matches.join('\n') + } + + return htmlStr +} diff --git a/playwriter/src/mcp.test.ts b/playwriter/src/mcp.test.ts index dd3b2d0..3f4ae3f 100644 --- a/playwriter/src/mcp.test.ts +++ b/playwriter/src/mcp.test.ts @@ -2436,6 +2436,96 @@ describe('MCP Server Tests', () => { await page.close() }, 60000) + it('should get clean HTML with getCleanHTML', async () => { + const browserContext = getBrowserContext() + const serviceWorker = await getExtensionServiceWorker(browserContext) + + const page = await browserContext.newPage() + await page.setContent(` + + + + + + +
+

Hello World

+ + About + +
+ + + `) + await page.bringToFront() + + await serviceWorker.evaluate(async () => { + await globalThis.toggleExtensionForActiveTab() + }) + await new Promise(r => setTimeout(r, 400)) + + // Test basic getCleanHTML + const result = await client.callTool({ + name: 'execute', + arguments: { + code: js` + let testPage; + for (const p of context.pages()) { + const html = await p.content(); + if (html.includes('Hello World')) { testPage = p; break; } + } + if (!testPage) throw new Error('Test page not found'); + const html = await getCleanHTML({ locator: testPage.locator('body') }); + return html; + `, + timeout: 15000, + }, + }) + + expect(result.isError).toBeFalsy() + const text = (result.content as any)[0]?.text || '' + + // Inline snapshot of cleaned HTML + expect(text).toMatchInlineSnapshot(` + "Return value: +
+

Hello World

+ + About + +
" + `) + + // Should NOT contain script/style tags (they're removed) + expect(text).not.toContain(' Promise + getCleanHTML: (options: GetCleanHTMLOptions) => Promise getLocatorStringForElement: (element: any) => Promise resetPlaywright: () => Promise<{ page: Page; context: BrowserContext }> getLatestLogs: (options?: { page?: Page; count?: number; search?: string | RegExp }) => Promise @@ -713,10 +714,9 @@ server.tool( const accessibilitySnapshot = async (options: { page: Page search?: string | RegExp - contextLines?: number showDiffSinceLastCall?: boolean }) => { - const { page: targetPage, search, contextLines = 10, showDiffSinceLastCall = false } = options + const { page: targetPage, search, showDiffSinceLastCall = false } = options if ((targetPage as any)._snapshotForAI) { const snapshot = await (targetPage as any)._snapshotForAI() // Sanitize to remove unpaired surrogates that break JSON encoding for Claude API @@ -732,7 +732,7 @@ server.tool( } const patch = createPatch('snapshot', previousSnapshot, snapshotStr, 'previous', 'current', { - context: contextLines, + context: 3, }) if (patch.split('\n').length <= 4) { return 'No changes detected since last snapshot' @@ -747,10 +747,9 @@ server.tool( } const lines = snapshotStr.split('\n') - const matches: { line: string; index: number }[] = [] + const matches: string[] = [] - for (let i = 0; i < lines.length; i++) { - const line = lines[i] + for (const line of lines) { let isMatch = false if (isRegExp(search)) { isMatch = search.test(line) @@ -759,7 +758,7 @@ server.tool( } if (isMatch) { - matches.push({ line, index: i }) + matches.push(line) if (matches.length >= 10) break } } @@ -768,13 +767,7 @@ server.tool( return 'No matches found' } - return matches - .map((m) => { - const start = Math.max(0, m.index - contextLines) - const end = Math.min(lines.length, m.index + contextLines + 1) - return lines.slice(start, end).join('\n') - }) - .join('\n\n---\n\n') + return matches.join('\n') } throw new Error('accessibilitySnapshot is not available on this page') } @@ -878,6 +871,7 @@ server.tool( state: userState, console: customConsole, accessibilitySnapshot, + getCleanHTML, getLocatorStringForElement, getLatestLogs, clearAllLogs, @@ -898,6 +892,7 @@ server.tool( state: userState, console: customConsole, accessibilitySnapshot, + getCleanHTML, getLocatorStringForElement, getLatestLogs, clearAllLogs, diff --git a/playwriter/src/prompt.md b/playwriter/src/prompt.md index 7fad98c..60b6980 100644 --- a/playwriter/src/prompt.md +++ b/playwriter/src/prompt.md @@ -38,13 +38,18 @@ If nothing changed, try `await page.waitForLoadState('networkidle', {timeout: 30 ## accessibility snapshots ```js -await accessibilitySnapshot({ page, search?, contextLines?, showDiffSinceLastCall? }) +await accessibilitySnapshot({ page, search?, showDiffSinceLastCall? }) ``` -- `search` - string/regex to filter results (returns first 10 matches with context) -- `contextLines` - lines of context around matches (default: 10) +- `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 @@ -174,6 +179,29 @@ 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