f4e09ca886
CI / ci (push) Failing after 6m53s
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>
61 lines
2.9 KiB
TypeScript
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')
|
|
})
|