Lesson Player
Módulos API: paths (learn endpoints), progress, guide-sections
Páginas web: LessonPlayerPage, LearnRedirectPage
Spec Playwright: apps/e2e/tests/student/lesson-player.spec.ts — 📋 pendiente
Módulos API cubiertos
Section titled “Módulos API cubiertos”| Endpoint | Guard | Descripción |
|---|---|---|
GET /paths/:slug/learn | JwtAuthGuard | Estructura del path + progreso por lección para el sidebar |
GET /paths/:slug/learn/:lessonId | JwtAuthGuard + access level | Contenido completo de la lección |
POST /progress/lesson/:lessonId/complete | JwtAuthGuard | Marcar lección completada, emitir XP, detectar módulo/path completo |
GET /progress/lesson/:lessonId | JwtAuthGuard | Estado de progreso de una lección concreta |
GET /guide-sections | público | Secciones de la guía de conocimiento publicadas |
GET /paths/:slug/preview/structure | JwtAuthGuard + CONTENT_ADMIN | Estructura para preview del creator |
GET /paths/:slug/preview/lessons/:lessonId | JwtAuthGuard + CONTENT_ADMIN | Contenido de lección para preview del creator |
User Stories
Section titled “User Stories”US-LESS-001: Reproducir el contenido de una lección con acceso correcto
Section titled “US-LESS-001: Reproducir el contenido de una lección con acceso correcto”Como estudiante con el nivel de suscripción adecuado Quiero abrir una lección y ver su contenido (video, texto o interactivo) Para aprender a mi ritmo dentro del path al que estoy inscrito
Criterios: AC-LESS-001 · Casos: TC-LESS-001, TC-LESS-002 · Prioridad: Alta · ✅ Implementado
US-LESS-002: Navegar a la lección anterior y siguiente
Section titled “US-LESS-002: Navegar a la lección anterior y siguiente”Como estudiante reproduciendo una lección Quiero usar los botones “Anterior” y “Siguiente” en la barra de acciones Para avanzar o retroceder sin volver al catálogo
Criterios: AC-LESS-002 · Casos: TC-LESS-003 · Prioridad: Alta · ✅ Implementado
US-LESS-003: Marcar una lección como completada
Section titled “US-LESS-003: Marcar una lección como completada”Como estudiante que ha visto el contenido de una lección Quiero pulsar “Marcar como completada” Para registrar mi progreso y desbloquear el avance en el path
Criterios: AC-LESS-003 · Casos: TC-LESS-004, TC-LESS-005 · Prioridad: Alta · ✅ Implementado
US-LESS-004: Ver el progreso del path en el sidebar de navegación
Section titled “US-LESS-004: Ver el progreso del path en el sidebar de navegación”Como estudiante dentro del player Quiero ver en el sidebar lateral los módulos, lecciones y mi porcentaje de avance Para saber cuánto me falta para completar el path
Criterios: AC-LESS-004 · Casos: TC-LESS-006 · Prioridad: Alta · ✅ Implementado
US-LESS-005: Intentar acceder a una lección de nivel superior (CTA de upgrade)
Section titled “US-LESS-005: Intentar acceder a una lección de nivel superior (CTA de upgrade)”Como estudiante con plan FREE o STARTER Quiero ver un mensaje claro que me indica qué plan necesito Para poder tomar la decisión de actualizar mi suscripción sin encontrarme un error técnico
Criterios: AC-LESS-005 · Casos: TC-LESS-007, TC-LESS-008 · Prioridad: Alta · ✅ Implementado
US-LESS-006: Acceder a la guía de conocimiento vinculada al path
Section titled “US-LESS-006: Acceder a la guía de conocimiento vinculada al path”Como estudiante en el player Quiero consultar las secciones de la guía disponibles en el módulo que estoy estudiando Para profundizar en los conceptos teóricos de la lección
Criterios: AC-LESS-006 · Casos: TC-LESS-009 · Prioridad: Media · ✅ Implementado
US-LESS-007: Redirección inteligente a la primera lección pendiente
Section titled “US-LESS-007: Redirección inteligente a la primera lección pendiente”Como estudiante que accede a /learn/:pathSlug sin especificar lección
Quiero ser redirigido automáticamente a la primera lección incompleta (o a la primera si todo está hecho)
Para continuar donde lo dejé sin tener que buscar manualmente
Criterios: AC-LESS-007 · Casos: TC-LESS-010 · Prioridad: Alta · ✅ Implementado
US-LESS-008: CONTENT_ADMIN puede previsualizar cualquier lección sin restricción de acceso
Section titled “US-LESS-008: CONTENT_ADMIN puede previsualizar cualquier lección sin restricción de acceso”Como CONTENT_ADMIN que está revisando un path antes de publicarlo
Quiero acceder al player en modo preview (?preview=true)
Para ver exactamente lo que verá un estudiante, independientemente de su accessLevel
Criterios: AC-LESS-008 · Casos: TC-LESS-011 · Prioridad: Alta · ✅ Implementado
US-LESS-009: Completar el path y recibir confirmación de certificado
Section titled “US-LESS-009: Completar el path y recibir confirmación de certificado”Como estudiante que acaba de completar la última lección del path Quiero ver un modal de felicitación que me indique si se ha emitido un certificado Para saber inmediatamente que mi esfuerzo ha quedado registrado y puedo descargarlo
Criterios: AC-LESS-009 · Casos: TC-LESS-012 · Prioridad: Alta · ✅ Implementado
Acceptance Criteria
Section titled “Acceptance Criteria”AC-LESS-001: Reproducir lección con acceso correcto
Section titled “AC-LESS-001: Reproducir lección con acceso correcto”US: US-LESS-001
AC-LESS-001.1 — Lección de tipo VIDEO renderiza el player
Dado que soy un usuario STARTER autenticadoY existe una lección con accessLevel PAID_STARTER y contentType VIDEOCuando navego a /learn/:pathSlug/:lessonIdEntonces la API responde 200 con el contenido de la lecciónY el player muestra el video (iframe YouTube o streaming-player nativo)Y veo el título de la lección y el breadcrumb con módulo y número de ordenAC-LESS-001.2 — Lección de tipo MARKDOWN renderiza el contenido rich text
Dado que soy un usuario FREE autenticadoY existe una lección con accessLevel FREE_REGISTERED y contentType MARKDOWNCuando navego a /learn/:pathSlug/:lessonIdEntonces el área de contenido muestra el texto renderizado (headings, listas, código)Y no se muestra ningún reproductor de videoAC-LESS-001.3 — Lección MIXED muestra pestañas Video / Contenido
Dado que la lección tiene contentType MIXED con videoUrl y contentMdCuando cargo la lecciónEntonces veo dos pestañas: "▶ Video" y "☰ Contenido"Y al cambiar de pestaña se muestra el contenido correspondiente sin recargar la páginaAC-LESS-001.4 — Lección H5P embebe el iframe de recursos
Dado que la lección tiene contentType H5P y un attachment con role MAIN_CONTENT y type H5PCuando cargo la lecciónEntonces se renderiza un iframe apuntando a /resources/:resourceId/h5p-serveAC-LESS-002: Navegación entre lecciones
Section titled “AC-LESS-002: Navegación entre lecciones”US: US-LESS-002
AC-LESS-002.1 — Botón “Anterior” habilitado cuando hay lección previa
Dado que estoy en la segunda lección del pathCuando cargo la páginaEntonces el botón "← Anterior" está activoY al pulsarlo navego a /learn/:pathSlug/:prevLessonId sin recargar el sidebarAC-LESS-002.2 — Botón “Anterior” deshabilitado en la primera lección
Dado que estoy en la primera lección del pathCuando cargo la páginaEntonces el botón "← Anterior" aparece deshabilitado (no interactivo)AC-LESS-002.3 — Botón “Siguiente” lleva al quiz cuando es el último item real del módulo
Dado que estoy en la última lección real de un módulo que tiene quiz publicado no superadoCuando pulso "Siguiente"Entonces navego a /learn/:pathSlug/quiz/:moduleIdY el botón mostraba el texto "Ir al quiz →" antes de pulsarloAC-LESS-002.4 — Botón “Siguiente” navega a la lección siguiente en caso normal
Dado que estoy en una lección con nextLesson disponible y sin quiz gate activoCuando pulso "Siguiente →"Entonces navego a /learn/:pathSlug/:nextLessonIdAC-LESS-002.5 — Botón “Siguiente” deshabilitado en la última lección del path (sin quiz pendiente)
Dado que estoy en la última lección del path y no hay quiz pendienteCuando cargo la páginaEntonces el botón "Siguiente →" aparece deshabilitadoAC-LESS-003: Marcar lección como completada
Section titled “AC-LESS-003: Marcar lección como completada”US: US-LESS-003
AC-LESS-003.1 — Primera vez completando la lección
Dado que la lección no está completada (status NOT_STARTED o IN_PROGRESS)Cuando pulso "Marcar como completada"Entonces la API recibe POST /progress/lesson/:lessonId/complete con timeSpentSecY la API responde 201 con { lessonId, completed: true, ... }Y el botón cambia a "✓ Completada" con fondo verdeY la lección se marca con un check verde en el sidebarAC-LESS-003.2 — Lección ya completada no puede volver a enviarse
Dado que la lección ya tiene status COMPLETEDCuando cargo la páginaEntonces el botón muestra "✓ Completada" y está deshabilitadoY no se envía ninguna llamada a POST /progress/lesson/:lessonId/completeAC-LESS-003.3 — Completar el último lección de un módulo muestra modal de celebración
Dado que estoy en la última lección de un módulo sin quiz publicadoCuando pulso "Marcar como completada"Y la API responde con moduleCompleted: true y quizId: nullEntonces aparece el modal "¡Módulo completado!" con el título del móduloAC-LESS-003.4 — Completar en modo preview no registra progreso
Dado que soy CONTENT_ADMIN en modo preview (?preview=true)Cuando visualizo la lecciónEntonces el botón "Marcar como completada" no está visibleY no se envía ninguna llamada a POST /progress/lesson/:lessonId/completeAC-LESS-004: Sidebar de progreso del path
Section titled “AC-LESS-004: Sidebar de progreso del path”US: US-LESS-004
AC-LESS-004.1 — El sidebar muestra módulos colapsables con lecciones
Dado que el path tiene 3 módulos con varias lecciones cada unoCuando cargo el playerEntonces el sidebar lateral muestra cada módulo como sección colapsableY el módulo que contiene la lección activa aparece expandido automáticamenteY la lección activa está resaltada con borde izquierdo púrpuraAC-LESS-004.2 — La barra de progreso superior muestra el porcentaje correcto
Dado que he completado 5 de 10 lecciones del pathCuando cargo el playerEntonces la barra de progreso en el sidebar muestra "5/10 lecciones" y "50%"AC-LESS-004.3 — Las lecciones completadas muestran el check verde
Dado que la lección tiene status COMPLETED en progreso del usuarioCuando se renderiza el sidebarEntonces el indicador de esa lección muestra "✓" con fondo verde #059669AC-LESS-004.4 — El sidebar puede ocultarse y mostrarse
Dado que el sidebar está visibleCuando pulso el botón "☰" en el topbarEntonces el sidebar desaparece y el área de contenido ocupa el ancho completoY al pulsarlo de nuevo el sidebar vuelve a aparecerAC-LESS-005: Acceso denegado con CTA de upgrade
Section titled “AC-LESS-005: Acceso denegado con CTA de upgrade”US: US-LESS-005
AC-LESS-005.1 — Usuario FREE intenta acceder a lección PAID_STARTER
Dado que soy un usuario con rol FREEY existe una lección con accessLevel PAID_STARTERCuando llamo a GET /paths/:slug/learn/:lessonIdEntonces la API responde 403 con body: { code: "INSUFFICIENT_ACCESS_LEVEL", requiredLevel: "PAID_STARTER", message: "Upgrade your plan to access this content" }AC-LESS-005.2 — El frontend redirige al detalle del path ante un error de acceso
Dado que la llamada a la lección devuelve errorCuando se detecta isError en el componenteEntonces el usuario es redirigido a /paths/:pathSlug con replace: trueY no se muestra ningún error técnico crudo al usuarioAC-LESS-005.3 — Usuario STARTER no puede acceder a lección PAID_PRO
Dado que soy un usuario con rol STARTERY la lección tiene accessLevel PAID_PROCuando llamo al endpointEntonces la API responde 403 con requiredLevel: "PAID_PRO"AC-LESS-006: Guía de conocimiento
Section titled “AC-LESS-006: Guía de conocimiento”US: US-LESS-006
AC-LESS-006.1 — Las secciones publicadas son accesibles públicamente
Dado que existen secciones de guía con isActive: true y bloques con isActive: trueCuando llamo a GET /guide-sections sin autenticaciónEntonces la API responde 200 con { sections: [...], meta: { version, updatedAt } }Y cada sección incluye sus bloques ordenados por campo orderAC-LESS-006.2 — Secciones inactivas no aparecen en la respuesta
Dado que existe una sección con isActive: falseCuando llamo a GET /guide-sectionsEntonces esa sección no aparece en la respuestaAC-LESS-007: Redirección inteligente desde LearnRedirectPage
Section titled “AC-LESS-007: Redirección inteligente desde LearnRedirectPage”US: US-LESS-007
AC-LESS-007.1 — Redirige a la primera lección incompleta
Dado que el usuario tiene varias lecciones completadas y algunas pendientesCuando navega a /learn/:pathSlug (sin lessonId)Entonces es redirigido con replace: true a /learn/:pathSlug/:primerLecciónIncompletaY no aparece la URL de redirección en el historial del navegadorAC-LESS-007.2 — Si todas las lecciones están completadas, redirige a la primera
Dado que todas las lecciones del path tienen status COMPLETEDCuando navega a /learn/:pathSlugEntonces es redirigido a /learn/:pathSlug/:primeraLecciónAC-LESS-007.3 — Muestra spinner mientras carga la estructura
Dado que la llamada a GET /paths/:slug/learn todavía no ha respondidoCuando navega a /learn/:pathSlugEntonces se muestra un spinner centrado en pantalla completaY no se muestra contenido de lección ni sidebarAC-LESS-007.4 — Muestra error con enlace de vuelta si la estructura falla
Dado que GET /paths/:slug/learn devuelve errorCuando carga la página de redirecciónEntonces se muestra el mensaje "No se pudo cargar la ruta"Y hay un enlace "Volver a la descripción →" que apunta a /paths/:pathSlugAC-LESS-008: CONTENT_ADMIN preview sin restricción de acceso
Section titled “AC-LESS-008: CONTENT_ADMIN preview sin restricción de acceso”US: US-LESS-008
AC-LESS-008.1 — Preview endpoint ignora el accessLevel de la lección
Dado que soy CONTENT_ADMIN propietario del pathY la lección tiene accessLevel PAID_ELITECuando llamo a GET /paths/:slug/preview/lessons/:lessonIdEntonces la API responde 200 con el contenido completo de la lecciónSin verificar ACCESS_HIERARCHY para el rol del creadorAC-LESS-008.2 — El banner amarillo de preview aparece en el player
Dado que soy CONTENT_ADMIN y accedo a /learn/:pathSlug/:lessonId?preview=trueCuando se carga LessonPlayerPageEntonces aparece un banner amarillo con texto "Vista previa" y advertencia de que el progreso no se registraY el topbar muestra "← Volver al editor" en lugar de "← Volver"AC-LESS-008.3 — CONTENT_ADMIN ajeno al path recibe 403
Dado que soy CONTENT_ADMIN pero NO soy el creador del pathCuando llamo a GET /paths/:slug/preview/lessons/:lessonIdEntonces la API responde 403AC-LESS-009: Completar el path y certificado
Section titled “AC-LESS-009: Completar el path y certificado”US: US-LESS-009
AC-LESS-009.1 — Modal de path completado aparece cuando pathCompleted es true
Dado que estoy en la última lección del pathCuando pulso "Marcar como completada"Y la API responde con pathCompleted: true y certificateIssued: trueEntonces aparece el modal "¡Ruta completada!" con el título del pathY el modal muestra el botón "Ver mi certificado →" que navega a /certificatesAC-LESS-009.2 — Modal sin certificado cuando certificateIssued es false
Dado que la respuesta incluye pathCompleted: true y certificateIssued: falseCuando aparece el modalEntonces el mensaje indica que se han completado todas las leccionesY no aparece el botón "Ver mi certificado →"Y sí aparece el botón "Ir al dashboard"Test Cases
Section titled “Test Cases”TC-LESS-001: Lección VIDEO accesible con nivel suficiente
Section titled “TC-LESS-001: Lección VIDEO accesible con nivel suficiente”AC: AC-LESS-001.1 · Prioridad: Alta · Tipo: API · Estado: 📋 Pendiente
Spec: apps/e2e/tests/student/lesson-player.spec.ts
Precondiciones: fixtures.starter tiene acceso a una lección con accessLevel: PAID_STARTER
// Llamada directa al endpoint de la lecciónconst res = await apiClient.get( `/paths/${fixtures.starter.enrolledPathSlug}/learn/${fixtures.starter.paidStarterLessonId}`, { headers: { Authorization: `Bearer ${fixtures.starter.token}` } })expect(res.status).toBe(200)expect(res.data).toMatchObject({ id: fixtures.starter.paidStarterLessonId, contentType: 'VIDEO', title: expect.any(String), module: expect.objectContaining({ id: expect.any(String) }), path: expect.objectContaining({ slug: fixtures.starter.enrolledPathSlug }),})// Prev/next disponiblesexpect(res.data.prevLesson === null || typeof res.data.prevLesson?.id === 'string').toBe(true)expect(res.data.nextLesson === null || typeof res.data.nextLesson?.id === 'string').toBe(true)TC-LESS-002: Lección MARKDOWN renderiza correctamente en el navegador
Section titled “TC-LESS-002: Lección MARKDOWN renderiza correctamente en el navegador”AC: AC-LESS-001.2 · Prioridad: Alta · Tipo: E2E · Estado: 📋 Pendiente
Spec: apps/e2e/tests/student/lesson-player.spec.ts
Precondiciones: fixtures.free inscrito en path con lección FREE_REGISTERED + MARKDOWN
await loginAs(page, fixtures.free)await page.goto(`/learn/${fixtures.free.enrolledPathSlug}/${fixtures.free.markdownLessonId}`)
// El título de la lección debe aparecerawait expect(page.getByRole('heading', { level: 1 })).toBeVisible()
// No debe haber iframe de videoawait expect(page.locator('iframe')).not.toBeVisible()
// El área de contenido markdown debe estar presenteawait expect(page.locator('[data-testid="lesson-markdown-content"]')).toBeVisible()TC-LESS-003: Navegación anterior / siguiente
Section titled “TC-LESS-003: Navegación anterior / siguiente”AC: AC-LESS-002.1, AC-LESS-002.2, AC-LESS-002.4 · Prioridad: Alta · Tipo: E2E · Estado: 📋 Pendiente
Spec: apps/e2e/tests/student/lesson-player.spec.ts
Precondiciones: fixtures.starter enrollado en path con al menos 3 lecciones PAID_STARTER consecutivas
await loginAs(page, fixtures.starter)
// Navegar a la segunda lección (tiene prev y next)await page.goto(`/learn/${fixtures.starter.enrolledPathSlug}/${fixtures.starter.secondLessonId}`)
// El botón "Anterior" está activoconst prevBtn = page.getByRole('button', { name: /anterior/i })await expect(prevBtn).toBeEnabled()
// El botón "Siguiente" está activoconst nextBtn = page.getByRole('button', { name: /siguiente/i })await expect(nextBtn).toBeEnabled()
// Navegar a la siguiente lecciónawait nextBtn.click()await expect(page).toHaveURL(new RegExp(`/learn/${fixtures.starter.enrolledPathSlug}/`))await expect(page).not.toHaveURL(new RegExp(fixtures.starter.secondLessonId))
// En la primera lección, "Anterior" está deshabilitadoawait page.goto(`/learn/${fixtures.starter.enrolledPathSlug}/${fixtures.starter.firstLessonId}`)await expect(page.getByRole('button', { name: /anterior/i })).toBeDisabled()TC-LESS-004: Marcar lección como completada — flujo exitoso
Section titled “TC-LESS-004: Marcar lección como completada — flujo exitoso”AC: AC-LESS-003.1 · Prioridad: Alta · Tipo: API + E2E · Estado: 📋 Pendiente
Spec: apps/e2e/tests/student/lesson-player.spec.ts
Precondiciones: fixtures.free tiene lección FREE_REGISTERED con status NOT_STARTED
// Test de API directoconst res = await apiClient.post( `/progress/lesson/${fixtures.free.notStartedLessonId}/complete`, { timeSpentSec: 120 }, { headers: { Authorization: `Bearer ${fixtures.free.token}` } })expect(res.status).toBe(201)expect(res.data).toMatchObject({ lessonId: fixtures.free.notStartedLessonId, completed: true, moduleCompleted: expect.any(Boolean), pathCompleted: expect.any(Boolean), certificateIssued: expect.any(Boolean),})
// Verificar que el progreso quedó guardadoconst progressRes = await apiClient.get( `/progress/lesson/${fixtures.free.notStartedLessonId}`, { headers: { Authorization: `Bearer ${fixtures.free.token}` } })expect(progressRes.data.status).toBe('COMPLETED')TC-LESS-005: Botón “Marcar como completada” deshabilitado si ya está completada
Section titled “TC-LESS-005: Botón “Marcar como completada” deshabilitado si ya está completada”AC: AC-LESS-003.2 · Prioridad: Media · Tipo: E2E · Estado: 📋 Pendiente
Spec: apps/e2e/tests/student/lesson-player.spec.ts
await loginAs(page, fixtures.free)// Navegar a una lección previamente completadaawait page.goto(`/learn/${fixtures.free.enrolledPathSlug}/${fixtures.free.completedLessonId}`)
const completeBtn = page.getByRole('button', { name: /completada/i })await expect(completeBtn).toBeDisabled()await expect(completeBtn).toHaveText(/✓ Completada/)
// Verificar que el indicador del sidebar también muestra el checkawait expect(page.locator('[data-testid="sidebar-lesson-completed"]').first()).toBeVisible()TC-LESS-006: Barra de progreso en el sidebar
Section titled “TC-LESS-006: Barra de progreso en el sidebar”AC: AC-LESS-004.2 · Prioridad: Media · Tipo: E2E · Estado: 📋 Pendiente
Spec: apps/e2e/tests/student/lesson-player.spec.ts
await loginAs(page, fixtures.starter)await page.goto(`/learn/${fixtures.starter.enrolledPathSlug}/${fixtures.starter.firstLessonId}`)
// El sidebar debe mostrar la fracción completada// Si el path tiene 10 lecciones y completó 4, debe mostrar "4/10 lecciones" y "40%"const progressText = page.locator('[data-testid="sidebar-progress-text"]')await expect(progressText).toContainText(/\d+\/\d+ lecciones/)
// La barra de progreso tiene un ancho > 0 si hay lecciones completadasconst progressBar = page.locator('[data-testid="sidebar-progress-bar"]')await expect(progressBar).toBeVisible()TC-LESS-007: Acceso denegado en API — usuario FREE a lección PAID_STARTER
Section titled “TC-LESS-007: Acceso denegado en API — usuario FREE a lección PAID_STARTER”AC: AC-LESS-005.1 · Prioridad: Alta · Tipo: API · Estado: 📋 Pendiente
Spec: apps/e2e/tests/student/lesson-player.spec.ts
const res = await apiClient.get( `/paths/${fixtures.paidStarterPath.slug}/learn/${fixtures.paidStarterPath.starterLessonId}`, { headers: { Authorization: `Bearer ${fixtures.free.token}` } })expect(res.status).toBe(403)expect(res.data).toMatchObject({ code: 'INSUFFICIENT_ACCESS_LEVEL', requiredLevel: 'PAID_STARTER',})TC-LESS-008: Frontend redirige a la descripción del path ante error de acceso
Section titled “TC-LESS-008: Frontend redirige a la descripción del path ante error de acceso”AC: AC-LESS-005.2 · Prioridad: Alta · Tipo: E2E · Estado: 📋 Pendiente
Spec: apps/e2e/tests/student/lesson-player.spec.ts
// Acceder a una URL de lección de nivel superior como usuario FREEawait loginAs(page, fixtures.free)await page.goto( `/learn/${fixtures.paidStarterPath.slug}/${fixtures.paidStarterPath.starterLessonId}`)
// El error dispara la redirección a la página de detalle del pathawait expect(page).toHaveURL(new RegExp(`/paths/${fixtures.paidStarterPath.slug}`))// No debe haber ninguna traza técnica visible en la URL ni en pantallaawait expect(page.locator('text=INSUFFICIENT_ACCESS_LEVEL')).not.toBeVisible()TC-LESS-009: Guía de conocimiento — secciones publicadas
Section titled “TC-LESS-009: Guía de conocimiento — secciones publicadas”AC: AC-LESS-006.1, AC-LESS-006.2 · Prioridad: Media · Tipo: API · Estado: 📋 Pendiente
Spec: apps/e2e/tests/student/lesson-player.spec.ts
// El endpoint es público — sin tokenconst res = await apiClient.get('/guide-sections')expect(res.status).toBe(200)expect(res.data).toMatchObject({ sections: expect.any(Array), meta: expect.objectContaining({ version: expect.any(String), updatedAt: expect.any(String), }),})
// Cada sección publicada tiene sus bloques activosfor (const section of res.data.sections) { expect(section.isActive).toBe(true) for (const block of section.blocks) { expect(block.isActive).toBe(true) }}TC-LESS-010: LearnRedirectPage redirige a primera lección incompleta
Section titled “TC-LESS-010: LearnRedirectPage redirige a primera lección incompleta”AC: AC-LESS-007.1, AC-LESS-007.2 · Prioridad: Alta · Tipo: E2E · Estado: 📋 Pendiente
Spec: apps/e2e/tests/student/lesson-player.spec.ts
await loginAs(page, fixtures.starter)
// Navegar a la ruta sin lessonIdawait page.goto(`/learn/${fixtures.starter.enrolledPathSlug}`)
// Debe redirigir a una URL con el ID de una lección concretaawait page.waitForURL(new RegExp(`/learn/${fixtures.starter.enrolledPathSlug}/[a-z0-9-]+`), { timeout: 5000 })
// La URL resultante no debe ser la misma que la original (confirmar redirección)expect(page.url()).toMatch(new RegExp(`/learn/${fixtures.starter.enrolledPathSlug}/[a-z0-9-]+`))
// La entrada en el historial del navegador se reemplaza (replace: true)// Pulsar "Atrás" no lleva de vuelta a /learn/:slug sin lessonIdawait page.goBack()await expect(page).not.toHaveURL(new RegExp(`/learn/${fixtures.starter.enrolledPathSlug}$`))TC-LESS-011: CONTENT_ADMIN accede al preview sin restricción de access level
Section titled “TC-LESS-011: CONTENT_ADMIN accede al preview sin restricción de access level”AC: AC-LESS-008.1, AC-LESS-008.2 · Prioridad: Alta · Tipo: API + E2E · Estado: 📋 Pendiente
Spec: apps/e2e/tests/student/lesson-player.spec.ts
// Test API: CONTENT_ADMIN puede leer lección PAID_ELITE sin errorconst res = await apiClient.get( `/paths/${fixtures.contentAdmin.pathSlug}/preview/lessons/${fixtures.contentAdmin.eliteLessonId}`, { headers: { Authorization: `Bearer ${fixtures.contentAdmin.token}` } })expect(res.status).toBe(200)expect(res.data.id).toBe(fixtures.contentAdmin.eliteLessonId)
// Test E2E: el banner de preview aparece en el playerawait loginAs(page, fixtures.contentAdmin)await page.goto( `/learn/${fixtures.contentAdmin.pathSlug}/${fixtures.contentAdmin.eliteLessonId}?preview=true`)await expect(page.locator('text=Vista previa')).toBeVisible()await expect(page.locator('text=El progreso no se registra')).toBeVisible()
// El topbar muestra "Volver al editor" en lugar de "Volver"await expect(page.getByText(/volver al editor/i)).toBeVisible()await expect(page.getByText(/^← Volver$/i)).not.toBeVisible()TC-LESS-012: Completar path — modal con certificado
Section titled “TC-LESS-012: Completar path — modal con certificado”AC: AC-LESS-009.1, AC-LESS-009.2 · Prioridad: Alta · Tipo: API · Estado: 📋 Pendiente
Spec: apps/e2e/tests/student/lesson-player.spec.ts
Precondiciones: Solo queda la última lección sin completar; el path tiene certificado configurado
// Marcar la última lección del pathconst res = await apiClient.post( `/progress/lesson/${fixtures.starter.lastLessonId}/complete`, { timeSpentSec: 240 }, { headers: { Authorization: `Bearer ${fixtures.starter.token}` } })expect(res.status).toBe(201)expect(res.data.pathCompleted).toBe(true)// Si el path tiene certificado, certificateIssued debe ser true// Si no tiene configurado, puede ser falseexpect(typeof res.data.certificateIssued).toBe('boolean')