commit
c600d7182b
18 changed files with 5823 additions and 0 deletions
-
1.gitignore
-
1.npmrc
-
6fixtures/all.js
-
17fixtures/index-node.js
-
9fixtures/index.html
-
3fixtures/package.json
-
14fixtures/request.js
-
13fixtures/run.js
-
5365package-lock.json
-
47package.json
-
60src/browser-drivers.ts
-
95src/index.ts
-
81src/platform-browser.ts
-
10src/platform-deno.ts
-
21src/platform-node.ts
-
45src/server.ts
-
21src/types.ts
-
14tsconfig.json
@ -0,0 +1 @@ |
|||
node_modules/ |
@ -0,0 +1 @@ |
|||
detect_chromedriver_version=true |
@ -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; |
|||
} |
5365
package-lock.json
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -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" |
|||
} |
|||
} |
Write
Preview
Loading…
Cancel
Save
Reference in new issue