Mise en place de tests utilisateurs avec playwright pour la partie angular + corrections au niveau des labels avec for et id pour cliquer dessus
Some checks failed
E2E Tests / e2e (push) Failing after 5m30s
Some checks failed
E2E Tests / e2e (push) Failing after 5m30s
This commit is contained in:
281
web/e2e/fixtures/api.ts
Normal file
281
web/e2e/fixtures/api.ts
Normal file
@@ -0,0 +1,281 @@
|
||||
import { APIRequestContext, expect } from '@playwright/test';
|
||||
|
||||
export interface SeededLore {
|
||||
id: string;
|
||||
name: string;
|
||||
rootFolderId: string;
|
||||
rootFolderName: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Seed un Lore + un dossier racine via l'API backend.
|
||||
* Les noms sont uniques (timestamp + random) pour éviter les collisions en parallèle.
|
||||
*/
|
||||
export async function seedLoreWithFolder(request: APIRequestContext): Promise<SeededLore> {
|
||||
const suffix = `${Date.now()}-${Math.floor(Math.random() * 10000)}`;
|
||||
const loreName = `E2E Lore ${suffix}`;
|
||||
const folderName = `E2E Folder ${suffix}`;
|
||||
|
||||
const loreRes = await request.post('/api/lores', {
|
||||
data: { name: loreName, description: 'Seeded by Playwright' },
|
||||
});
|
||||
expect(loreRes.ok(), `POST /api/lores -> ${loreRes.status()}`).toBeTruthy();
|
||||
const lore = await loreRes.json();
|
||||
|
||||
const folderRes = await request.post('/api/lore-nodes', {
|
||||
data: { loreId: lore.id, name: folderName, icon: 'folder', description: '' },
|
||||
});
|
||||
expect(folderRes.ok(), `POST /api/lore-nodes -> ${folderRes.status()}`).toBeTruthy();
|
||||
const folder = await folderRes.json();
|
||||
|
||||
return { id: lore.id, name: loreName, rootFolderId: folder.id, rootFolderName: folderName };
|
||||
}
|
||||
|
||||
/** Cleanup best-effort — n'échoue pas si déjà supprimé. */
|
||||
export async function deleteLore(request: APIRequestContext, loreId: string): Promise<void> {
|
||||
await request.delete(`/api/lores/${loreId}`).catch(() => undefined);
|
||||
}
|
||||
|
||||
export async function getTemplatesForLore(
|
||||
request: APIRequestContext,
|
||||
loreId: string,
|
||||
): Promise<Array<{ id: string; name: string }>> {
|
||||
const res = await request.get(`/api/templates?loreId=${loreId}`);
|
||||
expect(res.ok(), `GET /api/templates -> ${res.status()}`).toBeTruthy();
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export interface SeededTemplate {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export async function seedTemplate(
|
||||
request: APIRequestContext,
|
||||
opts: { loreId: string; defaultNodeId: string; name?: string; fieldNames?: string[] },
|
||||
): Promise<SeededTemplate> {
|
||||
const templateName = opts.name ?? `E2E Template ${Date.now()}-${Math.floor(Math.random() * 10000)}`;
|
||||
const fields = (opts.fieldNames ?? ['Nom', 'Description']).map((name) => ({ name, type: 'TEXT' }));
|
||||
|
||||
const res = await request.post('/api/templates', {
|
||||
data: {
|
||||
loreId: opts.loreId,
|
||||
name: templateName,
|
||||
description: 'Seeded by Playwright',
|
||||
defaultNodeId: opts.defaultNodeId,
|
||||
fields,
|
||||
},
|
||||
});
|
||||
expect(res.ok(), `POST /api/templates -> ${res.status()}`).toBeTruthy();
|
||||
const tpl = await res.json();
|
||||
return { id: tpl.id, name: templateName };
|
||||
}
|
||||
|
||||
export async function deleteCampaign(request: APIRequestContext, campaignId: string): Promise<void> {
|
||||
await request.delete(`/api/campaigns/${campaignId}`).catch(() => undefined);
|
||||
}
|
||||
|
||||
export interface SeededCampaign {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export async function seedCampaign(
|
||||
request: APIRequestContext,
|
||||
opts: { name?: string; loreId?: string | null; playerCount?: number } = {},
|
||||
): Promise<SeededCampaign> {
|
||||
const name = opts.name ?? `E2E Campaign ${Date.now()}-${Math.floor(Math.random() * 10000)}`;
|
||||
const res = await request.post('/api/campaigns', {
|
||||
data: {
|
||||
name,
|
||||
description: 'Seeded by Playwright',
|
||||
playerCount: opts.playerCount ?? 4,
|
||||
loreId: opts.loreId ?? null,
|
||||
gameSystemId: null,
|
||||
},
|
||||
});
|
||||
expect(res.ok(), `POST /api/campaigns -> ${res.status()}`).toBeTruthy();
|
||||
const c = await res.json();
|
||||
return { id: c.id, name };
|
||||
}
|
||||
|
||||
export interface SeededArc {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export async function seedArc(
|
||||
request: APIRequestContext,
|
||||
opts: { campaignId: string; name?: string; order?: number },
|
||||
): Promise<SeededArc> {
|
||||
const name = opts.name ?? `E2E Arc ${Date.now()}-${Math.floor(Math.random() * 10000)}`;
|
||||
const res = await request.post('/api/arcs', {
|
||||
data: {
|
||||
campaignId: opts.campaignId,
|
||||
name,
|
||||
description: '',
|
||||
order: opts.order ?? 1,
|
||||
},
|
||||
});
|
||||
expect(res.ok(), `POST /api/arcs -> ${res.status()}`).toBeTruthy();
|
||||
const a = await res.json();
|
||||
return { id: a.id, name };
|
||||
}
|
||||
|
||||
export interface SeededChapter {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export async function seedChapter(
|
||||
request: APIRequestContext,
|
||||
opts: { arcId: string; name?: string; order?: number },
|
||||
): Promise<SeededChapter> {
|
||||
const name = opts.name ?? `E2E Chapter ${Date.now()}-${Math.floor(Math.random() * 10000)}`;
|
||||
const res = await request.post('/api/chapters', {
|
||||
data: { arcId: opts.arcId, name, description: '', order: opts.order ?? 1 },
|
||||
});
|
||||
expect(res.ok(), `POST /api/chapters -> ${res.status()}`).toBeTruthy();
|
||||
const c = await res.json();
|
||||
return { id: c.id, name };
|
||||
}
|
||||
|
||||
export async function getChapterById(
|
||||
request: APIRequestContext,
|
||||
chapterId: string,
|
||||
): Promise<{
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
gmNotes?: string | null;
|
||||
playerObjectives?: string | null;
|
||||
narrativeStakes?: string | null;
|
||||
}> {
|
||||
const res = await request.get(`/api/chapters/${chapterId}`);
|
||||
expect(res.ok(), `GET /api/chapters/${chapterId} -> ${res.status()}`).toBeTruthy();
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export interface SeededScene {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export async function seedScene(
|
||||
request: APIRequestContext,
|
||||
opts: { chapterId: string; name?: string; order?: number },
|
||||
): Promise<SeededScene> {
|
||||
const name = opts.name ?? `E2E Scene ${Date.now()}-${Math.floor(Math.random() * 10000)}`;
|
||||
const res = await request.post('/api/scenes', {
|
||||
data: { chapterId: opts.chapterId, name, description: '', order: opts.order ?? 1 },
|
||||
});
|
||||
expect(res.ok(), `POST /api/scenes -> ${res.status()}`).toBeTruthy();
|
||||
const s = await res.json();
|
||||
return { id: s.id, name };
|
||||
}
|
||||
|
||||
export async function getSceneById(
|
||||
request: APIRequestContext,
|
||||
sceneId: string,
|
||||
): Promise<{
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
location?: string | null;
|
||||
timing?: string | null;
|
||||
atmosphere?: string | null;
|
||||
playerNarration?: string | null;
|
||||
gmSecretNotes?: string | null;
|
||||
choicesConsequences?: string | null;
|
||||
combatDifficulty?: string | null;
|
||||
enemies?: string | null;
|
||||
branches?: Array<{ label: string; targetSceneId: string; condition?: string }>;
|
||||
}> {
|
||||
const res = await request.get(`/api/scenes/${sceneId}`);
|
||||
expect(res.ok(), `GET /api/scenes/${sceneId} -> ${res.status()}`).toBeTruthy();
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function getArcById(
|
||||
request: APIRequestContext,
|
||||
arcId: string,
|
||||
): Promise<{
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
themes?: string | null;
|
||||
stakes?: string | null;
|
||||
gmNotes?: string | null;
|
||||
rewards?: string | null;
|
||||
resolution?: string | null;
|
||||
}> {
|
||||
const res = await request.get(`/api/arcs/${arcId}`);
|
||||
expect(res.ok(), `GET /api/arcs/${arcId} -> ${res.status()}`).toBeTruthy();
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function getCampaigns(
|
||||
request: APIRequestContext,
|
||||
): Promise<Array<{ id: string; name: string; loreId: string | null }>> {
|
||||
const res = await request.get('/api/campaigns');
|
||||
expect(res.ok(), `GET /api/campaigns -> ${res.status()}`).toBeTruthy();
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function getPagesForLore(
|
||||
request: APIRequestContext,
|
||||
loreId: string,
|
||||
): Promise<Array<{ id: string; title: string; nodeId: string; templateId: string }>> {
|
||||
const res = await request.get(`/api/pages?loreId=${loreId}`);
|
||||
expect(res.ok(), `GET /api/pages -> ${res.status()}`).toBeTruthy();
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export interface SeededPage {
|
||||
id: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export async function seedPage(
|
||||
request: APIRequestContext,
|
||||
opts: { loreId: string; nodeId: string; templateId: string; title?: string },
|
||||
): Promise<SeededPage> {
|
||||
const title = opts.title ?? `E2E Page ${Date.now()}-${Math.floor(Math.random() * 10000)}`;
|
||||
const res = await request.post('/api/pages', {
|
||||
data: { loreId: opts.loreId, nodeId: opts.nodeId, templateId: opts.templateId, title },
|
||||
});
|
||||
expect(res.ok(), `POST /api/pages -> ${res.status()}`).toBeTruthy();
|
||||
const page = await res.json();
|
||||
return { id: page.id, title };
|
||||
}
|
||||
|
||||
export async function getPageById(
|
||||
request: APIRequestContext,
|
||||
pageId: string,
|
||||
): Promise<{
|
||||
id: string;
|
||||
title: string;
|
||||
nodeId: string;
|
||||
values?: Record<string, string>;
|
||||
tags?: string[];
|
||||
notes?: string;
|
||||
}> {
|
||||
const res = await request.get(`/api/pages/${pageId}`);
|
||||
expect(res.ok(), `GET /api/pages/${pageId} -> ${res.status()}`).toBeTruthy();
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function getTemplateById(
|
||||
request: APIRequestContext,
|
||||
templateId: string,
|
||||
): Promise<{
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
defaultNodeId?: string | null;
|
||||
fields: Array<{ name: string; type: string }>;
|
||||
}> {
|
||||
const res = await request.get(`/api/templates/${templateId}`);
|
||||
expect(res.ok(), `GET /api/templates/${templateId} -> ${res.status()}`).toBeTruthy();
|
||||
return res.json();
|
||||
}
|
||||
76
web/e2e/tests/campaign/arc-edit.spec.ts
Normal file
76
web/e2e/tests/campaign/arc-edit.spec.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import {
|
||||
seedCampaign,
|
||||
seedArc,
|
||||
deleteCampaign,
|
||||
getArcById,
|
||||
type SeededCampaign,
|
||||
type SeededArc,
|
||||
} from '../../fixtures/api';
|
||||
|
||||
test.describe('Arc edit', () => {
|
||||
let campaign: SeededCampaign;
|
||||
let arc: SeededArc;
|
||||
|
||||
test.beforeEach(async ({ request }) => {
|
||||
campaign = await seedCampaign(request);
|
||||
arc = await seedArc(request, { campaignId: campaign.id });
|
||||
});
|
||||
|
||||
test.afterEach(async ({ request }) => {
|
||||
if (campaign?.id) await deleteCampaign(request, campaign.id);
|
||||
});
|
||||
|
||||
test('form is prefilled with the arc name', async ({ page }) => {
|
||||
await page.goto(`/campaigns/${campaign.id}/arcs/${arc.id}/edit`);
|
||||
await expect(page.getByLabel(/Titre de l'arc/i)).toHaveValue(arc.name);
|
||||
});
|
||||
|
||||
test('edits all narrative fields and persists them to API', async ({ page, request }) => {
|
||||
const newName = `${arc.name} renamed`;
|
||||
const values = {
|
||||
description: "Un arc sombre où la trahison s'installe.",
|
||||
themes: 'Trahison, rédemption, dette de sang.',
|
||||
stakes: 'La survie du royaume est en jeu.',
|
||||
gmNotes: 'Révéler le traître en scène 3.',
|
||||
rewards: 'Relique ancienne + alliance avec le clan nordique.',
|
||||
resolution: 'Le héros pardonne au traître ou le tue.',
|
||||
};
|
||||
|
||||
await page.goto(`/campaigns/${campaign.id}/arcs/${arc.id}/edit`);
|
||||
|
||||
await page.getByLabel(/Titre de l'arc/i).fill(newName);
|
||||
await page.getByLabel(/Synopsis de l'arc/i).fill(values.description);
|
||||
await page.getByLabel(/Thèmes principaux/i).fill(values.themes);
|
||||
await page.getByLabel(/Enjeux globaux/i).fill(values.stakes);
|
||||
await page.getByLabel(/Notes et planification du MJ/i).fill(values.gmNotes);
|
||||
await page.getByLabel(/Récompenses et progression/i).fill(values.rewards);
|
||||
await page.getByLabel(/Dénouement prévu/i).fill(values.resolution);
|
||||
|
||||
await page.getByRole('button', { name: /^Sauvegarder$/i }).click();
|
||||
|
||||
await expect(page).toHaveURL(new RegExp(`/campaigns/${campaign.id}/arcs/${arc.id}$`));
|
||||
|
||||
const persisted = await getArcById(request, arc.id);
|
||||
expect(persisted.name).toBe(newName);
|
||||
expect(persisted.description).toBe(values.description);
|
||||
expect(persisted.themes).toBe(values.themes);
|
||||
expect(persisted.stakes).toBe(values.stakes);
|
||||
expect(persisted.gmNotes).toBe(values.gmNotes);
|
||||
expect(persisted.rewards).toBe(values.rewards);
|
||||
expect(persisted.resolution).toBe(values.resolution);
|
||||
});
|
||||
|
||||
test('save button is disabled when name is empty', async ({ page }) => {
|
||||
await page.goto(`/campaigns/${campaign.id}/arcs/${arc.id}/edit`);
|
||||
|
||||
const nameField = page.getByLabel(/Titre de l'arc/i);
|
||||
const saveBtn = page.getByRole('button', { name: /^Sauvegarder$/i });
|
||||
|
||||
await expect(saveBtn).toBeEnabled();
|
||||
await nameField.fill('');
|
||||
await expect(saveBtn).toBeDisabled();
|
||||
await nameField.fill('Valid');
|
||||
await expect(saveBtn).toBeEnabled();
|
||||
});
|
||||
});
|
||||
96
web/e2e/tests/campaign/campaign-create.spec.ts
Normal file
96
web/e2e/tests/campaign/campaign-create.spec.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import {
|
||||
seedLoreWithFolder,
|
||||
deleteLore,
|
||||
deleteCampaign,
|
||||
getCampaigns,
|
||||
type SeededLore,
|
||||
} from '../../fixtures/api';
|
||||
|
||||
test.describe('Campaign creation', () => {
|
||||
const createdCampaignIds: string[] = [];
|
||||
let linkedLore: SeededLore;
|
||||
|
||||
test.beforeEach(async ({ request }) => {
|
||||
linkedLore = await seedLoreWithFolder(request);
|
||||
});
|
||||
|
||||
test.afterEach(async ({ request }) => {
|
||||
while (createdCampaignIds.length) {
|
||||
await deleteCampaign(request, createdCampaignIds.pop()!);
|
||||
}
|
||||
if (linkedLore?.id) await deleteLore(request, linkedLore.id);
|
||||
});
|
||||
|
||||
test('creates a standalone campaign (no lore, no system) and shows it in the grid', async ({
|
||||
page,
|
||||
request,
|
||||
}) => {
|
||||
const campaignName = `Campagne E2E ${Date.now()}`;
|
||||
const description = 'Une campagne créée par les tests automatisés.';
|
||||
|
||||
await page.goto('/campaigns');
|
||||
await expect(page.getByRole('heading', { name: /Vos Campagnes|Campagnes/i })).toBeVisible();
|
||||
|
||||
await page.locator('.campaign-card.card-new').click();
|
||||
|
||||
const modal = page.locator('.modal');
|
||||
await expect(modal).toBeVisible();
|
||||
|
||||
await modal.getByLabel(/Nom de la campagne/i).fill(campaignName);
|
||||
await modal.getByLabel(/Description/i).fill(description);
|
||||
await modal.getByLabel(/Nombre de joueurs/i).fill('5');
|
||||
|
||||
await modal.getByRole('button', { name: /^Créer la campagne$/i }).click();
|
||||
|
||||
await expect(modal).not.toBeVisible();
|
||||
|
||||
const newCard = page.locator('.campaign-card', { hasText: campaignName });
|
||||
await expect(newCard).toBeVisible();
|
||||
|
||||
const campaigns = await getCampaigns(request);
|
||||
const created = campaigns.find((c) => c.name === campaignName);
|
||||
expect(created).toBeDefined();
|
||||
expect(created!.loreId).toBeNull();
|
||||
createdCampaignIds.push(created!.id);
|
||||
});
|
||||
|
||||
test('creates a campaign linked to an existing lore', async ({ page, request }) => {
|
||||
const campaignName = `Campagne liée ${Date.now()}`;
|
||||
|
||||
await page.goto('/campaigns');
|
||||
await page.locator('.campaign-card.card-new').click();
|
||||
|
||||
const modal = page.locator('.modal');
|
||||
await modal.getByLabel(/Nom de la campagne/i).fill(campaignName);
|
||||
await modal.getByLabel(/Univers associé/i).selectOption({ label: linkedLore.name });
|
||||
|
||||
await modal.getByRole('button', { name: /^Créer la campagne$/i }).click();
|
||||
await expect(modal).not.toBeVisible();
|
||||
|
||||
const campaigns = await getCampaigns(request);
|
||||
const created = campaigns.find((c) => c.name === campaignName);
|
||||
expect(created).toBeDefined();
|
||||
expect(created!.loreId).toBe(linkedLore.id);
|
||||
createdCampaignIds.push(created!.id);
|
||||
});
|
||||
|
||||
test('submit is disabled without a name and when player count is invalid', async ({ page }) => {
|
||||
await page.goto('/campaigns');
|
||||
await page.locator('.campaign-card.card-new').click();
|
||||
|
||||
const modal = page.locator('.modal');
|
||||
const submit = modal.getByRole('button', { name: /^Créer la campagne$/i });
|
||||
|
||||
await expect(submit).toBeDisabled();
|
||||
|
||||
await modal.getByLabel(/Nom de la campagne/i).fill('Valid name');
|
||||
await expect(submit).toBeEnabled();
|
||||
|
||||
await modal.getByLabel(/Nombre de joueurs/i).fill('0');
|
||||
await expect(submit).toBeDisabled();
|
||||
|
||||
await modal.getByLabel(/Nombre de joueurs/i).fill('3');
|
||||
await expect(submit).toBeEnabled();
|
||||
});
|
||||
});
|
||||
72
web/e2e/tests/campaign/chapter-edit.spec.ts
Normal file
72
web/e2e/tests/campaign/chapter-edit.spec.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
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);
|
||||
await page.getByLabel(/Notes du Maître de Jeu/i).fill(values.gmNotes);
|
||||
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();
|
||||
});
|
||||
});
|
||||
147
web/e2e/tests/campaign/scene-edit.spec.ts
Normal file
147
web/e2e/tests/campaign/scene-edit.spec.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import {
|
||||
seedCampaign,
|
||||
seedArc,
|
||||
seedChapter,
|
||||
seedScene,
|
||||
deleteCampaign,
|
||||
getSceneById,
|
||||
type SeededCampaign,
|
||||
type SeededArc,
|
||||
type SeededChapter,
|
||||
type SeededScene,
|
||||
} from '../../fixtures/api';
|
||||
|
||||
test.describe('Scene edit', () => {
|
||||
let campaign: SeededCampaign;
|
||||
let arc: SeededArc;
|
||||
let chapter: SeededChapter;
|
||||
let scene: SeededScene;
|
||||
|
||||
test.beforeEach(async ({ request }) => {
|
||||
campaign = await seedCampaign(request);
|
||||
arc = await seedArc(request, { campaignId: campaign.id });
|
||||
chapter = await seedChapter(request, { arcId: arc.id });
|
||||
scene = await seedScene(request, { chapterId: chapter.id });
|
||||
});
|
||||
|
||||
test.afterEach(async ({ request }) => {
|
||||
if (campaign?.id) await deleteCampaign(request, campaign.id);
|
||||
});
|
||||
|
||||
test('edits all text fields across sections and persists them to API', async ({ page, request }) => {
|
||||
const newName = `${scene.name} renamed`;
|
||||
const values = {
|
||||
description: "Les PJ arrivent au village à la nuit tombée.",
|
||||
location: "Taverne du Dragon d'Or",
|
||||
timing: 'Soir, pleine lune',
|
||||
atmosphere: 'Silence pesant, regards fuyants des villageois.',
|
||||
playerNarration: 'Vous poussez la porte de la taverne…',
|
||||
gmSecretNotes: 'Le tavernier est complice des bandits.',
|
||||
choicesConsequences: 'Accepter = piégés à l\'étage. Refuser = filature.',
|
||||
combatDifficulty: 'Moyenne, 4 bandits niveau 3',
|
||||
enemies: 'Bandit chef (feuille jointe) + 3 sbires.',
|
||||
};
|
||||
|
||||
await page.goto(
|
||||
`/campaigns/${campaign.id}/arcs/${arc.id}/chapters/${chapter.id}/scenes/${scene.id}/edit`,
|
||||
);
|
||||
|
||||
await expect(page.getByLabel(/Titre de la scène/i)).toHaveValue(scene.name);
|
||||
|
||||
await page.getByLabel(/Titre de la scène/i).fill(newName);
|
||||
await page.getByLabel(/Description courte/i).fill(values.description);
|
||||
await page.getByLabel(/^Lieu$/i).fill(values.location);
|
||||
await page.getByLabel(/^Moment$/i).fill(values.timing);
|
||||
await page.getByLabel(/Ambiance et atmosphère/i).fill(values.atmosphere);
|
||||
|
||||
// Les sections suivantes sont fermées par défaut : on les ouvre avant de taper.
|
||||
// Un clic sur le header de la section toggle son état.
|
||||
await page.locator('app-expandable-section', { hasText: 'Narration pour les joueurs' }).click();
|
||||
await page.getByPlaceholder(/Le texte que vous lirez aux joueurs/i).fill(values.playerNarration);
|
||||
|
||||
await page.locator('app-expandable-section', { hasText: 'Notes et secrets du MJ' }).click();
|
||||
await page
|
||||
.getByPlaceholder(/Informations cachées, indices, éléments secrets/i)
|
||||
.fill(values.gmSecretNotes);
|
||||
|
||||
await page.locator('app-expandable-section', { hasText: 'Choix et conséquences' }).click();
|
||||
await page
|
||||
.getByPlaceholder(/Décrivez les différentes options/i)
|
||||
.fill(values.choicesConsequences);
|
||||
|
||||
await page.locator('app-expandable-section', { hasText: 'Combat ou rencontre' }).click();
|
||||
await page.getByLabel(/Difficulté estimée/i).fill(values.combatDifficulty);
|
||||
await page.getByLabel(/Ennemis et créatures/i).fill(values.enemies);
|
||||
|
||||
await page.getByRole('button', { name: /^Sauvegarder$/i }).click();
|
||||
|
||||
await expect(page).toHaveURL(
|
||||
new RegExp(
|
||||
`/campaigns/${campaign.id}/arcs/${arc.id}/chapters/${chapter.id}/scenes/${scene.id}$`,
|
||||
),
|
||||
);
|
||||
|
||||
const persisted = await getSceneById(request, scene.id);
|
||||
expect(persisted.name).toBe(newName);
|
||||
expect(persisted.description).toBe(values.description);
|
||||
expect(persisted.location).toBe(values.location);
|
||||
expect(persisted.timing).toBe(values.timing);
|
||||
expect(persisted.atmosphere).toBe(values.atmosphere);
|
||||
expect(persisted.playerNarration).toBe(values.playerNarration);
|
||||
expect(persisted.gmSecretNotes).toBe(values.gmSecretNotes);
|
||||
expect(persisted.choicesConsequences).toBe(values.choicesConsequences);
|
||||
expect(persisted.combatDifficulty).toBe(values.combatDifficulty);
|
||||
expect(persisted.enemies).toBe(values.enemies);
|
||||
});
|
||||
|
||||
test('adds a narrative branch to a sibling scene and persists it', async ({ page, request }) => {
|
||||
const sibling = await seedScene(request, {
|
||||
chapterId: chapter.id,
|
||||
name: 'Scène alternative',
|
||||
order: 2,
|
||||
});
|
||||
|
||||
await page.goto(
|
||||
`/campaigns/${campaign.id}/arcs/${arc.id}/chapters/${chapter.id}/scenes/${scene.id}/edit`,
|
||||
);
|
||||
|
||||
const branchesSection = page.locator('app-expandable-section', { hasText: 'Branches narratives' });
|
||||
await branchesSection.click();
|
||||
|
||||
await branchesSection.getByRole('button', { name: /Ajouter une branche/i }).click();
|
||||
|
||||
const branchItem = branchesSection.locator('.branch-item').first();
|
||||
await branchItem.getByPlaceholder(/Ex: Si les joueurs attaquent/i).fill('Si les PJ fuient');
|
||||
await branchItem.locator('select').selectOption({ label: sibling.name });
|
||||
await branchItem.getByPlaceholder(/Jet de Persuasion/i).fill('Sur échec initiative');
|
||||
|
||||
await page.getByRole('button', { name: /^Sauvegarder$/i }).click();
|
||||
|
||||
await expect(page).toHaveURL(
|
||||
new RegExp(
|
||||
`/campaigns/${campaign.id}/arcs/${arc.id}/chapters/${chapter.id}/scenes/${scene.id}$`,
|
||||
),
|
||||
);
|
||||
|
||||
const persisted = await getSceneById(request, scene.id);
|
||||
expect(persisted.branches).toHaveLength(1);
|
||||
expect(persisted.branches![0].label).toBe('Si les PJ fuient');
|
||||
expect(persisted.branches![0].targetSceneId).toBe(sibling.id);
|
||||
expect(persisted.branches![0].condition).toBe('Sur échec initiative');
|
||||
});
|
||||
|
||||
test('save button is disabled when name is empty', async ({ page }) => {
|
||||
await page.goto(
|
||||
`/campaigns/${campaign.id}/arcs/${arc.id}/chapters/${chapter.id}/scenes/${scene.id}/edit`,
|
||||
);
|
||||
const nameField = page.getByLabel(/Titre de la scène/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();
|
||||
});
|
||||
});
|
||||
77
web/e2e/tests/lore/lore-create.spec.ts
Normal file
77
web/e2e/tests/lore/lore-create.spec.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { deleteLore } from '../../fixtures/api';
|
||||
|
||||
test.describe('Lore creation', () => {
|
||||
const createdLoreIds: string[] = [];
|
||||
|
||||
test.afterEach(async ({ request }) => {
|
||||
while (createdLoreIds.length) {
|
||||
const id = createdLoreIds.pop()!;
|
||||
await deleteLore(request, id);
|
||||
}
|
||||
});
|
||||
|
||||
test('opens the modal, creates a lore, and shows it in the grid', async ({ page, request }) => {
|
||||
const loreName = `Univers E2E ${Date.now()}`;
|
||||
const description = "Un univers créé par les tests automatisés.";
|
||||
|
||||
await page.goto('/lore');
|
||||
|
||||
await expect(page.getByRole('heading', { name: /Vos Univers/i })).toBeVisible();
|
||||
|
||||
await page.locator('.lore-card.card-new').click();
|
||||
|
||||
const modal = page.locator('.modal');
|
||||
await expect(modal).toBeVisible();
|
||||
await expect(modal.getByRole('heading', { name: /Créer un nouveau Lore/i })).toBeVisible();
|
||||
|
||||
await modal.getByLabel(/Nom de l'univers/i).fill(loreName);
|
||||
await modal.getByLabel('Description').fill(description);
|
||||
|
||||
await modal.getByRole('button', { name: /^Créer le lore$/i }).click();
|
||||
|
||||
await expect(modal).not.toBeVisible();
|
||||
|
||||
const newCard = page.locator('.lore-card', { hasText: loreName });
|
||||
await expect(newCard).toBeVisible();
|
||||
await expect(newCard).toContainText(description);
|
||||
|
||||
const allLores = await request.get('/api/lores').then((r) => r.json());
|
||||
const created = allLores.find((l: { name: string; id: string }) => l.name === loreName);
|
||||
expect(created).toBeDefined();
|
||||
createdLoreIds.push(created.id);
|
||||
});
|
||||
|
||||
test('submit button is disabled when name is empty', async ({ page }) => {
|
||||
await page.goto('/lore');
|
||||
await page.locator('.lore-card.card-new').click();
|
||||
|
||||
const modal = page.locator('.modal');
|
||||
const submit = modal.getByRole('button', { name: /^Créer le lore$/i });
|
||||
|
||||
await expect(submit).toBeDisabled();
|
||||
|
||||
await modal.getByLabel(/Nom de l'univers/i).fill('Quelque chose');
|
||||
await expect(submit).toBeEnabled();
|
||||
|
||||
await modal.getByLabel(/Nom de l'univers/i).fill('');
|
||||
await expect(submit).toBeDisabled();
|
||||
});
|
||||
|
||||
test('clicking the backdrop closes the modal without creating', async ({ page }) => {
|
||||
const typedButAbandoned = `Univers abandonné ${Date.now()}`;
|
||||
|
||||
await page.goto('/lore');
|
||||
await page.locator('.lore-card.card-new').click();
|
||||
|
||||
const modal = page.locator('.modal');
|
||||
await expect(modal).toBeVisible();
|
||||
|
||||
await modal.getByLabel(/Nom de l'univers/i).fill(typedButAbandoned);
|
||||
|
||||
await page.locator('.modal-backdrop').click({ position: { x: 5, y: 5 } });
|
||||
await expect(modal).not.toBeVisible();
|
||||
|
||||
await expect(page.locator('.lore-card', { hasText: typedButAbandoned })).toHaveCount(0);
|
||||
});
|
||||
});
|
||||
90
web/e2e/tests/lore/page-create.spec.ts
Normal file
90
web/e2e/tests/lore/page-create.spec.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import {
|
||||
seedLoreWithFolder,
|
||||
seedTemplate,
|
||||
deleteLore,
|
||||
getPagesForLore,
|
||||
type SeededLore,
|
||||
type SeededTemplate,
|
||||
} from '../../fixtures/api';
|
||||
|
||||
test.describe('Page creation', () => {
|
||||
let seeded: SeededLore;
|
||||
let template: SeededTemplate;
|
||||
|
||||
test.beforeEach(async ({ request }) => {
|
||||
seeded = await seedLoreWithFolder(request);
|
||||
template = await seedTemplate(request, {
|
||||
loreId: seeded.id,
|
||||
defaultNodeId: seeded.rootFolderId,
|
||||
fieldNames: ['Apparence', 'Motivation'],
|
||||
});
|
||||
});
|
||||
|
||||
test.afterEach(async ({ request }) => {
|
||||
if (seeded?.id) await deleteLore(request, seeded.id);
|
||||
});
|
||||
|
||||
test('creates an empty page from a template and redirects to edit', async ({ page, request }) => {
|
||||
const pageTitle = `Maître Eldrin ${Date.now()}`;
|
||||
|
||||
await page.goto(`/lore/${seeded.id}/pages/create`);
|
||||
|
||||
await expect(page.getByRole('heading', { name: /Créer une nouvelle Page/i })).toBeVisible();
|
||||
|
||||
await page.getByLabel(/Titre de la page/i).fill(pageTitle);
|
||||
|
||||
await page.locator('.template-card', { hasText: template.name }).click();
|
||||
await expect(page.locator('.template-card.selected', { hasText: template.name })).toBeVisible();
|
||||
|
||||
await expect(page.locator('#page-node')).toHaveValue(seeded.rootFolderId);
|
||||
|
||||
await page.getByRole('button', { name: /^Créer la page$/i }).click();
|
||||
|
||||
await expect(page).toHaveURL(new RegExp(`/lore/${seeded.id}/pages/[^/]+/edit$`));
|
||||
|
||||
const pages = await getPagesForLore(request, seeded.id);
|
||||
const created = pages.find((p) => p.title === pageTitle);
|
||||
expect(created).toBeDefined();
|
||||
expect(created?.templateId).toBe(template.id);
|
||||
expect(created?.nodeId).toBe(seeded.rootFolderId);
|
||||
});
|
||||
|
||||
test('submit is disabled until title, template and folder are set', async ({ page }) => {
|
||||
await page.goto(`/lore/${seeded.id}/pages/create`);
|
||||
|
||||
const submit = page.getByRole('button', { name: /^Créer la page$/i });
|
||||
await expect(submit).toBeDisabled();
|
||||
|
||||
await page.getByLabel(/Titre de la page/i).fill('Un titre');
|
||||
await expect(submit).toBeDisabled();
|
||||
|
||||
await page.locator('.template-card', { hasText: template.name }).click();
|
||||
await expect(submit).toBeEnabled();
|
||||
});
|
||||
|
||||
test('entering on a folder-scoped route preselects that folder', async ({ page, request }) => {
|
||||
const pageTitle = `Page scoped ${Date.now()}`;
|
||||
|
||||
const secondFolderRes = await request.post('/api/lore-nodes', {
|
||||
data: { loreId: seeded.id, name: 'Autre dossier', icon: 'folder', description: '' },
|
||||
});
|
||||
expect(secondFolderRes.ok()).toBeTruthy();
|
||||
const secondFolderId = (await secondFolderRes.json()).id;
|
||||
|
||||
await page.goto(`/lore/${seeded.id}/nodes/${secondFolderId}/pages/create`);
|
||||
|
||||
const nodeSelect = page.locator('#page-node');
|
||||
await expect(nodeSelect).toHaveValue(secondFolderId);
|
||||
await expect(nodeSelect).toBeDisabled();
|
||||
|
||||
await page.getByLabel(/Titre de la page/i).fill(pageTitle);
|
||||
await page.locator('.template-card', { hasText: template.name }).click();
|
||||
await page.getByRole('button', { name: /^Créer la page$/i }).click();
|
||||
|
||||
await expect(page).toHaveURL(new RegExp(`/lore/${seeded.id}/pages/[^/]+/edit$`));
|
||||
|
||||
const pages = await getPagesForLore(request, seeded.id);
|
||||
expect(pages.find((p) => p.title === pageTitle)?.nodeId).toBe(secondFolderId);
|
||||
});
|
||||
});
|
||||
95
web/e2e/tests/lore/page-edit.spec.ts
Normal file
95
web/e2e/tests/lore/page-edit.spec.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import {
|
||||
seedLoreWithFolder,
|
||||
seedTemplate,
|
||||
seedPage,
|
||||
deleteLore,
|
||||
getPageById,
|
||||
type SeededLore,
|
||||
type SeededTemplate,
|
||||
type SeededPage,
|
||||
} from '../../fixtures/api';
|
||||
|
||||
test.describe('Page edit', () => {
|
||||
let seeded: SeededLore;
|
||||
let template: SeededTemplate;
|
||||
let pageEntity: SeededPage;
|
||||
|
||||
test.beforeEach(async ({ request }) => {
|
||||
seeded = await seedLoreWithFolder(request);
|
||||
template = await seedTemplate(request, {
|
||||
loreId: seeded.id,
|
||||
defaultNodeId: seeded.rootFolderId,
|
||||
fieldNames: ['Apparence', 'Motivation'],
|
||||
});
|
||||
pageEntity = await seedPage(request, {
|
||||
loreId: seeded.id,
|
||||
nodeId: seeded.rootFolderId,
|
||||
templateId: template.id,
|
||||
});
|
||||
});
|
||||
|
||||
test.afterEach(async ({ request }) => {
|
||||
if (seeded?.id) await deleteLore(request, seeded.id);
|
||||
});
|
||||
|
||||
test('edits title, dynamic fields, notes and persists to API', async ({ page, request }) => {
|
||||
const newTitle = `Maître Eldrin ${Date.now()}`;
|
||||
const apparence = 'Vieil homme au regard perçant.';
|
||||
const motivation = 'Protéger la cité à tout prix.';
|
||||
const notes = 'MJ : il connaît le secret du roi.';
|
||||
|
||||
await page.goto(`/lore/${seeded.id}/pages/${pageEntity.id}/edit`);
|
||||
|
||||
await expect(page.locator('input[name="title"]')).toHaveValue(pageEntity.title);
|
||||
|
||||
await page.locator('input[name="title"]').fill(newTitle);
|
||||
await page.getByPlaceholder('Valeur pour Apparence...').fill(apparence);
|
||||
await page.getByPlaceholder('Valeur pour Motivation...').fill(motivation);
|
||||
await page.locator('textarea[name="notes"]').fill(notes);
|
||||
|
||||
await page.getByRole('button', { name: /^Sauvegarder$/i }).click();
|
||||
|
||||
await expect(page).toHaveURL(new RegExp(`/lore/${seeded.id}/pages/${pageEntity.id}$`));
|
||||
|
||||
const persisted = await getPageById(request, pageEntity.id);
|
||||
expect(persisted.title).toBe(newTitle);
|
||||
expect(persisted.values?.['Apparence']).toBe(apparence);
|
||||
expect(persisted.values?.['Motivation']).toBe(motivation);
|
||||
expect(persisted.notes).toBe(notes);
|
||||
});
|
||||
|
||||
test('adds a tag via chips input and persists it', async ({ page, request }) => {
|
||||
await page.goto(`/lore/${seeded.id}/pages/${pageEntity.id}/edit`);
|
||||
|
||||
const chipsInput = page.locator('app-chips-input input');
|
||||
await chipsInput.fill('dangereux');
|
||||
await chipsInput.press('Enter');
|
||||
await chipsInput.fill('ancien');
|
||||
await chipsInput.press('Enter');
|
||||
|
||||
await expect(page.locator('app-chips-input').getByText('dangereux')).toBeVisible();
|
||||
await expect(page.locator('app-chips-input').getByText('ancien')).toBeVisible();
|
||||
|
||||
await page.getByRole('button', { name: /^Sauvegarder$/i }).click();
|
||||
await expect(page).toHaveURL(new RegExp(`/lore/${seeded.id}/pages/${pageEntity.id}$`));
|
||||
|
||||
const persisted = await getPageById(request, pageEntity.id);
|
||||
expect(persisted.tags).toEqual(expect.arrayContaining(['dangereux', 'ancien']));
|
||||
});
|
||||
|
||||
test('save button is disabled when title is empty', async ({ page }) => {
|
||||
await page.goto(`/lore/${seeded.id}/pages/${pageEntity.id}/edit`);
|
||||
|
||||
const titleInput = page.locator('input[name="title"]');
|
||||
const saveBtn = page.getByRole('button', { name: /^Sauvegarder$/i });
|
||||
|
||||
await expect(saveBtn).toBeEnabled();
|
||||
await titleInput.fill('');
|
||||
await expect(saveBtn).toBeDisabled();
|
||||
await titleInput.fill(' ');
|
||||
await expect(saveBtn).toBeDisabled();
|
||||
await titleInput.fill('OK');
|
||||
await expect(saveBtn).toBeEnabled();
|
||||
});
|
||||
});
|
||||
72
web/e2e/tests/lore/template-create.spec.ts
Normal file
72
web/e2e/tests/lore/template-create.spec.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { seedLoreWithFolder, deleteLore, getTemplatesForLore, type SeededLore } from '../../fixtures/api';
|
||||
|
||||
test.describe('Template creation', () => {
|
||||
let seeded: SeededLore;
|
||||
|
||||
test.beforeEach(async ({ request }) => {
|
||||
seeded = await seedLoreWithFolder(request);
|
||||
});
|
||||
|
||||
test.afterEach(async ({ request }) => {
|
||||
if (seeded?.id) await deleteLore(request, seeded.id);
|
||||
});
|
||||
|
||||
test('creates a template with default fields', async ({ page, request }) => {
|
||||
const templateName = `Auberge ${Date.now()}`;
|
||||
|
||||
await page.goto(`/lore/${seeded.id}/templates/create`);
|
||||
|
||||
await expect(page.getByRole('heading', { name: /Créer un nouveau Template/i })).toBeVisible();
|
||||
|
||||
await page.getByLabel(/Nom du template/i).fill(templateName);
|
||||
await page.getByLabel('Description').fill('Template créé via E2E');
|
||||
await page.getByLabel(/Dossier par défaut/i).selectOption({ label: seeded.rootFolderName });
|
||||
|
||||
await page.getByRole('button', { name: /^Créer le template$/i }).click();
|
||||
|
||||
await expect(page).toHaveURL(new RegExp(`/lore/${seeded.id}$`));
|
||||
|
||||
const templates = await getTemplatesForLore(request, seeded.id);
|
||||
expect(templates.map((t) => t.name)).toContain(templateName);
|
||||
});
|
||||
|
||||
test('submit is disabled when name is empty', async ({ page }) => {
|
||||
await page.goto(`/lore/${seeded.id}/templates/create`);
|
||||
|
||||
const submit = page.getByRole('button', { name: /^Créer le template$/i });
|
||||
await expect(submit).toBeDisabled();
|
||||
|
||||
await page.getByLabel(/Nom du template/i).fill('Valid name');
|
||||
await page.getByLabel(/Dossier par défaut/i).selectOption({ label: seeded.rootFolderName });
|
||||
await expect(submit).toBeEnabled();
|
||||
});
|
||||
|
||||
test('can add and remove a custom field before creating', async ({ page, request }) => {
|
||||
const templateName = `Artefact ${Date.now()}`;
|
||||
|
||||
await page.goto(`/lore/${seeded.id}/templates/create`);
|
||||
|
||||
await page.getByLabel(/Nom du template/i).fill(templateName);
|
||||
await page.getByLabel(/Dossier par défaut/i).selectOption({ label: seeded.rootFolderName });
|
||||
|
||||
const addFieldInput = page.getByPlaceholder('+ Ajouter un champ');
|
||||
await addFieldInput.fill('Pouvoir');
|
||||
await addFieldInput.press('Enter');
|
||||
await expect(page.locator('.fields-list .field-chip', { hasText: 'Pouvoir' })).toBeVisible();
|
||||
|
||||
await addFieldInput.fill('Origine');
|
||||
await addFieldInput.press('Enter');
|
||||
await expect(page.locator('.fields-list .field-chip', { hasText: 'Origine' })).toBeVisible();
|
||||
|
||||
const origineRow = page.locator('.fields-list .field-row', { hasText: 'Origine' });
|
||||
await origineRow.getByRole('button', { name: 'Supprimer' }).click();
|
||||
await expect(page.locator('.fields-list .field-chip', { hasText: 'Origine' })).toHaveCount(0);
|
||||
|
||||
await page.getByRole('button', { name: /^Créer le template$/i }).click();
|
||||
await expect(page).toHaveURL(new RegExp(`/lore/${seeded.id}$`));
|
||||
|
||||
const templates = await getTemplatesForLore(request, seeded.id);
|
||||
expect(templates.find((t) => t.name === templateName)).toBeDefined();
|
||||
});
|
||||
});
|
||||
74
web/e2e/tests/lore/template-edit.spec.ts
Normal file
74
web/e2e/tests/lore/template-edit.spec.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import {
|
||||
seedLoreWithFolder,
|
||||
seedTemplate,
|
||||
deleteLore,
|
||||
getTemplateById,
|
||||
type SeededLore,
|
||||
type SeededTemplate,
|
||||
} from '../../fixtures/api';
|
||||
|
||||
test.describe('Template edit', () => {
|
||||
let seeded: SeededLore;
|
||||
let template: SeededTemplate;
|
||||
|
||||
test.beforeEach(async ({ request }) => {
|
||||
seeded = await seedLoreWithFolder(request);
|
||||
template = await seedTemplate(request, {
|
||||
loreId: seeded.id,
|
||||
defaultNodeId: seeded.rootFolderId,
|
||||
fieldNames: ['Nom', 'Description'],
|
||||
});
|
||||
});
|
||||
|
||||
test.afterEach(async ({ request }) => {
|
||||
if (seeded?.id) await deleteLore(request, seeded.id);
|
||||
});
|
||||
|
||||
test('form is prefilled with the current template data', async ({ page }) => {
|
||||
await page.goto(`/lore/${seeded.id}/templates/${template.id}`);
|
||||
|
||||
await expect(page.getByLabel(/^Nom$/)).toHaveValue(template.name);
|
||||
await expect(page.getByLabel(/Dossier par défaut/i)).toHaveValue(seeded.rootFolderId);
|
||||
await expect(page.locator('.fields-list .field-chip', { hasText: 'Nom' })).toBeVisible();
|
||||
await expect(page.locator('.fields-list .field-chip', { hasText: 'Description' })).toBeVisible();
|
||||
});
|
||||
|
||||
test('renames the template and persists to API', async ({ page, request }) => {
|
||||
const newName = `${template.name} renamed`;
|
||||
|
||||
await page.goto(`/lore/${seeded.id}/templates/${template.id}`);
|
||||
|
||||
const nameInput = page.getByLabel(/^Nom$/);
|
||||
await nameInput.fill(newName);
|
||||
await page.getByLabel(/Description/i).fill('Description mise à jour');
|
||||
|
||||
await page.getByRole('button', { name: /^Sauvegarder$/i }).click();
|
||||
|
||||
await expect(page).toHaveURL(new RegExp(`/lore/${seeded.id}$`));
|
||||
|
||||
const persisted = await getTemplateById(request, template.id);
|
||||
expect(persisted.name).toBe(newName);
|
||||
expect(persisted.description).toBe('Description mise à jour');
|
||||
});
|
||||
|
||||
test('adds a new field, removes an existing one, and persists the order', async ({ page, request }) => {
|
||||
await page.goto(`/lore/${seeded.id}/templates/${template.id}`);
|
||||
|
||||
const addInput = page.getByPlaceholder('+ Ajouter un champ');
|
||||
await addInput.fill('Stats');
|
||||
await addInput.press('Enter');
|
||||
await expect(page.locator('.fields-list .field-chip', { hasText: 'Stats' })).toBeVisible();
|
||||
|
||||
const descriptionRow = page.locator('.fields-list .field-row', { hasText: 'Description' });
|
||||
await descriptionRow.getByRole('button', { name: 'Supprimer' }).click();
|
||||
await expect(page.locator('.fields-list .field-chip', { hasText: 'Description' })).toHaveCount(0);
|
||||
|
||||
await page.getByRole('button', { name: /^Sauvegarder$/i }).click();
|
||||
await expect(page).toHaveURL(new RegExp(`/lore/${seeded.id}$`));
|
||||
|
||||
const persisted = await getTemplateById(request, template.id);
|
||||
const names = persisted.fields.map((f) => f.name);
|
||||
expect(names).toEqual(['Nom', 'Stats']);
|
||||
});
|
||||
});
|
||||
15
web/e2e/tests/smoke.spec.ts
Normal file
15
web/e2e/tests/smoke.spec.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('Smoke', () => {
|
||||
test('app loads without uncaught errors', async ({ page }) => {
|
||||
const errors: string[] = [];
|
||||
page.on('pageerror', (err) => errors.push(err.message));
|
||||
|
||||
await page.goto('/');
|
||||
|
||||
await expect(page.locator('app-sidebar')).toBeVisible();
|
||||
await expect(page.locator('main.main-content')).toBeAttached();
|
||||
|
||||
expect(errors, `Page errors:\n${errors.join('\n')}`).toEqual([]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user