diff --git a/.github/workflows/pr-validation.yml b/.github/workflows/pr-validation.yml index c6df716..8315212 100644 --- a/.github/workflows/pr-validation.yml +++ b/.github/workflows/pr-validation.yml @@ -107,6 +107,8 @@ jobs: baseImage: ubuntu:latest - features: vite-plus baseImage: mcr.microsoft.com/devcontainers/typescript-node:20 + - features: playwright-dev + baseImage: mcr.microsoft.com/devcontainers/typescript-node:20 - features: typescript-dev baseImage: mcr.microsoft.com/devcontainers/typescript-node:20 - features: auto-header diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 42025dc..a902e76 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -59,6 +59,10 @@ jobs: - features: vite-plus baseImage: mcr.microsoft.com/devcontainers/typescript-node:20 + # playwright-dev — requires Node.js (dependsOn typescript-dev) + - features: playwright-dev + baseImage: mcr.microsoft.com/devcontainers/typescript-node:20 + # typescript-dev — depends on essential-dev:1 via dependsOn (hard dep) - features: typescript-dev baseImage: mcr.microsoft.com/devcontainers/typescript-node:20 diff --git a/AGENTS.md b/AGENTS.md index 3cdc5db..14519ec 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -37,6 +37,7 @@ devcontainer features test . | `angular-dev` | 1.0.6 | Angular dev, port 4200 | | `vite-plus` | 1.0.3 | vp CLI, Oxlint/Oxfmt, Vitest | | `package-auto-install` | 1.0.7 | Auto-detect and install packages | +| `playwright-dev` | 1.0.0 | Playwright OS deps (Chromium/Firefox/WebKit) + shared browser-binary volume + VS Code extension | | `pnpm-store` | 1.0.4 | Shared pnpm store via Docker named volume (dependsOn helpers4-common) | | `auto-header` | — | LGPL-3.0 license headers | | `git-absorb` | 1.0.7 | git-absorb from GitHub releases | diff --git a/README.md b/README.md index 0990b32..8dbb03c 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,19 @@ Complete Vite+ toolchain setup with VS Code extensions (Oxc, Vitest), optimized [📖 Documentation](./src/vite-plus/README.md) +### playwright-dev + +OS-level dependencies for headless Chromium, Firefox, and WebKit, a browser-binary cache shared across rebuilds via a Docker named volume, and the official Playwright Test VS Code extension. + +**Key benefits:** +- OS packages installed once via the official `playwright install-deps` (no hand-maintained apt list) +- Browser binaries cached in a Docker volume — no re-download on every rebuild +- Official Playwright Test VS Code extension, pre-configured +- Doesn't install the `playwright` npm package itself — stays in sync with your project's own version +- Enables Chromium's CDP `WebAuthn.addVirtualAuthenticator` for passkey/WebAuthn testing without hardware + +[📖 Documentation](./src/playwright-dev/README.md) + ### package-auto-install Automatically detects and runs npm/yarn/pnpm install in non-interactive mode after container creation. Handles corepack setup for Node 24+ and intelligently detects the package manager from package.json or lockfiles. @@ -195,6 +208,7 @@ Features from this repository are available via GitHub Container Registry. Refer "ghcr.io/helpers4/devcontainer/essential-dev:1": {}, "ghcr.io/helpers4/devcontainer/github-dev:1": {}, "ghcr.io/helpers4/devcontainer/vite-plus:1": {}, + "ghcr.io/helpers4/devcontainer/playwright-dev:1": {}, "ghcr.io/helpers4/devcontainer/package-auto-install:1": {}, "ghcr.io/helpers4/devcontainer/pnpm-store:1": {}, "ghcr.io/helpers4/devcontainer/typescript-dev:1": {}, @@ -218,6 +232,7 @@ Features from this repository are available via GitHub Container Registry. Refer | [github-dev](./src/github-dev) | gh CLI, Copilot, PR & Issues, Actions, RemoteHub | [README](./src/github-dev/README.md) | | [auto-header](./src/auto-header) | Automatic file headers with customizable templates (simple or custom) | [README](./src/auto-header/README.md) | | [vite-plus](./src/vite-plus) | Complete Vite+ toolchain with Oxc, Vitest, and VS Code integration | [README](./src/vite-plus/README.md) | +| [playwright-dev](./src/playwright-dev) | Playwright OS deps (Chromium/Firefox/WebKit) + shared browser-binary volume + VS Code extension | [README](./src/playwright-dev/README.md) | | [package-auto-install](./src/package-auto-install) | Automatic package installation with corepack support for Node 24+ | [README](./src/package-auto-install/README.md) | | [pnpm-store](./src/pnpm-store) | Shared pnpm store on the same filesystem as your code — no stray .pnpm-store in repos | [README](./src/pnpm-store/README.md) | | [typescript-dev](./src/typescript-dev) | TypeScript/JavaScript dev with indexing and web tools (requires essential-dev) | [README](./src/typescript-dev/README.md) | diff --git a/scopes.json b/scopes.json index e12894b..3234cac 100644 --- a/scopes.json +++ b/scopes.json @@ -15,6 +15,7 @@ "mistral-dev", "package-auto-install", "peon-ping", + "playwright-dev", "pnpm-store", "shell-history-per-project", "typescript-dev", diff --git a/src/playwright-dev/README.md b/src/playwright-dev/README.md new file mode 100644 index 0000000..3d8d00a --- /dev/null +++ b/src/playwright-dev/README.md @@ -0,0 +1,151 @@ +# Playwright Development Environment (playwright-dev) + +OS-level dependencies for headless Chromium, Firefox, and WebKit, a browser-binary cache shared across rebuilds via a Docker named volume, and the official Playwright Test VS Code extension — pre-configured so `npx playwright test` and `npx playwright install` just work, without re-downloading browsers on every rebuild. + +## Why this feature exists + +Headless browser automation (Playwright, and anything built on Chromium's DevTools Protocol — including `WebAuthn.addVirtualAuthenticator`, useful for testing passkey/WebAuthn flows without physical hardware) needs a real browser binary plus a long list of OS shared libraries. `npx playwright install --with-deps` can fetch both, but: + +- it needs `sudo`/root at test-run time to apt-install system packages, +- it re-downloads the browser binaries into `~/.cache/ms-playwright` on every fresh container, since that path isn't normally persisted. + +This feature moves both steps into the devcontainer lifecycle: OS packages are installed once at image build time (via the official `playwright install-deps`, not a hand-maintained apt list that would drift across base-image OS versions), and the browser binaries are downloaded once into a named volume that survives rebuilds — the same shape as the `pnpm-store` feature. + +It deliberately does **not** install the `playwright` npm package itself — that stays a devDependency of the consuming project, so the CLI version always matches the project's own Playwright version instead of drifting from a separately-installed global one. + +### Alternative: Microsoft's prebuilt Playwright image + +Microsoft publishes `mcr.microsoft.com/playwright:v-`, a Docker image with browsers and OS deps already baked in — zero install time, nothing to cache. If your `devcontainer.json` doesn't need to compose with other `helpers4` features on top of your own base image, using that image directly as `"image"` is a legitimate, simpler alternative to this feature. The tradeoff: it pins your whole devcontainer to Microsoft's base image and its Playwright version/OS combination, rather than letting you add browser support to whatever base image and feature set (`typescript-dev`, `vite-plus`, `pnpm-store`, â€Ļ) you're already using — which is the reason this feature exists as a feature rather than a documentation note pointing at that image. + +## Usage + +Add this feature to your `devcontainer.json`: + +```json +{ + "features": { + "ghcr.io/helpers4/devcontainer/playwright-dev:1": {} + } +} +``` + +This will: +1. Install the OS packages required to run Chromium, Firefox, and WebKit headless +2. Set `PLAYWRIGHT_BROWSERS_PATH` to a Docker-volume-backed path shared across rebuilds +3. Download the browser binaries into that volume on first container start +4. Install the official Playwright Test VS Code extension, pre-configured + +### Chromium only + +If your project only needs Chromium (e.g. CDP-based WebAuthn testing), skip the Firefox/WebKit dependencies to keep the image smaller: + +```json +{ + "features": { + "ghcr.io/helpers4/devcontainer/playwright-dev:1": { + "browsers": "chromium" + } + } +} +``` + +## Options + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `browsers` | string (`chromium` \| `firefox` \| `webkit` \| `all`) | `all` | Which browser engine(s) to install OS-level dependencies for, and to pre-download into the shared cache. | +| `installDeps` | boolean | `true` | Install the OS packages required to run the selected browser(s) headless, via `playwright install-deps`. Disable if the base image already provides them. | + +## IDE support + +| Editor | Status | ID | +| ------ | ------ | -- | +| VS Code | ✅ | `ms-playwright.playwright` | +| Cursor | ✅ | `ms-playwright.playwright` (same registry as VS Code) | +| WebStorm / IntelliJ IDEA (2023.3+) | â„šī¸ | Playwright tests are recognized natively via the built-in Test Automation plugin — not something this feature installs, nothing to configure here. | +| Zed | 🔜 | no standard devcontainer customization format yet | + +## Browser cache volume + +Binaries live in a Docker named volume (`helpers4-playwright-browsers-${devcontainerId}`) mounted at `/usr/local/share/playwright-browsers`, exposed to every shell via `PLAYWRIGHT_BROWSERS_PATH`. A `postCreateCommand` guard script takes ownership of the volume at container creation and downloads the browsers only if they haven't been fetched yet for the current `browsers` selection — so a rebuild reuses what's already there instead of re-fetching. + +```bash +# Confirm what's cached +ls "$PLAYWRIGHT_BROWSERS_PATH" +``` + +## VS Code Extension Included + +### Playwright Test for VS Code (ms-playwright.playwright) +- Test explorer: run/debug Playwright tests from the sidebar +- Pick locators, record new tests (codegen), and view traces inline + +Settings applied: + +```json +{ + "playwright.reuseBrowser": true, + "playwright.showTrace": true +} +``` + +## Testing a WebAuthn/passkey flow without hardware + +Chromium's DevTools Protocol exposes `WebAuthn.addVirtualAuthenticator` — no extra software authenticator needed. With Playwright: + +```ts +const client = await context.newCDPSession(page); +await client.send("WebAuthn.enable"); +await client.send("WebAuthn.addVirtualAuthenticator", { + options: { + protocol: "ctap2", + transport: "internal", + hasResidentKey: true, + hasUserVerification: true, + isUserVerified: true, + }, +}); +``` + +## Works Great With + +```json +{ + "features": { + "ghcr.io/helpers4/devcontainer/playwright-dev:1": {}, + "ghcr.io/helpers4/devcontainer/typescript-dev:1": {}, + "ghcr.io/helpers4/devcontainer/vite-plus:1": {}, + "ghcr.io/helpers4/devcontainer/pnpm-store:1": {} + } +} +``` + +## Troubleshooting + +### Browsers didn't download on first start + +The guard script needs network access at `postCreateCommand` time. Re-run it manually, or install directly: + +```bash +npx playwright install +``` + +### `install-deps` fails on an unsupported base image + +`playwright install-deps` only knows the apt package names for the Debian/Ubuntu versions Playwright officially supports. Pin your base image to one of those, or set `installDeps: false` and install the equivalent packages yourself. + +## Links + +- **Playwright**: https://playwright.dev/ +- **VS Code extension**: https://marketplace.visualstudio.com/items?itemName=ms-playwright.playwright +- **CDP WebAuthn domain**: https://chromedevtools.github.io/devtools-protocol/tot/WebAuthn/ + +## Version History + +- **v1.0.2**: Fixed three review findings. (1) Both `npx playwright` calls dropped their `@latest` pin — it was forcing the newest registry release instead of letting npx resolve the project's own pinned `playwright` devDependency once one exists, which contradicted this feature's own stated goal of never drifting from the project's version. (2) `dependsOn` now also includes `ghcr.io/devcontainers/features/node:1` — `typescript-dev` alone does not install Node.js (its `install.sh` is a no-op beyond `essential-dev`), so the feature's own `npm not found` guard was not actually covered by the dependency it named. (3) The browser-cache guard now writes a completion marker (scoped to the current `browsers` selection) instead of treating "directory non-empty" as "fully downloaded" — an interrupted first download no longer gets stuck as a permanently broken, silently-skipped cache. +- **v1.0.1**: Added "IDE support" table (VS Code/Cursor/WebStorm/Zed) and documented the `mcr.microsoft.com/playwright` prebuilt-image alternative. No behavior change. +- **v1.0.0**: Initial release. + +## License + +LGPL-3.0 - See LICENSE file for details diff --git a/src/playwright-dev/devcontainer-feature.json b/src/playwright-dev/devcontainer-feature.json new file mode 100644 index 0000000..9301bff --- /dev/null +++ b/src/playwright-dev/devcontainer-feature.json @@ -0,0 +1,65 @@ +{ + "id": "playwright-dev", + "version": "1.0.0", + "name": "Playwright Development Environment", + "description": "Playwright E2E/browser-automation toolchain: OS-level dependencies for headless Chromium, Firefox, and WebKit (via the official `playwright install-deps`), a browser-binary cache shared across rebuilds through a Docker named volume, and the official Playwright Test VS Code extension.", + "documentationURL": "https://github.com/helpers4/devcontainer/tree/main/src/playwright-dev", + "licenseURL": "https://github.com/helpers4/devcontainer/blob/main/LICENSE", + "keywords": [ + "helpers4", + "playwright", + "e2e", + "chromium", + "firefox", + "webkit", + "testing", + "webauthn" + ], + "options": { + "browsers": { + "type": "string", + "enum": [ + "chromium", + "firefox", + "webkit", + "all" + ], + "default": "all", + "description": "Which browser engine(s) to install OS-level dependencies for, and to pre-download into the shared browser cache." + }, + "installDeps": { + "type": "boolean", + "default": true, + "description": "Install the OS packages required to run the selected browser(s) headless, via `playwright install-deps`. Disable if the base image already provides them." + } + }, + "containerEnv": { + "PLAYWRIGHT_BROWSERS_PATH": "/usr/local/share/playwright-browsers" + }, + "mounts": [ + { + "source": "helpers4-playwright-browsers-${devcontainerId}", + "target": "/usr/local/share/playwright-browsers", + "type": "volume" + } + ], + "postCreateCommand": "/usr/local/bin/devcontainer-playwright-browsers", + "customizations": { + "vscode": { + "extensions": [ + "ms-playwright.playwright" + ], + "settings": { + "playwright.reuseBrowser": true, + "playwright.showTrace": true + } + } + }, + "dependsOn": { + "ghcr.io/helpers4/devcontainer/typescript-dev:1": {}, + "ghcr.io/devcontainers/features/node:1": {} + }, + "installsAfter": [ + "ghcr.io/devcontainers/features/common-utils" + ] +} diff --git a/src/playwright-dev/install.sh b/src/playwright-dev/install.sh new file mode 100644 index 0000000..3a93969 --- /dev/null +++ b/src/playwright-dev/install.sh @@ -0,0 +1,209 @@ +#!/usr/bin/env bash + +# This file is part of helpers4. +# Copyright (C) 2025 baxyz +# SPDX-License-Identifier: LGPL-3.0-or-later +# +# Sets up a Playwright browser-automation environment: OS-level dependencies +# for headless Chromium/Firefox/WebKit (via the official `playwright +# install-deps`, rather than a hand-maintained apt package list that would +# drift across base-image OS versions), plus a postCreateCommand guard that +# downloads the browser binaries themselves into a Docker named volume shared +# across rebuilds — the same store-across-rebuilds shape as the pnpm-store +# feature, because /workspaces (and any other volume mount) isn't available +# yet at image build time, only once the container actually starts. +# +# Deliberately does NOT install the `playwright` npm package itself — that +# stays a devDependency of the consuming project, so the CLI version always +# matches the project's own Playwright version instead of drifting from a +# separately-installed global one. + +set -euo pipefail + +# Bootstrap helpers4 shared library. helpers4-common installs it; if running +# standalone (e.g. devcontainer features test), create it inline so the feature +# is self-contained without a GHCR pull. +if [ ! -f /usr/local/share/helpers4/common.sh ]; then + mkdir -p /usr/local/share/helpers4 + cat > /usr/local/share/helpers4/common.sh << 'H4_COMMON' +# shellcheck shell=bash +h4_detect_user() { + USERNAME="${USERNAME:-${_REMOTE_USER:-automatic}}" + if [ "${USERNAME}" = "auto" ] || [ "${USERNAME}" = "automatic" ]; then + USERNAME="" + local _uid1000 + _uid1000="$(awk -v val=1000 -F: '$3==val{print $1; exit}' /etc/passwd 2>/dev/null || true)" + local candidate + for candidate in vscode node codespace "${_uid1000}"; do + if [ -n "${candidate}" ] && id -u "${candidate}" >/dev/null 2>&1; then + USERNAME="${candidate}"; break + fi + done + [ -z "${USERNAME}" ] && USERNAME=root + elif [ "${USERNAME}" = "none" ] || ! id -u "${USERNAME}" >/dev/null 2>&1; then + USERNAME=root + fi + export USERNAME +} +h4_resolve_home() { + if [ "${USERNAME}" = "root" ]; then + USER_HOME=/root + else + USER_HOME="$(getent passwd "${USERNAME}" 2>/dev/null | cut -d: -f6)" + [ -n "${USER_HOME}" ] || USER_HOME="/home/${USERNAME}" + fi + export USER_HOME +} +h4_apt_update() { + if [ "$(find /var/lib/apt/lists -maxdepth 1 \( -name '*.lz4' -o -name '*.gz' \) 2>/dev/null | wc -l)" = "0" ]; then + apt-get update -y -q + fi +} +h4_ensure_packages() { + local missing=() pkg + for pkg in "$@"; do dpkg -s "${pkg}" >/dev/null 2>&1 || missing+=("${pkg}"); done + if [ "${#missing[@]}" -gt 0 ]; then + h4_apt_update + apt-get install -y -q --no-install-recommends "${missing[@]}" + fi +} +H4_COMMON +fi +# shellcheck source=/dev/null +. /usr/local/share/helpers4/common.sh + +echo "🔧 Setting up playwright-dev devcontainer feature..." + +# Get options +BROWSERS="${BROWSERS:-all}" +INSTALL_DEPS="${INSTALLDEPS:-true}" + +# Fixed path — kept in sync with "containerEnv" / "mounts" in devcontainer-feature.json. +BROWSERS_PATH="/usr/local/share/playwright-browsers" + +if [ "$(id -u)" -ne 0 ]; then + echo "❌ This script must be run as root." + exit 1 +fi + +h4_detect_user +h4_resolve_home +export DEBIAN_FRONTEND=noninteractive + +if ! command -v npm >/dev/null 2>&1; then + echo "❌ npm not found. Please ensure Node.js is installed first (this feature depends on typescript-dev)." + exit 1 +fi + +h4_ensure_packages ca-certificates + +# "all" installs deps for every browser Playwright supports; a single engine +# name restricts `install-deps` to that one. +browser_arg="" +if [ "${BROWSERS}" != "all" ]; then + browser_arg="${BROWSERS}" +fi + +if [ "${INSTALL_DEPS}" = "true" ]; then + echo "đŸ“Ļ Installing OS dependencies for Playwright browser(s): ${BROWSERS}..." + # No @latest pin: npx resolves the project's own devDependency version + # first if one is already installed, falling back to the newest release + # only when nothing local exists yet (e.g. here, at image build time, + # before the workspace is even mounted). + # shellcheck disable=SC2086 + if npx -y playwright install-deps ${browser_arg}; then + echo "✅ Playwright OS dependencies installed (${BROWSERS})" + else + echo "❌ Failed to install Playwright OS dependencies." + exit 1 + fi +else + echo "â„šī¸ Skipping OS dependency install (installDeps=false)." +fi + +# Pre-create the mountpoint during the image build, same rationale as +# pnpm-store's STORE_DIR: the named volume shadows it once the container +# starts, but a valid, correctly-owned directory must exist beforehand for +# any tool that touches it before the volume is attached. +mkdir -p "${BROWSERS_PATH}" || true +if [ "${USERNAME}" != "root" ]; then + USER_GROUP="$(id -gn "${USERNAME}" 2>/dev/null || echo "${USERNAME}")" + chown "${USERNAME}:${USER_GROUP}" "${BROWSERS_PATH}" 2>/dev/null || true +fi +echo " ✅ Created browser cache directory at ${BROWSERS_PATH}" + +# Install the postCreate guard script. It runs once the volume is mounted, so +# it can take ownership of the cache and download the actual browser binaries +# (the image-build step above only installs OS packages, not the browsers +# themselves — those need the volume, which isn't available until container +# start, exactly like pnpm-store's guard). +GUARD="/usr/local/bin/devcontainer-playwright-browsers" + +{ + cat <<'HEADER' +#!/usr/bin/env bash +# This file is part of helpers4. +# Copyright (C) 2025 baxyz +# SPDX-License-Identifier: LGPL-3.0-or-later +set -euo pipefail +HEADER + printf 'BROWSERS_PATH=%q\n' "${BROWSERS_PATH}" + printf 'BROWSER_ARG=%q\n' "${browser_arg}" +} > "${GUARD}" + +cat >> "${GUARD}" <<'EOF' + +echo "🎭 playwright-dev: ensuring browser cache at ${BROWSERS_PATH}" + +if [ ! -d "${BROWSERS_PATH}" ]; then + mkdir -p "${BROWSERS_PATH}" 2>/dev/null \ + || { command -v sudo >/dev/null 2>&1 && sudo mkdir -p "${BROWSERS_PATH}"; } \ + || { echo "❌ playwright-dev: could not create ${BROWSERS_PATH}"; exit 1; } +fi + +# Named volumes are created root-owned; hand the cache to the current user so +# Playwright's installer can write to it. +cache_owner="$(stat -c '%u' "${BROWSERS_PATH}" 2>/dev/null || echo 'unknown')" +if [ "${cache_owner}" != "$(id -u)" ]; then + if command -v sudo >/dev/null 2>&1; then + sudo chown -R "$(id -u):$(id -g)" "${BROWSERS_PATH}" \ + || echo "âš ī¸ playwright-dev: chown failed — Playwright may not be able to write to the cache" + else + echo "âš ī¸ playwright-dev: cache is owned by uid ${cache_owner} and sudo is unavailable; downloads will fail (EACCES)" + fi +fi + +# Download the actual browser binaries only if not already fetched for this +# browser selection. A completion marker (rather than "directory non-empty") +# is used so an interrupted first download gets retried on the next start +# instead of being silently treated as done forever; the marker is scoped +# per browser selection so switching the `browsers` option after a rebuild +# re-triggers a download instead of trusting a stale, incomplete cache. +MARKER="${BROWSERS_PATH}/.h4-installed-${BROWSER_ARG:-all}" +if [ ! -f "${MARKER}" ]; then + echo "đŸ“Ĩ playwright-dev: downloading browser binaries (${BROWSER_ARG:-all}) into ${BROWSERS_PATH}..." + # No @latest pin — see install.sh's install-deps step for why. + # shellcheck disable=SC2086 + if npx -y playwright install ${BROWSER_ARG}; then + touch "${MARKER}" + echo "✅ playwright-dev: browsers installed" + else + echo "âš ī¸ playwright-dev: browser download failed — check network access, or run 'npx playwright install' manually" + fi +else + echo "✅ playwright-dev: browser cache already populated, skipping download" +fi +EOF + +chmod +x "${GUARD}" +echo " ✅ Installed guard script at ${GUARD}" + +echo "" +echo "✅ playwright-dev feature installed successfully!" +echo "" +echo "📝 Browser binaries are cached in a Docker volume, shared across rebuilds:" +echo " PLAYWRIGHT_BROWSERS_PATH=${BROWSERS_PATH}" +echo "" +echo "🔗 Resources:" +echo " - Playwright: https://playwright.dev/" +echo " - VS Code extension: https://marketplace.visualstudio.com/items?itemName=ms-playwright.playwright" diff --git a/test/playwright-dev/test.sh b/test/playwright-dev/test.sh new file mode 100644 index 0000000..695c09e --- /dev/null +++ b/test/playwright-dev/test.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +# This file is part of helpers4. +# Copyright (C) 2025 baxyz +# SPDX-License-Identifier: LGPL-3.0-or-later + +# Test script for playwright-dev feature + +set -e + +# Source test framework +source dev-container-features-test-lib + +check "node is available" command -v node + +check "npm is available" command -v npm + +# The guard script (postCreateCommand) must be installed and executable — +# it's what actually downloads the browsers once the volume is mounted. +check "postCreate guard script is installed" test -x /usr/local/bin/devcontainer-playwright-browsers + +# OS dependencies for headless Chromium are installed by default (browsers=all). +# libnss3 is a good proxy: Chromium headless fails immediately without it. +check "libnss3 is installed (Chromium OS deps)" dpkg -s libnss3 + +reportResults