home
  • Spanish (ES)
  • English (UK)
  • Portuguese (PT)
  • star Featured
  • Hermes Agent
  • draw UX Projects
  • person About me
  • mail Contact
arrow_back Back to project Home

Project testing

Testing · Presencia

5 markdown files (overview, unit, integration, e2e, GitHub Actions).

On this page Testing · visión 0% expand_more
Testing · visión Testing · unitarios Asistencia (src/app/services/attendance.service.spec.ts) Modo online (src/app/services/app-mode.service.spec.ts) Errores de auth (src/app/utils/auth-error.mapper.spec.ts) URLs de Neon (src/app/services/neon.service.spec.ts) Sync (src/app/services/cloud-sync.service.spec.ts) Guard (src/app/guards/online-auth.guard.spec.ts) Testing · integración Rutas y guards (src/app/app.routes.integration.spec.ts) Persistencia (src/app/attendance.integration.spec.ts) Testing · e2e Semilla de localStorage Specs reales Testing · GitHub Actions Job unit-and-integration-tests Job e2e-tests Por qué no hay errores de entorno

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.

CapaRunnerQué cubreComando
UnitarioJasmine + Karma (ChromeHeadlessCI)Servicios, guards, mapper, páginasnpm run test:ci
IntegraciónJasmine + Karma (*.integration.spec.ts)Rutas + guards, persistencia localStoragenpm run test:integration
E2EPlaywright (Chromium en CI)Shell, dashboard, log, cursos, settingsnpm run e2e:ci

CI (.github/workflows/test.yml):

  1. Job unit-and-integration-tests — npm ci, Chrome, test:ci, test:integration.
  2. Job e2e-tests — instala Chromium y lanza e2e:ci (el webServer arranca ng 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

RiesgoMitigación
Neon Auth / Data API no están en ActionscreateMockNeonService en unit/integración; e2e 100 % localStorage
Chrome headless en Ubuntubrowser-actions/setup-chrome + flags CI + CHROME_BIN
ng serve tardawebServer.timeout: 120000
Lockfile desfasadonpm ci --include=optional
Fechas / i18n flakytimezoneId: 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.

On this page

Testing · visión Testing · unitarios Asistencia (src/app/services/attendance.service.spec.ts) Modo online (src/app/services/app-mode.service.spec.ts) Errores de auth (src/app/utils/auth-error.mapper.spec.ts) URLs de Neon (src/app/services/neon.service.spec.ts) Sync (src/app/services/cloud-sync.service.spec.ts) Guard (src/app/guards/online-auth.guard.spec.ts) Testing · integración Rutas y guards (src/app/app.routes.integration.spec.ts) Persistencia (src/app/attendance.integration.spec.ts) Testing · e2e Semilla de localStorage Specs reales Testing · GitHub Actions Job unit-and-integration-tests Job e2e-tests Por qué no hay errores de entorno

0% read

folder_zip Download all (ZIP)
Hermes Agent