12-testing.md
Testing · visión
Presencia tiene tres capas de prueba más un workflow de GitHub Actions. Todas corren sin Neon real: el cliente de Auth/Data API se mockea y el e2e siembra localStorage.
| Capa | Runner | Qué cubre | Comando |
|---|---|---|---|
| Unitario | Jasmine + Karma (ChromeHeadlessCI) | Servicios, guards, mapper, páginas | npm run test:ci |
| Integración | Jasmine + Karma (*.integration.spec.ts) | Rutas + guards, persistencia localStorage | npm run test:integration |
| E2E | Playwright (Chromium en CI) | Shell, dashboard, log, cursos, settings | npm run e2e:ci |
CI (.github/workflows/test.yml):
- Job unit-and-integration-tests —
npm ci, Chrome,test:ci,test:integration. - Job e2e-tests — instala Chromium y lanza
e2e:ci(elwebServerarrancang serve --port 8100).
No hace falta un proyecto Neon en Actions. CloudSyncService habla con un NeonService falso; el e2e trabaja offline-first.
Siguientes documentos: unitarios · integración · e2e · CI.
13-testing-unit.md
Testing · unitarios
Los unitarios instancian la clase sin HTTP a Neon. Runner: Jasmine + Karma (npm run test:ci). Hay fixtures compartidas en src/testing/.
Asistencia (src/app/services/attendance.service.spec.ts)
AttendanceService persiste cursos en courses_v1 y registros en attendance_v3. El reloj se fija al 15 de mayo de 2026 para que los días futuros sean deterministas.
it('should calculate late hours from entry and exit times', () => {
const course = createMockCourse({ hoursPerDay: 5, startTime: '09:00' });
svc.saveCourse(course);
svc.setDayRecord(
'2026-05-05',
{ status: 'late', entryTime: '09:30', exitTime: '14:00' },
course.id,
);
const stats = svc.getMonthStats('2026-05', course.id);
expect(stats.lateDays).toBe(1);
expect(stats.totalHoursAttended).toBe(4.5);
expect(stats.totalLostMinutes).toBe(30);
});
También se afirma overallStatus === 'failed' al superar maxAbsences, la migración attendance_v2 → v3 y los periodos por módulo.
Modo online (src/app/services/app-mode.service.spec.ts)
it('should default to offline mode', () => {
expect(service.isOffline()).toBeTrue();
expect(service.hasOnlineIntent()).toBeFalse();
});
enableOnlineMode() escribe app_mode_v1=online y limpia el intent. Un TestBed.resetTestingModule() comprueba que el modo se rehidrata.
Errores de auth (src/app/utils/auth-error.mapper.spec.ts)
Neon no expone un contrato único. El mapper acepta code, status, message o body:
expect(getAuthErrorKey({ code: 'invalid_credentials' })).toBe(
'AUTH.ERRORS.INVALID_CREDENTIALS',
);
expect(getAuthErrorKey({ status: 401 })).toBe('AUTH.ERRORS.INVALID_CREDENTIALS');
expect(getAuthErrorKey(null)).toBe('AUTH.ERRORS.GENERIC');
URLs de Neon (src/app/services/neon.service.spec.ts)
En producción las URLs son same-origin (/__neon-auth) para que la cookie sobreviva en iOS/PWA. En local son absolutas.
expect(service['resolveUrl']('/__neon-auth')).toBe(
`${window.location.origin}/__neon-auth`,
);
expect(service['resolveUrl']('https://ep-example.neonauth.aws.neon.tech/neondb/auth'))
.toBe('https://ep-example.neonauth.aws.neon.tech/neondb/auth');
Sync (src/app/services/cloud-sync.service.spec.ts)
createMockNeonService sustituye client.from('courses' | 'attendance_records'). Se afirma upload (incluye cancelled, excluye unlogged), download al storage local y que no hay push si isOffline().
Guard (src/app/guards/online-auth.guard.spec.ts)
Offline → true. Online sin sesión → UrlTree a /auth. Online con sesión → true.
14-testing-integration.md
Testing · integración
Comando: npm run test:integration.
Karma solo incluye **/*.integration.spec.ts. En CI corre después de los unitarios.
No hay Nest ni Postgres. La integración aquí es el cableado real de Angular: router + guards, y dos instancias de AttendanceService que se hablan a través de localStorage.
Rutas y guards (src/app/app.routes.integration.spec.ts)
Se monta un provideRouter que espeja AppRoutingModule. NeonService está mockeado.
it('should block root when online without session and send user to auth', async () => {
appMode.enableOnlineMode();
neon.getSession.and.returnValue(Promise.resolve(null));
const result = await TestBed.runInInjectionContext(() =>
TestBed.inject(OnlineAuthGuard).canActivate(),
);
expect(router.serializeUrl(result as never)).toBe('/auth');
});
it('should keep authenticated online users off auth route', async () => {
appMode.enableOnlineMode();
neon.getSession.and.returnValue(Promise.resolve({ user: { id: 'u1' } }));
const result = await TestBed.runInInjectionContext(() =>
TestBed.inject(GuestAuthGuard).canActivate(),
);
expect(router.serializeUrl(result as never)).toBe('/');
});
Offline, / sigue abierto: el producto es usable al 100 % sin cuenta.
Persistencia (src/app/attendance.integration.spec.ts)
Una instancia guarda curso + registros. Se destruye el TestBed y se crea otra: tiene que leer courses_v1 / attendance_v3 / selected_course_id.
const first = makeService();
first.saveCourse(course);
first.setDayRecord('2026-05-05', { status: 'present' }, course.id);
const reloaded = makeService();
expect(reloaded.getCourse('persist-1')?.name).toBe('Sistemas');
expect(reloaded.getDayRecord('2026-05-05', 'persist-1').status).toBe('present');
El segundo caso compara getMonthStats antes y después del reload (presentes, impuntualidades, horas, minutos perdidos). Si el cálculo o la serialización se desalinean, falla aquí y no en un e2e lento.
15-testing-e2e.md
Testing · e2e
Playwright en la raíz. Config: playwright.config.ts. Specs: e2e/*.spec.ts. Helpers: e2e/helpers/. Catálogo de casos: e2e/E2E-TEST-CATALOG.md.
npm run e2e:ci
En CI, Playwright arranca Angular solo:
webServer: {
command: 'npm run start -- --port 8100',
url: 'http://localhost:8100',
reuseExistingServer: !process.env.CI,
timeout: 120_000,
}
locale: 'es-ES' y timezoneId: 'Europe/Madrid' — el copy y los días laborables tienen que coincidir con los fixtures.
Semilla de localStorage
export async function seedLocalStorage(page: Page, data: SeedData): Promise<void> {
await page.addInitScript((payload) => {
localStorage.clear();
if (payload.courses) localStorage.setItem('courses_v1', JSON.stringify(payload.courses));
if (payload.records) localStorage.setItem('attendance_v3', JSON.stringify(payload.records));
if (payload.selectedCourseId !== undefined) {
localStorage.setItem('selected_course_id', payload.selectedCourseId);
}
}, data);
}
addInitScript corre antes de que Angular hidrate. Si siembras después del goto, el servicio ya leyó storage vacío.
Specs reales
test('G-001: app loads and shows tab bar', async ({ page }) => {
await page.goto('/');
await expect(page.locator('ion-tab-bar')).toBeVisible();
await expectTabBar(page, 'es');
});
test('D-001: loads stats for selected course', async ({ page }) => {
const course = makeCourse({ name: 'Dashboard Stats Course' });
await seedLocalStorage(page, {
courses: [course],
selectedCourseId: course.id,
records: { [course.id]: { '2026-06-02': { status: 'present' } } },
});
await gotoTab(page, 'dashboard');
await expect(page.locator('.course-name')).toHaveText('Dashboard Stats Course');
await expect(page.locator('.ring-wrap')).toBeVisible();
});
Los tabs se pulsan con getByRole('tab', { name: 'Resumen' | 'Registro' | … }) — Ionic sí expone role="tab" en esta app.
La PWA pinta dashboard, log, historial y cursos offline. No se afirma login/sync Neon en CI (eso requeriría secretos y correo verificado).
16-testing-ci.md
Testing · GitHub Actions
Workflow: .github/workflows/test.yml. Se dispara en push y pull_request.
Job unit-and-integration-tests
- name: Setup Chrome
uses: browser-actions/setup-chrome@v1
- name: Install dependencies
run: npm ci --include=optional
- name: Run unit tests
run: npm run test:ci
env:
CHROME_BIN: chrome
CI: true
- name: Run integration tests
run: npm run test:integration
env:
CHROME_BIN: chrome
CI: true
ChromeHeadlessCI (en karma.conf.js) añade --no-sandbox y --disable-dev-shm-usage para el runner de Ubuntu. CI=true elige ese launcher.
Job e2e-tests
- name: Install Playwright browser
run: npm run e2e:install
- name: Run E2E tests
run: npm run e2e:ci
env:
CI: true
Chromium se instala en el runner; no se sube al repo. retries: 2 y un solo worker solo en CI (playwright.config.ts). Reporter github para anotaciones en el PR.
Por qué no hay errores de entorno
| Riesgo | Mitigación |
|---|---|
| Neon Auth / Data API no están en Actions | createMockNeonService en unit/integración; e2e 100 % localStorage |
| Chrome headless en Ubuntu | browser-actions/setup-chrome + flags CI + CHROME_BIN |
ng serve tarda | webServer.timeout: 120000 |
| Lockfile desfasado | npm ci --include=optional |
| Fechas / i18n flaky | timezoneId: Europe/Madrid, locale: es-ES, reloj de Jasmine en 2026-05-15 |
Si un e2e rojo es de Ionic, el primer sitio a mirar es el helper gotoTab (espera ion-tab-bar) y si la semilla se aplicó antes del primer goto.