Compare commits

...

3 Commits

Author SHA1 Message Date
deepak1556
b3534815b0 ci: restore required wpt folder to checkout cache 2024-11-22 16:41:24 +09:00
deepak1556
8a394e16a4 chore: add wpt to artifacts 2024-11-22 01:56:46 +09:00
deepak1556
e5a6036ce0 build: add support for running wpt suites 2024-11-21 23:51:01 +09:00
15 changed files with 1846 additions and 11 deletions

View File

@@ -139,7 +139,10 @@ runs:
run: |
rm -rf src/android_webview
rm -rf src/ios/chrome
mv src/third_party/blink/web_tests/external/wpt ./wpt
rm -rf src/third_party/blink/web_tests
mkdir -p src/third_party/blink/web_tests/external
mv ./wpt src/third_party/blink/web_tests/external/wpt
rm -rf src/third_party/blink/perf_tests
rm -rf src/chrome/test/data/xr/webvr_info
rm -rf src/third_party/angle/third_party/VK-GL-CTS/src

View File

@@ -161,3 +161,53 @@ jobs:
do
sleep 60
done
wpt-tests:
name: Run WPT Tests
runs-on: electron-arc-linux-amd64-4core
timeout-minutes: 20
env:
TARGET_ARCH: ${{ inputs.target-arch }}
BUILD_TYPE: linux
container: ${{ fromJSON(inputs.test-container) }}
steps:
- name: Checkout Electron
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
path: src/electron
fetch-depth: 0
- name: Install Dependencies
run: |
cd src/electron
node script/yarn install --frozen-lockfile
- name: Download Generated Artifacts
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16
with:
name: generated_artifacts_${{ env.BUILD_TYPE }}_${{ env.TARGET_ARCH }}
path: ./generated_artifacts_${{ env.BUILD_TYPE }}_${{ env.TARGET_ARCH }}
- name: Download Src Artifacts
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16
with:
name: src_artifacts_linux_${{ env.TARGET_ARCH }}
path: ./src_artifacts_linux_${{ env.TARGET_ARCH }}
- name: Restore Generated Artifacts
run: ./src/electron/script/actions/restore-artifacts.sh
- name: Unzip Dist
run: |
cd src/out/Default
unzip -:o dist.zip
- name: Setup Linux for Headless Testing
run: sh -e /etc/init.d/xvfb start
- name: Run WPT Tests
run: |
cd src
chown :builduser . && chmod g+w .
chown -R :builduser ./electron && chmod -R g+w ./electron
chmod 4755 ./out/Default/chrome-sandbox
runuser -u builduser -- xvfb-run electron/script/actions/run-tests.sh electron/script/wpt-spec-runner.js
- name: Wait for active SSH sessions
if: always() && !cancelled()
run: |
while [ -f /var/.ssh-lock ]
do
sleep 60
done

View File

@@ -62,7 +62,8 @@ move_src_dirs_if_exist() {
src/third_party/libc++ \
src/third_party/libc++abi \
src/out/Default/obj/buildtools/third_party \
src/v8/tools/builtins-pgo
src/v8/tools/builtins-pgo \
src/third_party/blink/web_tests/external/wpt
do
if [ -d "$dir" ]; then
mkdir -p src_artifacts/$(dirname $dir)

View File

@@ -16,7 +16,8 @@ const HASH_VERSIONS = {
const filesToHash = [
path.resolve(__dirname, '../DEPS'),
path.resolve(__dirname, '../yarn.lock'),
path.resolve(__dirname, '../script/sysroots.json')
path.resolve(__dirname, '../script/sysroots.json'),
path.resolve(__dirname, '../.github/actions/checkout/action.yml')
];
const addAllFiles = (dir) => {

29
script/wpt-spec-runner.js Normal file
View File

@@ -0,0 +1,29 @@
const cp = require('node:child_process');
const path = require('node:path');
const utils = require('./lib/utils');
const BASE = path.resolve(__dirname, '../..');
const WPT_DIR = path.resolve(BASE, 'third_party', 'blink', 'web_tests', 'external', 'wpt');
if (!require.main) {
throw new Error('Must call the wpt spec runner directly');
}
async function main () {
const testChild = cp.spawn(utils.getAbsoluteElectronExec(), [path.join(__dirname, 'wpt'), '--enable-logging=stderr'], {
env: {
...process.env,
WPT_DIR
},
stdio: 'inherit'
});
testChild.on('exit', (testCode) => {
process.exit(testCode);
});
}
main().catch((err) => {
console.error('An unhandled error occurred in the wpt spec runner', err);
process.exit(1);
});

5
script/wpt/package.json Normal file
View File

@@ -0,0 +1,5 @@
{
"name": "electron-test-wpt",
"main": "start.mjs",
"type": "module"
}

View File

@@ -0,0 +1,388 @@
import { utilityProcess } from 'electron';
import { EventEmitter, once } from 'node:events';
import { readdirSync, readFileSync, statSync } from 'node:fs';
import { isAbsolute, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { colors, handlePipes, normalizeName, parseMeta, resolveStatusPath } from './util.mjs';
const basePath = fileURLToPath(join(import.meta.url, '../..'));
const testPath = process.env.WPT_DIR;
const statusPath = join(basePath, 'status');
// https://github.com/web-platform-tests/wpt/blob/b24eedd/resources/testharness.js#L3705
function sanitizeUnpairedSurrogates (str) {
return str.replace(
/([\ud800-\udbff]+)(?![\udc00-\udfff])|(^|[^\ud800-\udbff])([\udc00-\udfff]+)/g,
function (_, low, prefix, high) {
let output = prefix || ''; // Prefix may be undefined
const string = low || high; // Only one of these alternates can match
for (let i = 0; i < string.length; i++) {
output += codeUnitStr(string[i]);
}
return output;
});
}
function codeUnitStr (char) {
return 'U+' + char.charCodeAt(0).toString(16);
}
export class WPTRunner extends EventEmitter {
/** @type {string} */
#folderName;
/** @type {string} */
#folderPath;
/** @type {string[]} */
#files = [];
/** @type {string[]} */
#initScripts = [];
/** @type {string} */
#url;
/** @type {import('../../status/fetch.status.json')} */
#status;
/** Tests that have expectedly failed mapped by file name */
#statusOutput = {};
#uncaughtExceptions = [];
#stats = {
completedTests: 0,
failedTests: 0,
passedTests: 0,
expectedFailures: 0,
failedFiles: 0,
passedFiles: 0,
skippedFiles: 0
};
constructor (folder, url) {
super();
this.#folderName = folder;
this.#folderPath = join(testPath, folder);
this.#files.push(
...WPTRunner.walk(
this.#folderPath,
(file) => file.endsWith('.any.js')
)
);
this.#status = JSON.parse(readFileSync(join(statusPath, `${folder}.status.json`)));
this.#url = url;
if (this.#files.length === 0) {
queueMicrotask(() => {
this.emit('completion');
});
}
this.once('completion', () => {
for (const { error, test } of this.#uncaughtExceptions) {
console.log(colors(`Uncaught exception in "${test}":`, 'red'));
console.log(colors(`${error.stack}`, 'red'));
console.log('='.repeat(96));
}
});
}
static walk (dir, fn) {
const ini = new Set(readdirSync(dir));
const files = new Set();
while (ini.size !== 0) {
for (const d of ini) {
const path = resolve(dir, d);
ini.delete(d); // remove from set
const stats = statSync(path);
if (stats.isDirectory()) {
for (const f of readdirSync(path)) {
ini.add(resolve(path, f));
}
} else if (stats.isFile() && fn(d)) {
files.add(path);
}
}
}
return [...files].sort();
}
async run () {
const workerPath = fileURLToPath(join(import.meta.url, '../worker.mjs'));
/** @type {Set<Worker>} */
const activeWorkers = new Set();
let finishedFiles = 1;
let total = this.#files.length;
const files = this.#files.map((test) => {
const code = test.includes('.sub.')
? handlePipes(readFileSync(test, 'utf-8'), this.#url)
: readFileSync(test, 'utf-8');
const meta = this.resolveMeta(code, test);
if (meta.variant.length) {
total += meta.variant.length - 1;
}
return [test, code, meta];
});
console.log('='.repeat(96));
for (const [test, code, meta] of files) {
console.log(`Started ${test}`);
const status = resolveStatusPath(test, this.#status);
if (status.file.skip || status.topLevel.skip) {
this.#stats.skippedFiles += 1;
console.log(colors(`[${finishedFiles}/${total}] SKIPPED - ${test}`, 'yellow'));
console.log('='.repeat(96));
finishedFiles++;
continue;
}
const start = performance.now();
for (const variant of meta.variant.length ? meta.variant : ['']) {
const url = new URL(this.#url);
if (variant) {
url.search = variant;
}
const worker = new utilityProcess.fork(workerPath, [], {
stdio: 'pipe'
});
await once(worker, 'spawn');
worker.postMessage({
workerData: {
// The test file.
test: code,
// Parsed META tag information
meta,
url: url.href,
path: test
}
});
worker.stdout.pipe(process.stdout);
worker.stderr.pipe(process.stderr);
const fileUrl = new URL(`/${this.#folderName}${test.slice(this.#folderPath.length)}`, 'http://wpt');
fileUrl.pathname = fileUrl.pathname.replace(/\.js$/, '.html');
fileUrl.search = variant;
const result = {
test: fileUrl.href.slice(fileUrl.origin.length),
subtests: [],
status: ''
};
activeWorkers.add(worker);
// These values come directly from the web-platform-tests
const timeout = meta.timeout === 'long' ? 60_000 : 10_000;
worker.on('message', (message) => {
if (message.type === 'result') {
this.handleIndividualTestCompletion(message, status, test, meta, result);
} else if (message.type === 'completion') {
this.handleTestCompletion(worker, status, test);
} else if (message.type === 'error') {
this.#uncaughtExceptions.push({ error: message.error, test });
this.#stats.failedTests += 1;
this.#stats.passedTests -= 1;
}
});
try {
await once(worker, 'exit', {
signal: AbortSignal.timeout(timeout)
});
if (result.subtests.some((subtest) => subtest?.isExpectedFailure === false)) {
this.#stats.failedFiles += 1;
console.log(colors(`[${finishedFiles}/${total}] FAILED - ${test}`, 'red'));
} else {
this.#stats.passedFiles += 1;
console.log(colors(`[${finishedFiles}/${total}] PASSED - ${test}`, 'green'));
}
if (variant) console.log('Variant:', variant);
console.log(`File took ${(performance.now() - start).toFixed(2)}ms`);
console.log('='.repeat(96));
} catch (_) {
// If the worker is terminated by the timeout signal, the test is marked as failed
this.#stats.failedFiles += 1;
console.log(colors(`[${finishedFiles}/${total}] FAILED - ${test}`, 'red'));
if (variant) console.log('Variant:', variant);
console.log(`File timed out after ${timeout}ms`);
console.log('='.repeat(96));
} finally {
finishedFiles++;
activeWorkers.delete(worker);
}
}
}
this.handleRunnerCompletion();
}
/**
* Called after a test has succeeded or failed.
*/
handleIndividualTestCompletion (message, status, path, meta, wptResult) {
this.#stats.completedTests += 1;
const { file, topLevel } = status;
const isFailure = message.result.status === 1;
const testResult = {
status: isFailure ? 'FAIL' : 'PASS',
name: sanitizeUnpairedSurrogates(message.result.name)
};
if (isFailure) {
let isExpectedFailure = false;
this.#stats.failedTests += 1;
const name = normalizeName(message.result.name);
const sanitizedMessage = sanitizeUnpairedSurrogates(message.result.message);
if (file.flaky?.includes(name)) {
isExpectedFailure = true;
this.#stats.expectedFailures += 1;
wptResult?.subtests.push({ ...testResult, message: sanitizedMessage, isExpectedFailure });
} else if (file.allowUnexpectedFailures || topLevel.allowUnexpectedFailures || file.fail?.includes(name)) {
if (!file.allowUnexpectedFailures && !topLevel.allowUnexpectedFailures) {
if (Array.isArray(file.fail)) {
this.#statusOutput[path] ??= [];
this.#statusOutput[path].push(name);
}
}
isExpectedFailure = true;
this.#stats.expectedFailures += 1;
wptResult?.subtests.push({ ...testResult, message: sanitizedMessage, isExpectedFailure });
} else {
wptResult?.subtests.push({ ...testResult, message: sanitizedMessage, isExpectedFailure });
process.exitCode = 1;
console.error(message.result);
}
if (!isExpectedFailure) {
process._rawDebug(`Failed test: ${path}`);
}
} else {
this.#stats.passedTests += 1;
wptResult?.subtests.push(testResult);
}
}
/**
* Called after all the tests in a worker are completed.
* @param {Worker} worker
*/
handleTestCompletion (worker, status, path) {
worker.kill();
const { file } = status;
const hasExpectedFailures = !!file.fail;
const testHasFailures = !!this.#statusOutput?.[path];
const failed = this.#statusOutput?.[path] ?? [];
if (hasExpectedFailures !== testHasFailures) {
console.log({ expected: file.fail, failed });
if (failed.length === 0) {
console.log(colors('Tests are marked as failure but did not fail, yay!', 'red'));
} else if (!hasExpectedFailures) {
console.log(colors('Test failed but there were no expected errors.', 'red'));
}
process.exitCode = 1;
} else if (hasExpectedFailures && testHasFailures) {
const diff = [
...file.fail.filter(x => !failed.includes(x)),
...failed.filter(x => !file.fail.includes(x))
];
if (diff.length) {
console.log({ diff });
console.log(colors('Expected failures did not match actual failures', 'red'));
process.exitCode = 1;
}
}
}
/**
* Called after every test has completed.
*/
handleRunnerCompletion () {
// tests that failed
if (Object.keys(this.#statusOutput).length !== 0) {
console.log(this.#statusOutput);
}
this.emit('completion');
const { passedFiles, failedFiles, skippedFiles } = this.#stats;
console.log(
`File results for folder [${this.#folderName}]: ` +
`completed: ${this.#files.length}, passed: ${passedFiles}, failed: ${failedFiles}, ` +
`skipped: ${skippedFiles}`
);
const { completedTests, failedTests, passedTests, expectedFailures } = this.#stats;
console.log(
`Test results for folder [${this.#folderName}]: ` +
`completed: ${completedTests}, failed: ${failedTests}, passed: ${passedTests}, ` +
`expected failures: ${expectedFailures}, ` +
`unexpected failures: ${failedTests - expectedFailures}`
);
process.exit(failedTests - expectedFailures ? 1 : process.exitCode);
}
/**
* Parses META tags and resolves any script file paths.
* @param {string} code
* @param {string} path The absolute path of the test
*/
resolveMeta (code, path) {
const meta = parseMeta(code);
const scripts = meta.scripts.map((filePath) => {
let content = '';
if (filePath === '/resources/WebIDLParser.js') {
// See https://github.com/web-platform-tests/wpt/pull/731
return readFileSync(join(testPath, '/resources/webidl2/lib/webidl2.js'), 'utf-8');
} else if (isAbsolute(filePath)) {
content = readFileSync(join(testPath, filePath), 'utf-8');
} else {
content = readFileSync(resolve(path, '..', filePath), 'utf-8');
}
// If the file has any built-in pipes.
if (filePath.includes('.sub.')) {
content = handlePipes(content, this.#url);
}
return content;
});
return {
...meta,
resourcePaths: meta.scripts,
scripts
};
}
}

172
script/wpt/runner/util.mjs Normal file
View File

@@ -0,0 +1,172 @@
import assert from 'node:assert';
import { sep } from 'node:path';
import { exit } from 'node:process';
import tty from 'node:tty';
import { inspect } from 'node:util';
/**
* Parse the `Meta:` tags sometimes included in tests.
* These can include resources to inject, how long it should
* take to timeout, and which globals to expose.
* @example
* // META: timeout=long
* // META: global=window,worker
* // META: script=/common/utils.js
* // META: script=/common/get-host-info.sub.js
* // META: script=../request/request-error.js
* @see https://nodejs.org/api/readline.html#readline_example_read_file_stream_line_by_line
* @param {string} fileContents
*/
export function parseMeta (fileContents) {
const lines = fileContents.split(/\r?\n/g);
const meta = {
/** @type {string|null} */
timeout: null,
/** @type {string[]} */
global: [],
/** @type {string[]} */
scripts: [],
/** @type {string[]} */
variant: []
};
for (const line of lines) {
if (!line.startsWith('// META: ')) {
break;
}
const groups = /^\/\/ META: (?<type>.*?)=(?<match>.*)$/.exec(line)?.groups;
if (!groups) {
console.log(`Failed to parse META tag: ${line}`);
exit(1);
}
switch (groups.type) {
case 'variant':
meta[groups.type].push(groups.match);
break;
case 'title':
case 'timeout': {
meta[groups.type] = groups.match;
break;
}
case 'global': {
// window,worker -> ['window', 'worker']
meta.global.push(...groups.match.split(','));
break;
}
case 'script': {
// A relative or absolute file path to the resources
// needed for the current test.
meta.scripts.push(groups.match);
break;
}
default: {
console.log(`Unknown META tag: ${groups.type}`);
exit(1);
}
}
}
return meta;
}
/**
* @param {string} sub
*/
function parseSubBlock (sub) {
const subName = sub.includes('[') ? sub.slice(0, sub.indexOf('[')) : sub;
const options = sub.matchAll(/\[(.*?)\]/gm);
return {
sub: subName,
options: [...options].map(match => match[1])
};
}
/**
* @see https://web-platform-tests.org/writing-tests/server-pipes.html?highlight=sub#built-in-pipes
* @param {string} code
* @param {string} url
*/
export function handlePipes (code, url) {
const server = new URL(url);
// "Substitutions are marked in a file using a block delimited by
// {{ and }}. Inside the block the following variables are available:"
return code.replace(/{{(.*?)}}/gm, (_, match) => {
const { sub } = parseSubBlock(match);
switch (sub) {
// "The host name of the server excluding any subdomain part."
// eslint-disable-next-line no-fallthrough
case 'host':
// "The domain name of a particular subdomain e.g.
// {{domains[www]}} for the www subdomain."
// eslint-disable-next-line no-fallthrough
case 'domains':
// "The domain name of a particular subdomain for a particular host.
// The first key may be empty (designating the “default” host) or
// the value alt; i.e., {{hosts[alt][]}} (designating the alternate
// host)."
// eslint-disable-next-line no-fallthrough
case 'hosts': {
return 'localhost';
}
// "The port number of servers, by protocol e.g. {{ports[http][0]}}
// for the first (and, depending on setup, possibly only) http server"
case 'ports': {
return server.port;
}
default: {
throw new TypeError(`Unknown substitute "${sub}".`);
}
}
});
}
/**
* Some test names may contain characters that JSON cannot handle.
* @param {string} name
*/
export function normalizeName (name) {
return name.replace(/(\v)/g, (_, match) => {
switch (inspect(match)) {
case '\'\\x0B\'': return '\\x0B';
default: return match;
}
});
}
export function colors (str, color) {
assert(Object.hasOwn(inspect.colors, color), `Missing color ${color}`);
if (!tty.WriteStream.prototype.hasColors()) {
return str;
}
const [start, end] = inspect.colors[color];
return `\u001b[${start}m${str}\u001b[${end}m`;
}
/** @param {string} path */
export function resolveStatusPath (path, status) {
const paths = path
.slice(process.cwd().length + sep.length)
.split(sep)
.slice(5); // [test, wpt, tests, fetch, b, c.js] -> [fetch, b, c.js]
// skip the first folder name
for (let i = 1; i < paths.length - 1; i++) {
status = status[paths[i]];
if (!status) {
break;
}
}
return { fullPath: path, topLevel: status ?? {}, file: status?.[paths.at(-1)] ?? {} };
}

View File

@@ -0,0 +1,128 @@
import { net } from 'electron';
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import { runInThisContext } from 'node:vm';
const basePath = process.env.WPT_DIR;
process.on('uncaughtException', (err) => {
console.log('uncaughtException', err);
process.parentPort.postMessage({
type: 'error',
error: {
message: err.message,
name: err.name,
stack: err.stack
}
});
});
const globalPropertyDescriptors = {
writable: true,
enumerable: false,
configurable: true
};
Object.defineProperties(globalThis, {
fetch: {
...globalPropertyDescriptors,
enumerable: true,
value: net.fetch
}
});
// TODO: remove once Float16Array is added. Otherwise a test throws with an uncaught exception.
globalThis.Float16Array ??= class Float16Array {};
process.parentPort.on('message', (message) => {
const { meta, test, url, path } = message.data.workerData;
const urlPath = path.slice(basePath.length);
// self is required by testharness
// GLOBAL is required by self
runInThisContext(`
globalThis.self = globalThis
globalThis.GLOBAL = {
isWorker () {
return false
},
isShadowRealm () {
return false
},
isWindow () {
return false
}
}
globalThis.window = globalThis
globalThis.location = new URL('${urlPath.replace(/\\/g, '/')}', '${url}')
globalThis.Window = Object.getPrototypeOf(globalThis).constructor
`);
if (meta.title) {
runInThisContext(`globalThis.META_TITLE = "${meta.title.replace(/"/g, '\\"')}"`);
}
const harness = readFileSync(join(basePath, '/resources/testharness.js'), 'utf-8');
runInThisContext(harness);
// add_*_callback comes from testharness
// stolen from node's wpt test runner
// eslint-disable-next-line no-undef
add_result_callback((result) => {
process.parentPort.postMessage({
type: 'result',
result: {
status: result.status,
name: result.name,
message: result.message,
stack: result.stack
}
});
});
// eslint-disable-next-line no-undef
add_completion_callback((_, status) => {
process.parentPort.postMessage({
type: 'completion',
status
});
});
const globalOrigin = Symbol.for('undici.globalOrigin.1');
function setGlobalOrigin (newOrigin) {
if (newOrigin === undefined) {
Object.defineProperty(globalThis, globalOrigin, {
value: undefined,
writable: true,
enumerable: false,
configurable: false
});
return;
}
const parsedURL = new URL(newOrigin);
if (parsedURL.protocol !== 'http:' && parsedURL.protocol !== 'https:') {
throw new TypeError(`Only http & https urls are allowed, received ${parsedURL.protocol}`);
}
Object.defineProperty(globalThis, globalOrigin, {
value: parsedURL,
writable: true,
enumerable: false,
configurable: false
});
}
setGlobalOrigin(globalThis.location);
// Inject any files from the META tags
for (const script of meta.scripts) {
runInThisContext(script);
}
// Finally, run the test.
runInThisContext(test);
});

View File

@@ -0,0 +1,3 @@
export const symbols = {
kContent: Symbol('content')
};

View File

@@ -0,0 +1,104 @@
import { setTimeout } from 'node:timers/promises';
const stash = new Map();
/**
* @see https://github.com/web-platform-tests/wpt/blob/master/fetch/connection-pool/resources/network-partition-key.py
* @param {Parameters<import('http').RequestListener>[0]} req
* @param {Parameters<import('http').RequestListener>[1]} res
* @param {URL} fullUrl
*/
export async function route (req, res, fullUrl) {
const { searchParams } = fullUrl;
let stashedData = { count: 0, preflight: 0 };
let status = 302;
res.setHeader('Content-Type', 'text/plain');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Pragma', 'no-cache');
if (Object.hasOwn(req.headers, 'origin')) {
res.setHeader('Access-Control-Allow-Origin', req.headers.origin ?? '');
res.setHeader('Access-Control-Allow-Credentials', 'true');
} else {
res.setHeader('Access-Control-Allow-Origin', '*');
}
let token = null;
if (searchParams.has('token')) {
token = searchParams.get('token');
const data = stash.get(token);
stash.delete(token);
if (data) {
stashedData = data;
}
}
if (req.method === 'OPTIONS') {
if (searchParams.has('allow_headers')) {
res.setHeader('Access-Control-Allow-Headers', searchParams.get('allow_headers'));
}
stashedData.preflight = '1';
if (!searchParams.has('redirect_preflight')) {
if (token) {
stash.set(searchParams.get('token'), stashedData);
}
res.statusCode = 200;
res.end('');
return;
}
}
if (searchParams.has('redirect_status')) {
status = parseInt(searchParams.get('redirect_status'));
}
stashedData.count += 1;
if (searchParams.has('location')) {
let url = decodeURIComponent(searchParams.get('location'));
if (!searchParams.has('simple')) {
const scheme = new URL(url, fullUrl).protocol;
if (scheme === 'http:' || scheme === 'https:') {
url += url.includes('?') ? '&' : '?';
for (const [key, value] of searchParams) {
url += '&' + encodeURIComponent(key) + '=' + encodeURIComponent(value);
}
url += '&count=' + stashedData.count;
}
}
res.setHeader('location', url);
}
if (searchParams.has('redirect_referrerpolicy')) {
res.setHeader('Referrer-Policy', searchParams.get('redirect_referrerpolicy'));
}
if (searchParams.has('delay')) {
await setTimeout(parseFloat(searchParams.get('delay') ?? 0));
}
if (token) {
stash.set(searchParams.get('token'), stashedData);
if (searchParams.has('max_count')) {
const maxCount = parseInt(searchParams.get('max_count'));
if (stashedData.count > maxCount) {
res.end((stashedData.count - 1).toString());
return;
}
}
}
res.statusCode = status;
res.end('');
}

View File

@@ -0,0 +1,350 @@
import { once } from 'node:events';
import { createReadStream, readFileSync, existsSync } from 'node:fs';
import { createServer } from 'node:http';
import { join } from 'node:path';
import process from 'node:process';
import { setTimeout as sleep } from 'node:timers/promises';
import { symbols } from './constants.mjs';
import { route as redirectRoute } from './routes/redirect.mjs';
const tests = process.env.WPT_DIR;
// https://web-platform-tests.org/tools/wptserve/docs/stash.html
class Stash extends Map {
take (key) {
if (this.has(key)) {
const value = this.get(key);
this.delete(key);
return value.value;
}
}
put (key, value, path) {
this.set(key, { value, path });
}
}
const stash = new Stash();
const server = createServer(async (req, res) => {
const fullUrl = new URL(req.url, `http://localhost:${server.address().port}`);
switch (fullUrl.pathname) {
case '/fetch/content-encoding/resources/big.text.gz':
case '/fetch/content-encoding/resources/foo.octetstream.gz':
case '/fetch/content-encoding/resources/foo.text.gz':
case '/fetch/api/resources/cors-top.txt':
case '/fetch/api/resources/top.txt':
case '/fetch/data-urls/resources/base64.json':
case '/fetch/data-urls/resources/data-urls.json':
case '/fetch/api/resources/empty.txt':
case '/fetch/api/resources/data.json': {
// If this specific resources requires custom headers
const customHeadersPath = join(tests, fullUrl.pathname + '.headers');
if (existsSync(customHeadersPath)) {
const headers = readFileSync(customHeadersPath, 'utf-8')
.trim()
.split(/\r?\n/g)
.map((h) => h.split(': '));
for (const [key, value] of headers) {
if (!key || !value) {
console.warn(`Skipping ${key}:${value} header pair`);
continue;
}
res.setHeader(key, value);
}
}
// https://github.com/web-platform-tests/wpt/blob/6ae3f702a332e8399fab778c831db6b7dca3f1c6/fetch/api/resources/data.json
createReadStream(join(tests, fullUrl.pathname))
.on('end', () => res.end())
.pipe(res);
break;
}
case '/fetch/api/resources/trickle.py': {
// Note: python's time.sleep(...) takes seconds, while setTimeout
// takes ms.
const delay = parseFloat(fullUrl.searchParams.get('ms') ?? 500);
const count = parseInt(fullUrl.searchParams.get('count') ?? 50);
// eslint-disable-next-line no-unused-vars
for await (const chunk of req); // read request body
await sleep(delay);
if (!fullUrl.searchParams.has('notype')) {
res.setHeader('Content-type', 'text/plain');
}
res.statusCode = 200;
await sleep(delay);
for (let i = 0; i < count; i++) {
res.write('TEST_TRICKLE\n');
await sleep(delay);
}
res.end();
break;
}
case '/fetch/api/resources/infinite-slow-response.py': {
// https://github.com/web-platform-tests/wpt/blob/master/fetch/api/resources/infinite-slow-response.py
const stateKey = fullUrl.searchParams.get('stateKey') ?? '';
const abortKey = fullUrl.searchParams.get('abortKey') ?? '';
if (stateKey) {
stash.put(stateKey, 'open', fullUrl.pathname);
}
res.setHeader('Content-Type', 'text/plain');
res.statusCode = 200;
res.write('.'.repeat(2048));
while (true) {
if (!res.write('.')) {
break;
} else if (abortKey && stash.take(abortKey)) {
break;
}
await sleep(100);
}
if (stateKey) {
stash.put(stateKey, 'closed', fullUrl.pathname);
}
res.end();
break;
}
case '/fetch/api/resources/stash-take.py': {
// https://github.com/web-platform-tests/wpt/blob/6ae3f702a332e8399fab778c831db6b7dca3f1c6/fetch/api/resources/stash-take.py
const key = fullUrl.searchParams.get('key');
res.setHeader('Access-Control-Allow-Origin', '*');
const took = stash.take(key, fullUrl.pathname) ?? null;
res.write(JSON.stringify(took));
res.end();
break;
}
case '/fetch/api/resources/echo-content.py': {
res.setHeader('X-Request-Method', req.method);
res.setHeader('X-Request-Content-Length', req.headers['content-length'] ?? 'NO');
res.setHeader('X-Request-Content-Type', req.headers['content-type'] ?? 'NO');
res.setHeader('Content-Type', 'text/plain');
for await (const chunk of req) {
res.write(chunk);
}
res.end();
break;
}
case '/fetch/api/resources/cache.py': {
if (req.headers['if-none-match'] === '"123abc"') {
res.statusCode = 304;
res.statusMessage = 'Not Modified';
res.setHeader('X-HTTP-STATUS', '304');
res.end();
} else {
// cache miss, so respond with the actual content
res.statusCode = 200;
res.statusMessage = 'OK';
res.setHeader('Content-Type', 'text/plain');
res.setHeader('ETag', '"123abc"');
res.end('lorem ipsum dolor sit amet');
}
break;
}
case '/fetch/api/resources/status.py': {
const code = parseInt(fullUrl.searchParams.get('code') ?? 200);
const text = fullUrl.searchParams.get('text') ?? 'OMG';
const content = fullUrl.searchParams.get('content') ?? '';
const type = fullUrl.searchParams.get('type') ?? '';
res.statusCode = code;
res.statusMessage = text;
res.setHeader('Content-Type', type);
res.setHeader('X-Request-Method', req.method);
res.end(content);
break;
}
case '/fetch/api/resources/inspect-headers.py': {
const query = fullUrl.searchParams;
const checkedHeaders = query.get('headers')
?.split('|')
.map(h => h.toLowerCase()) ?? [];
if (query.has('headers')) {
for (const header of checkedHeaders) {
if (Object.hasOwn(req.headers, header)) {
res.setHeader(`x-request-${header}`, req.headers[header] ?? '');
}
}
}
if (query.has('cors')) {
if (Object.hasOwn(req.headers, 'origin')) {
res.setHeader('Access-Control-Allow-Origin', req.headers.origin ?? '');
} else {
res.setHeader('Access-Control-Allow-Origin', '*');
}
res.setHeader('Access-Control-Allow-Credentials', 'true');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, HEAD');
const exposedHeaders = checkedHeaders.map(h => `x-request-${h}`).join(', ');
res.setHeader('Access-Control-Expose-Headers', exposedHeaders);
if (query.has('allow_headers')) {
res.setHeader('Access-Control-Allow-Headers', query.get('allowed_headers'));
} else {
res.setHeader('Access-Control-Allow-Headers', Object.keys(req.headers).join(', '));
}
}
res.setHeader('content-type', 'text/plain');
res.end('');
break;
}
case '/fetch/api/resources/bad-chunk-encoding.py': {
const query = fullUrl.searchParams;
const delay = parseFloat(query.get('ms') ?? 1000);
const count = parseInt(query.get('count') ?? 50);
await sleep(delay);
res.socket.write(
'HTTP/1.1 200 OK\r\ntransfer-encoding: chunked\r\n\r\n'
);
await sleep(delay);
for (let i = 0; i < count; i++) {
res.socket.write('a\r\nTEST_CHUNK\r\n');
await sleep(delay);
}
res.end('garbage');
break;
}
case '/fetch/api/resources/redirect.py': {
redirectRoute(req, res, fullUrl);
break;
}
case '/fetch/api/resources/method.py': {
if (fullUrl.searchParams.has('cors')) {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Credentials', 'true');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, FOO');
res.setHeader('Access-Control-Allow-Headers', 'x-test, x-foo');
res.setHeader('Access-Control-Expose-Headers', 'x-request-method');
}
res.setHeader('x-request-method', req.method);
res.setHeader('x-request-content-type', req.headers['content-type'] ?? 'NO');
res.setHeader('x-request-content-length', req.headers['content-length'] ?? 'NO');
res.setHeader('x-request-content-encoding', req.headers['content-encoding'] ?? 'NO');
res.setHeader('x-request-content-language', req.headers['content-language'] ?? 'NO');
res.setHeader('x-request-content-location', req.headers['content-location'] ?? 'NO');
for await (const chunk of req) {
res.write(chunk);
}
res.end();
break;
}
case '/fetch/api/resources/clean-stash.py': {
const token = fullUrl.searchParams.get('token');
const took = stash.take(token);
if (took) {
res.end('1');
} else {
res.end('0');
}
break;
}
case '/fetch/content-encoding/resources/bad-gzip-body.py': {
res.setHeader('Content-Encoding', 'gzip');
res.end('not actually gzip');
break;
}
case '/fetch/api/resources/dump-authorization-header.py': {
res.setHeader('Content-Type', 'text/html');
res.setHeader('Cache-Control', 'no-cache');
if (req.headers.origin) {
res.setHeader('Access-Control-Allow-Origin', req.headers.origin);
res.setHeader('Access-Control-Allow-Credentials', 'true');
} else {
res.setHeader('Access-Control-Allow-Origin', '*');
}
res.setHeader('Access-Control-Allow-Headers', 'Authorization');
res.statusCode = 200;
if (req.headers.authorization) {
res.end(req.headers.authorization);
break;
}
res.end('none');
break;
}
case '/fetch/api/resources/authentication.py': {
const auth = Buffer.from(req.headers.authorization.slice('Basic '.length), 'base64');
const [user, password] = auth.toString().split(':');
if (user === 'user' && password === 'password') {
res.end('Authentication done');
break;
}
const realm = fullUrl.searchParams.get('realm') ?? 'test';
res.statusCode = 401;
res.setHeader('WWW-Authenticate', `Basic realm="${realm}"`);
res.end('Please login with credentials \'user\' and \'password\'');
break;
}
case '/fetch/api/resources/redirect-empty-location.py': {
res.setHeader('location', '');
res.statusCode = 302;
res.end('');
break;
}
default: {
res.statusCode = 200;
res.end(fullUrl.toString());
}
}
if (res[symbols.kContent]) {
res.write(res[symbols.kContent]);
}
}).listen(0);
await once(server, 'listening');
const send = (message) => {
if (typeof process.send === 'function') {
process.send(message);
}
};
const url = `http://localhost:${server.address().port}`;
console.log('server opened ' + url);
send({ server: url });
process.on('message', (message) => {
if (message === 'shutdown') {
server.close((err) => process.exit(err ? 1 : 0));
}
});
export { server };

32
script/wpt/start.mjs Normal file
View File

@@ -0,0 +1,32 @@
import { app } from 'electron';
import { fork } from 'node:child_process';
import { on } from 'node:events';
import { join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { WPTRunner } from './runner/runner.mjs';
const serverPath = fileURLToPath(join(import.meta.url, '../server/server.mjs'));
app.whenReady().then(async () => {
const child = fork(serverPath, [], {
stdio: ['pipe', 'pipe', 'pipe', 'ipc']
});
child.stdout.pipe(process.stdout);
child.stderr.pipe(process.stderr);
child.on('exit', (code) => process.exit(code));
for await (const [message] of on(child, 'message')) {
if (message.server) {
const runner = new WPTRunner('fetch', message.server);
runner.run();
runner.once('completion', () => {
if (child.connected) {
child.send('shutdown');
}
});
}
}
});

View File

@@ -0,0 +1,578 @@
{
"api": {
"abort": {
"general.any.js": {
"fail": [
"response.arrayBuffer() rejects if already aborted",
"response.blob() rejects if already aborted",
"response.formData() rejects if already aborted",
"response.json() rejects if already aborted",
"response.text() rejects if already aborted",
"response.bytes() rejects if already aborted",
"Call text() twice on aborted response",
"Fetch aborted & connection closed when aborted after calling response.arrayBuffer()",
"Fetch aborted & connection closed when aborted after calling response.blob()",
"Fetch aborted & connection closed when aborted after calling response.formData()",
"Fetch aborted & connection closed when aborted after calling response.json()",
"Fetch aborted & connection closed when aborted after calling response.text()",
"Fetch aborted & connection closed when aborted after calling response.bytes()",
"Stream errors once aborted. Underlying connection closed.",
"Stream errors once aborted, after reading. Underlying connection closed."
]
},
"cache.https.any.js": {
"note": "undici doesn't implement http caching",
"skip": true
}
},
"basic": {
"accept-header.any.js": {
"fail": [
"Request through fetch should have 'accept' header with value '*/*'",
"Request through fetch should have 'accept' header with value 'custom/*'",
"Request through fetch should have a 'accept-language' header",
"Request through fetch should have 'accept-language' header with value 'bzh'"
]
},
"conditional-get.any.js": {
"fail": [
"Testing conditional GET with ETags"
],
"note": "undici doesn't keep track of etags"
},
"error-after-response.any.js": {
"fail": [
"Response reader read() promise should reject after a network error happening after resolving fetch promise",
"Response reader closed promise should reject after a network error happening after resolving fetch promise"
]
},
"header-value-combining.any.js": {
"fail": [
"response.headers.get('content-length') expects 0, 0",
"response.headers.get('foo-test') expects 1, 2, 3",
"response.headers.get('heya') expects , \\x0B\f, 1, , , 2"
],
"flaky": [
"response.headers.get('content-length') expects 0",
"response.headers.get('double-trouble') expects , ",
"response.headers.get('www-authenticate') expects 1, 2, 3, 4"
]
},
"header-value-null-byte.any.js": {
"fail": [
"Ensure fetch() rejects null bytes in headers"
]
},
"http-response-code.any.js": {
"fail": [
"Fetch on 425 response should not be retried for non TLS early data."
]
},
"integrity.sub.any.js": {
"note": "Electron: integrity is not working",
"skip": true
},
"keepalive.any.js": {
"note": "document is not defined",
"skip": true
},
"mode-no-cors.sub.any.js": {
"note": "undici doesn't implement CORs",
"skip": true
},
"mode-same-origin.any.js": {
"note": "undici doesn't respect RequestInit.mode",
"skip": true
},
"referrer.any.js": {
"note": "Electron: fix referrrer handling",
"skip": true
},
"request-forbidden-headers.any.js": {
"note": "undici doesn't filter headers",
"skip": true
},
"request-headers.any.js": {
"note": "Electron: fix response type",
"skip": true
},
"request-headers-case.any.js": {
"note": "Electron: rework header generation",
"skip": true
},
"request-private-network-headers.tentative.any.js": {
"note": "undici doesn't filter headers",
"skip": true
},
"request-referrer.any.js": {
"note": "Electron: fix referrrer handling",
"skip": true
},
"request-upload.any.js": {
"note": "no Float16Array",
"fail": [
"Fetch with POST with Float16Array body",
"Fetch with POST with text body on 421 response should be retried once on new connection."
]
},
"request-upload.h2.any.js": {
"note": "undici doesn't support http/2",
"skip": true
},
"response-url.sub.any.js": {
"note": "Electron: does not support response.url",
"skip": true
},
"scheme-about.any.js": {
"note": "Electron: does not handle about urls",
"skip": true
},
"scheme-blob.sub.any.js": {
"note": "Electron: does not support blob urls",
"skip": true
},
"scheme-data.any.js": {
"note": "Electron: does not support data urls",
"skip": true
},
"scheme-others.sub.any.js": {
"note": "Electron: does not support unknown urls",
"skip": true
},
"status.h2.any.js": {
"note": "undici doesn't support http/2",
"skip": true
},
"stream-response.any.js": {
"fail": [
"Stream response's body when content-type is not present"
]
},
"stream-safe-creation.any.js": {
"note": "Electron: stream accessors are broken",
"skip": true
}
},
"body": {
"mime-type.any.js": {
"note": "fails on all platforms, https://wpt.fyi/results/fetch/api/body/mime-type.any.html?label=master&label=experimental&product=chrome&product=firefox&product=safari&product=node.js&product=deno&aligned",
"fail": [
"Response: Extract a MIME type with clone"
]
}
},
"cors": {
"note": "undici doesn't implement CORs",
"skip": true
},
"credentials": {
"authentication-basic.any.js": {
"note": "Electron: fix response type",
"skip": true
},
"authentication-redirection.any.js": {
"note": "connects to https server",
"fail": [
"getAuthorizationHeaderValue - cross origin redirection",
"getAuthorizationHeaderValue - same origin redirection"
]
},
"cookies.any.js": {
"note": "Electron: fix response type",
"skip": true
}
},
"fetch-later": {
"note": "this is not part of the spec, only a proposal",
"skip": true
},
"headers": {
"header-setcookie.any.js": {
"note": "undici doesn't filter headers",
"fail": [
"Set-Cookie is a forbidden response header"
]
},
"header-values-normalize.any.js": {
"note": "TODO(@KhafraDev): https://github.com/nodejs/undici/issues/1680",
"fail": [
"XMLHttpRequest with value %00",
"XMLHttpRequest with value %01",
"XMLHttpRequest with value %02",
"XMLHttpRequest with value %03",
"XMLHttpRequest with value %04",
"XMLHttpRequest with value %05",
"XMLHttpRequest with value %06",
"XMLHttpRequest with value %07",
"XMLHttpRequest with value %08",
"XMLHttpRequest with value %09",
"XMLHttpRequest with value %0A",
"XMLHttpRequest with value %0D",
"XMLHttpRequest with value %0E",
"XMLHttpRequest with value %0F",
"XMLHttpRequest with value %10",
"XMLHttpRequest with value %11",
"XMLHttpRequest with value %12",
"XMLHttpRequest with value %13",
"XMLHttpRequest with value %14",
"XMLHttpRequest with value %15",
"XMLHttpRequest with value %16",
"XMLHttpRequest with value %17",
"XMLHttpRequest with value %18",
"XMLHttpRequest with value %19",
"XMLHttpRequest with value %1A",
"XMLHttpRequest with value %1B",
"XMLHttpRequest with value %1C",
"XMLHttpRequest with value %1D",
"XMLHttpRequest with value %1E",
"XMLHttpRequest with value %1F",
"XMLHttpRequest with value %20",
"fetch() with value %01",
"fetch() with value %02",
"fetch() with value %03",
"fetch() with value %04",
"fetch() with value %05",
"fetch() with value %06",
"fetch() with value %07",
"fetch() with value %08",
"fetch() with value %0E",
"fetch() with value %0F",
"fetch() with value %10",
"fetch() with value %11",
"fetch() with value %12",
"fetch() with value %13",
"fetch() with value %14",
"fetch() with value %15",
"fetch() with value %16",
"fetch() with value %17",
"fetch() with value %18",
"fetch() with value %19",
"fetch() with value %1A",
"fetch() with value %1B",
"fetch() with value %1C",
"fetch() with value %1D",
"fetch() with value %1E",
"fetch() with value %1F"
]
},
"header-values.any.js": {
"fail": [
"XMLHttpRequest with value x%00x needs to throw",
"XMLHttpRequest with value x%0Ax needs to throw",
"XMLHttpRequest with value x%0Dx needs to throw",
"XMLHttpRequest with all valid values",
"fetch() with all valid values"
]
},
"headers-no-cors.any.js": {
"note": "undici doesn't implement CORs",
"skip": true
}
},
"redirect": {
"redirect-back-to-original-origin.any.js": {
"note": "Electron: fix response type",
"skip": true
},
"redirect-count.any.js": {
"note": "Electron: handle too many redirects",
"skip": true
},
"redirect-empty-location.any.js": {
"note": "undici handles redirect: manual differently than browsers",
"fail": [
"redirect response with empty Location, manual mode",
"redirect response with empty Location, follow mode"
]
},
"redirect-keepalive.any.js": {
"note": "document is not defined",
"skip": true
},
"redirect-keepalive.https.any.js": {
"note": "document is not defined",
"skip": true
},
"redirect-location-escape.tentative.any.js": {
"note": "TODO(@KhafraDev): crashes runner",
"skip": true
},
"redirect-location.any.js": {
"note": "Electron: fix redirect handling",
"skip": true
},
"redirect-method.any.js": {
"note": "Electron: fix response type",
"skip": true
},
"redirect-mode.any.js": {
"note": "mode isn't respected",
"skip": true
},
"redirect-origin.any.js": {
"note": "TODO(@KhafraDev): investigate",
"skip": true
},
"redirect-referrer-override.any.js": {
"note": "TODO(@KhafraDev): investigate",
"skip": true
},
"redirect-referrer.any.js": {
"note": "TODO(@KhafraDev): investigate",
"skip": true
},
"redirect-schemes.any.js": {
"note": "Electron: fix redirect handling",
"skip": true
},
"redirect-to-dataurl.any.js": {
"note": "Electron: does not support data urls",
"skip": true
},
"redirect-upload.h2.any.js": {
"note": "undici doesn't support http/2",
"skip": true
}
},
"request": {
"request-cache-default-conditional.any.js": {
"note": "undici doesn't implement an http cache",
"skip": true
},
"request-cache-default.any.js": {
"note": "undici doesn't implement an http cache",
"skip": true
},
"request-cache-force-cache.any.js": {
"note": "undici doesn't implement an http cache",
"skip": true
},
"request-cache-no-cache.any.js": {
"note": "undici doesn't implement an http cache",
"skip": true
},
"request-cache-no-store.any.js": {
"note": "undici doesn't implement an http cache",
"skip": true
},
"request-cache-only-if-cached.any.js": {
"note": "undici doesn't implement an http cache",
"skip": true
},
"request-cache-reload.any.js": {
"note": "undici doesn't implement an http cache",
"skip": true
},
"request-consume-empty.any.js": {
"note": "the semantics about this test are being discussed - https://github.com/web-platform-tests/wpt/pull/3950",
"fail": [
"Consume empty FormData request body as text"
]
},
"request-disturbed.any.js": {
"note": "this test fails in all other platforms - https://wpt.fyi/results/fetch/api/request/request-disturbed.any.html?label=master&label=experimental&product=chrome&product=firefox&product=safari&product=deno&aligned&view=subtest",
"fail": [
"Input request used for creating new request became disturbed even if body is not used"
]
},
"request-headers.any.js": {
"note": "undici doesn't filter headers",
"fail": [
"Adding invalid request header \"Accept-Charset: KO\"",
"Adding invalid request header \"accept-charset: KO\"",
"Adding invalid request header \"ACCEPT-ENCODING: KO\"",
"Adding invalid request header \"Accept-Encoding: KO\"",
"Adding invalid request header \"Access-Control-Request-Headers: KO\"",
"Adding invalid request header \"Access-Control-Request-Method: KO\"",
"Adding invalid request header \"Connection: KO\"",
"Adding invalid request header \"Content-Length: KO\"",
"Adding invalid request header \"Cookie: KO\"",
"Adding invalid request header \"Cookie2: KO\"",
"Adding invalid request header \"Date: KO\"",
"Adding invalid request header \"DNT: KO\"",
"Adding invalid request header \"Expect: KO\"",
"Adding invalid request header \"Host: KO\"",
"Adding invalid request header \"Keep-Alive: KO\"",
"Adding invalid request header \"Origin: KO\"",
"Adding invalid request header \"Referer: KO\"",
"Adding invalid request header \"Set-Cookie: KO\"",
"Adding invalid request header \"TE: KO\"",
"Adding invalid request header \"Trailer: KO\"",
"Adding invalid request header \"Transfer-Encoding: KO\"",
"Adding invalid request header \"Upgrade: KO\"",
"Adding invalid request header \"Via: KO\"",
"Adding invalid request header \"Proxy-: KO\"",
"Adding invalid request header \"proxy-a: KO\"",
"Adding invalid request header \"Sec-: KO\"",
"Adding invalid request header \"sec-b: KO\"",
"Adding invalid no-cors request header \"Content-Type: KO\"",
"Adding invalid no-cors request header \"Potato: KO\"",
"Adding invalid no-cors request header \"proxy: KO\"",
"Adding invalid no-cors request header \"proxya: KO\"",
"Adding invalid no-cors request header \"sec: KO\"",
"Adding invalid no-cors request header \"secb: KO\"",
"Adding invalid no-cors request header \"Empty-Value: \"",
"Check that request constructor is filtering headers provided as init parameter",
"Check that no-cors request constructor is filtering headers provided as init parameter",
"Check that no-cors request constructor is filtering headers provided as part of request parameter"
]
},
"request-init-priority.any.js": {
"note": "undici doesn't implement priority hints, yet(?)",
"skip": true
}
},
"response": {
"json.any.js": {
"note": "Electron: does not support data urls",
"skip": true
},
"response-blob-realm.any.js": {
"note": "onload is not defined (globalThis does not extend EventTarget)",
"fail": [
"realm of the Uint8Array from Response bytes()"
]
},
"response-clone.any.js": {
"note": "Node streams are too buggy currently.",
"skip": true
},
"response-consume-empty.any.js": {
"fail": [
"Consume empty FormData response body as text"
]
},
"response-consume-stream.any.js": {
"note": "only fail in node v18",
"flaky": [
"Read blob response's body as readableStream with mode=byob",
"Read text response's body as readableStream with mode=byob",
"Read URLSearchParams response's body as readableStream with mode=byob",
"Read array buffer response's body as readableStream with mode=byob",
"Read form data response's body as readableStream with mode=byob"
]
},
"response-headers-guard.any.js": {
"fail": [
"Ensure response headers are immutable"
]
},
"response-stream-with-broken-then.any.js": {
"note": "this is a bug in webstreams, see https://github.com/nodejs/node/issues/46786",
"skip": true
}
},
"idlharness.any.js": {
"note": "Electron: fix idl generation",
"skip": true
}
},
"content-encoding": {
"br": {
"bad-br-body.https.any.js": {
"note": "TODO(@KhafraDev): investigate failure",
"fail": [
"Consuming the body of a resource with bad br content with arrayBuffer() should reject"
]
},
"big-br-body.https.any.js": {
"note": "TODO(@KhafraDev): investigate failure",
"fail": [
"large br data should be decompressed successfully",
"large br data should be decompressed successfully with byte stream"
]
},
"br-body.https.any.js": {
"note": "TODO(@KhafraDev): investigate failure",
"fail": [
"fetched br data with content type text should be decompressed.",
"fetched br data with content type octetstream should be decompressed."
]
}
},
"gzip": {
"bad-gzip-body.any.js": {
"note": "TODO(@KhafraDev): investigate failure",
"fail": [
"Consuming the body of a resource with bad gzip content with arrayBuffer() should reject",
"Consuming the body of a resource with bad gzip content with blob() should reject",
"Consuming the body of a resource with bad gzip content with json() should reject",
"Consuming the body of a resource with bad gzip content with text() should reject"
]
},
"gzip-body.any.js": {
"note": "TODO(@KhafraDev): investigate failure",
"fail": [
"fetched gzip data with content type text should be decompressed.",
"fetched gzip data with content type octetstream should be decompressed."
]
},
"big-gzip-body.https.any.js": {
"note": "TODO(@KhafraDev): investigate failure",
"fail": [
"large gzip data should be decompressed successfully",
"large gzip data should be decompressed successfully with byte stream"
]
}
},
"zstd": {
"note": "node does not have zstd yet",
"skip": true
}
},
"content-length": {
"api-and-duplicate-headers.any.js": {
"fail": [
"XMLHttpRequest and duplicate Content-Length/Content-Type headers",
"fetch() and duplicate Content-Length/Content-Type headers"
]
}
},
"cross-origin-resource-policy": {
"note": "undici doesn't implement CORs",
"skip": true
},
"data-urls": {
"note": "Electron: does not support data urls",
"skip": true
},
"http-cache": {
"note": "undici doesn't implement http caching",
"skip": true
},
"metadata": {
"note": "undici doesn't respect RequestInit.mode",
"skip": true
},
"orb": {
"tentative": {
"note": "undici doesn't implement orb",
"skip": true
}
},
"range": {
"note": "undici doesn't respect range header",
"skip": true
},
"security": {
"1xx-response.any.js": {
"note": "TODO(@KhafraDev): investigate timeout",
"skip": true,
"fail": [
"Status(100) should be ignored.",
"Status(101) should be accepted, with removing body.",
"Status(103) should be ignored.",
"Status(199) should be ignored."
]
}
},
"stale-while-revalidate": {
"note": "undici doesn't implement http caching",
"skip": true
},
"idlharness.any.js": {
"flaky": [
"Window interface: operation fetch(RequestInfo, optional RequestInit)"
]
}
}

View File

@@ -1508,15 +1508,6 @@ describe('net module', () => {
});
describe('net.fetch', () => {
// NB. there exist much more comprehensive tests for fetch() in the form of
// the WPT: https://github.com/web-platform-tests/wpt/tree/master/fetch
// It's possible to run these tests against net.fetch(), but the test
// harness to do so is quite complex and hasn't been munged to smoothly run
// inside the Electron test runner yet.
//
// In the meantime, here are some tests for basic functionality and
// Electron-specific behavior.
describe('basic', () => {
test('can fetch http urls', async () => {
const serverUrl = await respondOnce.toSingleURL((request, response) => {