Fix workflow gitea action pour e2e (tests automatisés via playwright) + correction d'une incohérence dans l'API coté java. Ajout d'autres tests utilisateur
Some checks failed
E2E Tests / e2e (push) Failing after 2m31s
Some checks failed
E2E Tests / e2e (push) Failing after 2m31s
This commit is contained in:
@@ -36,6 +36,42 @@ export async function deleteLore(request: APIRequestContext, loreId: string): Pr
|
||||
await request.delete(`/api/lores/${loreId}`).catch(() => undefined);
|
||||
}
|
||||
|
||||
export async function getLoreById(
|
||||
request: APIRequestContext,
|
||||
loreId: string,
|
||||
): Promise<{ id: string; name: string; description: string }> {
|
||||
const res = await request.get(`/api/lores/${loreId}`);
|
||||
expect(res.ok(), `GET /api/lores/${loreId} -> ${res.status()}`).toBeTruthy();
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function getArcsForCampaign(
|
||||
request: APIRequestContext,
|
||||
campaignId: string,
|
||||
): Promise<Array<{ id: string; name: string; campaignId: string }>> {
|
||||
const res = await request.get(`/api/arcs?campaignId=${campaignId}`);
|
||||
expect(res.ok(), `GET /api/arcs -> ${res.status()}`).toBeTruthy();
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function getChaptersForArc(
|
||||
request: APIRequestContext,
|
||||
arcId: string,
|
||||
): Promise<Array<{ id: string; name: string; arcId: string }>> {
|
||||
const res = await request.get(`/api/chapters?arcId=${arcId}`);
|
||||
expect(res.ok(), `GET /api/chapters -> ${res.status()}`).toBeTruthy();
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function getScenesForChapter(
|
||||
request: APIRequestContext,
|
||||
chapterId: string,
|
||||
): Promise<Array<{ id: string; name: string; chapterId: string }>> {
|
||||
const res = await request.get(`/api/scenes?chapterId=${chapterId}`);
|
||||
expect(res.ok(), `GET /api/scenes -> ${res.status()}`).toBeTruthy();
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function getTemplatesForLore(
|
||||
request: APIRequestContext,
|
||||
loreId: string,
|
||||
|
||||
50
web/e2e/tests/campaign/arc-create.spec.ts
Normal file
50
web/e2e/tests/campaign/arc-create.spec.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import {
|
||||
seedCampaign,
|
||||
deleteCampaign,
|
||||
getArcById,
|
||||
type SeededCampaign,
|
||||
} from '../../fixtures/api';
|
||||
|
||||
test.describe('Arc creation', () => {
|
||||
let campaign: SeededCampaign;
|
||||
|
||||
test.beforeEach(async ({ request }) => {
|
||||
campaign = await seedCampaign(request);
|
||||
});
|
||||
|
||||
test.afterEach(async ({ request }) => {
|
||||
if (campaign?.id) await deleteCampaign(request, campaign.id);
|
||||
});
|
||||
|
||||
test('creates an arc and redirects to its view', async ({ page, request }) => {
|
||||
const arcName = `Arc ${Date.now()}`;
|
||||
const description = 'Synopsis test';
|
||||
|
||||
await page.goto(`/campaigns/${campaign.id}/arcs/create`);
|
||||
await expect(page.getByRole('heading', { name: /Créer un nouvel arc/i })).toBeVisible();
|
||||
|
||||
await page.getByLabel(/Nom de l'arc/i).fill(arcName);
|
||||
await page.getByLabel(/Description/i).fill(description);
|
||||
|
||||
await page.getByRole('button', { name: /^Créer l'arc$/i }).click();
|
||||
|
||||
await expect(page).toHaveURL(new RegExp(`/campaigns/${campaign.id}/arcs/\\d+$`));
|
||||
|
||||
const createdId = page.url().match(/\/arcs\/(\d+)$/)?.[1];
|
||||
expect(createdId).toBeTruthy();
|
||||
const persisted = await getArcById(request, createdId!);
|
||||
expect(persisted.name).toBe(arcName);
|
||||
expect(persisted.description).toBe(description);
|
||||
});
|
||||
|
||||
test('submit is disabled when name is empty', async ({ page }) => {
|
||||
await page.goto(`/campaigns/${campaign.id}/arcs/create`);
|
||||
|
||||
const submit = page.getByRole('button', { name: /^Créer l'arc$/i });
|
||||
await expect(submit).toBeDisabled();
|
||||
|
||||
await page.getByLabel(/Nom de l'arc/i).fill('Quelque chose');
|
||||
await expect(submit).toBeEnabled();
|
||||
});
|
||||
});
|
||||
54
web/e2e/tests/campaign/chapter-create.spec.ts
Normal file
54
web/e2e/tests/campaign/chapter-create.spec.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import {
|
||||
seedCampaign,
|
||||
seedArc,
|
||||
deleteCampaign,
|
||||
getChapterById,
|
||||
type SeededCampaign,
|
||||
type SeededArc,
|
||||
} from '../../fixtures/api';
|
||||
|
||||
test.describe('Chapter creation', () => {
|
||||
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('creates a chapter and redirects to its view', async ({ page, request }) => {
|
||||
const chapterName = `Chapitre ${Date.now()}`;
|
||||
|
||||
await page.goto(`/campaigns/${campaign.id}/arcs/${arc.id}/chapters/create`);
|
||||
await expect(page.getByRole('heading', { name: /Créer un nouveau chapitre/i })).toBeVisible();
|
||||
|
||||
await page.getByLabel(/Nom du chapitre/i).fill(chapterName);
|
||||
await page.getByLabel(/Description/i).fill('Synopsis du chapitre');
|
||||
|
||||
await page.getByRole('button', { name: /^Créer le chapitre$/i }).click();
|
||||
|
||||
await expect(page).toHaveURL(
|
||||
new RegExp(`/campaigns/${campaign.id}/arcs/${arc.id}/chapters/\\d+$`),
|
||||
);
|
||||
|
||||
const createdId = page.url().match(/\/chapters\/(\d+)$/)?.[1];
|
||||
expect(createdId).toBeTruthy();
|
||||
const persisted = await getChapterById(request, createdId!);
|
||||
expect(persisted.name).toBe(chapterName);
|
||||
});
|
||||
|
||||
test('submit is disabled when name is empty', async ({ page }) => {
|
||||
await page.goto(`/campaigns/${campaign.id}/arcs/${arc.id}/chapters/create`);
|
||||
|
||||
const submit = page.getByRole('button', { name: /^Créer le chapitre$/i });
|
||||
await expect(submit).toBeDisabled();
|
||||
|
||||
await page.getByLabel(/Nom du chapitre/i).fill('OK');
|
||||
await expect(submit).toBeEnabled();
|
||||
});
|
||||
});
|
||||
64
web/e2e/tests/campaign/scene-create.spec.ts
Normal file
64
web/e2e/tests/campaign/scene-create.spec.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import {
|
||||
seedCampaign,
|
||||
seedArc,
|
||||
seedChapter,
|
||||
deleteCampaign,
|
||||
getSceneById,
|
||||
type SeededCampaign,
|
||||
type SeededArc,
|
||||
type SeededChapter,
|
||||
} from '../../fixtures/api';
|
||||
|
||||
test.describe('Scene creation', () => {
|
||||
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('creates a scene and redirects to its view', async ({ page, request }) => {
|
||||
const sceneName = `Scène ${Date.now()}`;
|
||||
|
||||
await page.goto(
|
||||
`/campaigns/${campaign.id}/arcs/${arc.id}/chapters/${chapter.id}/scenes/create`,
|
||||
);
|
||||
await expect(page.getByRole('heading', { name: /Créer une nouvelle scène/i })).toBeVisible();
|
||||
|
||||
await page.getByLabel(/Nom de la scène/i).fill(sceneName);
|
||||
await page.getByLabel(/Description/i).fill('Résumé rapide de la scène.');
|
||||
|
||||
await page.getByRole('button', { name: /^Créer la scène$/i }).click();
|
||||
|
||||
await expect(page).toHaveURL(
|
||||
new RegExp(
|
||||
`/campaigns/${campaign.id}/arcs/${arc.id}/chapters/${chapter.id}/scenes/\\d+$`,
|
||||
),
|
||||
);
|
||||
|
||||
const createdId = page.url().match(/\/scenes\/(\d+)$/)?.[1];
|
||||
expect(createdId).toBeTruthy();
|
||||
const persisted = await getSceneById(request, createdId!);
|
||||
expect(persisted.name).toBe(sceneName);
|
||||
});
|
||||
|
||||
test('submit is disabled when name is empty', async ({ page }) => {
|
||||
await page.goto(
|
||||
`/campaigns/${campaign.id}/arcs/${arc.id}/chapters/${chapter.id}/scenes/create`,
|
||||
);
|
||||
|
||||
const submit = page.getByRole('button', { name: /^Créer la scène$/i });
|
||||
await expect(submit).toBeDisabled();
|
||||
|
||||
await page.getByLabel(/Nom de la scène/i).fill('OK');
|
||||
await expect(submit).toBeEnabled();
|
||||
});
|
||||
});
|
||||
70
web/e2e/tests/lore/lore-detail-edit.spec.ts
Normal file
70
web/e2e/tests/lore/lore-detail-edit.spec.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { seedLoreWithFolder, deleteLore, getLoreById, type SeededLore } from '../../fixtures/api';
|
||||
|
||||
test.describe('Lore inline edit (on detail page)', () => {
|
||||
let seeded: SeededLore;
|
||||
|
||||
test.beforeEach(async ({ request }) => {
|
||||
seeded = await seedLoreWithFolder(request);
|
||||
});
|
||||
|
||||
test.afterEach(async ({ request }) => {
|
||||
if (seeded?.id) await deleteLore(request, seeded.id);
|
||||
});
|
||||
|
||||
test('renames the lore inline and persists the change', async ({ page, request }) => {
|
||||
const newName = `${seeded.name} renamed`;
|
||||
const newDescription = 'Nouvelle description éditée via UI';
|
||||
|
||||
await page.goto(`/lore/${seeded.id}`);
|
||||
|
||||
await expect(page.locator('.detail-header h1')).toHaveText(seeded.name);
|
||||
|
||||
await page.getByRole('button', { name: /^Modifier$/i }).click();
|
||||
|
||||
const nameInput = page.getByLabel(/^Nom$/);
|
||||
const descInput = page.getByLabel(/^Description$/);
|
||||
await expect(nameInput).toHaveValue(seeded.name);
|
||||
|
||||
await nameInput.fill(newName);
|
||||
await descInput.fill(newDescription);
|
||||
|
||||
await page.getByRole('button', { name: /^Sauvegarder$/i }).click();
|
||||
|
||||
// Sortie du mode édition : le header bascule en mode lecture avec la nouvelle valeur.
|
||||
await expect(page.locator('.detail-header h1')).toHaveText(newName);
|
||||
await expect(page.locator('.detail-header .description')).toHaveText(newDescription);
|
||||
|
||||
const persisted = await getLoreById(request, seeded.id);
|
||||
expect(persisted.name).toBe(newName);
|
||||
expect(persisted.description).toBe(newDescription);
|
||||
});
|
||||
|
||||
test('save button is disabled when name is emptied during edit', async ({ page }) => {
|
||||
await page.goto(`/lore/${seeded.id}`);
|
||||
await page.getByRole('button', { name: /^Modifier$/i }).click();
|
||||
|
||||
const saveBtn = page.getByRole('button', { name: /^Sauvegarder$/i });
|
||||
await expect(saveBtn).toBeEnabled();
|
||||
|
||||
await page.getByLabel(/^Nom$/).fill('');
|
||||
await expect(saveBtn).toBeDisabled();
|
||||
|
||||
await page.getByLabel(/^Nom$/).fill(' ');
|
||||
await expect(saveBtn).toBeDisabled();
|
||||
});
|
||||
|
||||
test('cancel discards the in-flight edits', async ({ page, request }) => {
|
||||
await page.goto(`/lore/${seeded.id}`);
|
||||
await page.getByRole('button', { name: /^Modifier$/i }).click();
|
||||
|
||||
await page.getByLabel(/^Nom$/).fill('Name jamais sauvegardé');
|
||||
await page.getByRole('button', { name: /^Annuler$/i }).click();
|
||||
|
||||
// Retour en mode lecture avec le nom d'origine.
|
||||
await expect(page.locator('.detail-header h1')).toHaveText(seeded.name);
|
||||
|
||||
const persisted = await getLoreById(request, seeded.id);
|
||||
expect(persisted.name).toBe(seeded.name);
|
||||
});
|
||||
});
|
||||
@@ -7,8 +7,9 @@
|
||||
<form [formGroup]="form" (ngSubmit)="submit()" class="arc-form">
|
||||
|
||||
<div class="field">
|
||||
<label>Nom de l'arc *</label>
|
||||
<label for="arc-create-name">Nom de l'arc *</label>
|
||||
<input
|
||||
id="arc-create-name"
|
||||
type="text"
|
||||
formControlName="name"
|
||||
placeholder="Ex: L'Ombre du Nord"
|
||||
@@ -17,8 +18,9 @@
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>Description</label>
|
||||
<label for="arc-create-description">Description</label>
|
||||
<textarea
|
||||
id="arc-create-description"
|
||||
formControlName="description"
|
||||
placeholder="Décrivez l'arc narratif principal..."
|
||||
rows="5">
|
||||
|
||||
@@ -8,8 +8,9 @@
|
||||
<form [formGroup]="form" (ngSubmit)="submit()" class="chapter-form">
|
||||
|
||||
<div class="field">
|
||||
<label>Nom du chapitre *</label>
|
||||
<label for="chapter-create-name">Nom du chapitre *</label>
|
||||
<input
|
||||
id="chapter-create-name"
|
||||
type="text"
|
||||
formControlName="name"
|
||||
placeholder="Ex: Chapitre 1: Les Disparitions"
|
||||
@@ -18,8 +19,9 @@
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>Description</label>
|
||||
<label for="chapter-create-description">Description</label>
|
||||
<textarea
|
||||
id="chapter-create-description"
|
||||
formControlName="description"
|
||||
placeholder="Décrivez ce chapitre..."
|
||||
rows="5">
|
||||
|
||||
@@ -8,8 +8,9 @@
|
||||
<form [formGroup]="form" (ngSubmit)="submit()" class="scene-form">
|
||||
|
||||
<div class="field">
|
||||
<label>Nom de la scène *</label>
|
||||
<label for="scene-create-name">Nom de la scène *</label>
|
||||
<input
|
||||
id="scene-create-name"
|
||||
type="text"
|
||||
formControlName="name"
|
||||
placeholder="Ex: Arrivée au village"
|
||||
@@ -18,8 +19,9 @@
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>Description</label>
|
||||
<label for="scene-create-description">Description</label>
|
||||
<textarea
|
||||
id="scene-create-description"
|
||||
formControlName="description"
|
||||
placeholder="Décrivez la scène, les événements clés, les PNJ présents..."
|
||||
rows="6">
|
||||
|
||||
@@ -21,12 +21,12 @@
|
||||
<!-- ============ Header : mode édition inline ============ -->
|
||||
<div class="detail-header edit-mode" *ngIf="editing">
|
||||
<div class="field">
|
||||
<label>Nom</label>
|
||||
<input type="text" [(ngModel)]="editName" name="editName" required />
|
||||
<label for="lore-detail-edit-name">Nom</label>
|
||||
<input id="lore-detail-edit-name" type="text" [(ngModel)]="editName" name="editName" required />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Description</label>
|
||||
<textarea [(ngModel)]="editDescription" name="editDescription" rows="3"></textarea>
|
||||
<label for="lore-detail-edit-description">Description</label>
|
||||
<textarea id="lore-detail-edit-description" [(ngModel)]="editDescription" name="editDescription" rows="3"></textarea>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<button type="button" class="btn-primary" (click)="saveEdit()" [disabled]="!editName.trim()">
|
||||
|
||||
@@ -60,7 +60,8 @@ export class CampaignService {
|
||||
|
||||
// ========== ARC ==========
|
||||
getArcs(campaignId: string): Observable<Arc[]> {
|
||||
return this.http.get<Arc[]>(`/api/arcs/campaign/${campaignId}`);
|
||||
const params = new HttpParams().set('campaignId', campaignId);
|
||||
return this.http.get<Arc[]>('/api/arcs', { params });
|
||||
}
|
||||
|
||||
getArcById(id: string): Observable<Arc> {
|
||||
@@ -85,7 +86,8 @@ export class CampaignService {
|
||||
|
||||
// ========== CHAPTER ==========
|
||||
getChapters(arcId: string): Observable<Chapter[]> {
|
||||
return this.http.get<Chapter[]>(`/api/chapters/arc/${arcId}`);
|
||||
const params = new HttpParams().set('arcId', arcId);
|
||||
return this.http.get<Chapter[]>('/api/chapters', { params });
|
||||
}
|
||||
|
||||
getChapterById(id: string): Observable<Chapter> {
|
||||
@@ -110,7 +112,8 @@ export class CampaignService {
|
||||
|
||||
// ========== SCENE ==========
|
||||
getScenes(chapterId: string): Observable<Scene[]> {
|
||||
return this.http.get<Scene[]>(`/api/scenes/chapter/${chapterId}`);
|
||||
const params = new HttpParams().set('chapterId', chapterId);
|
||||
return this.http.get<Scene[]>('/api/scenes', { params });
|
||||
}
|
||||
|
||||
getSceneById(id: string): Observable<Scene> {
|
||||
|
||||
Reference in New Issue
Block a user