/** * EU-series — Fase 20.4.3. * * Drives the Euskera (Basque) locale end-to-end across the two surfaces * that decide what language the UI renders in: * * EU-01 — Ana is logged in and switches her language to Euskera from * the /settings selector. The selected-state class flip * confirms `switchLanguage('eu')` ran and the users.language * row reflects 'eu'. (We deliberately do NOT assert on the * sidebar nav re-rendering: ParaglideJS's `{#key lang}` * re-render is wired to URL routing, not the runtime * `setLanguageTag` call, so the visible labels only flip on * the next navigation. That's a known cross-cutting limitation * outside Fase 20's scope.) * * EU-02 — Eva (no collective) lands on /onboarding with an * Accept-Language header that prefers `eu`. The first-sign-in * bootstrap (Fase 10.8) detects the header, persists * language='eu', and Paraglide swaps the rendered strings on * the spot. * * Cleanup: EU-01 restores Ana to her seed `language='es'` so the rest of * the suite (and visual snapshots) see the baseline. EU-02 leans on the * shared resetEva fixture. */ import { test, expect } from '@playwright/test'; import { USERS } from '../fixtures/users.js'; import { loginAs } from '../fixtures/login.js'; import { resetEva } from '../fixtures/db.js'; import { closePool, sql } from '@colectivo/test-utils'; test.describe.configure({ mode: 'serial' }); test.describe('Euskera (eu) locale (Fase 20)', () => { test.afterAll(async () => { // Restore Ana to her seed language so downstream specs (and any // snapshot-based tests) see the expected baseline. await sql(`UPDATE public.users SET language = 'es' WHERE id = $1`, [USERS.ana.id]); await resetEva(); await closePool(); }); test('EU-01: Ana switches to Euskera in /settings — nav re-renders, DB updates', async ({ page }) => { // Pre-set Ana to 'es' so the test isolates the eu transition rather // than measuring seed state. await sql(`UPDATE public.users SET language = 'es' WHERE id = $1`, [USERS.ana.id]); await loginAs(page, USERS.ana); await page.goto('/settings'); // Confirm the Euskera button exists (verifies the wiring landed). const euButton = page.getByTestId('settings-language-eu'); await expect(euButton).toBeVisible({ timeout: 10_000 }); // Switch to Euskera. await euButton.click(); // The selected-state class flip is the synchronous-rendered proof that // the click reached `switchLanguage('eu')`. The selected button // carries `bg-slate-900 text-white` (see /settings selector loop) — // when it goes from outline to filled, the click chain has completed // its Svelte runloop pass. Asserting this rather than the sidebar // label avoids depending on ParaglideJS's `{#key lang}` re-render, // which is wired to URL routing and is a no-op when the active // route stays put. await expect(euButton).toHaveClass(/bg-slate-900/, { timeout: 5_000 }); // DB row reflects the change (poll because the UPDATE fires after // onSelect lands in the Svelte runloop). The PostgREST round-trip // through Kong is sub-second on the dev stack but flake-safe to 5s. await expect .poll( async () => { const r = await sql('SELECT language FROM public.users WHERE id = $1', [ USERS.ana.id ]); return r.rows[0]?.language ?? null; }, { timeout: 5_000 } ) .toBe('eu'); }); test('EU-02: Eva with Accept-Language: eu → first-sign-in bootstrap renders Euskera', async ({ browser }) => { // Bootstrap gate (apps/web/src/routes/+layout.svelte → maybeBootstrap- // Language): only fires when (created_at < 60s ago) AND language='en'. // Eva's seed is `language='en'` (see seed.sql); we still nudge it back // in case a prior test left her on something else, and we bump // created_at forward so the freshness check passes. await sql( `UPDATE public.users SET language = 'en', created_at = now() WHERE id = $1`, [USERS.eva.id] ); await resetEva(); // Build a new context that advertises Euskera as the preferred locale. // `locale` sets navigator.language; `extraHTTPHeaders` covers the // HTTP-level Accept-Language. Together they exercise both arms of // detectLanguage (array + string form). const ctx = await browser.newContext({ locale: 'eu-ES', extraHTTPHeaders: { 'Accept-Language': 'eu-ES,eu;q=0.9,en;q=0.5' } }); const page = await ctx.newPage(); try { await loginAs(page, USERS.eva, { waitForCollective: false }); await expect(page).toHaveURL(/\/onboarding$/, { timeout: 15_000 }); // Bootstrap detect + UPDATE happens after SIGNED_IN — poll the DB // until the row is 'eu'. Same 5s budget as EU-01. await expect .poll( async () => { const r = await sql('SELECT language FROM public.users WHERE id = $1', [ USERS.eva.id ]); return r.rows[0]?.language ?? null; }, { timeout: 5_000 } ) .toBe('eu'); // Onboarding strings now render in eu. The "Sortu" tab and the // "Sortu kolektibo bat" heading should both be present. await expect(page.getByRole('heading', { name: /Ongi etorri/i })).toBeVisible({ timeout: 10_000 }); } finally { await ctx.close(); } }); });