diff --git a/package.json b/package.json index 1b692e728d78a..f0081fd89c44a 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "check-deps": "node utils/check_deps.js", "build-android-driver": "./utils/build_android_driver.sh", "innerloop": "playwright run-server --reuse-browser", - "playwright-cli": "node packages/playwright/lib/cli/client/program.js", + "playwright-cli": "node packages/playwright/lib/cli/client/cli.js", "test-playwright-cli": "playwright test --config=tests/mcp/playwright.config.ts --project=chrome cli-", "playwright-cli-readme": "node utils/generate_cli_help.js --readme" }, diff --git a/packages/playwright/src/cli/client/cli.ts b/packages/playwright/src/cli/client/cli.ts new file mode 100644 index 0000000000000..6db3e40ee050b --- /dev/null +++ b/packages/playwright/src/cli/client/cli.ts @@ -0,0 +1,24 @@ +/** + * 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. + */ + +import { program } from './program'; + +program().catch(e => { + /* eslint-disable no-console */ + console.error(e.message); + /* eslint-disable no-restricted-properties */ + process.exit(1); +}); diff --git a/packages/playwright/src/cli/client/program.ts b/packages/playwright/src/cli/client/program.ts index 780578f46dbeb..9826339a420f3 100644 --- a/packages/playwright/src/cli/client/program.ts +++ b/packages/playwright/src/cli/client/program.ts @@ -74,7 +74,7 @@ const booleanOptions: (keyof (GlobalOptions & OpenOptions & { all?: boolean }))[ 'version', ]; -async function program() { +export async function program(options?: { embedderVersion?: string}) { const clientInfo = createClientInfo(); const help = require('./help.json'); @@ -104,7 +104,7 @@ async function program() { const commandName = args._?.[0]; if (args.version || args.v) { - console.log(clientInfo.version); + console.log(options?.embedderVersion ?? clientInfo.version); process.exit(0); } @@ -436,10 +436,3 @@ async function renderSessionStatus(session: Session) { text.push(...renderResolvedConfig(config.resolvedConfig)); return text.join('\n'); } - -program().catch(e => { - /* eslint-disable no-console */ - console.error(e.message); - /* eslint-disable no-restricted-properties */ - process.exit(1); -}); diff --git a/packages/playwright/src/cli/client/socketConnection.ts b/packages/playwright/src/cli/client/socketConnection.ts index 7ebaa01519c23..0d5c35c9fb543 100644 --- a/packages/playwright/src/cli/client/socketConnection.ts +++ b/packages/playwright/src/cli/client/socketConnection.ts @@ -89,15 +89,34 @@ export class SocketConnection { } export function compareSemver(a: string, b: string): number { - a = a.replace(/-.*$/, ''); - b = b.replace(/-.*$/, ''); - const aParts = a.split('.').map(Number); - const bParts = b.split('.').map(Number); + const aBase = a.replace(/-.*$/, ''); + const bBase = b.replace(/-.*$/, ''); + const aParts = aBase.split('.').map(Number); + const bParts = bBase.split('.').map(Number); for (let i = 0; i < 3; i++) { if (aParts[i] > bParts[i]) return 1; if (aParts[i] < bParts[i]) return -1; } + const aTimestamp = parseSuffixTimestamp(a); + const bTimestamp = parseSuffixTimestamp(b); + if (aTimestamp > bTimestamp) + return 1; + if (aTimestamp < bTimestamp) + return -1; return 0; } + +function parseSuffixTimestamp(version: string): number { + // Stable release (no suffix) is greater than any pre-release. + const match = version.match(/^\d+\.\d+\.\d+-(?:alpha|beta)-(.+)$/); + if (!match) + return Infinity; + const suffix = match[1]; + // Daily alpha: 1.59.0-alpha-2026-02-16 + if (/^\d{4}-\d{2}-\d{2}$/.test(suffix)) + return new Date(suffix).getTime(); + // Nightly/per-commit: 1.59.0-alpha-1771260841000 + return Number(suffix); +} diff --git a/tests/mcp/cli-fixtures.ts b/tests/mcp/cli-fixtures.ts index b90ed6d251093..a40b1a8d980ab 100644 --- a/tests/mcp/cli-fixtures.ts +++ b/tests/mcp/cli-fixtures.ts @@ -62,7 +62,7 @@ async function runCli(childProcess: CommonFixtures['childProcess'], args: string return await test.step(stepTitle, async () => { const testInfo = test.info(); const cli = childProcess({ - command: [process.execPath, require.resolve('../../packages/playwright/lib/cli/client/program.js'), ...args], + command: [process.execPath, require.resolve('../../packages/playwright/lib/cli/client/cli.js'), ...args], cwd: cliOptions.cwd ?? testInfo.outputPath(), env: { ...process.env, diff --git a/tests/mcp/semver.spec.ts b/tests/mcp/semver.spec.ts new file mode 100644 index 0000000000000..675908f21a0b2 --- /dev/null +++ b/tests/mcp/semver.spec.ts @@ -0,0 +1,54 @@ +/** + * 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. + */ + +import { test, expect } from '@playwright/test'; +import { compareSemver } from '../../packages/playwright/lib/cli/client/socketConnection'; + +test('compareSemver', () => { + // Stable versions. + expect(compareSemver('1.59.0', '1.58.0')).toBe(1); + expect(compareSemver('1.58.0', '1.59.0')).toBe(-1); + expect(compareSemver('1.58.2', '1.58.2')).toBe(0); + expect(compareSemver('1.58.2', '1.58.1')).toBe(1); + + // Different base versions ignore suffix. + expect(compareSemver('1.59.0-alpha-2026-02-16', '1.58.0')).toBe(1); + expect(compareSemver('1.58.0', '1.59.0-alpha-1771260841000')).toBe(-1); + + // Stable beats alpha/beta at same version. + expect(compareSemver('1.59.0', '1.59.0-alpha-2026-02-16')).toBe(1); + expect(compareSemver('1.59.0-alpha-2026-02-16', '1.59.0')).toBe(-1); + expect(compareSemver('1.59.0', '1.59.0-beta-1771260841000')).toBe(1); + expect(compareSemver('1.59.0-beta-1771260841000', '1.59.0')).toBe(-1); + + // Daily alphas compared by date. + expect(compareSemver('1.59.0-alpha-2026-02-16', '1.59.0-alpha-2026-02-15')).toBe(1); + expect(compareSemver('1.59.0-alpha-2026-02-15', '1.59.0-alpha-2026-02-16')).toBe(-1); + expect(compareSemver('1.59.0-alpha-2026-02-16', '1.59.0-alpha-2026-02-16')).toBe(0); + + // Nightly timestamps. + expect(compareSemver('1.59.0-alpha-1771260841000', '1.59.0-alpha-1771260840000')).toBe(1); + expect(compareSemver('1.59.0-alpha-1771260840000', '1.59.0-alpha-1771260841000')).toBe(-1); + expect(compareSemver('1.59.0-alpha-1771260841000', '1.59.0-alpha-1771260841000')).toBe(0); + + // Daily alpha vs nightly timestamp (date normalizes to ms). + expect(compareSemver('1.59.0-alpha-1771260841000', '1.59.0-alpha-2026-02-16')).toBe(1); + expect(compareSemver('1.59.0-alpha-2026-02-16', '1.59.0-alpha-1771260841000')).toBe(-1); + + // Beta suffixes. + expect(compareSemver('1.59.0-beta-1771260841000', '1.59.0-beta-1771260840000')).toBe(1); + expect(compareSemver('1.59.0-beta-2026-02-16', '1.59.0-beta-2026-02-15')).toBe(1); +});