opencode/packages/codemode/test/web-wpt.test.ts

424 lines
20 KiB
TypeScript

/**
* Portions adapted from web-platform-tests at revision 863077959ca8c1a7ceecfbe2534b75d2527b9013:
* - html/webappapis/atob/base64.any.js (btoa reference encoder, input list, and atob WebIDL cases)
* - fetch/data-urls/resources/base64.json (copied to fixtures/wpt-base64.json)
* - WebCryptoAPI/randomUUID.https.any.js
* - fetch/api/headers/{headers-basic,headers-errors}.any.js
*
* Copyright © web-platform-tests contributors. Governed by the 3-Clause BSD license in LICENSE.wpt.
*
* `assert_throws_dom("InvalidCharacterError", …)` becomes a check for a TypeError: CodeMode has no DOMException.
* `checkIteratorProperties` (prototype chain and property descriptors) and the custom iterator on a Headers
* instance are left out.
*/
import { describe, expect, test } from "bun:test"
import { Effect } from "effect"
import { CodeMode } from "../src/index.js"
const base64Cases = (await Bun.file(new URL("./fixtures/wpt-base64.json", import.meta.url)).json()) as Array<
[string, Array<number> | null]
>
const value = async (code: string) => {
const result = await Effect.runPromise(CodeMode.execute({ code, tools: {} }))
if (!result.ok) throw new Error(`expected success, got ${result.error.kind}: ${result.error.message}`)
return result.value
}
// The reference encoder from base64.any.js, run inside the interpreter so btoa is checked against
// an independent implementation rather than against the host's btoa.
const referenceEncoder = `
function btoaLookup(idx) {
if (idx < 26) return String.fromCharCode(idx + "A".charCodeAt(0))
if (idx < 52) return String.fromCharCode(idx - 26 + "a".charCodeAt(0))
if (idx < 62) return String.fromCharCode(idx - 52 + "0".charCodeAt(0))
if (idx == 62) return "+"
if (idx == 63) return "/"
}
function mybtoa(s) {
s = String(s)
for (var i = 0; i < s.length; i++) if (s.charCodeAt(i) > 255) return "INVALID_CHARACTER_ERR"
var out = ""
for (var i = 0; i < s.length; i += 3) {
var groupsOfSix = [undefined, undefined, undefined, undefined]
groupsOfSix[0] = s.charCodeAt(i) >> 2
groupsOfSix[1] = (s.charCodeAt(i) & 0x03) << 4
if (s.length > i + 1) {
groupsOfSix[1] |= s.charCodeAt(i + 1) >> 4
groupsOfSix[2] = (s.charCodeAt(i + 1) & 0x0f) << 2
}
if (s.length > i + 2) {
groupsOfSix[2] |= s.charCodeAt(i + 2) >> 6
groupsOfSix[3] = s.charCodeAt(i + 2) & 0x3f
}
for (var j = 0; j < groupsOfSix.length; j++) {
out += typeof groupsOfSix[j] == "undefined" ? "=" : btoaLookup(groupsOfSix[j])
}
}
return out
}
function testBtoa(input) {
var expected = mybtoa(input)
if (expected === "INVALID_CHARACTER_ERR") {
try { btoa(input) } catch (error) { return error instanceof TypeError ? "ok" : error.name }
return "did not throw"
}
if (btoa(input) !== expected) return "btoa mismatch"
if (atob(btoa(input)) !== String(input)) return "roundtrip mismatch"
return "ok"
}
`
describe("btoa WPT parity (html/webappapis/atob/base64.any.js)", () => {
test("every input encodes like the reference encoder and round-trips through atob", async () => {
expect(
await value(`
${referenceEncoder}
var tests = ["עברית", "", "ab", "abc", "abcd", "abcde", "\\xff\\xff\\xc0", "\\0a", "a\\0b",
undefined, null, 7, 12, 1.5, true, false, NaN, +Infinity, -Infinity, 0, -0]
for (var i = 0; i < 258; i++) tests.push(String.fromCharCode(i))
tests.push(String.fromCharCode(10000), String.fromCharCode(65534), String.fromCharCode(65535))
tests.push(String.fromCharCode(0xd800, 0xdc00))
var everything = ""
for (var i = 0; i < 256; i++) everything += String.fromCharCode(i)
tests.push(everything)
return tests.map(testBtoa).filter((outcome) => outcome !== "ok")
`),
).toEqual([])
})
})
describe("atob WPT parity (fetch/data-urls/resources/base64.json)", () => {
const idlCases: Array<[unknown, Array<number> | null]> = [
[undefined, null],
[null, [158, 233, 101]],
[7, null],
[12, [215]],
[1.5, null],
[true, [182, 187]],
[false, null],
[NaN, [53, 163]],
[Infinity, [34, 119, 226, 158, 43, 114]],
[-Infinity, null],
[0, null],
[-0, null],
]
test(`${base64Cases.length} forgiving-base64 inputs decode to the expected bytes or throw a TypeError`, async () => {
expect(
await value(`
const cases = ${JSON.stringify(base64Cases)}
return cases.flatMap(([input, output]) => {
try {
const result = atob(input)
if (output === null) return [[input, "expected throw"]]
const bytes = Array.from({ length: result.length }, (_, i) => result.charCodeAt(i))
return JSON.stringify(bytes) === JSON.stringify(output) ? [] : [[input, bytes]]
} catch (error) {
return output === null && error instanceof TypeError ? [] : [[input, error.name]]
}
})
`),
).toEqual([])
})
test("WebIDL argument conversion stringifies non-string inputs", async () => {
const literal = (input: unknown) =>
Object.is(input, -0)
? "-0"
: typeof input === "number" || input === undefined
? String(input)
: JSON.stringify(input)
expect(
await value(`
const cases = [${idlCases.map(([input, output]) => `[${literal(input)}, ${JSON.stringify(output)}]`).join(",")}]
return cases.flatMap(([input, output]) => {
try {
const result = atob(input)
if (output === null) return [[String(input), "expected throw"]]
// The source loop checks only the listed prefix of the decoded bytes.
const bytes = output.map((_, i) => result.charCodeAt(i))
return JSON.stringify(bytes) === JSON.stringify(output) ? [] : [[String(input), bytes]]
} catch (error) {
return output === null && error instanceof TypeError ? [] : [[String(input), error.name]]
}
})
`),
).toEqual([])
})
})
describe("crypto.randomUUID WPT parity (WebCryptoAPI/randomUUID.https.any.js)", () => {
test("namespace format, version, and variant bits over 256 iterations without collision", async () => {
expect(
await value(`
const uuids = new Set()
const randomUUID = () => {
const uuid = crypto.randomUUID()
if (uuids.has(uuid)) throw new Error("uuid collision " + uuid)
uuids.add(uuid)
return uuid
}
const UUIDRegex = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/
let format = true, version = true, variant = true
for (let i = 0; i < 256; i++) format = format && UUIDRegex.test(randomUUID())
for (let i = 0; i < 256; i++) version = version && (parseInt(randomUUID().split("-")[2].slice(0, 2), 16) & 0b11110000) === 0b01000000
for (let i = 0; i < 256; i++) variant = variant && (parseInt(randomUUID().split("-")[3].slice(0, 2), 16) & 0b11000000) === 0b10000000
return [format, version, variant, uuids.size]
`),
).toEqual([true, true, true, 768])
})
})
// Enough of testharness.js to run the Headers files close to verbatim; each `test` records its failure, if any.
const testharness = `
const failures = []
function test(run, name) { try { run() } catch (error) { failures.push(name + ": " + (error && error.message ? error.message : error)) } }
function assert_equals(actual, expected, message) { if (actual !== expected) throw new Error((message || "") + " expected " + JSON.stringify(expected) + " got " + JSON.stringify(actual)) }
function assert_true(actual, message) { assert_equals(actual, true, message) }
function assert_false(actual, message) { assert_equals(actual, false, message) }
function assert_array_equals(actual, expected, message) { assert_equals(JSON.stringify(actual), JSON.stringify(expected), message) }
function assert_throws_js(type, run) { try { run() } catch (error) { if (error instanceof type) return; throw new Error("threw " + error.name) } throw new Error("did not throw") }
function assert_unreached() { throw new Error("unreachable") }
`
describe("Headers WPT parity (fetch/api/headers)", () => {
test("headers-basic.any.js", async () => {
expect(
await value(`
${testharness}
test(function() { new Headers() }, "Create headers from no parameter")
test(function() { new Headers(undefined) }, "Create headers from undefined parameter")
test(function() { new Headers({}) }, "Create headers from empty object")
var parameters = [null, 1]
parameters.forEach(function(parameter) {
test(function() { assert_throws_js(TypeError, function() { new Headers(parameter) }) }, "Create headers with " + parameter + " should throw")
})
var headerDict = {"name1": "value1", "name2": "value2", "name3": "value3", "name4": null, "name5": undefined, "name6": 1, "Content-Type": "value4"}
var headerSeq = []
for (var name in headerDict) headerSeq.push([name, headerDict[name]])
test(function() {
var headers = new Headers(headerSeq)
for (name in headerDict) assert_equals(headers.get(name), String(headerDict[name]), "name: " + name + " has value: " + headerDict[name])
assert_equals(headers.get("length"), null, "init should be treated as a sequence, not as a dictionary")
}, "Create headers with sequence")
test(function() {
var headers = new Headers(headerDict)
for (name in headerDict) assert_equals(headers.get(name), String(headerDict[name]), "name: " + name + " has value: " + headerDict[name])
}, "Create headers with record")
test(function() {
var headers = new Headers(headerDict)
var headers2 = new Headers(headers)
for (name in headerDict) assert_equals(headers2.get(name), String(headerDict[name]), "name: " + name + " has value: " + headerDict[name])
}, "Create headers with existing headers")
test(function() {
var headers = new Headers()
for (name in headerDict) {
headers.append(name, headerDict[name])
assert_equals(headers.get(name), String(headerDict[name]), "name: " + name + " has value: " + headerDict[name])
}
}, "Check append method")
test(function() {
var headers = new Headers()
for (name in headerDict) {
headers.set(name, headerDict[name])
assert_equals(headers.get(name), String(headerDict[name]), "name: " + name + " has value: " + headerDict[name])
}
}, "Check set method")
test(function() {
var headers = new Headers(headerDict)
for (name in headerDict) assert_true(headers.has(name), "headers has name " + name)
assert_false(headers.has("nameNotInHeaders"), "headers do not have header: nameNotInHeaders")
}, "Check has method")
test(function() {
var headers = new Headers(headerDict)
for (name in headerDict) {
assert_true(headers.has(name), "headers have a header: " + name)
headers.delete(name)
assert_true(!headers.has(name), "headers do not have anymore a header: " + name)
}
}, "Check delete method")
test(function() {
var headers = new Headers(headerDict)
for (name in headerDict) assert_equals(headers.get(name), String(headerDict[name]), "name: " + name + " has value: " + headerDict[name])
assert_equals(headers.get("nameNotInHeaders"), null, "header: nameNotInHeaders has no value")
}, "Check get method")
var headerEntriesDict = {"name1": "value1", "Name2": "value2", "name": "value3", "content-Type": "value4", "Content-Typ": "value5", "Content-Types": "value6"}
var sortedHeaderDict = {}
var headerValues = []
var sortedHeaderKeys = Object.keys(headerEntriesDict).map(function(value) {
sortedHeaderDict[value.toLowerCase()] = headerEntriesDict[value]
headerValues.push(headerEntriesDict[value])
return value.toLowerCase()
}).sort()
test(function() {
var headers = new Headers(headerEntriesDict)
var actual = headers.keys()
sortedHeaderKeys.forEach(function(key) {
const entry = actual.next()
assert_false(entry.done)
assert_equals(entry.value, key)
})
assert_true(actual.next().done)
assert_true(actual.next().done)
for (const key of headers.keys()) assert_true(sortedHeaderKeys.indexOf(key) != -1)
}, "Check keys method")
test(function() {
var headers = new Headers(headerEntriesDict)
var actual = headers.values()
sortedHeaderKeys.forEach(function(key) {
const entry = actual.next()
assert_false(entry.done)
assert_equals(entry.value, sortedHeaderDict[key])
})
assert_true(actual.next().done)
assert_true(actual.next().done)
for (const value of headers.values()) assert_true(headerValues.indexOf(value) != -1)
}, "Check values method")
test(function() {
var headers = new Headers(headerEntriesDict)
var actual = headers.entries()
sortedHeaderKeys.forEach(function(key) {
const entry = actual.next()
assert_false(entry.done)
assert_equals(entry.value[0], key)
assert_equals(entry.value[1], sortedHeaderDict[key])
})
assert_true(actual.next().done)
assert_true(actual.next().done)
for (const entry of headers.entries()) assert_equals(entry[1], sortedHeaderDict[entry[0]])
}, "Check entries method")
test(function() {
var headers = new Headers(headerEntriesDict)
var actual = headers[Symbol.iterator]()
sortedHeaderKeys.forEach(function(key) {
const entry = actual.next()
assert_false(entry.done)
assert_equals(entry.value[0], key)
assert_equals(entry.value[1], sortedHeaderDict[key])
})
assert_true(actual.next().done)
assert_true(actual.next().done)
}, "Check Symbol.iterator method")
test(function() {
var headers = new Headers(headerEntriesDict)
var reference = sortedHeaderKeys[Symbol.iterator]()
headers.forEach(function(value, key, container) {
assert_equals(headers, container)
const entry = reference.next()
assert_false(entry.done)
assert_equals(key, entry.value)
assert_equals(value, sortedHeaderDict[entry.value])
})
assert_true(reference.next().done)
}, "Check forEach method")
test(() => {
const headers = new Headers({"foo": "2", "baz": "1", "BAR": "0"})
const actualKeys = []
const actualValues = []
for (const [header, value] of headers) {
actualKeys.push(header)
actualValues.push(value)
headers.delete("foo")
}
assert_array_equals(actualKeys, ["bar", "baz"])
assert_array_equals(actualValues, ["0", "1"])
}, "Iteration skips elements removed while iterating")
test(() => {
const headers = new Headers({"foo": "2", "baz": "1", "BAR": "0", "quux": "3"})
const actualKeys = []
const actualValues = []
for (const [header, value] of headers) {
actualKeys.push(header)
actualValues.push(value)
if (header === "baz") headers.delete("bar")
}
assert_array_equals(actualKeys, ["bar", "baz", "quux"])
assert_array_equals(actualValues, ["0", "1", "3"])
}, "Removing elements already iterated over causes an element to be skipped during iteration")
test(() => {
const headers = new Headers({"foo": "2", "baz": "1", "BAR": "0", "quux": "3"})
const actualKeys = []
const actualValues = []
for (const [header, value] of headers) {
actualKeys.push(header)
actualValues.push(value)
if (header === "baz") headers.append("X-yZ", "4")
}
assert_array_equals(actualKeys, ["bar", "baz", "foo", "quux", "x-yz"])
assert_array_equals(actualValues, ["0", "1", "2", "3", "4"])
}, "Appending a value pair during iteration causes it to be reached during iteration")
test(() => {
const headers = new Headers({"foo": "2", "baz": "1", "BAR": "0", "quux": "3"})
const actualKeys = []
const actualValues = []
for (const [header, value] of headers) {
actualKeys.push(header)
actualValues.push(value)
if (header === "baz") headers.append("abc", "-1")
}
assert_array_equals(actualKeys, ["bar", "baz", "baz", "foo", "quux"])
assert_array_equals(actualValues, ["0", "1", "1", "2", "3"])
}, "Prepending a value pair before the current element position causes it to be skipped during iteration and adds the current element a second time")
return failures
`),
).toEqual([])
})
test("headers-errors.any.js", async () => {
expect(
await value(`
${testharness}
test(function() { assert_throws_js(TypeError, function() { new Headers([["name"]]) }) }, "Create headers giving an array having one string as init argument")
test(function() { assert_throws_js(TypeError, function() { new Headers([["invalid", "invalidValue1", "invalidValue2"]]) }) }, "Create headers giving an array having three strings as init argument")
test(function() { assert_throws_js(TypeError, function() { new Headers([["invalid\u0100", "Value1"]]) }) }, "Create headers giving bad header name as init argument")
test(function() { assert_throws_js(TypeError, function() { new Headers([["name", "invalidValue\u0100"]]) }) }, "Create headers giving bad header value as init argument")
var badNames = ["invalid\u0100", {}]
var badValues = ["invalid\u0100"]
badNames.forEach(function(name) {
test(function() { var headers = new Headers(); assert_throws_js(TypeError, function() { headers.get(name) }) }, "Check headers get with an invalid name " + name)
})
badNames.forEach(function(name) {
test(function() { var headers = new Headers(); assert_throws_js(TypeError, function() { headers.delete(name) }) }, "Check headers delete with an invalid name " + name)
})
badNames.forEach(function(name) {
test(function() { var headers = new Headers(); assert_throws_js(TypeError, function() { headers.has(name) }) }, "Check headers has with an invalid name " + name)
})
badNames.forEach(function(name) {
test(function() { var headers = new Headers(); assert_throws_js(TypeError, function() { headers.set(name, "Value1") }) }, "Check headers set with an invalid name " + name)
})
badValues.forEach(function(value) {
test(function() { var headers = new Headers(); assert_throws_js(TypeError, function() { headers.set("name", value) }) }, "Check headers set with an invalid value " + value)
})
badNames.forEach(function(name) {
test(function() { var headers = new Headers(); assert_throws_js(TypeError, function() { headers.append("invalid\u0100", "Value1") }) }, "Check headers append with an invalid name " + name)
})
badValues.forEach(function(value) {
test(function() { var headers = new Headers(); assert_throws_js(TypeError, function() { headers.append("name", value) }) }, "Check headers append with an invalid value " + value)
})
test(function() {
var headers = new Headers([["name", "value"]])
assert_throws_js(TypeError, function() { headers.forEach() })
assert_throws_js(TypeError, function() { headers.forEach(undefined) })
assert_throws_js(TypeError, function() { headers.forEach(1) })
}, "Headers forEach throws if argument is not callable")
test(function() {
var headers = new Headers([["name1", "value1"], ["name2", "value2"], ["name3", "value3"]])
var counter = 0
try {
headers.forEach(function(value, name) {
counter++
if (name == "name2") throw "error"
})
} catch (e) {
assert_equals(counter, 2)
assert_equals(e, "error")
return
}
assert_unreached()
}, "Headers forEach loop should stop if callback is throwing exception")
return failures
`),
).toEqual([])
})
})