test(webui): add vitest parser tests and playwright ui coverage
This commit is contained in:
@@ -43,13 +43,13 @@ test('@mock detects freenove_legacy and limits unsupported actions', async ({ pa
|
||||
|
||||
await page.goto('/')
|
||||
|
||||
await expect(page.getByText('Freenove Legacy API')).toBeVisible()
|
||||
await expect(page.getByText('API Legacy')).toBeVisible()
|
||||
await expect(
|
||||
page.getByText('Legacy mode detected. Scenario selection and start are unavailable, but you can monitor'),
|
||||
page.getByText('Mode legacy detecte: selection/start sont indisponibles.'),
|
||||
).toBeVisible()
|
||||
|
||||
await page.getByRole('button', { name: 'Open monitor' }).first().click()
|
||||
await expect(page.getByRole('heading', { name: 'Live Orchestrator' })).toBeVisible()
|
||||
await page.getByRole('button', { name: 'Ouvrir monitor' }).first().click()
|
||||
await expect(page.getByRole('heading', { name: 'Orchestrateur live' })).toBeVisible()
|
||||
await expect(page.getByRole('button', { name: 'Pause' })).toBeDisabled()
|
||||
|
||||
await page.getByRole('button', { name: 'Skip' }).click()
|
||||
@@ -75,12 +75,9 @@ test('@mock keeps Story Designer in read/edit mode for legacy APIs', async ({ pa
|
||||
})
|
||||
|
||||
await page.goto('/')
|
||||
await page.getByRole('button', { name: 'Story Designer' }).click()
|
||||
await page.getByRole('button', { name: 'Designer' }).click()
|
||||
|
||||
await expect(
|
||||
page.getByText('Story Designer is in read/edit mode. Validate/deploy actions require Story V2 API support.'),
|
||||
).toBeVisible()
|
||||
await expect(page.getByRole('button', { name: 'Validate' })).toBeDisabled()
|
||||
await expect(page.getByRole('button', { name: 'Deploy' })).toBeDisabled()
|
||||
await expect(page.getByText('Mode lecture/edition: validate/deploy requierent les APIs Story V2.')).toBeVisible()
|
||||
await expect(page.getByRole('button', { name: 'Valider' })).toBeDisabled()
|
||||
await expect(page.getByRole('button', { name: 'Déployer' })).toBeDisabled()
|
||||
})
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { expect, test } from '@playwright/test'
|
||||
|
||||
test('@mock story designer imports yaml, edits bindings and supports undo/redo', async ({ page }) => {
|
||||
await page.addInitScript(() => window.localStorage.clear())
|
||||
|
||||
await page.route('**/api/story/list', async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({ scenarios: [{ id: 'DEFAULT', estimated_duration_s: 120 }] }),
|
||||
})
|
||||
})
|
||||
|
||||
await page.goto('/')
|
||||
await page.getByRole('button', { name: 'Designer' }).click()
|
||||
|
||||
await expect(page.getByText('Nodes: 2')).toBeVisible()
|
||||
await page.getByRole('button', { name: 'Ajouter node' }).click()
|
||||
await expect(page.getByText('Nodes: 3')).toBeVisible()
|
||||
|
||||
await page.getByRole('button', { name: 'Import YAML → Graphe' }).click()
|
||||
await expect(page.getByText('YAML importé dans le graphe.')).toBeVisible()
|
||||
await expect(page.getByText('Nodes: 2')).toBeVisible()
|
||||
|
||||
await page.getByRole('button', { name: 'Ajouter node' }).click()
|
||||
await expect(page.getByText('Nodes: 3')).toBeVisible()
|
||||
|
||||
await page.getByRole('button', { name: 'Annuler' }).click()
|
||||
await expect(page.getByText('Nodes: 2')).toBeVisible()
|
||||
|
||||
await page.getByRole('button', { name: 'Retablir' }).click()
|
||||
await expect(page.getByText('Nodes: 3')).toBeVisible()
|
||||
|
||||
const firstBindingIdInput = page.locator('label:has-text("ID") input').first()
|
||||
await firstBindingIdInput.fill('APP_TEST_BIND')
|
||||
|
||||
await page.getByRole('button', { name: 'Export Graphe → YAML' }).click()
|
||||
await firstBindingIdInput.fill('APP_TEMP_BIND')
|
||||
await page.getByRole('button', { name: 'Import YAML → Graphe' }).click()
|
||||
await expect(firstBindingIdInput).toHaveValue('APP_TEST_BIND')
|
||||
})
|
||||
@@ -42,10 +42,10 @@ test('@mock detects story_v2 and opens orchestrator', async ({ page }) => {
|
||||
|
||||
await page.goto('/')
|
||||
|
||||
await expect(page.getByText('Story V2 API')).toBeVisible()
|
||||
await expect(page.getByRole('button', { name: 'Play' }).first()).toBeVisible()
|
||||
await page.getByRole('button', { name: 'Play' }).first().click()
|
||||
await expect(page.getByRole('heading', { name: 'Live Orchestrator' })).toBeVisible()
|
||||
await expect(page.getByText('API Story V2')).toBeVisible()
|
||||
await expect(page.getByRole('button', { name: 'Lancer' }).first()).toBeVisible()
|
||||
await page.getByRole('button', { name: 'Lancer' }).first().click()
|
||||
await expect(page.getByRole('heading', { name: 'Orchestrateur live' })).toBeVisible()
|
||||
})
|
||||
|
||||
test('@mock maps API 404 and 507 errors to friendly messages', async ({ page }) => {
|
||||
@@ -77,11 +77,10 @@ test('@mock maps API 404 and 507 errors to friendly messages', async ({ page })
|
||||
|
||||
await page.goto('/')
|
||||
|
||||
await page.getByRole('button', { name: 'Play' }).first().click()
|
||||
await expect(page.getByText('Scenario not found. Browse available scenarios.')).toBeVisible()
|
||||
await page.getByRole('button', { name: 'Lancer' }).first().click()
|
||||
await expect(page.getByText('Scenario introuvable. Verifie la liste disponible.')).toBeVisible()
|
||||
|
||||
await page.getByRole('button', { name: 'Story Designer' }).click()
|
||||
await page.getByRole('button', { name: 'Deploy' }).click()
|
||||
await expect(page.getByText('Device storage full. Delete old scenarios.')).toBeVisible()
|
||||
await page.getByRole('button', { name: 'Designer' }).click()
|
||||
await page.getByRole('button', { name: 'Déployer' }).click()
|
||||
await expect(page.getByText("Stockage du device plein. Supprime d'anciens scenarios.")).toBeVisible()
|
||||
})
|
||||
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
generateStoryYamlFromGraph,
|
||||
importStoryYamlToGraph,
|
||||
type StoryGraphDocument,
|
||||
validateStoryGraph,
|
||||
} from '../../src/features/story-designer'
|
||||
|
||||
const canonicalYaml = `id: STORY_CANON
|
||||
version: 2
|
||||
initial_step: STEP_A
|
||||
app_bindings:
|
||||
- id: APP_LA
|
||||
app: LA_DETECTOR
|
||||
config:
|
||||
hold_ms: 5000
|
||||
unlock_event: UNLOCK
|
||||
require_listening: true
|
||||
- id: APP_AUDIO
|
||||
app: AUDIO_PACK
|
||||
steps:
|
||||
- step_id: STEP_A
|
||||
screen_scene_id: SCENE_A
|
||||
audio_pack_id: PACK_A
|
||||
actions: [ACTION_TRACE_STEP]
|
||||
apps: [APP_LA]
|
||||
mp3_gate_open: false
|
||||
transitions:
|
||||
- trigger: on_event
|
||||
event_type: unlock
|
||||
event_name: UNLOCK
|
||||
target_step_id: STEP_B
|
||||
after_ms: 0
|
||||
priority: 100
|
||||
- step_id: STEP_B
|
||||
screen_scene_id: SCENE_B
|
||||
audio_pack_id: ""
|
||||
actions: [ACTION_TRACE_STEP]
|
||||
apps: [APP_AUDIO]
|
||||
mp3_gate_open: true
|
||||
transitions: []
|
||||
`
|
||||
|
||||
const normalizeDocument = (document: StoryGraphDocument) => ({
|
||||
scenarioId: document.scenarioId,
|
||||
version: document.version,
|
||||
initialStep: document.initialStep,
|
||||
appBindings: [...document.appBindings]
|
||||
.map((binding) => ({
|
||||
id: binding.id,
|
||||
app: binding.app,
|
||||
config: binding.config
|
||||
? {
|
||||
hold_ms: binding.config.hold_ms,
|
||||
unlock_event: binding.config.unlock_event,
|
||||
require_listening: binding.config.require_listening,
|
||||
}
|
||||
: undefined,
|
||||
}))
|
||||
.sort((left, right) => left.id.localeCompare(right.id)),
|
||||
nodes: [...document.nodes]
|
||||
.map((node) => ({
|
||||
stepId: node.stepId,
|
||||
screenSceneId: node.screenSceneId,
|
||||
audioPackId: node.audioPackId,
|
||||
actions: [...node.actions],
|
||||
apps: [...node.apps].sort(),
|
||||
mp3GateOpen: node.mp3GateOpen,
|
||||
isInitial: node.isInitial,
|
||||
}))
|
||||
.sort((left, right) => left.stepId.localeCompare(right.stepId)),
|
||||
edges: [...document.edges]
|
||||
.map((edge) => ({
|
||||
fromNodeId: edge.fromNodeId,
|
||||
toNodeId: edge.toNodeId,
|
||||
trigger: edge.trigger,
|
||||
eventType: edge.eventType,
|
||||
eventName: edge.eventName,
|
||||
afterMs: edge.afterMs,
|
||||
priority: edge.priority,
|
||||
}))
|
||||
.sort((left, right) => `${left.fromNodeId}-${left.toNodeId}`.localeCompare(`${right.fromNodeId}-${right.toNodeId}`)),
|
||||
})
|
||||
|
||||
describe('storyYaml import/export', () => {
|
||||
it('importe un YAML Story V2 canonique', () => {
|
||||
const result = importStoryYamlToGraph(canonicalYaml)
|
||||
|
||||
expect(result.errors).toEqual([])
|
||||
expect(result.document).toBeDefined()
|
||||
expect(result.document?.nodes).toHaveLength(2)
|
||||
expect(result.document?.edges).toHaveLength(1)
|
||||
expect(result.document?.appBindings).toHaveLength(2)
|
||||
expect(result.document?.initialStep).toBe('STEP_A')
|
||||
expect(result.document?.edges[0]?.eventType).toBe('unlock')
|
||||
})
|
||||
|
||||
it('importe un YAML legacy simplifie et mappe les transitions', () => {
|
||||
const legacyYaml = `id: LEGACY
|
||||
version: 2
|
||||
initial_step_id: STEP_WAIT_UNLOCK
|
||||
steps:
|
||||
- id: STEP_WAIT_UNLOCK
|
||||
screen_scene_id: SCENE_LOCKED
|
||||
transitions:
|
||||
- event: unlock
|
||||
target: STEP_DONE
|
||||
- id: STEP_DONE
|
||||
screen_scene_id: SCENE_DONE
|
||||
transitions: []
|
||||
`
|
||||
|
||||
const result = importStoryYamlToGraph(legacyYaml)
|
||||
|
||||
expect(result.errors).toEqual([])
|
||||
expect(result.warnings.length).toBeGreaterThan(0)
|
||||
expect(result.document?.nodes).toHaveLength(2)
|
||||
expect(result.document?.edges).toHaveLength(1)
|
||||
expect(result.document?.edges[0]).toMatchObject({
|
||||
trigger: 'on_event',
|
||||
eventType: 'unlock',
|
||||
eventName: 'UNLOCK',
|
||||
afterMs: 0,
|
||||
priority: 100,
|
||||
})
|
||||
})
|
||||
|
||||
it('retourne une erreur claire si le YAML est invalide', () => {
|
||||
const result = importStoryYamlToGraph('id: BAD\nsteps: [')
|
||||
|
||||
expect(result.document).toBeUndefined()
|
||||
expect(result.errors.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('clamp les transitions invalides (after_ms et priority)', () => {
|
||||
const yamlWithInvalidTransition = `id: CLAMP_TEST
|
||||
version: 2
|
||||
initial_step: STEP_A
|
||||
steps:
|
||||
- step_id: STEP_A
|
||||
transitions:
|
||||
- trigger: after_ms
|
||||
event_type: timer
|
||||
event_name: TIMER_A
|
||||
target_step_id: STEP_B
|
||||
after_ms: -42
|
||||
priority: 999
|
||||
- step_id: STEP_B
|
||||
transitions: []
|
||||
`
|
||||
|
||||
const result = importStoryYamlToGraph(yamlWithInvalidTransition)
|
||||
expect(result.document).toBeDefined()
|
||||
expect(result.document?.edges[0]?.afterMs).toBe(0)
|
||||
expect(result.document?.edges[0]?.priority).toBe(255)
|
||||
})
|
||||
|
||||
it('maintient une structure stable en round-trip import -> export -> import', () => {
|
||||
const imported = importStoryYamlToGraph(canonicalYaml)
|
||||
expect(imported.document).toBeDefined()
|
||||
|
||||
const exportedYaml = generateStoryYamlFromGraph(imported.document as StoryGraphDocument)
|
||||
const reimported = importStoryYamlToGraph(exportedYaml)
|
||||
|
||||
expect(reimported.errors).toEqual([])
|
||||
expect(reimported.document).toBeDefined()
|
||||
expect(normalizeDocument(reimported.document as StoryGraphDocument)).toEqual(
|
||||
normalizeDocument(imported.document as StoryGraphDocument),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('validation', () => {
|
||||
it('signale les bindings dupliques et priorites invalides', () => {
|
||||
const base = importStoryYamlToGraph(canonicalYaml).document as StoryGraphDocument
|
||||
const document: StoryGraphDocument = {
|
||||
...base,
|
||||
appBindings: [
|
||||
{ id: 'APP_AUDIO', app: 'AUDIO_PACK' },
|
||||
{ id: 'APP_AUDIO', app: 'AUDIO_PACK' },
|
||||
],
|
||||
edges: [
|
||||
{
|
||||
...base.edges[0],
|
||||
priority: 999,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const result = validateStoryGraph(document)
|
||||
|
||||
expect(result.errors.some((entry) => entry.includes('app_binding duplique'))).toBe(true)
|
||||
expect(result.errors.some((entry) => entry.includes('priority invalide'))).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,9 @@
|
||||
import { defineConfig } from 'vitest/config'
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
include: ['tests/unit/**/*.test.ts'],
|
||||
environment: 'node',
|
||||
passWithNoTests: false,
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user