veza/tests/e2e/launch-audit.spec.ts

458 lines
20 KiB
TypeScript
Raw Normal View History

import { test, expect } from '@chromatic-com/playwright';
import { CONFIG, loginViaUI, navigateTo, assertNoDebugText } from './helpers';
// =============================================================================
// AUDIT — Landing page pré-lancement (/launch)
// =============================================================================
const USER = { email: 'user@veza.music', password: 'User123!' };
test.describe('AUDIT — Landing page pré-lancement (/launch)', () => {
// ─── Chargement & Rendu ────────────────────────────────────────────
test.describe('Chargement & Rendu', () => {
test('01. la page se charge sans crash', async ({ page }) => {
await navigateTo(page, '/launch');
await expect(page.getByTestId('launch-page')).toBeVisible({ timeout: 15_000 });
const body = await page.textContent('body');
expect(body).not.toMatch(/500|Internal Server Error/i);
});
test('02. pas de texte de debug ([object Object], undefined)', async ({ page }) => {
await navigateTo(page, '/launch');
await page.getByTestId('launch-page').waitFor({ state: 'visible', timeout: 15_000 });
await assertNoDebugText(page);
});
test('03. le titre TALAS est visible dans le hero', async ({ page }) => {
await navigateTo(page, '/launch');
const h1 = page.locator('h1');
await expect(h1).toBeVisible({ timeout: 10_000 });
await expect(h1).toContainText('TALAS');
});
test('04. le formulaire email hero est visible', async ({ page }) => {
await navigateTo(page, '/launch');
await expect(page.getByTestId('launch-hero-form')).toBeVisible({ timeout: 10_000 });
await expect(page.getByTestId('launch-hero-email')).toBeVisible();
await expect(page.getByTestId('launch-hero-submit')).toBeVisible();
});
test('05. la navigation est visible avec liens', async ({ page }) => {
await navigateTo(page, '/launch');
await expect(page.getByTestId('launch-nav')).toBeVisible({ timeout: 10_000 });
await expect(page.getByTestId('launch-login-link')).toBeVisible();
});
test('06. le footer est visible avec copyright', async ({ page }) => {
await navigateTo(page, '/launch');
await page.getByTestId('launch-footer').scrollIntoViewIfNeeded();
await expect(page.getByTestId('launch-footer')).toBeVisible({ timeout: 10_000 });
const footerText = await page.getByTestId('launch-footer').textContent();
expect(footerText).toContain('2026');
});
test('07. les 3 cartes "engagements" sont visibles', async ({ page }) => {
await navigateTo(page, '/launch');
// Scroll to values section to trigger animation
await page.evaluate(() => {
const section = document.querySelector('[data-testid="launch-values"]') ||
document.querySelector('h2');
section?.scrollIntoView({ behavior: 'instant' });
});
await page.waitForTimeout(1_000);
const h3s = page.locator('h3');
await expect(h3s).toHaveCount(3, { timeout: 5_000 });
});
test('08. la section produit (#product) est presente', async ({ page }) => {
await navigateTo(page, '/launch');
await expect(page.getByTestId('launch-product')).toBeAttached();
});
});
// ─── Fonctionnalites ───────────────────────────────────────────────
test.describe('Fonctionnalites', () => {
test('09. le formulaire hero envoie POST /newsletter/subscribe', async ({ page }) => {
await navigateTo(page, '/launch');
await page.getByTestId('launch-hero-form').waitFor({ state: 'visible', timeout: 10_000 });
const requestPromise = page.waitForRequest(
(req) => req.url().includes('/newsletter/subscribe') && req.method() === 'POST',
{ timeout: 10_000 },
);
await page.getByTestId('launch-hero-email').fill('audit@test.com');
await page.getByTestId('launch-hero-submit').click();
const request = await requestPromise;
const body = JSON.parse(request.postData() ?? '{}');
expect(body.email).toBe('audit@test.com');
});
test('10. apres soumission reussie, le message de succes s affiche', async ({ page }) => {
await navigateTo(page, '/launch');
await page.getByTestId('launch-hero-form').waitFor({ state: 'visible', timeout: 10_000 });
// Mock the newsletter API to succeed
await page.route('**/newsletter/subscribe', (route) => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ success: true, data: { message: 'Subscribed' } }),
});
});
await page.getByTestId('launch-hero-email').fill('success@test.com');
await page.getByTestId('launch-hero-submit').click();
await expect(page.getByTestId('launch-hero-success')).toBeVisible({ timeout: 5_000 });
});
test('11. le lien CONNEXION navigue vers /login', async ({ page }) => {
await navigateTo(page, '/launch');
await page.getByTestId('launch-login-link').click();
await page.waitForURL('**/login', { timeout: 10_000 });
expect(page.url()).toContain('/login');
});
test('12. le lien ancre #product scrolle vers la section produit', async ({ page }) => {
await navigateTo(page, '/launch');
const productLink = page.locator('a[href="#product"]');
if (await productLink.isVisible()) {
await productLink.click();
await page.waitForTimeout(500);
const productSection = page.getByTestId('launch-product');
await expect(productSection).toBeInViewport({ timeout: 5_000 });
}
});
test('13. le lien ancre #notify scrolle vers le CTA', async ({ page }) => {
await navigateTo(page, '/launch');
const notifyLink = page.locator('a[href="#notify"]');
// Scroll to make it visible first
await page.evaluate(() => {
const el = document.querySelector('a[href="#notify"]');
el?.scrollIntoView({ behavior: 'instant' });
});
await page.waitForTimeout(500);
if (await notifyLink.isVisible()) {
await notifyLink.click();
await page.waitForTimeout(500);
const notifySection = page.getByTestId('launch-notify');
await expect(notifySection).toBeInViewport({ timeout: 5_000 });
}
});
test('14. validation email : email invalide ne soumet pas', async ({ page }) => {
await navigateTo(page, '/launch');
await page.getByTestId('launch-hero-form').waitFor({ state: 'visible', timeout: 10_000 });
let requestFired = false;
page.on('request', (req) => {
if (req.url().includes('/newsletter/subscribe')) requestFired = true;
});
// Type invalid email and try to submit
await page.getByTestId('launch-hero-email').fill('not-an-email');
await page.getByTestId('launch-hero-submit').click();
await page.waitForTimeout(500);
expect(requestFired).toBe(false);
});
test('15. le formulaire CTA (#notify) fonctionne aussi', async ({ page }) => {
await navigateTo(page, '/launch');
await page.route('**/newsletter/subscribe', (route) => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ success: true, data: { message: 'Subscribed' } }),
});
});
// Scroll to notify section
await page.evaluate(() => document.querySelector('#notify')?.scrollIntoView({ behavior: 'instant' }));
await page.waitForTimeout(500);
await page.getByTestId('launch-notify-email').fill('cta@test.com');
await page.getByTestId('launch-notify-submit').click();
await expect(page.getByTestId('launch-notify-success')).toBeVisible({ timeout: 5_000 });
});
test('16. gestion erreur API : message d erreur affiche', async ({ page }) => {
await navigateTo(page, '/launch');
await page.getByTestId('launch-hero-form').waitFor({ state: 'visible', timeout: 10_000 });
await page.route('**/newsletter/subscribe', (route) => {
route.fulfill({
status: 400,
contentType: 'application/json',
body: JSON.stringify({ error: { message: 'Email already registered' } }),
});
});
await page.getByTestId('launch-hero-email').fill('error@test.com');
await page.getByTestId('launch-hero-submit').click();
await expect(page.getByTestId('launch-error-message')).toBeVisible({ timeout: 5_000 });
});
});
// ─── i18n ──────────────────────────────────────────────────────────
test.describe('i18n', () => {
test('17. pas de clef i18n brute visible (landing.xxx)', async ({ page }) => {
await navigateTo(page, '/launch');
await page.getByTestId('launch-page').waitFor({ state: 'visible', timeout: 15_000 });
const body = await page.textContent('body');
expect(body).not.toMatch(/landing\.\w+\.\w+/);
});
test('18. le document.title est specifique a la page', async ({ page }) => {
await navigateTo(page, '/launch');
await page.getByTestId('launch-page').waitFor({ state: 'visible', timeout: 15_000 });
const title = await page.title();
expect(title).toMatch(/TALAS/i);
});
test('19. pas de melange de langues dans la nav', async ({ page }) => {
await navigateTo(page, '/launch');
await page.getByTestId('launch-nav').waitFor({ state: 'visible', timeout: 10_000 });
const navAriaLabel = await page.getByTestId('launch-nav').getAttribute('aria-label');
expect(navAriaLabel).toBeTruthy();
expect(navAriaLabel).not.toMatch(/landing\.\w+/);
});
test('20. les messages d erreur sont dans la langue de la page (pas EN dans page FR)', async ({ page }) => {
await navigateTo(page, '/launch');
await page.getByTestId('launch-hero-form').waitFor({ state: 'visible', timeout: 10_000 });
await page.route('**/newsletter/subscribe', (route) => {
route.fulfill({
status: 500,
contentType: 'application/json',
body: JSON.stringify({}),
});
});
await page.getByTestId('launch-hero-email').fill('i18n@test.com');
await page.getByTestId('launch-hero-submit').click();
await expect(page.getByTestId('launch-error-message')).toBeVisible({ timeout: 5_000 });
const errorText = await page.getByTestId('launch-error-message').textContent();
// Should NOT contain the old hardcoded English strings
expect(errorText).not.toBe('Subscription failed');
expect(errorText).not.toBe('An error occurred');
});
});
// ─── Accessibilite ─────────────────────────────────────────────────
test.describe('Accessibilite', () => {
test('21. utilise un element main semantique', async ({ page }) => {
await navigateTo(page, '/launch');
const main = page.locator('main');
await expect(main).toBeVisible({ timeout: 15_000 });
const tagName = await main.evaluate((el) => el.tagName);
expect(tagName).toBe('MAIN');
});
test('22. main a un id="main-content" (target du skip-to-content)', async ({ page }) => {
await navigateTo(page, '/launch');
const mainContent = page.locator('#main-content');
await expect(mainContent).toBeAttached({ timeout: 10_000 });
});
test('23. la nav principale a un aria-label', async ({ page }) => {
await navigateTo(page, '/launch');
const nav = page.getByTestId('launch-nav');
await expect(nav).toBeVisible({ timeout: 10_000 });
const ariaLabel = await nav.getAttribute('aria-label');
expect(ariaLabel?.trim().length).toBeGreaterThan(0);
});
test('24. les inputs email ont des aria-labels', async ({ page }) => {
await navigateTo(page, '/launch');
const heroEmail = page.getByTestId('launch-hero-email');
await expect(heroEmail).toBeVisible({ timeout: 10_000 });
const ariaLabel = await heroEmail.getAttribute('aria-label');
expect(ariaLabel?.trim().length).toBeGreaterThan(0);
});
test('25. le heading structure est correct (H1 > H2 > H3)', async ({ page }) => {
await navigateTo(page, '/launch');
await page.getByTestId('launch-page').waitFor({ state: 'visible', timeout: 15_000 });
const headings = await page.evaluate(() =>
Array.from(document.querySelectorAll('h1, h2, h3')).map((h) => h.tagName),
);
// H1 should come first
expect(headings[0]).toBe('H1');
// Should have H1 followed by H2s and H3s
expect(headings.filter((h) => h === 'H1')).toHaveLength(1);
});
test('26. les boutons ont des noms accessibles non-vides', async ({ page }) => {
await navigateTo(page, '/launch');
await page.getByTestId('launch-hero-form').waitFor({ state: 'visible', timeout: 10_000 });
const buttons = await page.evaluate(() =>
Array.from(document.querySelectorAll('button')).map((b) => ({
text: b.textContent?.trim(),
ariaLabel: b.getAttribute('aria-label'),
})),
);
for (const btn of buttons) {
const hasName = (btn.text?.length ?? 0) > 0 || (btn.ariaLabel?.length ?? 0) > 0;
expect(hasName).toBe(true);
}
});
});
// ─── Responsive ────────────────────────────────────────────────────
test.describe('Responsive', () => {
test('27. pas d overflow horizontal en mobile (375px)', async ({ page }) => {
await page.setViewportSize({ width: 375, height: 812 });
await navigateTo(page, '/launch');
await page.getByTestId('launch-page').waitFor({ state: 'visible', timeout: 15_000 });
const hasOverflow = await page.evaluate(
() => document.documentElement.scrollWidth > document.documentElement.clientWidth,
);
expect(hasOverflow).toBe(false);
});
test('28. pas d overflow horizontal en tablet (768px)', async ({ page }) => {
await page.setViewportSize({ width: 768, height: 1024 });
await navigateTo(page, '/launch');
await page.getByTestId('launch-page').waitFor({ state: 'visible', timeout: 15_000 });
const hasOverflow = await page.evaluate(
() => document.documentElement.scrollWidth > document.documentElement.clientWidth,
);
expect(hasOverflow).toBe(false);
});
test('29. le formulaire hero est visible en mobile', async ({ page }) => {
await page.setViewportSize({ width: 375, height: 812 });
await navigateTo(page, '/launch');
await expect(page.getByTestId('launch-hero-form')).toBeVisible({ timeout: 10_000 });
await expect(page.getByTestId('launch-hero-email')).toBeVisible();
await expect(page.getByTestId('launch-hero-submit')).toBeVisible();
});
});
// ─── Securite ──────────────────────────────────────────────────────
test.describe('Securite', () => {
test('30. pas de credentials dans localStorage', async ({ page }) => {
await navigateTo(page, '/launch');
await page.getByTestId('launch-page').waitFor({ state: 'visible', timeout: 15_000 });
const sensitive = await page.evaluate(() => {
const keys = Object.keys(localStorage);
return keys.filter(
(k) =>
k.toLowerCase().includes('password') ||
k.toLowerCase().includes('secret') ||
k.toLowerCase().includes('api_key'),
);
});
expect(sensitive).toHaveLength(0);
});
test('31. liens externes ont target="_blank" et rel="noopener"', async ({ page }) => {
await navigateTo(page, '/launch');
await page.getByTestId('launch-footer').scrollIntoViewIfNeeded();
const externalLinks = await page.evaluate(() =>
Array.from(document.querySelectorAll('a[href^="http"]')).map((a) => ({
href: a.getAttribute('href'),
target: a.getAttribute('target'),
rel: a.getAttribute('rel'),
})),
);
for (const link of externalLinks) {
expect(link.target).toBe('_blank');
expect(link.rel).toContain('noopener');
}
});
test('32. le token n est pas visible dans l URL', async ({ page }) => {
await navigateTo(page, '/launch');
expect(page.url()).not.toContain('token');
expect(page.url()).not.toContain('api_key');
});
});
// ─── Regression ────────────────────────────────────────────────────
test.describe('Regression', () => {
test('33. BUG-03 FIX: la page utilise un element <main>', async ({ page }) => {
await navigateTo(page, '/launch');
const main = page.locator('main');
await expect(main).toBeVisible({ timeout: 15_000 });
const id = await main.getAttribute('id');
expect(id).toBe('main-content');
});
test('34. BUG-04 FIX: les data-testid sont presents', async ({ page }) => {
await navigateTo(page, '/launch');
await page.getByTestId('launch-page').waitFor({ state: 'visible', timeout: 15_000 });
const testIds = await page.evaluate(() =>
Array.from(document.querySelectorAll('[data-testid]')).map((el) =>
el.getAttribute('data-testid'),
),
);
expect(testIds).toContain('launch-page');
expect(testIds).toContain('launch-nav');
expect(testIds).toContain('launch-hero');
expect(testIds).toContain('launch-hero-form');
expect(testIds).toContain('launch-footer');
});
test('35. BUG-05 FIX: le bouton CTA ne dit plus "NOTIFIER MOI"', async ({ page }) => {
await navigateTo(page, '/launch');
// Scroll to notify section
await page.evaluate(() => document.querySelector('#notify')?.scrollIntoView({ behavior: 'instant' }));
await page.waitForTimeout(500);
const buttonText = await page.getByTestId('launch-notify-submit').textContent();
expect(buttonText?.trim()).not.toBe('NOTIFIER MOI');
});
test('36. BUG-06 FIX: pas de texte duplique dans la description produit', async ({ page }) => {
await navigateTo(page, '/launch');
await page.evaluate(() => document.querySelector('#product')?.scrollIntoView({ behavior: 'instant' }));
await page.waitForTimeout(500);
const productText = await page.getByTestId('launch-product').textContent() ?? '';
// "composants standards" should only appear once
const matches = productText.match(/composants standards|standard components/gi);
expect((matches?.length ?? 0)).toBeLessThanOrEqual(1);
});
test('37. BUG-10 FIX: la nav principale a un aria-label', async ({ page }) => {
await navigateTo(page, '/launch');
const nav = page.getByTestId('launch-nav');
await expect(nav).toBeVisible({ timeout: 10_000 });
const ariaLabel = await nav.getAttribute('aria-label');
expect(ariaLabel?.trim().length).toBeGreaterThan(0);
expect(ariaLabel).not.toMatch(/landing\.\w+/);
});
test('38. BUG-15 FIX: pas d overflow en mobile 375px', async ({ page }) => {
await page.setViewportSize({ width: 375, height: 812 });
await navigateTo(page, '/launch');
await page.getByTestId('launch-page').waitFor({ state: 'visible', timeout: 15_000 });
const hasOverflow = await page.evaluate(
() => document.documentElement.scrollWidth > document.documentElement.clientWidth,
);
expect(hasOverflow).toBe(false);
});
});
});