From 0582690dca3ea1a436151f9e9fcdad37c2cd6b94 Mon Sep 17 00:00:00 2001 From: "IETM_FIXE\\ietm6" Date: Sat, 25 Apr 2026 09:23:56 +0200 Subject: [PATCH] =?UTF-8?q?Correction=20d'un=20test=20unitaire=20Ajout=20d?= =?UTF-8?q?'un=20champs=20image=20dans=20les=20templates=20par=20d=C3=A9fa?= =?UTF-8?q?ut=20en=20+=20du=20champs=20nom,=20description=20pour=20avoir?= =?UTF-8?q?=20un=20exemple=20Correction=20du=20visuel=20du=20champs=20d'aj?= =?UTF-8?q?out=20lors=20de=20la=20modification=20d'un=20template=20(appari?= =?UTF-8?q?tion=20ligne=20pleine=20au=20lieu=20de=20texte=20en=20pointill?= =?UTF-8?q?=C3=A9)=20Ajout=20d'un=20intercepteur=20pour=20la=20partie=20d?= =?UTF-8?q?=C3=A9mo=20de=20l'application=20afin=20de=20bien=20rafraichir?= =?UTF-8?q?=20le=20cache=20angular=20lorsque=20le=20temps=20de=20d=C3=A9mo?= =?UTF-8?q?=20est=20expir=C3=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/e2e/tests/lore/page-create.spec.ts | 45 ++++++++++++- .../session-expired.interceptor.ts | 65 +++++++++++++++++++ .../template-create.component.ts | 3 +- .../template-edit.component.html | 2 +- .../template-edit.component.scss | 13 +--- web/src/main.ts | 5 +- 6 files changed, 116 insertions(+), 17 deletions(-) create mode 100644 web/src/app/interceptors/session-expired.interceptor.ts diff --git a/web/e2e/tests/lore/page-create.spec.ts b/web/e2e/tests/lore/page-create.spec.ts index ffe87fa..7e30ace 100644 --- a/web/e2e/tests/lore/page-create.spec.ts +++ b/web/e2e/tests/lore/page-create.spec.ts @@ -50,7 +50,21 @@ test.describe('Page creation', () => { expect(created?.nodeId).toBe(seeded.rootFolderId); }); - test('submit is disabled until title, template and folder are set', async ({ page }) => { + test('submit is disabled until title, template and folder are set', async ({ page, request }) => { + // On seed un 2ᵉ template pour empêcher l'auto-sélection (qui se déclenche + // quand un seul template a un defaultNodeId valide). Avec deux candidats, + // l'utilisateur doit choisir explicitement → on retrouve le comportement + // initial du test : submit disabled tant qu'un template n'est pas cliqué. + const secondFolderRes = await request.post('/api/lore-nodes', { + data: { loreId: seeded.id, name: 'Autre dossier', icon: 'folder', description: '' }, + }); + const secondFolderId = (await secondFolderRes.json()).id; + await seedTemplate(request, { + loreId: seeded.id, + defaultNodeId: secondFolderId, + name: `Second template ${Date.now()}`, + }); + await page.goto(`/lore/${seeded.id}/pages/create`); const submit = page.getByRole('button', { name: /^Créer la page$/i }); @@ -66,6 +80,8 @@ test.describe('Page creation', () => { test('entering on a folder-scoped route preselects that folder', async ({ page, request }) => { const pageTitle = `Page scoped ${Date.now()}`; + // Dossier sans template par défaut → pas d'auto-sélection de template, + // l'utilisateur clique manuellement (ce qu'on veut tester ici). const secondFolderRes = await request.post('/api/lore-nodes', { data: { loreId: seeded.id, name: 'Autre dossier', icon: 'folder', description: '' }, }); @@ -87,4 +103,31 @@ test.describe('Page creation', () => { const pages = await getPagesForLore(request, seeded.id); expect(pages.find((p) => p.title === pageTitle)?.nodeId).toBe(secondFolderId); }); + + test('auto-selects the template on free route when it is the only candidate', async ({ page }) => { + // Le seed donne EXACTEMENT 1 template avec defaultNodeId valide → la + // logique d'auto-sélection doit s'enclencher au chargement. + await page.goto(`/lore/${seeded.id}/pages/create`); + + await expect(page.locator('.template-card.selected', { hasText: template.name })).toBeVisible(); + await expect(page.locator('#page-node')).toHaveValue(seeded.rootFolderId); + + // Conséquence : juste taper un titre suffit pour activer le submit. + const submit = page.getByRole('button', { name: /^Créer la page$/i }); + await expect(submit).toBeDisabled(); + await page.getByLabel(/Titre de la page/i).fill('Auto'); + await expect(submit).toBeEnabled(); + }); + + test('auto-selects the template on folder-scoped route when its defaultNodeId matches', async ({ + page, + }) => { + // Le template seedé pointe sur seeded.rootFolderId — entrer sur la route + // folder-scoped de ce dossier doit auto-sélectionner ce template. + await page.goto(`/lore/${seeded.id}/nodes/${seeded.rootFolderId}/pages/create`); + + await expect(page.locator('.template-card.selected', { hasText: template.name })).toBeVisible(); + await expect(page.locator('#page-node')).toHaveValue(seeded.rootFolderId); + await expect(page.locator('#page-node')).toBeDisabled(); + }); }); diff --git a/web/src/app/interceptors/session-expired.interceptor.ts b/web/src/app/interceptors/session-expired.interceptor.ts new file mode 100644 index 0000000..05abcd0 --- /dev/null +++ b/web/src/app/interceptors/session-expired.interceptor.ts @@ -0,0 +1,65 @@ +import { HttpInterceptorFn, HttpErrorResponse } from '@angular/common/http'; +import { catchError, throwError } from 'rxjs'; + +/** + * Detecte la perte de session demo (orchestrateur) via les codes 401/502 sur + * les appels /api/*, affiche un overlay puis force un rechargement de la page. + * Le reload renvoie l'utilisateur sur la page "Preparation" pour creer une + * nouvelle session sans qu'il ait a faire Ctrl+Shift+R. + * + * Cet interceptor est inerte en mode normal (non-demo) : si le backend natif + * renvoie un 401 legitime, ca declenche aussi le reload, ce qui est sans + * consequence puisqu'aucun flux d'auth utilisateur n'existe encore cote app. + */ + +// Module-level flag : evite de declencher overlay + reload plusieurs fois si +// plusieurs appels echouent en parallele juste apres l'expiration. +let alreadyTriggered = false; + +export const sessionExpiredInterceptor: HttpInterceptorFn = (req, next) => { + return next(req).pipe( + catchError((err) => { + const isApiCall = req.url.includes('/api/'); + const isSessionLoss = + err instanceof HttpErrorResponse && (err.status === 401 || err.status === 502); + + if (isApiCall && isSessionLoss && !alreadyTriggered) { + alreadyTriggered = true; + showExpiredOverlay(); + setTimeout(() => window.location.reload(), 2500); + } + return throwError(() => err); + }) + ); +}; + +function showExpiredOverlay(): void { + const overlay = document.createElement('div'); + overlay.setAttribute('data-session-expired', 'true'); + overlay.style.cssText = [ + 'position:fixed', 'inset:0', + 'background:rgba(26,22,37,0.96)', + 'color:#e4def5', + 'display:flex', 'flex-direction:column', + 'align-items:center', 'justify-content:center', + 'gap:1rem', + 'font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif', + 'z-index:99999', + 'text-align:center', 'padding:2rem', + ].join(';'); + overlay.innerHTML = ` +
✦ Votre session démo a expiré
+
+ Une nouvelle session va être préparée automatiquement.
+ Vos données précédentes ne sont pas conservées. +
+
+ + `; + document.body.appendChild(overlay); +} diff --git a/web/src/app/lore/template-create/template-create.component.ts b/web/src/app/lore/template-create/template-create.component.ts index 90b438e..8e3c9fa 100644 --- a/web/src/app/lore/template-create/template-create.component.ts +++ b/web/src/app/lore/template-create/template-create.component.ts @@ -42,7 +42,8 @@ export class TemplateCreateComponent implements OnInit, OnDestroy { */ fields: TemplateField[] = [ { name: 'Nom', type: 'TEXT' }, - { name: 'Description', type: 'TEXT' } + { name: 'Description', type: 'TEXT' }, + { name: 'Illustration', type: 'IMAGE', layout: 'GALLERY' } ]; /** Valeur courante de l'input d'ajout de champ (non binding direct pour reset facile). */ newFieldName = ''; diff --git a/web/src/app/lore/template-edit/template-edit.component.html b/web/src/app/lore/template-edit/template-edit.component.html index 75188b4..81e7824 100644 --- a/web/src/app/lore/template-edit/template-edit.component.html +++ b/web/src/app/lore/template-edit/template-edit.component.html @@ -103,7 +103,7 @@ - diff --git a/web/src/app/lore/template-edit/template-edit.component.scss b/web/src/app/lore/template-edit/template-edit.component.scss index fdddbfc..5213a31 100644 --- a/web/src/app/lore/template-edit/template-edit.component.scss +++ b/web/src/app/lore/template-edit/template-edit.component.scss @@ -198,18 +198,7 @@ &::placeholder { color: #6b7280; } } - &.add-row { - margin-top: 0.25rem; - border: 1px dashed #2a2a3d; - border-radius: 6px; - padding: 0; - - input { - border: none; - background: transparent; - &:focus { border: none; } - } - } + &.add-row { margin-top: 0.5rem; } .reorder-stack { display: flex; diff --git a/web/src/main.ts b/web/src/main.ts index 044a7cc..2c7eec8 100644 --- a/web/src/main.ts +++ b/web/src/main.ts @@ -2,9 +2,10 @@ import { bootstrapApplication } from '@angular/platform-browser'; import { AppComponent } from './app/app.component'; import { PreloadAllModules, provideRouter, withPreloading } from '@angular/router'; import { routes } from './app/app.routes'; -import { provideHttpClient } from '@angular/common/http'; +import { provideHttpClient, withInterceptors } from '@angular/common/http'; import { APP_INITIALIZER } from '@angular/core'; import { ConfigService } from './app/services/config.service'; +import { sessionExpiredInterceptor } from './app/interceptors/session-expired.interceptor'; // withPreloading(PreloadAllModules) : une fois l'app initiale rendue, Angular // telecharge en arriere-plan tous les chunks lazy-loades. Consequence : la @@ -14,7 +15,7 @@ import { ConfigService } from './app/services/config.service'; bootstrapApplication(AppComponent, { providers: [ provideRouter(routes, withPreloading(PreloadAllModules)), - provideHttpClient(), + provideHttpClient(withInterceptors([sessionExpiredInterceptor])), { provide: APP_INITIALIZER, useFactory: (config: ConfigService) => () => config.load(),