release: playwriter@0.0.43

This commit is contained in:
Tommy D. Rossi
2026-01-11 22:24:40 +01:00
parent 77a40d3c99
commit 69d2d24815
6 changed files with 278 additions and 19 deletions
+15
View File
@@ -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
+1 -1
View File
@@ -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",
+131
View File
@@ -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<Page, Map<string, string>> = 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<string> {
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
}
+90
View File
@@ -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(`
<html>
<head>
<style>.hidden { display: none; }</style>
<script>console.log('test')</script>
</head>
<body>
<div class="container" data-testid="main">
<h1>Hello World</h1>
<button id="btn" aria-label="Click me">Submit</button>
<a href="/about" title="About page">About</a>
<input type="text" placeholder="Enter name" />
</div>
</body>
</html>
`)
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:
<div data-testid="main">
<h1>Hello World</h1>
<button aria-label="Click me">Submit</button>
<a href="/about" title="About page">About</a>
<input type="text" placeholder="Enter name">
</div>"
`)
// Should NOT contain script/style tags (they're removed)
expect(text).not.toContain('<script')
expect(text).not.toContain('<style')
expect(text).not.toContain('console.log')
// Test search functionality
const searchResult = 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, search: /button/i });
return html;
`,
timeout: 15000,
},
})
expect(searchResult.isError).toBeFalsy()
const searchText = (searchResult.content as any)[0]?.text || ''
expect(searchText).toContain('button')
await page.close()
}, 60000)
})
+10 -15
View File
@@ -22,6 +22,7 @@ import { getStylesForLocator, formatStylesAsText, type StylesResult } from './st
import { getReactSource, type ReactSourceLocation } from './react-source.js'
import { ScopedFS } from './scoped-fs.js'
import { screenshotWithAccessibilityLabels, type ScreenshotResult } from './aria-snapshot.js'
import { getCleanHTML, type GetCleanHTMLOptions } from './clean-html.js'
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
@@ -72,9 +73,9 @@ interface VMContext {
accessibilitySnapshot: (options: {
page: Page
search?: string | RegExp
contextLines?: number
showDiffSinceLastCall?: boolean
}) => Promise<string>
getCleanHTML: (options: GetCleanHTMLOptions) => Promise<string>
getLocatorStringForElement: (element: any) => Promise<string>
resetPlaywright: () => Promise<{ page: Page; context: BrowserContext }>
getLatestLogs: (options?: { page?: Page; count?: number; search?: string | RegExp }) => Promise<string[]>
@@ -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,
+31 -3
View File
@@ -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