diff --git a/.github/workflows/health-check.yml b/.github/workflows/health-check.yml index 4c0717d..2db7f05 100644 --- a/.github/workflows/health-check.yml +++ b/.github/workflows/health-check.yml @@ -17,6 +17,7 @@ jobs: linkinator: name: Crawl live sites for broken links runs-on: ubuntu-latest + timeout-minutes: 45 steps: - name: Setup Node uses: actions/setup-node@v4 diff --git a/404.html b/404.html index a9244c3..394ae83 100644 --- a/404.html +++ b/404.html @@ -2,6 +2,7 @@ + diff --git a/tests/not-found.spec.js b/tests/not-found.spec.js new file mode 100644 index 0000000..ba61f9d --- /dev/null +++ b/tests/not-found.spec.js @@ -0,0 +1,33 @@ +// @ts-check +const { test, expect } = require('@playwright/test'); + +const BASE = process.env.BASE_URL || 'http://localhost:8080'; +const MISSING = '/no/such/page/'; + +// GitHub Pages serves /404.html at whatever path was requested, so the page's +// stylesheet, scripts and links must resolve from the site root. Relative +// URLs resolved under the missing path instead: every asset 404'd, and a link +// checker following them generated ever-deeper missing paths without end. +test.describe('404 page served at a nested missing path', () => { + test('its assets load and its links resolve from the site root', async ({ page }) => { + const failed = []; + page.on('response', (r) => { + const url = r.url(); + if (url.startsWith(BASE) && r.status() >= 400 && new URL(url).pathname !== MISSING) { + failed.push(`${r.status()} ${url}`); + } + }); + const res = await page.goto(`${BASE}${MISSING}`); + expect(res && res.status()).toBe(404); + await page.waitForLoadState('networkidle'); + expect(failed).toEqual([]); + + const internal = await page.$$eval('a[href]', (as) => + as.map((a) => a.href).filter((h) => h.startsWith(location.origin)), + ); + expect(internal.length).toBeGreaterThan(0); + for (const href of internal) { + expect(new URL(href).pathname.startsWith(MISSING), href).toBe(false); + } + }); +});