From c991011d534dd5066b3d0174185b60feb73e46fd Mon Sep 17 00:00:00 2001 From: John McCall Date: Wed, 17 Jun 2026 09:35:00 -0400 Subject: [PATCH 01/25] feat: add axe-core WCAG 2.2 AA accessibility tests to CI - Add __tests__/a11y.test.mjs: Node built-in test runner + @axe-core/playwright against a minimal static server on the Docusaurus build output - Add .github/workflows/a11y.yml: dedicated Accessibility CI job on every PR - Add test:a11y npm script - Install @axe-core/playwright and playwright as devDependencies Closes #404 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: John McCall --- .github/workflows/a11y.yml | 43 ++++++++++++++++ __tests__/a11y.test.mjs | 102 +++++++++++++++++++++++++++++++++++++ package-lock.json | 72 ++++++++++++++++++++++++++ package.json | 3 ++ 4 files changed, 220 insertions(+) create mode 100644 .github/workflows/a11y.yml create mode 100644 __tests__/a11y.test.mjs diff --git a/.github/workflows/a11y.yml b/.github/workflows/a11y.yml new file mode 100644 index 00000000..0b9f2292 --- /dev/null +++ b/.github/workflows/a11y.yml @@ -0,0 +1,43 @@ +--- +name: Accessibility + +on: + pull_request: + workflow_dispatch: + +concurrency: + group: a11y-${{ github.event.number || github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + a11y: + name: WCAG 2.2 AA + runs-on: ubuntu-latest + steps: + - name: Checkout repo + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false + + - name: Set up Node.js + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version-file: package.json + + - name: Configure sustainable npm + uses: lowlydba/sustainable-npm@31d51025884f424f58f22e4e6578178bb4e79632 # v3.0.0 + + - name: Install NPM dependencies + run: npm ci + + - name: Build + run: npm run build + + - name: Install Playwright browser + run: npx playwright install chromium --with-deps + + - name: Run accessibility tests + run: npm run test:a11y diff --git a/__tests__/a11y.test.mjs b/__tests__/a11y.test.mjs new file mode 100644 index 00000000..df614e12 --- /dev/null +++ b/__tests__/a11y.test.mjs @@ -0,0 +1,102 @@ +/** + * WCAG 2.2 AA accessibility tests using Node's built-in test runner. + * Requires a built `build/` directory — run `npm run build` first. + * Run with: npm run test:a11y + */ + +import { test, before, after } from 'node:test'; +import assert from 'node:assert/strict'; +import { createServer } from 'node:http'; +import { readFile, access } from 'node:fs/promises'; +import { extname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { chromium } from 'playwright'; +import AxeBuilder from '@axe-core/playwright'; + +const __dirname = fileURLToPath(new URL('.', import.meta.url)); +const BUILD_DIR = join(__dirname, '..', 'build'); +const PORT = 3778; +const BASE_URL = `http://localhost:${PORT}`; + +const MIME = { + '.html': 'text/html', + '.js': 'application/javascript', + '.mjs': 'application/javascript', + '.css': 'text/css', + '.png': 'image/png', + '.svg': 'image/svg+xml', + '.woff2': 'font/woff2', + '.json': 'application/json', +}; + +let server, browser; + +before(async () => { + await access(join(BUILD_DIR, 'index.html')).catch(() => { + throw new Error(`build/index.html not found — run 'npm run build' before running accessibility tests`); + }); + + // ponytail: minimal static file server, falls back to index.html for Docusaurus client-side routing + server = createServer(async (req, res) => { + const urlPath = req.url.split('?')[0]; + const filePath = join(BUILD_DIR, urlPath === '/' ? 'index.html' : urlPath); + try { + const data = await readFile(filePath); + res.writeHead(200, { 'Content-Type': MIME[extname(filePath)] ?? 'application/octet-stream' }); + res.end(data); + } catch { + try { + const data = await readFile(join(BUILD_DIR, 'index.html')); + res.writeHead(200, { 'Content-Type': 'text/html' }); + res.end(data); + } catch { + res.writeHead(404); + res.end('Not found'); + } + } + }); + await new Promise((resolve) => server.listen(PORT, resolve)); + browser = await chromium.launch(); +}); + +after(async () => { + await browser?.close(); + await new Promise((resolve) => server.close(resolve)); +}); + +function formatViolations(violations, label) { + if (violations.length === 0) return; + const detail = violations + .map((v) => + `[${v.impact}] ${v.id}: ${v.description}\n` + + v.nodes.map((n) => ` • ${n.target}`).join('\n') + ) + .join('\n\n'); + assert.fail(`${violations.length} WCAG 2.2 AA violation(s) — ${label}:\n\n${detail}`); +} + +async function runAxe(url) { + const context = await browser.newContext(); + const page = await context.newPage(); + try { + await page.goto(`${BASE_URL}${url}`, { waitUntil: 'domcontentloaded' }); + await page.waitForSelector('main', { timeout: 10000 }); + const { violations } = await new AxeBuilder({ page }) + .withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa', 'wcag22aa']) + .analyze(); + return violations; + } finally { + await context.close(); + } +} + +const PAGES = [ + { path: '/', label: 'homepage' }, + { path: '/docs/', label: 'docs index' }, +]; + +for (const { path, label } of PAGES) { + test(`${label} passes WCAG 2.2 AA`, async () => { + formatViolations(await runAxe(path), label); + }); +} diff --git a/package-lock.json b/package-lock.json index 9f28f54b..abd5e23c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ "react-dom": "^19.2.7" }, "devDependencies": { + "@axe-core/playwright": "^4.11.3", "@docusaurus/module-type-aliases": "^3.10.0", "@docusaurus/types": "^3.10.0", "@eslint/js": "^10.0.0", @@ -36,6 +37,7 @@ "globals": "^17.6.0", "jsdom": "^29.1.1", "markdownlint-cli": "^0.48.0", + "playwright": "^1.61.0", "prettier": "^3.8.4", "typescript": "^6.0.3", "vitest": "^4.1.8" @@ -471,6 +473,19 @@ "dev": true, "license": "MIT" }, + "node_modules/@axe-core/playwright": { + "version": "4.11.3", + "resolved": "https://registry.npmjs.org/@axe-core/playwright/-/playwright-4.11.3.tgz", + "integrity": "sha512-h/kfksv4F0cVIDlKpT4700OehdRgpvuVskuQ2nb7/JmtWUXpe9ftHAPtwyXGvVSsa6SJ64A9ER7Zrzc/sIvC4w==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "axe-core": "~4.11.4" + }, + "peerDependencies": { + "playwright-core": ">= 1.0.0" + } + }, "node_modules/@babel/code-frame": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", @@ -8179,6 +8194,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/axe-core": { + "version": "4.11.4", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.4.tgz", + "integrity": "sha512-KunSNx+TVpkAw/6ULfhnx+HWRecjqZGTOyquAoWHYLRSdK1tB5Ihce1ZW+UY3fj33bYAFWPu7W/GRSmmrCGuxA==", + "dev": true, + "license": "MPL-2.0", + "engines": { + "node": ">=4" + } + }, "node_modules/babel-loader": { "version": "9.2.1", "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.2.1.tgz", @@ -17949,6 +17974,53 @@ "node": ">=16.0.0" } }, + "node_modules/playwright": { + "version": "1.61.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.0.tgz", + "integrity": "sha512-Z+7BeeqQPRRzklHsVFP4KTGIyMxKUmfeRA4WisM6G3/XW6nwGeX6fX9qYaDa+CiUqpOkb2f6X3nar05R3kSuJQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.61.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.61.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.0.tgz", + "integrity": "sha512-caX7TrY3Ml6egyDX0WUcTHDxodl/b51y5wJOdCEA36QviK/s2g081hvmGs8eaE3DWb6NYZQ6BjO/QkNRPenoPA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/pmtiles": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/pmtiles/-/pmtiles-4.4.1.tgz", diff --git a/package.json b/package.json index e63d3f72..4b2074d5 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "format": "prettier --write .", "format:check": "prettier --check .", "test": "vitest run", + "test:a11y": "node --test --test-reporter=spec __tests__/a11y.test.mjs", "test:watch": "vitest", "typecheck": "tsc --noEmit" }, @@ -40,6 +41,7 @@ "react-dom": "^19.2.7" }, "devDependencies": { + "@axe-core/playwright": "^4.11.3", "@docusaurus/module-type-aliases": "^3.10.0", "@docusaurus/types": "^3.10.0", "@eslint/js": "^10.0.0", @@ -52,6 +54,7 @@ "globals": "^17.6.0", "jsdom": "^29.1.1", "markdownlint-cli": "^0.48.0", + "playwright": "^1.61.0", "prettier": "^3.8.4", "typescript": "^6.0.3", "vitest": "^4.1.8" From 28f85f86c6a73c858264d4c662e046f45ed96d6c Mon Sep 17 00:00:00 2001 From: John McCall Date: Wed, 17 Jun 2026 09:36:37 -0400 Subject: [PATCH 02/25] docs: add WCAG 2.2 AA badge to README Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: John McCall --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index edb45b31..d8ca86d6 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # Overture Documentation +[![WCAG 2.2 AA](https://img.shields.io/badge/WCAG_2.2-AA-green)](https://www.w3.org/WAI/WCAG22/quickref/) + This repository uses [Docusaurus](https://docusaurus.io/) to publish the documentation pages seen at [docs.overturemaps.org](https://docs.overturemaps.org) ## Structure From 1b9b2d9c3913a03bd8ea5366396e2807367c0180 Mon Sep 17 00:00:00 2001 From: John McCall Date: Wed, 17 Jun 2026 09:40:35 -0400 Subject: [PATCH 03/25] fix: resolve 3 WCAG 2.2 AA violations caught by axe CI - home.module.css: darken .featuredIcon from #f97316 (2.8:1) to #c2410c (5.1:1 on white) to pass the 4.5:1 AA contrast threshold - home.module.css: underline inline links in .featuredDesc paragraphs so they're distinguishable without relying on color (WCAG 1.4.1) - docusaurus.config.js: add aria-label to github-link navbar icon so the link has discernible text (WCAG 4.1.2) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: John McCall --- docusaurus.config.js | 1 + src/components/home.module.css | 9 ++++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/docusaurus.config.js b/docusaurus.config.js index f12b2ce2..bfd801fa 100644 --- a/docusaurus.config.js +++ b/docusaurus.config.js @@ -225,6 +225,7 @@ const config = { position: 'right', target: '_blank', className: 'github-link', + 'aria-label': 'View source on GitHub', }, ], }, diff --git a/src/components/home.module.css b/src/components/home.module.css index ce496ef2..9016411c 100644 --- a/src/components/home.module.css +++ b/src/components/home.module.css @@ -32,7 +32,8 @@ .featuredIcon { display: inline-block; - color: #f97316; + /* ponytail: #c2410c is orange-700, 5.1:1 on white — passes WCAG AA (4.5:1) */ + color: #c2410c; margin-right: 0.4em; font-style: normal; } @@ -50,6 +51,12 @@ line-height: 1.6; } +/* Underline inline links so they're distinguishable without relying on color (WCAG 1.4.1) */ +.featuredDesc p a { + text-decoration: underline; + text-underline-offset: 2px; +} + /* ── SECTIONS ── */ .docSection { max-width: 760px; From e0939427fa73738a62ef17776b27be167ab1d763 Mon Sep 17 00:00:00 2001 From: John McCall Date: Wed, 17 Jun 2026 09:43:06 -0400 Subject: [PATCH 04/25] fix: prevent path traversal in a11y test static server (CodeQL js/path-injection) Resolve req.url against BUILD_DIR and reject any path that escapes it before passing it to readFile. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: John McCall --- __tests__/a11y.test.mjs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/__tests__/a11y.test.mjs b/__tests__/a11y.test.mjs index df614e12..dc6b0831 100644 --- a/__tests__/a11y.test.mjs +++ b/__tests__/a11y.test.mjs @@ -8,7 +8,7 @@ import { test, before, after } from 'node:test'; import assert from 'node:assert/strict'; import { createServer } from 'node:http'; import { readFile, access } from 'node:fs/promises'; -import { extname, join } from 'node:path'; +import { extname, join, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import { chromium } from 'playwright'; import AxeBuilder from '@axe-core/playwright'; @@ -39,7 +39,12 @@ before(async () => { // ponytail: minimal static file server, falls back to index.html for Docusaurus client-side routing server = createServer(async (req, res) => { const urlPath = req.url.split('?')[0]; - const filePath = join(BUILD_DIR, urlPath === '/' ? 'index.html' : urlPath); + const filePath = resolve(join(BUILD_DIR, urlPath === '/' ? 'index.html' : urlPath)); + if (!filePath.startsWith(BUILD_DIR + '/') && filePath !== BUILD_DIR) { + res.writeHead(403); + res.end('Forbidden'); + return; + } try { const data = await readFile(filePath); res.writeHead(200, { 'Content-Type': MIME[extname(filePath)] ?? 'application/octet-stream' }); From 81c1e824b0ae10b7acdbe5cc0e01de9fb0dc2a9c Mon Sep 17 00:00:00 2001 From: John McCall Date: Wed, 17 Jun 2026 09:45:40 -0400 Subject: [PATCH 05/25] chore: move CODEOWNERS and SECURITY.md to .github/ GitHub recognizes both files in root, .github/, or docs/. No functional change. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: John McCall --- CODEOWNERS => .github/CODEOWNERS | 0 SECURITY.md => .github/SECURITY.md | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename CODEOWNERS => .github/CODEOWNERS (100%) rename SECURITY.md => .github/SECURITY.md (100%) diff --git a/CODEOWNERS b/.github/CODEOWNERS similarity index 100% rename from CODEOWNERS rename to .github/CODEOWNERS diff --git a/SECURITY.md b/.github/SECURITY.md similarity index 100% rename from SECURITY.md rename to .github/SECURITY.md From bce69b725c9db658acc37886773d074c62e9a846 Mon Sep 17 00:00:00 2001 From: John McCall Date: Wed, 17 Jun 2026 09:54:14 -0400 Subject: [PATCH 06/25] feat: add DuckDB query smoke tests to CI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the stale root test_queries.py (which only linted and had a broken release-version regex) with a proper execution test. scripts/test_queries.py: - Fetches current release from the STAC catalog (falls back to a hardcoded version on failure) - Walks src/queries/duckdb/*.sql - Single-statement / COPY-wrapper queries: runs as SELECT * FROM (...) LIMIT 1 to validate S3 paths and schema with minimal data transfer - Multi-statement scripts (SET VARIABLE / CREATE TABLE chains): runs in full inside a temp directory so any COPY...TO output is cleaned up - Exits non-zero on any failure .github/workflows/query-tests.yml: - Runs on every PR and on a Monday 06:00 UTC schedule (catches data-side breakage between releases without a code change) Athena/Synapse/Snowflake queries are intentionally excluded — they require credentials that live outside this repo and are already covered by sqlfluff linting in lint.yml. Relates to: https://github.com/OvertureMaps/tf-data-platform/issues/256 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: John McCall --- .github/workflows/query-tests.yml | 36 ++++++++ scripts/test_queries.py | 144 ++++++++++++++++++++++++++++++ test_queries.py | 32 ------- 3 files changed, 180 insertions(+), 32 deletions(-) create mode 100644 .github/workflows/query-tests.yml create mode 100644 scripts/test_queries.py delete mode 100644 test_queries.py diff --git a/.github/workflows/query-tests.yml b/.github/workflows/query-tests.yml new file mode 100644 index 00000000..6f590618 --- /dev/null +++ b/.github/workflows/query-tests.yml @@ -0,0 +1,36 @@ +--- +name: Query Tests + +on: + pull_request: + schedule: + - cron: '0 6 * * 1' # Monday 06:00 UTC — catches data-side breakage between releases + workflow_dispatch: + +concurrency: + group: query-tests-${{ github.event.number || github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + duckdb: + name: DuckDB query smoke tests + runs-on: ubuntu-latest + steps: + - name: Checkout repo + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false + + - name: Set up Python + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v6.2.0 + with: + python-version: '3.12' + + - name: Install duckdb + run: pip install duckdb + + - name: Run query smoke tests + run: python scripts/test_queries.py diff --git a/scripts/test_queries.py b/scripts/test_queries.py new file mode 100644 index 00000000..9fb74f71 --- /dev/null +++ b/scripts/test_queries.py @@ -0,0 +1,144 @@ +#!/usr/bin/env python3 +""" +Execute every DuckDB query in src/queries/duckdb/ against live Overture S3 data. + +Single-statement queries (SELECT / COPY wrappers) run as SELECT * FROM (...) LIMIT 1 +to validate paths and schema with minimal data transfer. + +Multi-statement scripts (SET VARIABLE, CREATE TABLE chains) run in full inside a +temporary directory so any COPY ... TO 'file' output is cleaned up automatically. + +Exits non-zero on any failure. + +Usage: + pip install duckdb + python scripts/test_queries.py +""" + +import json +import os +import re +import sys +import tempfile +from pathlib import Path +from urllib.request import urlopen + +import duckdb + +STAC_URL = "https://stac.overturemaps.org/catalog.json" +FALLBACK_RELEASE = "2026-05-20.0" +QUERIES_DIR = Path(__file__).parent.parent / "src" / "queries" / "duckdb" + +# Lines handled by the shared connection setup — strip before classification +_PREAMBLE = re.compile( + r"^\s*(LOAD|INSTALL)\s+\S+\s*;?\s*$" + r"|^\s*SET\s+s3_region\s*=\s*[^;]+;\s*$", + re.IGNORECASE | re.MULTILINE, +) + +# COPY() TO '' [WITH (...)] — unwrap to just the inner SELECT +# COPY( ) TO — split into prefix / inner / suffix so the inner +# SELECT can be wrapped in LIMIT 0. Greedy inner backtracks to the last ") TO". +_COPY_STMT = re.compile( + r"\A((?:\s*--[^\n]*\n)*\s*COPY\s*\()(.+)(\)\s+TO\s+.+)\Z", + re.DOTALL | re.IGNORECASE, +) + + +def limit_copy(stmt: str) -> str: + """ + Wrap a COPY's inner SELECT in LIMIT 0 so the file is still written (correct + schema, zero rows) without scanning S3. Non-COPY statements pass through, so + SET/CREATE steps that resolve variables still run for real. + """ + m = _COPY_STMT.match(stmt) + if not m: + return stmt + prefix, inner, suffix = m.groups() + return f"{prefix}SELECT * FROM (\n{inner}\n) _q LIMIT 0{suffix}" + def fetch_release() -> str: try: @@ -98,7 +118,7 @@ def run_multi(con: duckdb.DuckDBPyConnection, sql: str) -> None: os.chdir(tmpdir) try: for stmt in split_statements(sql): - con.execute(stmt) + con.execute(limit_copy(stmt)) finally: os.chdir(prev) From eb46c7aa64b57d1dcb015f5e7d19a69120e2b303 Mon Sep 17 00:00:00 2001 From: John McCall Date: Wed, 17 Jun 2026 12:10:26 -0400 Subject: [PATCH 25/25] fix(a11y): harden static server and dedupe Tabs default Address Copilot review feedback: - Guard req.url (can be undefined) before split. - SPA-fallback to index.html only for route-like (extensionless) requests so missing JS/CSS/image assets return 404 instead of masking broken builds. - Guard server.close() in after() so a failing before() hook surfaces its real error instead of crashing on an undefined server. - Keep 'default' on only the first TabItem in the divisions counts block (Docusaurus expects at most one default tab). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: John McCall --- __tests__/a11y.test.mjs | 22 +++++++++++++--------- docs/guides/divisions/index.mdx | 10 +++++----- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/__tests__/a11y.test.mjs b/__tests__/a11y.test.mjs index 7822827c..03bf94c1 100644 --- a/__tests__/a11y.test.mjs +++ b/__tests__/a11y.test.mjs @@ -38,7 +38,7 @@ before(async () => { // minimal static file server, falls back to index.html for Docusaurus client-side routing server = createServer(async (req, res) => { - const urlPath = req.url.split('?')[0]; + const urlPath = (req.url ?? '/').split('?')[0]; const filePath = resolve(join(BUILD_DIR, urlPath === '/' ? 'index.html' : urlPath)); // relative() is cross-platform; startsWith(BUILD_DIR + '/') broke on Windows backslashes const rel = relative(BUILD_DIR, filePath); @@ -52,14 +52,18 @@ before(async () => { res.writeHead(200, { 'Content-Type': MIME[extname(filePath)] ?? 'application/octet-stream' }); res.end(data); } catch { - try { - const data = await readFile(join(BUILD_DIR, 'index.html')); - res.writeHead(200, { 'Content-Type': 'text/html' }); - res.end(data); - } catch { - res.writeHead(404); - res.end('Not found'); + // SPA fallback only for route-like requests (no extension); a missing + // asset (.js/.css/.png) must 404 so broken builds aren't masked. + if (extname(filePath) === '') { + try { + const data = await readFile(join(BUILD_DIR, 'index.html')); + res.writeHead(200, { 'Content-Type': 'text/html' }); + res.end(data); + return; + } catch { /* fall through to 404 */ } } + res.writeHead(404); + res.end('Not found'); } }); await new Promise((resolve) => server.listen(PORT, resolve)); @@ -68,7 +72,7 @@ before(async () => { after(async () => { await browser?.close(); - await new Promise((resolve) => server.close(resolve)); + if (server) await new Promise((resolve) => server.close(resolve)); }); function formatViolations(violations, label) { diff --git a/docs/guides/divisions/index.mdx b/docs/guides/divisions/index.mdx index 9a405cbe..2eefdb37 100644 --- a/docs/guides/divisions/index.mdx +++ b/docs/guides/divisions/index.mdx @@ -261,23 +261,23 @@ Using these queries, you can get counts for each feature type in divisions. - + - + - + - + - +