Skip to content

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


EndpointGuardDescripción
GET /paths/:slug/learnJwtAuthGuardEstructura del path + progreso por lección para el sidebar
GET /paths/:slug/learn/:lessonIdJwtAuthGuard + access levelContenido completo de la lección
POST /progress/lesson/:lessonId/completeJwtAuthGuardMarcar lección completada, emitir XP, detectar módulo/path completo
GET /progress/lesson/:lessonIdJwtAuthGuardEstado de progreso de una lección concreta
GET /guide-sectionspúblicoSecciones de la guía de conocimiento publicadas
GET /paths/:slug/preview/structureJwtAuthGuard + CONTENT_ADMINEstructura para preview del creator
GET /paths/:slug/preview/lessons/:lessonIdJwtAuthGuard + CONTENT_ADMINContenido de lección para preview del creator

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


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 autenticado
Y existe una lección con accessLevel PAID_STARTER y contentType VIDEO
Cuando navego a /learn/:pathSlug/:lessonId
Entonces la API responde 200 con el contenido de la lección
Y 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 orden

AC-LESS-001.2 — Lección de tipo MARKDOWN renderiza el contenido rich text

Dado que soy un usuario FREE autenticado
Y existe una lección con accessLevel FREE_REGISTERED y contentType MARKDOWN
Cuando navego a /learn/:pathSlug/:lessonId
Entonces el área de contenido muestra el texto renderizado (headings, listas, código)
Y no se muestra ningún reproductor de video

AC-LESS-001.3 — Lección MIXED muestra pestañas Video / Contenido

Dado que la lección tiene contentType MIXED con videoUrl y contentMd
Cuando cargo la lección
Entonces veo dos pestañas: "▶ Video" y "☰ Contenido"
Y al cambiar de pestaña se muestra el contenido correspondiente sin recargar la página

AC-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 H5P
Cuando cargo la lección
Entonces se renderiza un iframe apuntando a /resources/:resourceId/h5p-serve

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 path
Cuando cargo la página
Entonces el botón "← Anterior" está activo
Y al pulsarlo navego a /learn/:pathSlug/:prevLessonId sin recargar el sidebar

AC-LESS-002.2 — Botón “Anterior” deshabilitado en la primera lección

Dado que estoy en la primera lección del path
Cuando cargo la página
Entonces 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 superado
Cuando pulso "Siguiente"
Entonces navego a /learn/:pathSlug/quiz/:moduleId
Y el botón mostraba el texto "Ir al quiz →" antes de pulsarlo

AC-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 activo
Cuando pulso "Siguiente →"
Entonces navego a /learn/:pathSlug/:nextLessonId

AC-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 pendiente
Cuando cargo la página
Entonces el botón "Siguiente →" aparece deshabilitado

AC-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 timeSpentSec
Y la API responde 201 con { lessonId, completed: true, ... }
Y el botón cambia a "✓ Completada" con fondo verde
Y la lección se marca con un check verde en el sidebar

AC-LESS-003.2 — Lección ya completada no puede volver a enviarse

Dado que la lección ya tiene status COMPLETED
Cuando cargo la página
Entonces el botón muestra "✓ Completada" y está deshabilitado
Y no se envía ninguna llamada a POST /progress/lesson/:lessonId/complete

AC-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 publicado
Cuando pulso "Marcar como completada"
Y la API responde con moduleCompleted: true y quizId: null
Entonces aparece el modal "¡Módulo completado!" con el título del módulo

AC-LESS-003.4 — Completar en modo preview no registra progreso

Dado que soy CONTENT_ADMIN en modo preview (?preview=true)
Cuando visualizo la lección
Entonces el botón "Marcar como completada" no está visible
Y no se envía ninguna llamada a POST /progress/lesson/:lessonId/complete

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 uno
Cuando cargo el player
Entonces el sidebar lateral muestra cada módulo como sección colapsable
Y el módulo que contiene la lección activa aparece expandido automáticamente
Y la lección activa está resaltada con borde izquierdo púrpura

AC-LESS-004.2 — La barra de progreso superior muestra el porcentaje correcto

Dado que he completado 5 de 10 lecciones del path
Cuando cargo el player
Entonces 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 usuario
Cuando se renderiza el sidebar
Entonces el indicador de esa lección muestra "✓" con fondo verde #059669

AC-LESS-004.4 — El sidebar puede ocultarse y mostrarse

Dado que el sidebar está visible
Cuando pulso el botón "☰" en el topbar
Entonces el sidebar desaparece y el área de contenido ocupa el ancho completo
Y al pulsarlo de nuevo el sidebar vuelve a aparecer

AC-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 FREE
Y existe una lección con accessLevel PAID_STARTER
Cuando llamo a GET /paths/:slug/learn/:lessonId
Entonces 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 error
Cuando se detecta isError en el componente
Entonces el usuario es redirigido a /paths/:pathSlug con replace: true
Y no se muestra ningún error técnico crudo al usuario

AC-LESS-005.3 — Usuario STARTER no puede acceder a lección PAID_PRO

Dado que soy un usuario con rol STARTER
Y la lección tiene accessLevel PAID_PRO
Cuando llamo al endpoint
Entonces la API responde 403 con requiredLevel: "PAID_PRO"

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: true
Cuando llamo a GET /guide-sections sin autenticación
Entonces la API responde 200 con { sections: [...], meta: { version, updatedAt } }
Y cada sección incluye sus bloques ordenados por campo order

AC-LESS-006.2 — Secciones inactivas no aparecen en la respuesta

Dado que existe una sección con isActive: false
Cuando llamo a GET /guide-sections
Entonces esa sección no aparece en la respuesta

AC-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 pendientes
Cuando navega a /learn/:pathSlug (sin lessonId)
Entonces es redirigido con replace: true a /learn/:pathSlug/:primerLecciónIncompleta
Y no aparece la URL de redirección en el historial del navegador

AC-LESS-007.2 — Si todas las lecciones están completadas, redirige a la primera

Dado que todas las lecciones del path tienen status COMPLETED
Cuando navega a /learn/:pathSlug
Entonces es redirigido a /learn/:pathSlug/:primeraLección

AC-LESS-007.3 — Muestra spinner mientras carga la estructura

Dado que la llamada a GET /paths/:slug/learn todavía no ha respondido
Cuando navega a /learn/:pathSlug
Entonces se muestra un spinner centrado en pantalla completa
Y no se muestra contenido de lección ni sidebar

AC-LESS-007.4 — Muestra error con enlace de vuelta si la estructura falla

Dado que GET /paths/:slug/learn devuelve error
Cuando carga la página de redirección
Entonces se muestra el mensaje "No se pudo cargar la ruta"
Y hay un enlace "Volver a la descripción →" que apunta a /paths/:pathSlug

AC-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 path
Y la lección tiene accessLevel PAID_ELITE
Cuando llamo a GET /paths/:slug/preview/lessons/:lessonId
Entonces la API responde 200 con el contenido completo de la lección
Sin verificar ACCESS_HIERARCHY para el rol del creador

AC-LESS-008.2 — El banner amarillo de preview aparece en el player

Dado que soy CONTENT_ADMIN y accedo a /learn/:pathSlug/:lessonId?preview=true
Cuando se carga LessonPlayerPage
Entonces aparece un banner amarillo con texto "Vista previa" y advertencia de que el progreso no se registra
Y 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 path
Cuando llamo a GET /paths/:slug/preview/lessons/:lessonId
Entonces la API responde 403

AC-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 path
Cuando pulso "Marcar como completada"
Y la API responde con pathCompleted: true y certificateIssued: true
Entonces aparece el modal "¡Ruta completada!" con el título del path
Y el modal muestra el botón "Ver mi certificado →" que navega a /certificates

AC-LESS-009.2 — Modal sin certificado cuando certificateIssued es false

Dado que la respuesta incluye pathCompleted: true y certificateIssued: false
Cuando aparece el modal
Entonces el mensaje indica que se han completado todas las lecciones
Y no aparece el botón "Ver mi certificado →"
Y sí aparece el botón "Ir al dashboard"

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ón
const 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 disponibles
expect(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 aparecer
await expect(page.getByRole('heading', { level: 1 })).toBeVisible()
// No debe haber iframe de video
await expect(page.locator('iframe')).not.toBeVisible()
// El área de contenido markdown debe estar presente
await 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á activo
const prevBtn = page.getByRole('button', { name: /anterior/i })
await expect(prevBtn).toBeEnabled()
// El botón "Siguiente" está activo
const nextBtn = page.getByRole('button', { name: /siguiente/i })
await expect(nextBtn).toBeEnabled()
// Navegar a la siguiente lección
await 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á deshabilitado
await 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 directo
const 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ó guardado
const 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 completada
await 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 check
await 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 completadas
const 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 FREE
await 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 path
await expect(page).toHaveURL(new RegExp(`/paths/${fixtures.paidStarterPath.slug}`))
// No debe haber ninguna traza técnica visible en la URL ni en pantalla
await 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 token
const 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 activos
for (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 lessonId
await page.goto(`/learn/${fixtures.starter.enrolledPathSlug}`)
// Debe redirigir a una URL con el ID de una lección concreta
await 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 lessonId
await 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 error
const 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 player
await 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 path
const 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 false
expect(typeof res.data.certificateIssued).toBe('boolean')