Progreso y XP
Módulos API: progress, xp, student-badges, users, dashboard
Páginas web: StudentProgressPage, StudentMyBadgesPage, DashboardPage
Spec Playwright: apps/e2e/tests/student/progress-xp.spec.ts — 📋 pendiente
Contexto del dominio
Section titled “Contexto del dominio”El motor de progreso y gamificación de NappAI Fluency cubre cinco subsistemas:
| Subsistema | Endpoint principal | Descripción |
|---|---|---|
| Progreso de lección | POST /progress/lesson/:id/complete | Marca lección completada, dispara XP y comprueba módulo/ruta |
| XP y niveles | calculado en XpService.getStudentXPSummary() | Total de XP = eventos + bonus de badges; nivel 1-10, 500 XP por nivel |
| Racha | GET /users/me/streak | Días consecutivos con al menos una lección completada |
| Badges | GET /users/me/badges · GET /users/me/badges/summary | 8 categorías automáticas + badges manuales; evaluador cron |
| Dashboard | GET /dashboard | Resumen unificado: stats, paths inscritos, nextLesson, actividad reciente, recomendaciones |
Fórmula XP:
LESSON_COMPLETED→ +10 XPROUTE_COMPLETED(path completo) → +100 XPQUIZ_PASSED→ XP variable- Juegos (
ESCAPE_ROOM_COMPLETED,CROSSWORD_COMPLETED,TRIVIA_COMPLETED, etc.) → XP variable - Badge bronze=+25, silver=+50, gold=+100, platinum=+200, legend=+500
- Niveles:
Math.min(10, Math.floor(xpTotal / 500) + 1)— umbral inferior(nivel-1)*500, umbral superiornivel*500
Nombres de nivel:
- 1-3 → Aprendiz IA · 4-5 → Practicante IA · 6-7 → Arquitecto IA · 8-9 → Experto IA · 10 → Maestro IA
Racha: Se considera activa si la última lección completada fue hoy o ayer (UTC). Se rompe si no hay actividad en 24 h y el último día activo no fue ayer.
Badges — categorías:
LEVEL(dominio IA),PATH(rutas completadas),FLUENCY(score de fluidez),STREAK(racha),QUIZ(quiz performance),SPECIALIZATION,SPEED,HONOR
User Stories
Section titled “User Stories”US-XP-001: Ver mi nivel actual de XP y puntos acumulados
Section titled “US-XP-001: Ver mi nivel actual de XP y puntos acumulados”Como estudiante autenticado Quiero ver mi nivel XP actual, los puntos acumulados y cuánto me falta para el siguiente nivel Para conocer mi progreso en la gamificación de la plataforma
Criterios: AC-XP-001 · Casos: TC-XP-001, TC-XP-002 · Prioridad: Alta · ✅ Implementado
US-XP-002: Recibir XP al completar una lección
Section titled “US-XP-002: Recibir XP al completar una lección”Como estudiante autenticado Quiero recibir automáticamente 10 XP cada vez que completo una lección Para ver mi progreso reflejado en el nivel y en el historial
Criterios: AC-XP-002 · Casos: TC-XP-003, TC-XP-004 · Prioridad: Alta · ✅ Implementado
US-XP-003: Recibir XP extra al completar una ruta entera
Section titled “US-XP-003: Recibir XP extra al completar una ruta entera”Como estudiante que termina todos los módulos de una ruta Quiero recibir 100 XP adicionales al completar la ruta Para ser recompensado por el esfuerzo de finalizar un path completo
Criterios: AC-XP-003 · Casos: TC-XP-005 · Prioridad: Alta · ✅ Implementado
US-XP-004: Ver y filtrar mis badges
Section titled “US-XP-004: Ver y filtrar mis badges”Como estudiante autenticado Quiero ver todos mis badges ganados y los disponibles, filtrados por categoría Para conocer mis logros y entender qué necesito para desbloquear los siguientes
Criterios: AC-XP-004 · Casos: TC-XP-006, TC-XP-007 · Prioridad: Alta · ✅ Implementado
US-XP-005: Obtener un badge automático al cumplir un criterio
Section titled “US-XP-005: Obtener un badge automático al cumplir un criterio”Como estudiante autenticado Quiero que el sistema me otorgue automáticamente un badge cuando cumplo su criterio Para recibir reconocimiento sin necesidad de solicitarlo manualmente
Criterios: AC-XP-005 · Casos: TC-XP-008 · Prioridad: Alta · ✅ Implementado
US-XP-006: Ver mi racha de días consecutivos
Section titled “US-XP-006: Ver mi racha de días consecutivos”Como estudiante autenticado Quiero ver cuántos días consecutivos he estudiado y el estado de la semana actual Para mantener mi motivación y no romper la racha
Criterios: AC-XP-006 · Casos: TC-XP-009, TC-XP-010 · Prioridad: Alta · ✅ Implementado
US-XP-007: Racha se rompe sin actividad en 24 h
Section titled “US-XP-007: Racha se rompe sin actividad en 24 h”Como estudiante autenticado Quiero que el sistema registre correctamente cuándo se rompe mi racha Para recibir un incentivo a volver y reconstruirla
Criterios: AC-XP-007 · Casos: TC-XP-011 · Prioridad: Alta · ✅ Implementado
US-XP-008: Dashboard personalizado con «Continuar donde lo dejé»
Section titled “US-XP-008: Dashboard personalizado con «Continuar donde lo dejé»”Como estudiante autenticado Quiero que el dashboard me muestre la siguiente lección pendiente de mi ruta más reciente Para retomar el aprendizaje sin buscar dónde me quedé
Criterios: AC-XP-008 · Casos: TC-XP-012, TC-XP-013 · Prioridad: Alta · ✅ Implementado
US-XP-009: Ver progreso detallado por path y módulo
Section titled “US-XP-009: Ver progreso detallado por path y módulo”Como estudiante autenticado Quiero ver el porcentaje completado de cada path inscrito, desglosado por módulo Para planificar el tiempo de estudio y saber exactamente dónde estoy
Criterios: AC-XP-009 · Casos: TC-XP-014, TC-XP-015 · Prioridad: Alta · ✅ Implementado
Acceptance Criteria
Section titled “Acceptance Criteria”AC-XP-001: Ver nivel actual de XP
Section titled “AC-XP-001: Ver nivel actual de XP”US: US-XP-001
AC-XP-001.1 — Resumen XP correcto en dashboard
Dado que soy un estudiante con 750 XP acumulados en eventosY no tengo badges otorgadosCuando llamo a GET /dashboardEntonces la respuesta contiene stats.xpTotal = 750Y stats.currentLevel = 2Y stats.levelName = "Aprendiz IA"Y stats.xpCurrentLevel = 500Y stats.xpNextLevel = 1000AC-XP-001.2 — XP de badges suma al total
Dado que tengo 400 XP de eventosY tengo un badge de tier 3 (gold = 100 XP)Cuando llamo a GET /dashboardEntonces stats.xpTotal = 500Y stats.currentLevel = 1AC-XP-001.3 — Nivel máximo (Maestro IA)
Dado que tengo 5000 XP o másCuando llamo a GET /dashboardEntonces stats.currentLevel = 10Y stats.levelName = "Maestro IA"Y stats.xpNextLevel = 5000AC-XP-002: Recibir XP al completar una lección
Section titled “AC-XP-002: Recibir XP al completar una lección”US: US-XP-002
AC-XP-002.1 — Evento XP creado con 10 puntos
Dado que soy un estudiante autenticadoY la lección "lesson-abc" está publicada y no la he completadoCuando llamo a POST /progress/lesson/lesson-abc/completeEntonces la respuesta contiene { completed: true }Y se crea un StudentXPEvent con eventType = "LESSON_COMPLETED" y xp = 10Y mis stats.xpTotal aumentan en 10AC-XP-002.2 — Segunda llamada es idempotente (upsert)
Dado que ya completé la lección "lesson-abc"Cuando llamo de nuevo a POST /progress/lesson/lesson-abc/completeEntonces la respuesta sigue siendo { completed: true }Y NO se crea un segundo StudentXPEvent para esa lecciónAC-XP-002.3 — Completar con timeSpentSec guarda el tiempo
Dado que completo la lección con body { timeSpentSec: 300 }Cuando llamo a POST /progress/lesson/lesson-abc/completeEntonces el registro Progress.timeSpentSec = 300AC-XP-003: Bonus XP al completar ruta entera
Section titled “AC-XP-003: Bonus XP al completar ruta entera”US: US-XP-003
AC-XP-003.1 — Evento ROUTE_COMPLETED con 100 XP
Dado que la ruta tiene 3 módulos con 1 lección publicada cada unoY ya he completado las 2 primeras leccionesCuando completo la tercera lecciónEntonces la respuesta contiene { pathCompleted: true }Y se crea un StudentXPEvent con eventType = "ROUTE_COMPLETED" y xp = 100AC-XP-003.2 — Certificado emitido al completar si el plan lo permite
Dado que soy usuario STARTER (plan con certificates = true)Y la ruta tiene una plantilla de certificado configuradaCuando se completa la rutaEntonces la respuesta contiene { certificateIssued: true }Y se crea un registro Certificate con verificationHash SHA-256AC-XP-003.3 — No se emite certificado si el plan no lo incluye
Dado que soy usuario FREE (plan con certificates = false)Y la ruta tiene plantilla de certificadoCuando se completa la rutaEntonces { certificateIssued: false }Y no se crea ningún registro CertificateAC-XP-004: Ver y filtrar badges
Section titled “AC-XP-004: Ver y filtrar badges”US: US-XP-004
AC-XP-004.1 — Lista completa de definiciones y grants
Dado que existen badges activos en el sistemaY tengo 2 badges otorgadosCuando llamo a GET /users/me/badgesEntonces la respuesta contiene definitions (todas las activas)Y grants con los 2 badges que tengoY cada grant tiene { badgeId, grantedAt, isActive: true }AC-XP-004.2 — Resumen ligero para cabecera
Dado que tengo 3 badges ganadosY mi XP total es 350Cuando llamo a GET /users/me/badges/summaryEntonces la respuesta contiene { earnedCount: 3, xp: 350 }Y currentLevel es el badge LEVEL de mayor tier que tengo (o null si ninguno)AC-XP-004.3 — Badge revocado no aparece en grants activos
Dado que tenía un badge de racha y luego la perdí (badge con autoRevoke = true)Cuando llamo a GET /users/me/badgesEntonces ese grant NO aparece en la lista de grants (revokedAt != null → filtrado)AC-XP-005: Obtener badge automático al cumplir criterio
Section titled “AC-XP-005: Obtener badge automático al cumplir criterio”US: US-XP-005
AC-XP-005.1 — Badge de XP otorgado al superar umbral
Dado que existe un badge con criteria.type = "xp" y criteria.minXP = 500Y mi XP actual es 490Cuando el evaluador de badges corre (manualmente o vía cron)Y mi XP llega a 500Entonces se crea un StudentBadgeGrant para ese badge con grantedBy = "system"Y el badge aparece en GET /users/me/badges en la sección grantsAC-XP-005.2 — Badge de racha otorgado con 7 días consecutivos
Dado que existe un badge con criteria.type = "streak" y criteria.minDays = 7Y llevo 7 días con al menos una lección completada cada díaCuando corre el evaluadorEntonces se otorga el badge de rachaAC-XP-005.3 — Badge de racha auto-revocado al perder la racha
Dado que tengo el badge de racha de 7 días (con autoRevoke = true en criteria)Y mi racha actual cae a 0Cuando corre el evaluadorEntonces el grant queda con revokedAt != nullY el badge desaparece de GET /users/me/badgesAC-XP-005.4 — Badge manual no se otorga automáticamente
Dado que existe un badge con criteria.type = "manual"Y el estudiante tiene XP suficiente para cualquier badgeCuando corre el evaluadorEntonces ese badge NO se otorga automáticamente (solo vía SUPER_ADMIN)AC-XP-006: Ver racha de días consecutivos
Section titled “AC-XP-006: Ver racha de días consecutivos”US: US-XP-006
AC-XP-006.1 — Racha calculada correctamente (hoy activo)
Dado que completé lecciones el lunes, martes y miércoles (hoy es miércoles UTC)Cuando llamo a GET /users/me/streakEntonces current = 3Y best >= 3Y hasActivityToday = trueAC-XP-006.2 — Racha mantiene si ayer fue activo pero hoy no
Dado que la última lección completada fue ayerY hoy no he hecho nadaCuando llamo a GET /users/me/streakEntonces current >= 1Y hasActivityToday = falseAC-XP-006.3 — weekDays refleja el estado de la semana actual
Dado que hoy es jueves (día 4 de la semana ISO)Y tengo actividad lunes, martes y miércolesCuando llamo a GET /users/me/streakEntonces weekDays tiene 7 elementosY los primeros 3 son "done"Y el 4º es "today" o "risk" (según si hay actividad hoy)Y los elementos 5, 6 y 7 son "future"AC-XP-006.4 — Hito de racha devuelto en la respuesta
Dado que hoy alcanzo exactamente 7 días de racha consecutivaCuando llamo a GET /users/me/streakEntonces la respuesta incluye milestoneReached = 7AC-XP-007: Racha se rompe sin actividad en 24 h
Section titled “AC-XP-007: Racha se rompe sin actividad en 24 h”US: US-XP-007
AC-XP-007.1 — Racha = 0 si el último día activo fue antes de ayer
Dado que la última lección completada fue hace 3 díasY no hay actividad ayer ni hoyCuando llamo a GET /users/me/streakEntonces current = 0Y hasActivityToday = falseAC-XP-007.2 — prevStreak devuelto si la racha se rompió recientemente
Dado que tenía una racha de 5 días que se rompió hace 2 díasY user.lastActiveAt fue hace 2 díasCuando llamo a GET /users/me/streakEntonces current = 0Y la respuesta incluye prevStreak = 5AC-XP-008: Dashboard personalizado — Continuar donde lo dejé
Section titled “AC-XP-008: Dashboard personalizado — Continuar donde lo dejé”US: US-XP-008
AC-XP-008.1 — nextLesson apunta a la primera lección incompleta del path más reciente
Dado que estoy inscrito en la ruta "ia-en-banca" y la tengo al 40%Y la siguiente lección sin completar es "Modelos de Lenguaje Avanzados"Cuando llamo a GET /dashboardEntonces la respuesta contiene nextLesson.id con el id de esa lecciónY nextLesson.pathSlug = "ia-en-banca"Y nextLesson.moduleTitle es el módulo al que perteneceAC-XP-008.2 — nextLesson es null si todas las rutas están completadas
Dado que todas mis rutas inscritas tienen pct = 100Cuando llamo a GET /dashboardEntonces nextLesson = nullAC-XP-008.3 — recentActivity incluye las últimas 8 lecciones completadas
Dado que he completado 10 lecciones en los últimos 7 díasCuando llamo a GET /dashboardEntonces recentActivity tiene 8 elementos como máximoY cada elemento tiene { type: "lesson_completed", description, pathTitle, timestamp }Y están ordenados por timestamp descendenteAC-XP-008.4 — recommendedPaths excluye rutas ya inscritas
Dado que estoy inscrito en "ia-en-banca"Y existen otras 5 rutas publicadasCuando llamo a GET /dashboardEntonces recommendedPaths no contiene "ia-en-banca"Y tiene como máximo 3 rutasAC-XP-009: Progreso detallado por path y módulo
Section titled “AC-XP-009: Progreso detallado por path y módulo”US: US-XP-009
AC-XP-009.1 — Porcentaje por path calculado correctamente
Dado que la ruta "ia-en-banca" tiene 10 lecciones publicadasY he completado 3Cuando llamo a GET /users/me/progressEntonces la ruta "ia-en-banca" en enrolledPaths tiene pct = 30Y status = "in_progress"AC-XP-009.2 — Módulo completo cuando todas sus lecciones están completadas
Dado que el módulo "Fundamentos" tiene 3 lecciones publicadasY he completado las 3Cuando llamo a GET /users/me/progressEntonces ese módulo tiene status = "completed"Y completedLessons = 3Y totalLessons = 3AC-XP-009.3 — Historial XP en actividad reciente
Dado que he completado lecciones y superado quizzes en los últimos díasCuando llamo a GET /users/me/progressEntonces recentActivity contiene elementos con type en ["lesson_completed", "quiz_passed", "quiz_failed", "certificate_earned", "path_enrolled"]Y cada item tiene { type, title, pathTitle, timestamp, meta }Y están ordenados por timestamp descendente (máximo 30)AC-XP-009.4 — Heatmap de actividad devuelve entradas por día
Dado que estudié 30 minutos el lunes y 60 el martesCuando llamo a GET /users/me/progressEntonces activityHeatmap contiene entradas para esos díasY cada entrada tiene { date: "YYYY-MM-DD", timeMinutes }Test Cases
Section titled “Test Cases”TC-XP-001: Dashboard devuelve resumen XP correcto
Section titled “TC-XP-001: Dashboard devuelve resumen XP correcto”AC: AC-XP-001.1 · Prioridad: Alta · Tipo: API · Estado: 📋 Pendiente
Spec: apps/e2e/tests/student/progress-xp.spec.ts
Precondiciones: fixtures.pro tiene historial de XP en BD con al menos 500 XP de eventos
const res = await apiClient.get('/dashboard', { headers: { Authorization: `Bearer ${fixtures.pro.token}` },})expect(res.status).toBe(200)expect(res.data.stats.xpTotal).toBeGreaterThanOrEqual(500)expect(res.data.stats.currentLevel).toBeGreaterThanOrEqual(1)expect(res.data.stats.currentLevel).toBeLessThanOrEqual(10)expect(res.data.stats.levelName).toMatch(/Aprendiz|Practicante|Arquitecto|Experto|Maestro/)expect(res.data.stats.xpCurrentLevel).toBe((res.data.stats.currentLevel - 1) * 500)expect(res.data.stats.xpNextLevel).toBe(res.data.stats.currentLevel * 500)TC-XP-002: Nivel 10 (Maestro IA) con XP >= 5000
Section titled “TC-XP-002: Nivel 10 (Maestro IA) con XP >= 5000”AC: AC-XP-001.3 · Prioridad: Media · Tipo: API · Estado: 📋 Pendiente
// Requiere fixture con XP >= 5000 o inserción directa en BD de testconst res = await apiClient.get('/dashboard', { headers: { Authorization: `Bearer ${fixtures.maestro.token}` },})expect(res.data.stats.currentLevel).toBe(10)expect(res.data.stats.levelName).toBe('Maestro IA')expect(res.data.stats.xpNextLevel).toBe(5000)TC-XP-003: POST /progress/lesson/:id/complete crea evento XP de 10 puntos
Section titled “TC-XP-003: POST /progress/lesson/:id/complete crea evento XP de 10 puntos”AC: AC-XP-002.1 · Prioridad: Alta · Tipo: API · Estado: 📋 Pendiente
Spec: apps/e2e/tests/student/progress-xp.spec.ts
Precondiciones: La lección fixtures.lesson.id existe, está publicada y el usuario fixtures.free no la ha completado
const xpBefore = await getXpTotal(fixtures.free.userId)
const res = await apiClient.post( `/progress/lesson/${fixtures.lesson.id}/complete`, { timeSpentSec: 180 }, { headers: { Authorization: `Bearer ${fixtures.free.token}` } },)expect(res.status).toBe(201)expect(res.data.completed).toBe(true)expect(res.data.lessonId).toBe(fixtures.lesson.id)
const xpAfter = await getXpTotal(fixtures.free.userId)expect(xpAfter - xpBefore).toBe(10)TC-XP-004: Completar la misma lección dos veces es idempotente
Section titled “TC-XP-004: Completar la misma lección dos veces es idempotente”AC: AC-XP-002.2 · Prioridad: Alta · Tipo: API · Estado: 📋 Pendiente
// Primera llamadaawait apiClient.post( `/progress/lesson/${fixtures.lesson.id}/complete`, {}, { headers: { Authorization: `Bearer ${fixtures.free.token}` } },)const xpMid = await getXpTotal(fixtures.free.userId)
// Segunda llamada — no debe añadir más XPawait apiClient.post( `/progress/lesson/${fixtures.lesson.id}/complete`, {}, { headers: { Authorization: `Bearer ${fixtures.free.token}` } },)const xpAfter = await getXpTotal(fixtures.free.userId)
// El XP no debe haber aumentado de nuevo// Nota: el servicio hace upsert del Progress pero emite XP fire-and-forget;// verificar via BD que solo existe 1 evento LESSON_COMPLETED para ese userId+lessonIdexpect(xpAfter).toBeLessThanOrEqual(xpMid + 10)TC-XP-005: Completar última lección de ruta emite ROUTE_COMPLETED (+100 XP)
Section titled “TC-XP-005: Completar última lección de ruta emite ROUTE_COMPLETED (+100 XP)”AC: AC-XP-003.1 · Prioridad: Alta · Tipo: API · Estado: 📋 Pendiente
Precondiciones: fixtures.pro tiene inscripción en una ruta con todas las lecciones completadas menos la última; esa última es fixtures.lastLesson.id
const xpBefore = await getXpTotal(fixtures.pro.userId)
const res = await apiClient.post( `/progress/lesson/${fixtures.lastLesson.id}/complete`, {}, { headers: { Authorization: `Bearer ${fixtures.pro.token}` } },)expect(res.data.pathCompleted).toBe(true)
const xpAfter = await getXpTotal(fixtures.pro.userId)// +10 por lección + 100 por ruta = 110expect(xpAfter - xpBefore).toBe(110)TC-XP-006: GET /users/me/badges devuelve definiciones y grants
Section titled “TC-XP-006: GET /users/me/badges devuelve definiciones y grants”AC: AC-XP-004.1 · Prioridad: Alta · Tipo: API · Estado: 📋 Pendiente
const res = await apiClient.get('/users/me/badges', { headers: { Authorization: `Bearer ${fixtures.pro.token}` },})expect(res.status).toBe(200)expect(Array.isArray(res.data.definitions)).toBe(true)expect(Array.isArray(res.data.grants)).toBe(true)// Estructura de cada definiciónconst def = res.data.definitions[0]expect(def).toMatchObject({ id: expect.any(String), slug: expect.any(String), name: expect.any(String), category: expect.any(String), tier: expect.any(Number), isActive: true,})// Grants del pro deben tener al menos unoexpect(res.data.grants.length).toBeGreaterThan(0)const grant = res.data.grants[0]expect(grant).toMatchObject({ badgeId: expect.any(String), grantedAt: expect.any(String), isActive: true,})TC-XP-007: GET /users/me/badges/summary devuelve conteo y XP
Section titled “TC-XP-007: GET /users/me/badges/summary devuelve conteo y XP”AC: AC-XP-004.2 · Prioridad: Media · Tipo: API · Estado: 📋 Pendiente
const res = await apiClient.get('/users/me/badges/summary', { headers: { Authorization: `Bearer ${fixtures.pro.token}` },})expect(res.status).toBe(200)expect(res.data).toMatchObject({ earnedCount: expect.any(Number), totalCount: expect.any(Number), xp: expect.any(Number),})expect(res.data.earnedCount).toBeLessThanOrEqual(res.data.totalCount)TC-XP-008: El evaluador de badges otorga badge cuando se cumple criterio
Section titled “TC-XP-008: El evaluador de badges otorga badge cuando se cumple criterio”AC: AC-XP-005.1 · Prioridad: Alta · Tipo: API · Estado: 📋 Pendiente
Nota: Requiere acceso SUPER_ADMIN para disparar el evaluador manualmente vía POST /admin/badges/evaluate
// 1. Verificar que el usuario free no tiene el badge "aprendiz-bronce"const before = await apiClient.get('/users/me/badges', { headers: { Authorization: `Bearer ${fixtures.free.token}` },})const hadBadge = before.data.grants.some( (g: any) => g.badgeId === fixtures.badge.aprendizBronceId,)
// 2. Asegurar que el usuario tiene los XP necesarios para el badge (via BD de test)// ...
// 3. Disparar el evaluador como SUPER_ADMINawait apiClient.post( '/admin/badges/evaluate', {}, { headers: { Authorization: `Bearer ${fixtures.superAdmin.token}` } },)
// 4. Esperar procesamiento asíncrono (polling o sleep)await new Promise((r) => setTimeout(r, 2000))
// 5. Verificar que el badge fue otorgadoconst after = await apiClient.get('/users/me/badges', { headers: { Authorization: `Bearer ${fixtures.free.token}` },})const hasBadgeNow = after.data.grants.some( (g: any) => g.badgeId === fixtures.badge.aprendizBronceId,)if (!hadBadge) { expect(hasBadgeNow).toBe(true)}TC-XP-009: GET /users/me/streak devuelve racha actual correcta
Section titled “TC-XP-009: GET /users/me/streak devuelve racha actual correcta”AC: AC-XP-006.1 · Prioridad: Alta · Tipo: API · Estado: 📋 Pendiente
const res = await apiClient.get('/users/me/streak', { headers: { Authorization: `Bearer ${fixtures.pro.token}` },})expect(res.status).toBe(200)expect(res.data).toMatchObject({ current: expect.any(Number), best: expect.any(Number), hasActivityToday: expect.any(Boolean), weekDays: expect.arrayContaining([expect.any(String)]),})expect(res.data.weekDays).toHaveLength(7)// weekDays values must be validconst validStates = ['done', 'today', 'risk', 'missed', 'future']res.data.weekDays.forEach((d: string) => { expect(validStates).toContain(d)})expect(res.data.best).toBeGreaterThanOrEqual(res.data.current)TC-XP-010: Hito de racha devuelve milestoneReached al llegar a 7 días
Section titled “TC-XP-010: Hito de racha devuelve milestoneReached al llegar a 7 días”AC: AC-XP-006.4 · Prioridad: Media · Tipo: API · Estado: 📋 Pendiente
Nota: Requiere insertar en BD de test registros de progreso con completedAt en los 7 últimos días exactos
// Insertar 7 lecciones completadas en días consecutivos via BD de test// incluyendo hoy (día 7)// ...
const res = await apiClient.get('/users/me/streak', { headers: { Authorization: `Bearer ${fixtures.free.token}` },})expect(res.data.current).toBe(7)expect(res.data.milestoneReached).toBe(7)TC-XP-011: Racha = 0 cuando la última actividad fue hace más de 2 días
Section titled “TC-XP-011: Racha = 0 cuando la última actividad fue hace más de 2 días”AC: AC-XP-007.1 · Prioridad: Alta · Tipo: API · Estado: 📋 Pendiente
Precondiciones: Usuario sin actividad en los últimos 3 días (puede usarse fixtures.free recién registrado o usuario con progreso antiguo)
const res = await apiClient.get('/users/me/streak', { headers: { Authorization: `Bearer ${fixtures.inactive.token}` },})expect(res.data.current).toBe(0)expect(res.data.hasActivityToday).toBe(false)// weekDays de días pasados deben ser 'missed'const pastDays = res.data.weekDays.filter((d: string) => d !== 'today' && d !== 'future')pastDays.forEach((d: string) => { expect(['missed', 'done']).toContain(d)})TC-XP-012: Dashboard muestra nextLesson de la ruta más reciente con progreso pendiente
Section titled “TC-XP-012: Dashboard muestra nextLesson de la ruta más reciente con progreso pendiente”AC: AC-XP-008.1 · Prioridad: Alta · Tipo: API · Estado: 📋 Pendiente
const res = await apiClient.get('/dashboard', { headers: { Authorization: `Bearer ${fixtures.pro.token}` },})expect(res.status).toBe(200)
if (res.data.nextLesson !== null) { expect(res.data.nextLesson).toMatchObject({ id: expect.any(String), title: expect.any(String), pathSlug: expect.any(String), pathTitle: expect.any(String), moduleTitle: expect.any(String), })}TC-XP-013: E2E — El banner “Continuar” dirige a la lección correcta
Section titled “TC-XP-013: E2E — El banner “Continuar” dirige a la lección correcta”AC: AC-XP-008.1 · Prioridad: Alta · Tipo: E2E · Estado: 📋 Pendiente
Spec: apps/e2e/tests/student/progress-xp.spec.ts
// data-testid a confirmar en DashboardPage: 'continue-lesson-btn'await loginAs(page, fixtures.pro)await page.goto('/dashboard')
const continueBtn = page.getByTestId('continue-lesson-btn')if (await continueBtn.isVisible()) { const href = await continueBtn.getAttribute('href') expect(href).toMatch(/\/learn\//) await continueBtn.click() await expect(page).toHaveURL(/\/learn\//)}TC-XP-014: GET /users/me/progress devuelve porcentaje por path
Section titled “TC-XP-014: GET /users/me/progress devuelve porcentaje por path”AC: AC-XP-009.1 · Prioridad: Alta · Tipo: API · Estado: 📋 Pendiente
const res = await apiClient.get('/users/me/progress', { headers: { Authorization: `Bearer ${fixtures.pro.token}` },})expect(res.status).toBe(200)expect(Array.isArray(res.data.enrolledPaths)).toBe(true)
for (const ep of res.data.enrolledPaths) { expect(ep.pct).toBeGreaterThanOrEqual(0) expect(ep.pct).toBeLessThanOrEqual(100) expect(['not_started', 'in_progress', 'completed']).toContain(ep.status) expect(ep.path).toMatchObject({ id: expect.any(String), title: expect.any(String), slug: expect.any(String), }) expect(Array.isArray(ep.modules)).toBe(true)}TC-XP-015: Progreso por módulo — lecciones completadas y estado correcto
Section titled “TC-XP-015: Progreso por módulo — lecciones completadas y estado correcto”AC: AC-XP-009.2 · Prioridad: Alta · Tipo: API · Estado: 📋 Pendiente
const res = await apiClient.get('/users/me/progress', { headers: { Authorization: `Bearer ${fixtures.pro.token}` },})
for (const ep of res.data.enrolledPaths) { for (const mod of ep.modules) { expect(mod).toMatchObject({ id: expect.any(String), title: expect.any(String), status: expect.stringMatching(/not_started|in_progress|completed/), completedLessons: expect.any(Number), totalLessons: expect.any(Number), }) expect(mod.completedLessons).toBeLessThanOrEqual(mod.totalLessons) if (mod.status === 'completed') { expect(mod.completedLessons).toBe(mod.totalLessons) } }}Mapa de cobertura
Section titled “Mapa de cobertura”| User Story | ACs | TCs | Endpoints cubiertos |
|---|---|---|---|
| US-XP-001 | AC-XP-001 (3 escenarios) | TC-XP-001, TC-XP-002 | GET /dashboard |
| US-XP-002 | AC-XP-002 (3 escenarios) | TC-XP-003, TC-XP-004 | POST /progress/lesson/:id/complete |
| US-XP-003 | AC-XP-003 (3 escenarios) | TC-XP-005 | POST /progress/lesson/:id/complete |
| US-XP-004 | AC-XP-004 (3 escenarios) | TC-XP-006, TC-XP-007 | GET /users/me/badges, GET /users/me/badges/summary |
| US-XP-005 | AC-XP-005 (4 escenarios) | TC-XP-008 | POST /admin/badges/evaluate |
| US-XP-006 | AC-XP-006 (4 escenarios) | TC-XP-009, TC-XP-010 | GET /users/me/streak |
| US-XP-007 | AC-XP-007 (2 escenarios) | TC-XP-011 | GET /users/me/streak |
| US-XP-008 | AC-XP-008 (4 escenarios) | TC-XP-012, TC-XP-013 | GET /dashboard |
| US-XP-009 | AC-XP-009 (4 escenarios) | TC-XP-014, TC-XP-015 | GET /users/me/progress |