Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions packages/injected/src/ariaSnapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -673,6 +673,8 @@ export function renderAriaTree(ariaSnapshot: AriaSnapshot, publicOptions: AriaTr

function convertToBestGuessRegex(text: string): string {
const dynamicContent = [
// 550e8400-e29b-41d4-a716-446655440000
{ regex: /\b[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\b/, replacement: '[0-9a-fA-F-]+' },
// 2mb
{ regex: /\b[\d,.]+[bkmBKM]+\b/, replacement: '[\\d,.]+[bkmBKM]+' },
// 2ms, 20s
Expand Down
22 changes: 16 additions & 6 deletions packages/injected/src/highlight.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,16 @@ export class Highlight {
const document = injectedScript.document;
this._isUnderTest = injectedScript.isUnderTest;
this._glassPaneElement = document.createElement('x-pw-glass');
this._glassPaneElement.style.position = 'fixed';
this._glassPaneElement.style.top = '0';
this._glassPaneElement.style.right = '0';
this._glassPaneElement.style.bottom = '0';
this._glassPaneElement.style.left = '0';
this._glassPaneElement.style.zIndex = '2147483647';
this._glassPaneElement.setAttribute('popover', 'manual');
this._glassPaneElement.style.inset = '0';
this._glassPaneElement.style.width = '100%';
this._glassPaneElement.style.height = '100%';
this._glassPaneElement.style.maxWidth = 'none';
this._glassPaneElement.style.maxHeight = 'none';
this._glassPaneElement.style.padding = '0';
this._glassPaneElement.style.margin = '0';
this._glassPaneElement.style.border = 'none';
this._glassPaneElement.style.overflow = 'visible';
this._glassPaneElement.style.pointerEvents = 'none';
this._glassPaneElement.style.display = 'flex';
this._glassPaneElement.style.backgroundColor = 'transparent';
Expand All @@ -88,6 +92,12 @@ export class Highlight {
return;
if (!this._injectedScript.document.documentElement.contains(this._glassPaneElement) || this._glassPaneElement.nextElementSibling)
this._injectedScript.document.documentElement.appendChild(this._glassPaneElement);
this._bringToFront();
}

private _bringToFront() {
this._glassPaneElement.hidePopover();
this._glassPaneElement.showPopover();
}

setLanguage(language: Language) {
Expand Down
2 changes: 1 addition & 1 deletion packages/playwright-core/src/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,4 @@ export { createPlaywright } from './playwright';

export type { DispatcherScope } from './dispatchers/dispatcher';
export type { Playwright } from './playwright';
export { installRootRedirect, openTraceInBrowser, openTraceViewerApp, runTraceViewerApp, startTraceViewerServer } from './trace/viewer/traceViewer';
export { installRootRedirect, openTraceInBrowser, openTraceViewerApp, startTraceViewerServer } from './trace/viewer/traceViewer';
40 changes: 26 additions & 14 deletions tests/config/traceViewerFixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@

import type { Fixtures, FrameLocator, Locator, Page, Browser, BrowserContext } from '@playwright/test';
import { step } from './baseTest';
import { runTraceViewerApp } from '../../packages/playwright-core/lib/server';
import path from 'path';
import { CommonFixtures, TestChildProcess } from './commonFixtures';

type BaseTestFixtures = {
type BaseTestFixtures = CommonFixtures & {
context: BrowserContext;
};

Expand All @@ -30,7 +31,7 @@ type BaseWorkerFixtures = {
};

export type TraceViewerFixtures = {
showTraceViewer: (trace: string | undefined, options?: {host?: string, port?: number}) => Promise<TraceViewerPage>;
showTraceViewer: (trace: string | undefined, options?: {host?: string, port?: number, stdin?: boolean}) => Promise<TraceViewerPage>;
runAndTrace: (body: () => Promise<void>, optsOverrides?: Parameters<BrowserContext['tracing']['start']>[0]) => Promise<TraceViewerPage>;
};

Expand All @@ -53,7 +54,7 @@ class TraceViewerPage {
themeSetting: Locator;
displayCanvasContentSetting: Locator;

constructor(public page: Page) {
constructor(public page: Page, public process: TestChildProcess) {
this.actionTitles = page.locator('.action-title');
this.actionsTree = page.getByTestId('actions-tree');
this.callLines = page.locator('.call-tab .call-line');
Expand Down Expand Up @@ -152,21 +153,32 @@ class TraceViewerPage {
}

export const traceViewerFixtures: Fixtures<TraceViewerFixtures, {}, BaseTestFixtures, BaseWorkerFixtures> = {
showTraceViewer: async ({ playwright, browserName, headless }, use, testInfo) => {
showTraceViewer: async ({ playwright, childProcess }, use) => {
const browsers: Browser[] = [];
const contextImpls: any[] = [];
await use(async (trace: string | undefined, { host, port } = {}) => {
const pageImpl = await runTraceViewerApp(trace, browserName, { headless, host, port });
const contextImpl = pageImpl.browserContext;
const browser = await playwright.chromium.connectOverCDP(contextImpl._browser.options.wsEndpoint);
await use(async (trace: string | undefined, { host, port, stdin } = {}) => {
const command = [
'node',
path.join(__dirname, '../../packages/playwright-core/cli.js'),
'show-trace',
'--port', '' + (port ?? '0'),
];
if (host)
command.push('--host', host);
if (stdin)
command.push('--stdin');
if (trace)
command.push(trace);
const cp = childProcess({ command });
await cp.waitForOutput('Listening on');
const browser = await playwright.chromium.launch();
browsers.push(browser);
contextImpls.push(contextImpl);
return new TraceViewerPage(browser.contexts()[0].pages()[0]);
const page = await browser.newPage();
const url = cp.output.match(/Listening on (http:\/\/[^\s]+)/)![1];
await page.goto(url);
return new TraceViewerPage(page, cp);
});
for (const browser of browsers)
await browser.close();
for (const contextImpl of contextImpls)
await contextImpl._browser.close({ reason: 'Trace viewer closed' });
},

runAndTrace: async ({ context, showTraceViewer }, use, testInfo) => {
Expand Down
133 changes: 133 additions & 0 deletions tests/library/inspector/cli-codegen-3.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -925,6 +925,139 @@ await page.GetByTestId("testid").HoverAsync();`);
await page.locator('x-pw-glass').click();
await expect(dialog).toBeHidden();
});

test('should record when top layer popover is open', { annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/39095' } }, async ({ openRecorder }) => {
const { page, recorder } = await openRecorder();

await recorder.setContentAndWait(`
<div
popover="manual"
id="mypopover"
style="inset: 0; width: 100%; height: 100%; max-width: none; max-height: none; margin: 0; padding: 20px;"
>
<button>Close</button>
</div>
<button popovertarget="mypopover">Show Popover</button>
`);

await page.getByRole('button', { name: 'Show Popover' }).click();
await expect(page.getByRole('button', { name: 'Close' })).toBeVisible();

await page.getByTitle('Assert text').click();
});

test('should record when top layer popover inside shadow DOM is open', { annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/39095' } }, async ({ openRecorder }) => {
const { page, recorder } = await openRecorder();

await recorder.setContentAndWait(`
<my-component></my-component>
<script>
class MyComponent extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
shadow.innerHTML = \`
<div
popover="manual"
id="mypopover"
style="inset: 0; width: 100%; height: 100%; max-width: none; max-height: none; margin: 0; padding: 20px;"
>
<button>Close</button>
</div>
<button popovertarget="mypopover">Show Popover</button>
\`;
}
}
customElements.define('my-component', MyComponent);
</script>
`);

await page.getByRole('button', { name: 'Show Popover' }).click();
await expect(page.getByRole('button', { name: 'Close' })).toBeVisible();

await page.getByTitle('Assert text').click();
});

test('should record when manual popover with fullscreen backdrop is open', { annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/39095' } }, async ({ openRecorder }) => {
const { page, recorder } = await openRecorder();

// Mirrors the dialog pattern from Angular Material: https://material.angular.dev/components/dialog/examples
// A manual popover with a fullscreen backdrop and a dialog content area.
// Difference: the dialog is not centered, because Playwright recorder has trouble clicking on backdrops with centered occlusion.
await recorder.setContentAndWait(`
<style>
.cdk-overlay-backdrop {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
pointer-events: auto;
background: rgba(0, 0, 0, 0.32);
opacity: 1;
}
.dialog-content {
position: fixed;
bottom: 20px;
right: 20px;
background: white;
padding: 20px;
z-index: 1001;
}
</style>
<div
popover="manual"
id="mypopover"
>
<div class="cdk-overlay-backdrop" id="backdrop"></div>
<div class="dialog-content">
<button id="closeBtn">Close Dialog</button>
</div>
</div>
<button id="openBtn">Open Dialog</button>
<script>
document.getElementById('openBtn').onclick = () => {
document.getElementById('mypopover').showPopover();
};
document.getElementById('backdrop').onclick = () => {
document.getElementById('mypopover').hidePopover();
};
document.getElementById('closeBtn').onclick = () => {
document.getElementById('mypopover').hidePopover();
};
</script>
`);

await page.getByRole('button', { name: 'Open Dialog' }).click();
await expect(page.getByRole('button', { name: 'Close Dialog' })).toBeVisible();

await page.mouse.click(10, 10);
await expect(page.getByRole('button', { name: 'Close Dialog' })).toBeHidden();
});

test('should record when fullscreen element is open', { annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/39095' } }, async ({ openRecorder }) => {
const { page, recorder } = await openRecorder();

await recorder.setContentAndWait(`
<div id="fullscreen-container">
<button id="closeBtn">Close Fullscreen</button>
</div>
<button id="openBtn">Go Fullscreen</button>
<script>
document.getElementById('openBtn').onclick = () => {
document.getElementById('fullscreen-container').requestFullscreen();
};
document.getElementById('closeBtn').onclick = () => {
document.exitFullscreen();
};
</script>
`);

await page.getByRole('button', { name: 'Go Fullscreen' }).click();
await expect(page.getByRole('button', { name: 'Close Fullscreen' })).toBeVisible();

await page.getByTitle('Assert text').click();
});
});

async function createFrameHierarchy(page: Page, recorder: Recorder, server: TestServer) {
Expand Down
13 changes: 13 additions & 0 deletions tests/library/inspector/cli-codegen-aria.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,19 @@ test.describe(() => {
recorder.text('C#')).toContain(`await Expect(page.GetByRole(AriaRole.Button)).ToMatchAriaSnapshotAsync("- button /Submit \\\\d+/");`);
});

test('should generate regex for uuid in aria snapshot', async ({ openRecorder }) => {
const { recorder } = await openRecorder();
await recorder.setContentAndWait(`<main><a href="/items/550e8400-e29b-41d4-a716-446655440000">Item 550e8400-e29b-41d4-a716-446655440000</a></main>`);

await recorder.page.click('x-pw-tool-item.snapshot');
await recorder.page.hover('a');
await recorder.trustedClick();

// url still contains full UUID, we can improve here.
await expect.poll(() =>
recorder.text('JavaScript')).toContain(`- link /Item [0-9a-fA-F-]+/:`);
});

test('should inspect aria snapshot', async ({ openRecorder }) => {
const { recorder } = await openRecorder();
await recorder.setContentAndWait(`<main><button>Submit</button></main>`);
Expand Down
15 changes: 6 additions & 9 deletions tests/library/trace-viewer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const test = playwrightTest.extend<TraceViewerFixtures>(traceViewerFixtures);

test.skip(({ trace }) => trace === 'on');
test.skip(({ mode }) => mode.startsWith('service'));
test.skip(process.env.PW_CLOCK === 'frozen');
test.slow();

let traceFile: string;
Expand Down Expand Up @@ -2249,15 +2250,11 @@ test('should capture iframe with srcdoc', async ({ page, server, runAndTrace })
await expect(frame.frameLocator('iframe').getByRole('button')).toHaveText('Hello iframe');
});

test('take trace paths via stdin', async ({ childProcess, page }) => {
const cliEntrypoint = path.join(__dirname, '../../packages/playwright-core/cli.js');
const cp = childProcess({ command: ['node', cliEntrypoint, 'show-trace', '--port', '0', '--stdin'] });
await cp.waitForOutput('Listening on');
const url = cp.output.match(/Listening on (http:\/\/[^\s]+)/)![1];
await page.goto(url);
await expect(page).toHaveTitle('Playwright Trace Viewer');
cp.write(traceFile);
await expect(page.locator('.action-title')).toContainText([
test('take trace paths via stdin', async ({ showTraceViewer }) => {
const traceViewer = await showTraceViewer(undefined, { stdin: true });
await expect(traceViewer.page).toHaveTitle('Playwright Trace Viewer');
traceViewer.process.write(traceFile);
await expect(traceViewer.actionTitles).toContainText([
/Create page/,
]);
});
Loading