Skip to content

Centro de Práctica

Módulos API: practice (19 endpoints) · Páginas web: PracticeCenterPage, PracticeQuizPlayerPage, PracticeHistoryPage Specs Playwright: apps/e2e/tests/student/practice.spec.ts — 📋 pendientes de crear


MétodoEndpointRol mínimoHistoria
GET/practice/suggestionsAutenticadoUS-PRAC-001
POST/practice/suggestions/:id/dismissAutenticadoUS-PRAC-002
GET/practice/modulesAutenticadoUS-PRAC-003
POST/practice/sessionsAutenticadoUS-PRAC-003
PATCH/practice/sessions/:idAutenticadoUS-PRAC-004
GET/practice/sessionsAutenticadoUS-PRAC-006
GET/practice/statsAutenticadoUS-PRAC-005
GET/practice/quizzes/:id/playerAutenticadoUS-PRAC-004
POST/practice/quizzesCONTENT_ADMIN, SUPER_ADMINUS-PRAC-007
GET/practice/quizzes/:idCONTENT_ADMIN, SUPER_ADMINUS-PRAC-007
PATCH/practice/quizzes/:idCONTENT_ADMIN, SUPER_ADMINUS-PRAC-007
DELETE/practice/quizzes/:idCONTENT_ADMIN, SUPER_ADMINUS-PRAC-007
POST/practice/quizzes/:id/questionsCONTENT_ADMIN, SUPER_ADMINUS-PRAC-007
PATCH/practice/quizzes/:id/questions/:qIdCONTENT_ADMIN, SUPER_ADMINUS-PRAC-007
DELETE/practice/quizzes/:id/questions/:qIdCONTENT_ADMIN, SUPER_ADMINUS-PRAC-007
PATCH/practice/quizzes/:id/questions/reorderCONTENT_ADMIN, SUPER_ADMINUS-PRAC-007
POST/practice/quizzes/:id/import-from-generationCONTENT_ADMIN, SUPER_ADMINUS-PRAC-008
GET/practice/items-by-module/:moduleIdCONTENT_ADMIN, SUPER_ADMINUS-PRAC-007

US-PRAC-001: Ver sugerencias de práctica adaptativas

Section titled “US-PRAC-001: Ver sugerencias de práctica adaptativas”

Como estudiante autenticado Quiero ver las sugerencias que el motor adaptativo ha identificado para mí Para saber qué temas debo reforzar sin tener que decidirlo manualmente

Criterios: AC-PRAC-001 · Casos: TC-PRAC-001, TC-PRAC-002 · Prioridad: Alta · ✅ Implementado


US-PRAC-002: Descartar una sugerencia de práctica

Section titled “US-PRAC-002: Descartar una sugerencia de práctica”

Como estudiante autenticado Quiero descartar sugerencias de práctica que no me interesan o ya domino Para mantener la lista limpia y relevante sin ser interrumpido por alertas irrelevantes

Criterios: AC-PRAC-002 · Casos: TC-PRAC-003, TC-PRAC-004 · Prioridad: Media · ✅ Implementado


US-PRAC-003: Iniciar una sesión de práctica libre

Section titled “US-PRAC-003: Iniciar una sesión de práctica libre”

Como estudiante autenticado Quiero elegir un módulo y comenzar una sesión de práctica libre Para repasar contenido a mi propio ritmo, sin restricciones de calificación

Criterios: AC-PRAC-003 · Casos: TC-PRAC-005, TC-PRAC-006 · Prioridad: Alta · ✅ Implementado


US-PRAC-004: Completar y enviar una sesión de práctica

Section titled “US-PRAC-004: Completar y enviar una sesión de práctica”

Como estudiante en una sesión de práctica activa Quiero poder completar el quiz o actividad y enviar mis resultados Para que el sistema registre mi progreso y recalcule las sugerencias adaptativas

Criterios: AC-PRAC-004 · Casos: TC-PRAC-007, TC-PRAC-008 · Prioridad: Alta · ✅ Implementado


US-PRAC-005: Ver mis estadísticas de práctica acumuladas

Section titled “US-PRAC-005: Ver mis estadísticas de práctica acumuladas”

Como estudiante autenticado Quiero ver un resumen de mis estadísticas de práctica: sesiones semanales, tiempo total, precisión media y racha Para entender mi nivel de constancia y mi evolución en el tiempo

Criterios: AC-PRAC-005 · Casos: TC-PRAC-009 · Prioridad: Media · ✅ Implementado


US-PRAC-006: Ver el historial de sesiones de práctica

Section titled “US-PRAC-006: Ver el historial de sesiones de práctica”

Como estudiante autenticado Quiero ver una lista paginada de todas mis sesiones de práctica anteriores Para revisar en qué módulos he practicado, cuándo y con qué resultados

Criterios: AC-PRAC-006 · Casos: TC-PRAC-010, TC-PRAC-011 · Prioridad: Media · ✅ Implementado


US-PRAC-007: Crear y gestionar un quiz de práctica (creator)

Section titled “US-PRAC-007: Crear y gestionar un quiz de práctica (creator)”

Como CONTENT_ADMIN o SUPER_ADMIN Quiero crear quizzes de práctica con preguntas de tipo opción múltiple, verdadero/falso y respuesta abierta Para dotar a los módulos de material de práctica estructurado para los estudiantes

Criterios: AC-PRAC-007 · Casos: TC-PRAC-012, TC-PRAC-013 · Prioridad: Alta · ✅ Implementado


US-PRAC-008: Importar preguntas generadas por IA a un quiz de práctica

Section titled “US-PRAC-008: Importar preguntas generadas por IA a un quiz de práctica”

Como CONTENT_ADMIN o SUPER_ADMIN Quiero importar preguntas generadas por el motor de IA directamente a un quiz de práctica existente Para agilizar la creación de contenido de práctica sin escribir las preguntas manualmente

Criterios: AC-PRAC-008 · Casos: TC-PRAC-014 · Prioridad: Media · ✅ Implementado


AC-PRAC-001: Ver sugerencias de práctica adaptativas

Section titled “AC-PRAC-001: Ver sugerencias de práctica adaptativas”

US: US-PRAC-001

AC-PRAC-001.1 — Lista de sugerencias activas

Dado que soy un estudiante con historial de actividad (quizzes fallidos o bajo score)
Cuando accedo a "/practice"
Entonces veo hasta 5 sugerencias activas ordenadas por prioridad ascendente
Y cada sugerencia muestra el módulo, el path al que pertenece, y la razón (quiz_failed, low_score, time_lapsed, no_attempt)
Y las sugerencias con dismissedAt !== null no aparecen en la lista

AC-PRAC-001.2 — Estado vacío cuando no hay sugerencias

Dado que soy un estudiante sin sugerencias activas pendientes
Cuando accedo a la sección "Recomendadas para ti"
Entonces veo el mensaje de estado vacío indicando que estoy al día
Y no se muestra ninguna tarjeta de sugerencia

AC-PRAC-001.3 — Acceso restringido a autenticados

Dado que soy un visitante no autenticado
Cuando llamo a GET /practice/suggestions sin token JWT
Entonces la API responde con status 401

US: US-PRAC-002

AC-PRAC-002.1 — Descarte propio exitoso

Dado que tengo una sugerencia activa con ID "sug-abc"
Cuando hago clic en "Descartar" en esa sugerencia
Entonces la API responde con status 200 o 201
Y la sugerencia desaparece de la lista visible (dismissedAt se rellena)
Y el motor la puede regenerar si la condición persiste en la próxima evaluación

AC-PRAC-002.2 — No se puede descartar sugerencia de otro usuario

Dado que la sugerencia "sug-xyz" pertenece al usuario B
Cuando el usuario A llama a POST /practice/suggestions/sug-xyz/dismiss
Entonces la API responde con status 403

AC-PRAC-002.3 — Sugerencia inexistente retorna 404

Dado que el ID "sug-nonexistent" no existe en la BD
Cuando llamo a POST /practice/suggestions/sug-nonexistent/dismiss
Entonces la API responde con status 404

AC-PRAC-003: Iniciar una sesión de práctica libre

Section titled “AC-PRAC-003: Iniciar una sesión de práctica libre”

US: US-PRAC-003

AC-PRAC-003.1 — Módulos de práctica disponibles solo para módulos con progreso

Dado que soy estudiante con progreso en los módulos M1 y M2
Cuando llamo a GET /practice/modules
Entonces recibo únicamente M1 y M2 con sus practiceItems publicados
Y cada módulo incluye lastPracticedAt, practiceCount y lastScorePercent

AC-PRAC-003.2 — Búsqueda y filtrado de módulos

Dado que tengo progreso en varios módulos
Cuando llamo a GET /practice/modules?search=python&filter=with_items
Entonces solo se devuelven módulos cuyo título contiene "python" y que tienen al menos un practiceItem publicado

AC-PRAC-003.3 — Creación de sesión libre exitosa

Dado que selecciono el módulo M1 y el ítem de práctica P1
Cuando llamo a POST /practice/sessions con { moduleId, practiceItemId, mode: "FREE" }
Entonces la API responde con status 201 y { sessionId: "<uuid>" }
Y se crea un registro PracticeSession en BD con studentId = mi ID

AC-PRAC-003.4 — Límite diario de 5 sesiones por ítem

Dado que ya he iniciado 5 sesiones de práctica para el ítem P1 hoy
Cuando intento crear una 6ª sesión para P1
Entonces la API responde con status 400
Y el mensaje de error indica que se alcanzó el límite diario

AC-PRAC-003.5 — Módulo inexistente retorna 404

Dado que el moduleId enviado no existe en BD
Cuando llamo a POST /practice/sessions
Entonces la API responde con status 404

AC-PRAC-004: Completar y enviar una sesión de práctica

Section titled “AC-PRAC-004: Completar y enviar una sesión de práctica”

US: US-PRAC-004

AC-PRAC-004.1 — Completar sesión con scorePercent

Dado que tengo una sesión activa con sessionId "ses-123"
Cuando llamo a PATCH /practice/sessions/ses-123 con { scorePercent: 85, completedInSec: 240 }
Entonces la API responde con la sesión actualizada con completedAt relleno
Y se dispara una notificación de tipo PRACTICE_SESSION_COMPLETED al estudiante
Y el motor adaptativo re-evalúa sugerencias en background (fire-and-forget)

AC-PRAC-004.2 — Sugerencia vinculada se descarta automáticamente al completar

Dado que la sesión estaba vinculada a un practiceItem con una sugerencia activa en el mismo módulo
Cuando completo la sesión
Entonces la sugerencia activa de ese módulo queda marcada con dismissedAt

AC-PRAC-004.3 — No se puede completar sesión de otro usuario

Dado que la sesión "ses-123" pertenece al usuario B
Cuando el usuario A llama a PATCH /practice/sessions/ses-123
Entonces la API responde con status 403

AC-PRAC-004.4 — Player del quiz: cálculo de score

Dado que abro un quiz de práctica con preguntas SINGLE_CHOICE y TRUE_FALSE
Cuando respondo todas las preguntas y envío
Entonces la página calcula el scorePercent localmente antes de mostrar resultados
Y la pregunta de tipo OPEN_ENDED requiere autoevaluación (correcto / incorrecto) para contribuir al score
Y al finalizar se llama a PATCH /practice/sessions/:id con el scorePercent calculado

AC-PRAC-005: Ver estadísticas de práctica acumuladas

Section titled “AC-PRAC-005: Ver estadísticas de práctica acumuladas”

US: US-PRAC-005

AC-PRAC-005.1 — Métricas correctas en el strip de estadísticas

Dado que soy un estudiante con historial de sesiones
Cuando accedo a GET /practice/stats
Entonces recibo sessionsThisWeek, sessionsPrevWeek, totalTimeSec, avgScorePercent, practiceStreak y heatmap (14 días)
Y avgScorePercent solo promedia sesiones con completedAt y scorePercent no nulos
Y practiceStreak cuenta días consecutivos hacia atrás desde hoy con al menos 1 sesión

AC-PRAC-005.2 — Racha se muestra en la UI como heatmap de 14 días

Dado que accedo a la página de práctica
Cuando el componente StreakPracticeCard carga
Entonces veo 14 cuadros de colores representando la actividad diaria
Y los días sin sesión aparecen en gris, con 1 sesión en amarillo, con 2+ en verde

AC-PRAC-005.3 — Estudiante sin sesiones recibe métricas en cero

Dado que soy un estudiante sin ninguna sesión de práctica previa
Cuando llamo a GET /practice/stats
Entonces recibo { sessionsThisWeek: 0, totalTimeSec: 0, avgScorePercent: 0, practiceStreak: 0 }

AC-PRAC-006: Ver historial de sesiones de práctica

Section titled “AC-PRAC-006: Ver historial de sesiones de práctica”

US: US-PRAC-006

AC-PRAC-006.1 — Lista paginada de sesiones, ordenada por más reciente

Dado que tengo 45 sesiones registradas
Cuando llamo a GET /practice/sessions?page=1&limit=20
Entonces recibo { items: [...20 sesiones], total: 45, page: 1, limit: 20 }
Y las sesiones están ordenadas por createdAt descendente
Y cada sesión incluye moduleTitle, mode, scorePercent, completedInSec y createdAt

AC-PRAC-006.2 — Estado vacío en historial sin sesiones

Dado que soy un estudiante sin sesiones previas
Cuando accedo a "/practice/history"
Entonces veo el estado vacío con CTA para ir a practicar
Y no se muestra ninguna fila en la lista

AC-PRAC-006.3 — Solo sesiones propias del estudiante

Dado que la BD tiene sesiones de múltiples usuarios
Cuando llamo a GET /practice/sessions
Entonces solo recibo mis propias sesiones (filtrado por studentId = mi ID)

AC-PRAC-007: Crear y gestionar un quiz de práctica (creator)

Section titled “AC-PRAC-007: Crear y gestionar un quiz de práctica (creator)”

US: US-PRAC-007

AC-PRAC-007.1 — Creación de quiz y vínculo automático al banco de práctica

Dado que soy CONTENT_ADMIN y el módulo M1 existe
Cuando llamo a POST /practice/quizzes con { moduleId, title, description }
Entonces se crea un PracticeQuiz con status DRAFT
Y se crea automáticamente un PracticeItem de tipo PRACTICE_QUIZ vinculado al módulo M1
Y la respuesta incluye el quiz con sus preguntas (vacías inicialmente)

AC-PRAC-007.2 — Añadir preguntas a un quiz

Dado que tengo un PracticeQuiz con ID "quiz-123"
Cuando llamo a POST /practice/quizzes/quiz-123/questions con { type: "SINGLE_CHOICE", question: "¿Qué es X?", options: [...], correctAnswer: "opt-1" }
Entonces se crea la pregunta con order incrementado automáticamente

AC-PRAC-007.3 — Reordenar preguntas

Dado que el quiz "quiz-123" tiene las preguntas [q1, q2, q3]
Cuando llamo a PATCH /practice/quizzes/quiz-123/questions/reorder con { questionIds: ["q3","q1","q2"] }
Entonces el order de las preguntas se actualiza en BD
Y la API responde con status 200

AC-PRAC-007.4 — Eliminar pregunta renormaliza el orden

Dado que el quiz tiene preguntas con order [0, 1, 2]
Cuando elimino la pregunta con order 1 (DELETE /practice/quizzes/:id/questions/:qId)
Entonces las preguntas restantes quedan con order [0, 1] (sin huecos)

AC-PRAC-007.5 — Acceso restringido a roles creator

Dado que soy un usuario con rol FREE, STARTER, PRO o ELITE
Cuando llamo a POST /practice/quizzes
Entonces la API responde con status 403

AC-PRAC-008: Importar preguntas generadas por IA

Section titled “AC-PRAC-008: Importar preguntas generadas por IA”

US: US-PRAC-008

AC-PRAC-008.1 — Importación exitosa de preguntas de IA

Dado que tengo un PracticeQuiz "quiz-123" y una tarea de IA con ID "task-456" con preguntas aceptadas
Cuando llamo a POST /practice/quizzes/quiz-123/import-from-generation con { taskId, acceptedQuestions: [...] }
Entonces se crean las preguntas en BD con aiGenerated = true
Y la AITask se marca con reviewStatus = "ACCEPTED"
Y la respuesta incluye { importedCount: N }

AC-PRAC-008.2 — Tipos de pregunta se normalizan correctamente

Dado que las preguntas generadas tienen tipo MULTIPLE_CHOICE
Cuando se importan al quiz de práctica
Entonces se almacenan como SINGLE_CHOICE (normalización de tipo)
Y las preguntas TRUE_FALSE y preguntas abiertas mantienen sus tipos correspondientes

AC-PRAC-008.3 — Quiz inexistente retorna 404

Dado que el quizId enviado no existe
Cuando llamo a POST /practice/quizzes/nonexistent/import-from-generation
Entonces la API responde con status 404

TC-PRAC-001: Sugerencias activas se muestran correctamente

Section titled “TC-PRAC-001: Sugerencias activas se muestran correctamente”

AC: AC-PRAC-001.1 · Prioridad: Alta · Tipo: API · Estado: 📋 Pendiente Spec: apps/e2e/tests/student/practice.spec.ts

Precondiciones: fixtures.pro tiene sesiones con score bajo en al menos un módulo

const res = await apiClient.get('/practice/suggestions', {
headers: { Authorization: `Bearer ${fixtures.pro.token}` },
})
expect(res.status).toBe(200)
expect(Array.isArray(res.data)).toBe(true)
expect(res.data.length).toBeLessThanOrEqual(5)
// Verificar estructura de cada sugerencia
const suggestion = res.data[0]
expect(suggestion).toHaveProperty('id')
expect(suggestion).toHaveProperty('moduleTitle')
expect(suggestion).toHaveProperty('pathTitle')
expect(suggestion).toHaveProperty('reason')
expect(suggestion).toHaveProperty('priority')
expect(suggestion.dismissedAt).toBeNull()
// Sugerencias están ordenadas por prioridad ascendente
const priorities = res.data.map((s: { priority: number }) => s.priority)
const sorted = [...priorities].sort((a: number, b: number) => a - b)
expect(priorities).toEqual(sorted)

TC-PRAC-002: Sin token retorna 401 en sugerencias

Section titled “TC-PRAC-002: Sin token retorna 401 en sugerencias”

AC: AC-PRAC-001.3 · Prioridad: Alta · Tipo: API · Estado: 📋 Pendiente

const res = await apiClient.get('/practice/suggestions')
expect(res.status).toBe(401)

AC: AC-PRAC-002.1 · Prioridad: Alta · Tipo: API · Estado: 📋 Pendiente Spec: apps/e2e/tests/student/practice.spec.ts

Precondiciones: fixtures.pro tiene al menos una sugerencia activa

// 1. Obtener sugerencias activas
const listRes = await apiClient.get('/practice/suggestions', {
headers: { Authorization: `Bearer ${fixtures.pro.token}` },
})
expect(listRes.data.length).toBeGreaterThan(0)
const suggestionId = listRes.data[0].id
// 2. Descartar la primera sugerencia
const dismissRes = await apiClient.post(
`/practice/suggestions/${suggestionId}/dismiss`,
{},
{ headers: { Authorization: `Bearer ${fixtures.pro.token}` } }
)
expect(dismissRes.status).toBeLessThan(300)
// 3. Verificar que ya no aparece en la lista activa
const listAfterRes = await apiClient.get('/practice/suggestions', {
headers: { Authorization: `Bearer ${fixtures.pro.token}` },
})
const ids = listAfterRes.data.map((s: { id: string }) => s.id)
expect(ids).not.toContain(suggestionId)

TC-PRAC-004: No se puede descartar sugerencia de otro usuario

Section titled “TC-PRAC-004: No se puede descartar sugerencia de otro usuario”

AC: AC-PRAC-002.2 · Prioridad: Alta · Tipo: API · Estado: 📋 Pendiente

// fixtures.pro tiene la sugerencia, fixtures.starter intenta descartarla
const listRes = await apiClient.get('/practice/suggestions', {
headers: { Authorization: `Bearer ${fixtures.pro.token}` },
})
const proSuggestionId = listRes.data[0]?.id
if (!proSuggestionId) return // skip si no hay sugerencias
const res = await apiClient.post(
`/practice/suggestions/${proSuggestionId}/dismiss`,
{},
{ headers: { Authorization: `Bearer ${fixtures.starter.token}` } }
)
expect(res.status).toBe(403)

TC-PRAC-005: Iniciar sesión de práctica libre

Section titled “TC-PRAC-005: Iniciar sesión de práctica libre”

AC: AC-PRAC-003.3 · Prioridad: Alta · Tipo: API · Estado: 📋 Pendiente Spec: apps/e2e/tests/student/practice.spec.ts

Precondiciones: fixtures.pro tiene progreso en al menos un módulo con practiceItems publicados

// Obtener módulos de práctica disponibles
const modulesRes = await apiClient.get('/practice/modules', {
headers: { Authorization: `Bearer ${fixtures.pro.token}` },
})
expect(modulesRes.status).toBe(200)
const moduleWithItems = modulesRes.data.items.find(
(m: { practiceItems: unknown[] }) => m.practiceItems.length > 0
)
if (!moduleWithItems) return // skip si no hay módulos con items
const practiceItem = moduleWithItems.practiceItems[0]
// Crear sesión
const sessionRes = await apiClient.post(
'/practice/sessions',
{
moduleId: moduleWithItems.moduleId,
practiceItemId: practiceItem.id,
mode: 'FREE',
triggerReason: 'manual',
},
{ headers: { Authorization: `Bearer ${fixtures.pro.token}` } }
)
expect(sessionRes.status).toBe(201)
expect(sessionRes.data).toHaveProperty('sessionId')
expect(typeof sessionRes.data.sessionId).toBe('string')

TC-PRAC-006: Límite diario de sesiones por ítem

Section titled “TC-PRAC-006: Límite diario de sesiones por ítem”

AC: AC-PRAC-003.4 · Prioridad: Media · Tipo: API · Estado: 📋 Pendiente

// Este test requiere fixtures con 5 sesiones pre-existentes para el mismo ítem hoy.
// Crear una sexta sesión debe fallar.
const res = await apiClient.post(
'/practice/sessions',
{
moduleId: fixtures.pro.moduleWithDailyLimit.moduleId,
practiceItemId: fixtures.pro.moduleWithDailyLimit.practiceItemId,
mode: 'FREE',
},
{ headers: { Authorization: `Bearer ${fixtures.pro.token}` } }
)
expect(res.status).toBe(400)
expect(res.data.message).toMatch(/límite/)

TC-PRAC-007: Completar sesión con score registrado

Section titled “TC-PRAC-007: Completar sesión con score registrado”

AC: AC-PRAC-004.1 · Prioridad: Alta · Tipo: API · Estado: 📋 Pendiente Spec: apps/e2e/tests/student/practice.spec.ts

// 1. Crear sesión
const sessionRes = await apiClient.post(
'/practice/sessions',
{ moduleId: fixtures.pro.moduleId, mode: 'FREE' },
{ headers: { Authorization: `Bearer ${fixtures.pro.token}` } }
)
const sessionId = sessionRes.data.sessionId
// 2. Completar la sesión
const completeRes = await apiClient.patch(
`/practice/sessions/${sessionId}`,
{
scorePercent: 75,
completedInSec: 180,
completedAt: new Date().toISOString(),
},
{ headers: { Authorization: `Bearer ${fixtures.pro.token}` } }
)
expect(completeRes.status).toBe(200)
expect(completeRes.data.scorePercent).toBe(75)
expect(completeRes.data.completedAt).not.toBeNull()
expect(completeRes.data.completedInSec).toBe(180)

TC-PRAC-008: No se puede completar sesión ajena

Section titled “TC-PRAC-008: No se puede completar sesión ajena”

AC: AC-PRAC-004.3 · Prioridad: Alta · Tipo: API · Estado: 📋 Pendiente

// Crear sesión con fixtures.pro
const sessionRes = await apiClient.post(
'/practice/sessions',
{ moduleId: fixtures.pro.moduleId, mode: 'FREE' },
{ headers: { Authorization: `Bearer ${fixtures.pro.token}` } }
)
const sessionId = sessionRes.data.sessionId
// Intentar completar con fixtures.starter
const res = await apiClient.patch(
`/practice/sessions/${sessionId}`,
{ scorePercent: 50 },
{ headers: { Authorization: `Bearer ${fixtures.starter.token}` } }
)
expect(res.status).toBe(403)

TC-PRAC-009: Estadísticas de práctica — estructura y valores correctos

Section titled “TC-PRAC-009: Estadísticas de práctica — estructura y valores correctos”

AC: AC-PRAC-005.1, AC-PRAC-005.3 · Prioridad: Media · Tipo: API · Estado: 📋 Pendiente

// Estadísticas con historial (fixtures.pro)
const resWithHistory = await apiClient.get('/practice/stats', {
headers: { Authorization: `Bearer ${fixtures.pro.token}` },
})
expect(resWithHistory.status).toBe(200)
const stats = resWithHistory.data
expect(stats).toHaveProperty('sessionsThisWeek')
expect(stats).toHaveProperty('sessionsPrevWeek')
expect(stats).toHaveProperty('totalTimeSec')
expect(stats).toHaveProperty('avgScorePercent')
expect(stats).toHaveProperty('practiceStreak')
expect(stats).toHaveProperty('heatmap')
expect(Array.isArray(stats.heatmap)).toBe(true)
expect(stats.heatmap).toHaveLength(14)
expect(stats.heatmap[0]).toHaveProperty('date')
expect(stats.heatmap[0]).toHaveProperty('count')
// Estadísticas sin historial (fixtures.free)
const resEmpty = await apiClient.get('/practice/stats', {
headers: { Authorization: `Bearer ${fixtures.free.token}` },
})
expect(resEmpty.data.sessionsThisWeek).toBe(0)
expect(resEmpty.data.totalTimeSec).toBe(0)
expect(resEmpty.data.avgScorePercent).toBe(0)
expect(resEmpty.data.practiceStreak).toBe(0)

TC-PRAC-010: Historial de sesiones paginado

Section titled “TC-PRAC-010: Historial de sesiones paginado”

AC: AC-PRAC-006.1 · Prioridad: Media · Tipo: API · Estado: 📋 Pendiente Spec: apps/e2e/tests/student/practice.spec.ts

const res = await apiClient.get('/practice/sessions?page=1&limit=20', {
headers: { Authorization: `Bearer ${fixtures.pro.token}` },
})
expect(res.status).toBe(200)
expect(res.data).toHaveProperty('items')
expect(res.data).toHaveProperty('total')
expect(res.data).toHaveProperty('page')
expect(res.data).toHaveProperty('limit')
expect(Array.isArray(res.data.items)).toBe(true)
expect(res.data.items.length).toBeLessThanOrEqual(20)
// Verificar que las sesiones están ordenadas por fecha descendente
if (res.data.items.length > 1) {
const dates = res.data.items.map(
(s: { createdAt: string }) => new Date(s.createdAt).getTime()
)
for (let i = 0; i < dates.length - 1; i++) {
expect(dates[i]).toBeGreaterThanOrEqual(dates[i + 1])
}
}
// Verificar que solo devuelve sesiones propias
const studentIds = res.data.items.map((s: { studentId: string }) => s.studentId)
const uniqueStudentIds = [...new Set(studentIds)]
expect(uniqueStudentIds).toHaveLength(1)
expect(uniqueStudentIds[0]).toBe(fixtures.pro.userId)

TC-PRAC-011: Historial vacío en PracticeHistoryPage

Section titled “TC-PRAC-011: Historial vacío en PracticeHistoryPage”

AC: AC-PRAC-006.2 · Prioridad: Baja · Tipo: E2E · Estado: 📋 Pendiente Spec: apps/e2e/tests/student/practice.spec.ts

// data-testid a confirmar en PracticeHistoryPage:
// 'practice-history-empty', 'go-to-practice-btn'
await loginAs(page, fixtures.free)
await page.goto('/practice/history')
await expect(page.getByTestId('practice-history-empty')).toBeVisible()
await page.getByTestId('go-to-practice-btn').click()
await expect(page).toHaveURL('/practice')

TC-PRAC-012: Crear quiz de práctica con restricción de rol

Section titled “TC-PRAC-012: Crear quiz de práctica con restricción de rol”

AC: AC-PRAC-007.1, AC-PRAC-007.5 · Prioridad: Alta · Tipo: API · Estado: 📋 Pendiente

// Creación exitosa como CONTENT_ADMIN
// (requiere token de content_admin en fixtures)
const createRes = await apiClient.post(
'/practice/quizzes',
{
moduleId: fixtures.contentAdmin.testModuleId,
title: 'Quiz de prueba QA',
description: 'Creado por test automatizado',
},
{ headers: { Authorization: `Bearer ${fixtures.contentAdmin.token}` } }
)
expect(createRes.status).toBe(201)
expect(createRes.data).toHaveProperty('id')
expect(createRes.data.status).toBe('DRAFT')
expect(createRes.data.questions).toHaveLength(0)
// Acceso denegado a roles de estudiante
const forbiddenRes = await apiClient.post(
'/practice/quizzes',
{ moduleId: fixtures.pro.moduleId, title: 'No permitido' },
{ headers: { Authorization: `Bearer ${fixtures.pro.token}` } }
)
expect(forbiddenRes.status).toBe(403)

TC-PRAC-013: Añadir preguntas y reordenar en quiz de práctica

Section titled “TC-PRAC-013: Añadir preguntas y reordenar en quiz de práctica”

AC: AC-PRAC-007.2, AC-PRAC-007.3 · Prioridad: Alta · Tipo: API · Estado: 📋 Pendiente

const quizId = fixtures.contentAdmin.testQuizId
// Añadir pregunta de opción múltiple
const q1Res = await apiClient.post(
`/practice/quizzes/${quizId}/questions`,
{
type: 'SINGLE_CHOICE',
question: '¿Cuál es la capital de Francia?',
options: [
{ id: 'a', text: 'Londres' },
{ id: 'b', text: 'París' },
{ id: 'c', text: 'Berlín' },
],
correctAnswer: 'b',
explanation: 'París es la capital de Francia.',
},
{ headers: { Authorization: `Bearer ${fixtures.contentAdmin.token}` } }
)
expect(q1Res.status).toBe(201)
const q1Id = q1Res.data.id
// Añadir segunda pregunta V/F
const q2Res = await apiClient.post(
`/practice/quizzes/${quizId}/questions`,
{
type: 'TRUE_FALSE',
question: 'El sol es una estrella',
correctAnswer: 'true',
},
{ headers: { Authorization: `Bearer ${fixtures.contentAdmin.token}` } }
)
const q2Id = q2Res.data.id
// Reordenar: q2 primero, q1 segundo
const reorderRes = await apiClient.patch(
`/practice/quizzes/${quizId}/questions/reorder`,
{ questionIds: [q2Id, q1Id] },
{ headers: { Authorization: `Bearer ${fixtures.contentAdmin.token}` } }
)
expect(reorderRes.status).toBe(200)
expect(reorderRes.data).toEqual({ ok: true })

TC-PRAC-014: Importar preguntas desde generación IA

Section titled “TC-PRAC-014: Importar preguntas desde generación IA”

AC: AC-PRAC-008.1, AC-PRAC-008.2 · Prioridad: Media · Tipo: API · Estado: 📋 Pendiente

const quizId = fixtures.contentAdmin.testQuizId
const aiTaskId = fixtures.contentAdmin.testAiTaskId // AITask con status PENDING
const importRes = await apiClient.post(
`/practice/quizzes/${quizId}/import-from-generation`,
{
taskId: aiTaskId,
acceptedQuestions: [
{
questionText: '¿Qué es el aprendizaje automático?',
type: 'MULTIPLE_CHOICE',
options: [
{ id: 'o1', text: 'Un tipo de base de datos', isCorrect: false },
{ id: 'o2', text: 'Un subconjunto de la IA que aprende de datos', isCorrect: true },
],
correctAnswer: null,
explanation: 'El ML es la capacidad de aprender sin ser programado explícitamente.',
},
{
questionText: 'Python es un lenguaje interpretado',
type: 'TRUE_FALSE',
options: [],
correctAnswer: 'true',
explanation: null,
},
],
},
{ headers: { Authorization: `Bearer ${fixtures.contentAdmin.token}` } }
)
expect(importRes.status).toBe(201)
expect(importRes.data.importedCount).toBe(2)
// Verificar que la pregunta MULTIPLE_CHOICE fue convertida a SINGLE_CHOICE
const quizRes = await apiClient.get(
`/practice/quizzes/${quizId}`,
{ headers: { Authorization: `Bearer ${fixtures.contentAdmin.token}` } }
)
const importedQ = quizRes.data.questions.find(
(q: { aiGenerated: boolean }) => q.aiGenerated === true
)
expect(importedQ).toBeDefined()
expect(importedQ.type).toBe('SINGLE_CHOICE')