End-to-end testing playbook
Hands-on guide for driving the real RoqueOS UI (not unit-test stubs) with Playwright. Covers Chrome CDP attach, Firebase test accounts, server bootstrap, and the gotchas that caught us during the 2026-05 Music app E2E session.
The internal reference companion lives at
.claude/memory/e2e_testing_playbook.mdand stays in lockstep with this file. If you change one, update the other.
When to use this
- You need to prove a feature end-to-end under a real browser. Unit tests with mocked stores miss the surprises that real network, COOP headers, Firebase Auth gates, and Vue runtime walks expose.
- Examples that paid off: Music app playback over SMB, Finder share links, terminal connect, container proxy URLs, RoqueClaw VNC.
If the workflow is purely unit-scoped (a pure function, a store mutation), prefer yarn test:unit — faster signal and CI-friendly. E2E is the heavy artillery.
Prerequisites
| Tool | Why | Where |
|---|---|---|
| Node 22+, Yarn 1.22+ | Run dev servers | package.json engines |
roqueos-front on :9200 | PORT=9200 yarn dev | Quasar dev + VitePress docs concurrent |
roqueos-server on :27021 | yarn start:dev | NestJS hot-reload |
ENCRYPTION_KEY fixed | Otherwise SMB/SFTP/RDP passwords go indecipherable on restart — see .env.example | roqueos-server/.env |
| Playwright | Installed at roqueos-server/node_modules/playwright (server dep) | Reuse from there; no need to install on front |
| Chrome 100+ | The CDP attach target. Use the system Chrome with your normal profile (Google sign-in already done) | C:\Program Files\Google\Chrome\Application\chrome.exe |
gcloud CLI | Used to bypass email verification via Identity Toolkit Admin API (Melhoria #16 + ADC) | ~/AppData/Local/Google/Cloud SDK/google-cloud-sdk/bin/gcloud.cmd |
Step 1 — Launch Chrome with CDP attachable
# 1. Kill all Chrome windows (CDP requires the profile to be unlocked).
powershell -Command "Get-Process chrome -ErrorAction SilentlyContinue | Stop-Process -Force"
# 2. Relaunch Chrome with --remote-debugging-port pointing at the real
# user profile so Google session/cookies are preserved.
powershell -Command "Start-Process 'C:\Program Files\Google\Chrome\Application\chrome.exe' -ArgumentList '--remote-debugging-port=9222', '--user-data-dir=C:\Users\<you>\AppData\Local\Google\Chrome\User Data', '--profile-directory=Default', '--no-first-run', '--no-default-browser-check', 'http://localhost:9200/app'"
# 3. Verify CDP is up
curl -sf http://localhost:9222/json/versionGotcha: --remote-debugging-port puts the profile into a debug sandbox; localStorage and IndexedDB (where Firebase Auth lives) are isolated from your normal Chrome session. You will need to register/log in again inside this Chrome instance, even with the same profile dir. This is a Chromium security mitigation, not a RoqueOS bug.
Step 2 — Create a test account (E2E-only)
The login screen registration wizard has 4 steps. Quirks that bit us:
- Step 1 (Dados): name must be letters only (Latin alphabet + accents). Dev mode also accepts digits — see P2 #10 fix. Email must be a real-looking TLD;
.test/.local/.exampleare rejected in production but accepted in dev mode (P2 #11 fix). Use@example.comto be safe across modes. - Step 2 (Senha): password validator checks 8+ chars, uppercase, lowercase, digit, special. Validator listens to
@inputevents so you must usepage.locator().fill()(which dispatches real input events) orsetter + dispatchEvent('input'). - Step 3 (Confirmar): toggle "Aceito termos" must be
aria-checked=true. Usepage.locator('.q-toggle').click()— manually settingaria-checkedwon't trigger the model. - Step 4 (Verificação): the user is created in Firebase Auth but
emailVerified=false. The verification email goes to@example.comwhich doesn't exist. Use Step 3 below to bypass.
Selector gotcha: the login screen and the register modal coexist in the DOM. Placeholders like "E-mail" exist twice. Scope your fills:
const modal = document.querySelector('.ros-register-modal')
const emailInput = modal.querySelector('input[placeholder="E-mail"]')Similarly, the submit button is .ros-register-modal__btn--primary:not([disabled]) — not match-by-text, because the login screen behind the modal also has a "Criar conta" button.
Step 3 — Bypass email verification
Two options:
Option A — VITE_SKIP_EMAIL_VERIFICATION=true (preferred for dev)
Set in roqueos-front/.env:
VITE_SKIP_EMAIL_VERIFICATION=trueAfter that, isEmailVerificationRequired() returns false and any account passes through. Never set in production.
Option B — Identity Toolkit Admin API (one-shot per account)
GCLOUD="/c/Users/<you>/AppData/Local/Google/Cloud SDK/google-cloud-sdk/bin/gcloud.cmd"
TOKEN=$("$GCLOUD" auth print-access-token)
LOCALID="<uid-from-signUp>"
curl -X POST "https://identitytoolkit.googleapis.com/v1/projects/roqueos/accounts:update" \
-H "Authorization: Bearer $TOKEN" \
-H "X-Goog-User-Project: roqueos" \
-H "Content-Type: application/json" \
-d "{\"localId\":\"$LOCALID\",\"emailVerified\":true}"The X-Goog-User-Project: roqueos header is required with ADC — without it you get 403 SERVICE_DISABLED.
Step 4 — Unlock the desktop
Even after auth + verify, rosStore.isLocked may stay true because the auto-unlock watch (P0 #3) only fires when both isAuthorized and bootComplete flip together. Force unlock from CDP if needed:
const ros = app.config.globalProperties.$pinia._s.get('roqueos')
if (ros.isLocked) ros.unlock()Step 5 — Connect localhost server
The store APIs:
const ros = app.config.globalProperties.$pinia._s.get('roqueos')
// P2 #14: accepts both positional and object signatures
const serviceId = await ros.addBackendService({
name: 'Localhost',
url: 'http://localhost:27021',
apiKey: '<your-admin-key>',
})
await ros.setApiCredentials('<key>', '<secret>', serviceId)
await ros.setActiveBackendService(serviceId)Step 6 — Inject apiService credentials
apiService is the front's HTTP wrapper. Without setCredentials(), requests miss X-API-Key/X-API-Secret headers and the server returns 401. Walk the Vue tree to find it:
for (const el of document.querySelectorAll('*')) {
const comp = el.__vueParentComponent
if (comp?.setupState) {
for (const k of Object.keys(comp.setupState)) {
const v = comp.setupState[k]
if (v?.setCredentials && v.request) {
v.setCredentials('<key>', '<secret>')
// found
}
}
}
}Step 7 — X-User-Id semantics
The front always sends X-User-Id: <firebase-uid>. Server uses it to scope user data when a shared admin API key serves multiple users.
Consequence: if you create a storage via curl without X-User-Id, it belongs to a UUID derived from the admin key, not your Firebase UID. The front will then lookup as Firebase UID → 404.
Always pass -H "X-User-Id: <your-firebase-uid>" when seeding test data via curl.
Step 8 — Drive the Music app (or any other)
Find the component's setupState via the DOM walk (el.__vueParentComponent) instead of recursive app._instance.subTree traversal — the latter is fragile across Vue minor versions.
function findMusicPlayer() {
for (const el of document.querySelectorAll('*')) {
const c = el.__vueParentComponent
if (c?.setupState?.musicLibrary && c.setupState?.fetchNetworkFiles) {
return c.setupState
}
}
return null
}
const state = findMusicPlayer()
const track = state.musicLibrary.sortedTracks.value[0]
await state.handlePlayTrack(track, 0)Windows-path gotcha for storages
JSON's \f, \r, \b, \n escapes mean C:\foo\bar saved verbatim corrupts to C: + control chars. The server now normalizes to C:/foo/bar on create/update (P0 #2). When passing paths from your own scripts, prefer forward slashes — Windows accepts them everywhere.
Reference scripts
tests/e2e/music-smb-playback.spec.js— full Music app flow over SMB (Melhoria #21).tmp-playwright/test-music-stream.cjs— API-only smoke (no UI), useful for nightly health-checks
Troubleshooting checklist
| Symptom | Likely cause |
|---|---|
isLocked: true after login | Auto-unlock watch not firing — call ros.unlock() manually |
| 404 on storage list with valid creds | Missing X-User-Id header, or storage owned by a different user |
| Music app shows folder with 0 tracks silently | Fixed (P1 #8): error notif now surfaces; check Notification Center |
| HLS endpoint returns 503 | ffmpeg not installed — front should hide HLS UI via /health/capabilities |
Many POST /api-keys/session-token per play | Fixed (P1 #7): networkStorageService now uses cached token |
Path corrupted on storage list (C:oqueos) | Fixed (P0 #2): self-heals on next server boot |
What this playbook does not cover
- Production smoke tests against
https://roqueos.com.br— use the/smoke-testskill (operator-only) - Cypress E2E in CI (
yarn test:e2e:ci) — different stack, different goals - Mobile (Capacitor) E2E — see
rules/wrappers/mobile.md