@keber/qa-framework 1.11.3 → 1.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/docs/architecture.md +30 -26
- package/docs/decisions/lane-heartbeat-caller.md +86 -0
- package/docs/final-report.md +23 -23
- package/docs/folder-structure-guide.md +100 -80
- package/docs/installation.md +35 -7
- package/docs/skills-architecture.md +23 -23
- package/docs/spec-driven-philosophy.md +9 -9
- package/docs/usage-with-agent.md +83 -9
- package/docs/visuals/diag-01-ejecutivo-1.png +0 -0
- package/docs/visuals/diag-02-tactico-1.png +0 -0
- package/docs/visuals/diag-03-tecnico-1.png +0 -0
- package/docs/visuals/info-01-ejecutivo-1.png +0 -0
- package/docs/visuals/info-02-tactico-1.png +0 -0
- package/docs/visuals/info-03-tecnico-1.png +0 -0
- package/docs/visuals/pres-01-ejecutivo-1.png +0 -0
- package/docs/visuals/pres-02-tactico-1.png +0 -0
- package/docs/visuals/pres-03-tecnico-1.png +0 -0
- package/docs/visuals/qa-01-problema-1.png +0 -0
- package/docs/visuals/qa-02-que-es-1.png +0 -0
- package/docs/visuals/qa-03-pipeline-1.png +0 -0
- package/docs/visuals/qa-04-6-archivos-1.png +0 -0
- package/docs/visuals/qa-05-humano-1.png +0 -0
- package/docs/visuals/qa-06-agente-1.png +0 -0
- package/docs/visuals/qa-07-skills-arch-1.png +0 -0
- package/docs/visuals/qa-08-traceability-1.png +0 -0
- package/docs/visuals/qa-en-01-problem-1.png +0 -0
- package/docs/visuals/qa-en-02-what-is-1.png +0 -0
- package/docs/visuals/qa-en-03-pipeline-1.png +0 -0
- package/docs/visuals/qa-en-04-6files-1.png +0 -0
- package/docs/visuals/qa-en-05-human-1.png +0 -0
- package/docs/visuals/qa-en-06-agent-1.png +0 -0
- package/docs/visuals/qa-en-07-skills-arch-1.png +0 -0
- package/docs/visuals/qa-en-08-traceability-1.png +0 -0
- package/examples/module-example/suppliers/00-inventory.md +1 -1
- package/examples/module-example/suppliers/suppliers-create.spec.ts +8 -8
- package/integrations/ado-powershell/README.md +1 -1
- package/integrations/ado-powershell/scripts/sync-ado-titles.ps1 +2 -2
- package/integrations/playwright/README.md +4 -4
- package/package.json +7 -3
- package/qa-framework.config.json +23 -2
- package/scripts/check-forbidden-chars.js +187 -0
- package/scripts/check-spanish-accents.js +159 -0
- package/scripts/cli.js +1 -1
- package/scripts/generate.js +2 -2
- package/scripts/init.js +55 -7
- package/scripts/lib/claude-agents.js +78 -0
- package/scripts/lib/claude-commands.js +56 -0
- package/scripts/sync-version.js +36 -0
- package/scripts/upgrade.js +356 -20
- package/scripts/validate.js +17 -17
- package/skills/qa-ado-integration/SKILL.md +19 -19
- package/skills/qa-ado-integration/references/scripts-and-config.md +6 -6
- package/skills/qa-automation/SKILL.md +49 -27
- package/skills/qa-automation/references/config-checklist.md +4 -4
- package/skills/qa-automation/references/dom-inspection-template.js +7 -7
- package/skills/qa-automation/references/patterns.md +20 -20
- package/skills/qa-automation/references/pom-template.md +161 -0
- package/skills/qa-maintenance/SKILL.md +11 -11
- package/skills/qa-maintenance/references/update-rules.md +8 -8
- package/skills/qa-module-analysis/SKILL.md +20 -19
- package/skills/qa-module-analysis/references/exploration-checklist.md +4 -4
- package/skills/qa-module-analysis/references/output-verification.md +69 -0
- package/skills/qa-module-analysis/references/spec-file-formats.md +14 -14
- package/skills/qa-spec-generation/SKILL.md +13 -7
- package/skills/qa-test-cases/SKILL.md +11 -81
- package/skills/qa-test-cases/references/test-case-template.md +5 -5
- package/skills/qa-test-plan/SKILL.md +22 -86
- package/skills/qa-test-plan/references/plan-de-pruebas-template.md +24 -24
- package/skills/qa-test-stabilization/SKILL.md +59 -30
- package/skills/qa-test-stabilization/references/classification-protocol.md +35 -35
- package/templates/agent-next-steps.md +10 -10
- package/templates/agents/qa-analisis.md +142 -0
- package/templates/agents/qa-asesoria.md +48 -0
- package/templates/agents/qa-informe-resultados.md +234 -0
- package/templates/agents/qa-plan.md +131 -0
- package/templates/automation-scaffold/.env.example +36 -6
- package/templates/automation-scaffold/fixtures/auth.ts +27 -5
- package/templates/automation-scaffold/fixtures/base.ts +23 -1
- package/templates/automation-scaffold/fixtures/test-helpers.ts +1 -1
- package/templates/automation-scaffold/global-setup.ts +117 -48
- package/templates/automation-scaffold/playwright.config.ts +72 -32
- package/templates/automation-scaffold/scripts/global-setup-guards.js +184 -0
- package/templates/automation-scaffold/scripts/lane-config.js +269 -0
- package/templates/automation-scaffold/scripts/lane-lock.js +589 -0
- package/templates/defect-report.md +17 -3
- package/templates/execution-report.md +9 -9
- package/templates/integration-scaffold/.env.example +1 -1
- package/templates/integration-scaffold/global-setup.ts +21 -1
- package/templates/integration-scaffold/playwright.config.ts +15 -1
- package/templates/qa-framework.instructions.md +8 -7
- package/templates/qa-framework.rules.md +53 -0
- package/templates/qa-readme.md +8 -8
- package/templates/session-summary.md +2 -2
- package/templates/specification/00-inventory.md +3 -3
- package/templates/specification/01-business-rules.md +6 -6
- package/templates/specification/02-workflows.md +1 -1
- package/templates/specification/03-roles-permissions.md +1 -1
- package/templates/specification/04-test-data.md +8 -8
- package/templates/specification/05-test-scenarios.md +5 -5
- package/templates/test-case.md +3 -3
- package/templates/test-data-guidelines.md +7 -7
- package/templates/test-plan-sprint.md +13 -13
- package/templates/test-plan.md +3 -3
- package/skills/qa-test-plan/references/priority-and-feasibility.md +0 -159
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: qa-plan
|
|
3
|
+
description: Genera el Plan de Pruebas manual de un sprint (resumen ejecutivo + tabla detallada de casos de prueba trazables a items de Azure DevOps) ejecutable en el timebox del sprint. Modo por defecto para pedidos de "plan de pruebas" de un sprint; también se invoca con "modo PLAN".
|
|
4
|
+
tools: Read, Write, Grep, Glob
|
|
5
|
+
model: sonnet
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Rol
|
|
9
|
+
|
|
10
|
+
Actúas como Asistente experto en Aseguramiento de la Calidad (QA) con foco práctico en planes de prueba **manuales** ejecutables en el timebox del sprint, en contextos de madurez inicial. Este agente cubre exclusivamente el modo **PLAN** (modo por defecto del flujo original).
|
|
11
|
+
|
|
12
|
+
# Audiencia y Estilo
|
|
13
|
+
|
|
14
|
+
- Público: equipo QA/dev de {{PROJECT_DISPLAY_NAME}} ({{LOCALE_LANGUAGE_LABEL}}).
|
|
15
|
+
- Tono: claro, directo y accionable (sin jerga innecesaria).
|
|
16
|
+
- Zona horaria: {{TIMEZONE}}.
|
|
17
|
+
- Fechas siempre en formato **{{DATE_FORMAT}}**.
|
|
18
|
+
|
|
19
|
+
# Contexto Operativo
|
|
20
|
+
|
|
21
|
+
- Sprints de {{SPRINT_DURATION_DAYS}} días con **{{MANUAL_TESTING_TIMEBOX_DAYS}} días** para ejecutar pruebas manuales.
|
|
22
|
+
- Procesos inmaduros: minutas/historias/criterios incompletos, evidencia parcial, ruido en transcripciones.
|
|
23
|
+
- Objetivo principal: **confirmar resolución** de issues/bugs/tasks del sprint y cubrir flujos críticos del área afectada.
|
|
24
|
+
- E2E/UI automatizadas: no prioridad, pero puedes sugerirlas brevemente si aportan.
|
|
25
|
+
- Riesgos locales a considerar: separador decimal (coma vs punto), cálculos monetarios, integraciones, permisos, datos maestros, impactos legales/tributarios (ajusta esta lista a los riesgos reales del dominio del proyecto).
|
|
26
|
+
|
|
27
|
+
# Entradas posibles
|
|
28
|
+
|
|
29
|
+
- Minutas o resúmenes de planificación (`<Proyecto>`, `<Sprint>`).
|
|
30
|
+
- Items de Azure DevOps (Issue/Bug/Task) en texto/Excel/PPT/imágenes/transcripciones.
|
|
31
|
+
- Imágenes de UI, descripciones de componentes e interfaces.
|
|
32
|
+
- Casos de prueba anteriores (XLS ADO).
|
|
33
|
+
|
|
34
|
+
# Política anti-alucinación y uso de insumos incompletos
|
|
35
|
+
|
|
36
|
+
- **Nunca inventes datos de negocio**.
|
|
37
|
+
- Cuando falten detalles críticos, crea el bloque **Faltantes críticos** con preguntas puntuales y sigue con un **Plan mínimo viable**, marcando **TODO:** donde falte.
|
|
38
|
+
- Cuando debas asumir algo, marca **Supuesto:** (fácil de remover).
|
|
39
|
+
- Si hay información contradictoria, prioriza lo más reciente y explícitalo.
|
|
40
|
+
- En la columna **Confirma**, si no hay ID, usa **"-"** y agrega el punto a **Faltantes críticos**.
|
|
41
|
+
|
|
42
|
+
# Pipeline (proceso)
|
|
43
|
+
|
|
44
|
+
0) **Resumen estructurado** (si las entradas son ruidosas): objetivo, áreas/módulos impactados, lista preliminar de items.
|
|
45
|
+
1) **Ingesta & Normalización**: Proyecto, Sprint, áreas afectadas, lista de items (ID + título).
|
|
46
|
+
2) **Deduplicación & Alcance**: elimina duplicados, agrupa por área; extrae criterios de aceptación si existen.
|
|
47
|
+
3) **Universo de Tests**: lista **todos** los escenarios de prueba identificables para el alcance del sprint (happy path, negativos, permisos, transiciones de estado, integraciones, edge cases), **sin filtrar aún por el timebox de {{MANUAL_TESTING_TIMEBOX_DAYS}} días**. Para cada escenario, clasifica su **factibilidad de automatización** (mismo criterio de `qa-framework`, no inventes uno nuevo):
|
|
48
|
+
- **Automatizable completo**: determinístico, observable en UI/API, sin dependencia de sistemas externos.
|
|
49
|
+
- **Automatizable parcial**: requiere mock de un sistema externo o inspección humana.
|
|
50
|
+
- **No automatizable**: acceso físico, no-determinístico, efectos irreversibles en QA, o `BLOCKED-PERMISSIONS`.
|
|
51
|
+
4) **Tests Priorizados (timebox {{MANUAL_TESTING_TIMEBOX_DAYS}} días)**: selecciona del universo el subconjunto que entra a la Tabla de Pruebas, combinando:
|
|
52
|
+
- **Riesgo/valor de negocio** via P0->P3:
|
|
53
|
+
- **P0**: Confirmación por cada Issue/Bug/Task del sprint; camino feliz crítico; riesgo de corrupción de datos o show-stopper.
|
|
54
|
+
- **P1**: Smoke crítico del flujo impactado; negativos comunes; dependencias cross-módulo.
|
|
55
|
+
- **P2**: Regresión mínima adyacente (alto uso/alto riesgo); features secundarias.
|
|
56
|
+
- **P3**: Exploratoria timeboxed (1-2 charters); edge cases de baja frecuencia.
|
|
57
|
+
- **Factibilidad de automatización**: a igualdad de prioridad, prefiere para el set ejecutable los escenarios automatizables (reducen costo de mantención futura); no automatizables de baja prioridad son los primeros candidatos a quedar fuera.
|
|
58
|
+
- Lo que no se selecciona va a **Universo excluido** con motivo - nunca se descarta silenciosamente.
|
|
59
|
+
- **No infles el universo por inflarlo**: documentarlo es para trazabilidad/auditoría de cobertura, no para maximizar conteo. Mergea variaciones casi idénticas y usa **Confirma** para referenciar múltiples IDs.
|
|
60
|
+
5) **Estrategia & Suites**: define TestSuites por área/épica impactada, en base a los Tests Priorizados.
|
|
61
|
+
6) **Casos de Prueba**: trazables a items del sprint (**Confirma**).
|
|
62
|
+
7) **Auto-revisión**: ejecuta el checklist de calidad.
|
|
63
|
+
|
|
64
|
+
# Priorización y Etiquetas
|
|
65
|
+
|
|
66
|
+
- La **Tabla de Pruebas** solo incluye **Tests Priorizados** - el Universo completo y el Universo excluido se documentan aparte (ver "Salida - Plan de Pruebas").
|
|
67
|
+
- Mantén P0->P1->P2->P3 alineado al timebox de {{MANUAL_TESTING_TIMEBOX_DAYS}} días.
|
|
68
|
+
- Etiquetas por caso (en el Título): **[SMOKE]**, **[REGRESIÓN]**, **[CONFIRMACIÓN]**, **[EXPLORATORIA]** (pueden coexistir con P0-P3).
|
|
69
|
+
- Cobertura mínima: al menos 1 **Smoke** por funcionalidad clave + **Regresión acotada** en áreas afectadas + **Confirmación** por cada item del sprint.
|
|
70
|
+
|
|
71
|
+
# Reglas de Calidad (Checklist interno)
|
|
72
|
+
|
|
73
|
+
- Cada TestCase incluye: **Área funcional**, **Título**, **Descripción** (con precondiciones y **datos mínimos**), **Steps** numerados, **Resultado Esperado** verificable, **Confirma** (ID o "-").
|
|
74
|
+
- El **último paso** siempre tiene **Resultado Esperado** explícito.
|
|
75
|
+
- Sin pasos huérfanos ni resultados vagos.
|
|
76
|
+
- **Deduplicación**: mergea casos idénticos; en **Confirma** puedes referenciar múltiples IDs.
|
|
77
|
+
- **Trazabilidad**: incluir **Matriz de Trazabilidad (ID <-> TestCases)**.
|
|
78
|
+
|
|
79
|
+
# Salida - Plan de Pruebas
|
|
80
|
+
|
|
81
|
+
1) **Resumen del Plan** (contenido de la respuesta en chat):
|
|
82
|
+
- **Marco de Pruebas**: Objetivo, Alcance (incluye fuera de alcance), Entregables
|
|
83
|
+
- **Universo de Tests**: conteo total de escenarios identificados, breakdown por factibilidad de automatización (completo/parcial/no automatizable).
|
|
84
|
+
- **Plan**: Estrategia (P0-P3), TestSuites, **Datos mínimos**, Precondiciones generales, Charters (si aplica) - sobre los Tests Priorizados.
|
|
85
|
+
- **Supuestos & Faltantes críticos**
|
|
86
|
+
|
|
87
|
+
2) **Plan detallado**: archivo Markdown con la **Tabla de Pruebas** (solo Tests Priorizados) y el **Universo excluido**:
|
|
88
|
+
|
|
89
|
+
| N | TestCase | Área Funcional | Título | Descripción | Steps | Resultado Esperado | Confirma | Tipo |
|
|
90
|
+
|---|----------|----------------|--------|-------------|-------|--------------------|----------|------|
|
|
91
|
+
|
|
92
|
+
**Convenciones obligatorias**:
|
|
93
|
+
- **TestCase**: dejar **en blanco** (otro proceso generara IDs, ver skill `qa-ado-integration` si se pide crear los casos en ADO).
|
|
94
|
+
- **Steps**: lista numerada **en una celda** con `<br>`:
|
|
95
|
+
`1) Precondicion...<br>2) Accion...<br>3) Verificacion...`
|
|
96
|
+
- **Resultado Esperado**: concreto y verificable (evitar "funciona correctamente").
|
|
97
|
+
- **Confirma**: usar **`Bug 17166`**, **`Issue 17168`**, **`Task 17179`**; si no aplica, **"N/A"**; si falta ID, **"-"** y mover a **Faltantes críticos**.
|
|
98
|
+
- **Tipo**: `Manual` | `Automatizado` | `Ambos` | `Bloqueado` - según la factibilidad de automatización determinada en la etapa de Universo de Tests (`Bloqueado` si es `BLOCKED-PERMISSIONS`/`PENDING-CODE`).
|
|
99
|
+
- **Etiquetas** en **Título**: [SMOKE]/[REGRESIÓN]/[CONFIRMACIÓN]/[EXPLORATORIA] + nivel P0-P3 si ayuda.
|
|
100
|
+
- Mantén trazabilidad: al menos un caso **P0** por item crítico del sprint.
|
|
101
|
+
|
|
102
|
+
Después de la Tabla de Pruebas, agrega la sección **Universo excluido**:
|
|
103
|
+
|
|
104
|
+
| TC | Título | Motivo de exclusión |
|
|
105
|
+
|----|--------|----------------------|
|
|
106
|
+
|
|
107
|
+
Motivos válidos: fuera de scope del sprint, baja probabilidad/impacto, `PENDING-CODE`, `BLOCKED-PERMISSIONS`, requiere automatización aún no lista, etc. Esta tabla es el respaldo de trazabilidad de todo lo que se generó pero no entró al timebox de {{MANUAL_TESTING_TIMEBOX_DAYS}} días.
|
|
108
|
+
|
|
109
|
+
## Regla de entrega
|
|
110
|
+
|
|
111
|
+
- Ruta de salida: `qa/02-test-plans/sprints/Sprint-<Sprint>/Plan-de-Pruebas-<Proyecto>-Sprint-<Sprint>.md`
|
|
112
|
+
(`<Proyecto>` es el `project.name` de `qa/qa-framework.config.json`; `<Sprint>` es el número de sprint sin padding, p.ej. `qa/02-test-plans/sprints/Sprint-12/Plan-de-Pruebas-{{PROJECT_NAME}}-Sprint-12.md`. Sigue la misma convención que los `Plan-de-Pruebas-*.md` ya archivados en `qa/02-test-plans/historical/sprint-*/` y documentada en `qa/QA-STRUCTURE-GUIDE.md`, si existen. Si el plan cubre un solo módulo, puedes agregar el sufijo `-{modulo}` al nombre, igual que el resto del pipeline).
|
|
113
|
+
- Si el directorio del sprint no existe, créalo antes de escribir el archivo.
|
|
114
|
+
- Antes de escribir, si ya existe un archivo con ese nombre, léelo primero y confirma con el usuario si se debe sobrescribir (no lo sobrescribas silenciosamente).
|
|
115
|
+
- El **Resumen del Plan** (punto 1) va en tu respuesta de chat; el **Plan detallado** con la tabla completa (punto 2) es el que se escribe al archivo.
|
|
116
|
+
- Al terminar, tu respuesta al usuario debe indicar la **ruta relativa exacta** del archivo creado.
|
|
117
|
+
- Nunca reportes la tarea como completa si el archivo no fue escrito con la herramienta `Write`.
|
|
118
|
+
|
|
119
|
+
# Checklist de calidad (marcar antes de entregar)
|
|
120
|
+
|
|
121
|
+
- [ ] Cada Bug/Issue/Task crítico tiene al menos un caso **P0** con **Confirma**.
|
|
122
|
+
- [ ] Los **Steps** usan `<br>` y están numerados.
|
|
123
|
+
- [ ] No hay **Resultados Esperados** vagos.
|
|
124
|
+
- [ ] La Tabla de Pruebas contiene solo **Tests Priorizados**; el **Universo de Tests** completo se resumió (conteo + factibilidad) y el **Universo excluido** quedó documentado con motivo.
|
|
125
|
+
- [ ] Cada fila de la Tabla de Pruebas tiene columna **Tipo** (Manual/Automatizado/Ambos/Bloqueado) coherente con su factibilidad de automatización.
|
|
126
|
+
- [ ] **TODO** y **Faltantes críticos** están claramente indicados.
|
|
127
|
+
- [ ] Se respetó el foco del timebox de {{MANUAL_TESTING_TIMEBOX_DAYS}} días y se documentaron exclusiones (backlog de regresión).
|
|
128
|
+
- [ ] Existe **Matriz de Trazabilidad** (ID <-> TestCases).
|
|
129
|
+
- [ ] El archivo `Plan-de-Pruebas-<Proyecto>-Sprint-<Sprint>.md` existe físicamente en `qa/02-test-plans/sprints/Sprint-<Sprint>/`.
|
|
130
|
+
|
|
131
|
+
Si alguna condición no se cumple, la respuesta se considera incompleta.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
# keber/qa-framework
|
|
1
|
+
# -----------------------------------------------------------------------
|
|
2
|
+
# keber/qa-framework - Environment Variables Reference
|
|
3
3
|
# -----------------------------------------------------------------------
|
|
4
4
|
# Copy this file to .env and fill in real values.
|
|
5
5
|
# NEVER commit .env to source control.
|
|
@@ -17,12 +17,42 @@ QA_SESSION_TTL_MS=7200000
|
|
|
17
17
|
# Optional path to a server-side logout endpoint if the app exposes one.
|
|
18
18
|
# QA_LOGOUT_PATH=/logout
|
|
19
19
|
|
|
20
|
-
#
|
|
20
|
+
# -----------------------------------------------------------------------
|
|
21
|
+
# Parallel QA lanes
|
|
22
|
+
# -----------------------------------------------------------------------
|
|
23
|
+
# A lane is a distinct QA account. Lanes exist because the app under test
|
|
24
|
+
# invalidates a session when the same account logs in again, so two workers
|
|
25
|
+
# sharing one account silently destroy each other's session.
|
|
26
|
+
#
|
|
27
|
+
# The lane table itself lives in qa-framework.config.json (parallelLanes.lanes).
|
|
28
|
+
# Each lane needs its own credentials here, named after that lane's "account"
|
|
29
|
+
# prefix. With no parallelLanes config, only the QA_USER pair below is used.
|
|
30
|
+
|
|
31
|
+
# Pin a run to a single lane. Required by global-setup unless QA_COLD_SETUP=1.
|
|
32
|
+
# QA_LANE_ONLY=1
|
|
33
|
+
|
|
34
|
+
# Escape hatch for an intentional relogin of EVERY lane. A cold setup can
|
|
35
|
+
# invalidate another agent's or CI job's running session, so it is never implicit.
|
|
36
|
+
# QA_COLD_SETUP=1
|
|
37
|
+
|
|
38
|
+
# Log in a specific set of lanes (comma-separated lane ids).
|
|
21
39
|
# QA_AUTH_USER=1
|
|
22
40
|
|
|
23
|
-
#
|
|
41
|
+
# Shared lane-lock state directory. Set this to an absolute shared path when
|
|
42
|
+
# running from multiple git worktrees or checkouts: with the per-checkout default
|
|
43
|
+
# each one gets its OWN lock state, so they never contend, every command reports
|
|
44
|
+
# success, and two sessions can land on the same account. The failure is silent.
|
|
45
|
+
# QA_LANE_LOCKS_DIR=/absolute/path/to/shared/.locks
|
|
46
|
+
|
|
47
|
+
# Minutes before a held lane is swept as stale. A lane with a recent heartbeat is
|
|
48
|
+
# never swept regardless of age.
|
|
49
|
+
# QA_LANE_STALE_MINUTES=20
|
|
50
|
+
|
|
51
|
+
# Credentials for additional lanes, one pair per lane account prefix.
|
|
24
52
|
# QA_USER2_EMAIL=qa-user2@example.com
|
|
25
53
|
# QA_USER2_PASSWORD=CHANGE_ME
|
|
54
|
+
# QA_USER3_EMAIL=qa-user3@example.com
|
|
55
|
+
# QA_USER3_PASSWORD=CHANGE_ME
|
|
26
56
|
|
|
27
57
|
# -----------------------------------------------------------------------
|
|
28
58
|
# Login Configuration
|
|
@@ -56,12 +86,12 @@ QA_USER_PASSWORD=CHANGE_ME
|
|
|
56
86
|
# QA_USER_RUT=12345678-9
|
|
57
87
|
|
|
58
88
|
# -----------------------------------------------------------------------
|
|
59
|
-
# Azure DevOps Integration (optional
|
|
89
|
+
# Azure DevOps Integration (optional - leave blank to disable)
|
|
60
90
|
# -----------------------------------------------------------------------
|
|
61
91
|
# ADO_ORG_URL=https://dev.azure.com/your-org
|
|
62
92
|
# ADO_PROJECT_NAME=YourProject
|
|
63
93
|
# ADO_PLAN_ID=00000
|
|
64
|
-
# AZURE_TOKEN=your-pat-token-here # Personal Access Token
|
|
94
|
+
# AZURE_TOKEN=your-pat-token-here # Personal Access Token - NEVER commit
|
|
65
95
|
|
|
66
96
|
# -----------------------------------------------------------------------
|
|
67
97
|
# CI / Pipeline overrides
|
|
@@ -10,11 +10,11 @@
|
|
|
10
10
|
* await loginAs(page, 'admin');
|
|
11
11
|
*
|
|
12
12
|
* Alternatively, use storageState via global-setup.ts (recommended for
|
|
13
|
-
* performance
|
|
13
|
+
* performance - avoids repeating the login flow on every test).
|
|
14
14
|
* Use loginAs() only in tests that need to validate the login flow itself
|
|
15
15
|
* or switch users mid-test.
|
|
16
16
|
*
|
|
17
|
-
* Supported roles are driven by env vars
|
|
17
|
+
* Supported roles are driven by env vars - see .env.example.
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
import { Page } from '@playwright/test';
|
|
@@ -26,7 +26,7 @@ interface LoginConfig {
|
|
|
26
26
|
password: string;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
/** Map role name
|
|
29
|
+
/** Map role name -> credentials from environment variables */
|
|
30
30
|
function getCredentials(role: QARole): LoginConfig {
|
|
31
31
|
switch (role) {
|
|
32
32
|
case 'admin':
|
|
@@ -58,7 +58,7 @@ export async function loginAs(page: Page, role: QARole = 'default'): Promise<voi
|
|
|
58
58
|
if (!email || !password) {
|
|
59
59
|
throw new Error(
|
|
60
60
|
`[qa-framework/auth] Missing credentials for role "${role}". ` +
|
|
61
|
-
`Check your .env file
|
|
61
|
+
`Check your .env file - expected ${role.toUpperCase()}_EMAIL and ${role.toUpperCase()}_PASSWORD.`
|
|
62
62
|
);
|
|
63
63
|
}
|
|
64
64
|
|
|
@@ -71,7 +71,29 @@ export async function loginAs(page: Page, role: QARole = 'default'): Promise<voi
|
|
|
71
71
|
|
|
72
72
|
await page.goto(`${baseURL}${loginPath}`, { waitUntil: 'domcontentloaded' });
|
|
73
73
|
await page.locator(emailSelector).fill(email);
|
|
74
|
-
|
|
74
|
+
// Trace safety: Playwright records fill() argument values in traces, and this
|
|
75
|
+
// project runs with trace/video 'retain-on-failure'. This helper runs for every
|
|
76
|
+
// test that logs in as a role, so a fill() here would leak the plaintext password
|
|
77
|
+
// into far more failure artifacts than the one-time global setup does. Setting the
|
|
78
|
+
// value through evaluate() keeps it out of the trace.
|
|
79
|
+
// Do not "simplify" this back to fill().
|
|
80
|
+
// The input/change events are required because assigning .value directly does
|
|
81
|
+
// not notify SPA frameworks (Blazor/Radzen bind on those events).
|
|
82
|
+
// evaluate() has no auto-wait, so the explicit waitFor replaces the one that
|
|
83
|
+
// locator().fill() performed implicitly.
|
|
84
|
+
await page.locator(passwordSelector).waitFor({ state: 'visible', timeout: 30_000 });
|
|
85
|
+
await page.evaluate(
|
|
86
|
+
([selector, pwd]) => {
|
|
87
|
+
const input = document.querySelector(selector) as HTMLInputElement | null;
|
|
88
|
+
if (!input) {
|
|
89
|
+
throw new Error(`[qa-framework] Password input not found for selector: ${selector}`);
|
|
90
|
+
}
|
|
91
|
+
input.value = pwd;
|
|
92
|
+
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
93
|
+
input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
94
|
+
},
|
|
95
|
+
[passwordSelector, password] as const
|
|
96
|
+
);
|
|
75
97
|
await page.locator(submitSelector).click();
|
|
76
98
|
await page.waitForSelector(successSelector, { timeout: 15_000 });
|
|
77
99
|
}
|
|
@@ -63,7 +63,29 @@ async function reauth(browser: import('@playwright/test').Browser, stateFile: st
|
|
|
63
63
|
try {
|
|
64
64
|
await page.goto(loginPath, { waitUntil: 'load', timeout: 60_000 });
|
|
65
65
|
await page.locator(emailSelector).fill(email);
|
|
66
|
-
|
|
66
|
+
// Trace safety: Playwright records fill() argument values in traces, and this
|
|
67
|
+
// project runs with trace/video 'retain-on-failure'. This reauth path runs for
|
|
68
|
+
// every test whose session exceeded the TTL, so a fill() here would leak the
|
|
69
|
+
// plaintext password into far more failure artifacts than the one-time global
|
|
70
|
+
// setup does. Setting the value through evaluate() keeps it out of the trace.
|
|
71
|
+
// Do not "simplify" this back to fill().
|
|
72
|
+
// The input/change events are required because assigning .value directly does
|
|
73
|
+
// not notify SPA frameworks (Blazor/Radzen bind on those events).
|
|
74
|
+
// evaluate() has no auto-wait, so the explicit waitFor replaces the one that
|
|
75
|
+
// locator().fill() performed implicitly.
|
|
76
|
+
await page.locator(passwordSelector).waitFor({ state: 'visible', timeout: 30_000 });
|
|
77
|
+
await page.evaluate(
|
|
78
|
+
([selector, pwd]) => {
|
|
79
|
+
const input = document.querySelector(selector) as HTMLInputElement | null;
|
|
80
|
+
if (!input) {
|
|
81
|
+
throw new Error(`[qa-framework] Password input not found for selector: ${selector}`);
|
|
82
|
+
}
|
|
83
|
+
input.value = pwd;
|
|
84
|
+
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
85
|
+
input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
86
|
+
},
|
|
87
|
+
[passwordSelector, password] as const
|
|
88
|
+
);
|
|
67
89
|
await page.locator(submitSelector).click();
|
|
68
90
|
await page.waitForSelector(successSelector, { timeout: 30_000 });
|
|
69
91
|
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
// -----------------------------------------------------------------------
|
|
9
|
-
// EXEC_IDX
|
|
9
|
+
// EXEC_IDX - unique numeric seed per minute-long execution window
|
|
10
10
|
// -----------------------------------------------------------------------
|
|
11
11
|
// Use this wherever test data needs to be unique (e.g., names, emails,
|
|
12
12
|
// codes) to avoid collisions between parallel or consecutive test runs
|
|
@@ -11,14 +11,14 @@
|
|
|
11
11
|
* - Reference the matching storageState in playwright.config.ts projects[].
|
|
12
12
|
*
|
|
13
13
|
* Environment variables required:
|
|
14
|
-
* QA_BASE_URL
|
|
15
|
-
* QA_USER_EMAIL
|
|
16
|
-
* QA_USER_PASSWORD
|
|
17
|
-
* QA_LOGIN_PATH
|
|
18
|
-
* QA_LOGIN_EMAIL_SELECTOR
|
|
19
|
-
* QA_LOGIN_PASSWORD_SELECTOR
|
|
20
|
-
* QA_LOGIN_SUBMIT_SELECTOR
|
|
21
|
-
* QA_LOGIN_SUCCESS_SELECTOR
|
|
14
|
+
* QA_BASE_URL - Base URL of the application under test
|
|
15
|
+
* QA_USER_EMAIL - Default QA user email (or username/RUT)
|
|
16
|
+
* QA_USER_PASSWORD - Default QA user password
|
|
17
|
+
* QA_LOGIN_PATH - Relative path to the login page (default: /login)
|
|
18
|
+
* QA_LOGIN_EMAIL_SELECTOR - CSS selector for the username/email input
|
|
19
|
+
* QA_LOGIN_PASSWORD_SELECTOR - CSS selector for the password input
|
|
20
|
+
* QA_LOGIN_SUBMIT_SELECTOR - CSS selector for the submit button
|
|
21
|
+
* QA_LOGIN_SUCCESS_SELECTOR - CSS selector that confirms successful login
|
|
22
22
|
*
|
|
23
23
|
* See .env.example for all supported variables.
|
|
24
24
|
*/
|
|
@@ -30,6 +30,20 @@ import * as fs from 'fs';
|
|
|
30
30
|
|
|
31
31
|
dotenv.config();
|
|
32
32
|
|
|
33
|
+
// Lane table and guards. Both are CommonJS so the guard logic stays testable with
|
|
34
|
+
// `node --test` without adding ts-node or tsx as a dependency.
|
|
35
|
+
const laneLock = require('./scripts/lane-lock.js');
|
|
36
|
+
const guards = require('./scripts/global-setup-guards.js');
|
|
37
|
+
|
|
38
|
+
type LaneInfo = {
|
|
39
|
+
account: string;
|
|
40
|
+
storageState: string;
|
|
41
|
+
mcpNamespace: string | null;
|
|
42
|
+
runnerProject: string | null;
|
|
43
|
+
capabilities: string[];
|
|
44
|
+
};
|
|
45
|
+
const LANE_INFO: Record<string, LaneInfo> = laneLock.LANE_INFO;
|
|
46
|
+
|
|
33
47
|
function resolveVar(param: string | undefined, envKey: string, defaultValue: string): [string, string] {
|
|
34
48
|
if (param !== undefined) {
|
|
35
49
|
return [param, 'param'];
|
|
@@ -40,37 +54,51 @@ function resolveVar(param: string | undefined, envKey: string, defaultValue: str
|
|
|
40
54
|
return [defaultValue, 'default'];
|
|
41
55
|
}
|
|
42
56
|
|
|
43
|
-
|
|
57
|
+
/**
|
|
58
|
+
* Decides which lanes to log in.
|
|
59
|
+
*
|
|
60
|
+
* Resolution order:
|
|
61
|
+
* 1. QA_LANE_ONLY - pins the run to exactly one lane. This is the normal path and
|
|
62
|
+
* the only one the guards permit without an explicit cold-setup opt-in.
|
|
63
|
+
* 2. QA_AUTH_USER - comma-separated lane ids, for an intentional multi-lane setup.
|
|
64
|
+
* 3. the projects' storageState, matched against the lane table.
|
|
65
|
+
* 4. every configured lane (the cold-setup path, gated by QA_COLD_SETUP=1).
|
|
66
|
+
*
|
|
67
|
+
* Step 3 matches storageState by EXACT equality against the lane table rather than by
|
|
68
|
+
* substring. The previous scaffold used storageState.includes('user-2'), which is what
|
|
69
|
+
* breaks first beyond two lanes: 'user-2' is a substring of 'user-20', and there was no
|
|
70
|
+
* branch at all for a third lane, so lanes past the second were silently never logged in.
|
|
71
|
+
*/
|
|
72
|
+
function resolveAuthLanes(config: FullConfig): string[] {
|
|
73
|
+
const laneIds = Object.keys(LANE_INFO);
|
|
74
|
+
|
|
75
|
+
const laneOnly = process.env.QA_LANE_ONLY;
|
|
76
|
+
if (laneOnly) {
|
|
77
|
+
// Throws on an unknown lane rather than guessing. Guessing is how a run logs into
|
|
78
|
+
// an account it does not hold.
|
|
79
|
+
return [laneLock.laneIdForQaLaneOnly(laneOnly)];
|
|
80
|
+
}
|
|
81
|
+
|
|
44
82
|
const explicit = process.env.QA_AUTH_USER;
|
|
45
83
|
if (explicit) {
|
|
46
|
-
const resolved =
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
52
|
-
if (resolved.size) {
|
|
53
|
-
return resolved;
|
|
54
|
-
}
|
|
84
|
+
const resolved = explicit
|
|
85
|
+
.split(',')
|
|
86
|
+
.map((value) => value.trim())
|
|
87
|
+
.filter((value) => laneIds.includes(value));
|
|
88
|
+
if (resolved.length) return resolved;
|
|
55
89
|
}
|
|
56
90
|
|
|
57
|
-
const fromProjects = new Set<
|
|
91
|
+
const fromProjects = new Set<string>();
|
|
58
92
|
for (const project of config.projects) {
|
|
59
93
|
const storageState = (project.use as { storageState?: unknown } | undefined)?.storageState;
|
|
60
|
-
if (typeof storageState
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
} else if (storageState.includes('user-1')) {
|
|
64
|
-
fromProjects.add('1');
|
|
65
|
-
}
|
|
94
|
+
if (typeof storageState !== 'string') continue;
|
|
95
|
+
for (const laneId of laneIds) {
|
|
96
|
+
if (LANE_INFO[laneId].storageState === storageState) fromProjects.add(laneId);
|
|
66
97
|
}
|
|
67
98
|
}
|
|
99
|
+
if (fromProjects.size) return [...fromProjects];
|
|
68
100
|
|
|
69
|
-
|
|
70
|
-
return fromProjects;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
return new Set(['1']);
|
|
101
|
+
return laneIds;
|
|
74
102
|
}
|
|
75
103
|
|
|
76
104
|
async function dismissOnboardingFlow(page: import('@playwright/test').Page): Promise<void> {
|
|
@@ -133,7 +161,27 @@ async function loginAs(params: {
|
|
|
133
161
|
for (let attempt = 1; attempt <= 3 && !signedIn; attempt++) {
|
|
134
162
|
await page.goto(loginUrl, { waitUntil: 'load', timeout: 60_000 });
|
|
135
163
|
await page.locator(emailSelector).fill(email);
|
|
136
|
-
|
|
164
|
+
// Trace safety: Playwright records fill() argument values in traces, and this
|
|
165
|
+
// project runs with trace/video 'retain-on-failure' - a failed CI run would
|
|
166
|
+
// persist the plaintext password in the artifact. Setting the value through
|
|
167
|
+
// evaluate() keeps it out of the trace. Do not "simplify" this back to fill().
|
|
168
|
+
// The input/change events are required because assigning .value directly does
|
|
169
|
+
// not notify SPA frameworks (Blazor/Radzen bind on those events).
|
|
170
|
+
// evaluate() has no auto-wait, so the explicit waitFor replaces the one that
|
|
171
|
+
// locator().fill() performed implicitly.
|
|
172
|
+
await page.locator(passwordSelector).waitFor({ state: 'visible', timeout: 30_000 });
|
|
173
|
+
await page.evaluate(
|
|
174
|
+
([selector, pwd]) => {
|
|
175
|
+
const input = document.querySelector(selector) as HTMLInputElement | null;
|
|
176
|
+
if (!input) {
|
|
177
|
+
throw new Error(`[qa-framework] Password input not found for selector: ${selector}`);
|
|
178
|
+
}
|
|
179
|
+
input.value = pwd;
|
|
180
|
+
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
181
|
+
input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
182
|
+
},
|
|
183
|
+
[passwordSelector, password] as const
|
|
184
|
+
);
|
|
137
185
|
await page.locator(submitSelector).click();
|
|
138
186
|
|
|
139
187
|
const result = await Promise.race([
|
|
@@ -167,30 +215,51 @@ async function loginAs(params: {
|
|
|
167
215
|
}
|
|
168
216
|
|
|
169
217
|
export default async function globalSetup(_config: FullConfig): Promise<void> {
|
|
170
|
-
const baseURL
|
|
171
|
-
const authUsers = resolveAuthUsers(_config);
|
|
218
|
+
const baseURL = process.env.QA_BASE_URL!;
|
|
172
219
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
220
|
+
// Guards run BEFORE any login. A lock nothing consults is decoration, and this is
|
|
221
|
+
// the last point at which a run about to drive an account it does not hold can
|
|
222
|
+
// still be stopped.
|
|
223
|
+
//
|
|
224
|
+
// global-setup NEVER acquires or reserves a lane itself. It only READS the lock
|
|
225
|
+
// state and verifies the lane is already held. Reserving is the caller's job - the
|
|
226
|
+
// operator, the orchestrator, or the CI job. A global-setup that reserved its own
|
|
227
|
+
// lane would make every accidental run look legitimate.
|
|
228
|
+
guards.assertColdSetupIsIntentional(process.env);
|
|
229
|
+
|
|
230
|
+
const laneOnly = process.env.QA_LANE_ONLY;
|
|
231
|
+
if (laneOnly) {
|
|
232
|
+
guards.assertLaneIsLocked(laneOnly, process.env, laneLock);
|
|
233
|
+
guards.assertProjectsMatchLane(laneOnly, _config.projects, process.argv, laneLock);
|
|
183
234
|
}
|
|
184
235
|
|
|
185
|
-
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
236
|
+
for (const laneId of resolveAuthLanes(_config)) {
|
|
237
|
+
const lane = LANE_INFO[laneId];
|
|
238
|
+
|
|
239
|
+
// Credentials come from the lane's declared account prefix, e.g. account
|
|
240
|
+
// 'QA_USER4' resolves QA_USER4_EMAIL / QA_USER4_PASSWORD. This is what replaces
|
|
241
|
+
// the old hardcoded QA_USER / QA_USER2 pair and is what lets a lane be added by
|
|
242
|
+
// config alone.
|
|
243
|
+
const emailKey = `${lane.account}_EMAIL`;
|
|
244
|
+
const passwordKey = `${lane.account}_PASSWORD`;
|
|
245
|
+
|
|
246
|
+
if (!process.env[emailKey] || !process.env[passwordKey]) {
|
|
247
|
+
throw new Error(
|
|
248
|
+
`[global-setup] Lane ${laneId} (account ${lane.account}) is missing ${emailKey} or ` +
|
|
249
|
+
`${passwordKey}. Every configured lane needs its own credentials, otherwise two lanes ` +
|
|
250
|
+
'end up sharing an account, which is exactly what lanes exist to prevent.'
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const [email, emailSource] = resolveVar(process.env[emailKey], emailKey, 'qa-user@example.com');
|
|
255
|
+
const [password, passwordSource] = resolveVar(process.env[passwordKey], passwordKey, 'CHANGE_ME');
|
|
256
|
+
console.log(`[global-setup] lane ${laneId} (${lane.account}) credentials from ${emailSource} / ${passwordSource}`);
|
|
257
|
+
|
|
189
258
|
await loginAs({
|
|
190
259
|
email,
|
|
191
260
|
password,
|
|
192
261
|
baseURL,
|
|
193
|
-
stateFile:
|
|
262
|
+
stateFile: lane.storageState,
|
|
194
263
|
});
|
|
195
264
|
}
|
|
196
265
|
}
|
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
import { defineConfig, devices } from '@playwright/test';
|
|
2
2
|
import * as dotenv from 'dotenv';
|
|
3
|
-
|
|
3
|
+
import * as fs from 'fs';
|
|
4
|
+
|
|
5
|
+
// Least privilege: this config only needs a handful of non-secret values, so it reads
|
|
6
|
+
// them with dotenv.parse(), which returns a plain object and never touches process.env.
|
|
7
|
+
// dotenv.config() would inject the WHOLE .env - every QA account password and ADO token
|
|
8
|
+
// living in it - into a process that has no use for them, where any dependency, crash
|
|
9
|
+
// dump or child process inherits them. Do not "simplify" this back to config().
|
|
10
|
+
const ENV_KEYS = ['QA_SESSION_TTL_MS', 'QA_BASE_URL'] as const;
|
|
11
|
+
if (fs.existsSync('.env')) {
|
|
12
|
+
const parsed = dotenv.parse(fs.readFileSync('.env'));
|
|
13
|
+
for (const key of ENV_KEYS) {
|
|
14
|
+
if (process.env[key] === undefined && parsed[key] !== undefined) {
|
|
15
|
+
process.env[key] = parsed[key];
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
4
19
|
|
|
5
20
|
export const SESSION_TTL_MS = Number(process.env.QA_SESSION_TTL_MS ?? 2 * 60 * 60 * 1000);
|
|
6
21
|
|
|
@@ -26,7 +41,7 @@ for (const key of required) {
|
|
|
26
41
|
// token: process.env.SYSTEM_ACCESSTOKEN ?? process.env.ADO_PAT,
|
|
27
42
|
// planId: Number(process.env.ADO_PLAN_ID),
|
|
28
43
|
// projectName: process.env.ADO_PROJECT!,
|
|
29
|
-
// testRunTitle: `[Auto] Sprint {{NNN}}
|
|
44
|
+
// testRunTitle: `[Auto] Sprint {{NNN}} - ${new Date().toISOString().slice(0, 10)}`,
|
|
30
45
|
// publishTestResultsMode: 'testRun' as const,
|
|
31
46
|
// uploadAttachments: true,
|
|
32
47
|
// attachmentsType: ['screenshot', 'video', 'trace'] as const,
|
|
@@ -38,6 +53,57 @@ for (const key of required) {
|
|
|
38
53
|
// },
|
|
39
54
|
// };
|
|
40
55
|
|
|
56
|
+
// -------------------------------------------------------------------
|
|
57
|
+
// Parallel QA lanes
|
|
58
|
+
// -------------------------------------------------------------------
|
|
59
|
+
// A lane is a distinct QA account. Lanes exist because the applications under test
|
|
60
|
+
// invalidate a session when the same account logs in again, so two concurrent workers
|
|
61
|
+
// sharing one account silently destroy each other's session. Speed is not the reason.
|
|
62
|
+
//
|
|
63
|
+
// The lane table lives in qa-framework.config.json (parallelLanes.lanes) and is read
|
|
64
|
+
// here through lane-lock.js, which is the SINGLE SOURCE OF TRUTH for the
|
|
65
|
+
// lane -> storageState mapping. Never hardcode a storageState on a project below. A
|
|
66
|
+
// parallel hardcoded mapping caused real evidence corruption once already: a project
|
|
67
|
+
// pinned to one lane's storageState while QA_LANE_ONLY named another, so a run was
|
|
68
|
+
// recorded under the wrong account and nothing failed at the time.
|
|
69
|
+
//
|
|
70
|
+
// A project with no parallelLanes block gets one implicit default lane, producing
|
|
71
|
+
// exactly the single 'chromium' project this scaffold shipped before lanes existed.
|
|
72
|
+
const laneLock = require('./scripts/lane-lock.js');
|
|
73
|
+
|
|
74
|
+
type LaneInfo = {
|
|
75
|
+
account: string;
|
|
76
|
+
storageState: string;
|
|
77
|
+
mcpNamespace: string | null;
|
|
78
|
+
runnerProject: string | null;
|
|
79
|
+
capabilities: string[];
|
|
80
|
+
};
|
|
81
|
+
const LANE_INFO: Record<string, LaneInfo> = laneLock.LANE_INFO;
|
|
82
|
+
|
|
83
|
+
// QA_LANE_ONLY pins the whole run to a single lane. When it is set, every lane project
|
|
84
|
+
// resolves its storageState through that lane, so a --project bound to a different lane
|
|
85
|
+
// cannot quietly drive an account this run does not hold. global-setup-guards.js
|
|
86
|
+
// additionally rejects a --project whose storageState does not match the reserved lane.
|
|
87
|
+
const QA_LANE_ONLY = process.env.QA_LANE_ONLY;
|
|
88
|
+
|
|
89
|
+
function resolveStorageState(laneId: string): string {
|
|
90
|
+
return QA_LANE_ONLY
|
|
91
|
+
? laneLock.storageStateForLaneOnly(QA_LANE_ONLY)
|
|
92
|
+
: LANE_INFO[laneId].storageState;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// One Playwright project per configured lane. A lane that declares a runnerProject uses
|
|
96
|
+
// that name; otherwise the project is named chromium-lane-<id>. With the single implicit
|
|
97
|
+
// default lane this yields one project named 'chromium'.
|
|
98
|
+
const laneProjects = Object.entries(LANE_INFO).map(([laneId, lane]) => ({
|
|
99
|
+
name: lane.runnerProject || `chromium-lane-${laneId}`,
|
|
100
|
+
use: {
|
|
101
|
+
...devices['Desktop Chrome'],
|
|
102
|
+
storageState: resolveStorageState(laneId),
|
|
103
|
+
},
|
|
104
|
+
dependencies: ['setup'],
|
|
105
|
+
}));
|
|
106
|
+
|
|
41
107
|
export default defineConfig({
|
|
42
108
|
// ------ Test discovery ------
|
|
43
109
|
testDir: './tests',
|
|
@@ -78,44 +144,18 @@ export default defineConfig({
|
|
|
78
144
|
globalSetup: './global-setup.ts',
|
|
79
145
|
|
|
80
146
|
// ------ Projects ------
|
|
147
|
+
// To add a lane, add an entry to parallelLanes.lanes in qa-framework.config.json.
|
|
148
|
+
// There is nothing to uncomment or copy-paste here: the lane projects are generated
|
|
149
|
+
// from that table, so the config stays the only place a lane is defined.
|
|
81
150
|
projects: [
|
|
82
151
|
{
|
|
83
152
|
name: 'setup',
|
|
84
153
|
use: { ...devices['Desktop Chrome'] },
|
|
85
154
|
testMatch: /global-setup\.ts/,
|
|
86
155
|
},
|
|
87
|
-
|
|
88
|
-
name: 'chromium',
|
|
89
|
-
use: {
|
|
90
|
-
...devices['Desktop Chrome'],
|
|
91
|
-
storageState: '.auth/user-default.json',
|
|
92
|
-
},
|
|
93
|
-
dependencies: ['setup'],
|
|
94
|
-
},
|
|
156
|
+
...laneProjects,
|
|
95
157
|
],
|
|
96
158
|
|
|
97
|
-
// ------ Parallel 2-user variant ------
|
|
98
|
-
// Uncomment and replace the single chromium project above to run two parallel
|
|
99
|
-
// workers with separate credentials. Also set QA_USER2_EMAIL and
|
|
100
|
-
// QA_USER2_PASSWORD in .env.
|
|
101
|
-
//
|
|
102
|
-
// {
|
|
103
|
-
// name: 'chromium-user1',
|
|
104
|
-
// use: {
|
|
105
|
-
// ...devices['Desktop Chrome'],
|
|
106
|
-
// storageState: '.auth/user-1.json',
|
|
107
|
-
// },
|
|
108
|
-
// dependencies: ['setup'],
|
|
109
|
-
// },
|
|
110
|
-
// {
|
|
111
|
-
// name: 'chromium-user2',
|
|
112
|
-
// use: {
|
|
113
|
-
// ...devices['Desktop Chrome'],
|
|
114
|
-
// storageState: '.auth/user-2.json',
|
|
115
|
-
// },
|
|
116
|
-
// dependencies: ['setup'],
|
|
117
|
-
// },
|
|
118
|
-
|
|
119
159
|
// ------ Output directories ------
|
|
120
160
|
outputDir: 'test-results/',
|
|
121
161
|
snapshotPathTemplate: '{testDir}/__snapshots__/{testFilePath}/{arg}{ext}',
|