diff --git a/playwright b/playwright index dabe14a..7552ca1 160000 --- a/playwright +++ b/playwright @@ -1 +1 @@ -Subproject commit dabe14a38d904992e85e19eb98adf6d6225f8a3e +Subproject commit 7552ca14f15756a55a5ead5814e7b410e1273892 diff --git a/playwriter/src/clean-html.ts b/playwriter/src/clean-html.ts index 8bc922b..62f8e30 100644 --- a/playwriter/src/clean-html.ts +++ b/playwriter/src/clean-html.ts @@ -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 { diff --git a/playwriter/src/executor.ts b/playwriter/src/executor.ts index 21b8ecb..40ca75b 100644 --- a/playwriter/src/executor.ts +++ b/playwriter/src/executor.ts @@ -251,7 +251,7 @@ export class PlaywrightExecutor { private userState: Record = {} private browserLogs: Map = new Map() - private lastSnapshots: WeakMap = new WeakMap() + private lastSnapshots: WeakMap> = new WeakMap() private lastRefToLocator: WeakMap> = 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 diff --git a/playwriter/src/locator-selector.test.ts b/playwriter/src/locator-selector.test.ts new file mode 100644 index 0000000..47a1768 --- /dev/null +++ b/playwriter/src/locator-selector.test.ts @@ -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 = ` + + +
+ + + + + + About Us +

Page Title

+

Some paragraph text

+
+ Card content +
+
+ Another card +
+
+ Third card +
+ + + Logo +
Dashboard
+
+ +` + +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""`) + }) +})