From 3897f1ba21f64ebf0b5c1555fed79d26b067a556 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jind=C5=99ich=20B=C3=A4r?= Date: Fri, 2 Oct 2026 17:01:40 +0200 Subject: [PATCH 1/3] fix: allow playwright-core up to 1.62 Playwright 1.61+ now requires Camoufox beta.30, and an outdated install throws instead of being re-downloaded in the background. Closes #337 --- package.json | 4 ++-- pnpm-lock.yaml | 12 ++++++------ src/__version__.ts | 8 ++++++++ src/pkgman.ts | 35 +++++++++++++++++++++++++++-------- test/pkgman.test.ts | 42 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 85 insertions(+), 16 deletions(-) diff --git a/package.json b/package.json index f8617d6..bbbdc26 100644 --- a/package.json +++ b/package.json @@ -51,13 +51,13 @@ "@types/language-tags": "^1.0.4", "@types/node": "^24.0.0", "@types/xml2js": "^0.4.14", - "playwright-core": "<1.61.0", + "playwright-core": "<1.63.0", "rimraf": "^6.0.1", "typescript": "^5.8.3", "vitest": "^4.0.0" }, "peerDependencies": { - "playwright-core": "<1.61.0" + "playwright-core": "<1.63.0" }, "devEngines": { "packageManager": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8217af6..f50348a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -70,8 +70,8 @@ importers: specifier: ^0.4.14 version: 0.4.14 playwright-core: - specifier: <1.61.0 - version: 1.59.1 + specifier: <1.63.0 + version: 1.62.1 rimraf: specifier: ^6.0.1 version: 6.1.3 @@ -659,9 +659,9 @@ packages: resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==} engines: {node: '>=12'} - playwright-core@1.59.1: - resolution: {integrity: sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==} - engines: {node: '>=18'} + playwright-core@1.62.1: + resolution: {integrity: sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==} + engines: {node: '>=20'} hasBin: true postcss@8.5.28: @@ -1288,7 +1288,7 @@ snapshots: picomatch@4.0.7: {} - playwright-core@1.59.1: {} + playwright-core@1.62.1: {} postcss@8.5.28: dependencies: diff --git a/src/__version__.ts b/src/__version__.ts index 274a286..bae4896 100644 --- a/src/__version__.ts +++ b/src/__version__.ts @@ -9,6 +9,14 @@ export class CONSTRAINTS { static readonly MIN_VERSION: string = "alpha.1"; static readonly MAX_VERSION: string = "1"; + /** + * Minimum browser releases required by newer Playwright versions, as [[major, minor], release]. + * Playwright 1.61 sends viewport fields that builds before beta.30 reject. + */ + static readonly PLAYWRIGHT_BROWSER_FLOORS: [[number, number], string][] = [ + [[1, 61], "beta.30"], + ]; + static asRange(): string { /** * Returns the version range as a string. diff --git a/src/pkgman.ts b/src/pkgman.ts index 8fdcedf..8398f02 100644 --- a/src/pkgman.ts +++ b/src/pkgman.ts @@ -1,6 +1,7 @@ import { execFileSync } from "node:child_process"; import type { PathLike } from "node:fs"; import * as fs from "node:fs"; +import { createRequire } from "node:module"; import * as os from "node:os"; import * as path from "node:path"; import type { Writable } from "node:stream"; @@ -77,6 +78,19 @@ const LAUNCH_FILE: { [key: string]: string } = { lin: "camoufox-bin", }; +// The resolved playwright-core [major, minor], or undefined if it can't be read. +function playwrightVersion(): [number, number] | undefined { + try { + const { version } = createRequire(import.meta.url)( + "playwright-core/package.json", + ); + const [major, minor] = version.split(".").map(Number); + return [major, minor]; + } catch { + return undefined; + } +} + class Version { release: string; version?: string; @@ -117,7 +131,7 @@ class Version { } isSupported(): boolean { - return VERSION_MIN.lessThan(this) && this.lessThan(VERSION_MAX); + return !this.lessThan(VERSION_MIN) && this.lessThan(VERSION_MAX); } static fromPath(filePath: PathLike = INSTALL_DIR): Version { @@ -136,10 +150,15 @@ class Version { } static buildMinMax(): [Version, Version] { - return [ - new Version(CONSTRAINTS.MIN_VERSION), - new Version(CONSTRAINTS.MAX_VERSION), - ]; + let min = new Version(CONSTRAINTS.MIN_VERSION); + const [major, minor] = playwrightVersion() ?? [0, 0]; + for (const [required, release] of CONSTRAINTS.PLAYWRIGHT_BROWSER_FLOORS) { + const floor = new Version(release); + const needsFloor = + major > required[0] || (major === required[0] && minor >= required[1]); + if (needsFloor && min.lessThan(floor)) min = floor; + } + return [min, new Version(CONSTRAINTS.MAX_VERSION)]; } } @@ -430,9 +449,9 @@ export function camoufoxPath(downloadIfMissing: boolean = true): PathLike { ) { return INSTALL_DIR; } else { - if (!downloadIfMissing) { - throw new UnsupportedVersion("Camoufox executable is outdated."); - } + throw new UnsupportedVersion( + "Camoufox executable is outdated. Please run `camoufox fetch` to update.", + ); } // Install and recheck diff --git a/test/pkgman.test.ts b/test/pkgman.test.ts index 46376f3..3972403 100644 --- a/test/pkgman.test.ts +++ b/test/pkgman.test.ts @@ -199,3 +199,45 @@ describe("CamoufoxFetcher.install cleanup", () => { ).toEqual({ version: "1.0", release: "beta.1" }); }); }); + +describe("camoufoxPath browser floor", () => { + let installDir: string; + + beforeEach(() => { + installDir = fs.mkdtempSync(path.join(os.tmpdir(), "cfx-floortest-")); + vi.stubEnv("CAMOUFOX_INSTALL_DIR", installDir); + }); + + afterEach(() => { + vi.unstubAllEnvs(); + vi.doUnmock("node:module"); + vi.resetModules(); + fs.rmSync(installDir, { recursive: true, force: true }); + }); + + async function camoufoxPathWith(playwright: string, release: string) { + fs.writeFileSync( + path.join(installDir, "version.json"), + JSON.stringify({ version: "152.0.4", release }), + ); + vi.doMock("node:module", async (importOriginal) => ({ + ...(await importOriginal()), + createRequire: () => () => ({ version: playwright }), + })); + vi.resetModules(); + const { camoufoxPath } = await import("../src/pkgman"); + return () => camoufoxPath(false); + } + + test("accepts older builds below Playwright 1.61", async () => { + expect((await camoufoxPathWith("1.60.0", "beta.29"))()).toBe(installDir); + }); + + test("rejects builds below beta.30 from Playwright 1.61", async () => { + expect(await camoufoxPathWith("1.61.0", "beta.29")).toThrow("outdated"); + }); + + test("accepts beta.30 from Playwright 1.61", async () => { + expect((await camoufoxPathWith("1.62.1", "beta.30"))()).toBe(installDir); + }); +}); From c0e83ddd4f8e9b39dea287e41927d0bf90f4a37a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jind=C5=99ich=20B=C3=A4r?= Date: Fri, 2 Oct 2026 17:11:25 +0200 Subject: [PATCH 2/3] refactor: simplify the Playwright browser floor and report the effective range --- src/__version__.ts | 14 ++------------ src/pkgman.ts | 29 ++++++++++++++--------------- test/pkgman.test.ts | 20 ++++++++++---------- 3 files changed, 26 insertions(+), 37 deletions(-) diff --git a/src/__version__.ts b/src/__version__.ts index bae4896..9fbbc57 100644 --- a/src/__version__.ts +++ b/src/__version__.ts @@ -10,17 +10,7 @@ export class CONSTRAINTS { static readonly MAX_VERSION: string = "1"; /** - * Minimum browser releases required by newer Playwright versions, as [[major, minor], release]. - * Playwright 1.61 sends viewport fields that builds before beta.30 reject. + * Minimum version with Playwright 1.61+, which sends viewport fields that older builds reject. */ - static readonly PLAYWRIGHT_BROWSER_FLOORS: [[number, number], string][] = [ - [[1, 61], "beta.30"], - ]; - - static asRange(): string { - /** - * Returns the version range as a string. - */ - return `>=${CONSTRAINTS.MIN_VERSION}, <${CONSTRAINTS.MAX_VERSION}`; - } + static readonly PLAYWRIGHT_1_61_MIN_VERSION: string = "beta.30"; } diff --git a/src/pkgman.ts b/src/pkgman.ts index 8398f02..31caf8a 100644 --- a/src/pkgman.ts +++ b/src/pkgman.ts @@ -78,16 +78,15 @@ const LAUNCH_FILE: { [key: string]: string } = { lin: "camoufox-bin", }; -// The resolved playwright-core [major, minor], or undefined if it can't be read. -function playwrightVersion(): [number, number] | undefined { +function isPlaywright161OrNewer(): boolean { try { const { version } = createRequire(import.meta.url)( "playwright-core/package.json", ); const [major, minor] = version.split(".").map(Number); - return [major, minor]; + return major > 1 || (major === 1 && minor >= 61); } catch { - return undefined; + return false; } } @@ -150,19 +149,19 @@ class Version { } static buildMinMax(): [Version, Version] { - let min = new Version(CONSTRAINTS.MIN_VERSION); - const [major, minor] = playwrightVersion() ?? [0, 0]; - for (const [required, release] of CONSTRAINTS.PLAYWRIGHT_BROWSER_FLOORS) { - const floor = new Version(release); - const needsFloor = - major > required[0] || (major === required[0] && minor >= required[1]); - if (needsFloor && min.lessThan(floor)) min = floor; - } - return [min, new Version(CONSTRAINTS.MAX_VERSION)]; + return [ + new Version( + isPlaywright161OrNewer() + ? CONSTRAINTS.PLAYWRIGHT_1_61_MIN_VERSION + : CONSTRAINTS.MIN_VERSION, + ), + new Version(CONSTRAINTS.MAX_VERSION), + ]; } } const [VERSION_MIN, VERSION_MAX] = Version.buildMinMax(); +const SUPPORTED_RANGE = `>=${VERSION_MIN.release}, <${VERSION_MAX.release}`; export class GitHubDownloader { githubRepo: string; @@ -253,7 +252,7 @@ export class CamoufoxFetcher extends GitHubDownloader { missingAssetError(): void { throw new MissingRelease( - `No matching release found for ${OS_NAME} ${this.arch} in the supported range: (${CONSTRAINTS.asRange()}). Please update the library.`, + `No matching release found for ${OS_NAME} ${this.arch} in the supported range: (${SUPPORTED_RANGE}). Please update the library.`, ); } @@ -450,7 +449,7 @@ export function camoufoxPath(downloadIfMissing: boolean = true): PathLike { return INSTALL_DIR; } else { throw new UnsupportedVersion( - "Camoufox executable is outdated. Please run `camoufox fetch` to update.", + `Camoufox executable is outdated (supported range: ${SUPPORTED_RANGE}). Please run \`camoufox fetch\` to update.`, ); } diff --git a/test/pkgman.test.ts b/test/pkgman.test.ts index 3972403..7d4986b 100644 --- a/test/pkgman.test.ts +++ b/test/pkgman.test.ts @@ -215,7 +215,7 @@ describe("camoufoxPath browser floor", () => { fs.rmSync(installDir, { recursive: true, force: true }); }); - async function camoufoxPathWith(playwright: string, release: string) { + async function loadCamoufoxPath(playwright: string, release: string) { fs.writeFileSync( path.join(installDir, "version.json"), JSON.stringify({ version: "152.0.4", release }), @@ -225,19 +225,19 @@ describe("camoufoxPath browser floor", () => { createRequire: () => () => ({ version: playwright }), })); vi.resetModules(); - const { camoufoxPath } = await import("../src/pkgman"); - return () => camoufoxPath(false); + return (await import("../src/pkgman")).camoufoxPath; } - test("accepts older builds below Playwright 1.61", async () => { - expect((await camoufoxPathWith("1.60.0", "beta.29"))()).toBe(installDir); + test.each([ + ["1.60.0", "beta.29"], + ["1.62.1", "beta.30"], + ])("accepts Playwright %s with %s", async (playwright, release) => { + const camoufoxPath = await loadCamoufoxPath(playwright, release); + expect(camoufoxPath(false)).toBe(installDir); }); test("rejects builds below beta.30 from Playwright 1.61", async () => { - expect(await camoufoxPathWith("1.61.0", "beta.29")).toThrow("outdated"); - }); - - test("accepts beta.30 from Playwright 1.61", async () => { - expect((await camoufoxPathWith("1.62.1", "beta.30"))()).toBe(installDir); + const camoufoxPath = await loadCamoufoxPath("1.61.0", "beta.29"); + expect(() => camoufoxPath(false)).toThrow(">=beta.30"); }); }); From 9fd55ea4762a296698c67b9375f1997f13e812fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jind=C5=99ich=20B=C3=A4r?= Date: Mon, 5 Oct 2026 10:44:30 +0200 Subject: [PATCH 3/3] fix: reject Firefox 156 Camoufox builds the library cannot configure yet --- src/__version__.ts | 5 ++++- src/pkgman.ts | 2 +- test/pkgman.test.ts | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/src/__version__.ts b/src/__version__.ts index 9fbbc57..d958db1 100644 --- a/src/__version__.ts +++ b/src/__version__.ts @@ -7,7 +7,10 @@ export class CONSTRAINTS { * The minimum and maximum supported versions of the Camoufox browser. */ static readonly MIN_VERSION: string = "alpha.1"; - static readonly MAX_VERSION: string = "1"; + /** + * Exclusive. The Firefox 156 builds (beta.32+) dropped properties this library still generates. + */ + static readonly MAX_VERSION: string = "beta.32"; /** * Minimum version with Playwright 1.61+, which sends viewport fields that older builds reject. diff --git a/src/pkgman.ts b/src/pkgman.ts index 31caf8a..8f2d511 100644 --- a/src/pkgman.ts +++ b/src/pkgman.ts @@ -449,7 +449,7 @@ export function camoufoxPath(downloadIfMissing: boolean = true): PathLike { return INSTALL_DIR; } else { throw new UnsupportedVersion( - `Camoufox executable is outdated (supported range: ${SUPPORTED_RANGE}). Please run \`camoufox fetch\` to update.`, + `Camoufox v${installedVerStr()} is not supported by this library (supported range: ${SUPPORTED_RANGE}). Please run \`camoufox fetch\` to install a supported version.`, ); } diff --git a/test/pkgman.test.ts b/test/pkgman.test.ts index 7d4986b..d441909 100644 --- a/test/pkgman.test.ts +++ b/test/pkgman.test.ts @@ -240,4 +240,37 @@ describe("camoufoxPath browser floor", () => { const camoufoxPath = await loadCamoufoxPath("1.61.0", "beta.29"); expect(() => camoufoxPath(false)).toThrow(">=beta.30"); }); + + test("rejects Firefox 156 builds", async () => { + const camoufoxPath = await loadCamoufoxPath("1.62.1", "beta.34"); + expect(() => camoufoxPath(false)).toThrow(" { + afterEach(() => { + vi.unstubAllGlobals(); + }); + + test("skips releases newer than the supported range", async () => { + const { CamoufoxFetcher, OS_NAME } = await import("../src/pkgman"); + const suffix = `${OS_NAME}.${CamoufoxFetcher.getPlatformArch()}.zip`; + const asset = (name: string) => ({ + name, + browser_download_url: `https://example.com/${name}`, + }); + vi.stubGlobal( + "fetch", + vi.fn(async () => ({ + ok: true, + json: async () => [ + { assets: [asset(`camoufox-156.0.1-beta.34-${suffix}`)] }, + { assets: [asset(`camoufox-152.0.4-beta.30-${suffix}`)] }, + ], + })), + ); + const fetcher = new CamoufoxFetcher(); + await fetcher.init(); + expect(fetcher.verstr).toBe("152.0.4-beta.30"); + }); });