From 2c0e1bda333389e6af471e78e5de1a3566025488 Mon Sep 17 00:00:00 2001 From: modem7 Date: Thu, 9 Jul 2026 17:25:47 +0100 Subject: [PATCH 1/2] Restart video from the beginning on reveal The video autoplays muted from t=0 on page load, so by the time someone actually clicks the decoy - anywhere from a second to minutes later depending how long it holds their attention - the audio unmutes wherever playback has already drifted to, often cutting off the intro entirely. Fixes #121 --- scripts/index/80-index.sh | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/scripts/index/80-index.sh b/scripts/index/80-index.sh index 4987921..45a02ed 100644 --- a/scripts/index/80-index.sh +++ b/scripts/index/80-index.sh @@ -348,6 +348,13 @@ tee /usr/share/nginx/html/index.html << EOF >/dev/null var events = ['click', 'keydown']; function reveal() { + // The video's been playing muted since page load, so by + // the time the decoy actually gets clicked - anywhere + // from a second to minutes later - it's already well + // past the intro. Jump back to the start so the payoff + // always lands on the actual song opening, not wherever + // playback happened to be. + video.currentTime = 0; video.muted = false; video.play().catch(function () {}); document.title = title; From b1d96656a223dfae57bc93e1008af82f72429435 Mon Sep 17 00:00:00 2001 From: modem7 Date: Thu, 9 Jul 2026 17:29:38 +0100 Subject: [PATCH 2/2] Cache CI downloads and test the restart-on-reveal fix - Cache Playwright's browser binaries (the slow part of installing it) across runs, keyed on a pinned exact version instead of a floating playwright@1. OS-level deps still install fresh every run since Actions runners aren't persistent, but that's fast on ubuntu-latest. - Cache the Docker build's video-fetch stage via GHA cache, same as ghcr-publish.yml already does - this workflow always builds the same default VIDEO_URL, so there's no reason to re-download it every run. - Extend test-autoplay.js to let muted autoplay run for a few seconds before clicking, then assert currentTime resets to near 0 after the click - regression coverage for the restart-on-reveal fix earlier in this PR. --- .github/scripts/test-autoplay.js | 17 +++++++++++++++ .github/workflows/test.yml | 36 +++++++++++++++++++++++++------- 2 files changed, 46 insertions(+), 7 deletions(-) diff --git a/.github/scripts/test-autoplay.js b/.github/scripts/test-autoplay.js index b4fd009..4bbbec4 100644 --- a/.github/scripts/test-autoplay.js +++ b/.github/scripts/test-autoplay.js @@ -72,6 +72,17 @@ async function waitFor(page, predicate, { timeout = 5000, interval = 100 } = {}) throw new Error(`expected the cookie banner to be visible alongside ${overlayType}, but it wasn't`); } + // Let the muted autoplay run for a few seconds before clicking, so + // currentTime has clearly moved past the intro - this is the whole + // point of the check below: a real visitor rarely clicks instantly, + // and the payoff should still start from the beginning regardless of + // how long the decoy held their attention. + await page.waitForTimeout(3000); + const beforeClickTime = await page.evaluate(() => document.getElementById('video').currentTime); + if (beforeClickTime < 1) { + throw new Error(`expected currentTime to have advanced past 1s after 3s of muted autoplay, got ${beforeClickTime}`); + } + await page.click(clickSelector); const unmuted = await waitFor(page, () => { @@ -88,6 +99,7 @@ async function waitFor(page, predicate, { timeout = 5000, interval = 100 } = {}) const cookie = document.querySelector(cookieSel); return { paused: v.paused, + currentTime: v.currentTime, title: document.title, overlayVisible: overlay ? !overlay.classList.contains('hidden') : null, cookieVisible: cookie ? !cookie.classList.contains('hidden') : null @@ -97,6 +109,11 @@ async function waitFor(page, predicate, { timeout = 5000, interval = 100 } = {}) if (after.paused) { throw new Error('expected video to still be playing after unmuting'); } + // The whole point of a rickroll is the intro - a visitor who takes a + // while to click shouldn't hear the song mid-way through instead. + if (after.currentTime >= 1) { + throw new Error(`expected video to restart from the beginning on reveal, but currentTime was ${after.currentTime}s right after clicking (was ${beforeClickTime}s before)`); + } if (after.title !== 'Rickroll') { throw new Error(`expected title "Rickroll" after interaction, got "${after.title}"`); } diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 82a34bb..aa55ef8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -34,6 +34,11 @@ jobs: file: ./Dockerfile load: true tags: rickroll:test + # Same video-fetch stage as ghcr-publish.yml, and this + # workflow always builds the same default VIDEO_URL - caching + # it means CI stops re-downloading the video on every run. + cache-from: type=gha,scope=test + cache-to: type=gha,mode=max,scope=test - name: Start container with default env vars run: docker run -d --name rickroll-default -p 8080:8080 rickroll:test @@ -157,15 +162,32 @@ jobs: with: node-version: 24 - - name: Install Playwright + - name: Install Playwright package run: | npm init -y >/dev/null - npm install --no-save playwright@1 - # Firefox gets its own engine check alongside Chromium - it's - # historically the most divergent on autoplay/gesture policy - # enforcement of the mainstream browsers, so a bug that's fine - # under one can still be broken under the other. - npx playwright install --with-deps chromium firefox + # Pinned exact version, not a floating "playwright@1" - the + # cache key below is keyed on this version, and an unpinned + # install could silently resolve a newer release whose browser + # binaries no longer match a stale cache. + npm install --no-save playwright@1.61.1 + + - name: Cache Playwright browsers + id: playwright-cache + uses: actions/cache@v4 + with: + path: ~/.cache/ms-playwright + key: playwright-browsers-${{ runner.os }}-1.61.1 + + - name: Install Playwright OS dependencies + run: npx playwright install-deps chromium firefox + + - name: Install Playwright browsers + if: steps.playwright-cache.outputs.cache-hit != 'true' + # Firefox gets its own engine check alongside Chromium - it's + # historically the most divergent on autoplay/gesture policy + # enforcement of the mainstream browsers, so a bug that's fine + # under one can still be broken under the other. + run: npx playwright install chromium firefox - name: Start container with OVERLAY=error run: docker run -d --name rickroll-error -p 8080:8080 -e OVERLAY=error rickroll:test