mirror of
https://github.com/anomalyco/opencode.git
synced 2026-09-28 21:45:51 +08:00
2022 lines
72 KiB
TypeScript
2022 lines
72 KiB
TypeScript
/*
|
|
* Portions adapted from Test262 at revision 250f204f23a9249ff204be2baec29600faae7b75:
|
|
* - test/built-ins/Date/value-to-primitive-result-non-string-prim.js
|
|
* - test/built-ins/Date/value-to-primitive-result-string.js
|
|
* - test/built-ins/Date/prototype/toUTCString/format.js
|
|
* - test/built-ins/Date/prototype/toUTCString/invalid-date.js
|
|
* - test/built-ins/RegExp/prototype/exec/S15.10.6.2_A4_T8.js
|
|
*
|
|
* CodeMode does not support Symbol.toPrimitive, so the Date-constructor cases exercise the same primitive-result
|
|
* handling through supported own valueOf and toString functions.
|
|
*
|
|
* Copyright (C) 2016 the V8 project authors. All rights reserved.
|
|
* Copyright (C) 2017 the V8 project authors. All rights reserved.
|
|
* Copyright 2009 the Sputnik authors. All rights reserved.
|
|
* Test262 portions are governed by the BSD license in LICENSE.test262.
|
|
*/
|
|
import { describe, expect, test } from "bun:test"
|
|
import { Effect, Schema } from "effect"
|
|
import { CodeMode, Tool } from "../src/index.js"
|
|
|
|
// Standard-library value types: Date, RegExp, Map, Set. Programs use them as ordinary JS;
|
|
// intra-CodeMode checkpoints (Object.* helpers, spread, coercion inputs) preserve the live
|
|
// values. JSON.stringify keeps Date -> ISO string (invalid -> null), URL -> href, and
|
|
// RegExp/Map/Set/URLSearchParams -> {}. The host boundary matches that except URLSearchParams,
|
|
// which cross as their query string, and Set, which crosses as an array.
|
|
const run = (code: string) => Effect.runPromise(CodeMode.execute({ code, tools: {} }))
|
|
const value = async (code: string) => {
|
|
const result = await run(code)
|
|
if (!result.ok) throw new Error(`expected success, got ${result.error.kind}: ${result.error.message}`)
|
|
return result.value
|
|
}
|
|
const error = async (code: string) => {
|
|
const result = await run(code)
|
|
if (result.ok) throw new Error(`expected failure, got value ${JSON.stringify(result.value)}`)
|
|
return result.error
|
|
}
|
|
|
|
describe("Number and Math", () => {
|
|
test("Math.random returns a number in [0, 1)", async () => {
|
|
expect(await value(`const n = Math.random(); return typeof n === "number" && n >= 0 && n < 1`)).toBe(true)
|
|
})
|
|
|
|
test("Number exposes native non-finite constants", async () => {
|
|
expect(
|
|
await value(
|
|
`return [Number.isNaN(Number.NaN), Number.POSITIVE_INFINITY === Infinity, Number.NEGATIVE_INFINITY === -Infinity]`,
|
|
),
|
|
).toEqual([true, true, true])
|
|
})
|
|
|
|
test("Number valueOf returns its primitive receiver", async () => {
|
|
expect(await value(`return (42).valueOf()`)).toBe(42)
|
|
})
|
|
|
|
test("Number valueOf does not enable boxed numbers", async () => {
|
|
const failure = await error(`return new Number(42)`)
|
|
expect(failure.kind).toBe("ExecutionFailure")
|
|
expect(failure.message).toContain("new Number(...) is not supported; call Number(...) without new instead.")
|
|
})
|
|
})
|
|
|
|
describe("Date", () => {
|
|
test("Date.now() returns a number", async () => {
|
|
expect(await value(`return typeof Date.now()`)).toBe("number")
|
|
})
|
|
|
|
test("epoch construction and ISO rendering", async () => {
|
|
expect(await value(`return new Date(0).toISOString()`)).toBe("1970-01-01T00:00:00.000Z")
|
|
})
|
|
|
|
test("string parsing round-trips", async () => {
|
|
expect(await value(`return new Date("2024-01-02T03:04:05.000Z").getTime()`)).toBe(1704164645000)
|
|
expect(await value(`return Date.parse("2024-01-02T03:04:05.000Z")`)).toBe(1704164645000)
|
|
})
|
|
|
|
test("one-argument construction coerces supported values like JavaScript", async () => {
|
|
expect(
|
|
await value(`return [new Date(true).getTime(), new Date(false).getTime(), new Date(null).getTime()]`),
|
|
).toEqual([1, 0, 0])
|
|
expect(await value(`return Number.isNaN(new Date(undefined).getTime())`)).toBe(true)
|
|
expect(await value(`return Number.isNaN(new Date([]).getTime())`)).toBe(true)
|
|
expect(await value(`return new Date(["1970-01-01T00:00:00.000Z"]).getTime()`)).toBe(0)
|
|
expect(await value(`return Number.isNaN(new Date({}).getTime())`)).toBe(true)
|
|
})
|
|
|
|
test("one-argument construction uses valueOf then toString for objects", async () => {
|
|
expect(
|
|
await value(`
|
|
const calls = []
|
|
const number = { valueOf: () => 8 }
|
|
const text = {
|
|
valueOf: () => { calls.push("valueOf"); return {} },
|
|
toString: () => { calls.push("toString"); return "2016-06-05T18:40:00.000Z" },
|
|
}
|
|
return [new Date(number).getTime(), new Date(text).getTime(), calls]
|
|
`),
|
|
).toEqual([8, 1465152000000, ["valueOf", "toString"]])
|
|
|
|
expect(
|
|
await value(`
|
|
const values = [
|
|
{ valueOf: () => undefined },
|
|
{ valueOf: () => true },
|
|
{ valueOf: () => false },
|
|
{ valueOf: () => null },
|
|
]
|
|
return values.map((item) => new Date(item).getTime())
|
|
`),
|
|
).toEqual([null, 1, 0, 0])
|
|
|
|
expect(
|
|
await value(`
|
|
try {
|
|
new Date({ valueOf: () => ({}), toString: () => ({}) })
|
|
} catch (error) {
|
|
return error.name
|
|
}
|
|
`),
|
|
).toBe("TypeError")
|
|
})
|
|
|
|
test("date arithmetic and comparison use the time value", async () => {
|
|
expect(await value(`const a = new Date(1000); const b = new Date(3000); return b - a`)).toBe(2000)
|
|
expect(await value(`const a = new Date(1000); const b = new Date(3000); return a < b`)).toBe(true)
|
|
expect(await value(`return +new Date(42)`)).toBe(42)
|
|
})
|
|
|
|
test("UTC getters read calendar components", async () => {
|
|
expect(
|
|
await value(
|
|
`const d = new Date("2024-03-05T06:07:08.009Z"); return [d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate(), d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds(), d.getUTCMilliseconds()]`,
|
|
),
|
|
).toEqual([2024, 2, 5, 6, 7, 8, 9])
|
|
})
|
|
|
|
test("invalid dates yield NaN times, guardable in-CodeMode", async () => {
|
|
expect(await value(`return Number.isNaN(new Date("garbage").getTime())`)).toBe(true)
|
|
expect(await value(`return new Date("garbage").toJSON()`)).toBeNull()
|
|
})
|
|
|
|
test("toISOString on an invalid date throws RangeError", async () => {
|
|
expect(await value(`try { new Date("garbage").toISOString() } catch (error) { return error.name }`)).toBe(
|
|
"RangeError",
|
|
)
|
|
})
|
|
|
|
test("template interpolation renders the ISO form", async () => {
|
|
expect(await value("return `at ${new Date(0)}`")).toBe("at 1970-01-01T00:00:00.000Z")
|
|
})
|
|
|
|
test("dates serialize to ISO strings at the boundary, direct and nested", async () => {
|
|
expect(await value(`return new Date(0)`)).toBe("1970-01-01T00:00:00.000Z")
|
|
expect(await value(`return { when: new Date(0), tags: [new Date(1000)] }`)).toEqual({
|
|
when: "1970-01-01T00:00:00.000Z",
|
|
tags: ["1970-01-01T00:00:01.000Z"],
|
|
})
|
|
expect(await value(`return JSON.stringify({ d: new Date(0) })`)).toBe('{"d":"1970-01-01T00:00:00.000Z"}')
|
|
})
|
|
|
|
test("coercions: Number is the time, String is ISO, Boolean is true", async () => {
|
|
expect(await value(`return Number(new Date(5))`)).toBe(5)
|
|
expect(await value(`return String(new Date(0))`)).toBe("1970-01-01T00:00:00.000Z")
|
|
expect(await value(`return Boolean(new Date(0))`)).toBe(true)
|
|
})
|
|
|
|
test("sorting dates with a numeric comparator", async () => {
|
|
expect(
|
|
await value(`
|
|
const dates = [new Date(3000), new Date(1000), new Date(2000)]
|
|
return dates.sort((a, b) => a - b).map((d) => d.getTime())
|
|
`),
|
|
).toEqual([1000, 2000, 3000])
|
|
})
|
|
|
|
test("new Date(year, month, day) accepts component form", async () => {
|
|
expect(await value(`const d = new Date(2024, 0, 2); return [d.getFullYear(), d.getMonth(), d.getDate()]`)).toEqual([
|
|
2024, 0, 2,
|
|
])
|
|
})
|
|
|
|
test("typeof and unknown properties are forgiving", async () => {
|
|
expect(await value(`return typeof new Date(0)`)).toBe("object")
|
|
expect(await value(`return new Date(0).nope === undefined`)).toBe(true)
|
|
})
|
|
|
|
test("toUTCString and toGMTString use the native UTC format", async () => {
|
|
expect(
|
|
await value(`
|
|
const date = new Date(0)
|
|
return [
|
|
date.toUTCString(),
|
|
date.toGMTString(),
|
|
new Date(NaN).toUTCString(),
|
|
new Date("0020-01-01T00:00:00Z").toUTCString(),
|
|
]
|
|
`),
|
|
).toEqual([
|
|
"Thu, 01 Jan 1970 00:00:00 GMT",
|
|
"Thu, 01 Jan 1970 00:00:00 GMT",
|
|
"Invalid Date",
|
|
"Wed, 01 Jan 0020 00:00:00 GMT",
|
|
])
|
|
})
|
|
})
|
|
|
|
describe("RegExp", () => {
|
|
test("literal test", async () => {
|
|
expect(await value(`return /ab+c/.test("xabbbc")`)).toBe(true)
|
|
expect(await value(`return /ab+c/.test("nope")`)).toBe(false)
|
|
})
|
|
|
|
test("exec exposes captures and index", async () => {
|
|
expect(await value(`const m = /a(b+)/.exec("xxabbc"); return { full: m[0], group: m[1], index: m.index }`)).toEqual(
|
|
{
|
|
full: "abb",
|
|
group: "bb",
|
|
index: 2,
|
|
},
|
|
)
|
|
expect(await value(`return /a/.exec("zzz")`)).toBeNull()
|
|
})
|
|
|
|
test("named groups read through", async () => {
|
|
expect(
|
|
await value(`const m = /(?<word>[a-z]+)-(?<num>\\d+)/.exec("id ab-42"); return m.groups.word + m.groups.num`),
|
|
).toBe("ab42")
|
|
})
|
|
|
|
test("global exec advances lastIndex across calls", async () => {
|
|
expect(
|
|
await value(`
|
|
const r = /\\d+/g
|
|
const first = r.exec("a1b22c")
|
|
const second = r.exec("a1b22c")
|
|
return [first[0], second[0]]
|
|
`),
|
|
).toEqual(["1", "22"])
|
|
})
|
|
|
|
test("lastIndex is writable and stores a number", async () => {
|
|
expect(
|
|
await value(`
|
|
const pattern = /(?:ab|cd)\\d?/g
|
|
pattern.lastIndex = "12"
|
|
const stored = [pattern.lastIndex, typeof pattern.lastIndex]
|
|
const match = pattern.exec("aacd2233ab12nm444ab42")
|
|
pattern.lastIndex = 0
|
|
return [stored, match[0], match.index, pattern.lastIndex]
|
|
`),
|
|
).toEqual([[12, "number"], "ab4", 17, 0])
|
|
// lastIndex is a prototype accessor, so delete is a no-op rather than a TypeError.
|
|
expect(await value(`const re = /a/; return [delete re.lastIndex, re.lastIndex]`)).toEqual([true, 0])
|
|
})
|
|
|
|
test("a non-numeric lastIndex runs from 0; non-global exec and test leave it alone", async () => {
|
|
expect(
|
|
await value(`
|
|
const pattern = /a/g
|
|
pattern.lastIndex = {}
|
|
const match = pattern.exec("ba")
|
|
pattern.lastIndex = 10
|
|
const missed = pattern.exec("a")
|
|
const plain = /a/
|
|
plain.lastIndex = 5
|
|
return [match.index, pattern.lastIndex, missed, plain.exec("ba").index, plain.test("ba"), plain.lastIndex]
|
|
`),
|
|
).toEqual([1, 0, null, 1, true, 5])
|
|
})
|
|
|
|
test("String methods read and update lastIndex like exec", async () => {
|
|
expect(
|
|
await value(`
|
|
const re = /a/y
|
|
re.exec("aa")
|
|
re.lastIndex = 0
|
|
return ["aa".replace(re, "b"), re.lastIndex]
|
|
`),
|
|
).toEqual(["ba", 1])
|
|
expect(
|
|
await value(`
|
|
const re = /a/g
|
|
re.exec("aaa")
|
|
return ["aaa".match(re), re.lastIndex]
|
|
`),
|
|
).toEqual([["a", "a", "a"], 0])
|
|
expect(
|
|
await value(`
|
|
const re = /a/g
|
|
re.lastIndex = 2
|
|
return ["aaa".replace(re, () => "b"), re.lastIndex]
|
|
`),
|
|
).toEqual(["bbb", 0])
|
|
expect(
|
|
await value(`
|
|
const re = /a/g
|
|
re.lastIndex = 2
|
|
return ["aaa".replaceAll(re, "b"), re.lastIndex]
|
|
`),
|
|
).toEqual(["bbb", 0])
|
|
expect(
|
|
await value(`
|
|
const re = /a/y
|
|
re.lastIndex = 1
|
|
const m = "baa".match(re)
|
|
return [m.index, re.lastIndex]
|
|
`),
|
|
).toEqual([1, 2])
|
|
})
|
|
|
|
test("split, search, and matchAll leave lastIndex unchanged like JS", async () => {
|
|
expect(
|
|
await value(`
|
|
const re = /a/y
|
|
re.lastIndex = 2
|
|
return ["banana".split(re), re.lastIndex]
|
|
`),
|
|
).toEqual([["b", "n", "n", ""], 2])
|
|
expect(
|
|
await value(`
|
|
const re = /a/g
|
|
re.lastIndex = 2
|
|
return ["banana".search(re), re.lastIndex]
|
|
`),
|
|
).toEqual([1, 2])
|
|
expect(
|
|
await value(`
|
|
const re = /a/y
|
|
re.lastIndex = 2
|
|
return ["banana".search(re), re.lastIndex]
|
|
`),
|
|
).toEqual([-1, 2])
|
|
expect(
|
|
await value(`
|
|
const re = /a/g
|
|
re.lastIndex = 2
|
|
return ["banana".matchAll(re).map((m) => m.index), re.lastIndex]
|
|
`),
|
|
).toEqual([[3, 5], 2])
|
|
expect(
|
|
await value(`
|
|
const re = /a/gy
|
|
re.lastIndex = 1
|
|
return ["banana".matchAll(re).map((m) => m.index), re.lastIndex]
|
|
`),
|
|
).toEqual([[1], 1])
|
|
})
|
|
|
|
test("String methods leave lastIndex untouched without g or y", async () => {
|
|
expect(
|
|
await value(`
|
|
const re = /a/
|
|
re.lastIndex = 5
|
|
return ["aaa".replace(re, "b"), "aaa".match(re).index, "aaa".split(re), "aaa".search(re), re.lastIndex]
|
|
`),
|
|
).toEqual(["baa", 0, ["", "", "", ""], 0, 5])
|
|
})
|
|
|
|
test("an unmatched string pattern returns null", async () => {
|
|
expect(await value(`return "abc".match(/\\d/)`)).toBeNull()
|
|
})
|
|
|
|
test("matchAll materializes match arrays with captures", async () => {
|
|
expect(await value(`return "a1b22".matchAll(/(\\d+)/g).map((m) => m[1])`)).toEqual(["1", "22"])
|
|
})
|
|
|
|
test("function replacers receive captures, offsets, input, and named groups", async () => {
|
|
expect(
|
|
await value(`
|
|
const seen = []
|
|
const output = "a1b22".replace(/(\\d)(\\d)?/g, (match, first, second, offset, input) => {
|
|
seen.push([match, first, second === undefined, offset, input])
|
|
return Number(match) * 2
|
|
})
|
|
return { output, seen }
|
|
`),
|
|
).toEqual({
|
|
output: "a2b44",
|
|
seen: [
|
|
["1", "1", true, 1, "a1b22"],
|
|
["22", "2", false, 3, "a1b22"],
|
|
],
|
|
})
|
|
expect(
|
|
await value(`
|
|
return "red-blue".replace(
|
|
/(?<left>[a-z]+)-(?<right>[a-z]+)/,
|
|
(match, left, right, offset, input, groups) => groups.right + ":" + groups.left,
|
|
)
|
|
`),
|
|
).toBe("blue:red")
|
|
})
|
|
|
|
test("function replacers support string searches, zero-length matches, and result coercion", async () => {
|
|
expect(await value(`return "banana".replace("na", (match, offset, input) => "[" + offset + "]")`)).toBe("ba[2]na")
|
|
expect(await value(`return "ab".replaceAll("", (match, offset) => offset)`)).toBe("0a1b2")
|
|
expect(await value(`return "😀".replaceAll(/(?:)/gu, (match, offset) => "[" + offset + "]")`)).toBe("[0]😀[2]")
|
|
expect(
|
|
await value(`return "123".replace(/\\d/g, (match) => match === "1" ? 7 : match === "2" ? null : { n: 3 })`),
|
|
).toBe("7null[object Object]")
|
|
})
|
|
|
|
test("promise-returning string replacers are coerced synchronously", async () => {
|
|
const decorate = Tool.make({
|
|
description: "Decorate a string",
|
|
input: Schema.String,
|
|
output: Schema.String,
|
|
execute: (input) => Effect.succeed(`[${input}]`),
|
|
})
|
|
const result = await Effect.runPromise(
|
|
CodeMode.execute({
|
|
tools: { host: { decorate } },
|
|
code: `return "a1b22".replace(/\\d+/g, async (match) => await tools.host.decorate(match))`,
|
|
}),
|
|
)
|
|
expect(result.ok && result.value).toBe("a[object Promise]b[object Promise]")
|
|
|
|
const missingAwait = await Effect.runPromise(
|
|
CodeMode.execute({
|
|
tools: { host: { decorate } },
|
|
code: `return "a1".replace(/\\d/, (match) => tools.host.decorate(match))`,
|
|
}),
|
|
)
|
|
expect(missingAwait.ok && missingAwait.value).toBe("a[object Promise]")
|
|
})
|
|
|
|
test("replaceAll without the g flag is a catchable error", async () => {
|
|
expect(await value(`try { "a".replaceAll(/a/, "b"); return "no" } catch { return "caught" }`)).toBe("caught")
|
|
})
|
|
|
|
test("new RegExp constructs from strings; invalid patterns are catchable", async () => {
|
|
expect(await value(`return new RegExp("a+", "i").test("AAA")`)).toBe(true)
|
|
expect(await value(`try { new RegExp("("); return "no" } catch { return "caught" }`)).toBe("caught")
|
|
expect(await value(`return [/a/ instanceof RegExp, /a/.source]`)).toEqual([true, "a"])
|
|
})
|
|
|
|
test("invalid patterns fail with actionable messages", async () => {
|
|
const fromString = await error(`return "abc".match("(")`)
|
|
expect(fromString.message).toContain('String.match received the string "("')
|
|
expect(fromString.message).toContain("escape them with a backslash")
|
|
|
|
const fromConstructor = await error(`return new RegExp("(")`)
|
|
expect(fromConstructor.message).toContain('new RegExp(...) received "("')
|
|
expect(fromConstructor.message).toContain("escape them with a backslash")
|
|
|
|
const fromFlags = await error(`return new RegExp("a", "xz")`)
|
|
expect(fromFlags.message).toContain('invalid flags "xz"')
|
|
expect(fromFlags.message).toContain("Valid flags are")
|
|
})
|
|
|
|
test("missing g-flag errors say how to fix the call", async () => {
|
|
expect((await error(`return "aa".replaceAll(/a/, "b")`)).message).toContain("write /a/g, or use String.replace")
|
|
expect((await error(`return "aa".matchAll(/a/)`)).message).toContain("write /a/g, or use String.match")
|
|
})
|
|
|
|
test("any argument is a pattern string, as new RegExp(arg) reads it", async () => {
|
|
expect(
|
|
await value(
|
|
`return ["a42b".match(42)[0], "xnullx".search(null), "abc".match(undefined), [..."1a1".matchAll(1)].length]`,
|
|
),
|
|
).toEqual(["42", 1, [""], 2])
|
|
})
|
|
|
|
test("source and flags properties read through", async () => {
|
|
expect(await value(`const r = /ab/gi; return { source: r.source, flags: r.flags, global: r.global }`)).toEqual({
|
|
source: "ab",
|
|
flags: "gi",
|
|
global: true,
|
|
})
|
|
})
|
|
|
|
test("regexes serialize as {} like JSON.stringify, at the boundary and inside the program", async () => {
|
|
expect(await value(`return [/a/, { r: /b/gi }]`)).toEqual([{}, { r: {} }])
|
|
expect(await value(`return JSON.stringify({ r: /a/g })`)).toBe('{"r":{}}')
|
|
})
|
|
|
|
test("template interpolation renders the literal form", async () => {
|
|
expect(await value("return `${/ab/g}`")).toBe("/ab/g")
|
|
})
|
|
})
|
|
|
|
describe("URL and URI helpers", () => {
|
|
test("encodes and decodes complete URIs and URI components", async () => {
|
|
expect(
|
|
await value(`
|
|
return [
|
|
encodeURI("https://example.test/a b?q=a/b"),
|
|
encodeURIComponent("a b/c?"),
|
|
decodeURI("https://example.test/a%20b?q=a/b"),
|
|
decodeURIComponent("a%20b%2Fc%3F"),
|
|
["a b", "c/d"].map(encodeURIComponent),
|
|
]
|
|
`),
|
|
).toEqual([
|
|
"https://example.test/a%20b?q=a/b",
|
|
"a%20b%2Fc%3F",
|
|
"https://example.test/a b?q=a/b",
|
|
"a b/c?",
|
|
["a%20b", "c%2Fd"],
|
|
])
|
|
expect(
|
|
await value(`try { decodeURIComponent("%zz"); return false } catch (error) { return error instanceof URIError }`),
|
|
).toBe(true)
|
|
})
|
|
|
|
test("resolves and mutates URLs with linked search parameters", async () => {
|
|
expect(
|
|
await value(`
|
|
const url = new URL("../users?id=old#top", "https://user:pass@example.com:8443/api/v1/")
|
|
url.pathname = "/items/a b"
|
|
url.searchParams.set("id", "a b")
|
|
url.searchParams.append("tag", "x/y")
|
|
url.hash = "part 1"
|
|
return {
|
|
href: url.href,
|
|
origin: url.origin,
|
|
host: url.host,
|
|
pathname: url.pathname,
|
|
search: url.search,
|
|
id: url.searchParams.get("id"),
|
|
string: String(url),
|
|
json: url.toJSON(),
|
|
instances: [
|
|
url instanceof URL,
|
|
url.searchParams instanceof URLSearchParams,
|
|
url.searchParams === url.searchParams,
|
|
],
|
|
}
|
|
`),
|
|
).toEqual({
|
|
href: "https://user:pass@example.com:8443/items/a%20b?id=a+b&tag=x%2Fy#part%201",
|
|
origin: "https://example.com:8443",
|
|
host: "example.com:8443",
|
|
pathname: "/items/a%20b",
|
|
search: "?id=a+b&tag=x%2Fy",
|
|
id: "a b",
|
|
string: "https://user:pass@example.com:8443/items/a%20b?id=a+b&tag=x%2Fy#part%201",
|
|
json: "https://user:pass@example.com:8443/items/a%20b?id=a+b&tag=x%2Fy#part%201",
|
|
instances: [true, true, true],
|
|
})
|
|
})
|
|
|
|
test("URLSearchParams supports records, pairs, mutation, callbacks, and materialization", async () => {
|
|
expect(
|
|
await value(`
|
|
const params = new URLSearchParams([["tag", "b"], ["tag", "a"], ["q", "a b"]])
|
|
const seen = []
|
|
params.forEach((value, key) => seen.push(key + "=" + value))
|
|
params.delete("tag", "b")
|
|
params.append("tag", "c")
|
|
params.sort()
|
|
return {
|
|
text: params.toString(),
|
|
size: params.size,
|
|
tags: params.getAll("tag"),
|
|
has: params.has("tag", "c"),
|
|
entries: Array.from(params),
|
|
object: Object.fromEntries(params),
|
|
record: new URLSearchParams({ page: 2, filter: "open" }).toString(),
|
|
seen,
|
|
}
|
|
`),
|
|
).toEqual({
|
|
text: "q=a+b&tag=a&tag=c",
|
|
size: 3,
|
|
tags: ["a", "c"],
|
|
has: true,
|
|
entries: [
|
|
["q", "a b"],
|
|
["tag", "a"],
|
|
["tag", "c"],
|
|
],
|
|
object: { q: "a b", tag: "c" },
|
|
record: "page=2&filter=open",
|
|
seen: ["tag=b", "tag=a", "q=a b"],
|
|
})
|
|
})
|
|
|
|
test("URL parsing failures are catchable and values use native JSON forms", async () => {
|
|
expect(
|
|
await value(`
|
|
const parsed = URL.parse("/users", "https://example.test/api/")
|
|
let invalidIsTypeError = false
|
|
try { new URL("not relative without a base") } catch (error) { invalidIsTypeError = error instanceof TypeError }
|
|
return {
|
|
canParse: URL.canParse("/users", "https://example.test/api/"),
|
|
cannotParse: URL.canParse("not relative without a base"),
|
|
parsed: parsed.href,
|
|
invalidIsTypeError,
|
|
boundary: [new URL("https://example.test/a"), new URLSearchParams("q=one")],
|
|
json: JSON.stringify({ url: new URL("https://example.test/a"), params: new URLSearchParams("q=one") }),
|
|
}
|
|
`),
|
|
).toEqual({
|
|
canParse: true,
|
|
cannotParse: false,
|
|
parsed: "https://example.test/users",
|
|
invalidIsTypeError: true,
|
|
boundary: ["https://example.test/a", "q=one"],
|
|
json: '{"url":"https://example.test/a","params":{}}',
|
|
})
|
|
})
|
|
|
|
test("distinguishes omitted URL arguments from explicit undefined", async () => {
|
|
expect(
|
|
await value(`
|
|
function throwsTypeError(run) {
|
|
try { run(); return false } catch (error) { return error instanceof TypeError }
|
|
}
|
|
const params = new URLSearchParams()
|
|
const required = [
|
|
() => params.append(),
|
|
() => params.delete(),
|
|
() => params.get(),
|
|
() => params.getAll(),
|
|
() => params.has(),
|
|
() => params.set(),
|
|
() => params.forEach(),
|
|
].map(throwsTypeError)
|
|
params.append(undefined, undefined)
|
|
return {
|
|
construct: throwsTypeError(() => new URL()),
|
|
canParse: throwsTypeError(() => URL.canParse()),
|
|
parse: throwsTypeError(() => URL.parse()),
|
|
explicitUndefined: new URL(undefined, "https://example.test/base/").href,
|
|
params: params.toString(),
|
|
required,
|
|
}
|
|
`),
|
|
).toEqual({
|
|
construct: true,
|
|
canParse: true,
|
|
parse: true,
|
|
explicitUndefined: "https://example.test/base/undefined",
|
|
params: "undefined=undefined",
|
|
required: [true, true, true, true, true, true, true],
|
|
})
|
|
})
|
|
})
|
|
|
|
describe("Headers", () => {
|
|
test("constructs from records, pairs, Maps, and Headers; names fold to lowercase and values combine", async () => {
|
|
expect(
|
|
await value(`
|
|
const headers = new Headers({ "Content-Type": "text/plain", "X-Count": 1, "X-Null": null })
|
|
headers.append("Accept", "text/html")
|
|
headers.append("accept", "application/json")
|
|
headers.set("x-count", "2")
|
|
headers.delete("x-null")
|
|
const copy = new Headers(headers)
|
|
copy.set("content-type", "text/html")
|
|
return {
|
|
get: headers.get("content-type"),
|
|
missing: headers.get("x-missing"),
|
|
combined: headers.get("ACCEPT"),
|
|
has: [headers.has("Accept"), headers.has("x-null")],
|
|
count: headers.get("x-count"),
|
|
copied: [headers.get("content-type"), copy.get("content-type")],
|
|
pairs: [...new Headers([["b", "2"], ["A", "1"]])],
|
|
map: [...new Headers(new Map([["k", "v"]]))],
|
|
keys: [...headers.keys()],
|
|
values: [...headers.values()],
|
|
entries: [...headers.entries()],
|
|
}
|
|
`),
|
|
).toEqual({
|
|
get: "text/plain",
|
|
missing: null,
|
|
combined: "text/html, application/json",
|
|
has: [true, false],
|
|
count: "2",
|
|
copied: ["text/plain", "text/html"],
|
|
pairs: [
|
|
["a", "1"],
|
|
["b", "2"],
|
|
],
|
|
map: [["k", "v"]],
|
|
keys: ["accept", "content-type", "x-count"],
|
|
values: ["text/html, application/json", "text/plain", "2"],
|
|
entries: [
|
|
["accept", "text/html, application/json"],
|
|
["content-type", "text/plain"],
|
|
["x-count", "2"],
|
|
],
|
|
})
|
|
})
|
|
|
|
test("iterates in sorted order everywhere iteration is allowed, and getSetCookie keeps cookies apart", async () => {
|
|
expect(
|
|
await value(`
|
|
const headers = new Headers({ b: "2", a: "1" })
|
|
headers.append("Set-Cookie", "x=1")
|
|
headers.append("set-cookie", "y=2")
|
|
const seen = []
|
|
headers.forEach((value, name, self) => seen.push(name + "=" + value + ":" + (self === headers)))
|
|
const [first] = headers
|
|
function* pairs() { yield* headers }
|
|
return {
|
|
seen,
|
|
first,
|
|
spread: [...headers],
|
|
from: Array.from(headers).length,
|
|
generator: [...pairs()].length,
|
|
object: Object.fromEntries(headers),
|
|
cookies: headers.getSetCookie(),
|
|
}
|
|
`),
|
|
).toEqual({
|
|
seen: ["a=1:true", "b=2:true", "set-cookie=x=1:true", "set-cookie=y=2:true"],
|
|
first: ["a", "1"],
|
|
spread: [
|
|
["a", "1"],
|
|
["b", "2"],
|
|
["set-cookie", "x=1"],
|
|
["set-cookie", "y=2"],
|
|
],
|
|
from: 4,
|
|
generator: 4,
|
|
object: { a: "1", b: "2", "set-cookie": "y=2" },
|
|
cookies: ["x=1", "y=2"],
|
|
})
|
|
})
|
|
|
|
test("serializes as a name-to-value object at the boundary and in JSON; prints for console", async () => {
|
|
const result = await run(`
|
|
const headers = new Headers({ "X-A": "1", b: "2" })
|
|
console.log(headers)
|
|
return { headers, json: JSON.stringify({ headers }), text: String(headers), type: typeof headers, is: headers instanceof Headers }
|
|
`)
|
|
expect(result.ok && result.value).toEqual({
|
|
headers: { b: "2", "x-a": "1" },
|
|
json: '{"headers":{"b":"2","x-a":"1"}}',
|
|
text: "[object Headers]",
|
|
type: "object",
|
|
is: true,
|
|
})
|
|
expect(result.ok && result.logs?.[0]).toBe('Headers {"b":"2","x-a":"1"}')
|
|
})
|
|
|
|
test("rejects what it cannot build from, and invalid names and values, with TypeErrors the program can catch", async () => {
|
|
expect(
|
|
await value(`
|
|
function message(run) {
|
|
try { run(); return null } catch (error) { return error instanceof TypeError ? error.message : error }
|
|
}
|
|
const headers = new Headers()
|
|
return [
|
|
message(() => Headers()),
|
|
message(() => new Headers(null)),
|
|
message(() => new Headers(1)),
|
|
message(() => new Headers("a=1")),
|
|
message(() => new Headers(new Date())),
|
|
message(() => new Headers(() => 1)),
|
|
message(() => new Headers([["name"]])),
|
|
message(() => new Headers([["a", "b", "c"]])),
|
|
message(() => new Headers({ "bad name": "x" })),
|
|
message(() => new Headers({ name: "bad\u0000value" })),
|
|
message(() => headers.get("invalid\u0100")),
|
|
message(() => headers.has({})),
|
|
message(() => headers.set("a", "invalid\u0100")),
|
|
message(() => headers.append("a")),
|
|
message(() => headers.forEach()),
|
|
message(() => headers.forEach(1)),
|
|
message(() => { const get = headers.get; return get("a") }),
|
|
]
|
|
`),
|
|
).toEqual([
|
|
"Constructor Headers requires 'new'.",
|
|
"new Headers(...) expects a record of names to values, iterable [name, value] pairs, or Headers.",
|
|
"new Headers(...) expects a record of names to values, iterable [name, value] pairs, or Headers.",
|
|
"new Headers(...) expects a record of names to values, iterable [name, value] pairs, or Headers.",
|
|
"new Headers(...) expects a record of names to values, iterable [name, value] pairs, or Headers.",
|
|
"new Headers(...) expects a record of names to values, iterable [name, value] pairs, or Headers.",
|
|
"new Headers(...) expects iterable [name, value] pairs.",
|
|
"new Headers(...) expects iterable [name, value] pairs.",
|
|
expect.stringContaining("bad name"),
|
|
expect.stringContaining("invalid value"),
|
|
expect.stringContaining("Invalid header name"),
|
|
expect.stringContaining("[object Object]"),
|
|
expect.stringContaining("invalid value"),
|
|
"Headers.append requires 2 arguments.",
|
|
"Headers.forEach requires 1 argument.",
|
|
"Headers.forEach expects a function callback.",
|
|
"Headers.prototype.get called on incompatible receiver undefined.",
|
|
])
|
|
})
|
|
})
|
|
|
|
describe("Map", () => {
|
|
test("get/set/has/size with chaining", async () => {
|
|
expect(
|
|
await value(`
|
|
const m = new Map()
|
|
m.set("a", 1).set("b", 2)
|
|
return { a: m.get("a"), b: m.get("b"), has: m.has("a"), miss: m.get("zz") === undefined, size: m.size }
|
|
`),
|
|
).toEqual({ a: 1, b: 2, has: true, miss: true, size: 5 - 3 })
|
|
})
|
|
|
|
test("object keys use identity", async () => {
|
|
expect(
|
|
await value(`
|
|
const key = { id: 1 }
|
|
const m = new Map()
|
|
m.set(key, "hit")
|
|
return [m.get(key), m.get({ id: 1 }) === undefined]
|
|
`),
|
|
).toEqual(["hit", true])
|
|
})
|
|
|
|
test("construction from entry pairs and another Map", async () => {
|
|
expect(await value(`const m = new Map([["a", 1], ["b", 2]]); return m.get("b")`)).toBe(2)
|
|
expect(
|
|
await value(
|
|
`const m = new Map([["a", 1]]); const n = new Map(m); n.set("b", 2); return [n.get("a"), n.get("b"), m.has("b")]`,
|
|
),
|
|
).toEqual([1, 2, false])
|
|
expect((await error(`return new Map("nope")`)).message).toMatch(/\[key, value\] pairs/)
|
|
expect((await error(`return new Map(["flat"])`)).message).toMatch(/\[key, value\] pairs/)
|
|
})
|
|
|
|
test("keys/values/entries return live iterators", async () => {
|
|
expect(
|
|
await value(`
|
|
const m = new Map([["a", 1], ["b", 2]])
|
|
const keys = m.keys()
|
|
const first = keys.next()
|
|
m.set("c", 3)
|
|
return { first, rest: [...keys], values: [...m.values()], entries: [...m.entries()], same: [...m[Symbol.iterator]()] }
|
|
`),
|
|
).toEqual({
|
|
first: { value: "a", done: false },
|
|
rest: ["b", "c"],
|
|
values: [1, 2, 3],
|
|
entries: [
|
|
["a", 1],
|
|
["b", 2],
|
|
["c", 3],
|
|
],
|
|
same: [
|
|
["a", 1],
|
|
["b", 2],
|
|
["c", 3],
|
|
],
|
|
})
|
|
})
|
|
|
|
test("Object.fromEntries(map) and Array.from(map)", async () => {
|
|
expect(await value(`return Object.fromEntries(new Map([["a", 1], ["b", 2]]))`)).toEqual({ a: 1, b: 2 })
|
|
expect(await value(`return Array.from(new Map([["a", 1]]))`)).toEqual([["a", 1]])
|
|
})
|
|
|
|
test("for...of iterates [key, value] pairs with destructuring", async () => {
|
|
expect(
|
|
await value(`
|
|
const m = new Map([["a", 1], ["b", 2]])
|
|
let total = 0
|
|
let names = ""
|
|
for (const [key, count] of m) { names += key; total += count }
|
|
return names + total
|
|
`),
|
|
).toBe("ab3")
|
|
})
|
|
|
|
test("spread produces entry pairs", async () => {
|
|
expect(await value(`return [...new Map([["a", 1]])]`)).toEqual([["a", 1]])
|
|
})
|
|
|
|
test("forEach passes (value, key)", async () => {
|
|
expect(
|
|
await value(`
|
|
const m = new Map([["a", 1], ["b", 2]])
|
|
const seen = []
|
|
m.forEach((count, key) => seen.push(key + count))
|
|
return seen
|
|
`),
|
|
).toEqual(["a1", "b2"])
|
|
})
|
|
|
|
test("delete and clear", async () => {
|
|
expect(
|
|
await value(`
|
|
const m = new Map([["a", 1], ["b", 2]])
|
|
const removed = m.delete("a")
|
|
const missed = m.delete("zz")
|
|
const sizeAfterDelete = m.size
|
|
m.clear()
|
|
return [removed, missed, sizeAfterDelete, m.size]
|
|
`),
|
|
).toEqual([true, false, 1, 0])
|
|
})
|
|
|
|
test("counting idiom: grouped tallies", async () => {
|
|
expect(
|
|
await value(`
|
|
const words = ["a", "b", "a", "c", "a"]
|
|
const counts = new Map()
|
|
for (const word of words) counts.set(word, (counts.get(word) ?? 0) + 1)
|
|
return Object.fromEntries(counts)
|
|
`),
|
|
).toEqual({ a: 3, b: 1, c: 1 })
|
|
})
|
|
|
|
test("getOrInsert and getOrInsertComputed insert only when the key is missing", async () => {
|
|
expect(
|
|
await value(`
|
|
const groups = new Map()
|
|
groups.getOrInsert("a", []).push(1)
|
|
groups.getOrInsert("a", []).push(2)
|
|
let calls = 0
|
|
const computed = (key) => { calls++; return key + "!" }
|
|
const first = groups.getOrInsertComputed("b", computed)
|
|
const second = groups.getOrInsertComputed("b", computed)
|
|
const zero = groups.getOrInsertComputed(-0, (key) => 1 / key === Infinity)
|
|
return [[...groups], first, second, calls, zero]
|
|
`),
|
|
).toEqual([
|
|
[
|
|
["a", [1, 2]],
|
|
["b", "b!"],
|
|
[0, true],
|
|
],
|
|
"b!",
|
|
"b!",
|
|
1,
|
|
true,
|
|
])
|
|
expect(
|
|
await value(`
|
|
const m = new Map()
|
|
const outer = m.getOrInsertComputed("k", () => { m.set("k", "inner"); return "outer" })
|
|
let thrown
|
|
try { m.getOrInsertComputed("j", () => { throw new Error("boom") }) } catch (error) { thrown = error.message }
|
|
return [outer, m.get("k"), thrown, m.has("j")]
|
|
`),
|
|
).toEqual(["outer", "outer", "boom", false])
|
|
expect((await error(`new Map().getOrInsertComputed("k", 5)`)).message).toContain("expects a function callback")
|
|
})
|
|
|
|
test("maps serialize to {} at the boundary, like JSON", async () => {
|
|
expect(await value(`return new Map([["a", 1]])`)).toEqual({})
|
|
expect(await value(`return JSON.stringify(new Map([["a", 1]]))`)).toBe("{}")
|
|
})
|
|
|
|
test("console.log renders map contents for debugging", async () => {
|
|
const result = await run(`console.log(new Map([["a", 1]])); return null`)
|
|
expect(result.ok).toBe(true)
|
|
expect(result.logs?.[0]).toBe(`Map(1) [["a",1]]`)
|
|
})
|
|
})
|
|
|
|
describe("Set", () => {
|
|
test("forEach is live on Map and Set: deleted entries are skipped and added ones visited", async () => {
|
|
expect(
|
|
await value(`
|
|
const m = new Map([[1, "a"], [2, "b"]])
|
|
const s = new Set([1, 2])
|
|
const seen = []
|
|
m.forEach((v, k) => { seen.push(k); if (k === 1) { m.delete(2); m.set(3, "c") } })
|
|
s.forEach((v) => { seen.push(v); if (v === 1) { s.delete(2); s.add(3) } })
|
|
return seen
|
|
`),
|
|
).toEqual([1, 3, 1, 3])
|
|
})
|
|
|
|
test("add/has/delete/size with chaining", async () => {
|
|
expect(
|
|
await value(`
|
|
const s = new Set()
|
|
s.add(1).add(2).add(1)
|
|
const removed = s.delete(2)
|
|
return [s.size, s.has(1), s.has(2), removed]
|
|
`),
|
|
).toEqual([1, true, false, true])
|
|
})
|
|
|
|
test("dedupe idiom: [...new Set(items)]", async () => {
|
|
expect(await value(`return [...new Set([1, 2, 2, 3, 1])]`)).toEqual([1, 2, 3])
|
|
})
|
|
|
|
test("construction from strings and other Sets", async () => {
|
|
expect(await value(`return [...new Set("aba")]`)).toEqual(["a", "b"])
|
|
expect(await value(`return Array.from(new Set(new Set([1, 2])))`)).toEqual([1, 2])
|
|
})
|
|
|
|
test("SameValueZero: NaN is findable", async () => {
|
|
expect(await value(`const s = new Set([NaN]); return s.has(NaN)`)).toBe(true)
|
|
})
|
|
|
|
test("for...of iterates values", async () => {
|
|
expect(
|
|
await value(`
|
|
let total = 0
|
|
for (const n of new Set([1, 2, 3])) total += n
|
|
return total
|
|
`),
|
|
).toBe(6)
|
|
})
|
|
|
|
test("sets cross the boundary as arrays; JSON.stringify keeps {} like JS", async () => {
|
|
expect(await value(`return { s: new Set([1, "a", { n: 1 }, undefined]) }`)).toEqual({ s: [1, "a", { n: 1 }, null] })
|
|
expect(await value(`return JSON.stringify(new Set([1]))`)).toBe("{}")
|
|
})
|
|
})
|
|
|
|
describe("Uint8Array", () => {
|
|
test("constructs from a length, an array, an iterable, or another Uint8Array", async () => {
|
|
expect(
|
|
await value(`
|
|
const a = new Uint8Array(2)
|
|
const b = new Uint8Array([1, 2, 300])
|
|
const c = Uint8Array.from(new Set([7, 8]))
|
|
const d = new Uint8Array(b)
|
|
d[0] = 9
|
|
return [[...a], [...b], [...c], [...d], [...b], new Uint8Array("3").length, [...Uint8Array.of(1, "2")]]
|
|
`),
|
|
).toEqual([[0, 0], [1, 2, 44], [7, 8], [9, 2, 44], [1, 2, 44], 3, [1, 2]])
|
|
expect((await error(`new Uint8Array(-1)`)).message).toContain("Invalid typed array length: -1")
|
|
expect((await error(`new Uint8Array(1.5)`)).message).toContain("Invalid typed array length")
|
|
expect((await error(`new Uint8Array(20_000_000)`)).message).toContain("Invalid array length")
|
|
expect((await error(`Uint8Array.from(/x/)`)).message).toContain("received a RegExp")
|
|
})
|
|
|
|
test("index reads and writes clamp to a byte and ignore out-of-range writes, like JS", async () => {
|
|
expect(
|
|
await value(`
|
|
const b = new Uint8Array(3)
|
|
b[0] = 300
|
|
b[1] = -1
|
|
b["2"] = "7"
|
|
b[5] = 9
|
|
b.tag = "x"
|
|
return [b[0], b[1], b[2], b[5], b.length, Object.keys(b), 1 in b, 5 in b, b.tag]
|
|
`),
|
|
).toEqual([44, 255, 7, null, 3, ["0", "1", "2", "tag"], true, false, "x"])
|
|
expect((await error(`delete new Uint8Array(1)[0]`)).message).toContain("Cannot delete property '0'")
|
|
expect((await error(`Uint8Array.prototype.length`)).message).toContain("incompatible receiver")
|
|
})
|
|
|
|
test("iteration, spread, destructuring, and Array.from", async () => {
|
|
expect(
|
|
await value(`
|
|
const b = new Uint8Array([1, 2, 3])
|
|
const [x, ...rest] = b
|
|
const seen = []
|
|
for (const byte of b) seen.push(byte)
|
|
function* g() { yield* b }
|
|
return [x, rest, seen, [...g()], Array.from(b, (n) => n * 2), new Set(b).size, Array.isArray(b)]
|
|
`),
|
|
).toEqual([1, [2, 3], [1, 2, 3], [1, 2, 3], [2, 4, 6], 3, false])
|
|
})
|
|
|
|
test("methods", async () => {
|
|
expect(
|
|
await value(`
|
|
const b = Uint8Array.from([1, 2, 3, 2])
|
|
const view = b.subarray(1, 3)
|
|
view.fill(9)
|
|
const copy = b.slice(0, 2)
|
|
copy[0] = 5
|
|
const target = new Uint8Array(4)
|
|
target.set([1, 2], 2)
|
|
target.set(b.subarray(0, 1))
|
|
return [
|
|
b.at(-1), [...b], [...copy], [...target], b.indexOf(9), b.lastIndexOf(9), b.includes(3), b.includes(2, 1),
|
|
b.join("-"), b.toString(), [...b.keys()], [...b.values()], [...b.entries()], [...Uint8Array.from([1, 2]).reverse()],
|
|
]
|
|
`),
|
|
).toEqual([
|
|
2,
|
|
[1, 9, 9, 2],
|
|
[5, 9],
|
|
[1, 0, 1, 2],
|
|
1,
|
|
2,
|
|
false,
|
|
true,
|
|
"1-9-9-2",
|
|
"1,9,9,2",
|
|
[0, 1, 2, 3],
|
|
[1, 9, 9, 2],
|
|
[
|
|
[0, 1],
|
|
[1, 9],
|
|
[2, 9],
|
|
[3, 2],
|
|
],
|
|
[2, 1],
|
|
])
|
|
expect((await error(`new Uint8Array(2).set([1, 2, 3])`)).message).toContain("does not fit")
|
|
expect((await error(`new Uint8Array(2).set([1], 2)`)).message).toContain("does not fit")
|
|
})
|
|
|
|
test("base64 and hex", async () => {
|
|
expect(
|
|
await value(`
|
|
return [
|
|
new Uint8Array([104, 105]).toBase64(), new Uint8Array([255, 0]).toHex(),
|
|
[...Uint8Array.fromBase64("aGk=")], [...Uint8Array.fromHex("ff00")],
|
|
]
|
|
`),
|
|
).toEqual(["aGk=", "ff00", [104, 105], [255, 0]])
|
|
expect(await error(`Uint8Array.fromBase64("!!!")`)).toMatchObject({
|
|
message: expect.stringContaining("SyntaxError"),
|
|
})
|
|
expect(await error(`Uint8Array.fromHex("zz")`)).toMatchObject({ message: expect.stringContaining("SyntaxError") })
|
|
})
|
|
|
|
test("coercion, console, JSON, and Object.prototype.toString", async () => {
|
|
expect(
|
|
await value(`
|
|
const b = new Uint8Array([1, 2])
|
|
console.log(b, new Uint8Array())
|
|
return [String(b), b + "", +new Uint8Array([5]), Number.isNaN(Number(b)), b == "1,2", JSON.stringify(b), b.toLocaleString(), typeof b, b instanceof Uint8Array]
|
|
`),
|
|
).toEqual(["1,2", "1,2", 5, true, true, '{"0":1,"1":2}', "1,2", "object", true])
|
|
expect((await run(`console.log(new Uint8Array([1, 2]), new Uint8Array())`)).logs).toEqual([
|
|
"Uint8Array(2) [1,2] Uint8Array(0) []",
|
|
])
|
|
})
|
|
|
|
test("cannot cross the tool boundary; the error says how to encode it", async () => {
|
|
expect((await error(`return new Uint8Array(1)`)).message).toContain("pass text instead")
|
|
expect((await error(`return { deep: [new Uint8Array(1)] }`)).message).toContain("bytes.toBase64()")
|
|
expect(await value(`return new Uint8Array([7, 8]).toBase64()`)).toBe("Bwg=")
|
|
})
|
|
})
|
|
|
|
describe("TextEncoder and TextDecoder", () => {
|
|
test("round-trips UTF-8 and exposes the standard fields", async () => {
|
|
expect(
|
|
await value(`
|
|
const encoder = new TextEncoder()
|
|
const bytes = encoder.encode("héllo ✓")
|
|
const decoder = new TextDecoder()
|
|
return [
|
|
[...bytes], decoder.decode(bytes), decoder.decode(), [...encoder.encode()], encoder.encoding,
|
|
decoder.encoding, decoder.fatal, decoder.ignoreBOM, new TextDecoder("UTF8", { fatal: true }).fatal,
|
|
new TextDecoder().decode(new Uint8Array([0xef, 0xbb, 0xbf, 0x41])),
|
|
new TextDecoder("utf-8", { ignoreBOM: true }).decode(new Uint8Array([0xef, 0xbb, 0xbf, 0x41])),
|
|
new TextDecoder().decode(new Uint8Array([0xff])),
|
|
encoder instanceof TextEncoder, decoder instanceof TextDecoder,
|
|
]
|
|
`),
|
|
).toEqual([
|
|
[104, 195, 169, 108, 108, 111, 32, 226, 156, 147],
|
|
"héllo ✓",
|
|
"",
|
|
[],
|
|
"utf-8",
|
|
"utf-8",
|
|
false,
|
|
false,
|
|
true,
|
|
"A",
|
|
"\ufeffA",
|
|
"\ufffd",
|
|
true,
|
|
true,
|
|
])
|
|
})
|
|
|
|
test("only UTF-8 is supported; fatal decoding rejects malformed input", async () => {
|
|
expect((await error(`new TextDecoder("latin1")`)).message).toContain('The "latin1" encoding is not supported')
|
|
expect((await error(`new TextDecoder("utf-8", { fatal: true }).decode(new Uint8Array([0xff]))`)).message).toContain(
|
|
"not valid utf-8",
|
|
)
|
|
expect((await error(`new TextDecoder().decode("text")`)).message).toContain(
|
|
"expects a Uint8Array, received a string",
|
|
)
|
|
expect((await error(`TextDecoder()`)).message).toContain("requires 'new'")
|
|
})
|
|
|
|
test("crypto.getRandomValues fills the given bytes in place", async () => {
|
|
expect(
|
|
await value(
|
|
`const b = new Uint8Array(16); const same = crypto.getRandomValues(b) === b; return [same, b.length, b.some((n) => n !== 0)]`,
|
|
),
|
|
).toEqual([true, 16, true])
|
|
expect((await error(`crypto.getRandomValues([1])`)).message).toContain("expects a Uint8Array, received an array")
|
|
})
|
|
})
|
|
|
|
describe("Iterator helpers", () => {
|
|
test("lazy helpers chain over collection iterators and generators, one source step per result", async () => {
|
|
expect(
|
|
await value(`
|
|
const pulled = []
|
|
function* naturals() { let n = 0; while (true) { pulled.push(n); yield n++ } }
|
|
const squares = naturals().map((n) => n * n).filter((n) => n % 2 === 0).drop(1).take(3)
|
|
const first = squares.next()
|
|
return {
|
|
first,
|
|
rest: squares.toArray(),
|
|
after: squares.next(),
|
|
pulled,
|
|
values: new Map([["a", 1], ["b", 2]]).values().map((v, i) => v * 10 + i).toArray(),
|
|
flat: [1, 2].values().flatMap((n) => [n, [n]]).toArray(),
|
|
entries: [...new Map([[1, 2]]).entries().map(([k, v]) => k + v)],
|
|
}
|
|
`),
|
|
).toEqual({
|
|
first: { value: 4, done: false },
|
|
rest: [16, 36],
|
|
after: { done: true },
|
|
pulled: [0, 1, 2, 3, 4, 5, 6],
|
|
values: [10, 21],
|
|
flat: [1, [1], 2, [2]],
|
|
entries: [3],
|
|
})
|
|
})
|
|
|
|
test("eager helpers consume the source and close it on early exit", async () => {
|
|
expect(
|
|
await value(`
|
|
const log = []
|
|
function* g() { try { yield 1; yield 2; yield 3 } finally { log.push("closed") } }
|
|
const seen = []
|
|
g().forEach((v, i) => seen.push([v, i]))
|
|
return {
|
|
seen,
|
|
sum: g().reduce((a, b) => a + b),
|
|
sumFrom: g().reduce((a, b) => a + b, 10),
|
|
some: g().some((v) => v === 2),
|
|
every: g().every((v) => v < 2),
|
|
find: g().find((v) => v > 1),
|
|
missing: g().find((v) => v > 5),
|
|
log,
|
|
}
|
|
`),
|
|
).toEqual({
|
|
seen: [
|
|
[1, 0],
|
|
[2, 1],
|
|
[3, 2],
|
|
],
|
|
sum: 6,
|
|
sumFrom: 16,
|
|
some: true,
|
|
every: false,
|
|
find: 2,
|
|
log: ["closed", "closed", "closed", "closed", "closed", "closed", "closed"],
|
|
})
|
|
})
|
|
|
|
test("a helper closes its source when a callback throws, on return(), and on early for...of exit", async () => {
|
|
expect(
|
|
await value(`
|
|
const log = []
|
|
function* g(name) { try { yield 1; yield 2 } finally { log.push(name) } }
|
|
const throwing = g("throw").map((v) => { if (v === 2) throw new Error("boom"); return v })
|
|
throwing.next()
|
|
let message
|
|
try { throwing.next() } catch (error) { message = error.message }
|
|
const returned = g("return").map((v) => v)
|
|
returned.next()
|
|
const closed = returned.return()
|
|
for (const v of g("loop").filter((v) => true)) break
|
|
return { message, afterThrow: throwing.next(), closed, afterReturn: returned.next(), log }
|
|
`),
|
|
).toEqual({
|
|
message: "boom",
|
|
afterThrow: { done: true },
|
|
closed: { done: true },
|
|
afterReturn: { done: true },
|
|
log: ["throw", "return", "loop"],
|
|
})
|
|
})
|
|
|
|
test("collection iterators have no return() and continue after an early exit, as in JS", async () => {
|
|
expect(
|
|
await value(`
|
|
const it = [1, 2, 3].values()
|
|
const found = it.some((v) => v === 2)
|
|
return [typeof it.return, found, it.next().value, typeof it.map(x => x).return]
|
|
`),
|
|
).toEqual(["undefined", true, 3, "function"])
|
|
})
|
|
|
|
test("Iterator.from and the abstract Iterator constructor", async () => {
|
|
expect(
|
|
await value(`
|
|
const it = [1].values()
|
|
let n = 0
|
|
const errors = []
|
|
for (const attempt of [() => Iterator(), () => new Iterator(), () => Iterator.from(5)]) {
|
|
try { attempt() } catch (error) { errors.push(error.name) }
|
|
}
|
|
return [
|
|
Iterator.from(it) === it,
|
|
Iterator.from("ab").toArray(),
|
|
Iterator.from([1, 2]).map((v) => v * 2).toArray(),
|
|
Iterator.from({ next: () => ({ done: n > 1, value: n++ }) }).toArray(),
|
|
it instanceof Iterator,
|
|
errors,
|
|
]
|
|
`),
|
|
).toEqual([true, ["a", "b"], [2, 4], [0, 1], true, ["TypeError", "TypeError", "TypeError"]])
|
|
})
|
|
|
|
test("argument validation", async () => {
|
|
const cases: Array<[string, string]> = [
|
|
["[1].values().map(1)", "Iterator.prototype.map expects a function callback."],
|
|
["[1].values().take(-1)", "Iterator.prototype.take expects a non-negative count, received -1."],
|
|
["[1].values().drop()", "Iterator.prototype.drop expects a non-negative count, received NaN."],
|
|
[
|
|
"[1].values().flatMap((v) => 'ab').toArray()",
|
|
"Iterator.prototype.flatMap expects an iterable or iterator, received a string.",
|
|
],
|
|
["[].values().reduce((a, b) => a)", "Iterator.prototype.reduce of an empty iterator with no initial value."],
|
|
]
|
|
for (const [code, message] of cases) {
|
|
expect((await error(`return ${code}`)).message).toContain(message)
|
|
}
|
|
expect(
|
|
await error(`
|
|
function* g() { while (true) yield 1 }
|
|
const it = g().map(() => it.next())
|
|
return it.next()
|
|
`),
|
|
).toMatchObject({ message: expect.stringContaining("Iterator helper is already running.") })
|
|
})
|
|
})
|
|
|
|
describe("built-in iterators", () => {
|
|
test("keys/values/entries and [Symbol.iterator] step with next() and stay live", async () => {
|
|
expect(
|
|
await value(`
|
|
const items = ["a"]
|
|
const it = items.entries()
|
|
items.push("b")
|
|
const steps = [it.next(), it.next(), it.next()]
|
|
items.push("c")
|
|
return { steps, after: it.next(), same: items[Symbol.iterator] === items.values }
|
|
`),
|
|
).toEqual({
|
|
steps: [{ value: [0, "a"], done: false }, { value: [1, "b"], done: false }, { done: true }],
|
|
after: { done: true },
|
|
same: true,
|
|
})
|
|
expect(
|
|
await value(`
|
|
const s = new Set([1, 2])
|
|
const u = new URLSearchParams("a=1&b=2")
|
|
const h = new Headers({ b: "2", a: "1" })
|
|
const bytes = new Uint8Array([7, 8])
|
|
return [
|
|
[...s.entries()], [...s[Symbol.iterator]()], s[Symbol.iterator] === s.values,
|
|
[...u.keys()], [...u[Symbol.iterator]()], u[Symbol.iterator] === u.entries,
|
|
[...h.values()], [...h[Symbol.iterator]()], h[Symbol.iterator] === h.entries,
|
|
[...bytes.entries()], [...bytes[Symbol.iterator]()], bytes[Symbol.iterator] === bytes.values,
|
|
[..."ab"[Symbol.iterator]()],
|
|
]
|
|
`),
|
|
).toEqual([
|
|
[
|
|
[1, 1],
|
|
[2, 2],
|
|
],
|
|
[1, 2],
|
|
true,
|
|
["a", "b"],
|
|
[
|
|
["a", "1"],
|
|
["b", "2"],
|
|
],
|
|
true,
|
|
["1", "2"],
|
|
[
|
|
["a", "1"],
|
|
["b", "2"],
|
|
],
|
|
true,
|
|
[
|
|
[0, 7],
|
|
[1, 8],
|
|
],
|
|
[7, 8],
|
|
true,
|
|
["a", "b"],
|
|
])
|
|
})
|
|
|
|
test("iterators are consumed once by every iteration site", async () => {
|
|
expect(
|
|
await value(`
|
|
const it = [1, 2, 3, 4].values()
|
|
const picked = []
|
|
for (const item of it) { picked.push(item); if (item === 2) break }
|
|
const [third] = it
|
|
return { picked, third, rest: [...it], spent: Array.from(it), again: it[Symbol.iterator]() === it }
|
|
`),
|
|
).toEqual({ picked: [1, 2], third: 3, rest: [4], spent: [], again: true })
|
|
expect(
|
|
await value(`
|
|
const m = new Map([["a", 1], ["b", 2]])
|
|
return [
|
|
Object.fromEntries(m.entries()), Array.from(m.keys(), (k) => k + "!"), new Set(m.values()).size,
|
|
await Promise.all([Promise.resolve(1), 2].values()),
|
|
]
|
|
`),
|
|
).toEqual([{ a: 1, b: 2 }, ["a!", "b!"], 2, [1, 2]])
|
|
expect(await value(`let s = 0; for await (const v of [Promise.resolve(1), 2].values()) s += v; return s`)).toBe(3)
|
|
expect(
|
|
await value(`return new Set([1, 2]).union({ size: 1, has: () => false, keys: () => new Set([3]).keys() })`),
|
|
).toEqual([1, 2, 3])
|
|
})
|
|
|
|
test("iterators are opaque references", async () => {
|
|
expect(await value(`return [1].keys()`)).toEqual({})
|
|
expect(await value(`return JSON.stringify({ it: [1].keys() })`)).toBe('{"it":{}}')
|
|
expect(await value(`return [typeof [1].keys(), Array.isArray([1].keys()), Object.keys([1].keys())]`)).toEqual([
|
|
"object",
|
|
false,
|
|
[],
|
|
])
|
|
const logged = await run(`console.log([1].keys()); return null`)
|
|
expect(logged.logs?.[0]).toBe("[opaque reference]")
|
|
expect((await error(`return [1].keys() + ""`)).message).toContain("Binary operators require data values")
|
|
expect((await error(`return [1].keys().next.call({})`)).message).toContain(
|
|
"Iterator.prototype.next called on incompatible receiver a data object",
|
|
)
|
|
expect((await error(`const it = [1].keys(); const next = it.next; return next()`)).message).toContain(
|
|
"Iterator.prototype.next called on incompatible receiver undefined",
|
|
)
|
|
})
|
|
})
|
|
|
|
describe("Object.prototype.toString", () => {
|
|
test("reports the built-in kind it is inherited by, as JS does through Symbol.toStringTag", async () => {
|
|
expect(
|
|
await value(`
|
|
return [
|
|
new Map().toString(), new Set().toString(), new Headers().toString(), Promise.resolve(1).toString(),
|
|
[1].values().toString(), ({}).toString(), String(new Map()), String(Promise.resolve(1)),
|
|
\`\${new Set([1])}\`, [new Map()] + "", new Map() == "[object Map]",
|
|
]
|
|
`),
|
|
).toEqual([
|
|
"[object Map]",
|
|
"[object Set]",
|
|
"[object Headers]",
|
|
"[object Promise]",
|
|
"[object Iterator]",
|
|
"[object Object]",
|
|
"[object Map]",
|
|
"[object Promise]",
|
|
"[object Set]",
|
|
"[object Map]",
|
|
true,
|
|
])
|
|
})
|
|
})
|
|
|
|
describe("console.log of errors", () => {
|
|
test("prints name and message, nested too", async () => {
|
|
const result = await run(`console.log(new Error("boom"), { e: new RangeError("r") })`)
|
|
expect(result.logs).toEqual(['Error: boom {"e":RangeError: r}'])
|
|
})
|
|
})
|
|
|
|
describe("toLocaleString", () => {
|
|
test("numbers and dates format as en-US in UTC; everything else falls back to toString", async () => {
|
|
expect(
|
|
await value(`
|
|
return [
|
|
(1234567.891).toLocaleString(), new Date(0).toLocaleString(), new Date(0).toLocaleDateString(),
|
|
new Date(0).toLocaleTimeString(), "a".toLocaleString(), true.toLocaleString(), ({}).toLocaleString(),
|
|
({ toString: () => "custom" }).toLocaleString(), new Uint8Array([1, 2]).toLocaleString(),
|
|
]
|
|
`),
|
|
).toEqual([
|
|
"1,234,567.891",
|
|
"1/1/1970, 12:00:00 AM",
|
|
"1/1/1970",
|
|
"12:00:00 AM",
|
|
"a",
|
|
"true",
|
|
"[object Object]",
|
|
"custom",
|
|
"1,2",
|
|
])
|
|
})
|
|
|
|
test("arrays join each element's toLocaleString, skipping holes and nullish elements", async () => {
|
|
expect(
|
|
await value(`
|
|
let calls = 0
|
|
const item = { toLocaleString() { calls++; return "o" } }
|
|
return [[1234.5, "x", null, undefined, item, new Date(0)].toLocaleString(), [, item, , item].toLocaleString(), calls]
|
|
`),
|
|
).toEqual(["1,234.5,x,,,o,1/1/1970, 12:00:00 AM", ",o,,o", 3])
|
|
expect((await error(`const f = ({}).toLocaleString; f()`)).message).toContain(
|
|
"Object.prototype.toLocaleString called on null or undefined",
|
|
)
|
|
})
|
|
|
|
test("toLocaleLowerCase and toLocaleUpperCase ignore the locale argument", async () => {
|
|
expect(
|
|
await value(`return ["ABC".toLocaleLowerCase("tr"), "abc".toLocaleUpperCase(), "İ".toLocaleLowerCase()]`),
|
|
).toEqual(["abc", "ABC", "i̇"])
|
|
})
|
|
})
|
|
|
|
describe("stdlib integration", () => {
|
|
test("constructor follows own keys, shadowing, writes, and new", async () => {
|
|
expect(
|
|
await value(`return [JSON.parse('{"constructor":"Foo"}').constructor, ({ constructor: 1 }).constructor]`),
|
|
).toEqual(["Foo", 1])
|
|
expect(await value(`const Array = 5; return [].constructor.isArray([])`)).toBe(true)
|
|
expect(await value(`const o = {}; o.constructor = 7; return o.constructor`)).toBe(7)
|
|
expect(await value(`return new ([].constructor)(3).length`)).toBe(3)
|
|
expect(await value(`return typeof ({}).constructor`)).toBe("function")
|
|
expect(await value(`return ({}).constructor.constructor === Function`)).toBe(true)
|
|
})
|
|
|
|
test("new dispatches on the constructor value, not its name", async () => {
|
|
expect(await value(`const D = Date; return new D(0) instanceof Date`)).toBe(true)
|
|
expect(await value(`const make = (C) => new C([["a", 1]]); return make(Map).get("a")`)).toBe(1)
|
|
expect(await value(`const t = { M: Map }; return new t.M() instanceof Map`)).toBe(true)
|
|
const shadowed = await error(`const Date = 5; return new Date()`)
|
|
expect(shadowed.message).toStartWith("TypeError: Date is not a constructor.")
|
|
const fn = await error(`const f = () => 1; return new f()`)
|
|
expect(fn.message).toStartWith("TypeError: f cannot be constructed")
|
|
})
|
|
|
|
test("Object.is uses SameValue semantics", async () => {
|
|
expect(
|
|
await value(`
|
|
const object = {}
|
|
return [
|
|
Object.is(NaN, NaN),
|
|
Object.is(0, -0),
|
|
Object.is(object, object),
|
|
Object.is({}, {}),
|
|
]
|
|
`),
|
|
).toEqual([true, false, true, false])
|
|
})
|
|
|
|
test("Object.is compares opaque runtime references by identity", async () => {
|
|
expect(await value(`return [Object.is(Math.max, Math.max), Object.is(Math.max, Math.min)]`)).toEqual([true, false])
|
|
})
|
|
|
|
test("Object values and entries accept arrays", async () => {
|
|
expect(await value(`return [Object.values(["a", "b"]), Object.entries(["a", "b"])]`)).toEqual([
|
|
["a", "b"],
|
|
[
|
|
["0", "a"],
|
|
["1", "b"],
|
|
],
|
|
])
|
|
expect(await value(`const match = /a/.exec("ba"); return [Object.values(match), Object.entries(match)]`)).toEqual([
|
|
["a", 1, "ba"],
|
|
[
|
|
["0", "a"],
|
|
["index", 1],
|
|
["input", "ba"],
|
|
],
|
|
])
|
|
expect(await value(`return Object.keys(Object.values({ match: /a/.exec("ba") })[0])`)).toEqual([
|
|
"0",
|
|
"index",
|
|
"input",
|
|
])
|
|
})
|
|
|
|
test("Object.fromEntries accepts every supported entry collection", async () => {
|
|
expect(
|
|
await value(`
|
|
return [
|
|
Object.fromEntries([["a", 1]]),
|
|
Object.fromEntries(new Map([["b", 2]])),
|
|
Object.fromEntries(new Set([["c", 3]])),
|
|
Object.fromEntries(new URLSearchParams("d=4")),
|
|
Object.fromEntries([{ 0: "e", 1: 5 }]),
|
|
Object.fromEntries(new Set([[{}, 6], [new Date(0), 7], [null, 8], [undefined, 9]])),
|
|
]
|
|
`),
|
|
).toEqual([
|
|
{ a: 1 },
|
|
{ b: 2 },
|
|
{ c: 3 },
|
|
{ d: "4" },
|
|
{ e: 5 },
|
|
{ "[object Object]": 6, "1970-01-01T00:00:00.000Z": 7, null: 8, undefined: 9 },
|
|
])
|
|
expect(await value(`try { Object.fromEntries(new Set([Math.max])); return false } catch { return true }`)).toBe(
|
|
true,
|
|
)
|
|
expect(
|
|
await value(`try { Object.fromEntries(new Map([["fn", Math.max]])); return false } catch { return true }`),
|
|
).toBe(true)
|
|
})
|
|
|
|
test("deterministic Math methods match the host runtime", async () => {
|
|
const result = await value(`
|
|
return [
|
|
Math.acos(0.5), Math.acosh(2), Math.asin(0.5), Math.asinh(2), Math.atan(1), Math.atan2(1, 2), Math.atanh(0.5),
|
|
Math.cos(0.5), Math.cosh(0.5), Math.sin(0.5), Math.sinh(0.5), Math.tan(0.5), Math.tanh(0.5),
|
|
Math.log1p(0.5), Math.expm1(0.5), Math.f16round(1.337), Math.fround(1.337), Math.clz32(1), Math.imul(2, 3),
|
|
]
|
|
`)
|
|
expect(result).toEqual([
|
|
Math.acos(0.5),
|
|
Math.acosh(2),
|
|
Math.asin(0.5),
|
|
Math.asinh(2),
|
|
Math.atan(1),
|
|
Math.atan2(1, 2),
|
|
Math.atanh(0.5),
|
|
Math.cos(0.5),
|
|
Math.cosh(0.5),
|
|
Math.sin(0.5),
|
|
Math.sinh(0.5),
|
|
Math.tan(0.5),
|
|
Math.tanh(0.5),
|
|
Math.log1p(0.5),
|
|
Math.expm1(0.5),
|
|
Math.f16round(1.337),
|
|
Math.fround(1.337),
|
|
Math.clz32(1),
|
|
Math.imul(2, 3),
|
|
])
|
|
})
|
|
|
|
test("Object.assign mutates and returns its target", async () => {
|
|
expect(
|
|
await value(`
|
|
const target = { a: 1 }
|
|
const result = Object.assign(target, { b: 2 })
|
|
return { target, result, same: target === result }
|
|
`),
|
|
).toEqual({ target: { a: 1, b: 2 }, result: { a: 1, b: 2 }, same: true })
|
|
expect(await value(`try { Object.assign(null, { a: 1 }); return false } catch { return true }`)).toBe(true)
|
|
})
|
|
|
|
test("Object.assign rejects direct and nested cycles", async () => {
|
|
expect(
|
|
await value(`
|
|
const target = { kept: true }
|
|
try { Object.assign(target, { self: target }) } catch { return target }
|
|
return null
|
|
`),
|
|
).toEqual({ kept: true })
|
|
expect(
|
|
await value(`
|
|
const target = { kept: true }
|
|
const nested = { target }
|
|
try { Object.assign(target, { nested }) } catch { return target }
|
|
return null
|
|
`),
|
|
).toEqual({ kept: true })
|
|
expect(
|
|
await value(`
|
|
const target = {}
|
|
const source = {}
|
|
source[Symbol.iterator] = target
|
|
try { Object.assign(target, source) } catch { return Object.hasOwn(target, Symbol.iterator) }
|
|
return true
|
|
`),
|
|
).toBe(false)
|
|
expect(
|
|
await value(`
|
|
const target = {}
|
|
const nested = {}
|
|
nested[Symbol.iterator] = target
|
|
try { Object.assign(target, { nested }) } catch { return Object.hasOwn(target, "nested") }
|
|
return true
|
|
`),
|
|
).toBe(false)
|
|
})
|
|
|
|
test("Object.assign preserves mutations before a circular field", async () => {
|
|
expect(
|
|
await value(`
|
|
const target = {}
|
|
try { Object.assign(target, { before: 1, cycle: { target }, after: 2 }) } catch { return target }
|
|
return null
|
|
`),
|
|
).toEqual({ before: 1 })
|
|
expect(
|
|
await value(`
|
|
const target = {}
|
|
const marker = {}
|
|
const source = {}
|
|
source[Symbol.iterator] = marker
|
|
source[Symbol.asyncIterator] = target
|
|
try { Object.assign(target, source) } catch {
|
|
return [target[Symbol.iterator] === marker, Object.hasOwn(target, Symbol.asyncIterator)]
|
|
}
|
|
return null
|
|
`),
|
|
).toEqual([true, false])
|
|
})
|
|
|
|
test("Object.assign preserves target identity and acyclic shared aliases", async () => {
|
|
expect(
|
|
await value(`
|
|
const shared = { count: 1 }
|
|
const target = {}
|
|
const result = Object.assign(target, { left: shared, right: shared })
|
|
result.left.count = 2
|
|
return [result === target, result.left === shared, result.left === result.right, shared.count]
|
|
`),
|
|
).toEqual([true, true, true, 2])
|
|
})
|
|
|
|
test("assignment resolves and reads its left side before evaluating the right side", async () => {
|
|
expect(await value(`let x = 1; x += (x = 5); return x`)).toBe(6)
|
|
expect(await value(`let i = 0; const values = [9]; values[i++] = i; return [values, i]`)).toEqual([[1], 1])
|
|
expect(await value(`let i = 0; const values = [10, 20]; values[i++] += i; return [values, i]`)).toEqual([
|
|
[11, 20],
|
|
1,
|
|
])
|
|
})
|
|
|
|
test("typeof reports constructors as functions and never throws", async () => {
|
|
expect(await value(`return typeof Map`)).toBe("function")
|
|
expect(await value(`return typeof ((x) => x)`)).toBe("function")
|
|
expect(await value(`return typeof Math`)).toBe("object")
|
|
expect(await value(`return typeof tools`)).toBe("object")
|
|
})
|
|
|
|
test("negation works on any value", async () => {
|
|
expect(await value(`return !new Map()`)).toBe(false)
|
|
expect(await value(`const fn = () => 1; return !fn`)).toBe(false)
|
|
})
|
|
|
|
test("object spread of CodeMode values is a no-op, like JS", async () => {
|
|
expect(await value(`return { ...new Map([["a", 1]]), kept: true }`)).toEqual({ kept: true })
|
|
})
|
|
|
|
test("dates inside Map values survive in-CodeMode reads", async () => {
|
|
expect(
|
|
await value(`
|
|
const m = new Map([["start", new Date(1000)]])
|
|
return m.get("start").getTime()
|
|
`),
|
|
).toBe(1000)
|
|
})
|
|
|
|
test("instanceof recognizes the stdlib value types", async () => {
|
|
expect(
|
|
await value(
|
|
`return [new Date(0) instanceof Date, /a/ instanceof RegExp, new Map() instanceof Map, new Set() instanceof Set]`,
|
|
),
|
|
).toEqual([true, true, true, true])
|
|
expect(
|
|
await value(`return [[1] instanceof Array, [1] instanceof Object, ({}) instanceof Object, 5 instanceof Object]`),
|
|
).toEqual([true, true, true, false])
|
|
expect(await value(`return [new Map() instanceof Set, "s" instanceof Date]`)).toEqual([false, false])
|
|
expect(
|
|
await value(`const p = Promise.resolve(1); const isPromise = p instanceof Promise; await p; return isPromise`),
|
|
).toBe(true)
|
|
})
|
|
|
|
test("realistic pipeline: parse, extract with regex, dedupe, count by day", async () => {
|
|
expect(
|
|
await value(`
|
|
const raw = '[{"at":"2024-01-01T05:00:00Z","tag":"a b"},{"at":"2024-01-01T09:00:00Z","tag":"b c"},{"at":"2024-01-02T01:00:00Z","tag":"a"}]'
|
|
const rows = JSON.parse(raw)
|
|
const tags = new Set()
|
|
const byDay = new Map()
|
|
for (const row of rows) {
|
|
for (const m of row.tag.matchAll(/[a-z]+/g)) tags.add(m[0])
|
|
const day = new Date(row.at).toISOString().slice(0, 10)
|
|
byDay.set(day, (byDay.get(day) ?? 0) + 1)
|
|
}
|
|
return { tags: [...tags].sort((a, b) => (a < b ? -1 : 1)), byDay: Object.fromEntries(byDay) }
|
|
`),
|
|
).toEqual({ tags: ["a", "b", "c"], byDay: { "2024-01-01": 2, "2024-01-02": 1 } })
|
|
})
|
|
})
|
|
|
|
describe("CodeMode values at intra-CodeMode checkpoints", () => {
|
|
test("Object.values/entries keep Dates usable", async () => {
|
|
expect(await value(`return Object.values({ d: new Date(0) })[0].getTime()`)).toBe(0)
|
|
expect(await value(`const [key, d] = Object.entries({ d: new Date(0) })[0]; return key + ":" + d.getTime()`)).toBe(
|
|
"d:0",
|
|
)
|
|
})
|
|
|
|
test("Object.values/entries preserve nested object identity", async () => {
|
|
expect(
|
|
await value(`
|
|
const child = { selected: false }
|
|
const rows = { a: child }
|
|
Object.values(rows)[0].selected = true
|
|
return child.selected
|
|
`),
|
|
).toBe(true)
|
|
expect(
|
|
await value(`
|
|
const child = { selected: false }
|
|
const rows = { a: child }
|
|
Object.entries(rows)[0][1].selected = true
|
|
return child.selected
|
|
`),
|
|
).toBe(true)
|
|
})
|
|
|
|
test("Object enumeration preserves promises and callable references", async () => {
|
|
expect(
|
|
await value(`
|
|
const pending = Promise.resolve(1)
|
|
const source = { pending }
|
|
return [Object.keys(source), Object.hasOwn(source, "pending"), await Object.values(source)[0], await Object.entries(source)[0][1]]
|
|
`),
|
|
).toEqual([["pending"], true, 1, 1])
|
|
expect(await value(`return Object.values({ max: Math.max })[0](1, 2)`)).toBe(2)
|
|
})
|
|
|
|
test("Object enumeration rejects invalid receivers and gives promises an await hint", async () => {
|
|
const diagnostic = await error(`return Object.keys(Promise.resolve({ a: 1 }))`)
|
|
expect(diagnostic.kind).toBe("InvalidDataValue")
|
|
expect(diagnostic.message).toContain("await")
|
|
expect(await value(`return Object.keys(Math)`)).toEqual([])
|
|
})
|
|
|
|
test("Object.assign keeps Maps usable", async () => {
|
|
expect(await value(`const merged = Object.assign({}, { m: new Map([["a", 1]]) }); return merged.m.get("a")`)).toBe(
|
|
1,
|
|
)
|
|
})
|
|
|
|
test("object and array spread keep CodeMode values usable", async () => {
|
|
expect(
|
|
await value(`
|
|
const src = { m: new Map([["a", 1]]) }
|
|
const copy = { ...src }
|
|
copy.m.set("b", 2)
|
|
return [copy.m.get("a"), src.m.get("b")]
|
|
`),
|
|
).toEqual([1, 2])
|
|
expect(await value(`const list = [new Date(1000)]; const copy = [...list]; return copy[0].getTime()`)).toBe(1000)
|
|
})
|
|
|
|
test("Array.from over arrays keeps nested CodeMode values usable", async () => {
|
|
expect(await value(`return Array.from([new Date(5)])[0].getTime()`)).toBe(5)
|
|
})
|
|
|
|
test("Array.from and Array.of preserve nested object identity", async () => {
|
|
expect(
|
|
await value(`
|
|
const child = { selected: false }
|
|
Array.from([child])[0].selected = true
|
|
return child.selected
|
|
`),
|
|
).toBe(true)
|
|
expect(
|
|
await value(`
|
|
const child = { selected: false }
|
|
Array.of(child)[0].selected = true
|
|
return child.selected
|
|
`),
|
|
).toBe(true)
|
|
})
|
|
|
|
test("Array.from and Array.of preserve promises and callable references", async () => {
|
|
expect(
|
|
await value(`
|
|
const pending = Promise.resolve(1)
|
|
return [await Array.from([pending])[0], await Array.of(pending)[0]]
|
|
`),
|
|
).toEqual([1, 1])
|
|
expect(await value(`return [Array.from([Math.max])[0](1, 2), Array.of(Math.max)[0](3, 4)]`)).toEqual([2, 4])
|
|
})
|
|
|
|
test("Array.from preserves identity across supported collection shapes", async () => {
|
|
expect(
|
|
await value(`
|
|
const child = { selected: false }
|
|
const fromArrayLike = Array.from({ 0: child, length: 1 })
|
|
const fromMap = Array.from(new Map([["child", child]]))
|
|
const fromSet = Array.from(new Set([child]))
|
|
fromArrayLike[0].selected = true
|
|
return [fromMap[0][1] === child, fromSet[0] === child, child.selected]
|
|
`),
|
|
).toEqual([true, true, true])
|
|
})
|
|
|
|
test("Array.from rejects invalid receivers and gives promises an await hint", async () => {
|
|
const diagnostic = await error(`return Array.from(Promise.resolve([1]))`)
|
|
expect(diagnostic.kind).toBe("InvalidDataValue")
|
|
expect(diagnostic.message).toContain("await")
|
|
expect(await value(`return Array.from((a, b) => 1)`)).toEqual([null, null])
|
|
})
|
|
|
|
test("regexes stay callable through Object.values", async () => {
|
|
expect(await value(`return Object.values({ r: /ab+/ })[0].test("abb")`)).toBe(true)
|
|
})
|
|
|
|
test("Object.* helpers see CodeMode values as empty objects, never internals", async () => {
|
|
expect(await value(`return Object.keys(new Map([["a", 1]]))`)).toEqual([])
|
|
expect(await value(`return Object.values(new Date(0))`)).toEqual([])
|
|
expect(await value(`return Object.entries(new Set([1]))`)).toEqual([])
|
|
expect(await value(`return Object.assign({}, new Map([["a", 1]]))`)).toEqual({})
|
|
expect(await value(`return Object.hasOwn(new Date(0), "time")`)).toBe(false)
|
|
})
|
|
|
|
test("the host boundary still serializes JSON forms: results, JSON.stringify, and tool arguments", async () => {
|
|
expect(await value(`return { d: new Date(0), m: new Map([["a", 1]]) }`)).toEqual({
|
|
d: "1970-01-01T00:00:00.000Z",
|
|
m: {},
|
|
})
|
|
expect(await value(`return JSON.stringify({ d: new Date(0) })`)).toBe('{"d":"1970-01-01T00:00:00.000Z"}')
|
|
|
|
const observed: Array<unknown> = []
|
|
const capture = Tool.make({
|
|
description: "Capture the exact input the host receives",
|
|
input: { type: "object" },
|
|
execute: (input) =>
|
|
Effect.sync(() => {
|
|
observed.push(input)
|
|
return "ok"
|
|
}),
|
|
})
|
|
const result = await Effect.runPromise(
|
|
CodeMode.execute({
|
|
tools: { host: { capture } },
|
|
code: `return await tools.host.capture({ when: new Date(0), tags: new Map([["a", 1]]) })`,
|
|
}),
|
|
)
|
|
expect(result.ok).toBe(true)
|
|
expect(observed).toStrictEqual([{ when: "1970-01-01T00:00:00.000Z", tags: {} }])
|
|
})
|
|
})
|
|
|
|
describe("Uint8Array callback methods", () => {
|
|
test("map and filter return new Uint8Arrays with clamped bytes", async () => {
|
|
expect(
|
|
await value(`
|
|
const b = new Uint8Array([1, 2, 3])
|
|
const mapped = b.map((byte) => byte * 100)
|
|
const filtered = b.filter((byte) => byte > 1)
|
|
mapped[0] = 9
|
|
return [
|
|
[...mapped], mapped instanceof Uint8Array, Array.isArray(mapped), [...b], [...filtered],
|
|
[...b.map(() => "7")], b.map((byte) => byte, {}).length, [...new Uint8Array().map((byte) => byte)],
|
|
]
|
|
`),
|
|
).toEqual([[9, 200, 44], true, false, [1, 2, 3], [2, 3], [7, 7, 7], 3, []])
|
|
})
|
|
|
|
test("find, findIndex, findLast, findLastIndex, some, every, and forEach", async () => {
|
|
expect(
|
|
await value(`
|
|
const b = new Uint8Array([1, 2, 3])
|
|
const seen = []
|
|
b.forEach((byte, index, array) => seen.push([byte, index, array === b]))
|
|
return [
|
|
b.find((byte) => byte > 1), b.find((byte) => byte > 5) === undefined, b.findIndex((byte) => byte > 1),
|
|
b.findIndex((byte) => byte > 5), b.findLast((byte) => byte < 3), b.findLastIndex((byte) => byte < 3),
|
|
b.findLastIndex((byte) => byte > 9), b.some((byte) => byte > 2), b.every((byte) => byte > 2),
|
|
new Uint8Array().some(() => true), new Uint8Array().every(() => false),
|
|
b.every((byte, index, array) => array === b), seen,
|
|
]
|
|
`),
|
|
).toEqual([
|
|
2,
|
|
true,
|
|
1,
|
|
-1,
|
|
2,
|
|
1,
|
|
-1,
|
|
true,
|
|
false,
|
|
false,
|
|
true,
|
|
true,
|
|
[
|
|
[1, 0, true],
|
|
[2, 1, true],
|
|
[3, 2, true],
|
|
],
|
|
])
|
|
})
|
|
|
|
test("reduce and reduceRight", async () => {
|
|
expect(
|
|
await value(`
|
|
const b = new Uint8Array([1, 2, 3])
|
|
return [
|
|
b.reduce((sum, byte) => sum + byte), b.reduce((sum, byte) => sum + byte, 10),
|
|
b.reduceRight((text, byte) => text + byte, ""), new Uint8Array().reduce((sum, byte) => sum + byte, 5),
|
|
b.reduce((_, byte, index, array) => array === b && index, 0),
|
|
]
|
|
`),
|
|
).toEqual([6, 16, "321", 5, 2])
|
|
expect((await error(`new Uint8Array().reduce((sum, byte) => sum + byte)`)).message).toContain(
|
|
"Uint8Array.reduce of an empty array with no initial value",
|
|
)
|
|
expect((await error(`new Uint8Array().reduceRight((sum, byte) => sum + byte)`)).message).toContain(
|
|
"Uint8Array.reduceRight of an empty array with no initial value",
|
|
)
|
|
expect((await error(`new Uint8Array([1]).map(null)`)).message).toContain("Uint8Array.map expects a function")
|
|
})
|
|
|
|
test("sort is numeric by default and in place, with an optional comparator", async () => {
|
|
expect(
|
|
await value(`
|
|
const b = new Uint8Array([10, 9, 1])
|
|
const same = b.sort() === b
|
|
const desc = new Uint8Array([3, 1, 2]).sort((x, y) => y - x)
|
|
return [same, [...b], [...desc], desc instanceof Uint8Array, [...new Uint8Array([2, 1]).sort(() => NaN)]]
|
|
`),
|
|
).toEqual([true, [1, 9, 10], [3, 2, 1], true, [2, 1]])
|
|
expect((await error(`new Uint8Array([2, 1]).sort(null)`)).message).toContain("Uint8Array.sort expects a function")
|
|
})
|
|
|
|
test("lastIndexOf treats an explicit undefined fromIndex as 0", async () => {
|
|
expect(
|
|
await value(`
|
|
const a = [1, 2, 1]
|
|
const b = new Uint8Array([1, 2, 1])
|
|
return [a.lastIndexOf(1, undefined), a.lastIndexOf(1), a.lastIndexOf(2, undefined), b.lastIndexOf(1, undefined), b.lastIndexOf(1)]
|
|
`),
|
|
).toEqual([0, 2, -1, 0, 2])
|
|
})
|
|
})
|