feat: graph T° interactif + widget iframe + MF auth + E2E Playwright
Some checks are pending
Deploy info-canicule / deploy (push) Waiting to run
Some checks are pending
Deploy info-canicule / deploy (push) Waiting to run
Graph T° (TemperatureChartInteractive.astro) : - Onglets 24 h / 7 j / 30 j (toggle JS, séries serialisées au SSR) - Hover vertical line + tooltip valeurs - Overlay normales mois en pointillé (TX orange, TN bleu) - Onglet 24 h dispo seulement si l'API MF a répondu (best-effort) Météo France OAuth2 (lib/meteofrance-auth.ts + observations.ts) : - client_credentials avec refresh auto, cache token Valkey - Fallback METEOFRANCE_STATIC_TOKEN pour debug - /synop endpoint pour 24h horaires par station SYNOP du dept - Mapping dept → station SYNOP la plus proche (src/data/stations-synop.json) - En attente de creds : SDK skip silencieusement, l'onglet 24h n'apparaît pas Widget iframe (/embed/dept/[code] + /embed doc) : - Layout minimal sans header/footer global - Réutilisable via iframe avec une ligne - Page /embed avec snippet copier-coller + aperçu live Tests E2E Playwright (tests/e2e/) : - home (carte 96 paths, tooltip dept, navigation) - api (health, vigilance, vigilance/dept) - departement (tabs période, DROM notice, 404) - static pages (a-propos, mentions, dependances, soutenir, conseils, embed) - embed widget (rendu minimal, headers X-Frame OK) - 20+ tests, run via pnpm test:e2e (live) ou test:e2e:local Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
9cfd4f8385
commit
2c4d91ce2f
20 changed files with 922 additions and 467 deletions
37
tests/e2e/api.spec.ts
Normal file
37
tests/e2e/api.spec.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('API', () => {
|
||||
test('/api/health retourne status ok et cache true', async ({ request }) => {
|
||||
const res = await request.get('/api/health');
|
||||
expect(res.status()).toBeLessThan(600); // accepte 200 ou 503 selon Valkey
|
||||
const body = await res.json();
|
||||
expect(body).toHaveProperty('status');
|
||||
expect(body).toHaveProperty('cache');
|
||||
expect(body).toHaveProperty('time');
|
||||
});
|
||||
|
||||
test('/api/vigilance retourne snapshot JSON valide CORS *', async ({ request }) => {
|
||||
const res = await request.get('/api/vigilance');
|
||||
expect(res.status()).toBe(200);
|
||||
expect(res.headers()['access-control-allow-origin']).toBe('*');
|
||||
const body = await res.json();
|
||||
expect(body).toHaveProperty('fetchedAt');
|
||||
expect(body).toHaveProperty('alerts');
|
||||
expect(Array.isArray(body.alerts)).toBe(true);
|
||||
});
|
||||
|
||||
test('/api/vigilance/dept/13 retourne enrichi avec labels', async ({ request }) => {
|
||||
const res = await request.get('/api/vigilance/dept/13');
|
||||
expect(res.status()).toBe(200);
|
||||
const body = await res.json();
|
||||
expect(body.departement.code).toBe('13');
|
||||
expect(body.departement.name).toBe('Bouches-du-Rhône');
|
||||
expect(body).toHaveProperty('today');
|
||||
expect(body).toHaveProperty('tomorrow');
|
||||
});
|
||||
|
||||
test('/api/vigilance/dept/999 retourne 404', async ({ request }) => {
|
||||
const res = await request.get('/api/vigilance/dept/999');
|
||||
expect(res.status()).toBe(404);
|
||||
});
|
||||
});
|
||||
31
tests/e2e/departement.spec.ts
Normal file
31
tests/e2e/departement.spec.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('Page département', () => {
|
||||
test('/departement/75 (Paris) — métropole', async ({ page }) => {
|
||||
await page.goto('/departement/75');
|
||||
await expect(page.getByRole('heading', { name: 'Paris', exact: true })).toBeVisible();
|
||||
// Graph interactif avec onglets période
|
||||
await expect(page.getByRole('tab', { name: '7 jours' })).toBeVisible();
|
||||
await expect(page.getByRole('tab', { name: '30 jours' })).toBeVisible();
|
||||
});
|
||||
|
||||
test('Tabs période changent activeselected', async ({ page }) => {
|
||||
await page.goto('/departement/75');
|
||||
const tab7 = page.getByRole('tab', { name: '7 jours' });
|
||||
const tab30 = page.getByRole('tab', { name: '30 jours' });
|
||||
await tab30.click();
|
||||
await expect(tab30).toHaveAttribute('aria-selected', 'true');
|
||||
await expect(tab7).toHaveAttribute('aria-selected', 'false');
|
||||
});
|
||||
|
||||
test('/departement/971 (Guadeloupe) — DROM affiche notice', async ({ page }) => {
|
||||
await page.goto('/departement/971');
|
||||
await expect(page.getByRole('heading', { name: 'Guadeloupe' })).toBeVisible();
|
||||
await expect(page.getByText(/Vigilance Outre-mer non couverte/i)).toBeVisible();
|
||||
});
|
||||
|
||||
test('Page dept invalide retourne 404', async ({ page }) => {
|
||||
const res = await page.goto('/departement/999');
|
||||
expect(res?.status()).toBe(404);
|
||||
});
|
||||
});
|
||||
24
tests/e2e/embed.spec.ts
Normal file
24
tests/e2e/embed.spec.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('Widget embed', () => {
|
||||
test('/embed/dept/75 charge avec Paris', async ({ page }) => {
|
||||
await page.goto('/embed/dept/75');
|
||||
await expect(page.locator('h1')).toHaveText('Paris');
|
||||
// Embed minimal : pas de header global Info Canicule
|
||||
await expect(page.getByRole('link', { name: 'Soutenir' })).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('/embed/dept/971 — DROM notice', async ({ page }) => {
|
||||
await page.goto('/embed/dept/971');
|
||||
await expect(page.locator('h1')).toHaveText('Guadeloupe');
|
||||
await expect(page.getByText(/Outre-mer non couverte/i)).toBeVisible();
|
||||
});
|
||||
|
||||
test('embed servi avec X-Frame-Options non bloquant', async ({ request }) => {
|
||||
const res = await request.get('/embed/dept/75');
|
||||
expect(res.status()).toBe(200);
|
||||
const xfo = res.headers()['x-frame-options'];
|
||||
// Soit absent (autorisé), soit SAMEORIGIN (pas DENY)
|
||||
expect(xfo === undefined || xfo.toUpperCase() !== 'DENY').toBe(true);
|
||||
});
|
||||
});
|
||||
35
tests/e2e/home.spec.ts
Normal file
35
tests/e2e/home.spec.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('Home', () => {
|
||||
test('charge avec carte SVG 96 départements', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await expect(page).toHaveTitle(/Info Canicule/);
|
||||
const paths = page.locator('#france-map path[data-code]');
|
||||
await expect(paths).toHaveCount(96);
|
||||
});
|
||||
|
||||
test('navigation principale visible', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await expect(page.getByRole('link', { name: /Carte/i }).first()).toBeVisible();
|
||||
await expect(page.getByRole('link', { name: /Conseils/i }).first()).toBeVisible();
|
||||
await expect(page.getByRole('link', { name: /Soutenir/i }).first()).toBeVisible();
|
||||
});
|
||||
|
||||
test('section "Départements en alerte" si alertes du jour', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
// Soit la section existe avec alertes, soit pas du tout (jour calme) — ne casse pas si vide
|
||||
const section = page.getByRole('heading', { name: /Départements en alerte/i });
|
||||
if (await section.count() > 0) {
|
||||
await expect(section).toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
test('tooltip carte au hover', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
const tooltip = page.locator('#map-tooltip');
|
||||
await expect(tooltip).toBeHidden();
|
||||
await page.locator('#france-map path[data-code="75"]').hover();
|
||||
await expect(tooltip).toBeVisible();
|
||||
await expect(tooltip).toContainText('Paris');
|
||||
});
|
||||
});
|
||||
39
tests/e2e/static-pages.spec.ts
Normal file
39
tests/e2e/static-pages.spec.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import { test, expect } from '@playwright/test';
|
||||
|
||||
const PAGES = [
|
||||
{ url: '/a-propos', title: /À propos/i },
|
||||
{ url: '/mentions-legales', title: /Mentions légales/i },
|
||||
{ url: '/dependances', title: /Dépendances/i },
|
||||
{ url: '/soutenir', title: /Soutenir/i },
|
||||
{ url: '/conseils', title: /Conseils officiels/i },
|
||||
{ url: '/conseils/registre-canicule', title: /Registre canicule/i },
|
||||
{ url: '/embed', title: /Widget intégrable/i },
|
||||
{ url: '/robots.txt', text: /Sitemap:/ },
|
||||
{ url: '/sitemap-departements.xml', text: /<urlset/ },
|
||||
];
|
||||
|
||||
for (const p of PAGES) {
|
||||
test(`page ${p.url} répond 200`, async ({ request, page }) => {
|
||||
if ('text' in p && p.text) {
|
||||
const res = await request.get(p.url);
|
||||
expect(res.status()).toBe(200);
|
||||
const body = await res.text();
|
||||
expect(body).toMatch(p.text);
|
||||
} else if ('title' in p && p.title) {
|
||||
await page.goto(p.url);
|
||||
await expect(page.locator('h1').first()).toHaveText(p.title);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
test('Ko-fi link sur /soutenir', async ({ page }) => {
|
||||
await page.goto('/soutenir');
|
||||
const link = page.getByRole('link', { name: /Soutenir sur Ko-fi/i }).first();
|
||||
await expect(link).toHaveAttribute('href', /ko-fi\.com\/daelwizhit/);
|
||||
});
|
||||
|
||||
test('Mentions légales — distinction Nocleus/perso', async ({ page }) => {
|
||||
await page.goto('/mentions-legales');
|
||||
await expect(page.getByText(/micro-entreprise/i)).toBeVisible();
|
||||
await expect(page.getByText(/non lucratif/i).first()).toBeVisible();
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue