Initial prototype
commit
c600d7182b
|
@ -0,0 +1 @@
|
||||||
|
node_modules/
|
|
@ -0,0 +1,6 @@
|
||||||
|
import request from './request.js';
|
||||||
|
|
||||||
|
const tests = {
|
||||||
|
...request,
|
||||||
|
};
|
||||||
|
export default tests;
|
|
@ -0,0 +1,17 @@
|
||||||
|
import process from 'node:process';
|
||||||
|
import fetch, {Request, Response, Headers} from 'node-fetch';
|
||||||
|
|
||||||
|
import run from './run.js';
|
||||||
|
|
||||||
|
global.Request = Request;
|
||||||
|
global.Response = Response;
|
||||||
|
global.Headers = Headers;
|
||||||
|
global.fetch = fetch;
|
||||||
|
|
||||||
|
async function main(file) {
|
||||||
|
const tests = await import('./' + file + '.js');
|
||||||
|
const result = await run.default(tests.default);
|
||||||
|
process.send(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
main(process.argv[2]);
|
|
@ -0,0 +1,9 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>blank</title>
|
||||||
|
</head>
|
||||||
|
<body></body>
|
||||||
|
</html>
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"type": "module"
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
/* eslint-disable no-new */
|
||||||
|
const tests = {
|
||||||
|
request: {
|
||||||
|
async invalidURL() {
|
||||||
|
try {
|
||||||
|
new Request('http://example.com%');
|
||||||
|
} catch ({name, message}) {
|
||||||
|
return {name, message};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
export default tests;
|
||||||
|
/* eslint-enable no-new */
|
|
@ -0,0 +1,13 @@
|
||||||
|
export default async function run(tests) {
|
||||||
|
const results = {};
|
||||||
|
/* eslint-disable guard-for-in, no-await-in-loop */
|
||||||
|
for (const sectionKey in tests) {
|
||||||
|
results[sectionKey] = {};
|
||||||
|
for (const fnKey in tests[sectionKey]) {
|
||||||
|
results[sectionKey][fnKey] = await tests[sectionKey][fnKey]();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* eslint-enable guard-for-in, no-await-in-loop */
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,47 @@
|
||||||
|
{
|
||||||
|
"name": "fetch-compare",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "Compare various implementations of the WHATWG fetch API",
|
||||||
|
"author": "Ambrose Chua",
|
||||||
|
"license": "MIT",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node --loader ts-node/esm src/index.ts",
|
||||||
|
"type-check": "tsc --pretty --noEmit",
|
||||||
|
"format": "prettier --ignore-path ../.gitignore --write .",
|
||||||
|
"lint": "xo"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@swc/core": "^1.2.85",
|
||||||
|
"@wdio/logger": "^7.7.0",
|
||||||
|
"chromedriver": "^93.0.1",
|
||||||
|
"cli-table3": "^0.6.0",
|
||||||
|
"deno-prebuilt": "^1.0.4",
|
||||||
|
"geckodriver": "^2.0.4",
|
||||||
|
"get-port": "^5.1.1",
|
||||||
|
"koa": "^2.13.1",
|
||||||
|
"koa-files": "^1.0.3",
|
||||||
|
"node-fetch": "^3.0.0",
|
||||||
|
"tcp-port-used": "^1.0.2",
|
||||||
|
"ts-node": "^10.2.1",
|
||||||
|
"webdriver": "^7.12.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/koa": "^2.13.4",
|
||||||
|
"@types/node": "^16.9.0",
|
||||||
|
"prettier": "^2.4.0",
|
||||||
|
"typescript": "^4.4.2",
|
||||||
|
"xo": "^0.44.0"
|
||||||
|
},
|
||||||
|
"prettier": {
|
||||||
|
"semi": true,
|
||||||
|
"useTabs": true,
|
||||||
|
"trailingComma": "all",
|
||||||
|
"singleQuote": true,
|
||||||
|
"bracketSpacing": false,
|
||||||
|
"jsxBracketSameLine": false
|
||||||
|
},
|
||||||
|
"xo": {
|
||||||
|
"prettier": true,
|
||||||
|
"globals": ["Request", "Response", "Headers", "fetch"]
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,60 @@
|
||||||
|
// @ts-expect-error: Missing types
|
||||||
|
import {path as chromepath} from 'chromedriver';
|
||||||
|
import {path as firefoxpath} from 'geckodriver';
|
||||||
|
|
||||||
|
import {BrowserDriver} from './types';
|
||||||
|
|
||||||
|
export const chrome: BrowserDriver = {
|
||||||
|
path: chromepath as string,
|
||||||
|
args: (port: number, logLevel = 'warn') => {
|
||||||
|
const logLevelArg = {
|
||||||
|
trace: 'ALL',
|
||||||
|
debug: 'DEBUG',
|
||||||
|
info: 'INFO',
|
||||||
|
warn: 'WARNING',
|
||||||
|
error: 'SEVERE',
|
||||||
|
silent: 'OFF',
|
||||||
|
}[logLevel];
|
||||||
|
if (!logLevelArg) {
|
||||||
|
throw new Error('invalid log level');
|
||||||
|
}
|
||||||
|
|
||||||
|
return [`--port=${port}`, `--log-level=${logLevelArg}`];
|
||||||
|
},
|
||||||
|
};
|
||||||
|
export const firefox: BrowserDriver = {
|
||||||
|
path: firefoxpath,
|
||||||
|
args: (port: number, logLevel = 'warn') => {
|
||||||
|
const logLevelArg = {
|
||||||
|
trace: 'trace',
|
||||||
|
debug: 'debug',
|
||||||
|
info: 'info',
|
||||||
|
warn: 'warn',
|
||||||
|
error: 'error',
|
||||||
|
silent: 'fatal',
|
||||||
|
}[logLevel];
|
||||||
|
if (!logLevelArg) {
|
||||||
|
throw new Error('invalid log level');
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['--port', `${port}`, '--log', `${logLevelArg}`];
|
||||||
|
},
|
||||||
|
};
|
||||||
|
export const safari: BrowserDriver = {
|
||||||
|
path: '/usr/bin/safaridriver',
|
||||||
|
args: (port: number, logLevel = 'warn') => {
|
||||||
|
const logLevelArg = {
|
||||||
|
trace: '',
|
||||||
|
debug: '',
|
||||||
|
info: '',
|
||||||
|
warn: '',
|
||||||
|
error: '',
|
||||||
|
silent: '',
|
||||||
|
}[logLevel];
|
||||||
|
if (!logLevelArg) {
|
||||||
|
throw new Error('invalid log level');
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['-p', `${port}`, '-l', `${logLevelArg}`];
|
||||||
|
},
|
||||||
|
};
|
|
@ -0,0 +1,95 @@
|
||||||
|
import logger from '@wdio/logger';
|
||||||
|
import Table from 'cli-table3';
|
||||||
|
|
||||||
|
import {Platform, Result, Context} from './types';
|
||||||
|
import PlatformBrowser from './platform-browser';
|
||||||
|
import PlatformNode from './platform-node';
|
||||||
|
import PlatformDeno from './platform-deno';
|
||||||
|
import {chrome, firefox, safari} from './browser-drivers';
|
||||||
|
import {FixturesServer} from './server';
|
||||||
|
|
||||||
|
const log = logger('fetch-compare');
|
||||||
|
logger.setLevel('fetch-compare', 'info');
|
||||||
|
|
||||||
|
const silent = true;
|
||||||
|
const platforms: Record<string, Platform> = {
|
||||||
|
chrome: new PlatformBrowser(chrome, 'chrome', silent),
|
||||||
|
firefox: new PlatformBrowser(firefox, 'firefox', silent),
|
||||||
|
safari: new PlatformBrowser(safari, 'safari', silent),
|
||||||
|
node: new PlatformNode(),
|
||||||
|
deno: new PlatformDeno(),
|
||||||
|
};
|
||||||
|
|
||||||
|
function* tabularNames(reference: Result): Iterable<[string, string, string]> {
|
||||||
|
/* eslint-disable guard-for-in */
|
||||||
|
for (const groupName in reference) {
|
||||||
|
const results = reference[groupName];
|
||||||
|
for (const resultName in results) {
|
||||||
|
const resultObject = results[resultName];
|
||||||
|
for (const key in resultObject) {
|
||||||
|
yield [groupName, resultName, key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* eslint-enable guard-for-in */
|
||||||
|
}
|
||||||
|
|
||||||
|
function* tabularResults(results: Record<string, Result>): Iterable<string[]> {
|
||||||
|
const ref = Object.values(results)[0];
|
||||||
|
if (!ref) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const names = Object.keys(results);
|
||||||
|
for (const [group, result, key] of tabularNames(ref)) {
|
||||||
|
for (const n of names) {
|
||||||
|
yield [
|
||||||
|
`${group}:${result}:${key}`,
|
||||||
|
n,
|
||||||
|
results[n][group][result][key].toString(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
// Set up fixtures server
|
||||||
|
const fixturesServer = new FixturesServer();
|
||||||
|
const addr = await fixturesServer.start();
|
||||||
|
|
||||||
|
// Shared data among platforms
|
||||||
|
const ctx: Context = {
|
||||||
|
fixturesURL: addr.base,
|
||||||
|
};
|
||||||
|
|
||||||
|
const results: Record<string, Result> = {};
|
||||||
|
|
||||||
|
// Call each platform
|
||||||
|
/* eslint-disable guard-for-in, no-await-in-loop */
|
||||||
|
for (const name in platforms) {
|
||||||
|
try {
|
||||||
|
results[name] = await platforms[name].run(ctx, 'all');
|
||||||
|
log.debug(name, results[name]);
|
||||||
|
} catch (error: Error) {
|
||||||
|
log.error(name, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* eslint-enable guard-for-in, no-await-in-loop */
|
||||||
|
|
||||||
|
const table = new Table({
|
||||||
|
head: ['Test', 'Platform', 'Value'],
|
||||||
|
chars: {mid: '', 'left-mid': '', 'mid-mid': '', 'right-mid': ''},
|
||||||
|
});
|
||||||
|
for (const row of tabularResults(results)) {
|
||||||
|
table.push(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(table.toString());
|
||||||
|
|
||||||
|
// Tear down fixtures server
|
||||||
|
await fixturesServer.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
run().catch((error) => {
|
||||||
|
log.error(error);
|
||||||
|
});
|
|
@ -0,0 +1,81 @@
|
||||||
|
import cp from 'node:child_process';
|
||||||
|
import fs from 'node:fs/promises';
|
||||||
|
import path from 'node:path';
|
||||||
|
import getPort from 'get-port';
|
||||||
|
// @ts-expect-error: Missing types
|
||||||
|
import tcpPortUsed from 'tcp-port-used';
|
||||||
|
import WebDriver from 'webdriver';
|
||||||
|
|
||||||
|
import {Platform, Result, BrowserDriver, Context} from './types.js';
|
||||||
|
|
||||||
|
async function start(
|
||||||
|
driver: BrowserDriver,
|
||||||
|
silent = false,
|
||||||
|
): Promise<{child: cp.ChildProcess; port: number}> {
|
||||||
|
const port = await getPort();
|
||||||
|
let command = driver.path;
|
||||||
|
try {
|
||||||
|
await fs.access(command);
|
||||||
|
} catch {
|
||||||
|
command = path.basename(command);
|
||||||
|
}
|
||||||
|
|
||||||
|
const stdout = silent ? 'ignore' : 'inherit';
|
||||||
|
const child = cp.spawn(command, driver.args(port), {
|
||||||
|
stdio: ['ignore', stdout, stdout],
|
||||||
|
});
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
child.once('error', reject);
|
||||||
|
child.once('spawn', resolve);
|
||||||
|
});
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
|
||||||
|
await tcpPortUsed.waitUntilUsed(port, 100, 10_000);
|
||||||
|
return {child, port};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function stop(child: cp.ChildProcess): Promise<void> {
|
||||||
|
if (child.exitCode === null) {
|
||||||
|
child.kill();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class PlatformBrowser implements Platform {
|
||||||
|
private readonly driver: BrowserDriver;
|
||||||
|
private readonly name: string;
|
||||||
|
private readonly silent: boolean;
|
||||||
|
|
||||||
|
constructor(driver: BrowserDriver, name: string, silent = false) {
|
||||||
|
this.driver = driver;
|
||||||
|
this.name = name;
|
||||||
|
this.silent = silent;
|
||||||
|
}
|
||||||
|
|
||||||
|
async run(ctx: Context, file: string): Promise<Result> {
|
||||||
|
const {child, port} = await start(this.driver, this.silent);
|
||||||
|
const client = await WebDriver.newSession({
|
||||||
|
logLevel: 'warn',
|
||||||
|
port,
|
||||||
|
capabilities: {browserName: this.name},
|
||||||
|
});
|
||||||
|
|
||||||
|
await client.navigateTo(`${ctx.fixturesURL}/index.html`);
|
||||||
|
const result: Result = (await client.executeScript(
|
||||||
|
`
|
||||||
|
async function main(file) {
|
||||||
|
document.body.style.backgroundColor = '#f00';
|
||||||
|
const run = await import('./run.js');
|
||||||
|
const tests = await import('./' + file + '.js');
|
||||||
|
const result = await run.default(tests.default);
|
||||||
|
document.body.style.backgroundColor = '#0f0';
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
return main(arguments[0]);
|
||||||
|
`,
|
||||||
|
[file],
|
||||||
|
)) as Result;
|
||||||
|
|
||||||
|
await client.deleteSession();
|
||||||
|
await stop(child);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
// @ts-expect-error: Missing types
|
||||||
|
import {binary} from 'deno-prebuilt';
|
||||||
|
|
||||||
|
import {Platform, Result} from './types.js';
|
||||||
|
|
||||||
|
export default class PlatformDeno implements Platform {
|
||||||
|
async run(): Promise<Result> {
|
||||||
|
throw new Error('not implemented');
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
import process from 'node:process';
|
||||||
|
import cp from 'node:child_process';
|
||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
|
import {Platform, Result} from './types.js';
|
||||||
|
|
||||||
|
export default class PlatformNode implements Platform {
|
||||||
|
async run(ctx: Record<string, any>, file: string): Promise<Result> {
|
||||||
|
const child = cp.fork(
|
||||||
|
path.join(process.cwd(), 'fixtures', 'index-node.js'),
|
||||||
|
[file],
|
||||||
|
);
|
||||||
|
const result: Result = await new Promise((resolve, reject) => {
|
||||||
|
child.once('error', reject);
|
||||||
|
child.once('message', resolve);
|
||||||
|
});
|
||||||
|
child.kill();
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
import process from 'node:process';
|
||||||
|
import net from 'node:net';
|
||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
|
import Koa from 'koa';
|
||||||
|
import files from 'koa-files';
|
||||||
|
|
||||||
|
import {ServerAddress} from './types.js';
|
||||||
|
|
||||||
|
export class FixturesServer {
|
||||||
|
private readonly koa: Koa;
|
||||||
|
|
||||||
|
private server: net.Server | null = null;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.koa = new Koa();
|
||||||
|
this.koa.use(files(path.join(process.cwd(), 'fixtures')));
|
||||||
|
}
|
||||||
|
|
||||||
|
async start(): Promise<ServerAddress> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.server = this.koa.listen(0, 'localhost', () => {
|
||||||
|
const addr = this.server?.address();
|
||||||
|
if (typeof addr === 'string' || !addr) {
|
||||||
|
reject(new Error('invalid address'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve({
|
||||||
|
address: addr.address,
|
||||||
|
port: addr.port,
|
||||||
|
base: `http://localhost:${addr.port}`,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async stop(): Promise<void> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.server?.close(() => {
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
export type ResultObject = Record<string, string | number>;
|
||||||
|
export type Result = Record<string, Record<string, ResultObject>>;
|
||||||
|
|
||||||
|
export interface Context {
|
||||||
|
fixturesURL: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Platform {
|
||||||
|
run(ctx: Context, file: string): Promise<Result>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BrowserDriver {
|
||||||
|
path: string;
|
||||||
|
args: (port: number) => string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ServerAddress {
|
||||||
|
address: string;
|
||||||
|
port: number;
|
||||||
|
base: string;
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es2020",
|
||||||
|
"types": ["node"],
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"strict": true
|
||||||
|
},
|
||||||
|
"exclude": ["node_modules"],
|
||||||
|
"ts-node": {
|
||||||
|
"transpileOnly": true,
|
||||||
|
"transpiler": "ts-node/transpilers/swc-experimental"
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue