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
4 changes: 2 additions & 2 deletions packages/playwright-core/browsers.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,14 @@
},
{
"name": "firefox-beta",
"revision": "1524",
"revision": "1525",
"installByDefault": false,
"browserVersion": "152.0b1",
"title": "Firefox Beta"
},
{
"name": "webkit",
"revision": "2310",
"revision": "2311",
"installByDefault": true,
"revisionOverrides": {
"mac14": "2251",
Expand Down
2 changes: 1 addition & 1 deletion tests/electron/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ const config: Config<PlaywrightWorkerOptions & PlaywrightTestOptions> = {
timeout: 10000,
},
timeout: 30000,
globalTimeout: 5400000,
globalTimeout: 7200000,
workers: process.env.CI ? 1 : undefined,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 3 : 0,
Expand Down
3 changes: 2 additions & 1 deletion tests/library/browsercontext-basic.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,9 @@ it('should be able to click across browser contexts', async function({ browser }
await page2.close();
});

it('should be able to hover across browser contexts in parallel', async function({ browser }) {
it('should be able to hover across browser contexts in parallel', async function({ browser, browserName, headless }) {
it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/40562' });
it.fixme(browserName === 'firefox' && !headless, 'Hover is flaky in headed Firefox');

const html = `
<style>
Expand Down
2 changes: 1 addition & 1 deletion tests/library/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ const config: Config<PlaywrightWorkerOptions & PlaywrightTestOptions & TestModeW
},
maxFailures: 200,
timeout: video ? 60000 : 30000,
globalTimeout: 5400000,
globalTimeout: 7200000,
workers: undefined,
fullyParallel: !process.env.CI,
forbidOnly: !!process.env.CI,
Expand Down
2 changes: 1 addition & 1 deletion tests/library/trace-viewer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -768,7 +768,7 @@ test('should capture attribute mutations inside a popup window', {
await expect(popup.locator('#overlay')).toBeHidden();
});

const frame = await traceViewer.snapshotFrame('Click');
const frame = await traceViewer.snapshotFrame('Expect');
await expect(frame.locator('#overlay')).toHaveClass('no-display');
});

Expand Down
33 changes: 33 additions & 0 deletions tests/page/page-autowaiting-no-hang.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,39 @@ it('clicking in the middle of navigation that commits', async ({ page, server })
await expect(page.locator('body')).toContainText('hello world');
});

it('clicking a link intercepted by the Navigation API same-document', {
annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/41125' },
}, async ({ page, server }) => {
server.setRoute('/intercept.html', (req, res) => {
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(`
<a id="go" href="/other">go</a>
<p id="status">initial</p>
<script>
navigation.addEventListener('navigate', event => {
if (!event.canIntercept)
return;
event.intercept({
handler: async () => {
const dest = new URL(event.destination.url).pathname;
document.getElementById('status').textContent = 'intercepted:' + dest;
},
});
});
</script>
`);
});

await page.goto(server.PREFIX + '/intercept.html');
await expect(page.locator('#status')).toHaveText('initial');

// Should not hang waiting for the same-document navigation to commit.
await page.locator('#go').click();

await expect(page.locator('#status')).toHaveText('intercepted:/other');
expect(new URL(page.url()).pathname).toBe('/other');
});

it('goBack in the middle of navigation that commits', async ({ page, server }) => {
let commitCallback;
const abortPromise = new Promise(f => commitCallback = f);
Expand Down
225 changes: 225 additions & 0 deletions utils/bisect-chromium.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
#!/usr/bin/env node
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

// Bisect Chrome for Testing per-commit builds between a known-good and a
// known-bad revision. Run with --help for usage.

import { execFileSync, spawnSync } from 'child_process';
import fs from 'fs';
import os from 'os';
import path from 'path';
import { parseArgs } from 'util';

const BUCKET = 'https://storage.googleapis.com/chrome-for-testing-per-commit-public';
const DEFAULT_CHECK = 'npm run ctest';

const HELP = `Bisect Chrome for Testing per-commit builds.

Usage:
node utils/bisect-chromium.mjs --good <rev> --bad <rev> [--check <command>]
Bisect between the revisions to find the last good and first bad build.

node utils/bisect-chromium.mjs <rev> [--check <command>]
Single-revision mode: download, extract and check just that build,
showing full output of the check command. Exits 0 if good, 1 if bad.

Options:
--good <rev> Known good revision (required for bisect mode).
--bad <rev> Known bad revision (required for bisect mode).
--check <command> Shell command that decides whether a build is good:
run with CRPATH set to the browser executable, exit
code 0 means good. Default:
${DEFAULT_CHECK}
--headed Use the full Chrome for Testing build instead of the
default chrome-headless-shell build.
--help Show this help.

Builds are cached in /tmp/chromium-r<rev>-<headless|headed> and reused on
subsequent runs.
The platform (${detectPlatform()}) is auto-detected.`;

function detectPlatform() {
const { platform, arch } = process;
if (platform === 'darwin')
return arch === 'arm64' ? 'mac-arm64' : 'mac-x64';
if (platform === 'linux')
return 'linux64';
if (platform === 'win32')
return arch === 'x64' ? 'win64' : 'win32';
throw new Error(`Unsupported platform: ${platform}/${arch}`);
}

const PLATFORM = detectPlatform();

function parseRevision(value, name) {
const rev = Number(String(value).replace(/^r/, ''));
if (!Number.isInteger(rev) || rev <= 0)
throw new Error(`Invalid ${name} revision: ${value}`);
return rev;
}

async function listRevisions(good, bad) {
const revisions = [];
let marker = `${PLATFORM}/r${good - 1}`;
while (true) {
const url = `${BUCKET}/?delimiter=/&prefix=${PLATFORM}/r&marker=${encodeURIComponent(marker)}`;
const text = await (await fetch(url)).text();
for (const m of text.matchAll(new RegExp(`<Prefix>${PLATFORM}/r(\\d+)/</Prefix>`, 'g'))) {
const rev = Number(m[1]);
if (rev >= good && rev <= bad)
revisions.push(rev);
}
const next = text.match(/<NextMarker>([^<]+)<\/NextMarker>/);
if (!next)
break;
marker = next[1];
const nextRev = Number(marker.match(/r(\d+)/)?.[1] ?? NaN);
if (nextRev > bad)
break;
}
return revisions.sort((a, b) => a - b);
}

function findExecutable(dir) {
const exeName = PLATFORM.startsWith('win') ? 'chrome-headless-shell.exe' : 'chrome-headless-shell';
const stack = [dir];
while (stack.length) {
const d = stack.pop();
for (const entry of fs.readdirSync(d)) {
const p = path.join(d, entry);
if (HEADED) {
if (PLATFORM.startsWith('mac') && entry.endsWith('.app')) {
const exeDir = path.join(p, 'Contents', 'MacOS');
return path.join(exeDir, fs.readdirSync(exeDir)[0]);
}
if (PLATFORM === 'linux64' && entry === 'chrome')
return p;
if (PLATFORM.startsWith('win') && entry === 'chrome.exe')
return p;
} else if (entry === exeName) {
return p;
}
if (fs.statSync(p).isDirectory())
stack.push(p);
}
}
throw new Error(`No browser executable found under ${dir}`);
}

function prepareBuild(rev) {
const dir = path.join(os.tmpdir(), `chromium-r${rev}-${HEADED ? 'headed' : 'headless'}`);
const marker = path.join(dir, '.ready');
if (!fs.existsSync(marker)) {
fs.rmSync(dir, { recursive: true, force: true });
fs.mkdirSync(dir, { recursive: true });
const zip = path.join(dir, 'build.zip');
const zipName = HEADED ? `chrome-${PLATFORM}.zip` : `chrome-headless-shell-${PLATFORM}.zip`;
const url = `${BUCKET}/${PLATFORM}/r${rev}/${zipName}`;
console.log(` downloading ${url}`);
execFileSync('curl', ['-sf', '-o', zip, url], { stdio: 'inherit' });
execFileSync('unzip', ['-q', zip, '-d', dir]);
fs.rmSync(zip);
if (PLATFORM.startsWith('mac'))
execFileSync('xattr', ['-cr', dir]);
fs.writeFileSync(marker, '');
}
return findExecutable(dir);
}

function isGood(rev, check, { verbose = false } = {}) {
const exe = prepareBuild(rev);
console.log(` running with CRPATH=${exe}: ${check}`);
const result = spawnSync(check, {
shell: true,
env: { ...process.env, CRPATH: exe, PLAYWRIGHT_HTML_OPEN: 'never' },
encoding: verbose ? undefined : 'utf8',
stdio: verbose ? 'inherit' : 'pipe',
});
const good = result.status === 0;
if (!good && !verbose) {
const tail = (result.stdout || '').split('\n').slice(-15).join('\n');
console.log(tail);
}
console.log(` r${rev}: ${good ? 'GOOD' : 'BAD'}`);
return good;
}

const { values: options, positionals } = parseArgs({
options: {
good: { type: 'string' },
bad: { type: 'string' },
check: { type: 'string', default: DEFAULT_CHECK },
headed: { type: 'boolean', default: false },
help: { type: 'boolean', default: false },
},
allowPositionals: true,
});

const HEADED = options.headed;

if (options.help) {
console.log(HELP);
process.exit(0);
}

// Single-revision mode.
if (positionals.length === 1) {
const rev = parseRevision(positionals[0], 'requested');
process.exit(isGood(rev, options.check, { verbose: true }) ? 0 : 1);
}
if (positionals.length > 1)
throw new Error(`Expected at most one positional argument, got: ${positionals.join(' ')}`);

if (!options.good || !options.bad)
throw new Error('Both --good and --bad are required for bisect mode. See --help.');
const good = parseRevision(options.good, '--good');
const bad = parseRevision(options.bad, '--bad');
if (good >= bad)
throw new Error(`--good (${good}) must be smaller than --bad (${bad}).`);

const revisions = await listRevisions(good, bad);
console.log(`Found ${revisions.length} available ${PLATFORM} builds in [${good}, ${bad}]`);
if (revisions.length < 2)
throw new Error('Not enough builds available to bisect.');
if (revisions[0] !== good)
console.warn(`Warning: good revision r${good} has no build; nearest is r${revisions[0]}`);
if (revisions[revisions.length - 1] !== bad)
console.warn(`Warning: bad revision r${bad} has no build; nearest is r${revisions[revisions.length - 1]}`);

console.log(`Verifying endpoints...`);
console.log(`Checking good endpoint r${revisions[0]}`);
if (!isGood(revisions[0], options.check))
throw new Error(`Supposedly good revision r${revisions[0]} is BAD; aborting.`);
console.log(`Checking bad endpoint r${revisions[revisions.length - 1]}`);
if (isGood(revisions[revisions.length - 1], options.check))
throw new Error(`Supposedly bad revision r${revisions[revisions.length - 1]} is GOOD; aborting.`);

let lo = 0; // known good index
let hi = revisions.length - 1; // known bad index
while (hi - lo > 1) {
const mid = (lo + hi) >> 1;
console.log(`\nBisecting r${revisions[mid]} (${hi - lo - 1} candidates left, ~${Math.ceil(Math.log2(hi - lo))} steps)`);
if (isGood(revisions[mid], options.check))
lo = mid;
else
hi = mid;
}

console.log(`\n=== RESULT ===`);
console.log(`Last good build: r${revisions[lo]}`);
console.log(`First bad build: r${revisions[hi]}`);
console.log(`Commits: https://crrev.com/${revisions[lo]} .. https://crrev.com/${revisions[hi]}`);
Loading