All checks were successful
mode séance, battlemaps multiples et export Foundry ciblé
- Quêtes (Niveau 1) : entité Quest orthogonale à l'arbre, rattachée à un arc HUB
ou libre (migrations V9-V12, V18, V20 ; réconciliation des jumeaux V10).
Fusion quête/conteneur dans la sidebar, progression par partie (statuts
disponible/en cours/terminée), et quêtes libres avec espace de scènes créé
automatiquement (arc technique « Quêtes libres », V21 : levée de la
contrainte arcs.type héritée du baseline).
- Guidage (Pilier B) : bilan de préparation 100 % dérivé (règles arc/chapitre/
scène/quête), panneau « Prochaines étapes » avec boutons « Corriger », et
pastilles détaillées (tooltip des manques) dans l'arbre.
- Assistant IA (Pilier A) : étoffage champ par champ (scène, chapitre, arc) et
brouillons de scènes en propose→applique (brain : narrative-fields,
scene-drafts).
- Horloges & menaces (V15-V17) : clocks à segments avec déclencheurs, fronts.
- Mode séance : préparation de séance (readiness + quêtes dispo), scène
épinglée (V19), récap « précédemment » (brain : session-recap), onglet
« Partie » du panneau de référence, graphe amélioré (pan/zoom, éditeur de
liens).
- Perf : endpoint agrégé GET /api/campaigns/{id}/tree — la sidebar charge en
1 requête au lieu de ~15.
- Battlemaps multiples par scène (variantes jour/nuit, étages…) : liste JSON
étiquetée (V22, reprise automatique de la carte existante), rendu PDF avec
légendes, export/import rétro-compatible.
- Export Foundry ciblé : modale de périmètre (cartes+ennemis / journaux /
tables), bundle filtré côté serveur (zip allégé), module Foundry à jour
(respect du périmètre + une Scene Foundry par variante de carte,
rétro-compatible anciens bundles).
79 lines
3.0 KiB
TypeScript
79 lines
3.0 KiB
TypeScript
import { test, expect } from '@playwright/test';
|
|
import {
|
|
seedCampaign,
|
|
seedArc,
|
|
seedChapter,
|
|
deleteCampaign,
|
|
getChapterById,
|
|
type SeededCampaign,
|
|
type SeededArc,
|
|
type SeededChapter,
|
|
} from '../../fixtures/api';
|
|
|
|
test.describe('Chapter edit', () => {
|
|
let campaign: SeededCampaign;
|
|
let arc: SeededArc;
|
|
let chapter: SeededChapter;
|
|
|
|
test.beforeEach(async ({ request }) => {
|
|
campaign = await seedCampaign(request);
|
|
arc = await seedArc(request, { campaignId: campaign.id });
|
|
chapter = await seedChapter(request, { arcId: arc.id });
|
|
});
|
|
|
|
test.afterEach(async ({ request }) => {
|
|
if (campaign?.id) await deleteCampaign(request, campaign.id);
|
|
});
|
|
|
|
test('edits all chapter fields and persists them to API', async ({ page, request }) => {
|
|
const newName = `${chapter.name} renamed`;
|
|
const values = {
|
|
description: 'Le chapitre ouvre sur un village en proie à la peur.',
|
|
gmNotes: 'Le maire cache un pacte avec les gobelins.',
|
|
playerObjectives: "Découvrir la source des disparitions.",
|
|
narrativeStakes: "La confiance du village est en jeu.",
|
|
};
|
|
|
|
await page.goto(`/campaigns/${campaign.id}/arcs/${arc.id}/chapters/${chapter.id}/edit`);
|
|
|
|
await expect(page.getByLabel(/Titre du chapitre/i)).toHaveValue(chapter.name);
|
|
|
|
await page.getByLabel(/Titre du chapitre/i).fill(newName);
|
|
await page.getByLabel(/Synopsis du chapitre/i).fill(values.description);
|
|
|
|
// Les champs narratifs sont dans des sections repliables fermées par défaut :
|
|
// on les ouvre avant de taper (un clic sur l'en-tête toggle la section).
|
|
await page.locator('app-expandable-section', { hasText: 'Notes du MJ' }).click();
|
|
await page.getByLabel(/Notes du Maître de Jeu/i).fill(values.gmNotes);
|
|
|
|
await page.locator('app-expandable-section', { hasText: 'Objectifs et enjeux' }).click();
|
|
await page.getByLabel(/Objectifs des joueurs/i).fill(values.playerObjectives);
|
|
await page.getByLabel(/Enjeux narratifs/i).fill(values.narrativeStakes);
|
|
|
|
await page.getByRole('button', { name: /^Sauvegarder$/i }).click();
|
|
|
|
await expect(page).toHaveURL(
|
|
new RegExp(`/campaigns/${campaign.id}/arcs/${arc.id}/chapters/${chapter.id}$`),
|
|
);
|
|
|
|
const persisted = await getChapterById(request, chapter.id);
|
|
expect(persisted.name).toBe(newName);
|
|
expect(persisted.description).toBe(values.description);
|
|
expect(persisted.gmNotes).toBe(values.gmNotes);
|
|
expect(persisted.playerObjectives).toBe(values.playerObjectives);
|
|
expect(persisted.narrativeStakes).toBe(values.narrativeStakes);
|
|
});
|
|
|
|
test('save button is disabled when name is empty', async ({ page }) => {
|
|
await page.goto(`/campaigns/${campaign.id}/arcs/${arc.id}/chapters/${chapter.id}/edit`);
|
|
const nameField = page.getByLabel(/Titre du chapitre/i);
|
|
const saveBtn = page.getByRole('button', { name: /^Sauvegarder$/i });
|
|
|
|
await expect(saveBtn).toBeEnabled();
|
|
await nameField.fill('');
|
|
await expect(saveBtn).toBeDisabled();
|
|
await nameField.fill('OK');
|
|
await expect(saveBtn).toBeEnabled();
|
|
});
|
|
});
|