Skip to content

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


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


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


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/paths
Cuando hago clic en "Nueva Ruta"
Entonces se llama a POST /admin/paths con título por defecto y slug único
Y 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/:id

AC-EDIT-001.2 — Usuario FREE no puede acceder al Creator Studio

Dado que estoy autenticado con rol FREE
Cuando navego directamente a /creator/editor/paths
Entonces soy redirigido a /dashboard
Y no veo ningún componente del Creator Studio
Y la API retorna 403 para cualquier llamada a POST /admin/paths

AC-EDIT-001.3 — Slug generado automáticamente desde el título

Dado que estoy en el editor de un path
Cuando escribo "Fundamentos de Marketing Digital" en el campo título
Entonces el campo slug se rellena automáticamente con "fundamentos-de-marketing-digital"
Y el slug reemplaza tildes, mayúsculas y caracteres especiales

US: US-EDIT-002

AC-EDIT-002.1 — Guardado de metadatos básicos

Dado que estoy editando un path que me pertenece
Cuando modifico el título, descripción o nivel de acceso
Y hago clic en "Guardar"
Entonces se llama a PATCH /admin/paths/:id con los campos actualizados
Y la API responde con status 200
Y el botón "Guardar" queda deshabilitado (no hay cambios pendientes)

AC-EDIT-002.2 — Subida de imagen de portada

Dado que estoy editando un path
Cuando subo una imagen de portada (JPG/PNG, máximo 5 MB)
Y guardo
Entonces se llama a POST /admin/paths/:id/preview-image con el archivo
Y la imagen previsualizada en el editor se actualiza

AC-EDIT-002.3 — Subida de imagen banner

Dado que estoy editando un path
Cuando subo una imagen banner (JPG/PNG, máximo 8 MB)
Y guardo
Entonces se llama a POST /admin/paths/:id/banner-image con el archivo

US: US-EDIT-003

AC-EDIT-003.1 — Añadir módulo

Dado que estoy en el editor del path
Cuando hago clic en "Añadir módulo" e introduzco un título
Entonces se llama a POST /admin/paths/:id/modules con el título y accessLevel
Y el nuevo módulo aparece al final de la lista con estado no publicado
Y la API responde con status 201

AC-EDIT-003.2 — Reordenar módulos con drag and drop

Dado que el path tiene al menos 2 módulos
Cuando 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 orden
Y la lista de módulos refleja el nuevo orden inmediatamente

AC-EDIT-003.3 — Eliminar módulo

Dado que un módulo no tiene lecciones vinculadas
Cuando hago clic en eliminar el módulo y confirmo
Entonces se llama a DELETE /admin/modules/:id
Y el módulo desaparece de la lista
Y la API responde con status 204

US: US-EDIT-004

AC-EDIT-004.1 — Crear lección en un módulo

Dado que estoy en el editor del módulo
Cuando creo una nueva lección con tipo MARKDOWN
Entonces se llama a POST /admin/modules/:moduleId/lessons
Y la API responde con status 201 con el ID de la lección nueva
Y la lección aparece en la lista del módulo con estado no publicada

AC-EDIT-004.2 — Editar contenido Markdown con auto-guardado

Dado que estoy editando el contenido Markdown de una lección
Cuando modifico el texto
Y espero el debounce (o hago clic en "Guardar")
Entonces se llama a PATCH /admin/lessons/:id con contentMd actualizado
Y el indicador "Guardado" aparece brevemente

AC-EDIT-004.3 — Tipos de contenido soportados

Dado que estoy creando una lección
Cuando selecciono tipo VIDEO
Entonces puedo introducir una URL de vídeo y duración en segundos
Y el tipo MARKDOWN permite un editor de texto enriquecido
Y el tipo MIXED combina texto y vídeo

AC-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_PRO
Cuando asigno accessLevel PUBLIC a una lección específica dentro de ese path
Entonces 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 preview
Y el resto de lecciones siguen requiriendo PAID_PRO

AC-EDIT-005.2 — Selector de access_level en el editor de módulo

Dado que estoy editando un módulo
Cuando cambio el accessLevel a PAID_STARTER
Y guardo
Entonces se llama a PATCH /admin/modules/:id con { accessLevel: "PAID_STARTER" }
Y la API responde con status 200

AC-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 contenido
Cuando hago clic en "Generar actividad IA" y selecciono el tipo (ej. Escape Room)
Y confirmo los parámetros de generación
Entonces se llama a POST /game-activities/roomscape/generate (o el endpoint del tipo elegido)
Y la API crea una AITask con estado PENDING
Y la UI muestra el estado "Generando..." para ese módulo

AC-EDIT-006.2 — Tipos de actividad disponibles

Dado que estoy en el Game Center del módulo
Cuando abro el selector de actividades
Entonces 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 Challenge
Y cada tipo muestra su ícono y nombre

AC-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 PENDING
Cuando el componente del módulo está visible
Entonces el cliente hace polling a GET /ai/tasks/:taskId/result cada pocos segundos
Y muestra el estado actual: PENDING, PROCESSING, COMPLETED o FAILED
Y al pasar a COMPLETED el botón "Ver resultado" se habilita automáticamente

AC-EDIT-007.2 — Tarea FAILED

Dado que la generación IA ha fallado (estado FAILED)
Cuando el polling detecta el estado
Entonces la UI muestra un indicador de error con el mensaje descriptivo
Y el creator puede intentar generar de nuevo

AC-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 COMPLETED
Cuando el creator hace clic en "Aceptar" desde el diálogo de resultado
Entonces 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 ACTIVE
Y la API responde con status 200
Y la actividad aparece en la lista de contenido del módulo

AC-EDIT-008.2 — La actividad aceptada es visible para estudiantes

Dado que una actividad ha sido aceptada
Cuando un estudiante con el nivel de acceso requerido visita el módulo
Entonces la actividad aparece en la lista de contenido del módulo
Y el estudiante puede iniciar la actividad desde el player correspondiente

US: US-EDIT-009

AC-EDIT-009.1 — Rechazar y eliminar

Dado que una AITask está en estado COMPLETED
Cuando el creator hace clic en "Rechazar" desde el diálogo de resultado
Entonces se llama a PATCH /ai/tasks/:taskId/review-status con { status: "REJECTED" }
Y la actividad pendiente queda marcada como rechazada
Y el creator puede lanzar una nueva generación para el mismo módulo

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 publicada
Cuando el creator hace clic en "Publicar"
Entonces se llama a PATCH /admin/paths/:id/publish con { isPublished: true }
Y la API responde con status 200
Y el path aparece en el catálogo público
Y 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ón
Entonces aparece el diálogo ReviewRequestGate con los items pendientes
Y no se llama a POST /admin/paths/:id/request-review hasta que el checklist esté completo

AC-EDIT-010.3 — Despublicar path

Dado que el path está publicado (isPublished: true)
Cuando el creator hace clic en "Despublicar" y confirma
Entonces se llama a PATCH /admin/paths/:id/publish con { isPublished: false }
Y el path desaparece del catálogo público
Y los estudiantes ya matriculados conservan su progreso

AC-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 creator
Cuando llamo a PATCH /admin/paths/path-ajeno
Entonces la API retorna 403
Y el mensaje es "No tienes acceso a esta ruta"

AC-EDIT-011.2 — CONTENT_ADMIN no lista paths ajenos

Dado que soy CONTENT_ADMIN
Cuando llamo a GET /admin/paths
Entonces la respuesta solo contiene mis paths (creatorId === mi creatorProfileId)
Y no aparecen los paths de otros creators

AC-EDIT-011.3 — SUPER_ADMIN puede editar cualquier contenido

Dado que soy SUPER_ADMIN
Cuando llamo a PATCH /admin/paths/:id para cualquier path
Entonces la API responde con status 200 independientemente del creatorId del path

AC-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 AdminModuleEditor
Cuando 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 PATCH

AC-EDIT-012.2 — Toast de confirmación

Dado que la petición PATCH ha completado con éxito
Cuando el overlay desaparece
Entonces aparece un toast en la esquina inferior derecha con fondo verde (#16A34A)
Y el toast muestra un ícono de check (✓) y el mensaje de éxito
Y el toast desaparece automáticamente tras 3 segundos

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:

  1. Navegar a /creator/editor/paths
  2. Hacer clic en el botón “Nueva Ruta”
  3. Verificar redirección a /creator/editor/paths/:newId
  4. 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')
})

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)
})

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)
})

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()
})

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')
})

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 })
})

MétodoEndpointControllerGuard
POST/admin/pathsAdminPathsControllerRolesGuard(CONTENT_ADMIN, SUPER_ADMIN)
GET/admin/pathsAdminPathsControllerRolesGuard(CONTENT_ADMIN, SUPER_ADMIN)
GET/admin/paths/:idAdminPathsControllerRolesGuard + ownership check
PATCH/admin/paths/:idAdminPathsControllerRolesGuard + ownership check
DELETE/admin/paths/:idAdminPathsControllerRolesGuard + ownership check
PATCH/admin/paths/:id/publishAdminPathsControllerRolesGuard + ownership check
POST/admin/paths/:id/preview-imageAdminPathsControllerRolesGuard + ownership check
POST/admin/paths/:id/banner-imageAdminPathsControllerRolesGuard + ownership check
PATCH/admin/paths/:id/modules/reorderAdminPathsControllerRolesGuard + ownership check
POST/admin/paths/:id/modulesAdminPathsControllerRolesGuard + ownership check
PATCH/admin/modules/:idAdminPathsControllerRolesGuard + ownership check
DELETE/admin/modules/:idAdminPathsControllerRolesGuard + ownership check
PATCH/admin/modules/:id/lessons/reorderAdminPathsControllerRolesGuard + ownership check
POST/admin/modules/:moduleId/lessonsAdminLessonsControllerRolesGuard + ownership check
PATCH/admin/lessons/:idAdminLessonsControllerRolesGuard + ownership check
DELETE/admin/lessons/:idAdminLessonsControllerRolesGuard + ownership check
POST/admin/modules/:moduleId/lessons/:lessonId/linkAdminLessonsControllerRolesGuard + ownership check
POST/game-activities/roomscape/generateEscapeRoomControllerRolesGuard(CONTENT_ADMIN, SUPER_ADMIN)
PATCH/game-activities/roomscape/:id/acceptEscapeRoomControllerRolesGuard
GET/ai/tasksAiTasksControllerJwtAuthGuard (filtra por userId)
GET/ai/tasks/:taskId/resultAiTasksControllerJwtAuthGuard (owner only)
PATCH/ai/tasks/:taskId/review-statusAiTasksControllerJwtAuthGuard (owner only)
PATCH/ai/tasks/:taskId/cancelAiTasksControllerJwtAuthGuard (owner only)
GET/admin/paths/:id/checklistAdminPathsControllerRolesGuard + ownership check
POST/admin/paths/:id/request-reviewAdminPathsControllerRolesGuard + ownership check