From 83c34428de845a48b520606bf3f6279df77c584d Mon Sep 17 00:00:00 2001 From: jdalton Date: Tue, 22 Sep 2026 13:57:26 -0400 Subject: [PATCH] test: pin the v2 WPT harness --- .gitignore | 1 + .gitmodules | 9 +++++ package.json | 5 ++- test/wpt/router.php | 21 ++++++----- test/wpt/wpt-launcher.mjs | 78 +++++++++++++++++++++++++++++++++++++++ test/wpt/wpt.sh | 42 ++------------------- 6 files changed, 108 insertions(+), 48 deletions(-) create mode 100644 .gitmodules create mode 100644 test/wpt/wpt-launcher.mjs diff --git a/.gitignore b/.gitignore index 23d67fc1..821c53e0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ node_modules/ yarn.lock +upstream/ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..88c6ce44 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,9 @@ +# wpt-fd98377 sha256:b00e229cb3ed89596d68d22c488abce63c18f308e172f5182d44e26465cf4a10 +[submodule "upstream/wpt"] + ignore = dirty + ref = fd983776a7cd19ebcda7a2bcb69c74330ee5d8c9 + path = upstream/wpt + url = https://github.com/web-platform-tests/wpt.git + branch = master + shallow = true + sparse-checkout = common css/css-conditional/js css/css-forms/parsing css/css-highlight-api css/css-multicol/parsing css/css-overflow/parsing css/css-pseudo/parsing css/css-shadow css/css-view-transitions/parsing css/selectors css/support custom-elements dom/nodes fullscreen/rendering html/browsers/browsing-the-web/scroll-to-fragid html/dom/elements/global-attributes html/resources html/semantics/forms html/semantics/popovers html/semantics/sections html/semantics/selectors inert quirks resources shadow-dom webvtt/api diff --git a/package.json b/package.json index 01b4f65e..16835d74 100644 --- a/package.json +++ b/package.json @@ -38,8 +38,11 @@ "url": "https://github.com/dperini/nwsapi.git" }, "scripts": { + "lint": "eslint ./src/nwsapi.js", "test": "node --test test/repo/integration/maintenance.test.mts", - "lint": "eslint ./src/nwsapi.js" + "wpt:serve": "node test/wpt/wpt-launcher.mjs serve", + "wpt:setup": "node test/wpt/wpt-launcher.mjs setup", + "wpt:verify": "node test/wpt/wpt-launcher.mjs verify" }, "devDependencies": { "acorn": "8.15.0", diff --git a/test/wpt/router.php b/test/wpt/router.php index 09b18e0f..ffd3d74f 100644 --- a/test/wpt/router.php +++ b/test/wpt/router.php @@ -31,9 +31,20 @@ function browserRoot(string $nwsapiRoot): string return $resolved; } -$nwsapiRoot = dirname(__DIR__, 2); +$nwsapiRoot = getenv('NWSAPI_ROOT') ?: dirname(__DIR__, 2); $browserRoot = browserRoot($nwsapiRoot); $requestPath = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH) ?: '/'; + +if ($requestPath === '/') { + header('Content-Type: text/html; charset=UTF-8'); + readfile(__DIR__ . DIRECTORY_SEPARATOR . 'index.html'); + exit; +} +if ($requestPath === '/favicon.svg') { + header('Content-Type: image/svg+xml'); + readfile(__DIR__ . DIRECTORY_SEPARATOR . 'favicon.svg'); + exit; +} $decodedPath = rawurldecode($requestPath); if (!str_starts_with($decodedPath, '/')) { @@ -60,14 +71,6 @@ function browserRoot(string $nwsapiRoot): string $realRequested = realpath($requestedFile); $isInsideBrowserRoot = $realRequested !== false && ($realRequested === $browserRoot || str_starts_with($realRequested, $rootPrefix)); -// A manually entered directory or one reached from a link always shows -// its corresponding tree without redirects: Back/Forward work normally. -if ($isInsideBrowserRoot && is_dir($realRequested)) { - $_SERVER['WPT_BROWSER_PATH'] = $relativePath; - require $browserRoot . DIRECTORY_SEPARATOR . 'wpt-browser.php'; - exit; -} - // Let the PHP server handle every file other than the harness. if (!preg_match('~(?:^|/)resources/testharness\.js$~', $requestPath)) { return false; diff --git a/test/wpt/wpt-launcher.mjs b/test/wpt/wpt-launcher.mjs new file mode 100644 index 00000000..586f1d34 --- /dev/null +++ b/test/wpt/wpt-launcher.mjs @@ -0,0 +1,78 @@ +#!/usr/bin/env node +import { execFileSync, spawnSync } from 'node:child_process' +import { createHash } from 'node:crypto' +import { existsSync, mkdirSync, readFileSync } from 'node:fs' +import path from 'node:path' +import process from 'node:process' +import { fileURLToPath } from 'node:url' + +const root = fileURLToPath(new URL('../../', import.meta.url)) +const maxBuffer = 512 * 1024 * 1024 +const modules = path.join(root, '.gitmodules') +const prefix = 'submodule.upstream/wpt.' +const get = key => execFileSync('git', ['config', '--file', modules, '--get', prefix + key], { encoding: 'utf8' }).trim() +const config = { path: get('path'), ref: get('ref'), url: get('url'), branch: get('branch'), sparse: get('sparse-checkout').split(/\s+/) } +const manifest = readFileSync(modules, 'utf8').match(/^#\s+wpt-[\w-]+\s+sha256:([0-9a-f]{64})$/m)?.[1] +const upstream = path.join(root, config.path) + +if ( + config.path !== 'upstream/wpt' || + !/^[0-9a-f]{40}$/.test(config.ref) || + !config.url.startsWith('https://') || + !/^[A-Za-z0-9._/-]+$/.test(config.branch) || + !manifest || + config.sparse.some(entry => entry.startsWith('-')) +) { + throw new Error('Invalid upstream/wpt pin in .gitmodules') +} + +switch (process.argv[2]) { + case 'setup': setup(); break + case 'verify': verify(); break + case 'serve': verify(); serve(); break + default: console.error('Usage: node test/wpt/wpt-launcher.mjs '); process.exitCode = 1 +} + +function git(args) { + execFileSync('git', args, { cwd: root, stdio: 'inherit' }) +} + +function phpInstallHint() { + switch (process.platform) { + case 'darwin': return 'Install it with Homebrew: brew install php' + case 'linux': return 'Install the PHP CLI with your distribution package manager, for example: sudo apt install php-cli' + case 'win32': return 'Install PHP with winget, then restart the terminal so php is on PATH.' + default: return 'Install the PHP CLI and make the php command available on PATH.' + } +} + +function serve() { + const php = spawnSync('php', ['--version'], { stdio: 'ignore' }) + if (php.error || php.status !== 0) { + throw new Error(`PHP is required for the interactive WPT server. ${phpInstallHint()}`) + } + const result = spawnSync('php', ['-S', `localhost:${process.env.NWSAPI_WPT_PORT || '8000'}`, '-t', upstream, path.join(root, 'test/wpt/router.php')], { + cwd: root, env: { ...process.env, BROWSER_ROOT: upstream, NWSAPI_ROOT: root }, stdio: 'inherit', + }) + if (result.error) throw result.error + process.exitCode = result.status ?? 1 +} + +function setup() { + if (existsSync(upstream)) return verify() + mkdirSync(path.dirname(upstream), { recursive: true }) + git(['clone', '--depth', '1', '--single-branch', '--filter=blob:none', '--no-checkout', '--branch', config.branch, config.url, upstream]) + git(['-C', upstream, 'sparse-checkout', 'set', '--cone', '--', ...config.sparse]) + git(['-C', upstream, 'fetch', '--depth', '1', '--filter=blob:none', 'origin', config.ref]) + git(['-C', upstream, 'checkout', '--detach', 'FETCH_HEAD', '--']) + verify() +} + +function verify() { + const head = existsSync(upstream) ? execFileSync('git', ['-C', upstream, 'rev-parse', 'HEAD'], { encoding: 'utf8' }).trim() : null + if (head !== config.ref) throw new Error(`${config.path} is not pinned at ${config.ref}; run npm run wpt:setup`) + if (!existsSync(path.join(upstream, 'resources', 'testharness.js'))) throw new Error(`${config.path} does not contain the WPT harness resources`) + if (execFileSync('git', ['-C', upstream, 'status', '--porcelain'], { encoding: 'utf8' }) !== '') throw new Error(`${config.path} is dirty; refusing to run modified upstream tests`) + const tree = execFileSync('git', ['-C', upstream, '-c', 'core.quotePath=false', 'ls-tree', '-r', config.ref], { maxBuffer }) + if (createHash('sha256').update(tree).digest('hex') !== manifest) throw new Error(`${config.path} does not match its pinned tree manifest`) +} diff --git a/test/wpt/wpt.sh b/test/wpt/wpt.sh index 82600344..4123f273 100755 --- a/test/wpt/wpt.sh +++ b/test/wpt/wpt.sh @@ -1,41 +1,7 @@ #!/bin/sh +set -eu -path=`dirname $0` +# The launcher serves the pinned upstream/wpt checkout on localhost:8000. -# -# NOTE: All this is preliminary work in progress. -# -# Both wpt and nwsapi repositories must be installed -# side by side at the same folder level to have them -# work together smoothly as intended for these tests. -# -# A working instance of PHP is needed to setup the -# local server, then from the main nwsapi folder -# execute the provided shell to setup for testing -# using the following terminal command: -# -# test/wpt/wpt.sh -# -# this will start the PHP local server and listen -# for browser connections on port 8000, after this -# you should be able to open the followeing URL: -# -# http://localhost:8000 -# -# After loading one of the tests, you willl have the -# option to execute it using the browser internal QS -# API or nwsapi alternatedly by pressing the reload -# button (CTRL-R in browsers), or the RERUN button -# found in each of the test pages itself. -# -# Each reload will change the environment used -# from browser intrernal QS api to nwsapi -# - -cp ./test/wpt/favicon.svg ../wpt/ -cp ./test/wpt/index.html ../wpt/ - -# start an instance of the PHP internal httpd server -# needed to interactively execute WPT tests in browsers - -php -S localhost:8000 -t ../wpt/ ./test/wpt/router.php +root=$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd) +exec node "$root/test/wpt/wpt-launcher.mjs" serve