Skip to content

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.md and 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

ToolWhyWhere
Node 22+, Yarn 1.22+Run dev serverspackage.json engines
roqueos-front on :9200PORT=9200 yarn devQuasar dev + VitePress docs concurrent
roqueos-server on :27021yarn start:devNestJS hot-reload
ENCRYPTION_KEY fixedOtherwise SMB/SFTP/RDP passwords go indecipherable on restart — see .env.exampleroqueos-server/.env
PlaywrightInstalled 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 CLIUsed 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

bash
# 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/version

Gotcha: --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:

  1. 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 / .example are rejected in production but accepted in dev mode (P2 #11 fix). Use @example.com to be safe across modes.
  2. Step 2 (Senha): password validator checks 8+ chars, uppercase, lowercase, digit, special. Validator listens to @input events so you must use page.locator().fill() (which dispatches real input events) or setter + dispatchEvent('input').
  3. Step 3 (Confirmar): toggle "Aceito termos" must be aria-checked=true. Use page.locator('.q-toggle').click() — manually setting aria-checked won't trigger the model.
  4. Step 4 (Verificação): the user is created in Firebase Auth but emailVerified=false. The verification email goes to @example.com which 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:

js
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:

bash
VITE_SKIP_EMAIL_VERIFICATION=true

After that, isEmailVerificationRequired() returns false and any account passes through. Never set in production.

Option B — Identity Toolkit Admin API (one-shot per account)

bash
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:

js
const ros = app.config.globalProperties.$pinia._s.get('roqueos')
if (ros.isLocked) ros.unlock()

Step 5 — Connect localhost server

The store APIs:

js
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:

js
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.

js
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

SymptomLikely cause
isLocked: true after loginAuto-unlock watch not firing — call ros.unlock() manually
404 on storage list with valid credsMissing X-User-Id header, or storage owned by a different user
Music app shows folder with 0 tracks silentlyFixed (P1 #8): error notif now surfaces; check Notification Center
HLS endpoint returns 503ffmpeg not installed — front should hide HLS UI via /health/capabilities
Many POST /api-keys/session-token per playFixed (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-test skill (operator-only)
  • Cypress E2E in CI (yarn test:e2e:ci) — different stack, different goals
  • Mobile (Capacitor) E2E — see rules/wrappers/mobile.md

Lançado sob a Licença MIT.