Editor de Contenido
Módulos API: admin-paths, admin-lessons, game-activities, escape-room, ai/tasks
Páginas web: AdminPathEditor.tsx, AdminModuleEditor.tsx, AdminLessonEditor.tsx, GameActivityEditorPage.tsx
Spec Playwright: apps/e2e/tests/creator/content-editor.spec.ts — 📋 pendiente
User Stories
Section titled “User Stories”US-EDIT-001: Crear un nuevo path en estado DRAFT
Section titled “US-EDIT-001: Crear un nuevo path en estado DRAFT”Como creator con rol CONTENT_ADMIN
Quiero crear un nuevo path de aprendizaje con título, descripción y nivel de acceso
Para comenzar a estructurar contenido antes de publicarlo
Criterios: AC-EDIT-001 · Casos: TC-EDIT-001, TC-EDIT-002 · Prioridad: Alta · ✅ Implementado
US-EDIT-002: Editar metadatos del path
Section titled “US-EDIT-002: Editar metadatos del path”Como creator Quiero actualizar el título, descripción larga, imagen de portada, nivel de dificultad y color de acento de un path existente Para mantener el catálogo de rutas bien descrito y visualmente coherente
Criterios: AC-EDIT-002 · Casos: TC-EDIT-003, TC-EDIT-004 · Prioridad: Alta · ✅ Implementado
US-EDIT-003: Añadir y reordenar módulos dentro de un path
Section titled “US-EDIT-003: Añadir y reordenar módulos dentro de un path”Como creator Quiero añadir nuevos módulos a un path y cambiar su orden mediante arrastrar y soltar Para organizar la secuencia de aprendizaje de forma intuitiva
Criterios: AC-EDIT-003 · Casos: TC-EDIT-005, TC-EDIT-006 · Prioridad: Alta · ✅ Implementado
US-EDIT-004: Crear y editar una lección con el editor de contenido rico
Section titled “US-EDIT-004: Crear y editar una lección con el editor de contenido rico”Como creator Quiero crear lecciones con contenido Markdown, vídeo o mixto dentro de un módulo Para dotar al módulo de material de aprendizaje consumible por los estudiantes
Criterios: AC-EDIT-004 · Casos: TC-EDIT-007, TC-EDIT-008 · Prioridad: Alta · ✅ Implementado
US-EDIT-005: Definir el access_level de un módulo o lección individualmente
Section titled “US-EDIT-005: Definir el access_level de un módulo o lección individualmente”Como creator Quiero asignar un nivel de acceso específico a un módulo o lección, independiente del path padre Para ofrecer previews públicas de lecciones dentro de paths de pago
Criterios: AC-EDIT-005 · Casos: TC-EDIT-009 · Prioridad: Alta · ✅ Implementado
US-EDIT-006: Generar una actividad IA para un módulo
Section titled “US-EDIT-006: Generar una actividad IA para un módulo”Como creator Quiero solicitar la generación de una actividad interactiva (quiz, escape room, flashcards, etc.) para un módulo Para enriquecer el contenido con ejercicios generados automáticamente a partir del material del módulo
Criterios: AC-EDIT-006 · Casos: TC-EDIT-010, TC-EDIT-011 · Prioridad: Alta · ✅ Implementado
US-EDIT-007: Ver el estado de la generación IA en tiempo real
Section titled “US-EDIT-007: Ver el estado de la generación IA en tiempo real”Como creator que ha lanzado una generación IA Quiero ver el estado actual de la tarea (PENDING → PROCESSING → COMPLETED / FAILED) sin recargar la página Para saber cuándo la actividad está lista para revisar
Criterios: AC-EDIT-007 · Casos: TC-EDIT-012, TC-EDIT-013 · Prioridad: Alta · ✅ Implementado
US-EDIT-008: Aceptar la actividad generada por IA
Section titled “US-EDIT-008: Aceptar la actividad generada por IA”Como creator que ha revisado la actividad generada Quiero aceptar la actividad para que quede disponible en el módulo para los estudiantes Para publicar contenido interactivo de calidad verificada
Criterios: AC-EDIT-008 · Casos: TC-EDIT-014 · Prioridad: Alta · ✅ Implementado
US-EDIT-009: Rechazar la actividad generada por IA
Section titled “US-EDIT-009: Rechazar la actividad generada por IA”Como creator que no está satisfecho con el resultado de la generación Quiero rechazar la actividad y poder solicitar una nueva generación Para mantener el estándar de calidad del contenido
Criterios: AC-EDIT-009 · Casos: TC-EDIT-015 · Prioridad: Media · ✅ Implementado
US-EDIT-010: Publicar un path con validación de contenido mínimo
Section titled “US-EDIT-010: Publicar un path con validación de contenido mínimo”Como creator Quiero publicar un path para que sea visible en el catálogo Para que los estudiantes puedan inscribirse y comenzar a aprender
Criterios: AC-EDIT-010 · Casos: TC-EDIT-016, TC-EDIT-017 · Prioridad: Alta · ✅ Implementado
US-EDIT-011: No poder editar contenido ajeno como CONTENT_ADMIN
Section titled “US-EDIT-011: No poder editar contenido ajeno como CONTENT_ADMIN”Como sistema de control de acceso
Quiero garantizar que un CONTENT_ADMIN solo puede editar paths, módulos y lecciones que le pertenecen
Para que los creators no puedan modificar accidentalmente (o intencionalmente) el contenido de otros
Criterios: AC-EDIT-011 · Casos: TC-EDIT-018, TC-EDIT-019 · Prioridad: Alta · ✅ Implementado
US-EDIT-012: Verificar feedback visual al guardar (Action Feedback UX)
Section titled “US-EDIT-012: Verificar feedback visual al guardar (Action Feedback UX)”Como creator que ha guardado cambios en un path o módulo Quiero ver un overlay con spinner durante el guardado y un toast de confirmación al terminar Para saber que mis cambios se han persistido correctamente
Criterios: AC-EDIT-012 · Casos: TC-EDIT-020 · Prioridad: Media · ✅ Implementado
Acceptance Criteria
Section titled “Acceptance Criteria”AC-EDIT-001: Crear un nuevo path en estado DRAFT
Section titled “AC-EDIT-001: Crear un nuevo path en estado DRAFT”US: US-EDIT-001
AC-EDIT-001.1 — Creación exitosa
Dado que estoy autenticado como CONTENT_ADMIN en /creator/editor/pathsCuando hago clic en "Nueva Ruta"Entonces se llama a POST /admin/paths con título por defecto y slug únicoY la API responde con status 201 y el path creado en estado DRAFT (isPublished: false)Y soy redirigido al editor del nuevo path con la URL /creator/editor/paths/:idAC-EDIT-001.2 — Usuario FREE no puede acceder al Creator Studio
Dado que estoy autenticado con rol FREECuando navego directamente a /creator/editor/pathsEntonces soy redirigido a /dashboardY no veo ningún componente del Creator StudioY la API retorna 403 para cualquier llamada a POST /admin/pathsAC-EDIT-001.3 — Slug generado automáticamente desde el título
Dado que estoy en el editor de un pathCuando escribo "Fundamentos de Marketing Digital" en el campo títuloEntonces el campo slug se rellena automáticamente con "fundamentos-de-marketing-digital"Y el slug reemplaza tildes, mayúsculas y caracteres especialesAC-EDIT-002: Editar metadatos del path
Section titled “AC-EDIT-002: Editar metadatos del path”US: US-EDIT-002
AC-EDIT-002.1 — Guardado de metadatos básicos
Dado que estoy editando un path que me perteneceCuando modifico el título, descripción o nivel de accesoY hago clic en "Guardar"Entonces se llama a PATCH /admin/paths/:id con los campos actualizadosY la API responde con status 200Y el botón "Guardar" queda deshabilitado (no hay cambios pendientes)AC-EDIT-002.2 — Subida de imagen de portada
Dado que estoy editando un pathCuando subo una imagen de portada (JPG/PNG, máximo 5 MB)Y guardoEntonces se llama a POST /admin/paths/:id/preview-image con el archivoY la imagen previsualizada en el editor se actualizaAC-EDIT-002.3 — Subida de imagen banner
Dado que estoy editando un pathCuando subo una imagen banner (JPG/PNG, máximo 8 MB)Y guardoEntonces se llama a POST /admin/paths/:id/banner-image con el archivoAC-EDIT-003: Añadir y reordenar módulos
Section titled “AC-EDIT-003: Añadir y reordenar módulos”US: US-EDIT-003
AC-EDIT-003.1 — Añadir módulo
Dado que estoy en el editor del pathCuando hago clic en "Añadir módulo" e introduzco un títuloEntonces se llama a POST /admin/paths/:id/modules con el título y accessLevelY el nuevo módulo aparece al final de la lista con estado no publicadoY la API responde con status 201AC-EDIT-003.2 — Reordenar módulos con drag and drop
Dado que el path tiene al menos 2 módulosCuando arrastro el módulo "Módulo 2" antes de "Módulo 1"Entonces se llama a PATCH /admin/paths/:id/modules/reorder con el array de IDs en nuevo ordenY la lista de módulos refleja el nuevo orden inmediatamenteAC-EDIT-003.3 — Eliminar módulo
Dado que un módulo no tiene lecciones vinculadasCuando hago clic en eliminar el módulo y confirmoEntonces se llama a DELETE /admin/modules/:idY el módulo desaparece de la listaY la API responde con status 204AC-EDIT-004: Crear y editar lecciones
Section titled “AC-EDIT-004: Crear y editar lecciones”US: US-EDIT-004
AC-EDIT-004.1 — Crear lección en un módulo
Dado que estoy en el editor del móduloCuando creo una nueva lección con tipo MARKDOWNEntonces se llama a POST /admin/modules/:moduleId/lessonsY la API responde con status 201 con el ID de la lección nuevaY la lección aparece en la lista del módulo con estado no publicadaAC-EDIT-004.2 — Editar contenido Markdown con auto-guardado
Dado que estoy editando el contenido Markdown de una lecciónCuando modifico el textoY espero el debounce (o hago clic en "Guardar")Entonces se llama a PATCH /admin/lessons/:id con contentMd actualizadoY el indicador "Guardado" aparece brevementeAC-EDIT-004.3 — Tipos de contenido soportados
Dado que estoy creando una lecciónCuando selecciono tipo VIDEOEntonces puedo introducir una URL de vídeo y duración en segundosY el tipo MARKDOWN permite un editor de texto enriquecidoY el tipo MIXED combina texto y vídeoAC-EDIT-005: Definir access_level de módulo o lección
Section titled “AC-EDIT-005: Definir access_level de módulo o lección”US: US-EDIT-005
AC-EDIT-005.1 — Lección más abierta que el módulo padre
Dado que un path tiene accessLevel PAID_PROCuando asigno accessLevel PUBLIC a una lección específica dentro de ese pathEntonces la API acepta la combinación (lección puede ser más abierta que el padre)Y esa lección es visible para usuarios anónimos como previewY el resto de lecciones siguen requiriendo PAID_PROAC-EDIT-005.2 — Selector de access_level en el editor de módulo
Dado que estoy editando un móduloCuando cambio el accessLevel a PAID_STARTERY guardoEntonces se llama a PATCH /admin/modules/:id con { accessLevel: "PAID_STARTER" }Y la API responde con status 200AC-EDIT-006: Generar actividad IA para un módulo
Section titled “AC-EDIT-006: Generar actividad IA para un módulo”US: US-EDIT-006
AC-EDIT-006.1 — Lanzar generación
Dado que estoy en el editor del módulo y el módulo tiene al menos 1 lección con contenidoCuando hago clic en "Generar actividad IA" y selecciono el tipo (ej. Escape Room)Y confirmo los parámetros de generaciónEntonces se llama a POST /game-activities/roomscape/generate (o el endpoint del tipo elegido)Y la API crea una AITask con estado PENDINGY la UI muestra el estado "Generando..." para ese móduloAC-EDIT-006.2 — Tipos de actividad disponibles
Dado que estoy en el Game Center del móduloCuando abro el selector de actividadesEntonces veo los 13 tipos disponibles: Roomscape, Crossword, Hangman, Trivia, Adventure, Flashcards, Drag and Drop, Matching Pairs, Word Puzzle, Scenario Challenge, Speed Quiz, Memory Match, Timeline ChallengeY cada tipo muestra su ícono y nombreAC-EDIT-007: Ver estado de la generación IA en tiempo real
Section titled “AC-EDIT-007: Ver estado de la generación IA en tiempo real”US: US-EDIT-007
AC-EDIT-007.1 — Polling de AITask
Dado que se ha lanzado una generación IA con estado PENDINGCuando el componente del módulo está visibleEntonces el cliente hace polling a GET /ai/tasks/:taskId/result cada pocos segundosY muestra el estado actual: PENDING, PROCESSING, COMPLETED o FAILEDY al pasar a COMPLETED el botón "Ver resultado" se habilita automáticamenteAC-EDIT-007.2 — Tarea FAILED
Dado que la generación IA ha fallado (estado FAILED)Cuando el polling detecta el estadoEntonces la UI muestra un indicador de error con el mensaje descriptivoY el creator puede intentar generar de nuevoAC-EDIT-008: Aceptar la actividad generada
Section titled “AC-EDIT-008: Aceptar la actividad generada”US: US-EDIT-008
AC-EDIT-008.1 — Aceptar resultado
Dado que una AITask está en estado COMPLETEDCuando el creator hace clic en "Aceptar" desde el diálogo de resultadoEntonces se llama a PATCH /game-activities/roomscape/:id/accept (o el endpoint del tipo)Y la actividad queda disponible en el módulo como GameActivity con status ACTIVEY la API responde con status 200Y la actividad aparece en la lista de contenido del móduloAC-EDIT-008.2 — La actividad aceptada es visible para estudiantes
Dado que una actividad ha sido aceptadaCuando un estudiante con el nivel de acceso requerido visita el móduloEntonces la actividad aparece en la lista de contenido del móduloY el estudiante puede iniciar la actividad desde el player correspondienteAC-EDIT-009: Rechazar actividad generada
Section titled “AC-EDIT-009: Rechazar actividad generada”US: US-EDIT-009
AC-EDIT-009.1 — Rechazar y eliminar
Dado que una AITask está en estado COMPLETEDCuando el creator hace clic en "Rechazar" desde el diálogo de resultadoEntonces se llama a PATCH /ai/tasks/:taskId/review-status con { status: "REJECTED" }Y la actividad pendiente queda marcada como rechazadaY el creator puede lanzar una nueva generación para el mismo móduloAC-EDIT-010: Publicar un path
Section titled “AC-EDIT-010: Publicar un path”US: US-EDIT-010
AC-EDIT-010.1 — Publicación exitosa
Dado que el path tiene al menos 1 módulo publicado con al menos 1 lección publicadaCuando el creator hace clic en "Publicar"Entonces se llama a PATCH /admin/paths/:id/publish con { isPublished: true }Y la API responde con status 200Y el path aparece en el catálogo públicoY el toggle de publicación muestra el estado "Publicado"AC-EDIT-010.2 — Validación del checklist antes de publicar
Dado que el checklist de publicación no está completo (isComplete: false)Cuando el creator intenta solicitar revisiónEntonces aparece el diálogo ReviewRequestGate con los items pendientesY no se llama a POST /admin/paths/:id/request-review hasta que el checklist esté completoAC-EDIT-010.3 — Despublicar path
Dado que el path está publicado (isPublished: true)Cuando el creator hace clic en "Despublicar" y confirmaEntonces se llama a PATCH /admin/paths/:id/publish con { isPublished: false }Y el path desaparece del catálogo públicoY los estudiantes ya matriculados conservan su progresoAC-EDIT-011: Aislamiento de contenido entre creators
Section titled “AC-EDIT-011: Aislamiento de contenido entre creators”US: US-EDIT-011
AC-EDIT-011.1 — CONTENT_ADMIN no puede editar path ajeno
Dado que soy CONTENT_ADMIN y el path con ID "path-ajeno" pertenece a otro creatorCuando llamo a PATCH /admin/paths/path-ajenoEntonces la API retorna 403Y el mensaje es "No tienes acceso a esta ruta"AC-EDIT-011.2 — CONTENT_ADMIN no lista paths ajenos
Dado que soy CONTENT_ADMINCuando llamo a GET /admin/pathsEntonces la respuesta solo contiene mis paths (creatorId === mi creatorProfileId)Y no aparecen los paths de otros creatorsAC-EDIT-011.3 — SUPER_ADMIN puede editar cualquier contenido
Dado que soy SUPER_ADMINCuando llamo a PATCH /admin/paths/:id para cualquier pathEntonces la API responde con status 200 independientemente del creatorId del pathAC-EDIT-012: Feedback visual al guardar (Action Feedback UX)
Section titled “AC-EDIT-012: Feedback visual al guardar (Action Feedback UX)”US: US-EDIT-012
AC-EDIT-012.1 — Overlay durante el guardado
Dado que estoy en AdminPathEditor o AdminModuleEditorCuando hago clic en "Guardar"Entonces aparece un overlay full-screen con blur y un spinner de color púrpura (--pr)Y el overlay tiene z-index 250 y cubre toda la pantalla durante la petición PATCHAC-EDIT-012.2 — Toast de confirmación
Dado que la petición PATCH ha completado con éxitoCuando el overlay desapareceEntonces aparece un toast en la esquina inferior derecha con fondo verde (#16A34A)Y el toast muestra un ícono de check (✓) y el mensaje de éxitoY el toast desaparece automáticamente tras 3 segundosTest Cases
Section titled “Test Cases”TC-EDIT-001: Crear path exitosamente como CONTENT_ADMIN
Section titled “TC-EDIT-001: Crear path exitosamente como CONTENT_ADMIN”AC: AC-EDIT-001.1 · Prioridad: Alta · Tipo: E2E · Estado: 📋 Pendiente
Spec: apps/e2e/tests/creator/content-editor.spec.ts
Precondiciones: fixtures.contentAdmin tiene sesión activa y perfil de creator creado
Pasos:
- Navegar a
/creator/editor/paths - Hacer clic en el botón “Nueva Ruta”
- Verificar redirección a
/creator/editor/paths/:newId - Verificar que la API creó el path en estado DRAFT
import { fixtures } from '../../fixtures/users.fixture'import { apiClient } from '../../utils/api-client'
test('CONTENT_ADMIN crea un nuevo path en DRAFT', async ({ page }) => { await loginAs(page, fixtures.contentAdmin) await page.goto('/creator/editor/paths') await page.getByTestId('btn-nueva-ruta').click() await expect(page).toHaveURL(/\/creator\/editor\/paths\/[a-zA-Z0-9]+/)
// Verificar via API que el path está en DRAFT const pathId = page.url().split('/').pop()! const res = await apiClient.get(`/admin/paths/${pathId}`, fixtures.contentAdmin.token) expect(res.status).toBe(200) expect(res.data.isPublished).toBe(false)})TC-EDIT-002: Usuario FREE no puede acceder al Creator Studio
Section titled “TC-EDIT-002: Usuario FREE no puede acceder al Creator Studio”AC: AC-EDIT-001.2 · Prioridad: Alta · Tipo: E2E + API · Estado: 📋 Pendiente
Spec: apps/e2e/tests/creator/content-editor.spec.ts
test('FREE user es redirigido al acceder a /creator/editor/paths', async ({ page }) => { await loginAs(page, fixtures.free) await page.goto('/creator/editor/paths') await expect(page).toHaveURL(/\/dashboard/) // Doble verificación: la API también rechaza const res = await apiClient.post('/admin/paths', { title: 'Intento', slug: 'intento' }, fixtures.free.token) expect(res.status).toBe(403)})
test('anonymous user es redirigido al login desde /creator/editor/paths', async ({ page }) => { await page.goto('/creator/editor/paths') await expect(page).toHaveURL(/\/auth\/login|\/login/)})TC-EDIT-003: Guardar metadatos del path dispara PATCH correcto
Section titled “TC-EDIT-003: Guardar metadatos del path dispara PATCH correcto”AC: AC-EDIT-002.1 · Prioridad: Alta · Tipo: API · Estado: 📋 Pendiente
Spec: apps/e2e/tests/creator/content-editor.spec.ts
test('PATCH /admin/paths/:id actualiza título y descripción', async () => { // Crear path de prueba const create = await apiClient.post('/admin/paths', { title: 'Test Path', slug: `test-path-${Date.now()}`, description: 'Original', }, fixtures.contentAdmin.token) expect(create.status).toBe(201) const pathId = create.data.id
// Actualizar const update = await apiClient.patch(`/admin/paths/${pathId}`, { title: 'Test Path Updated', description: 'Descripción actualizada', accessLevel: 'PAID_STARTER', }, fixtures.contentAdmin.token) expect(update.status).toBe(200) expect(update.data.title).toBe('Test Path Updated') expect(update.data.accessLevel).toBe('PAID_STARTER')
// Limpiar await apiClient.delete(`/admin/paths/${pathId}`, fixtures.contentAdmin.token)})TC-EDIT-004: El slug se genera automáticamente desde el título
Section titled “TC-EDIT-004: El slug se genera automáticamente desde el título”AC: AC-EDIT-001.3 · Prioridad: Media · Tipo: E2E · Estado: 📋 Pendiente
Spec: apps/e2e/tests/creator/content-editor.spec.ts
test('slug se auto-genera a partir del título con normalización', async ({ page }) => { await loginAs(page, fixtures.contentAdmin) await page.goto('/creator/editor/paths') // Crear un nuevo path y verificar el campo slug await page.getByTestId('btn-nueva-ruta').click() await page.getByTestId('path-title-input').fill('Fundamentos de IA Generativa') const slugValue = await page.getByTestId('path-slug-input').inputValue() expect(slugValue).toBe('fundamentos-de-ia-generativa')})TC-EDIT-005: Añadir módulo a un path
Section titled “TC-EDIT-005: Añadir módulo a un path”AC: AC-EDIT-003.1 · Prioridad: Alta · Tipo: API · Estado: 📋 Pendiente
Spec: apps/e2e/tests/creator/content-editor.spec.ts
test('POST /admin/paths/:id/modules crea módulo correctamente', async () => { const create = await apiClient.post('/admin/paths', { title: 'Path para módulos', slug: `path-modules-${Date.now()}`, description: 'Test', }, fixtures.contentAdmin.token) const pathId = create.data.id
const module = await apiClient.post(`/admin/paths/${pathId}/modules`, { title: 'Módulo 1', accessLevel: 'FREE_REGISTERED', }, fixtures.contentAdmin.token) expect(module.status).toBe(201) expect(module.data.title).toBe('Módulo 1') expect(module.data.isPublished).toBe(false)
await apiClient.delete(`/admin/paths/${pathId}`, fixtures.contentAdmin.token)})TC-EDIT-006: Reordenar módulos
Section titled “TC-EDIT-006: Reordenar módulos”AC: AC-EDIT-003.2 · Prioridad: Media · Tipo: API · Estado: 📋 Pendiente
Spec: apps/e2e/tests/creator/content-editor.spec.ts
test('PATCH /admin/paths/:id/modules/reorder cambia el orden', async () => { // Setup: crear path con 2 módulos const create = await apiClient.post('/admin/paths', { title: 'Path reorder', slug: `path-reorder-${Date.now()}`, description: 'Test', }, fixtures.contentAdmin.token) const pathId = create.data.id const m1 = await apiClient.post(`/admin/paths/${pathId}/modules`, { title: 'M1' }, fixtures.contentAdmin.token) const m2 = await apiClient.post(`/admin/paths/${pathId}/modules`, { title: 'M2' }, fixtures.contentAdmin.token)
// Reordenar: M2 primero const reorder = await apiClient.patch(`/admin/paths/${pathId}/modules/reorder`, { moduleIds: [m2.data.id, m1.data.id], }, fixtures.contentAdmin.token) expect(reorder.status).toBe(200)
// Verificar orden const detail = await apiClient.get(`/admin/paths/${pathId}`, fixtures.contentAdmin.token) expect(detail.data.modules[0].id).toBe(m2.data.id)
await apiClient.delete(`/admin/paths/${pathId}`, fixtures.contentAdmin.token)})TC-EDIT-007: Crear lección en un módulo
Section titled “TC-EDIT-007: Crear lección en un módulo”AC: AC-EDIT-004.1 · Prioridad: Alta · Tipo: API · Estado: 📋 Pendiente
Spec: apps/e2e/tests/creator/content-editor.spec.ts
test('POST /admin/modules/:moduleId/lessons crea lección no publicada', async () => { // Setup: path + módulo const create = await apiClient.post('/admin/paths', { title: 'Path lección', slug: `path-lesson-${Date.now()}`, description: 'Test', }, fixtures.contentAdmin.token) const pathId = create.data.id const mod = await apiClient.post(`/admin/paths/${pathId}/modules`, { title: 'Mod' }, fixtures.contentAdmin.token) const moduleId = mod.data.id
const lesson = await apiClient.post(`/admin/modules/${moduleId}/lessons`, { title: 'Lección 1', contentType: 'MARKDOWN', }, fixtures.contentAdmin.token) expect(lesson.status).toBe(201) expect(lesson.data.isPublished).toBe(false) expect(lesson.data.contentType).toBe('MARKDOWN')
await apiClient.delete(`/admin/paths/${pathId}`, fixtures.contentAdmin.token)})TC-EDIT-008: Actualizar contenido Markdown de una lección
Section titled “TC-EDIT-008: Actualizar contenido Markdown de una lección”AC: AC-EDIT-004.2 · Prioridad: Alta · Tipo: API · Estado: 📋 Pendiente
Spec: apps/e2e/tests/creator/content-editor.spec.ts
test('PATCH /admin/lessons/:id actualiza contentMd', async () => { // Asume que fixtures.contentAdmin tiene al menos una lección propia — usar una creada dinámicamente const create = await apiClient.post('/admin/paths', { title: 'Path md', slug: `path-md-${Date.now()}`, description: 'Test', }, fixtures.contentAdmin.token) const mod = await apiClient.post(`/admin/paths/${create.data.id}/modules`, { title: 'M' }, fixtures.contentAdmin.token) const lessonRes = await apiClient.post(`/admin/modules/${mod.data.id}/lessons`, { title: 'L', contentType: 'MARKDOWN' }, fixtures.contentAdmin.token) const lessonId = lessonRes.data.id
const update = await apiClient.patch(`/admin/lessons/${lessonId}`, { contentMd: '## Introducción\n\nContenido de prueba.', }, fixtures.contentAdmin.token) expect(update.status).toBe(200) expect(update.data.contentMd).toContain('## Introducción')
await apiClient.delete(`/admin/paths/${create.data.id}`, fixtures.contentAdmin.token)})TC-EDIT-009: Asignar access_level PUBLIC a lección dentro de path PAID_PRO
Section titled “TC-EDIT-009: Asignar access_level PUBLIC a lección dentro de path PAID_PRO”AC: AC-EDIT-005.1 · Prioridad: Alta · Tipo: API · Estado: 📋 Pendiente
Spec: apps/e2e/tests/creator/content-editor.spec.ts
test('lección puede tener accessLevel más abierto que el path padre', async () => { const create = await apiClient.post('/admin/paths', { title: 'Path PRO', slug: `path-pro-${Date.now()}`, description: 'Test', accessLevel: 'PAID_PRO', }, fixtures.contentAdmin.token) const mod = await apiClient.post(`/admin/paths/${create.data.id}/modules`, { title: 'M', accessLevel: 'PAID_PRO' }, fixtures.contentAdmin.token) const lessonRes = await apiClient.post(`/admin/modules/${mod.data.id}/lessons`, { title: 'Preview' }, fixtures.contentAdmin.token)
// Asignar PUBLIC (más abierto que PAID_PRO) const update = await apiClient.patch(`/admin/lessons/${lessonRes.data.id}`, { accessLevel: 'PUBLIC', }, fixtures.contentAdmin.token) expect(update.status).toBe(200) expect(update.data.accessLevel).toBe('PUBLIC')
await apiClient.delete(`/admin/paths/${create.data.id}`, fixtures.contentAdmin.token)})TC-EDIT-010: Lanzar generación IA — Escape Room
Section titled “TC-EDIT-010: Lanzar generación IA — Escape Room”AC: AC-EDIT-006.1 · Prioridad: Alta · Tipo: API · Estado: 📋 Pendiente
Spec: apps/e2e/tests/creator/content-editor.spec.ts
test('POST /game-activities/roomscape/generate crea AITask en PENDING', async () => { // Requiere path + módulo con lección con contenido // ... setup ... const generate = await apiClient.post('/game-activities/roomscape/generate', { moduleId: '<moduleId>', pathId: '<pathId>', treeDepth: 2, scenarioTheme: 'professional', }, fixtures.contentAdmin.token) expect(generate.status).toBe(201) expect(generate.data.taskId).toBeDefined()
// Verificar que se creó una AITask const task = await apiClient.get(`/ai/tasks/${generate.data.taskId}/result`, fixtures.contentAdmin.token) expect(task.status).toBe(200) expect(['PENDING', 'PROCESSING', 'COMPLETED']).toContain(task.data.status)})TC-EDIT-011: Usuario FREE no puede lanzar generación IA
Section titled “TC-EDIT-011: Usuario FREE no puede lanzar generación IA”AC: AC-EDIT-006.1 · Prioridad: Alta · Tipo: API · Estado: 📋 Pendiente
Spec: apps/e2e/tests/creator/content-editor.spec.ts
test('FREE user recibe 403 en POST /game-activities/roomscape/generate', async () => { const res = await apiClient.post('/game-activities/roomscape/generate', { moduleId: 'any-id', pathId: 'any-id', treeDepth: 2, scenarioTheme: 'auto', }, fixtures.free.token) expect(res.status).toBe(403)})TC-EDIT-012: Polling de AITask hasta COMPLETED
Section titled “TC-EDIT-012: Polling de AITask hasta COMPLETED”AC: AC-EDIT-007.1 · Prioridad: Alta · Tipo: API · Estado: 📋 Pendiente
Spec: apps/e2e/tests/creator/content-editor.spec.ts
test('GET /ai/tasks/:taskId/result retorna estado actualizado', async () => { // Asume taskId de una tarea existente en estado COMPLETED (mock) const res = await apiClient.get('/ai/tasks/<completedTaskId>/result', fixtures.contentAdmin.token) expect(res.status).toBe(200) expect(res.data.status).toBe('COMPLETED') expect(res.data.result).toBeDefined()})
test('tarea de otro usuario retorna 403', async () => { // La tarea fue creada por contentAdmin const res = await apiClient.get('/ai/tasks/<contentAdminTaskId>/result', fixtures.free.token) expect(res.status).toBe(403)})TC-EDIT-013: Tarea FAILED muestra error en la UI
Section titled “TC-EDIT-013: Tarea FAILED muestra error en la UI”AC: AC-EDIT-007.2 · Prioridad: Media · Tipo: E2E · Estado: 📋 Pendiente
Spec: apps/e2e/tests/creator/content-editor.spec.ts
test('módulo con AITask FAILED muestra indicador de error', async ({ page }) => { await loginAs(page, fixtures.contentAdmin) // Navegar al módulo con tarea fallida (fixture de datos o mock) await page.goto(`/creator/editor/modules/<moduleId>`) await expect(page.getByTestId('ai-task-status-failed')).toBeVisible() await expect(page.getByTestId('btn-retry-generation')).toBeVisible()})TC-EDIT-014: Aceptar actividad generada
Section titled “TC-EDIT-014: Aceptar actividad generada”AC: AC-EDIT-008.1 · Prioridad: Alta · Tipo: API · Estado: 📋 Pendiente
Spec: apps/e2e/tests/creator/content-editor.spec.ts
test('PATCH /game-activities/roomscape/:id/accept publica la actividad', async () => { // Asume gameActivityId en estado PENDING_REVIEW (después de generación) const accept = await apiClient.patch(`/game-activities/roomscape/<gameActivityId>/accept`, { taskId: '<completedTaskId>', }, fixtures.contentAdmin.token) expect(accept.status).toBe(200)
// Verificar que la actividad está activa en el módulo const activities = await apiClient.get('/game-activities?moduleId=<moduleId>', fixtures.contentAdmin.token) const accepted = activities.data.find((a: { id: string }) => a.id === '<gameActivityId>') expect(accepted).toBeDefined() expect(accepted.status).toBe('ACTIVE')})TC-EDIT-015: Rechazar actividad generada
Section titled “TC-EDIT-015: Rechazar actividad generada”AC: AC-EDIT-009.1 · Prioridad: Media · Tipo: API · Estado: 📋 Pendiente
Spec: apps/e2e/tests/creator/content-editor.spec.ts
test('PATCH /ai/tasks/:taskId/review-status rechaza la tarea', async () => { const res = await apiClient.patch(`/ai/tasks/<taskId>/review-status`, { status: 'REJECTED', }, fixtures.contentAdmin.token) expect(res.status).toBe(200) expect(res.data.status).toBe('REJECTED')})TC-EDIT-016: Publicar path con módulo y lección publicados
Section titled “TC-EDIT-016: Publicar path con módulo y lección publicados”AC: AC-EDIT-010.1 · Prioridad: Alta · Tipo: API · Estado: 📋 Pendiente
Spec: apps/e2e/tests/creator/content-editor.spec.ts
test('PATCH /admin/paths/:id/publish publica correctamente', async () => { // Setup: path + módulo publicado + lección publicada const create = await apiClient.post('/admin/paths', { title: 'Publish Test', slug: `publish-test-${Date.now()}`, description: 'Test', }, fixtures.contentAdmin.token) const pathId = create.data.id const mod = await apiClient.post(`/admin/paths/${pathId}/modules`, { title: 'M' }, fixtures.contentAdmin.token) const lesson = await apiClient.post(`/admin/modules/${mod.data.id}/lessons`, { title: 'L' }, fixtures.contentAdmin.token)
// Publicar módulo y lección await apiClient.patch(`/admin/modules/${mod.data.id}`, { isPublished: true }, fixtures.contentAdmin.token) await apiClient.patch(`/admin/lessons/${lesson.data.id}`, { isPublished: true }, fixtures.contentAdmin.token)
// Publicar path const publish = await apiClient.patch(`/admin/paths/${pathId}/publish`, { isPublished: true }, fixtures.contentAdmin.token) expect(publish.status).toBe(200) expect(publish.data.isPublished).toBe(true)
// El path debe estar en el catálogo público const catalog = await apiClient.get(`/paths/${create.data.slug}`) expect(catalog.status).toBe(200)
// Limpiar await apiClient.patch(`/admin/paths/${pathId}/publish`, { isPublished: false }, fixtures.contentAdmin.token) await apiClient.delete(`/admin/paths/${pathId}`, fixtures.contentAdmin.token)})TC-EDIT-017: Checklist incompleto bloquea solicitud de revisión
Section titled “TC-EDIT-017: Checklist incompleto bloquea solicitud de revisión”AC: AC-EDIT-010.2 · Prioridad: Alta · Tipo: E2E · Estado: 📋 Pendiente
Spec: apps/e2e/tests/creator/content-editor.spec.ts
test('ReviewRequestGate aparece si el checklist no está completo', async ({ page }) => { await loginAs(page, fixtures.contentAdmin) await page.goto(`/creator/editor/paths/<pathIdWithIncompleteChecklist>`) await page.getByTestId('btn-solicitar-revision').click() // Debe aparecer el diálogo ReviewRequestGate, no llamar al endpoint await expect(page.getByTestId('review-request-gate-dialog')).toBeVisible() await expect(page.getByTestId('checklist-incomplete-warning')).toBeVisible()})TC-EDIT-018: CONTENT_ADMIN no puede editar path de otro creator (API)
Section titled “TC-EDIT-018: CONTENT_ADMIN no puede editar path de otro creator (API)”AC: AC-EDIT-011.1 · Prioridad: Alta · Tipo: API · Estado: 📋 Pendiente
Spec: apps/e2e/tests/creator/content-editor.spec.ts
test('CONTENT_ADMIN recibe 403 al editar path de otro creator', async () => { // pathIdFromOtherCreator debe estar pre-creado por un SUPER_ADMIN fixture const res = await apiClient.patch('/admin/paths/<pathIdFromOtherCreator>', { title: 'Intento de edición', }, fixtures.contentAdmin.token) expect(res.status).toBe(403) expect(res.data.message).toMatch(/No tienes acceso/)})TC-EDIT-019: CONTENT_ADMIN solo ve sus propios paths en el listado
Section titled “TC-EDIT-019: CONTENT_ADMIN solo ve sus propios paths en el listado”AC: AC-EDIT-011.2 · Prioridad: Alta · Tipo: API · Estado: 📋 Pendiente
Spec: apps/e2e/tests/creator/content-editor.spec.ts
test('GET /admin/paths solo retorna paths del creator autenticado', async () => { const res = await apiClient.get('/admin/paths', fixtures.contentAdmin.token) expect(res.status).toBe(200) // Todos los paths deben pertenecer al creatorProfileId del contentAdmin for (const path of res.data) { // No tenemos el creatorProfileId en la respuesta pública, pero podemos verificar // que el path con ID ajeno no aparece en la lista expect(path.id).not.toBe('<pathIdFromOtherCreator>') }})TC-EDIT-020: Overlay y toast al guardar en AdminPathEditor
Section titled “TC-EDIT-020: Overlay y toast al guardar en AdminPathEditor”AC: AC-EDIT-012.1, AC-EDIT-012.2 · Prioridad: Media · Tipo: E2E · Estado: 📋 Pendiente
Spec: apps/e2e/tests/creator/content-editor.spec.ts
test('guardar path muestra overlay spinner y luego toast verde', async ({ page }) => { await loginAs(page, fixtures.contentAdmin) await page.goto(`/creator/editor/paths/<existingPathId>`)
// Modificar un campo para activar el botón guardar await page.getByTestId('path-title-input').fill(`Test updated ${Date.now()}`)
// Hacer clic en guardar await page.getByTestId('btn-guardar-path').click()
// Overlay debe aparecer inmediatamente await expect(page.getByTestId('saving-overlay')).toBeVisible()
// Overlay desaparece y toast aparece await expect(page.getByTestId('saving-overlay')).not.toBeVisible({ timeout: 5000 }) await expect(page.getByTestId('save-success-toast')).toBeVisible()
// Toast desaparece en ~3 segundos await expect(page.getByTestId('save-success-toast')).not.toBeVisible({ timeout: 5000 })})Referencia de endpoints cubiertos
Section titled “Referencia de endpoints cubiertos”| Método | Endpoint | Controller | Guard |
|---|---|---|---|
POST | /admin/paths | AdminPathsController | RolesGuard(CONTENT_ADMIN, SUPER_ADMIN) |
GET | /admin/paths | AdminPathsController | RolesGuard(CONTENT_ADMIN, SUPER_ADMIN) |
GET | /admin/paths/:id | AdminPathsController | RolesGuard + ownership check |
PATCH | /admin/paths/:id | AdminPathsController | RolesGuard + ownership check |
DELETE | /admin/paths/:id | AdminPathsController | RolesGuard + ownership check |
PATCH | /admin/paths/:id/publish | AdminPathsController | RolesGuard + ownership check |
POST | /admin/paths/:id/preview-image | AdminPathsController | RolesGuard + ownership check |
POST | /admin/paths/:id/banner-image | AdminPathsController | RolesGuard + ownership check |
PATCH | /admin/paths/:id/modules/reorder | AdminPathsController | RolesGuard + ownership check |
POST | /admin/paths/:id/modules | AdminPathsController | RolesGuard + ownership check |
PATCH | /admin/modules/:id | AdminPathsController | RolesGuard + ownership check |
DELETE | /admin/modules/:id | AdminPathsController | RolesGuard + ownership check |
PATCH | /admin/modules/:id/lessons/reorder | AdminPathsController | RolesGuard + ownership check |
POST | /admin/modules/:moduleId/lessons | AdminLessonsController | RolesGuard + ownership check |
PATCH | /admin/lessons/:id | AdminLessonsController | RolesGuard + ownership check |
DELETE | /admin/lessons/:id | AdminLessonsController | RolesGuard + ownership check |
POST | /admin/modules/:moduleId/lessons/:lessonId/link | AdminLessonsController | RolesGuard + ownership check |
POST | /game-activities/roomscape/generate | EscapeRoomController | RolesGuard(CONTENT_ADMIN, SUPER_ADMIN) |
PATCH | /game-activities/roomscape/:id/accept | EscapeRoomController | RolesGuard |
GET | /ai/tasks | AiTasksController | JwtAuthGuard (filtra por userId) |
GET | /ai/tasks/:taskId/result | AiTasksController | JwtAuthGuard (owner only) |
PATCH | /ai/tasks/:taskId/review-status | AiTasksController | JwtAuthGuard (owner only) |
PATCH | /ai/tasks/:taskId/cancel | AiTasksController | JwtAuthGuard (owner only) |
GET | /admin/paths/:id/checklist | AdminPathsController | RolesGuard + ownership check |
POST | /admin/paths/:id/request-review | AdminPathsController | RolesGuard + ownership check |