mirror of
https://github.com/Hestia-Homes/assessment-model.git
synced 2026-08-02 12:58:24 +00:00
chore(devcontainer): add Playwright with chromium and a headed noVNC viewer
Mirrors the Model devcontainer's Playwright setup for this Next.js repo, so agents and humans can drive the running app in a browser. The image bakes in chromium's apt libraries (as root) and the browser binary (as vscode, so it lands in that user's cache), meaning a fresh container is ready to run with no first-use download. The @playwright/test version and the Dockerfile's PLAYWRIGHT_VERSION are pinned to the same number because Playwright ties browser builds to a library version. Also ports Model's headed viewer stack (Xvfb -> fluxbox -> x11vnc -> noVNC on :6080) as scripts/start_viewer.sh, for watching a headed run. Cypress is untouched; Playwright specs live in e2e/. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
769030ab49
commit
b914aca3ee
9 changed files with 277 additions and 1 deletions
|
|
@ -5,6 +5,11 @@ ARG USER_UID=1000
|
|||
ARG USER_GID=1000
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# Keep in lockstep with "@playwright/test" in package.json. Playwright pins its
|
||||
# browser build to the library version, so a mismatch makes the baked-in
|
||||
# chromium invisible to the tests ("Executable doesn't exist").
|
||||
ARG PLAYWRIGHT_VERSION=1.61.1
|
||||
|
||||
# Base CLI tooling (sudo, git, ripgrep/fd for editors, etc.).
|
||||
RUN apt update && apt install -y --no-install-recommends \
|
||||
sudo jq vim curl bash-completion \
|
||||
|
|
@ -35,6 +40,22 @@ RUN curl -fsSL https://github.com/neovim/neovim/releases/latest/download/nvim-li
|
|||
| tar -xz -C /opt \
|
||||
&& ln -s /opt/nvim-linux-x86_64/bin/nvim /usr/local/bin/nvim
|
||||
|
||||
# Playwright's OS-level libraries for chromium (fonts, libnss, libasound, ...).
|
||||
# The npm package is a devDependency; this only pulls the apt side, which needs
|
||||
# root. The browser binary itself is fetched as ${USER} further down so it lands
|
||||
# in that user's cache, which is where the tests run from.
|
||||
RUN npx --yes playwright@${PLAYWRIGHT_VERSION} install-deps chromium \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Headed-browser viewer stack, mirroring the Model devcontainer. Pipeline:
|
||||
# Xvfb (virtual :99 screen) -> fluxbox (window manager) -> x11vnc (VNC on 5900)
|
||||
# -> novnc/websockify (desktop in a browser tab on 6080). Lets you watch and
|
||||
# click a headed Playwright run; see scripts/start_viewer.sh. Headless runs
|
||||
# don't need any of this.
|
||||
RUN apt update && apt install -y --no-install-recommends \
|
||||
xvfb fluxbox x11vnc novnc websockify \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
USER ${USER}
|
||||
|
||||
# LazyVim starter config (.git stripped so the user owns the files).
|
||||
|
|
@ -45,6 +66,12 @@ RUN git clone https://github.com/LazyVim/starter /home/${USER}/.config/nvim \
|
|||
RUN curl -fsSL https://claude.ai/install.sh | bash
|
||||
ENV PATH="/home/${USER}/.local/bin:${PATH}"
|
||||
|
||||
# Chromium browser binary, baked into the image so a fresh container is ready to
|
||||
# run without a several-hundred-MB download on first use. Cached under
|
||||
# /home/${USER}/.cache/ms-playwright, which (unlike the workspace) isn't a bind
|
||||
# mount, so it survives as part of the image.
|
||||
RUN npx --yes playwright@${PLAYWRIGHT_VERSION} install chromium
|
||||
|
||||
USER root
|
||||
|
||||
WORKDIR /workspaces/assessment-model
|
||||
|
|
|
|||
|
|
@ -16,7 +16,17 @@
|
|||
// trial for the home-page redesign; drop its `skills add` step to remove it.
|
||||
"postCreateCommand": "gh repo clone Hestia-Homes/agentic-toolkit /tmp/agentic-toolkit -- --branch 0.0.8 --depth 1 && bash /tmp/agentic-toolkit/setup.sh && npx --yes skills@latest add pbakaus/impeccable --agent claude-code --copy --global --yes && npm install",
|
||||
|
||||
"forwardPorts": ["frontend:3000", "pgadmin:80"],
|
||||
"forwardPorts": ["frontend:3000", "pgadmin:80", "frontend:6080"],
|
||||
|
||||
"portsAttributes": {
|
||||
"frontend:3000": {
|
||||
"label": "Next.js"
|
||||
},
|
||||
"frontend:6080": {
|
||||
"label": "Playwright noVNC desktop",
|
||||
"onAutoForward": "silent"
|
||||
}
|
||||
},
|
||||
|
||||
"mounts": [
|
||||
// Optional, just makes getting from Downloads (local env) easier
|
||||
|
|
|
|||
6
.gitignore
vendored
6
.gitignore
vendored
|
|
@ -34,6 +34,12 @@ cypress.env.json
|
|||
# vercel
|
||||
.vercel
|
||||
|
||||
# playwright run artifacts
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/blob-report/
|
||||
/playwright/.cache/
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
|
|
|||
30
README.md
30
README.md
|
|
@ -123,6 +123,36 @@ the JWT, and avoid the functionality defined in the signIn function as defined i
|
|||
|
||||
**cypress/support/commands.ts** creates a custom login user command which sets a JWT and allows us to actually authenticate. `cy.intercept` only mocks the client side behaviour of the apis and therefore does not set any cookies, do this function does this manually.
|
||||
|
||||
# Playwright
|
||||
|
||||
Playwright sits alongside Cypress rather than replacing it. Specs live in `e2e/`
|
||||
(Cypress keeps `cypress/e2e/`), and `playwright.config.ts` boots `next dev` for
|
||||
you unless something is already serving http://localhost:3000.
|
||||
|
||||
```bash
|
||||
npm run test:playwright
|
||||
```
|
||||
|
||||
The devcontainer image bakes in chromium and its OS libraries, so there is
|
||||
nothing to install on a fresh container. Chromium is the only browser baked in —
|
||||
adding firefox or webkit to the config means adding them to
|
||||
`.devcontainer/Dockerfile` too. The `@playwright/test` version in `package.json`
|
||||
and `PLAYWRIGHT_VERSION` in that Dockerfile are pinned to the same number on
|
||||
purpose: Playwright ties each browser build to a library version, so if they
|
||||
drift, runs fail with "Executable doesn't exist".
|
||||
|
||||
## Watching the browser
|
||||
|
||||
Runs are headless by default. To watch one drive, start the virtual desktop
|
||||
(Xvfb -> fluxbox -> x11vnc -> noVNC, mirroring the Model devcontainer), open the
|
||||
forwarded port 6080 in your browser, then run headed against that display:
|
||||
|
||||
```bash
|
||||
bash scripts/start_viewer.sh
|
||||
DISPLAY=:99 npm run test:playwright:headed
|
||||
bash scripts/start_viewer.sh stop
|
||||
```
|
||||
|
||||
# Generating pre-signed urls
|
||||
|
||||
In our terraform stack, we have a module called `s3_presignable_bucket` which contains the definition for our bucket which we will use to store retrofit plan input csv's in.
|
||||
|
|
|
|||
12
e2e/smoke.spec.ts
Normal file
12
e2e/smoke.spec.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import { expect, test } from "@playwright/test";
|
||||
|
||||
// Deliberately auth-agnostic: src/middleware.ts bounces unauthenticated
|
||||
// visitors to a sign-in page, so this asserts only that the app boots and
|
||||
// serves a page rather than erroring — enough to prove the devcontainer's
|
||||
// Playwright + chromium wiring works end to end.
|
||||
test("the app serves the home route", async ({ page }) => {
|
||||
const response = await page.goto("/");
|
||||
|
||||
expect(response?.status()).toBeLessThan(500);
|
||||
await expect(page).toHaveTitle(/.+/);
|
||||
});
|
||||
64
package-lock.json
generated
64
package-lock.json
generated
|
|
@ -74,6 +74,7 @@
|
|||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "1.61.1",
|
||||
"@tailwindcss/forms": "^0.5.10",
|
||||
"@testing-library/cypress": "^10.0.3",
|
||||
"@types/node": "^24.10.1",
|
||||
|
|
@ -3236,6 +3237,22 @@
|
|||
"integrity": "sha512-8awtpHXCx/bNpFt4mt2xdkgtgVvKqty8VbjHI/WWWQuEw+KLzFot3f4+LkQY9YmOtq7A5GdOnqoIC8Pdygjk2g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@playwright/test": {
|
||||
"version": "1.61.1",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.61.1.tgz",
|
||||
"integrity": "sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright": "1.61.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@puppeteer/browsers": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.3.0.tgz",
|
||||
|
|
@ -12745,6 +12762,53 @@
|
|||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.61.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.1.tgz",
|
||||
"integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.61.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.61.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz",
|
||||
"integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright/node_modules/fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/possible-typed-array-names": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@
|
|||
"test:watch": "vitest",
|
||||
"test:e2e:open": "start-server-and-test dev http://localhost:3000 \"cypress open --e2e\"",
|
||||
"test:e2e:run": "cypress run",
|
||||
"test:playwright": "playwright test",
|
||||
"test:playwright:headed": "playwright test --headed",
|
||||
"migration:generate": "drizzle-kit generate",
|
||||
"migration:migrate": "drizzle-kit migrate",
|
||||
"create_user": "tsx src/app/db/create_user.ts",
|
||||
|
|
@ -86,6 +88,7 @@
|
|||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "1.61.1",
|
||||
"@tailwindcss/forms": "^0.5.10",
|
||||
"@testing-library/cypress": "^10.0.3",
|
||||
"@types/node": "^24.10.1",
|
||||
|
|
|
|||
33
playwright.config.ts
Normal file
33
playwright.config.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import { defineConfig, devices } from "@playwright/test";
|
||||
|
||||
// Cypress still owns the suites under cypress/e2e; Playwright specs live in
|
||||
// e2e/ so the two don't collide.
|
||||
export default defineConfig({
|
||||
testDir: "./e2e",
|
||||
fullyParallel: true,
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
reporter: process.env.CI ? "list" : "html",
|
||||
|
||||
use: {
|
||||
baseURL: process.env.PLAYWRIGHT_BASE_URL ?? "http://localhost:3000",
|
||||
trace: "on-first-retry",
|
||||
screenshot: "only-on-failure",
|
||||
},
|
||||
|
||||
// Chromium only: it's the single browser baked into the devcontainer image
|
||||
// (see .devcontainer/Dockerfile). Adding firefox/webkit here means adding
|
||||
// them there too, or runs fail with "Executable doesn't exist".
|
||||
projects: [
|
||||
{ name: "chromium", use: { ...devices["Desktop Chrome"] } },
|
||||
],
|
||||
|
||||
// Boot `next dev` unless something is already serving the base URL, so a dev
|
||||
// server you already have running gets reused rather than fought over.
|
||||
webServer: {
|
||||
command: "npm run dev",
|
||||
url: "http://localhost:3000",
|
||||
reuseExistingServer: !process.env.CI,
|
||||
timeout: 120_000,
|
||||
},
|
||||
});
|
||||
91
scripts/start_viewer.sh
Executable file
91
scripts/start_viewer.sh
Executable file
|
|
@ -0,0 +1,91 @@
|
|||
#!/usr/bin/env bash
|
||||
# Stand up a *viewable* desktop inside the dev container so you can watch and
|
||||
# click a headed Playwright browser. Pipeline:
|
||||
#
|
||||
# Xvfb (:99 virtual screen) -> fluxbox (window manager)
|
||||
# -> x11vnc (exposes :99 over VNC on 5900)
|
||||
# -> websockify/noVNC (serves VNC in a browser tab on 6080)
|
||||
#
|
||||
# Then run Playwright headed with DISPLAY=:99 and the browser appears on that
|
||||
# desktop:
|
||||
#
|
||||
# bash scripts/start_viewer.sh # start it
|
||||
# DISPLAY=:99 npx playwright test --headed # watch it drive
|
||||
# bash scripts/start_viewer.sh stop # tear it down
|
||||
#
|
||||
# Headless runs (the default) need none of this. Idempotent: re-running won't
|
||||
# stack duplicate servers.
|
||||
set -euo pipefail
|
||||
|
||||
DISPLAY_NUM=":99"
|
||||
SCREEN="1600x1000x24"
|
||||
VNC_PORT=5900
|
||||
WEB_PORT=6080
|
||||
RUN_DIR="/tmp/pw-viewer"
|
||||
mkdir -p "$RUN_DIR"
|
||||
|
||||
_pids() { pgrep -f "$1" 2>/dev/null || true; }
|
||||
|
||||
stop() {
|
||||
echo "Stopping viewer..."
|
||||
for pat in "websockify.*$WEB_PORT" "x11vnc.*$DISPLAY_NUM" "fluxbox" "Xvfb $DISPLAY_NUM"; do
|
||||
for pid in $(_pids "$pat"); do kill "$pid" 2>/dev/null || true; done
|
||||
done
|
||||
rm -f "/tmp/.X${DISPLAY_NUM#:}-lock" 2>/dev/null || true
|
||||
echo "Stopped."
|
||||
}
|
||||
|
||||
start() {
|
||||
# Xvfb — the virtual screen everything draws into.
|
||||
if [ -z "$(_pids "Xvfb $DISPLAY_NUM")" ]; then
|
||||
Xvfb "$DISPLAY_NUM" -screen 0 "$SCREEN" -ac >"$RUN_DIR/xvfb.log" 2>&1 &
|
||||
sleep 1
|
||||
echo "Xvfb up on $DISPLAY_NUM ($SCREEN)"
|
||||
else
|
||||
echo "Xvfb already running on $DISPLAY_NUM"
|
||||
fi
|
||||
|
||||
# fluxbox — a window manager, so the browser window is movable/resizable.
|
||||
if [ -z "$(_pids "fluxbox")" ]; then
|
||||
DISPLAY="$DISPLAY_NUM" fluxbox >"$RUN_DIR/fluxbox.log" 2>&1 &
|
||||
sleep 1
|
||||
echo "fluxbox window manager up"
|
||||
fi
|
||||
|
||||
# x11vnc — exposes the screen over VNC. -forever keeps it alive across client
|
||||
# reconnects; -nopw is fine because the port is only reachable through the
|
||||
# container's forwarded port, not the public internet.
|
||||
if [ -z "$(_pids "x11vnc.*$DISPLAY_NUM")" ]; then
|
||||
x11vnc -display "$DISPLAY_NUM" -rfbport "$VNC_PORT" -forever -shared \
|
||||
-nopw -quiet -bg -o "$RUN_DIR/x11vnc.log"
|
||||
echo "x11vnc up on VNC port $VNC_PORT"
|
||||
else
|
||||
echo "x11vnc already running"
|
||||
fi
|
||||
|
||||
# noVNC/websockify — serves the VNC stream as a web page on WEB_PORT.
|
||||
if [ -z "$(_pids "websockify.*$WEB_PORT")" ]; then
|
||||
websockify --web=/usr/share/novnc "$WEB_PORT" "localhost:$VNC_PORT" \
|
||||
>"$RUN_DIR/websockify.log" 2>&1 &
|
||||
sleep 1
|
||||
echo "noVNC up on web port $WEB_PORT"
|
||||
fi
|
||||
|
||||
cat <<EOF
|
||||
|
||||
Desktop ready.
|
||||
1. Open the forwarded port $WEB_PORT in your browser:
|
||||
http://localhost:$WEB_PORT/vnc.html?autoconnect=1&resize=remote
|
||||
(In VS Code: PORTS tab -> open $WEB_PORT in browser.)
|
||||
2. Run Playwright headed, pointing at this display:
|
||||
DISPLAY=$DISPLAY_NUM npx playwright test --headed
|
||||
The browser window appears on the desktop from step 1.
|
||||
EOF
|
||||
}
|
||||
|
||||
case "${1:-start}" in
|
||||
start) start ;;
|
||||
stop) stop ;;
|
||||
restart) stop; start ;;
|
||||
*) echo "usage: $0 [start|stop|restart]"; exit 2 ;;
|
||||
esac
|
||||
Loading…
Add table
Reference in a new issue