Skip to content

chef test unit resolves playwright from chef's own node_modules, chef test e2e from the project's #8

Description

@compote2

Summary

chef test unit resolves playwright from chef's own (global) node_modules, while chef test e2e runs the project's Playwright. When the two versions expect different browser revisions, e2e passes and unit dies with a missing-browser error — in the same repo, on the same machine, at the same moment.

Where the two strategies diverge

src/modules/engines/test/e2e/playwright/playwright-e2e-strategy.ts hands the run to the project:

const childProcess = spawn("npx", args, {
  cwd: options.projectRoot,
  ...
});

src/modules/engines/test/unit/playwright/playwright-unit-strategy.ts imports Playwright into chef's own process:

const playwright = await import("playwright");
const browserLauncher = playwright[browserType];

Being a bare specifier inside @bitrix/chef/dist/cli.js, that resolves from chef's install directory, never from projectRoot.

This is inconsistent with what the same method already does a few lines above — it locates and reads the project's playwright.config.ts (findPlaywrightConfig(packageRoot, projectRoot)) and requires use.baseURL from it. So the unit strategy already treats the project's Playwright setup as authoritative for configuration, but not for the Playwright it actually launches.

Why the versions drift apart in practice

They are not pinned together, and nothing keeps them in step:

spec resolved chromium revision
project @playwright/test ^1.58.0, pinned by package-lock.json 1.61.1 1228
chef (global) playwright ^1.57.0, no lockfile 1.62.1 1234

A global npm install has no lockfile, so chef's caret resolves to whatever is newest on the registry at install time, while the project's caret stays frozen by its lock.

Our repo's postinstall makes it systematic rather than accidental:

"postinstall": "npm install --global @bitrix/chef && npx playwright install"

The first half floats chef — and chef's bundled Playwright — forward. The second half resolves the project's binary (node_modules/.bin/playwright -> ../@playwright/test/cli.js) and therefore downloads browsers only for the project's pinned version. Nothing ever installs browsers for chef's copy, so every npm install widens the gap.

The same asymmetry also makes the failure hard to diagnose: the obvious remedy, npx playwright install chromium, is a no-op here — it re-installs the revision that is already present.

Reproduction

  1. In a project pinning @playwright/test to a version whose chromium revision is already installed (here 1.61.1 → 1228).
  2. Install a chef whose bundled Playwright wants a newer revision (here chef 1.21.0 → playwright 1.62.1 → 1234), without installing browsers for it.
  3. chef test e2e <ext> <spec>.spec.ts — passes.
  4. chef test unit <ext> — fails:
[CF9002] browserType.launch: Executable doesn't exist at
/Users/…/Library/Caches/ms-playwright/chromium_headless_shell-1234/chrome-headless-shell-mac-arm64/chrome-headless-shell
╔════════════════════════════════════════════════════════════╗
║ Looks like Playwright was just installed or updated.       ║
║ Please run the following command to download new browsers: ║
║     npx playwright install                                 ║
╚════════════════════════════════════════════════════════════╝

The suggested command does not help, for the reason above.

Environment: chef 1.21.0, node 22, macOS arm64.

Suggested fix

Resolve playwright from projectRoot first, falling back to chef's bundled copy when the project has none — mirroring what the e2e strategy already achieves by spawning npx in the project directory. createRequire is already used elsewhere in the codebase:

import { createRequire } from 'node:module';
import { pathToFileURL } from 'node:url';

async function importPlaywright(projectRoot) {
  try {
    const require = createRequire(pathToFileURL(path.join(projectRoot, 'package.json')));

    return await import(pathToFileURL(require.resolve('playwright')).href);
  } catch {
    return import('playwright'); // chef's own copy
  }
}

That way a project that declares Playwright gets one Playwright, one browser set, and one npx playwright install that covers both runners.

A smaller alternative, if importing the project's copy is undesirable: detect the mismatch and say so. Comparing chef's playwright-core/browsers.json revision against ~/Library/Caches/ms-playwright would let CF9002 explain that the browsers belong to chef's Playwright and name the command that actually installs them, instead of suggesting a npx playwright install that resolves elsewhere.

Workaround for anyone hitting this

Either install browsers for chef's own copy:

"$(npm root -g)/@bitrix/chef/node_modules/.bin/playwright" install chromium

or, without downloading anything, point PLAYWRIGHT_BROWSERS_PATH at a directory that symlinks the wanted revision onto an installed neighbouring one — adjacent revisions are compatible enough for a full run.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions