fix: isolate snapshot diff cache by locator scope + use Locator.selector()
The snapshot diff cache in executor.ts used only the Page object as key, so
calling snapshot({ page }) then snapshot({ page, locator }) would diff a
locator-scoped subtree against the previous full-page snapshot — producing
misleading diffs full of 'removed' lines that are simply outside the scope.
Fix: change lastSnapshots from WeakMap<Page, string> to a two-level
WeakMap<Page, Map<string, string>> keyed by locator.selector() (or '__page__'
for full-page). This matches the pattern already used by getCleanHTML in
clean-html.ts.
Also replaced (locator as any)._selector with the new typed
locator.selector() API from @xmorse/playwright-core 1.59.5 in both
executor.ts and clean-html.ts — zero `as any` casts.
Added locator-selector.test.ts with 28 inline snapshots covering CSS
selectors, getByRole, getByText, getByPlaceholder, getByLabel, getByAltText,
getByTestId, chained/filtered/described locators, and and/or combinators.
This commit is contained in:
+1
-1
Submodule playwright updated: dabe14a38d...7552ca14f1
@@ -28,8 +28,7 @@ function getSnapshotKey(locator: Locator | Page): string {
|
||||
if (isPage(locator)) {
|
||||
return '__page__'
|
||||
}
|
||||
// For locators, use a string representation
|
||||
return (locator as any)._selector || '__locator__'
|
||||
return locator.selector()
|
||||
}
|
||||
|
||||
export async function getCleanHTML(options: GetCleanHTMLOptions): Promise<string> {
|
||||
|
||||
@@ -251,7 +251,7 @@ export class PlaywrightExecutor {
|
||||
|
||||
private userState: Record<string, any> = {}
|
||||
private browserLogs: Map<string, string[]> = new Map()
|
||||
private lastSnapshots: WeakMap<Page, string> = new WeakMap()
|
||||
private lastSnapshots: WeakMap<Page, Map<string, string>> = new WeakMap()
|
||||
private lastRefToLocator: WeakMap<Page, Map<string, string>> = new WeakMap()
|
||||
private warningEvents: WarningEvent[] = []
|
||||
private nextWarningEventId = 0
|
||||
@@ -788,9 +788,17 @@ export class PlaywrightExecutor {
|
||||
this.lastRefToLocator.set(resolvedPage, refToLocator)
|
||||
|
||||
const shouldCacheSnapshot = !frame
|
||||
const previousSnapshot = shouldCacheSnapshot ? this.lastSnapshots.get(resolvedPage) : undefined
|
||||
// Cache keyed by locator selector so full-page and locator-scoped snapshots
|
||||
// don't pollute each other's diff baselines
|
||||
const snapshotKey = locator ? locator.selector() : '__page__'
|
||||
let pageSnapshots = this.lastSnapshots.get(resolvedPage)
|
||||
if (!pageSnapshots) {
|
||||
pageSnapshots = new Map()
|
||||
this.lastSnapshots.set(resolvedPage, pageSnapshots)
|
||||
}
|
||||
const previousSnapshot = shouldCacheSnapshot ? pageSnapshots.get(snapshotKey) : undefined
|
||||
if (shouldCacheSnapshot) {
|
||||
this.lastSnapshots.set(resolvedPage, snapshotStr)
|
||||
pageSnapshots.set(snapshotKey, snapshotStr)
|
||||
}
|
||||
|
||||
// Diff defaults off when search is provided, but agent can explicitly enable both
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
// Tests for Locator.selector() — verifies the raw selector strings returned
|
||||
// by various locator creation methods (getByRole, locator, getByText, etc.)
|
||||
|
||||
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
|
||||
import { chromium, type Page, type Browser } from '@xmorse/playwright-core'
|
||||
|
||||
const HTML = `<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<div id="main">
|
||||
<button>Submit</button>
|
||||
<button>Cancel</button>
|
||||
<button class="primary">Save</button>
|
||||
<input type="text" placeholder="Enter name" />
|
||||
<input type="email" placeholder="Enter email" />
|
||||
<a href="/about">About Us</a>
|
||||
<h1>Page Title</h1>
|
||||
<p>Some paragraph text</p>
|
||||
<div class="card">
|
||||
<span>Card content</span>
|
||||
</div>
|
||||
<div class="card">
|
||||
<span>Another card</span>
|
||||
</div>
|
||||
<div class="card">
|
||||
<span>Third card</span>
|
||||
</div>
|
||||
<label for="age">Age</label>
|
||||
<input id="age" type="number" />
|
||||
<img alt="Logo" src="/logo.png" />
|
||||
<div data-testid="dashboard">Dashboard</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
describe('Locator.selector()', () => {
|
||||
let browser: Browser
|
||||
let page: Page
|
||||
|
||||
beforeAll(async () => {
|
||||
browser = await chromium.launch({ headless: true })
|
||||
const context = await browser.newContext()
|
||||
page = await context.newPage()
|
||||
await page.setContent(HTML)
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await browser.close()
|
||||
})
|
||||
|
||||
it('CSS selectors', () => {
|
||||
expect(page.locator('#main').selector()).toMatchInlineSnapshot(`"#main"`)
|
||||
expect(page.locator('.card').selector()).toMatchInlineSnapshot(`".card"`)
|
||||
expect(page.locator('button').selector()).toMatchInlineSnapshot(`"button"`)
|
||||
expect(page.locator('div.card > span').selector()).toMatchInlineSnapshot(`"div.card > span"`)
|
||||
expect(page.locator('#main .card:first-child').selector()).toMatchInlineSnapshot(`"#main .card:first-child"`)
|
||||
})
|
||||
|
||||
it('getByRole', () => {
|
||||
expect(page.getByRole('button').selector()).toMatchInlineSnapshot(`"internal:role=button"`)
|
||||
expect(page.getByRole('button', { name: 'Submit' }).selector()).toMatchInlineSnapshot(`"internal:role=button[name="Submit"i]"`)
|
||||
expect(page.getByRole('link').selector()).toMatchInlineSnapshot(`"internal:role=link"`)
|
||||
expect(page.getByRole('heading').selector()).toMatchInlineSnapshot(`"internal:role=heading"`)
|
||||
expect(page.getByRole('textbox').selector()).toMatchInlineSnapshot(`"internal:role=textbox"`)
|
||||
})
|
||||
|
||||
it('getByText', () => {
|
||||
expect(page.getByText('Submit').selector()).toMatchInlineSnapshot(`"internal:text="Submit"i"`)
|
||||
expect(page.getByText('Some paragraph').selector()).toMatchInlineSnapshot(`"internal:text="Some paragraph"i"`)
|
||||
expect(page.getByText(/card/i).selector()).toMatchInlineSnapshot(`"internal:text=/card/i"`)
|
||||
})
|
||||
|
||||
it('getByPlaceholder', () => {
|
||||
expect(page.getByPlaceholder('Enter name').selector()).toMatchInlineSnapshot(`"internal:attr=[placeholder="Enter name"i]"`)
|
||||
expect(page.getByPlaceholder('Enter email').selector()).toMatchInlineSnapshot(`"internal:attr=[placeholder="Enter email"i]"`)
|
||||
})
|
||||
|
||||
it('getByLabel', () => {
|
||||
expect(page.getByLabel('Age').selector()).toMatchInlineSnapshot(`"internal:label="Age"i"`)
|
||||
})
|
||||
|
||||
it('getByAltText', () => {
|
||||
expect(page.getByAltText('Logo').selector()).toMatchInlineSnapshot(`"internal:attr=[alt="Logo"i]"`)
|
||||
})
|
||||
|
||||
it('getByTestId', () => {
|
||||
expect(page.getByTestId('dashboard').selector()).toMatchInlineSnapshot(`"internal:testid=[data-testid="dashboard"s]"`)
|
||||
})
|
||||
|
||||
it('chained locators', () => {
|
||||
expect(page.locator('#main').locator('.card').selector()).toMatchInlineSnapshot(`"#main >> .card"`)
|
||||
expect(page.locator('.card').first().selector()).toMatchInlineSnapshot(`".card >> nth=0"`)
|
||||
expect(page.locator('.card').last().selector()).toMatchInlineSnapshot(`".card >> nth=-1"`)
|
||||
expect(page.locator('.card').nth(1).selector()).toMatchInlineSnapshot(`".card >> nth=1"`)
|
||||
})
|
||||
|
||||
it('filtered locators', () => {
|
||||
expect(page.locator('button').filter({ hasText: 'Save' }).selector()).toMatchInlineSnapshot(`"button >> internal:has-text="Save"i"`)
|
||||
expect(page.locator('div').filter({ has: page.locator('span') }).selector()).toMatchInlineSnapshot(`"div >> internal:has="span""`)
|
||||
expect(page.locator('button').filter({ hasNotText: 'Cancel' }).selector()).toMatchInlineSnapshot(`"button >> internal:has-not-text="Cancel"i"`)
|
||||
})
|
||||
|
||||
it('described locators', () => {
|
||||
expect(page.locator('button').describe('main action button').selector()).toMatchInlineSnapshot(`"button >> internal:describe="main action button""`)
|
||||
})
|
||||
|
||||
it('combined with and/or', () => {
|
||||
expect(
|
||||
page.locator('button').and(page.locator('.primary')).selector(),
|
||||
).toMatchInlineSnapshot(`"button >> internal:and=".primary""`)
|
||||
expect(
|
||||
page.locator('button').or(page.locator('a')).selector(),
|
||||
).toMatchInlineSnapshot(`"button >> internal:or="a""`)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user