Rebuild du site sur Astro : i18n multilingue, faits verifies, Docker, CI, tests
- Astro statique, EN par defaut + /fr/ + /nl/, detection langue navigateur - i18n par fichiers JSON, ajouter une langue = ajouter des traductions - Contenu original porte a l'identique (diff d'inventaire par langue) - Chronologie legislative corrigee sur sources primaires (PE, Conseil, votes) - Timeline 23 precedents + observatoire 35 items + annuaire 66 outils FOSS, chaque affirmation verifiee et sourcee - Zero requete tierce (teste), lisible JS coupe, axe WCAG AA x2 themes - Version offline monofichier par langue a chaque build - Docker multi-stage vers nginx + CSP stricte auto-generee - CI GitHub Actions (SHA-pinnees) + verification hebdo des liens - CONTRIBUTING : divulgation d'affiliation obligatoire, allowlist de domaines testee en CI
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
import AxeBuilder from '@axe-core/playwright'
|
||||
import { expect, test } from '@playwright/test'
|
||||
import { localePaths } from './helpers'
|
||||
|
||||
// WCAG 2 A/AA on every locale, in BOTH themes. The theme override is
|
||||
// applied exactly like the boot script does (data-theme on <html>).
|
||||
test.skip(({ javaScriptEnabled }) => javaScriptEnabled === false, 'axe needs JS')
|
||||
|
||||
for (const path of localePaths) {
|
||||
for (const theme of ['light', 'dark'] as const) {
|
||||
test(`axe WCAG A/AA · ${path} · ${theme}`, async ({ page }) => {
|
||||
// documentElement doesn't exist yet at init-script time — go through
|
||||
// localStorage, which the theme boot script applies before first paint.
|
||||
await page.addInitScript((t) => {
|
||||
localStorage.setItem('ecc-theme', t)
|
||||
}, theme)
|
||||
await page.goto(path)
|
||||
const results = await new AxeBuilder({ page }).withTags(['wcag2a', 'wcag2aa']).analyze()
|
||||
expect(
|
||||
results.violations.map((v) => `${v.id}: ${v.nodes.length} node(s) — ${v.help}`),
|
||||
).toEqual([])
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { expect, test } from '@playwright/test'
|
||||
import { localePaths } from './helpers'
|
||||
|
||||
// Legacy deep links (/#messagerie, /#t-signal…) are shared all over the
|
||||
// place — every anchor id from the old index.html must resolve on every
|
||||
// locale page. The inventory is produced by migration/extract.mjs.
|
||||
// Four legacy ids were toggle BUTTONS in the old chrome (never link
|
||||
// targets, nothing on the web points at them) — the new chrome replaces
|
||||
// them, so they are deliberately not preserved.
|
||||
const LEGACY_CHROME_IDS = new Set(['lang-fr', 'lang-en', 'lang-nl', 'theme'])
|
||||
|
||||
const inventory = (
|
||||
JSON.parse(
|
||||
readFileSync(new URL('../../migration/inventory/anchors.json', import.meta.url), 'utf8'),
|
||||
) as string[]
|
||||
).filter((id) => !LEGACY_CHROME_IDS.has(id))
|
||||
|
||||
for (const path of localePaths) {
|
||||
test(`all ${inventory.length} legacy anchors exist on ${path}`, async ({ page }) => {
|
||||
await page.goto(path)
|
||||
const present = await page.evaluate(
|
||||
(ids) => ids.filter((id) => !document.getElementById(id)),
|
||||
inventory,
|
||||
)
|
||||
expect(present, 'missing legacy anchor ids').toEqual([])
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { defaultLocale, locales } from '../../src/i18n/locales'
|
||||
|
||||
export { defaultLocale, locales }
|
||||
|
||||
/** ['/', '/fr/', '/nl/', …] derived from the locales tuple. */
|
||||
export const localePaths = locales.map((l) => (l === defaultLocale ? '/' : `/${l}/`))
|
||||
@@ -0,0 +1,25 @@
|
||||
import { expect, test } from '@playwright/test'
|
||||
import { localePaths } from './helpers'
|
||||
|
||||
// Tor Browser "safest" mode is part of this site's audience: the FULL
|
||||
// guide must be readable with JavaScript disabled. These run in the
|
||||
// `nojs` project (javaScriptEnabled: false) — and still pass with JS on.
|
||||
for (const path of localePaths) {
|
||||
test(`guide fully readable without JS on ${path}`, async ({ page }) => {
|
||||
await page.goto(path)
|
||||
await expect(page.locator('h1')).toBeVisible()
|
||||
// the guide's narrative sections are prerendered
|
||||
const sections = page.locator('main section[id]')
|
||||
expect(await sections.count()).toBeGreaterThan(10)
|
||||
// <details> is native HTML — install steps must open with JS off
|
||||
const details = page.locator('main details').first()
|
||||
if ((await details.count()) > 0) {
|
||||
await details.locator('summary').click()
|
||||
await expect(details).toHaveAttribute('open', '')
|
||||
}
|
||||
// the language switcher is a native <details> dropdown: it opens and
|
||||
// exposes plain links with no script at all
|
||||
await page.locator('[data-lang-menu] summary').click()
|
||||
await expect(page.locator('[data-lang-menu] a').first()).toBeVisible()
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { expect, test } from '@playwright/test'
|
||||
|
||||
test.skip(({ javaScriptEnabled }) => javaScriptEnabled === false, 'prefs need JS')
|
||||
|
||||
test('theme toggle persists across reloads', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
const before = await page.evaluate(() => document.documentElement.dataset.theme || '')
|
||||
await page.locator('[data-theme-toggle]').click()
|
||||
const after = await page.evaluate(() => document.documentElement.dataset.theme)
|
||||
expect(after).toMatch(/^(light|dark)$/)
|
||||
expect(after).not.toBe(before)
|
||||
await page.reload()
|
||||
await expect(page.locator('html')).toHaveAttribute('data-theme', after!)
|
||||
})
|
||||
|
||||
test('explicit language choice is remembered on the root', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
// pick French from the dropdown: stores ecc-lang and navigates
|
||||
await page.locator('[data-lang-menu] summary').click()
|
||||
await page.locator('[data-lang-choice="fr"]').click()
|
||||
await page.waitForURL('**/fr/')
|
||||
// back to the root: the boot script honors the stored choice
|
||||
await page.goto('/')
|
||||
await page.waitForURL('**/fr/')
|
||||
await expect(page.locator('html')).toHaveAttribute('lang', 'fr')
|
||||
// switching back to English sticks too (no redirect loop)
|
||||
await page.locator('[data-lang-menu] summary').click()
|
||||
await page.locator('[data-lang-choice="en"]').click()
|
||||
await page.waitForURL((url) => url.pathname === '/')
|
||||
await page.goto('/')
|
||||
await expect(page.locator('html')).toHaveAttribute('lang', 'en')
|
||||
})
|
||||
|
||||
test('checklist state survives a reload (when present)', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
const box = page.locator('[data-checklist] input[type="checkbox"]').first()
|
||||
test.skip((await box.count()) === 0, 'checklist not yet wired')
|
||||
await box.check()
|
||||
await page.reload()
|
||||
await expect(page.locator('[data-checklist] input[type="checkbox"]').first()).toBeChecked()
|
||||
})
|
||||
@@ -0,0 +1,20 @@
|
||||
import { expect, test } from '@playwright/test'
|
||||
import { localePaths } from './helpers'
|
||||
|
||||
// The site's core promise, enforced: not a single request leaves for a
|
||||
// third-party host. Fonts, icons, styles, scripts — everything same-origin.
|
||||
for (const path of localePaths) {
|
||||
test(`zero third-party requests on ${path}`, async ({ page, baseURL }) => {
|
||||
const origin = new URL(baseURL!).origin
|
||||
const offenders: string[] = []
|
||||
page.on('request', (req) => {
|
||||
const url = new URL(req.url())
|
||||
if (url.origin !== origin && url.protocol !== 'data:') offenders.push(req.url())
|
||||
})
|
||||
await page.goto(path, { waitUntil: 'networkidle' })
|
||||
// scroll to the bottom to trigger any lazy loading
|
||||
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight))
|
||||
await page.waitForTimeout(500)
|
||||
expect(offenders).toEqual([])
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user