Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,6 @@ const disabledFeatures = (assistantMode?: boolean) => [
'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',
assistantMode ? 'AutomationControlled' : '',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ export type SerializedValue =
{ o: { k: string, v: SerializedValue }[], id: number } |
{ ref: number } |
{ h: number } |
{ ta: { b: string, k: TypedArrayKind } };
{ ta: { b: string, k: TypedArrayKind } } |
{ ab: { b: string } };

type HandleOrValue = { h: number } | { fallThrough: any };

Expand Down Expand Up @@ -78,6 +79,14 @@ function isTypedArray(obj: any, constructor: Function): boolean {
}
}

function isArrayBuffer(obj: any): obj is ArrayBuffer {
try {
return obj instanceof ArrayBuffer || Object.prototype.toString.call(obj) === '[object ArrayBuffer]';
} catch (error) {
return false;
}
}

const typedArrayConstructors: Record<TypedArrayKind, Function> = {
i8: Int8Array,
ui8: Uint8Array,
Expand Down Expand Up @@ -170,6 +179,8 @@ export function parseEvaluationResultValue(value: SerializedValue, handles: any[
return handles[value.h];
if ('ta' in value)
return base64ToTypedArray(value.ta.b, typedArrayConstructors[value.ta.k]);
if ('ab' in value)
return base64ToTypedArray(value.ab.b, Uint8Array).buffer;
}
return value;
}
Expand Down Expand Up @@ -244,6 +255,8 @@ function innerSerialize(value: any, handleSerializer: (value: any) => HandleOrVa
if (isTypedArray(value, ctor))
return { ta: { b: typedArrayToBase64(value), k } };
}
if (isArrayBuffer(value))
return { ab: { b: typedArrayToBase64(new Uint8Array(value)) } };

const id = visitorInfo.visited.get(value);
if (id)
Expand Down
8 changes: 0 additions & 8 deletions packages/playwright-core/src/utilsBundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,6 @@ export type { Range as YAMLRange, Scalar as YAMLScalar, YAMLError, YAMLMap, YAML
export type { Command } from '../bundles/utils/node_modules/commander';
export type { EventEmitter as WebSocketEventEmitter, RawData as WebSocketRawData, WebSocket, WebSocketServer } from '../bundles/utils/node_modules/@types/ws';

program.exitOverride(err => {
// Calling process.exit() might truncate large stdout/stderr output.
// See https://github.com/nodejs/node/issues/6456.
// See https://github.com/nodejs/node/issues/12921
// eslint-disable-next-line no-restricted-properties
process.stdout.write('', () => process.stderr.write('', () => process.exit(err.exitCode)));
});

export function ms(ms: number): string {
if (!isFinite(ms))
return '-';
Expand Down
5 changes: 3 additions & 2 deletions packages/playwright/src/mcp/terminal/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,11 @@ class Session {
method,
params,
};
await this._connection.send(message);
return new Promise<any>((resolve, reject) => {
const responsePromise = new Promise<any>((resolve, reject) => {
this._callbacks.set(messageId, { resolve, reject });
});
const [result] = await Promise.all([responsePromise, this._connection.send(message)]);
return result;
}

close() {
Expand Down
7 changes: 4 additions & 3 deletions packages/playwright/src/worker/fixtureRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ export class FixtureRunner {
throw firstError;
}

async resolveParametersForFunction(fn: Function, testInfo: TestInfoImpl, autoFixtures: 'worker' | 'test' | 'all-hooks-only', runnable: RunnableDescription): Promise<object | null> {
async resolveParametersForFunction(fn: Function, testInfo: TestInfoImpl, autoFixtures: 'worker' | 'test' | 'all-hooks-only', runnable: RunnableDescription): Promise<{ result: object } | null> {
const collector = new Set<FixtureRegistration>();

// Collect automatic fixtures.
Expand Down Expand Up @@ -264,7 +264,8 @@ export class FixtureRunner {
return null;
params[name] = fixture.value;
}
return params;
// Wrap in an object to avoid returning a thenable if a fixture is named 'then'.
return { result: params };
}

async resolveParametersAndRunFunction(fn: Function, testInfo: TestInfoImpl, autoFixtures: 'worker' | 'test' | 'all-hooks-only', runnable: RunnableDescription) {
Expand All @@ -273,7 +274,7 @@ export class FixtureRunner {
// Do not run the function when fixture setup has already failed.
return null;
}
await testInfo._runWithTimeout(runnable, () => fn(params, testInfo));
await testInfo._runWithTimeout(runnable, () => fn(params.result, testInfo));
}

private async _setupFixtureForRegistration(registration: FixtureRegistration, testInfo: TestInfoImpl, runnable: RunnableDescription): Promise<Fixture> {
Expand Down
5 changes: 4 additions & 1 deletion packages/playwright/src/worker/workerMain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -391,7 +391,10 @@ export class WorkerMain extends ProcessRunner {
await this._runEachHooksForSuites(suites, 'beforeEach', testInfo);

// Setup fixtures required by the test.
testFunctionParams = await this._fixtureRunner.resolveParametersForFunction(test.fn, testInfo, 'test', { type: 'test' });
const params = await this._fixtureRunner.resolveParametersForFunction(test.fn, testInfo, 'test', { type: 'test' });
if (params !== null)
testFunctionParams = params.result;

});

if (testFunctionParams === null) {
Expand Down
8 changes: 5 additions & 3 deletions tests/assets/to-do-notifications/scripts/todo.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,11 +91,13 @@ window.onload = () => {
}

// Check which suffix the deadline day of the month needs
const { hours, minutes, day, month, year, notified, taskTitle } = cursor.value;
const { hours, minutes, day, month, year, notified, taskTitle, binaryTitle } = cursor.value;
const ordDay = ordinal(day);

const decodedBinaryTitle = new TextDecoder().decode(new Uint8Array(binaryTitle));

// Build the to-do list entry and put it into the list item.
const toDoText = `${taskTitle} — ${hours}:${minutes}, ${month} ${ordDay} ${year}.`;
const toDoText = `${taskTitle} [${decodedBinaryTitle}] — ${hours}:${minutes}, ${month} ${ordDay} ${year}.`;
const listItem = createListItem(toDoText);

if (notified === 'yes') {
Expand Down Expand Up @@ -140,7 +142,7 @@ window.onload = () => {

// Grab the values entered into the form fields and store them in an object ready for being inserted into the IndexedDB
const newItem = [
{ taskTitle: title.value, hours: hours.value, minutes: minutes.value, day: day.value, month: month.value, year: year.value, notified: 'no' },
{ taskTitle: title.value, hours: hours.value, minutes: minutes.value, day: day.value, month: month.value, year: year.value, notified: 'no', binaryTitle: new TextEncoder().encode(title.value).buffer },
];

// Open a read/write DB transaction, ready for adding the data
Expand Down
4 changes: 3 additions & 1 deletion tests/library/agent-perform.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,9 @@ test('perform reports error', async ({ context }) => {
expect(e.message).toContain('Agent refused to perform action:');
});

test('should dispatch event and respect dispose()', async ({ context, server }) => {
test('should dispatch event and respect dispose()', async ({ context, server, mode }) => {
test.skip(mode !== 'default', 'Different errors due to timing');

let apiResponse;
server.setRoute('/api', (req, res) => {
apiResponse = res;
Expand Down
22 changes: 13 additions & 9 deletions tests/library/browsercontext-storage-state.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -409,14 +409,18 @@ it('should support IndexedDB', async ({ page, server, contextFactory }) => {
keyPath: 'taskTitle',
records: [
{
value: {
day: '01',
hours: '1',
minutes: '1',
month: 'January',
notified: 'no',
taskTitle: 'Pet the cat',
year: '2025',
valueEncoded: {
id: 1,
o: [
{ k: 'taskTitle', v: 'Pet the cat' },
{ k: 'hours', v: '1' },
{ k: 'minutes', v: '1' },
{ k: 'day', v: '01' },
{ k: 'month', v: 'January' },
{ k: 'year', v: '2025' },
{ k: 'notified', v: 'no' },
{ k: 'binaryTitle', v: { ab: { b: 'UGV0IHRoZSBjYXQ=' } }, }
]
},
},
],
Expand Down Expand Up @@ -473,7 +477,7 @@ it('should support IndexedDB', async ({ page, server, contextFactory }) => {
await expect(recreatedPage.locator('#task-list')).toMatchAriaSnapshot(`
- list:
- listitem:
- text: /Pet the cat/
- text: /Pet the cat \\[Pet the cat\\]/
`);

expect(await context.storageState()).toEqual({ cookies: [], origins: [] });
Expand Down
4 changes: 2 additions & 2 deletions tests/library/inspector/cli-codegen-1.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ await page.GetByRole(AriaRole.Button, new() { Name = "Submit" }).DblClickAsync()
]);

// Do not trigger double click.
await page.waitForTimeout(1000);
await page.waitForTimeout(3000);

const [sources] = await Promise.all([
recorder.waitForOutput('JavaScript', `click();\n await`),
Expand All @@ -132,7 +132,7 @@ await page.GetByRole(AriaRole.Button, new() { Name = "Submit" }).DblClickAsync()
]);

// Do not trigger double click.
await page.waitForTimeout(1000);
await page.waitForTimeout(3000);

await Promise.all([
recorder.waitForOutput('JavaScript', `click();\n await`),
Expand Down
1 change: 0 additions & 1 deletion tests/library/tracing.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,6 @@ test('should not include trace resources from the previous chunks', async ({ con
const names = Array.from(resources.keys());
expect(names.filter(n => n.endsWith('.html')).length).toBe(1);
jpegs = names.filter(n => n.endsWith('.jpeg'));
expect(jpegs.length).toBeGreaterThan(0);
// 1 source file for the test.
expect(names.filter(n => n.endsWith('.txt')).length).toBe(1);
}
Expand Down
4 changes: 2 additions & 2 deletions tests/mcp/cli.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ test.describe('core', () => {
});

test('check', async ({ cli, server, mcpBrowser }) => {
const active = mcpBrowser === 'webkit' && process.platform === 'darwin' ? '' : '[active] ';
const active = mcpBrowser === 'webkit' && process.platform !== 'linux' ? '' : '[active] ';
server.setContent('/', `<input type="checkbox">`, 'text/html');
await cli('open', server.PREFIX);
await cli('check', 'e2');
Expand All @@ -119,7 +119,7 @@ test.describe('core', () => {
});

test('uncheck', async ({ cli, server, mcpBrowser }) => {
const active = mcpBrowser === 'webkit' && process.platform === 'darwin' ? '' : '[active] ';
const active = mcpBrowser === 'webkit' && process.platform !== 'linux' ? '' : '[active] ';
server.setContent('/', `<input type="checkbox" checked>`, 'text/html');
await cli('open', server.PREFIX);
await cli('uncheck', 'e2');
Expand Down
2 changes: 1 addition & 1 deletion tests/page/expect-misc.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -565,7 +565,7 @@ test.describe('toBeInViewport', () => {
test('should have good stack', async ({ page }) => {
let error;
try {
await expect(page.locator('body')).not.toBeInViewport({ timeout: 500 });
await expect(page.locator('body')).not.toBeInViewport({ timeout: 3000 });
} catch (e) {
error = e;
}
Expand Down
16 changes: 16 additions & 0 deletions tests/playwright-test/fixtures.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -834,3 +834,19 @@ test('should error if use is not called', async ({ runInlineTest }) => {
expect(result.failed).toBe(1);
expect(result.output).toContain(`use() was not called in fixture "fixture"`);
});

test('should not treat fixtures as thenable promise', async ({ runInlineTest }) => {
const { results } = await runInlineTest({
'a.test.ts': `
import { test as base, expect } from '@playwright/test';
const test = base.extend({
then: async ({}, use) => await use(() => 'test-function'),
});

test('should not treat fixtures as thenable promise', async ({ then }) => {
expect(typeof then).toBe('function');
});
`,
});
expect(results[0].status).toBe('passed');
});
46 changes: 0 additions & 46 deletions tests/playwright-test/runner.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,52 +115,6 @@ test('should report subprocess creation error', async ({ runInlineTest }, testIn
expect(result.output).toContain('Error: worker process exited unexpectedly (code=42, signal=null)');
});

test.describe(() => {
// Since we create worker processes in the same process group, SIGINT is issued concurrently to
// all processes. Therefore, we might end up with a dead worker before the runner starts to shutdown.
// This in turn leads to the "worker process exited unexpectedly" error. Retries should help.
test.describe.configure({ retries: 2 });

test('should ignore subprocess creation error because of SIGINT', async ({ interactWithTestRunner }, testInfo) => {
test.skip(process.platform === 'win32', 'No sending SIGINT on Windows');

const readyFile = testInfo.outputPath('ready.txt');
const testProcess = await interactWithTestRunner({
'hang.js': `
require('fs').writeFileSync(${JSON.stringify(readyFile)}, 'ready');
setInterval(() => {}, 1000);
`,
'preload.js': `
require('child_process').spawnSync(
process.argv[0],
[require('path').resolve('./hang.js')],
{ env: { ...process.env, NODE_OPTIONS: '' } },
);
`,
'a.spec.js': `
import { test, expect } from '@playwright/test';
test('fails', () => {});
test('skipped', () => {});
// Infect subprocesses to immediately hang when spawning a worker.
process.env.NODE_OPTIONS = '--require ${JSON.stringify(testInfo.outputPath('preload.js'))}';
`
});

while (!fs.existsSync(readyFile))
await new Promise(f => setTimeout(f, 100));
process.kill(-testProcess.process.pid!, 'SIGINT');

const { exitCode } = await testProcess.exited;
expect.soft(exitCode).toBe(130);

const result = parseTestRunnerOutput(testProcess.output);
expect.soft(result.passed).toBe(0);
expect.soft(result.failed).toBe(0);
expect.soft(result.didNotRun).toBe(2);
expect.soft(result.output).not.toContain('worker process exited unexpectedly');
});
});

test('sigint should stop workers', async ({ interactWithTestRunner }) => {
test.skip(process.platform === 'win32', 'No sending SIGINT on Windows');

Expand Down
4 changes: 3 additions & 1 deletion tests/playwright-test/ui-mode-test-network-tab.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -321,10 +321,10 @@ test('should copy network request', async ({ runUITest, server }) => {

await page.getByRole('tab', { name: 'Network' }).click();
await page.getByRole('listitem').filter({ hasText: 'post-data-1' }).click();
await page.getByRole('button', { name: 'Copy request' }).hover();

await page.context().grantPermissions(['clipboard-read', 'clipboard-write']);

await page.getByRole('button', { name: 'Copy request' }).hover();
await page.getByRole('button', { name: 'Copy as cURL' }).click();
await expect(async () => {
const curlRequest = await page.evaluate(() => (window as any).__clipboardCall);
Expand All @@ -339,6 +339,7 @@ test('should copy network request', async ({ runUITest, server }) => {
}
}).toPass();

await page.getByRole('button', { name: 'Copy request' }).hover();
await page.getByRole('button', { name: 'Copy as Fetch' }).click();
await expect(async () => {
const fetchRequest = await page.evaluate(() => (window as any).__clipboardCall);
Expand All @@ -348,6 +349,7 @@ test('should copy network request', async ({ runUITest, server }) => {
expect(fetchRequest).toContain(`"method": "POST"`);
}).toPass();

await page.getByRole('button', { name: 'Copy request' }).hover();
await page.getByRole('button', { name: 'Copy as Playwright' }).click();
await expect(async () => {
const playwrightRequest = await page.evaluate(() => (window as any).__clipboardCall);
Expand Down
6 changes: 3 additions & 3 deletions tests/playwright-test/ui-mode-trace.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -451,11 +451,11 @@ test('should work behind reverse proxy', { annotation: { type: 'issue', descript

await expect(page.getByTestId('actions-tree')).toMatchAriaSnapshot(`
- tree:
- treeitem /Before Hooks \\d+[hmsp]+/
- treeitem /Set content \\d+[hmsp]+/
- treeitem /Before Hooks/
- treeitem /Set content/
- treeitem /Click.*getByRole/
- treeitem /Expect "toBe"/
- treeitem /After Hooks \\d+[hmsp]+/
- treeitem /After Hooks/
`);

await expect(
Expand Down
8 changes: 6 additions & 2 deletions utils/doclint/api_parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,9 @@ class ApiParser {
if (!match)
throw new Error('Invalid member: ' + spec.text);
const metainfo = extractMetainfo(spec);
if (metainfo.hidden)
return;

const name = match[3];
let returnType = null;
let optional = false;
Expand Down Expand Up @@ -125,8 +128,6 @@ class ApiParser {
const clazz = /** @type {docs.Class} */(this.classes.get(match[2]));
if (!clazz)
throw new Error(`Unknown class ${match[2]} for member: ` + spec.text);
if (metainfo.hidden)
return;

const existingMember = clazz.membersArray.find(m => m.name === name && m.kind === member.kind);
if (existingMember && isTypeOverride(existingMember, member)) {
Expand All @@ -146,6 +147,9 @@ class ApiParser {
const match = spec.text.match(/(param|option): (.*)/);
if (!match)
throw `Something went wrong with matching ${spec.text}`;
const metainfo = extractMetainfo(spec);
if (metainfo.hidden)
return null;

// For "test.describe.only.title":
// - className is "test"
Expand Down
Loading
Loading