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
12 changes: 5 additions & 7 deletions .github/workflows/publish_release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,8 @@ on:
push:
branches:
- release-*
tags:
# TODO: revert this to "published release" once github.ref is set there.
# See https://github.com/actions/runner/issues/2788 as well.
- 'v1.61.1'
release:
types: [published]

jobs:
publish-npm-and-driver:
Expand Down Expand Up @@ -49,7 +47,7 @@ jobs:
node utils/build/update_canary_version.js --beta --commit-timestamp
utils/publish_all_packages.sh --beta
- name: "publish release to NPM"
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
if: github.event_name == 'release' && github.event.action == 'published'
run: utils/publish_all_packages.sh --release

- name: Azure Login
Expand All @@ -60,7 +58,7 @@ jobs:
subscription-id: ${{ secrets.AZURE_PW_CDN_SUBSCRIPTION_ID }}
- name: build & publish driver
env:
AZ_UPLOAD_FOLDER: ${{ startsWith(github.ref, 'refs/tags/v') && 'driver' || 'driver/next' }}
AZ_UPLOAD_FOLDER: ${{ github.event_name == 'release' && 'driver' || 'driver/next' }}
run: |
utils/build/build-playwright-driver.sh
utils/build/upload-playwright-driver.sh
Expand Down Expand Up @@ -91,7 +89,7 @@ jobs:
env:
GH_SERVICE_ACCOUNT_TOKEN: ${{ steps.app-token.outputs.token }}
- name: Deploy Stable
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
if: github.event_name == 'release' && github.event.action == 'published'
run: bash utils/build/deploy-trace-viewer.sh --stable
env:
GH_SERVICE_ACCOUNT_TOKEN: ${{ steps.app-token.outputs.token }}
Expand Down
3 changes: 2 additions & 1 deletion docs/src/api/class-coverage.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,10 @@ will have `__playwright_evaluation_script__` as their URL.

### option: Coverage.startJSCoverage.resetOnNavigation
* since: v1.11
* discouraged: Settings this to `false` may still reset on navigations.
- `resetOnNavigation` <[boolean]>

Whether to reset coverage on every navigation. Defaults to `true`.
Whether to reset coverage on every navigation. Defaults to `true`. Note that passing `false` does not guarantee that coverage persists through navigations, due to browser architecture limitations.

### option: Coverage.startJSCoverage.reportAnonymousScripts
* since: v1.11
Expand Down
9 changes: 9 additions & 0 deletions packages/html-reporter/src/tabbedPane.css
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,20 @@
align-items: center;
justify-content: center;
user-select: none;
background: none;
border: none;
border-bottom: 2px solid transparent;
color: inherit;
font: inherit;
outline: none;
height: 100%;
}

.tabbed-pane-tab-element:focus-visible {
outline: 1px solid var(--color-accent-fg);
outline-offset: -1px;
}

.tabbed-pane-tab-label {
max-width: 250px;
white-space: pre;
Expand Down
4 changes: 2 additions & 2 deletions packages/html-reporter/src/tabbedPane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,14 @@ export const TabbedPane: React.FunctionComponent<{
<div className='hbox' style={{ flex: 'none' }}>
<div className='tabbed-pane-tab-strip' role='tablist'>{
tabs.map(tab => (
<div className={clsx('tabbed-pane-tab-element', selectedTab === tab.id && 'selected')}
<button className={clsx('tabbed-pane-tab-element', selectedTab === tab.id && 'selected')}
onClick={() => setSelectedTab(tab.id)}
id={`${idPrefix}-${tab.id}`}
key={tab.id}
role='tab'
aria-selected={selectedTab === tab.id}>
<div className='tabbed-pane-tab-label'>{tab.title}</div>
</div>
</button>
))
}</div>
</div>
Expand Down
4 changes: 2 additions & 2 deletions packages/injected/src/ariaSnapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
*/

import * as aria from '@isomorphic/ariaSnapshot';
import { escapeRegExp, longestCommonSubstring, normalizeWhiteSpace } from '@isomorphic/stringUtils';
import { escapeRegExp, longestCommonSubstring, normalizeWhiteSpace, truncateDataUrl } from '@isomorphic/stringUtils';
import { yamlEscapeKeyIfNeeded, yamlEscapeValueIfNeeded } from '@isomorphic/yaml';

import { computeBox, getElementComputedStyle, isElementVisible } from './domUtils';
Expand Down Expand Up @@ -184,7 +184,7 @@ export function generateAriaTree(rootElement: Element, publicOptions: AriaTreeOp

if (ariaNode.role === 'link' && element.hasAttribute('href')) {
const href = element.getAttribute('href')!;
ariaNode.props['url'] = href;
ariaNode.props['url'] = truncateDataUrl(href);
}

if (ariaNode.role === 'textbox' && element.hasAttribute('placeholder') && element.getAttribute('placeholder') !== ariaNode.name) {
Expand Down
2 changes: 1 addition & 1 deletion packages/isomorphic/selectorParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,7 @@ export function parseAttributeSelector(selector: string, allowUnquotedStrings: b
syntaxError('parsing regular expression');
let flags = '';
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions
while (!EOL && next().match(/[dgimsuy]/))
while (!EOL && next().match(/[dgimsuvy]/))
flags += eat1();
try {
return new RegExp(source, flags);
Expand Down
11 changes: 11 additions & 0 deletions packages/isomorphic/stringUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,17 @@ export function trimStringWithEllipsis(input: string, cap: number): string {
return trimString(input, cap, '\u2026');
}

export function truncateDataUrl(url: string): string {
// Data URLs can carry megabytes of base64 payload, which is never useful in
// human/AI-facing output. Keep the media type prefix for context, drop the data.
if (!url.startsWith('data:'))
return url;
const comma = url.indexOf(',');
if (comma === -1)
return url;
return url.slice(0, comma + 1) + '\u2026';
}

export function escapeRegExp(s: string) {
// From https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
Expand Down
5 changes: 4 additions & 1 deletion packages/playwright-client/types/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19024,7 +19024,10 @@ export interface Coverage {
reportAnonymousScripts?: boolean;

/**
* Whether to reset coverage on every navigation. Defaults to `true`.
* **NOTE** Settings this to `false` may still reset on navigations.
*
* Whether to reset coverage on every navigation. Defaults to `true`. Note that passing `false` does not guarantee
* that coverage persists through navigations, due to browser architecture limitations.
*/
resetOnNavigation?: boolean;
}): Promise<void>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,6 @@ const disabledFeatures = [
'Translate',
// See https://issues.chromium.org/u/1/issues/435410220
'AutoDeElevate',
// See https://github.com/microsoft/playwright/issues/37714
'RenderDocument',
// Prevents downloading optimization hints on startup.
'OptimizationHints',
// Disables forced sign-in in Edge.
Expand Down
8 changes: 7 additions & 1 deletion packages/playwright-core/src/server/frames.ts
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,7 @@ export class FrameManager {
const frame = this._frames.get(frameId)!;
this.removeChildFramesRecursively(frame);
this._clearWebSockets(frame);
const previousUrl = frame._url;
frame._url = url;
frame._name = name;

Expand Down Expand Up @@ -269,6 +270,11 @@ export class FrameManager {
if (!initial) {
frame.apiLog(` navigated to "${url}"`);
this._page.frameNavigatedToNewDocument(frame);
// Re-number the main frame when it navigates away from a real document, so that aria
// refs (f<seq>e<n>) minted against the previous document do not accidentally resolve
// to elements in the new one.
if (frame === this._mainFrame && previousUrl && previousUrl !== 'about:blank')
frame.seq = this._allocateFrameSeq();
}
// Restore pending if any - see comments above about keepPending.
frame._setPendingDocument(keepPending);
Expand Down Expand Up @@ -501,7 +507,7 @@ export class Frame extends SdkObject<FrameEventMap> {
static Events = FrameEvent;

_id: string;
readonly seq: number;
seq: number;
_firedLifecycleEvents = new Set<types.LifecycleEvent>();
private _firedNetworkIdleSelf = false;
_currentDocument: DocumentInfo;
Expand Down
5 changes: 3 additions & 2 deletions packages/playwright-core/src/tools/backend/network.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import * as z from 'zod';

import { getExtensionForMimeType, isTextualMimeType } from '@isomorphic/mimeType';
import { isRegexString } from '@isomorphic/rtti';
import { truncateDataUrl } from '@isomorphic/stringUtils';

import { defineTool, defineTabTool } from './tool';

Expand Down Expand Up @@ -128,7 +129,7 @@ export function isFetch(request: playwright.Request): boolean {

export function renderRequestLine(request: playwright.Request): string {
const response = request.existingResponse();
let line = `[${request.method().toUpperCase()}] ${request.url()}`;
let line = `[${request.method().toUpperCase()}] ${truncateDataUrl(request.url())}`;
if (response)
line += ` => [${response.status()}] ${response.statusText()}`;
else if (request.failure())
Expand All @@ -140,7 +141,7 @@ function renderRequestDetails(index: number, request: playwright.Request, skillM
const httpResponse = request.existingResponse();
const responseHeaders = httpResponse?.headers();
const lines: string[] = [];
lines.push(`#${index} [${request.method().toUpperCase()}] ${request.url()}`);
lines.push(`#${index} [${request.method().toUpperCase()}] ${truncateDataUrl(request.url())}`);

lines.push('');
lines.push(' General');
Expand Down
5 changes: 4 additions & 1 deletion packages/playwright-core/types/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19024,7 +19024,10 @@ export interface Coverage {
reportAnonymousScripts?: boolean;

/**
* Whether to reset coverage on every navigation. Defaults to `true`.
* **NOTE** Settings this to `false` may still reset on navigations.
*
* Whether to reset coverage on every navigation. Defaults to `true`. Note that passing `false` does not guarantee
* that coverage persists through navigations, due to browser architecture limitations.
*/
resetOnNavigation?: boolean;
}): Promise<void>;
Expand Down
5 changes: 3 additions & 2 deletions packages/playwright/src/reporters/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,9 @@ class StripAnsiStream extends Writable {
this._target = target;
}

override _write(chunk: any, encoding: any, callback: any) {
this._target.write(stripAnsiEscapes(chunk.toString()), callback);
override write(chunk: any, encodingOrCallback?: any, callback?: any): boolean {
const cb = typeof encodingOrCallback === 'function' ? encodingOrCallback : callback;
return this._target.write(stripAnsiEscapes(chunk.toString()), cb);
}
}

Expand Down
8 changes: 0 additions & 8 deletions tests/library/chromium/js-coverage.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,14 +63,6 @@ it('should report multiple scripts', async function({ page, server }) {
expect(coverage[1].url).toContain('/jscoverage/script2.js');
});

it('should report scripts across navigations when disabled', async function({ page, server }) {
await page.coverage.startJSCoverage({ resetOnNavigation: false });
await page.goto(server.PREFIX + '/jscoverage/multiple.html');
await page.goto(server.EMPTY_PAGE);
const coverage = await page.coverage.stopJSCoverage();
expect(coverage.length).toBe(2);
});

it('should NOT report scripts across navigations when enabled', async function({ page, server }) {
await page.coverage.startJSCoverage(); // Enabled by default.
await page.goto(server.PREFIX + '/jscoverage/multiple.html');
Expand Down
15 changes: 15 additions & 0 deletions tests/mcp/config-resolve.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,21 @@ test.describe('merge order', () => {
const config = await resolveCLIConfigForMCP({ config: configFile }, emptyEnv);
expect(config.browser.cdpHeaders).toEqual({ Authorization: 'Bearer token-from-file' });
});

test('env browser.cdpHeaders overrides config file and preserves colons in values', async ({}, testInfo) => {
const configFile = testInfo.outputPath('config.json');
const fileConfig: Config = {
browser: {
cdpEndpoint: 'ws://example.invalid',
cdpHeaders: { Authorization: 'Bearer token-from-file' },
},
};
await fs.promises.writeFile(configFile, JSON.stringify(fileConfig));
const config = await resolveCLIConfigForMCP({ config: configFile }, {
PLAYWRIGHT_MCP_CDP_HEADERS: 'X-Forwarded-Proto: value:with:colons',
});
expect(config.browser.cdpHeaders).toEqual({ 'X-Forwarded-Proto': 'value:with:colons' });
});
});

// ---------------------------------------------------------------------------
Expand Down
43 changes: 40 additions & 3 deletions tests/page/page-aria-snapshot-ai.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,36 @@ it('should stitch all frame snapshots', async ({ page, server }) => {
}
});

it('should re-number refs across navigations but not same-document navigations', async ({ page, server }) => {
server.setRoute('/one.html', (req, res) => {
res.setHeader('Content-Type', 'text/html');
res.end('<button>One</button>');
});
server.setRoute('/two.html', (req, res) => {
res.setHeader('Content-Type', 'text/html');
res.end('<button>Two</button>');
});

// The first committed document keeps the base seq, so the main frame has no prefix.
await page.goto(server.PREFIX + '/one.html');
const oneRef = (await snapshotForAI(page)).match(/button "One" \[ref=(e\d+)\]/)![1];
await expect(page.locator(`aria-ref=${oneRef}`)).toHaveText('One');

// Cross-document navigation re-numbers the main frame, so its refs gain a frame prefix.
await page.goto(server.PREFIX + '/two.html');
const twoRef = (await snapshotForAI(page)).match(/button "Two" \[ref=(f\d+e\d+)\]/)![1];
await expect(page.locator(`aria-ref=${twoRef}`)).toHaveText('Two');

// The stale ref from the previous document must not resolve against the new one.
const error = await page.locator(`aria-ref=${oneRef}`).normalize().catch(e => e);
expect(error.message).toContain(`No element matching aria-ref=${oneRef}`);

// Same-document navigation keeps refs intact.
await page.evaluate(() => history.pushState({}, '', '/pushed.html'));
expect(await snapshotForAI(page)).toContain(`button "Two" [ref=${twoRef}]`);
await expect(page.locator(`aria-ref=${twoRef}`)).toHaveText('Two');
});

it('should persist iframe references', async ({ page }) => {
await page.setContent(`
<ul>
Expand Down Expand Up @@ -318,6 +348,14 @@ it('should not nest cursor pointer hints', async ({ page }) => {
`);
});

it('should truncate data url in link', async ({ page }) => {
const base64 = Buffer.from('<p>hello</p>').toString('base64');
await page.setContent(`<a href="data:text/html;base64,${base64}">a link</a>`);
const snapshot = await snapshotForAI(page);
expect(snapshot).toContain('/url: data:text/html;base64,…');
expect(snapshot).not.toContain(base64);
});

it('should gracefully fallback when child frame cant be captured', async ({ page, server }) => {
await page.setContent(`
<p>Test</p>
Expand All @@ -337,9 +375,8 @@ it('should auto-wait for navigation', async ({ page, server }) => {
page.evaluate(() => window.location.reload()),
snapshotForAI(page)
]);
expect(snapshot).toContainYaml(`
- generic [ref=e2]: Hi, I'm frame
`);
// The snapshot races the reload, which may re-number the main frame, so accept any ref.
expect(snapshot).toMatch(/- generic \[ref=(?:f\d+)?e\d+\]: Hi, I'm frame/);
});

it('should auto-wait for blocking CSS', async ({ page, server }) => {
Expand Down
8 changes: 8 additions & 0 deletions tests/page/selectors-get-by.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,14 @@ it('getByRole escaping', async ({ page }) => {
]);
});

it('getByRole should accept regexp with v flag', async ({ page }) => {
// Regression test for https://github.com/microsoft/playwright/issues/41457
await page.setContent(`<button>Click me</button><button>Submit</button>`);
await expect(page.getByRole('button', { name: /Click me/v })).toHaveCount(1);
await expect(page.getByRole('button', { name: /click me/iv })).toHaveCount(1);
await expect(page.getByRole('button', { name: /Missing/v })).toHaveCount(0, { timeout: 1000 });
});

it('getByRole with description', async ({ page }) => {
await page.setContent(`
<div role="alert" aria-label="Upload successful" aria-description="File doc-2025.pdf was uploaded successfully">Alert 1</div>
Expand Down
Loading
Loading