diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..efb19cb --- /dev/null +++ b/.editorconfig @@ -0,0 +1,32 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +charset = utf-8 + +[*.{html,css,js}] +indent_style = space +indent_size = 4 + +[*.py] +indent_style = space +indent_size = 4 + +[*.{yml,yaml}] +indent_style = space +indent_size = 2 + +[*.md] +indent_style = space +indent_size = 2 +trim_trailing_whitespace = false + +[*.json] +indent_style = space +indent_size = 2 + +[justfile] +indent_style = space +indent_size = 4 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..eeb96a9 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,24 @@ +# Normalize line endings +* text=auto + +# Source files +*.html text +*.css text +*.js text +*.json text +*.md text +*.yml text +*.yaml text +*.py text +*.txt text + +# Binary files +*.png binary +*.jpg binary +*.jpeg binary +*.gif binary +*.webp binary +*.avif binary +*.ico binary +*.woff binary +*.woff2 binary diff --git a/.github/comment-templates/lighthouse-report.md b/.github/comment-templates/lighthouse-report.md new file mode 100644 index 0000000..cb26f9d --- /dev/null +++ b/.github/comment-templates/lighthouse-report.md @@ -0,0 +1,9 @@ +# Lighthouse Performance Report + +{{status}} + +## Audit Results + +{{results}} + +All categories must score 90 or higher. diff --git a/.github/comment-templates/preview-build.md b/.github/comment-templates/preview-build.md new file mode 100644 index 0000000..498a957 --- /dev/null +++ b/.github/comment-templates/preview-build.md @@ -0,0 +1,21 @@ +# Preview Build + +āœ… Build completed successfully! + +## Download Preview + +The built site is available as an artifact: **{{artifactName}}** + +To preview locally: + +1. Download the artifact from the Actions tab +2. Unzip the archive +3. Open `index.html` in your browser + +Or run a simple HTTP server: + +```bash +python3 -m http.server 8000 +``` + +Then visit diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 397ca2c..1903b8b 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -8,3 +8,12 @@ updates: dependencies: patterns: - '*' + + - package-ecosystem: 'npm' + directory: '/' + schedule: + interval: 'weekly' + groups: + dependencies: + patterns: + - '*' diff --git a/.github/workflows/check-sri.yaml b/.github/workflows/check-sri.yaml new file mode 100644 index 0000000..b6d86b1 --- /dev/null +++ b/.github/workflows/check-sri.yaml @@ -0,0 +1,66 @@ +name: Check SRI Hash + +on: + schedule: + # Run daily at 6 AM UTC + - cron: '0 6 * * *' + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + +jobs: + check-sri: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Compute remote SRI hash + id: remote + run: | + HASH=$(curl -sf https://gc.zgo.at/count.js | openssl dgst -sha384 -binary | openssl base64 -A) + echo "hash=$HASH" >> "$GITHUB_OUTPUT" + echo "Remote hash: $HASH" + + - name: Extract current SRI hash + id: current + run: | + HASH=$(grep -oP 'integrity="sha384-\K[^"]+' site/index.html) + echo "hash=$HASH" >> "$GITHUB_OUTPUT" + echo "Current hash: $HASH" + + - name: Compare hashes + if: steps.remote.outputs.hash == steps.current.outputs.hash + run: echo "SRI hash is up to date" + + - name: Update SRI hash in all HTML files + if: steps.remote.outputs.hash != steps.current.outputs.hash + run: | + OLD="${{ steps.current.outputs.hash }}" + NEW="${{ steps.remote.outputs.hash }}" + echo "Updating SRI hash: $OLD -> $NEW" + sed -i "s|integrity=\"sha384-${OLD}\"|integrity=\"sha384-${NEW}\"|g" \ + site/index.html \ + site/404.html \ + site/about/index.html \ + site/karaoke/index.html \ + site/generate/index.html + + - name: Create Pull Request + if: steps.remote.outputs.hash != steps.current.outputs.hash + uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0 + with: + branch: fix/update-goatcounter-sri + commit-message: 'fix: update GoatCounter SRI hash' + title: 'fix: update GoatCounter SRI hash' + body: | + ## GoatCounter SRI hash update + + The `count.js` script at `gc.zgo.at` has changed, causing the SRI integrity hash to become stale. + + **Old hash:** `sha384-${{ steps.current.outputs.hash }}` + **New hash:** `sha384-${{ steps.remote.outputs.hash }}` + + This was detected automatically by the `check-sri` workflow. + labels: sri-update diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 035710c..722d256 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -15,7 +15,9 @@ jobs: steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: extractions/setup-just@f8a3cce218d9f83db3a2ecd90e41ac3de6cdfd9b # v3.1.0 + - uses: taiki-e/install-action@42721ded7ddc3cd90f687527e8602066e4e1ff3a # v2.69.2 + with: + tool: just - name: Lint HTML, CSS, and JS id: lint @@ -27,14 +29,18 @@ jobs: run: | # Extract external URLs from site files (src, href, content, @import, data-goatcounter) # Then normalize protocol-relative URLs and deduplicate - urls=$(grep -roEh '(https?://|//)[^"'"'"' <>)]+' site/ \ + urls=$(grep -roEhI --include='*.html' --include='*.css' --include='*.js' --include='*.svg' \ + '(https?://|//)[^"'"'"' <>)]+' site/ \ | sed 's/^\/\//https:\/\//' \ + | sed 's/;$//' \ | grep -v 'xmlns' \ + | grep -v 'w3\.org' \ | grep -v 'wraas\.github\.io' \ | grep -v 'youtube\.com' \ | grep -v 'lyricsondemand\.com' \ | grep -v 'goatcounter\.com' \ - | sort -u) + | grep -v 'gc\.zgo\.at' \ + | sort -u || true) if [ -z "$urls" ]; then echo "No external URLs found" @@ -113,9 +119,29 @@ jobs: const body = `## CI Failures\n\n${sections.join('\n\n')}\n\nPlease fix before merging.`; - await github.rest.issues.createComment({ + const marker = ""; + const bodyWithMarker = marker + "\n" + body; + + const { data: comments } = await github.rest.issues.listComments({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, - body: body }); + + const existing = comments.find(c => c.body.includes(marker)); + + if (existing) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existing.id, + body: bodyWithMarker, + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: bodyWithMarker, + }); + } diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 1848663..8a6eba4 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -23,10 +23,12 @@ jobs: - name: Setup Pages uses: actions/configure-pages@983d7736d9b0ae728b81ab479565c72886d7745b # v5.0.0 + - uses: taiki-e/install-action@42721ded7ddc3cd90f687527e8602066e4e1ff3a # v2.69.2 + with: + tool: just + - name: Build site - run: | - mkdir -p _site - cp -r site/* _site/ + run: just build - name: Upload artifact uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b # v4.0.0 diff --git a/.github/workflows/lighthouse.yaml b/.github/workflows/lighthouse.yaml new file mode 100644 index 0000000..35bdad1 --- /dev/null +++ b/.github/workflows/lighthouse.yaml @@ -0,0 +1,132 @@ +name: Lighthouse CI + +on: + pull_request: + push: + branches: [main] + +permissions: + contents: read + pull-requests: write + +jobs: + lighthouse: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - uses: taiki-e/install-action@42721ded7ddc3cd90f687527e8602066e4e1ff3a # v2.69.2 + with: + tool: just + + - name: Build site + run: just build + + - name: Start local server + id: server + run: | + cd _site + python3 -m http.server 8000 > /dev/null 2>&1 & + echo "pid=$!" >> $GITHUB_OUTPUT + sleep 2 + + - name: Install Lighthouse + run: npm install -g lighthouse + + - name: Run Lighthouse on key pages + id: lighthouse + run: | + set +e + + pages=("/" "/about/" "/generate/" "/karaoke/" "/404.html") + results_json="" + all_passed=true + + for page in "${pages[@]}"; do + url="http://localhost:8000${page}" + echo "Testing $url..." + + lighthouse "$url" \ + --chrome-flags="--headless=new --no-sandbox" \ + --output=json \ + --output-path=/tmp/lighthouse-${page//\//-}.json \ + --disable-full-page-screenshot \ + --throttling-method=devtools \ + > /dev/null 2>&1 || true + + if [ -f "/tmp/lighthouse-${page//\//-}.json" ]; then + perf=$(jq '.categories.performance.score * 100' "/tmp/lighthouse-${page//\//-}.json") + access=$(jq '.categories.accessibility.score * 100' "/tmp/lighthouse-${page//\//-}.json") + bp=$(jq '.categories."best-practices".score * 100' "/tmp/lighthouse-${page//\//-}.json") + seo=$(jq '.categories.seo.score * 100' "/tmp/lighthouse-${page//\//-}.json") + + printf "Page: %-20s | Performance: %3.0f | Accessibility: %3.0f | Best Practices: %3.0f | SEO: %3.0f\n" "$page" "$perf" "$access" "$bp" "$seo" + + if (( $(echo "$perf < 90" | bc -l) )); then + all_passed=false + fi + if (( $(echo "$access < 90" | bc -l) )); then + all_passed=false + fi + if (( $(echo "$bp < 90" | bc -l) )); then + all_passed=false + fi + + results_json+=" + - **$page**: Performance $perf, Accessibility $access, Best Practices $bp, SEO $seo" + fi + done + + if [ "$all_passed" = false ]; then + echo "LIGHTHOUSE_RESULTS<> $GITHUB_ENV + echo "$results_json" >> $GITHUB_ENV + echo "EOF" >> $GITHUB_ENV + echo "All Lighthouse checks must have scores >= 90" + exit 1 + else + echo "LIGHTHOUSE_RESULTS<> $GITHUB_ENV + echo "$results_json" >> $GITHUB_ENV + echo "EOF" >> $GITHUB_ENV + fi + + - name: Comment on pull request with results + if: always() && github.event_name == 'pull_request' + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + script: | + const fs = require("fs"); + const results = process.env.LIGHTHOUSE_RESULTS || "No results available"; + const status = '${{ steps.lighthouse.outcome }}' === 'success' ? 'āœ… PASSED' : 'āš ļø NEEDS IMPROVEMENT'; + const template = fs.readFileSync(".github/comment-templates/lighthouse-report.md", "utf8"); + const body = template.replace("{{status}}", status).replace("{{results}}", results); + + const marker = ""; + const bodyWithMarker = marker + "\n" + body; + + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }); + + const existing = comments.find(c => c.body.includes(marker)); + + if (existing) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existing.id, + body: bodyWithMarker, + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: bodyWithMarker, + }); + } + + - name: Cleanup + if: always() + run: kill "${{ steps.server.outputs.pid }}" 2>/dev/null || true diff --git a/.github/workflows/links.yaml b/.github/workflows/links.yaml new file mode 100644 index 0000000..6d1600a --- /dev/null +++ b/.github/workflows/links.yaml @@ -0,0 +1,72 @@ +name: Broken Links Check + +on: + push: + branches: [main] + schedule: + # Run weekly on Monday at 9 AM UTC + - cron: '0 9 * * 1' + workflow_dispatch: + +permissions: + contents: read + issues: write + +jobs: + check-links: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Link Checker + id: lychee + uses: lycheeverse/lychee-action@8646ba30535128ac92d33dfc9133794bfdd9b411 # v2.8.0 + with: + args: | + --exclude youtube.com + --exclude lyricsondemand.com + --exclude localhost + --exclude 127.0.0.1 + --exclude rickroll + --exclude 'rick' + --timeout 30 + --max-retries 2 + site/ + fail: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Create issue if links are broken + if: failure() + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + script: | + const issue_title = "šŸ”— Broken Links Detected"; + const issue_body = `## Broken Links Found + + The link checker detected broken links in the repository. + + **Action Required:** Please review and fix the broken links. + + For details, check the [workflow run](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}). + + --- + This issue was automatically created by the Broken Links Check workflow.`; + + // Check if issue already exists + const issues = await github.rest.issues.listForRepo({ + owner: context.repo.owner, + repo: context.repo.repo, + state: 'open', + labels: ['broken-links'] + }); + + if (issues.data.length === 0) { + await github.rest.issues.create({ + owner: context.repo.owner, + repo: context.repo.repo, + title: issue_title, + body: issue_body, + labels: ['broken-links'] + }); + } diff --git a/.github/workflows/preview.yaml b/.github/workflows/preview.yaml new file mode 100644 index 0000000..b44277f --- /dev/null +++ b/.github/workflows/preview.yaml @@ -0,0 +1,47 @@ +name: Preview Build + +on: + pull_request: + +permissions: + contents: read + pull-requests: write + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - uses: taiki-e/install-action@42721ded7ddc3cd90f687527e8602066e4e1ff3a # v2.69.2 + with: + tool: just + + - name: Build site + id: build + run: just build + + - name: Upload build artifact + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: preview-site-${{ github.event.pull_request.number }} + path: _site + retention-days: 7 + + - name: Comment on pull request + if: always() + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + script: | + const fs = require("fs"); + const prNumber = context.issue.number; + const artifactName = `preview-site-${prNumber}`; + const template = fs.readFileSync(".github/comment-templates/preview-build.md", "utf8"); + const comment = template.replace("{{artifactName}}", artifactName); + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + body: comment + }); diff --git a/.gitignore b/.gitignore index 57510a2..bdb8082 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,12 @@ +# OS files +.DS_Store + +# Build output _site/ + +# Dependencies +node_modules/ + +# Test artifacts +playwright-report/ +test-results/ diff --git a/.node-version b/.node-version new file mode 100644 index 0000000..2bd5a0a --- /dev/null +++ b/.node-version @@ -0,0 +1 @@ +22 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5381b20..1e6dcb7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1 +1,23 @@ -https://wraas.github.io/contributing +# How to Contribute to W.R.A.A.S. + +New to the project? Start with the [First Theme Tutorial](docs/tutorials/first-theme.md) — it walks you through the full workflow end-to-end. + +## Guides + +- [How to Run Locally](docs/how-to/run-locally.md) — set up the dev environment +- [How to Add a Theme](docs/how-to/add-a-theme.md) — add a new themed scenario +- [How to Run Tests](docs/how-to/run-tests.md) — run the test suite +- [How to Submit a PR](docs/how-to/submit-a-pr.md) — open a pull request +- [How to Debug CI Failures](docs/how-to/debug-ci-failures.md) — diagnose and fix CI failures + +## Reference + +- [Theme Configuration](docs/reference/theme-configuration.md) — theme object fields and constraints +- [Test Suite](docs/reference/test-suite.md) — test catalog, coverage, and timing values +- [CI Workflows](docs/reference/ci-workflows.md) — GitHub Actions workflow reference + +## Understanding the codebase + +- [Architecture](docs/explanation/architecture.md) — how the rickroll pipeline works +- [Image Formats](docs/explanation/image-formats.md) — why both GIF and WebP +- [SRI and Supply-Chain Protection](docs/explanation/sri-and-supply-chain.md) — SRI trade-offs for third-party scripts diff --git a/README.md b/README.md index a1b47a5..b1eaf2a 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,7 @@ +

+ W.R.A.A.S. logo +

+ # W.R.A.A.S. > [!NOTE] @@ -24,43 +28,11 @@ Any path that doesn't match a themed scenario goes straight to Rick — so `http ### Run locally -Prerequisites: Python 3 and [just](https://github.com/casey/just). - -```sh -just dev -``` - -This starts a local server at `http://localhost:8000` that mimics GitHub Pages 404 behavior (all unknown paths serve `404.html`). - -To verify the developer easter eggs: - -```sh -# Check custom HTTP headers -curl -I http://localhost:8000 - -# Check humans.txt -curl http://localhost:8000/humans.txt +See [How to Run Locally](docs/how-to/run-locally.md) for setup and dev server instructions. -# Check security.txt -curl http://localhost:8000/.well-known/security.txt -``` - -Then open `http://localhost:8000` in a browser and check the DevTools console for ASCII art. - -### Add a themed scenario - -Add an entry to the `themes` object in `site/script.js`: - -```js -"/your-path": { - loadingText: "Your loading message...", - fakeDelay: 2000, - title: "Browser Tab Title", - desc: "Description for OG meta tag." -} -``` +### Contribute -The path is matched against the URL, so `/your-path` triggers on `https://wraas.github.io/your-path`. Keyword matching also works — if the URL contains the keyword anywhere, the theme activates. +See [CONTRIBUTING.md](CONTRIBUTING.md) for guides on adding themes, running tests, and submitting PRs. New contributors should start with the [First Theme Tutorial](docs/tutorials/first-theme.md). --- @@ -158,10 +130,14 @@ Developers who inspect the site get rickrolled too: ### CI/CD -Two GitHub Actions workflows keep the rickroll alive: +Five GitHub Actions workflows keep the rickroll alive: -- **CI** — Runs on every PR and push to `main`. Lints HTML, CSS, and JS via `just lint`, checks that critical external URLs are reachable (Rick GIF, Google Fonts, GoatCounter), and validates OG metadata on the rickroll pages. Comments on the PR with details if anything fails. +- **CI** — Runs on every PR and push to `main`. Lints HTML, CSS, and JS via `just lint`, checks that critical external URLs are reachable, and validates OG metadata on the rickroll pages. Comments on the PR with details if anything fails. - **Check robots.txt** — Validates `robots.txt` syntax on every PR that modifies it. Ensures directives are valid, every `Allow`/`Disallow` has a preceding `User-agent`, and the catch-all `User-agent: *` rule exists. +- **Check SRI Hash** — Runs daily. Verifies the GoatCounter `count.js` SRI integrity hash is still valid and opens a PR to update it if it has changed. +- **Lighthouse CI** — Audits performance, accessibility, best practices, and SEO on every PR and push to `main`. Comments scores on the PR; all categories must score 90+. +- **Broken Links** — Runs weekly and on push to `main`. Checks all links in `site/` and auto-creates an issue if any are broken. +- **Preview Build** — Builds the site on every PR and uploads the artifact for local preview. ### Analytics @@ -175,7 +151,7 @@ Page views and mute button clicks are tracked via [GoatCounter](https://www.goat - **GitHub Pages** — Static hosting with custom 404.html for universal path matching - **Web Audio API** — Synthesized melody without external audio files - **GoatCounter** — Privacy-respecting analytics -- **GitHub Actions** — CI lint checks, external URL validation, and robots.txt syntax enforcement +- **GitHub Actions** — CI lint checks, Lighthouse audits, broken link monitoring, and PR preview builds ## File Structure @@ -185,6 +161,17 @@ site/ ā”œā”€ā”€ index.html # The rickroll (served for the root path) ā”œā”€ā”€ style.css # Styles, animations, fake loading overlay ā”œā”€ā”€ script.js # Reveal sequence, audio system, theme router +ā”œā”€ā”€ sw.js # Service worker for offline caching +ā”œā”€ā”€ logo-200.webp # W.R.A.A.S. logo (WebP, used in pages) +ā”œā”€ā”€ logo-200.png # W.R.A.A.S. logo (PNG fallback) +ā”œā”€ā”€ favicon.ico # Multi-size favicon (16/32/48px) +ā”œā”€ā”€ favicon-16x16.png # 16x16 favicon +ā”œā”€ā”€ favicon-32x32.png # 32x32 favicon +ā”œā”€ā”€ apple-touch-icon.png # 180x180 Apple touch icon +ā”œā”€ā”€ android-chrome-192x192.png # 192x192 Android icon +ā”œā”€ā”€ android-chrome-512x512.png # 512x512 Android icon +ā”œā”€ā”€ rick.gif # Self-hosted Rick Astley GIF +ā”œā”€ā”€ rick.webp # WebP version of the GIF ā”œā”€ā”€ robots.txt # The other rickroll ā”œā”€ā”€ humans.txt # Rickrolled humans.txt ā”œā”€ā”€ og-image.png # Social media preview image @@ -196,11 +183,35 @@ site/ ā”œā”€ā”€ generate/ │ └── index.html # Link disguise generator tool └── karaoke/ - └── index.html # Karaoke troll page + ā”œā”€ā”€ index.html # Karaoke troll page + └── karaoke.js # Karaoke page JavaScript module .github/workflows/ +ā”œā”€ā”€ check-robots-txt.yaml # robots.txt syntax validation on PR +ā”œā”€ā”€ check-sri.yaml # Daily GoatCounter SRI hash verification ā”œā”€ā”€ ci.yaml # Lint, external URL checks, OG validation -└── check-robots-txt.yaml # robots.txt syntax validation on PR +ā”œā”€ā”€ deploy.yaml # GitHub Pages deployment +ā”œā”€ā”€ lighthouse.yaml # Lighthouse performance audits +ā”œā”€ā”€ links.yaml # Broken link checker +└── preview.yaml # PR preview build + +docs/ +ā”œā”€ā”€ tutorials/ +│ └── first-theme.md # End-to-end tutorial for new contributors +ā”œā”€ā”€ how-to/ +│ ā”œā”€ā”€ add-a-theme.md # Add a themed scenario +│ ā”œā”€ā”€ debug-ci-failures.md # Diagnose and fix CI failures +│ ā”œā”€ā”€ run-locally.md # Set up the dev environment +│ ā”œā”€ā”€ run-tests.md # Run the test suite +│ └── submit-a-pr.md # Open a pull request +ā”œā”€ā”€ explanation/ +│ ā”œā”€ā”€ architecture.md # How the rickroll pipeline works +│ ā”œā”€ā”€ image-formats.md # Why both GIF and WebP +│ └── sri-and-supply-chain.md # SRI trade-offs for third-party scripts +└── reference/ + ā”œā”€ā”€ ci-workflows.md # GitHub Actions workflow reference + ā”œā”€ā”€ test-suite.md # Test catalog, coverage, and timing + └── theme-configuration.md # Theme object fields and constraints ``` ## License diff --git a/SECURITY.md b/SECURITY.md index 826e766..4f28b84 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,3 +1,46 @@ # Security Policy -https://wraas.github.io/security-policy +## Scope + +W.R.A.A.S. is a static joke site hosted on GitHub Pages. It has no backend, no database, no user accounts, and no server-side processing. The attack surface is limited to: + +- Client-side HTML, CSS, and JavaScript served from `wraas.github.io` +- Third-party scripts (GoatCounter analytics via `gc.zgo.at`) +- GitHub Actions CI/CD workflows + +## Supported versions + +Only the latest version deployed to `main` is supported. There are no versioned releases. + +## Reporting a vulnerability + +If you discover a security issue, please report it privately: + +- **Email:** romain.lespinasse@gmail.com +- **GitHub:** [Open a private security advisory](https://github.com/wraas/wraas.github.io/security/advisories/new) + +Please include: + +- Description of the vulnerability +- Steps to reproduce +- Potential impact + +You should receive an acknowledgment within 48 hours. Please do not open a public issue for security vulnerabilities. + +## Security measures + +### Subresource Integrity (SRI) + +All third-party scripts are loaded with `integrity` and `crossorigin` attributes to prevent execution of tampered code. A daily CI workflow (`check-sri.yaml`) verifies that SRI hashes are current and opens a PR if they need updating. + +### GitHub Actions supply-chain security + +All GitHub Actions are pinned to specific commit SHAs rather than mutable tags, preventing compromised or hijacked actions from running in CI. See the [CI Workflows Reference](docs/reference/ci-workflows.md) for the full list of pinned versions. + +### No sensitive data + +The site collects no personal data, stores no credentials, and uses no cookies. Analytics are handled by GoatCounter, a privacy-respecting service that does not track individual users. + +## Note about `security.txt` + +The `/.well-known/security.txt` file on the live site is an easter egg — it is part of the rickroll. For actual security matters, use the contact methods listed above. diff --git a/docs/explanation/architecture.md b/docs/explanation/architecture.md new file mode 100644 index 0000000..2b360da --- /dev/null +++ b/docs/explanation/architecture.md @@ -0,0 +1,83 @@ +# How the Rickroll Pipeline Works + +Every visit to wraas.github.io passes through a four-stage pipeline: **routing**, **fake loading**, **reveal**, and **audio**. Each stage is designed to maximize the deception and ensure no visitor escapes unrickrolled. + +## The entry point: GitHub Pages 404 + +GitHub Pages serves `404.html` for any URL that doesn't match a real file. W.R.A.A.S. exploits this by making `404.html` and `index.html` identical — the rickroll page. This means every possible URL (`/quarterly-report`, `/onboarding-form`, `/literally-anything`) serves the same rickroll. + +The only real pages that exist as separate files are `/about/`, `/generate/`, and `/karaoke/`. Everything else hits the 404 catch-all. + +## Stage 1: Routing + +When `script.js` loads, the first thing it does is check the URL path against the `themes` object — a map of paths to fake loading screen configurations: + +```js +var themes = { + "/meeting": { + loadingText: "Redirecting to your meeting...", + fakeDelay: 2000, + title: "You've been invited to a meeting", + desc: "Your attendance is required." + }, + // ... +}; +``` + +The router tries two matching strategies: + +1. **Exact match** — `/meeting` matches the `/meeting` theme directly. +2. **Keyword match** — `/my-team-meeting-notes` matches because it contains the keyword `meeting`. + +If a theme matches, the browser tab title and meta description are updated to match the theme's `title` and `desc` before any visual changes occur. This sells the deception in the brief moment before the page renders. + +If no theme matches, the rickroll reveals immediately with no fake loading. + +## Stage 2: Fake loading + +For themed URLs, a white overlay appears with a spinner and the theme's `loadingText`. This is the key deception layer — the victim sees a professional-looking loading screen and believes they're waiting for a real document, meeting link, or invoice. + +The overlay includes a progress bar that advances through scripted steps (15%, 35%, 58%, 72%, 89%, 94%) at fixed intervals, creating the illusion of a real page load. + +A "Taking too long? Click to speed up" skip button is pure trolling: each click resets the timer and adds 1.5 seconds of extra delay, cycling through messages like "Optimizing connection..." and "Recalibrating servers..." to keep the victim waiting longer. + +After the theme's `fakeDelay` elapses (1.8s–2.5s depending on the theme), the progress bar jumps to 100% and the overlay fades out, revealing the rickroll beneath. + +## Stage 3: Reveal + +The reveal sequence is a timed animation: + +1. The Rick Astley GIF background fades in (via the `.visible` CSS class on `.rick-bg`). +2. Each lyric line fades in one by one — 600ms initial delay, then 300ms between each line. +3. The mute button appears after the last lyric line. + +If the user has `prefers-reduced-motion` enabled, all animations are skipped: lyrics, GIF, and mute button appear instantly. The fake loading stage is also skipped entirely. + +## Stage 4: Audio + +The melody is synthesized entirely in-browser using the Web Audio API. No audio files are loaded — the notes of "Never Gonna Give You Up" are defined as an array of frequency/duration pairs and played through square wave oscillators at 113 BPM (the original song's tempo). + +Audio in browsers requires a user gesture to start. W.R.A.A.S. handles this with two mechanisms: + +1. **Consent banner** — A fake cookie consent banner appears after the reveal. Both "Accept All" and "Reject All" trigger the melody (the choice was never real). +2. **Global click/tap listener** — Any click or tap anywhere on the page starts the melody if it isn't already playing, ensuring eventual rickrolling even if the banner is dismissed. + +The melody loops indefinitely. The mute button suspends and resumes the AudioContext without destroying it, so unmuting picks up where the music left off. + +## Link expiration + +Links created with the generator can include an `?expires=` query parameter (a Unix timestamp). When `script.js` loads, it checks this parameter first — if the link has expired, it shows a static "This link has expired" page and short-circuits the entire pipeline. No rickroll, no audio. The victim gets away clean. + +## Blocker detection + +After the reveal, a background check looks for signs that the rickroll might be blocked: + +- The GIF failed to load (blocked by a content blocker or ad blocker). +- The Web Audio API is unavailable or errored. +- Known rickroll-blocker extension markers exist in the DOM. + +If a blocker is detected, the page falls back to a gradient background with pulsing lyrics and a notice: "It seems you have excellent taste in browser extensions. But you can't escape Rick." + +## Service worker + +A service worker (`sw.js`) caches critical assets (HTML, CSS, JS, GIF, WebP, fonts) for offline access. Once cached, the rickroll works without an internet connection. There is no escape. diff --git a/docs/explanation/image-formats.md b/docs/explanation/image-formats.md new file mode 100644 index 0000000..b46acb2 --- /dev/null +++ b/docs/explanation/image-formats.md @@ -0,0 +1,29 @@ +# Why Both rick.gif and rick.webp? + +W.R.A.A.S. ships two copies of the Rick Astley dancing animation in different formats. This is intentional. + +## The `` element + +Every rickroll page uses a `` element to serve the right format: + +```html + + + + +``` + +The browser picks the first `` it supports. If it understands WebP, it loads `rick.webp` and never fetches the GIF. If it doesn't, it falls back to the `` and loads `rick.gif`. + +## Why not just one format? + +Our mission is to rickroll **absolutely anyone, seriously**. That means: + +- **WebP-only** would exclude users on older browsers (IE11, legacy Android WebViews, some embedded browsers, corporate machines frozen on ancient software). Those people would see a blank background and miss the GIF entirely. A failed rickroll is worse than no rickroll at all. +- **GIF-only** would work everywhere but misses the opportunity to serve a more efficient format to modern browsers. WebP generally offers better compression, though in this specific case the file sizes are comparable (~4.5 MB each). + +The dual-format approach guarantees that every visitor — regardless of browser vintage — gets the full Rick Astley experience with zero JavaScript required for the fallback. + +## When could we drop the GIF? + +When the last browser that doesn't support WebP stops being used. As of 2024, global WebP support exceeds 97%, but the remaining 3% still deserve to be rickrolled. The GIF stays. diff --git a/docs/explanation/sri-and-supply-chain.md b/docs/explanation/sri-and-supply-chain.md new file mode 100644 index 0000000..740fd51 --- /dev/null +++ b/docs/explanation/sri-and-supply-chain.md @@ -0,0 +1,76 @@ +# SRI and Supply-Chain Protection for Third-Party Scripts + +W.R.A.A.S. loads GoatCounter's `count.js` from an external CDN (`gc.zgo.at`). This page explains why we pin that script with Subresource Integrity (SRI), what breaks when the hash goes stale, and how the automated check keeps things working. + +## What is SRI? + +Subresource Integrity lets you tell the browser: "only execute this script if its content matches this cryptographic hash." The `integrity` attribute on a ` +``` + +If the file served by the CDN does not match the hash, the browser refuses to execute it. This protects against: + +- **CDN compromise** — if an attacker modifies the script on the CDN, the tampered version will not run. +- **Supply-chain attacks** — if the upstream project is compromised and pushes malicious code to their CDN, the hash mismatch blocks it. +- **Accidental changes** — if the CDN serves a different version due to a deployment error, it is caught. + +The `crossorigin="anonymous"` attribute is required for SRI to work on cross-origin scripts. Without it, the browser skips the integrity check. + +## The trade-off + +SRI is a one-way lock: it protects you from unauthorized changes, but it also blocks *authorized* changes. When GoatCounter legitimately updates `count.js` (bug fixes, new features, performance improvements), the hash no longer matches and the browser silently refuses to load the script. + +The failure mode is invisible: + +- No error message is shown to the user. +- No console error is logged (unless the developer has the Network tab open). +- Analytics simply stop working. Page views are no longer counted. + +This is the fundamental tension: **SRI trades availability for integrity**. Without SRI, the script always loads but you trust the CDN implicitly. With SRI, you verify every byte but risk silent breakage when the upstream changes. + +## Why we chose SRI anyway + +For W.R.A.A.S., the trade-off favors integrity: + +1. **GoatCounter updates infrequently.** The `count.js` script is a small, stable analytics snippet. It changes rarely — typically a few times a year at most. + +2. **Analytics loss is low-impact.** If the hash goes stale and analytics stops working for a day, no user-facing functionality is affected. The rickroll still works. Nobody's workflow is blocked. + +3. **Supply-chain attacks are high-impact.** If the CDN is compromised and serves malicious JavaScript, it runs on every page load for every visitor. Even for a joke site, serving malware is not funny. + +4. **We automated the detection.** The `check-sri` workflow runs daily, computes the remote script's hash, and opens a PR if it has changed. This narrows the window of broken analytics to at most 24 hours plus the time to merge the PR. + +## How the automated check works + +The `check-sri` workflow (`.github/workflows/check-sri.yaml`) runs daily at 6 AM UTC: + +1. Fetches `https://gc.zgo.at/count.js` with `curl`. +2. Computes the SHA-384 hash: `openssl dgst -sha384 -binary | openssl base64 -A`. +3. Extracts the current hash from `site/index.html` (source of truth). +4. If they match, the workflow exits successfully. +5. If they differ, it updates the `integrity` attribute in all 5 HTML files and creates a PR with the `sri-update` label. + +The PR includes the old and new hashes so you can verify the change before merging. You should review the PR rather than auto-merging it — the whole point of SRI is to verify before trusting, and blindly accepting a new hash defeats that purpose. + +## The alternative: removing SRI + +Some projects choose to load third-party scripts without SRI, accepting the trade-off in the other direction. This is a valid choice when: + +- The script changes frequently and breakage would be disruptive. +- The CDN is highly trusted (e.g., a first-party CDN you control). +- The site has a Content Security Policy (CSP) that limits what the script can do. + +W.R.A.A.S. has none of these conditions. GoatCounter's CDN is a third-party service, the script changes rarely, and the impact of a compromise outweighs the impact of stale analytics. + +## The Google Fonts exception + +The CSS file (`site/style.css`) imports Google Fonts without SRI. This is noted in a comment at the top of the file. Google Fonts responses vary by user-agent and browser capabilities — the same URL returns different CSS depending on who is requesting it. SRI is not feasible when the response is intentionally non-deterministic. + +## Further reading + +- [MDN: Subresource Integrity](https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity) +- [CI Workflows Reference](../reference/ci-workflows.md) — details on all workflows including `check-sri` diff --git a/docs/how-to/add-a-theme.md b/docs/how-to/add-a-theme.md new file mode 100644 index 0000000..48145a3 --- /dev/null +++ b/docs/how-to/add-a-theme.md @@ -0,0 +1,53 @@ +# How to Add a Themed Scenario + +This guide shows you how to add a new fake loading screen theme to W.R.A.A.S. + +## Prerequisites + +- Local dev environment set up (see [How to Run Locally](run-locally.md)) +- Familiarity with the theme fields (see [Theme Configuration Reference](../reference/theme-configuration.md)) + +## Steps + +### 1. Add the theme entry + +Add an entry to the `themes` object in `site/script.js`: + +```js +"/your-path": { + loadingText: "Your loading message...", + fakeDelay: 2000, + title: "Browser Tab Title", + desc: "Description for OG meta tag." +} +``` + +- `loadingText` — What the victim sees during the fake loading screen. +- `fakeDelay` — How long (in ms) to show the loading screen before the rickroll. Keep it between 1500–3000ms for believability. +- `title` — Sets the browser tab title while the loading screen is active. +- `desc` — Used in Open Graph meta tags when the link is shared on social platforms. + +### 2. Test locally + +```sh +just dev +# Open http://localhost:8000/your-path in a browser +``` + +Verify that: + +- The loading screen appears with your text +- The rickroll reveals after the specified delay +- The browser tab shows your title + +### 3. Add tests + +Add your new path to the E2E theme tests in `tests/e2e/themes.spec.js`. See [How to Run Tests](run-tests.md) for details on the test suite. + +### 4. Update the README + +Add your theme to the "Themed Scenarios" table in `README.md`. + +### 5. Submit a pull request + +See [How to Submit a Pull Request](submit-a-pr.md). diff --git a/docs/how-to/debug-ci-failures.md b/docs/how-to/debug-ci-failures.md new file mode 100644 index 0000000..b1ef24b --- /dev/null +++ b/docs/how-to/debug-ci-failures.md @@ -0,0 +1,100 @@ +# How to Debug CI Failures + +This guide shows you how to diagnose and fix failures in the W.R.A.A.S. CI workflows when they block your pull request. + +## Prerequisites + +- A pull request with a failing CI check +- Access to the GitHub Actions logs (click "Details" next to the failing check on the PR) + +## Identify which workflow failed + +Look at the checks section on your pull request. Each workflow reports independently: + +- **CI** — lint, external URLs, or OG metadata +- **Lighthouse CI** — performance, accessibility, best practices, or SEO scores below 90 +- **Check robots.txt** — invalid `robots.txt` syntax (only runs when `site/robots.txt` is modified) +- **Preview Build** — the site failed to build + +Most workflows comment on the PR with details. Check the PR comments first before digging into logs. + +## CI workflow failures + +### Lint failures + +The CI workflow runs `just lint`. If it fails: + +```sh +just lint +``` + +Fix any reported issues and push again. + +### External URL failures + +The CI workflow checks that all external URLs in `site/` are reachable. If a URL fails: + +1. Check if the URL is genuinely down (try it in a browser). +2. If it is a transient failure, re-run the workflow from the Actions tab. +3. If the URL is permanently broken, update or remove it from the source file mentioned in the PR comment. + +### OG metadata failures + +The CI workflow validates that `site/index.html` and `site/404.html` contain required Open Graph tags (`og:title`, `og:description`, `og:image`, `og:url`, `og:site_name`). If a tag is missing, add it back. + +## Lighthouse failures + +Lighthouse audits run on all key pages (`/`, `/about/`, `/generate/`, `/karaoke/`, `/404.html`). All categories must score 90 or above. + +The PR comment shows per-page scores. To reproduce locally: + +```sh +just build +cd _site && python3 -m http.server 8000 & +lighthouse http://localhost:8000/ --chrome-flags="--headless=new" --output=json --output-path=report.json +``` + +Common causes: +- **Performance** — large unoptimized assets, render-blocking resources +- **Accessibility** — missing ARIA labels, insufficient color contrast, missing alt text +- **Best Practices** — mixed content, deprecated APIs, console errors +- **SEO** — missing meta description, missing canonical URL + +## robots.txt failures + +This workflow only triggers on PRs that modify `site/robots.txt`. It validates: + +- Every `Allow`/`Disallow` directive has a preceding `User-agent` +- `Sitemap` values are valid URLs +- A catch-all `User-agent: *` rule exists + +Fix the syntax issues reported in the PR comment. + +## Preview Build failures + +If the preview build fails, the site did not build successfully: + +```sh +just build +``` + +Fix any build errors locally and push again. + +## Re-running a workflow + +If a failure was transient (network timeout, flaky external service): + +1. Go to the Actions tab on the repository. +2. Find the failed workflow run. +3. Click "Re-run failed jobs". + +## Troubleshooting + +### CI passes locally but fails in CI + +- Ensure you are running the same lint rules: `just lint`, not a local linter config. +- External URL checks may fail due to rate limiting or geo-blocking in CI. Re-run the workflow to confirm. + +### Lighthouse scores differ locally vs CI + +CI runs Lighthouse with DevTools throttling on a shared runner. Local scores on a fast machine will be higher. Optimize until CI passes, not just local. diff --git a/docs/how-to/run-locally.md b/docs/how-to/run-locally.md new file mode 100644 index 0000000..61d0549 --- /dev/null +++ b/docs/how-to/run-locally.md @@ -0,0 +1,57 @@ +# How to Run Locally + +This guide shows you how to set up and run the W.R.A.A.S. site on your local machine. + +## Prerequisites + +- Python 3 +- [just](https://github.com/casey/just) + +## Steps + +### 1. Start the dev server + +```sh +just dev +``` + +This serves the site at `http://localhost:8000`. The dev server mimics GitHub Pages 404 behavior — all unknown paths serve `404.html`, just like production. + +### 2. Verify the site + +Open `http://localhost:8000` in a browser. You should see the rickroll reveal with lyrics and the dancing GIF. + +Try a themed URL like `http://localhost:8000/meeting` to see a fake loading screen. + +### 3. Verify developer easter eggs + +```sh +# Check custom HTTP headers +curl -I http://localhost:8000 + +# Check humans.txt +curl http://localhost:8000/humans.txt + +# Check security.txt +curl http://localhost:8000/.well-known/security.txt +``` + +Open the browser DevTools console to see the ASCII art and styled banner. + +### 4. Install test dependencies (optional) + +If you plan to run the test suite: + +```sh +npm install +``` + +## Troubleshooting + +### Port already in use + +The dev server defaults to port 8000. If that port is taken, edit the `port` variable in the `justfile` or kill the existing process. + +### Python not found + +The dev server requires Python 3. Verify with `python3 --version`. On some systems, `python` may point to Python 2. diff --git a/docs/how-to/run-tests.md b/docs/how-to/run-tests.md new file mode 100644 index 0000000..382df12 --- /dev/null +++ b/docs/how-to/run-tests.md @@ -0,0 +1,89 @@ +# How to Run Tests + +This guide shows you how to run the W.R.A.A.S. test suite. + +## Prerequisites + +- Node.js +- Dependencies installed (`npm install`) + +## Run all tests + +```sh +npm test +``` + +This runs unit tests (Node.js built-in runner) followed by all Playwright tests. + +## Run specific test suites + +```sh +npm run test:unit # Unit tests only +npm run test:e2e # E2E tests (themes + generator) +npm run test:visual # Visual regression tests +npm run test:a11y # Accessibility audits +npm run test:all # All Playwright tests with HTML report +``` + +## Run a single test file + +```sh +npx playwright test tests/e2e/themes.spec.js +``` + +## Run a single test by name + +```sh +npx playwright test -g "meeting: shows fake loading" +``` + +## Run in headed mode + +See the browser while tests execute: + +```sh +npx playwright test --headed +``` + +## Run in debug mode + +Step through tests line by line: + +```sh +npx playwright test --debug +``` + +## Run in CI mode + +```sh +CI=1 npm test +``` + +This enables 2 retries and single-worker mode for consistency. + +## Update visual snapshots + +When UI changes are intentional, regenerate baseline screenshots: + +```sh +npx playwright test --update-snapshots +``` + +## Troubleshooting + +### Tests timeout + +- Check if the dev server starts correctly: `curl http://localhost:8080` +- Increase the default timeout in the test: `page.setDefaultTimeout(30000)` + +### Audio tests fail + +Web Audio API may be blocked in headless environments. The tests handle this gracefully, but if failures persist, run in headed mode to verify. + +### Visual tests differ + +Monitor resolution differences cause screenshot mismatches across machines. Use `--update-snapshots` to regenerate baselines for your environment. + +### Flaky tests + +Increase timeouts for animation-dependent assertions. In CI, the 2-retry policy handles most transient failures. diff --git a/docs/how-to/submit-a-pr.md b/docs/how-to/submit-a-pr.md new file mode 100644 index 0000000..67059e3 --- /dev/null +++ b/docs/how-to/submit-a-pr.md @@ -0,0 +1,45 @@ +# How to Submit a Pull Request + +This guide shows you how to submit a change to W.R.A.A.S. + +## Steps + +### 1. Fork and branch + +Fork the repository and create a branch from `main`: + +```sh +git checkout -b your-branch-name main +``` + +### 2. Make your changes + +Edit files and verify they work locally: + +```sh +just dev +``` + +### 3. Run checks + +```sh +# Lint HTML, CSS, and JS +just lint + +# Run the full test suite +npm test +``` + +Both must pass before submitting. + +### 4. Open a pull request + +Push your branch and open a pull request with a clear description of what your change does. + +CI will automatically run: + +- Lint checks (HTML, CSS, JS) +- External URL validation +- OG metadata verification +- Lighthouse performance audits +- Broken link checks diff --git a/docs/reference/ci-workflows.md b/docs/reference/ci-workflows.md new file mode 100644 index 0000000..13cf602 --- /dev/null +++ b/docs/reference/ci-workflows.md @@ -0,0 +1,144 @@ +# CI Workflows Reference + +Complete reference for the W.R.A.A.S. GitHub Actions workflows. For troubleshooting failures, see [How to Debug CI Failures](../how-to/debug-ci-failures.md). + +## Overview + +| Workflow | File | Trigger | Purpose | +|----------|------|---------|---------| +| CI | `ci.yaml` | PR, push to `main` | Lint, external URL checks, OG metadata validation | +| Lighthouse CI | `lighthouse.yaml` | PR, push to `main` | Performance, accessibility, best practices, SEO audits | +| Check robots.txt | `check-robots-txt.yaml` | PR (when `site/robots.txt` changes) | robots.txt syntax validation | +| Check SRI Hash | `check-sri.yaml` | Daily (6 AM UTC), manual | GoatCounter SRI integrity hash verification | +| Broken Links | `links.yaml` | Weekly (Monday 9 AM UTC), push to `main`, manual | Broken link detection across `site/` | +| Preview Build | `preview.yaml` | PR | Build artifact for PR preview | +| Deploy | `deploy.yaml` | Push to `main`, manual | Deploy to GitHub Pages | + +## CI (`ci.yaml`) + +**Triggers:** `pull_request`, `push` to `main` + +**Permissions:** `contents: read`, `pull-requests: write` + +**Steps:** + +1. Lint HTML, CSS, and JS via `just lint` +2. Extract and validate all external URLs in `site/` (excludes `youtube.com`, `lyricsondemand.com`, `goatcounter.com`, `wraas.github.io`) +3. Validate required OG metadata tags on `site/index.html` and `site/404.html` +4. On failure: comment on PR with details (updates existing comment if present) + +**Required OG tags:** `og:title`, `og:description`, `og:image`, `og:url`, `og:site_name` + +## Lighthouse CI (`lighthouse.yaml`) + +**Triggers:** `pull_request`, `push` to `main` + +**Permissions:** `contents: read`, `pull-requests: write` + +**Steps:** + +1. Build site via `just build` +2. Start a local Python HTTP server on port 8000 +3. Run Lighthouse on 5 pages: `/`, `/about/`, `/generate/`, `/karaoke/`, `/404.html` +4. Fail if any category (performance, accessibility, best practices) scores below 90 +5. Comment on PR with per-page scores (updates existing comment if present) + +**Lighthouse flags:** `--headless=new`, `--no-sandbox`, `--throttling-method=devtools`, `--disable-full-page-screenshot` + +## Check robots.txt (`check-robots-txt.yaml`) + +**Triggers:** `pull_request` (only when `site/robots.txt` is modified) + +**Permissions:** `pull-requests: write` + +**Steps:** + +1. Parse `site/robots.txt` line by line +2. Validate directive syntax: `User-agent`, `Allow`, `Disallow`, `Sitemap`, `Crawl-delay`, `Host` +3. Verify every `Allow`/`Disallow` has a preceding `User-agent` +4. Verify `Sitemap` values are valid URLs +5. Verify a catch-all `User-agent: *` rule exists +6. On failure: comment on PR with details + +## Check SRI Hash (`check-sri.yaml`) + +**Triggers:** `schedule` (daily at 6 AM UTC), `workflow_dispatch` + +**Permissions:** `contents: write`, `pull-requests: write` + +**Steps:** + +1. Fetch `https://gc.zgo.at/count.js` and compute its `sha384` hash +2. Extract the current SRI hash from `site/index.html` +3. If hashes match: exit successfully +4. If mismatched: update the `integrity` attribute in all 5 HTML files and create a PR + +**Updated files:** `site/index.html`, `site/404.html`, `site/about/index.html`, `site/karaoke/index.html`, `site/generate/index.html` + +**PR details:** branch `fix/update-goatcounter-sri`, label `sri-update` + +## Broken Links (`links.yaml`) + +**Triggers:** `schedule` (weekly, Monday at 9 AM UTC), `push` to `main`, `workflow_dispatch` + +**Permissions:** `contents: read`, `issues: write` + +**Steps:** + +1. Run lychee link checker on `site/` directory +2. Excludes: `youtube.com`, `lyricsondemand.com`, `localhost`, `127.0.0.1`, rickroll-related URLs +3. On failure: create an issue labeled `broken-links` (skips if one already exists) + +**Lychee config:** `--timeout 30`, `--max-retries 2` + +## Preview Build (`preview.yaml`) + +**Triggers:** `pull_request` + +**Permissions:** `contents: read`, `pull-requests: write` + +**Steps:** + +1. Build site via `just build` +2. Upload `_site` as artifact `preview-site-{PR number}` (retained 7 days) +3. Comment on PR with download instructions + +## Deploy (`deploy.yaml`) + +**Triggers:** `push` to `main`, `workflow_dispatch` + +**Permissions:** `contents: read`, `pages: write`, `id-token: write` + +**Concurrency:** group `pages`, does not cancel in-progress deploys + +**Steps:** + +1. Build site via `just build` +2. Upload `_site` as a Pages artifact +3. Deploy to the `github-pages` environment + +## Action versions + +All actions are pinned to commit SHAs for supply-chain security. + +| Action | Version | SHA | +|--------|---------|-----| +| `actions/checkout` | v6.0.2 | `de0fac2e4500dabe0009e67214ff5f5447ce83dd` | +| `actions/configure-pages` | v5.0.0 | `983d7736d9b0ae728b81ab479565c72886d7745b` | +| `actions/upload-pages-artifact` | v4.0.0 | `7b1f4a764d45c48632c6b24a0339c27f5614fb0b` | +| `actions/deploy-pages` | v4.0.5 | `d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e` | +| `actions/upload-artifact` | v4.6.2 | `ea165f8d65b6e75b540449e92b4886f43607fa02` | +| `actions/github-script` | v8.0.0 | `ed597411d8f924073f98dfc5c65a23a2325f34cd` | +| `taiki-e/install-action` | v2.69.2 | `42721ded7ddc3cd90f687527e8602066e4e1ff3a` | +| `lycheeverse/lychee-action` | v2.8.0 | `8646ba30535128ac92d33dfc9133794bfdd9b411` | +| `peter-evans/create-pull-request` | v8.1.0 | `c0f553fe549906ede9cf27b5156039d195d2ece0` | + +## Running workflows manually + +Workflows with `workflow_dispatch` can be triggered manually: + +1. Go to the Actions tab on the repository. +2. Select the workflow from the left sidebar. +3. Click "Run workflow" and select the branch. + +Manual dispatch is available on: **Deploy**, **Broken Links**, **Check SRI Hash**. diff --git a/docs/reference/test-suite.md b/docs/reference/test-suite.md new file mode 100644 index 0000000..20b13e3 --- /dev/null +++ b/docs/reference/test-suite.md @@ -0,0 +1,137 @@ +# Test Suite Reference + +Complete catalog of the W.R.A.A.S. test suite. For instructions on running tests, see [How to Run Tests](../how-to/run-tests.md). + +## Dependencies + +| Package | Purpose | +|---------|---------| +| `playwright` | Browser automation for E2E, visual, and accessibility tests | +| `@axe-core/playwright` | Automated WCAG 2.1 accessibility audits | + +No runtime dependencies. Unit tests use the Node.js built-in test runner. + +## Configuration + +**File:** `playwright.config.js` + +| Setting | Value | +|---------|-------| +| Base URL | `http://localhost:8080` | +| Web server | `python3 dev-server.py` (auto-started) | +| Browsers | Chromium, Firefox, WebKit | +| Retries (local) | 0 | +| Retries (CI) | 2 | +| Workers (CI) | 1 | +| Screenshots | On failure only, stored in `test-results/` | +| Traces | On first retry | + +### Viewport sizes + +| Profile | Dimensions | +|---------|-----------| +| Desktop | 1280 x 720 (default) | +| Mobile | 375 x 667 | +| Tablet | 768 x 1024 | + +## Test files + +### `tests/e2e/themes.spec.js` + +E2E tests for all themed scenarios. + +**Coverage:** +- All 8 theme variations (`/meeting`, `/document`, `/password-reset`, `/invoice`, `/survey`, `/clickbait`, `/karaoke`, `/about`) +- Base URL (no theme) — direct rickroll without fake loading +- Fake loading overlay visibility and text content +- Rickroll reveal timing and visibility +- Consent banner elements (Accept/Reject buttons) +- Skip button troll behavior +- Progress bar animation timing + +### `tests/e2e/generator.spec.js` + +Integration tests for the link generator (`/generate/`). + +**Coverage:** +- Scenario selection updates preview (title, description, pun) +- Custom path input (URL generation, keyword detection, path sanitization) +- Copy button (clipboard API, "Copied!" feedback, auto-revert after 2s) +- QR code canvas (dimensions, pixel data, updates on URL change) +- Suggestion chips (appear/disappear, clickable, fill input) +- URL output (base domain, dynamic updates) +- QR code download (PNG file, filename `rickroll-qr.png`) + +### `tests/unit/audio.test.js` + +Unit tests for audio synthesis (runs in-browser via Playwright). + +**Coverage:** +- Melody note frequencies (A4: 440 Hz, B4: 493.88 Hz, D5: 587.33 Hz, E5: 659.25 Hz, F#4: 369.99 Hz, E4: 329.63 Hz, D4: 293.66 Hz) +- 113 BPM timing calculations +- Mute/unmute button state and aria-label +- Consent banner triggers audio playback +- Audio context state management (suspended/running) +- Both Accept and Reject buttons trigger playback + +### `tests/visual/rickroll.spec.js` + +Visual regression tests using Playwright screenshot comparison. + +**Coverage:** +- Rickroll reveal stages (initial, lyrics visible, mute button visible) +- Mute button states (unmuted/muted icons) +- Consent banner layout +- Fake loading screens per theme +- Full page layout, generate page, about page +- Reduced motion layout (no animations) +- Responsive layouts (mobile, tablet, desktop) + +**Screenshot masks:** hides ` @@ -54,10 +64,10 @@ - - - - + + + +