Files
lage-chat-control/tests/e2e/quiz.spec.ts
T
Aurealibe f4e09ca886
CI / ci (push) Failing after 6m53s
Add educational censorship-resistance quiz as a native Astro page
Reworks the standalone-HTML quiz from PR #7 into the site's real
architecture so it actually ships and respects every project rule.

- /quiz, /fr/quiz, /nl/quiz — prerendered per-locale routes with correct
  canonical + hreflang (new optional `path` prop on Base.astro).
- Progressive enhancement: the twelve questions and the full scoring key
  (bands + per-area guidance with deep links into the guide) are
  server-rendered and readable with JavaScript disabled; the client script
  only computes the live score and reveals the result panel.
- CSP-clean: bundled same-origin script (no inline handlers), bar widths
  set via a CSS custom property through the CSSOM (no inline styles), no
  third-party request. docker/csp.conf needs no new hash.
- i18n via the existing core+overlay model: src/data/quiz.json (neutral
  scoring) + src/i18n/content/<locale>/quiz.json (prose), loaded by a
  dedicated loadQuiz() that checks question/option/band parity in all three
  locales. Page chrome added to the `quiz` namespace (key parity tested).
- Share is copy-link + native Web Share + X (already allowlisted); the
  third-party mastodonshare.com redirector from PR #7 is dropped.
- TopBar "Contents" link made absolute (/#toc) so it works off the guide,
  plus a new "Quiz" nav link. New tests/e2e/quiz.spec.ts covers no-JS
  readability, scoring, the TOC link and zero third-party requests.

Co-Authored-By: arnaudom <1535627+arnaudom@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 13:24:41 +01:00

61 lines
2.9 KiB
TypeScript

import { expect, test } from '@playwright/test'
import { defaultLocale, locales } from '../../src/i18n/locales'
// The generic nojs/zero-request specs only visit the three home pages, so the
// /quiz route gets its own coverage: it must be readable with JS off (the
// twelve questions and the scoring key are server-rendered), reveal a score
// once fully answered (JS enhancement), and never touch a third-party host.
const quizPaths = locales.map((l) => (l === defaultLocale ? '/quiz' : `/${l}/quiz`))
for (const path of quizPaths) {
test(`quiz is readable and self-contained without JS on ${path}`, async ({ page }) => {
await page.goto(path)
await expect(page.locator('h1')).toBeVisible()
// all twelve questions are prerendered as native fieldsets
await expect(page.locator('[data-quiz-question]')).toHaveCount(12)
await expect(page.locator('[data-quiz-question]').first()).toBeVisible()
// the scoring key (bands + per-area guidance) is always visible
await expect(page.locator('#quiz-key')).toBeVisible()
await expect(page.locator('[data-quiz-cat-key] [data-cat-link]').first()).toBeVisible()
// the computed result panel stays hidden until answered (needs JS)
await expect(page.locator('[data-quiz-result]')).toBeHidden()
// the TOC ("Contents") nav link must target the home guide, not a dead
// in-page #toc that only exists on the guide page
await expect(page.locator('#toc')).toHaveCount(0)
await expect(page.locator('header a[href$="/#toc"]')).toHaveCount(1)
})
test(`quiz has 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' })
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight))
await page.waitForTimeout(500)
expect(offenders).toEqual([])
})
}
test('answering every question reveals a 100/100 sovereign score', async ({
page,
javaScriptEnabled,
}) => {
test.skip(javaScriptEnabled === false, 'scoring is a JS enhancement')
await page.goto('/quiz')
const questions = page.locator('[data-quiz-question]')
const count = await questions.count()
for (let i = 0; i < count; i += 1) {
// the strongest option is last in every question and worth the max points
await questions.nth(i).locator('input[type="radio"]').last().check()
}
const result = page.locator('[data-quiz-result]')
await expect(result).toBeVisible()
await expect(page.locator('[data-quiz-score]')).toHaveText('100')
await expect(page.locator('[data-quiz-recs] > *')).toHaveCount(3)
// the live progress counter tracks answers (regression: it once stuck at 0)
await expect(page.locator('[data-quiz-progress-text]')).toContainText('12 / 12')
})