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
8 changes: 4 additions & 4 deletions scripts/upstream-snapshot/versions.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@
},
"playwrightCli": {
"repository": "microsoft/playwright-cli",
"commit": "b85c7a736bb473bf55b584e54a09ffa698d6d871",
"date": "2026-09-28T23:22:31Z",
"trackedAt": "2026-10-19",
"commit": "4bc00fb7fa5b4329f2fb53ac4743a066c2c664e8",
"date": "2026-11-05T23:09:31Z",
"trackedAt": "2026-11-08",
"vendorSha256": {
"vendor/playwright-cli/tests/integration.spec.ts": "16e8a83a94ce268a916e17fca63b79c61375abf2fc870e15edcb251c824d6ed8"
"vendor/playwright-cli/tests/integration.spec.ts": "4c289020b156eedbb731c093395445d52d969d944b3b312fb85b44c2d2b1a005"
}
}
}
12 changes: 11 additions & 1 deletion tests/integration/core.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,17 @@ import { test, expect } from 'patchright/test';

import { runCli } from '../fixtures';

test('open data URL', async ({}) => {
test('open data URL', { annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright-cli/issues/502' } }, async ({}) => {
const argumentError = "boolean option '--headed' should not be passed with '=value', use '--headed' or '--no-headed' instead";
expect(await runCli(['open', '--headed=false'])).toEqual({
output: '',
error: argumentError,
exitCode: 1,
});
// Upstream also expects `--json` to wrap argument errors as { isError, error } on stdout
// (microsoft/playwright-cli#522); that client change ships after Playwright 1.64, so it
// arrives with a later patchright-core roll.

expect(await runCli(['open', 'data:text/html,hello', '--persistent'])).toEqual(expect.objectContaining({
output: expect.stringContaining('hello'),
exitCode: 0,
Expand Down
159 changes: 147 additions & 12 deletions vendor/playwright-cli/tests/integration.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,62 @@
*/

import fs from 'fs';
import http from 'http';
import path from 'path';
import { spawn } from 'child_process';
import { test, expect } from '@playwright/test';
import { test as baseTest, expect } from '@playwright/test';
import type { AddressInfo } from 'net';

type CliResult = {
output: string;
error: string;
exitCode: number | null;
};

type NpmRegistry = {
url: string;
status: number;
hang: boolean;
requests: string[];
};

const test = baseTest.extend<{ npmRegistry: NpmRegistry }>({
npmRegistry: async ({}, use) => {
const registry: NpmRegistry = { url: '', status: 200, hang: false, requests: [] };
const server = http.createServer((req, res) => {
registry.requests.push(req.url!);
if (registry.hang)
return;
res.statusCode = registry.status;
res.setHeader('content-type', 'application/json');
res.end(JSON.stringify({
'name': '@playwright/cli',
'dist-tags': { latest: '99.0.0' },
'versions': { '99.0.0': { name: '@playwright/cli', version: '99.0.0' } },
}));
});
await new Promise<void>(resolve => server.listen(0, '127.0.0.1', resolve));
registry.url = `http://127.0.0.1:${(server.address() as AddressInfo).port}`;
await use(registry);
server.closeAllConnections();
await new Promise(resolve => server.close(resolve));
},
});

// Enables the update check and points npm at the test registry, isolated from
// the npm config of the machine running the tests.
function updateCheckEnv(npmRegistry: NpmRegistry): Record<string, string> {
return {
'CI': '',
'NO_UPDATE_NOTIFIER': '',
'npm_config_registry': npmRegistry.url,
'npm_config_@playwright:registry': '',
'npm_config_userconfig': test.info().outputPath('user-npmrc'),
'npm_config_globalconfig': test.info().outputPath('global-npmrc'),
'npm_config_cache': test.info().outputPath('npm-cache'),
};
}

async function runCli(args: string[], env: Record<string, string> = {}): Promise<CliResult> {
const cliPath = path.join(__dirname, '../playwright-cli.js');

Expand Down Expand Up @@ -61,65 +107,83 @@ async function runCli(args: string[], env: Record<string, string> = {}): Promise
});
}

test('open data URL', async ({}) => {
test('open data URL', { annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright-cli/issues/502' } }, async ({}) => {
const argumentError = "boolean option '--headed' should not be passed with '=value', use '--headed' or '--no-headed' instead";
expect(await runCli(['open', '--headed=false'])).toEqual({
output: '',
error: argumentError,
exitCode: 1,
});

expect(await runCli(['--json', 'open', '--headed=false'])).toEqual({
output: JSON.stringify({ isError: true, error: argumentError }, null, 2),
error: '',
exitCode: 1,
});

expect(await runCli(['open', 'data:text/html,hello', '--persistent'])).toEqual(expect.objectContaining({
output: expect.stringContaining('hello'),
exitCode: 0,
}));

expect(await runCli(['--json', 'screenshot', '--filename'])).toEqual({
output: JSON.stringify({ isError: true, error: "error: '--filename' option: expected string, received boolean" }, null, 2),
error: '',
exitCode: 1,
});

expect(await runCli(['delete-data'])).toEqual(expect.objectContaining({
output: expect.stringContaining('Deleted user data for'),
exitCode: 0,
}));
});

test('warns when installed skill is out of date', async ({}) => {
test('warns when installed skill is out of date', async ({ npmRegistry }) => {
expect(await runCli(['install', '--skills'], { NO_UPDATE_NOTIFIER: '1' })).toEqual(expect.objectContaining({
exitCode: 0,
}));

const skillFile = path.join(test.info().outputPath(), '.claude', 'skills', 'playwright-cli', 'SKILL.md');
fs.appendFileSync(skillFile, 'x');

const env = { CI: '', NO_UPDATE_NOTIFIER: '' };
expect(await runCli(['--help'], env)).toEqual(expect.objectContaining({
const env = updateCheckEnv(npmRegistry);
expect(await runCli(['-s', 'foo', 'list'], env)).toEqual(expect.objectContaining({
error: expect.stringContaining('does not match the tool version'),
}));

expect(await runCli(['--help'], env)).toEqual(expect.objectContaining({
expect(await runCli(['list'], env)).toEqual(expect.objectContaining({
error: expect.not.stringContaining('does not match the tool version'),
}));
});

test('does not warn when installed skill only differs in line endings', async ({}) => {
test('does not warn when installed skill only differs in line endings', async ({ npmRegistry }) => {
expect(await runCli(['install', '--skills'], { NO_UPDATE_NOTIFIER: '1' })).toEqual(expect.objectContaining({
exitCode: 0,
}));

const skillFile = path.join(test.info().outputPath(), '.claude', 'skills', 'playwright-cli', 'SKILL.md');
fs.writeFileSync(skillFile, fs.readFileSync(skillFile, 'utf8').replace(/\n/g, '\r\n'));

expect(await runCli(['--help'], { CI: '', NO_UPDATE_NOTIFIER: '' })).toEqual(expect.objectContaining({
expect(await runCli(['list'], updateCheckEnv(npmRegistry))).toEqual(expect.objectContaining({
error: expect.not.stringContaining('does not match the tool version'),
}));
});

test('caches the update check in the default registry directory', async ({}) => {
test('caches the update check in the default registry directory', async ({ npmRegistry }) => {
// Redirect the home/cache directories so the real user cache is untouched, and
// leave PLAYWRIGHT_CLI_INSTALLATION_FOR_TEST empty so the default path is used.
const home = test.info().outputPath('home');
fs.mkdirSync(home, { recursive: true });
const env = {
CI: '',
NO_UPDATE_NOTIFIER: '',
...updateCheckEnv(npmRegistry),
PLAYWRIGHT_CLI_INSTALLATION_FOR_TEST: '',
HOME: home,
USERPROFILE: home,
XDG_CACHE_HOME: path.join(home, '.cache'),
LOCALAPPDATA: path.join(home, 'AppData', 'Local'),
};

expect(await runCli(['--version'], env)).toEqual(expect.objectContaining({ exitCode: 0 }));
expect(await runCli(['list'], env)).toEqual(expect.objectContaining({ exitCode: 0 }));

const found: string[] = [];
const walk = (dir: string) => {
Expand All @@ -136,3 +200,74 @@ test('caches the update check in the default registry directory', async ({}) =>
expect(found).toHaveLength(1);
expect(JSON.parse(fs.readFileSync(found[0], 'utf8')).lastCheck).toEqual(expect.any(Number));
});

test('update check asks npm for the latest version on setup commands only', {
annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright-cli/issues/505' },
}, async ({ npmRegistry }) => {
const env = updateCheckEnv(npmRegistry);
expect(await runCli(['--version'], env)).toEqual(expect.objectContaining({
error: '',
exitCode: 0,
}));
expect(npmRegistry.requests).toEqual([]);

// A session named after a setup command is not the command.
expect(await runCli(['-s', 'list', 'snapshot'], env)).toEqual(expect.objectContaining({
error: expect.not.stringContaining('Update available for @playwright/cli'),
exitCode: 1,
}));
expect(npmRegistry.requests).toEqual([]);
expect(fs.existsSync(test.info().outputPath('cli-update-check.json'))).toBe(false);

// The command is recognized behind the global options.
expect(await runCli(['--session', 'foo', 'list'], env)).toEqual(expect.objectContaining({
error: expect.stringContaining('Update available for @playwright/cli'),
exitCode: 0,
}));
expect(npmRegistry.requests).toEqual(['/@playwright%2fcli']);
});

test('update check on install does not warn about the skills being installed', async ({ npmRegistry }) => {
expect(await runCli(['install', '--skills'], { NO_UPDATE_NOTIFIER: '1' })).toEqual(expect.objectContaining({
exitCode: 0,
}));

const skillFile = path.join(test.info().outputPath(), '.claude', 'skills', 'playwright-cli', 'SKILL.md');
fs.appendFileSync(skillFile, 'x');

const result = await runCli(['--session', 'list', 'install', '--skills'], updateCheckEnv(npmRegistry));
expect(result.exitCode).toBe(0);
expect(result.error).toContain('Update available for @playwright/cli');
expect(result.error).not.toContain('does not match the tool version');
expect(npmRegistry.requests).toEqual(['/@playwright%2fcli']);
});

test('update check uses the scoped registry from project .npmrc', async ({ npmRegistry }) => {
fs.writeFileSync(test.info().outputPath('package.json'), '{}');
fs.writeFileSync(test.info().outputPath('.npmrc'), `@playwright:registry=${npmRegistry.url}/scoped/`);

expect(await runCli(['list'], updateCheckEnv(npmRegistry))).toEqual(expect.objectContaining({
error: expect.stringContaining('Update available for @playwright/cli'),
exitCode: 0,
}));
expect(npmRegistry.requests).toEqual(['/scoped/@playwright%2fcli']);
});

test('update check failure is silent', async ({ npmRegistry }) => {
npmRegistry.status = 401;
expect(await runCli(['list'], updateCheckEnv(npmRegistry))).toEqual(expect.objectContaining({
error: '',
exitCode: 0,
}));
expect(npmRegistry.requests).toEqual(['/@playwright%2fcli']);
});

test('update check does not wait for a hanging registry', async ({ npmRegistry }) => {
npmRegistry.hang = true;
const start = Date.now();
expect(await runCli(['list'], updateCheckEnv(npmRegistry))).toEqual(expect.objectContaining({
error: '',
exitCode: 0,
}));
expect(Date.now() - start).toBeLessThan(10000);
});
Loading