Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
f8e9762
Refactor storage key for school ID in ConfirmschoolPage
Victor1Ja May 15, 2026
1211351
fix: repair spec imports broken by earlier renames
Victor1Ja Aug 7, 2026
20d725d
feat: retry scheduled tests for the whole slot window, paced by failu…
Victor1Ja Aug 7, 2026
ae5d6ba
feat: flag sync-recovered measurements and record their planned slot
Victor1Ja Aug 7, 2026
b1b65e5
feat: use @m-lab/ndt7 npm package instead of vendored copies
Victor1Ja Aug 10, 2026
dd579dc
fix: repair spec imports broken by earlier renames
Victor1Ja Aug 7, 2026
c9b1796
chore: allow @m-lab/ndt7 as a CommonJS dependency
Victor1Ja Aug 10, 2026
f405083
i18n: update Uzbek translations from the reviewed translation sheet
Victor1Ja Aug 10, 2026
d1666d7
feat(research): add system/network info probe for plan 0008
Victor1Ja Aug 11, 2026
d99dda3
feat(research): run the probe inside the Electron main process
Victor1Ja Aug 11, 2026
913e0f6
test(e2e): Playwright happy-path suite for the v2.0.4 RC
Victor1Ja Aug 17, 2026
9b41185
Integrate PostHog analytics (renderer + main) with school-group identity
vipulbhavsar94 Aug 17, 2026
06ab980
Use PostHog EU region host
vipulbhavsar94 Aug 17, 2026
f63ce71
feat: PostHog product analytics in the app client (v2.0.4)
Victor1Ja Aug 17, 2026
3e8bc26
feat: report Electron auto-update events to PostHog over IPC
Victor1Ja Aug 17, 2026
aa0e4e6
Merge branch 'feat/posthog' into release-candidate-test-2.0.4
Victor1Ja Aug 18, 2026
7736ff8
Merge branch 'feat/ndt7-npm-package' into release-candidate-test-2.0.4
Victor1Ja Aug 18, 2026
25d4b70
Revert "feat: use @m-lab/ndt7 npm package instead of vendored copies"
Victor1Ja Aug 24, 2026
dc200ea
feat: capture M-Lab's server clock alongside each measurement
Victor1Ja Aug 24, 2026
7603faa
chore(research): translate probe strings and comments to English
Victor1Ja Aug 25, 2026
39dd398
Merge pull request #84 from unicef/research/network-device-info
Victor1Ja Aug 25, 2026
e2f9f18
feat: capture network/device context and diagnose empty Wi-Fi reads
Victor1Ja Aug 26, 2026
621a302
docs: drop references to a private planning workspace
Victor1Ja Aug 26, 2026
e16402e
chore: update version to 2.0.4 and enhance school registration handling
Victor1Ja Aug 26, 2026
24bf472
Merge pull request #87 from unicef/fix/duplicate-school-registrations
Victor1Ja Aug 26, 2026
e69fa2b
Merge pull request #86 from unicef/feat/device-context-capture
Victor1Ja Aug 26, 2026
9848364
Merge release-candidate-test-2.0.4 into fix/ndt7-vendored-server-time…
Victor1Ja Aug 27, 2026
30d0861
Merge origin/release-candidate-test-2.0.4 into fix/use-of-school-id
Victor1Ja Aug 27, 2026
62cad87
Merge pull request #77 from unicef/fix/use-of-school-id
Victor1Ja Aug 27, 2026
781af8b
Merge pull request #85 from unicef/fix/ndt7-vendored-server-timestamp
Victor1Ja Aug 31, 2026
abd88cc
feat: add Uzbek language support to environment configuration
Victor1Ja Aug 31, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,8 @@ npm-debug.log*
_environment.prod.ts
electron/electron-builder.env
src/environments/_environment.prod.ts

# Playwright e2e artifacts
/test-results
/e2e/playwright-report
/e2e/playwright-report-stg
11 changes: 11 additions & 0 deletions angular.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,14 @@
},
"ci": {
"progress": false
},
"e2e": {
"fileReplacements": [
{
"replace": "src/environments/_environment.prod.ts",
"with": "src/environments/_environment.e2e.ts"
}
]
}
}
},
Expand All @@ -88,6 +96,9 @@
},
"ci": {
"progress": false
},
"e2e": {
"buildTarget": "app:build:e2e"
}
}
},
Expand Down
131 changes: 131 additions & 0 deletions e2e/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
# E2E — happy path (Playwright)

Suite Playwright del happy path del Daily Check App corriendo en navegador
(`ng serve`) contra el backend real en docker. Corresponde al plan 0010 del
`project-memory` del workspace.

> La carpeta `e2e/src` + `protractor.conf.js` son restos de Protractor
> (descontinuado) y no se usan.

## Qué cubre

Los 8 pasos del checklist del plan 0010, en serie sobre la misma instalación
(comparten página: el checklist es un recorrido lineal y el estado se acumula):

| # | Paso | Verificación |
|---|------|--------------|
| 1-4 | Instalación limpia → registro (España / `ES-TEST-SCHOOL-01`) → primer test ndt7 **real** → upload en tiempo real | payload y fila en DB con `upload_failed=false`, slot `null`; cola offline vacía |
| 5 | Test manual desde el medidor | fila con `scheduled_slot`/`scheduled_at` en `null` |
| 6 | Slot programado | fila con `scheduled_slot='A'` y `scheduled_at` = la hora planificada, sin reintentos |
| 7 | UI post-medición | historial local + cifras visibles en la tarjeta de última medición |
| 8 | Reinicio | el registro persiste (sin onboarding) y el scheduler se rearma |

El paso 6 no espera a un slot real (serían horas): inyecta un semáforo vencido
para el slot A y deja que lo recoja el tick de 60 s del scheduler, como en
producción. Los tres campos del plan 0006 se comprueban **en columna de DB**
(`e2e/playwright/db.ts`, vía `psql` en el contenedor), no solo en el payload.

Lo que NO cubre (por diseño): el main process de Electron
(`systeminformation`, wifi, hardware id — llegan como `null`/`'N/A'`), el
instalador, y el camino de fallo de upload → sync (QA del plan 0006).

## Requisitos

- Docker Desktop corriendo.
- `giga-meter-backend` como repo hermano (layout del workspace giga).
- Internet (el speed test ndt7 corre contra servidores M-Lab reales).
- Una vez: `npx playwright install chromium`.

### Rama del backend hermano

El stack construye el backend desde el **working tree** del repo hermano, así
que la suite corre contra la rama que tengas ahí checked out. Funciona con
`develop` y con `staging` (verificado en ambas, 2026-08-17).

Los fixtures viven en `e2e/seed/` de **este** repo y se montan en el
contenedor, para no depender de que el backend traiga el tooling de seed:

- `seed.js` — cargador con `pg`. Sustituye a `src/prisma/scripts/seed-runner.ts`
del backend, que solo existe en la línea de `develop` (lo añadió el commit
`ef07c5b` del trabajo health-entity y nunca llegó a `staging`).
- `seed-spain.sql` — fixture ES mínimo: `country`, `dailycheckapp_country` y la
escuela `ES-TEST-SCHOOL-01`. Nada de `facility_type`, whitelist ni `health`:
eso es del multi-facility (plan 0003), sus tablas no existen en `staging` y el
RC 2.0.4 solo hace el flujo school sobre `/api/v1`.

Única condición extra: para que pasen las aserciones en DB de
`upload_failed`/`scheduled_slot`/`scheduled_at`, la rama del backend necesita la
migración del plan 0006.

## Contra el staging real

```bash
npm run e2e:stg
```

Usa `playwright.stg.config.ts`: no levanta docker, sirve el app con `npm start`
(sin la config `e2e`, así que `_environment.prod.ts` manda — está en
`mode: 'stg'` y resuelve `restAPIStg` + `tokenStg`) y corre el mismo spec con
`E2E_SKIP_DB=1`, que omite las comprobaciones en columna porque no hay
contenedor de Postgres. Quedan las de payload, storage y UI.

**Escribe datos reales**: un registro de dispositivo y hasta 3 mediciones ndt7
por corrida, en un entorno compartido. No es desechable.

## Correr

```bash
npm run e2e
```

Playwright levanta solo los dos servidores (config `webServer`):

1. `docker compose -f e2e/docker-compose.e2e.yml up --build` — Postgres
(PostGIS, puerto host 55432) + Redis + backend en `:3000` + un mock del
servicio de validación de api keys de Project Connect (acepta cualquier
token con write access y categoría `giga_meter`). Al arrancar aplica
migraciones y los seeds idempotentes (`seed-spain-project-connect.sql` +
`e2e/seed/e2e-category-config.sql`). La primera vez el build de la imagen
tarda varios minutos.
2. `npm run start:e2e` — `ng serve --configuration e2e`, que reemplaza
`_environment.prod.ts` por `src/environments/_environment.e2e.ts`
(API → `http://localhost:3000/api/v1/`).

Ambos usan `reuseExistingServer`: si ya los tienes levantados a mano, los
reutiliza. Para bajar y limpiar el stack docker (borra la DB):

```bash
npm run e2e:down
```

Modo visible / debug:

```bash
npm run e2e:headed
```

```bash
npx playwright test --debug
```

Reporte HTML tras un fallo: `e2e/playwright-report/` (trace y video se
guardan solo en fallos).

## Notas de estabilidad

- `api.ipinfo.io` y `ipv4.geojs.io` van interceptados con respuestas fijas
(evita ~14 s de reintentos y flakiness).
- El **startup test** se silencia sembrando `startupTestScheduled`/
`lastStartupTest`/`lastMeasurement` en localStorage antes de cargar la app,
para que su delay aleatorio de 0-15 min no compita con los tests del
checklist. El scheduler en sí sigue vivo: el paso 6 lo necesita.
- `scheduledTesting` viene desactivado por defecto; el paso 6 lo habilita en
`savedSettings` antes de inyectar el semáforo, o `getSemaphore()` lo vaciaría
en cada tick.
- El primer test dispara un modal de felicitación que tapa el medidor: el paso 5
lo cierra antes de pinchar.
- La suite completa tarda ~4-5 min (cada speed test real dura ~25-45 s y el
paso 6 espera hasta un minuto al tick del scheduler).
- El compose define `DIRECT_DATABASE_URL` además de `DATABASE_URL`: algunas
ramas declaran `directUrl` en el datasource de Prisma y sin ella el backend
no arranca (P1012).
116 changes: 116 additions & 0 deletions e2e/docker-compose.e2e.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# Backend stack for the Playwright e2e suite: Postgres (PostGIS) + Redis +
# giga-meter-backend built from the sibling repo in the giga workspace.
#
# Layout requirement: this repo and giga-meter-backend must be siblings
# (giga-workspace/giga/{project-connect-daily-check-app,giga-meter-backend}).
#
# Started automatically by playwright.config.ts (webServer). Manual usage:
# docker compose -f e2e/docker-compose.e2e.yml up --build
# npm run e2e:down # tears it down and drops the DB
#
# On every start the backend container applies migrations and re-runs the
# idempotent Spain seed (country ES + school ES-TEST-SCHOOL-01), so the suite
# always finds its fixture data.
name: giga-meter-e2e

services:
e2e-db:
# PostGIS build: the backend migrations/seeds use the postgis extension
# and ST_MakePoint (same image as the workspace docker-compose-local.yml).
image: postgis/postgis:15-3.3
environment:
POSTGRES_USER: giga
POSTGRES_PASSWORD: giga
POSTGRES_DB: giga_e2e
ports:
- '55432:5432' # offset so it never clashes with a dev postgres on 5432
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U giga -d giga_e2e']
interval: 5s
timeout: 5s
retries: 10

e2e-redis:
image: redis:7
healthcheck:
test: ['CMD', 'redis-cli', 'ping']
interval: 5s
timeout: 5s
retries: 10

e2e-auth-mock:
# Stand-in for the Project Connect service the AuthGuard calls to validate
# bearer tokens (GET /api/v1/validate_api_key/DAILY_CHECK_APP). Accepts any
# token and grants write access with category giga_meter, so the backend
# can run with USE_AUTH=true without reaching Azure.
image: node:22-alpine
command:
- node
- -e
- |
const http = require('http');
const body = JSON.stringify({
success: true,
message: 'ok',
timestamp: new Date().toISOString(),
data: {
has_write_access: true,
countries: [],
apiCategory: { code: 'giga_meter' },
},
});
http.createServer((req, res) => {
res.setHeader('content-type', 'application/json');
res.end(body);
}).listen(80, () => console.log('e2e auth mock listening on :80'));

e2e-backend:
build:
context: ../../giga-meter-backend
args:
# The backend Dockerfile pipes this into chpasswd (ssh access for the
# Azure deployment) and fails on an empty value. Never exposed here:
# the e2e container doesn't publish the ssh port.
SSH_PASSWD: 'root:e2e-local-only'
depends_on:
e2e-db:
condition: service_healthy
e2e-redis:
condition: service_healthy
e2e-auth-mock:
condition: service_started
environment:
DATABASE_URL: postgresql://giga:giga@e2e-db:5432/giga_e2e?schema=public
# Algunas ramas del backend declaran `directUrl = env("DIRECT_DATABASE_URL")`
# en el datasource de Prisma y sin la variable el arranque muere con P1012.
# Aquí no hay pooler, así que apunta a la misma DB; sobra sin hacer daño en
# las ramas cuyo schema no la usa.
DIRECT_DATABASE_URL: postgresql://giga:giga@e2e-db:5432/giga_e2e?schema=public
REDIS_URL: redis://e2e-redis:6379
NODE_ENV: development # dev mode => CORS allows the ng serve origin
# Real auth path, fake validator: several endpoints gate on the
# write_access/category that only AuthGuard.validateToken sets, so
# USE_AUTH=false is not enough — validate against the local mock instead.
USE_AUTH: 'true'
PROJECT_CONNECT_SERVICE_URL: http://e2e-auth-mock
DAILY_CHECK_APP_API_CODE: DAILY_CHECK_APP
PCDC_APP_DOWNLOAD_URL: http://unused.invalid
CACHE_EXPIRE: '60'
ports:
- '3000:3000'
volumes:
# Fixtures y cargador viven en ESTE repo, no en el backend: el seed-runner
# y el seed de España del backend solo existen en la línea de `develop`
# (los añadió el trabajo health-entity), así que depender de ellos ataba la
# suite a esa rama. Montados aquí, funciona con cualquier rama del backend.
- ./seed:/APP/e2e-seed:ro
# La imagen arranca por defecto con start.sh (ssh + boot de prod); se
# sustituye por migrate -> seed -> serve. `node` y `pg` salen de /APP.
entrypoint:
- /bin/sh
- -c
- >
npx prisma migrate deploy &&
node /APP/e2e-seed/seed.js
/APP/e2e-seed/seed-spain.sql /APP/e2e-seed/e2e-category-config.sql &&
npm run start:prod
85 changes: 85 additions & 0 deletions e2e/playwright/db.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// Consultas a la DB del stack e2e (plan 0010, pasos 5-6: "verificar en DB").
//
// Se habla con Postgres vía `docker compose exec psql` en vez de un cliente
// node: el stack de docker ya es requisito duro de la suite, así que esto no
// añade ninguna dependencia nueva al repo del app.
//
// Las queries devuelven JSON generado por Postgres y se parsean con JSON.parse,
// así no hay que elegir separador de campos ni escapar la salida de psql.
import { execFileSync } from 'node:child_process';

const COMPOSE_FILE = 'e2e/docker-compose.e2e.yml';
const DB_SERVICE = 'e2e-db';

// Formato que produce exactamente lo mismo que Date#toISOString() en JS, para
// poder comparar scheduled_at contra el timestamp inyectado en el semáforo.
const ISO_MS = `'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'`;

/** Corre una query que devuelve una sola columna JSON y la parsea. */
function queryJson<T>(sql: string): T {
const stdout = execFileSync(
'docker',
[
'compose',
'-f',
COMPOSE_FILE,
'exec',
'-T',
DB_SERVICE,
'psql',
'-U',
'giga',
'-d',
'giga_e2e',
'-t', // solo tuplas, sin cabecera
'-A', // sin alineación ni padding
'-c',
sql,
],
{ encoding: 'utf8' },
);
return JSON.parse(stdout.trim());
}

export interface MeasurementRow {
notes: string | null;
upload_failed: boolean | null;
scheduled_slot: string | null;
scheduled_at: string | null;
giga_id_school: string | null;
}

/**
* Última fila de `measurements` que cumple la condición, o null si no hay
* ninguna. `where` se interpola tal cual: solo se llama con literales del test.
*/
export function latestMeasurement(where: string): MeasurementRow | null {
return queryJson<MeasurementRow | null>(
`SELECT coalesce(to_json(t), 'null'::json)
FROM (
SELECT notes,
upload_failed,
scheduled_slot,
to_char(scheduled_at AT TIME ZONE 'UTC', ${ISO_MS}) AS scheduled_at,
giga_id_school
FROM measurements
WHERE ${where}
ORDER BY id DESC
LIMIT 1
) t;`,
);
}

/** Cuántas mediciones hay para la escuela del fixture. */
export function measurementCount(gigaIdSchool: string): number {
return queryJson<number>(
`SELECT to_json(count(*)) FROM measurements WHERE giga_id_school = '${gigaIdSchool}';`,
);
}

/** Cuántos registros de dispositivo hay para la escuela del fixture. */
export function schoolRegistrationCount(gigaIdSchool: string): number {
return queryJson<number>(
`SELECT to_json(count(*)) FROM dailycheckapp_school WHERE giga_id_school = '${gigaIdSchool}';`,
);
}
Loading