From 4d882b354b0bf847c00065829dd76fcfdbe11e64 Mon Sep 17 00:00:00 2001 From: Bob Killen Date: Sun, 11 Oct 2026 09:43:16 -0500 Subject: [PATCH] docs: document running the end-to-end suite without root CONTRIBUTING.md gave one way to get Playwright's OS packages, npx playwright install --with-deps chromium, which shells out to apt-get install and so needs root. On an unprivileged container or shared dev host the documented recipe cannot be followed, and the failure it leaves behind (browserType.launch: Target page, context or browser has been closed) names neither the missing library nor the install step. Add the non-root path beside it: install the browser bundle alone, apt-get download the same packages into a private sysroot with a private APT state dir, dpkg-deb -x them, and point LD_LIBRARY_PATH at the result. The package list covers both binaries Playwright unpacks (the 28 chrome-headless-shell needs and the 17 more the full chrome binary loads), with an ldd loop over both as the check, and notes for the two things that look like failures but are not. tests/contributing-e2e-setup.test.mjs treated any npx playwright install without --with-deps as incomplete unless the doc also named install-deps, which needs root just the same. It now also accepts a doc that unpacks the packages into a sysroot and hands the loader that directory, and a fixture test pins that neither half alone is enough. Verified on Ubuntu 26.04 as an unprivileged user: all 45 packages download, both binaries report 0 not found under ldd, and npm run test:e2e against a production build passes (312 passed, 30 skipped). Closes #1277 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Bob Killen --- CONTRIBUTING.md | 54 +++++++++++++++++++++++++++ tests/contributing-e2e-setup.test.mjs | 28 +++++++++++++- 2 files changed, 80 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 809e32d2..b231fb58 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -239,6 +239,60 @@ npx playwright install --with-deps chromium # once per machine: downloads the npm run test:e2e ``` +`--with-deps` runs `apt-get install`, so it needs root. Without the OS packages +the browser bundle is present but will not launch, and every test fails with +`browserType.launch: Target page, context or browser has been closed`, which +names neither the missing library nor the install step. On a machine where you +are not root (an unprivileged container, a shared dev host), unpack the same +packages into a private sysroot and point the loader at it instead. The browser +bundle itself installs fine as an ordinary user: + + + +```bash +npx playwright install chromium # the browser bundle only, no --with-deps +mkdir -p /tmp/sysroot/apt/{state,cache}/ /tmp/debs +printf 'Dir::State "/tmp/sysroot/apt/state";\nDir::State::status "/tmp/sysroot/apt/state/status";\nDir::Cache "/tmp/sysroot/apt/cache";\n' > /tmp/sysroot/apt.conf +touch /tmp/sysroot/apt/state/status +export APT_CONFIG=/tmp/sysroot/apt.conf +apt-get update +cd /tmp/debs && apt-get download \ + libglib2.0-0t64 libnspr4 libnss3 libdbus-1-3 libatk1.0-0t64 \ + libatk-bridge2.0-0t64 libatspi2.0-0t64 libx11-6 libxcomposite1 libxdamage1 \ + libxext6 libxfixes3 libxrandr2 libgbm1 libxcb1 libxkbcommon0 libasound2t64 \ + libpango-1.0-0 libcairo2 libcups2t64 libexpat1 libudev1 libdrm2 \ + libwayland-client0 libxrender1 libxau6 libxdmcp6 libxi6 \ + libavahi-common3 libavahi-client3 libpng16-16t64 libfontconfig1 libfreetype6 \ + libxcb-render0 libxcb-shm0 libpixman-1-0 libfribidi0 libthai0 libharfbuzz0b \ + libgraphite2-3 libthai-data libdatrie1 libbrotli1 libbsd0 libmd0 +for d in *.deb; do dpkg-deb -x "$d" /tmp/sysroot/fs; done +cd - && export LD_LIBRARY_PATH=/tmp/sysroot/fs/usr/lib/x86_64-linux-gnu:/tmp/sysroot/fs/lib/x86_64-linux-gnu +``` + + + +The first 28 names are the packages Playwright itself installs on Debian and +Ubuntu plus the four the `chrome-headless-shell` binary needs beyond them; the +last 17 are what the full `chrome` binary additionally loads, so a headed or +`channel: 'chromium'` run works too. Both binaries have to resolve, so check +both. Zero `not found` lines on each is the goal: + +```bash +for b in ~/.cache/ms-playwright/chromium*/*/chrome \ + ~/.cache/ms-playwright/chromium*/*/chrome-headless-shell; do + echo "$b: $(ldd "$b" | grep -c 'not found') missing" +done +``` + +Two things that look like failures and are not: `apt-get download` may print +`rm: cannot remove '/var/cache/apt/archives/partial/*.deb': Permission denied` +on a host whose `/var/cache/apt` is root-owned (the download still succeeds and +exits 0), and the `LD_LIBRARY_PATH` export is per shell, so set it again in a +new terminal before `npm run test:e2e`. Do not set `XDG_CACHE_HOME` for the run: +Playwright resolves the downloaded browser under it, and overriding it makes +every test fail with "Executable doesn't exist at ...". On an `arm64` host +substitute `aarch64-linux-gnu` for `x86_64-linux-gnu` in the loader path. + The suite starts its own server on `localhost:3000` (override with `E2E_PORT`) and serves the static output already built in `build/` via `docusaurus serve` — it does not rebuild your changes, so re-run `npm run build:production` after diff --git a/tests/contributing-e2e-setup.test.mjs b/tests/contributing-e2e-setup.test.mjs index ca95d14c..3bfd7986 100644 --- a/tests/contributing-e2e-setup.test.mjs +++ b/tests/contributing-e2e-setup.test.mjs @@ -50,6 +50,16 @@ function workflowRunSteps() { // naming the real cause. ci.yml never hits that because it always installs the // packages -- `--with-deps` on a cache miss, `install-deps` when the cached // bundle is restored. +// +// Both of those need root. The one documented way to get the packages without +// it is to unpack the same .debs into a private sysroot (`dpkg-deb -x`) and +// hand the loader that directory through LD_LIBRARY_PATH, which is the only +// context in which a bare `npx playwright install chromium` is complete. +function documentsSystemPackages(source) { + if (/npx playwright install-deps\b/.test(source)) return true; + return /dpkg-deb -x\b/.test(source) && /LD_LIBRARY_PATH=/.test(source); +} + test('the docs install Playwright the way ci.yml does, system packages included', () => { const workflowInstallsDeps = workflowRunSteps().some( (run) => @@ -68,10 +78,10 @@ test('the docs install Playwright the way ci.yml does, system packages included' // so it must not satisfy the browser-install match on its own. const installs = source.match(/npx playwright install(?!-deps)\b[^\n`]*/g); if (!installs) continue; - const mentionsStandaloneDeps = /npx playwright install-deps\b/.test(source); + const packagesDocumented = documentsSystemPackages(source); for (const invocation of installs) { if (invocation.includes('--with-deps')) continue; - if (mentionsStandaloneDeps) continue; + if (packagesDocumented) continue; incomplete.push(`${doc}: ${invocation.trim()}`); } } @@ -83,6 +93,20 @@ test('the docs install Playwright the way ci.yml does, system packages included' ); }); +test('a bare browser install is complete only beside a way to get the packages', () => { + assert.ok(documentsSystemPackages('npx playwright install-deps chromium')); + assert.ok( + documentsSystemPackages( + 'for d in *.deb; do dpkg-deb -x "$d" /tmp/sysroot/fs; done\nexport LD_LIBRARY_PATH=/tmp/sysroot/fs/usr/lib', + ), + ); + // Unpacking without pointing the loader at the result launches nothing, and + // naming the variable without unpacking anything has nothing to point at. + assert.ok(!documentsSystemPackages('dpkg-deb -x pkg.deb /tmp/sysroot/fs')); + assert.ok(!documentsSystemPackages('export LD_LIBRARY_PATH=/tmp/sysroot')); + assert.ok(!documentsSystemPackages('npx playwright install chromium')); +}); + // `report:e2e:coverage` is a gate, not a readout: the e2e-coverage job hands it // --check-source, --check-source-regions and --require-source-files, and // tests/e2e-coverage-gate.test.mjs holds floors under the first two. A recipe