Skip to content
Closed
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/src/api/class-locatorassertions.md
Original file line number Diff line number Diff line change
Expand Up @@ -2373,6 +2373,8 @@ assertThat(page.locator("body")).matchesAriaSnapshot("""
### option: LocatorAssertions.toMatchAriaSnapshot.timeout = %%-csharp-java-python-assertions-timeout-%%
* since: v1.49

### option: LocatorAssertions.toMatchAriaSnapshot.update = %%-assertions-aria-snapshot-update-%%

## async method: LocatorAssertions.toMatchAriaSnapshot#2
* since: v1.50
* langs: js
Expand All @@ -2396,6 +2398,8 @@ await expect(page.locator('body')).toMatchAriaSnapshot({ name: 'body.aria.yml' }
Name of the snapshot to store in the snapshot folder corresponding to this test.
Generates sequential names if not specified.

### option: LocatorAssertions.toMatchAriaSnapshot#2.update = %%-assertions-aria-snapshot-update-%%

### option: LocatorAssertions.toMatchAriaSnapshot#2.timeout = %%-js-assertions-timeout-%%
* since: v1.50

Expand Down
7 changes: 7 additions & 0 deletions docs/src/api/params.md
Original file line number Diff line number Diff line change
Expand Up @@ -1898,3 +1898,10 @@ In this config:
1. Since `snapshotPathTemplate` resolves to relative path, it will be resolved relative to `configDir`.
1. Forward slashes `"/"` can be used as path separators on any platform.

## assertions-aria-snapshot-update
* since: v1.57
- `update` <["raw"]|["relaxed"]>

Defines how snapshots are written when updating. Defaults to `'relaxed'`.
* `'relaxed'` - Writes an ARIA snapshot that uses flexible patterns (for example, regular expressions for numbers) to reduce flakiness from minor changes.
* `'raw'` - Writes the exact ARIA snapshot without transformations. This is strict and will fail if values change.
12 changes: 9 additions & 3 deletions packages/playwright/src/matchers/toMatchAriaSnapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,14 @@ type ToMatchAriaSnapshotExpected = {
name?: string;
path?: string;
timeout?: number;
update?: 'raw' | 'relaxed';
} | string;

export async function toMatchAriaSnapshot(
this: ExpectMatcherState,
locator: LocatorEx,
expectedParam?: ToMatchAriaSnapshotExpected,
options: { timeout?: number } = {},
options: { timeout?: number, update?: 'raw' | 'relaxed'; } = {},
): Promise<MatcherResult<string | RegExp, string>> {
const matcherName = 'toMatchAriaSnapshot';

Expand All @@ -57,6 +58,8 @@ export async function toMatchAriaSnapshot(
let expected: string;
let timeout: number;
let expectedPath: string | undefined;
let updateMode = options.update ?? 'relaxed';

if (isString(expectedParam)) {
expected = expectedParam;
timeout = options.timeout ?? this.timeout;
Expand All @@ -69,6 +72,7 @@ export async function toMatchAriaSnapshot(
expectedPath = legacyPath;
expected = await fs.promises.readFile(expectedPath, 'utf8').catch(() => '');
timeout = expectedParam?.timeout ?? this.timeout;
updateMode = expectedParam?.update ?? updateMode;
}

const generateMissingBaseline = updateSnapshots === 'missing' && !expected;
Expand Down Expand Up @@ -116,13 +120,15 @@ export async function toMatchAriaSnapshot(
if (errorMessage)
return { pass: this.isNot, message, name: 'toMatchAriaSnapshot', expected };

const newSnapshot = updateMode === 'raw' ? typedReceived.raw : typedReceived.regex;

if (!this.isNot) {
if ((updateSnapshots === 'all') ||
(updateSnapshots === 'changed' && pass === this.isNot) ||
generateMissingBaseline) {
if (expectedPath) {
await fs.promises.mkdir(path.dirname(expectedPath), { recursive: true });
await fs.promises.writeFile(expectedPath, typedReceived.regex, 'utf8');
await fs.promises.writeFile(expectedPath, newSnapshot, 'utf8');
const relativePath = path.relative(process.cwd(), expectedPath);
if (updateSnapshots === 'missing') {
const message = `A snapshot doesn't exist at ${relativePath}, writing actual.`;
Expand All @@ -135,7 +141,7 @@ export async function toMatchAriaSnapshot(
}
return { pass: true, message: () => '', name: 'toMatchAriaSnapshot' };
} else {
const suggestedRebaseline = `\`\n${escapeTemplateString(indent(typedReceived.regex, '{indent} '))}\n{indent}\``;
const suggestedRebaseline = `\`\n${escapeTemplateString(indent(newSnapshot, '{indent} '))}\n{indent}\``;
if (updateSnapshots === 'missing') {
const message = 'A snapshot is not provided, generating new baseline.';
testInfo._hasNonRetriableError = true;
Expand Down
18 changes: 18 additions & 0 deletions packages/playwright/types/test.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9518,6 +9518,15 @@ interface LocatorAssertions {
* Time to retry the assertion for in milliseconds. Defaults to `timeout` in `TestConfig.expect`.
*/
timeout?: number;

/**
* Defines how snapshots are written when updating. Defaults to `'relaxed'`.
* - `'relaxed'` - Writes an ARIA snapshot that uses flexible patterns (for example, regular expressions for
* numbers) to reduce flakiness from minor changes.
* - `'raw'` - Writes the exact ARIA snapshot without transformations. This is strict and will fail if values
* change.
*/
update?: "raw"|"relaxed";
}): Promise<void>;

/**
Expand Down Expand Up @@ -9546,6 +9555,15 @@ interface LocatorAssertions {
* Time to retry the assertion for in milliseconds. Defaults to `timeout` in `TestConfig.expect`.
*/
timeout?: number;

/**
* Defines how snapshots are written when updating. Defaults to `'relaxed'`.
* - `'relaxed'` - Writes an ARIA snapshot that uses flexible patterns (for example, regular expressions for
* numbers) to reduce flakiness from minor changes.
* - `'raw'` - Writes the exact ARIA snapshot without transformations. This is strict and will fail if values
* change.
*/
update?: "raw"|"relaxed";
}): Promise<void>;

/**
Expand Down
75 changes: 75 additions & 0 deletions tests/page/to-match-aria-snapshot.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -878,3 +878,78 @@ test('treat bad regex as a string', async ({ page }) => {
expect(stripAnsi(error.message)).toContain('- - /url: /[a/');
expect(stripAnsi(error.message)).toContain('+ - /url: /foo');
});

test('should match with update param', async ({ page }) => {
{
await page.setContent(`<h1>Issues 12</h1>`);
await expect(page.locator('body')).toMatchAriaSnapshot(`
- heading "Issues 12"
`, { update: 'raw' });
}
{
await page.setContent(`<h1>Issues 1/2</h1>`);
await expect(page.locator('body')).toMatchAriaSnapshot(`
- heading "Issues 1/2"
`, { update: 'raw' });
}
{
await page.setContent(`<h1>Issues 1[</h1>`);
await expect(page.locator('body')).toMatchAriaSnapshot(`
- heading "Issues 1["
`, { update: 'raw' });
}
{
await page.setContent(`<h1>Issues 1]]2</h1>`);
await expect(page.locator('body')).toMatchAriaSnapshot(`
- heading "Issues 1]]2"
`, { update: 'raw' });
}
{
await page.setContent(`<h1>Issues 12</h1>`);
await expect(page.locator('body')).toMatchAriaSnapshot(`
- heading ${/Issues \d+/}
`, { update: 'relaxed' });
}
{
await page.setContent(`<h1>Issues 1/2</h1>`);
await expect(page.locator('body')).toMatchAriaSnapshot(`
- heading ${/Issues 1[/]2/}
`);
}
{
await page.setContent(`<h1>Issues 1[</h1>`);
await expect(page.locator('body')).toMatchAriaSnapshot(`
- heading ${/Issues 1\[/}
`);
}
{
await page.setContent(`<h1>Issues 1]]2</h1>`);
await expect(page.locator('body')).toMatchAriaSnapshot(`
- heading ${/Issues 1[\]]]2/}
`);
}
{
await page.setContent(`<h1>Issues 12</h1>`);
await expect(page.locator('body')).toMatchAriaSnapshot(`
- heading "Issues 12"
`);
}
{
await page.setContent(`<h1>Issues 1/2</h1>`);
await expect(page.locator('body')).toMatchAriaSnapshot(`
- heading "Issues 1/2"
`);
}
{
await page.setContent(`<h1>Issues 1[</h1>`);
await expect(page.locator('body')).toMatchAriaSnapshot(`
- heading "Issues 1["
`);
}
{
await page.setContent(`<h1>Issues 1]]2</h1>`);
await expect(page.locator('body')).toMatchAriaSnapshot(`
- heading "Issues 1]]2"
`);
}
});
45 changes: 45 additions & 0 deletions tests/playwright-test/aria-snapshot-file.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,3 +251,48 @@ test('should respect config.expect.toMatchAriaSnapshot.pathTemplate', async ({ r
expect(result.exitCode).toBe(0);
expect(result.passed).toBe(1);
});

test('should save aria snapshot in raw mode', async ({ runInlineTest }) => {
const result = await runInlineTest({
'a.spec.ts': `
import { test, expect } from '@playwright/test';
import fs from 'fs';
test('test', async ({ page }) => {
await page.setContent(\`<h1>hello world 123</h1>\`);
await expect(page.locator('body')).toMatchAriaSnapshot({ name: 'test.aria.yml', update: 'raw' });
expect(fs.readFileSync('a.spec.ts-snapshots/test.aria.yml', { encoding: 'utf8' })).toEqual('- heading "hello world 123" [level=1]');
});
`
}, { 'update-snapshots': 'all' });
expect(result.exitCode).toBe(0);
});

test('should save aria snapshot in regex mode', async ({ runInlineTest }) => {
const result = await runInlineTest({
'a.spec.ts': `
import { test, expect } from '@playwright/test';
import fs from 'fs';
test('test', async ({ page }) => {
await page.setContent(\`<h1>hello world 123</h1>\`);
await expect(page.locator('body')).toMatchAriaSnapshot({ name: 'test.aria.yml', update: 'relaxed' });
expect(fs.readFileSync('a.spec.ts-snapshots/test.aria.yml', { encoding: 'utf8' })).toEqual('- heading /hello world \\\\d+/ [level=1]');
});
`
}, { 'update-snapshots': 'all' });
expect(result.exitCode).toBe(0);
});

test('should save aria snapshot in regex mode without exact argument', async ({ runInlineTest }) => {
const result = await runInlineTest({
'a.spec.ts': `
import { test, expect } from '@playwright/test';
import fs from 'fs';
test('test', async ({ page }) => {
await page.setContent(\`<h1>hello world 123</h1>\`);
await expect(page.locator('body')).toMatchAriaSnapshot({ name: 'test.aria.yml' });
expect(fs.readFileSync('a.spec.ts-snapshots/test.aria.yml', { encoding: 'utf8' })).toEqual('- heading /hello world \\\\d+/ [level=1]');
});
`
}, { 'update-snapshots': 'all' });
expect(result.exitCode).toBe(0);
});
80 changes: 80 additions & 0 deletions tests/playwright-test/update-aria-snapshot.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -706,4 +706,84 @@ test.describe('update-source-method', () => {
const result2 = await runInlineTest({});
expect(result2.exitCode).toBe(0);
});

test('should update when option raw is specified', async ({ runInlineTest }, testInfo) => {
const result = await runInlineTest({
'.git/marker': '',
'a.spec.ts': `
import { test, expect } from '@playwright/test';
test('test', async ({ page }) => {
await page.setContent(\`<input value="hello world 123">\`);
await expect(page.locator('body')).toMatchAriaSnapshot('',
{
timeout: 2500,
update: 'raw'
});
});
`
});

expect(result.exitCode).toBe(1);
const patchPath = testInfo.outputPath('test-results/rebaselines.patch');
const data = fs.readFileSync(patchPath, 'utf-8');
expect(trimPatch(data)).toBe(`diff --git a/a.spec.ts b/a.spec.ts
--- a/a.spec.ts
+++ b/a.spec.ts
@@ -2,7 +2,9 @@
import { test, expect } from '@playwright/test';
test('test', async ({ page }) => {
await page.setContent(\`<input value="hello world 123">\`);
- await expect(page.locator('body')).toMatchAriaSnapshot('',
+ await expect(page.locator('body')).toMatchAriaSnapshot(\`
+ - textbox: hello world 123
+ \`,
{
timeout: 2500,
update: 'raw'
`);

execSync(`patch -p1 < ${patchPath}`, { cwd: testInfo.outputPath() });
const result2 = await runInlineTest({});
expect(result2.exitCode).toBe(0);
});

test('should update when option relaxed is specified', async ({ runInlineTest }, testInfo) => {
const result = await runInlineTest({
'.git/marker': '',
'a.spec.ts': `
import { test, expect } from '@playwright/test';
test('test', async ({ page }) => {
await page.setContent(\`<input value="hello world 123">\`);
await expect(page.locator('body')).toMatchAriaSnapshot('',
{
timeout: 2500,
update: 'relaxed'
});
});
`
});

expect(result.exitCode).toBe(1);
const patchPath = testInfo.outputPath('test-results/rebaselines.patch');
const data = fs.readFileSync(patchPath, 'utf-8');
expect(trimPatch(data)).toBe(`diff --git a/a.spec.ts b/a.spec.ts
--- a/a.spec.ts
+++ b/a.spec.ts
@@ -2,7 +2,9 @@
import { test, expect } from '@playwright/test';
test('test', async ({ page }) => {
await page.setContent(\`<input value="hello world 123">\`);
- await expect(page.locator('body')).toMatchAriaSnapshot('',
+ await expect(page.locator('body')).toMatchAriaSnapshot(\`
+ - textbox: /hello world \\\\d+/
+ \`,
{
timeout: 2500,
update: 'relaxed'
`);

execSync(`patch -p1 < ${patchPath}`, { cwd: testInfo.outputPath() });
const result2 = await runInlineTest({});
expect(result2.exitCode).toBe(0);
});
});