diff --git a/scripts/upstream-snapshot/versions.json b/scripts/upstream-snapshot/versions.json index 7f837cb9..862532d1 100644 --- a/scripts/upstream-snapshot/versions.json +++ b/scripts/upstream-snapshot/versions.json @@ -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" } } } diff --git a/tests/integration/core.spec.ts b/tests/integration/core.spec.ts index 4c16f6f3..2e417973 100644 --- a/tests/integration/core.spec.ts +++ b/tests/integration/core.spec.ts @@ -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, diff --git a/vendor/playwright-cli/tests/integration.spec.ts b/vendor/playwright-cli/tests/integration.spec.ts index 9e898a26..47c1afa2 100644 --- a/vendor/playwright-cli/tests/integration.spec.ts +++ b/vendor/playwright-cli/tests/integration.spec.ts @@ -15,9 +15,11 @@ */ 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; @@ -25,6 +27,50 @@ type CliResult = { 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(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 { + 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 = {}): Promise { const cliPath = path.join(__dirname, '../playwright-cli.js'); @@ -61,19 +107,38 @@ async function runCli(args: string[], env: Record = {}): 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, })); @@ -81,17 +146,17 @@ test('warns when installed skill is out of date', async ({}) => { 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, })); @@ -99,19 +164,18 @@ test('does not warn when installed skill only differs in line endings', async ({ 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, @@ -119,7 +183,7 @@ test('caches the update check in the default registry directory', async ({}) => 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) => { @@ -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); +});