mirror of
https://github.com/anomalyco/opencode.git
synced 2026-09-12 00:24:40 +00:00
Replace record-keyed tools and the separate namespace-description map with an OpenAI-style array of named tools and namespaces. Namespace descriptions are optional, namespaces may nest, and duplicate or dotted names are rejected.
1510 lines
54 KiB
TypeScript
1510 lines
54 KiB
TypeScript
/*
|
|
* Portions adapted from Test262 at revision 250f204f23a9249ff204be2baec29600faae7b75.
|
|
* Every test names its upstream source; test.failing cases are executable conformance
|
|
* targets for intended Promise behavior that CodeMode does not implement yet.
|
|
*
|
|
* Copyright 2014 Cubane Canada, Inc. All rights reserved.
|
|
* Copyright 2015 Microsoft Corporation. All rights reserved.
|
|
* Copyright 2016 Microsoft, Inc. All rights reserved.
|
|
* Copyright 2017 Caitlin Potter. All rights reserved.
|
|
* Copyright (C) 2016-2020 the V8 project authors. All rights reserved.
|
|
* Copyright (C) 2018-2020 Rick Waldron. All rights reserved.
|
|
* Copyright (C) 2019 Leo Balter. All rights reserved.
|
|
* Test262 portions are governed by the BSD license in LICENSE.test262.
|
|
*/
|
|
import { describe, expect, test } from "bun:test"
|
|
import { Effect } from "effect"
|
|
import { CodeMode } from "../src/index.js"
|
|
|
|
const execute = (code: string) => Effect.runPromise(CodeMode.execute({ code, limits: { timeoutMs: 1_000 } }))
|
|
|
|
const value = async (code: string) => {
|
|
const result = await execute(code)
|
|
if (!result.ok) throw new Error(`expected success, got ${result.error.kind}: ${result.error.message}`)
|
|
return result.value
|
|
}
|
|
|
|
describe("Test262 Promise statics", () => {
|
|
test("statics are callable and return promises", async () => {
|
|
// Sources:
|
|
// test/built-ins/Promise/all/S25.4.4.1_A1.1_T1.js
|
|
// test/built-ins/Promise/allSettled/is-function.js
|
|
// test/built-ins/Promise/allSettled/returns-promise.js
|
|
// test/built-ins/Promise/race/S25.4.4.3_A1.1_T1.js
|
|
// test/built-ins/Promise/resolve/S25.4.4.5_A1.1_T1.js
|
|
// test/built-ins/Promise/reject/S25.4.4.4_A1.1_T1.js
|
|
expect(
|
|
await value(`
|
|
const values = [
|
|
Promise.all([]),
|
|
Promise.allSettled([]),
|
|
Promise.race([undefined]),
|
|
Promise.resolve(),
|
|
Promise.reject(),
|
|
]
|
|
const callable = [
|
|
typeof Promise.all,
|
|
typeof Promise.allSettled,
|
|
typeof Promise.race,
|
|
typeof Promise.resolve,
|
|
typeof Promise.reject,
|
|
]
|
|
try { await values[4] } catch {}
|
|
return [callable, values.map((item) => item instanceof Promise)]
|
|
`),
|
|
).toEqual([
|
|
["function", "function", "function", "function", "function"],
|
|
[true, true, true, true, true],
|
|
])
|
|
})
|
|
|
|
test("Promise.all returns fresh arrays for empty and settled inputs", async () => {
|
|
// Sources:
|
|
// test/built-ins/Promise/all/S25.4.4.1_A2.1_T1.js
|
|
// test/built-ins/Promise/all/S25.4.4.1_A2.3_T1.js
|
|
// test/built-ins/Promise/all/S25.4.4.1_A2.3_T2.js
|
|
// test/built-ins/Promise/all/S25.4.4.1_A2.3_T3.js
|
|
// test/built-ins/Promise/all/S25.4.4.1_A7.1_T1.js
|
|
expect(
|
|
await value(`
|
|
const input = []
|
|
const emptyPromise = Promise.all(input)
|
|
const empty = await emptyPromise
|
|
const onePromise = Promise.all([Promise.resolve(3)])
|
|
const one = await onePromise
|
|
return [
|
|
emptyPromise instanceof Promise,
|
|
empty instanceof Array,
|
|
empty.length,
|
|
empty !== input,
|
|
onePromise instanceof Promise,
|
|
one instanceof Array,
|
|
one.length,
|
|
one[0],
|
|
]
|
|
`),
|
|
).toEqual([true, true, 0, true, true, true, 1, 3])
|
|
})
|
|
|
|
test("Promise.all adopts values and preserves input order and identity", async () => {
|
|
// Sources:
|
|
// test/built-ins/Promise/all/resolve-non-thenable.js
|
|
// test/built-ins/Promise/all/S25.4.4.1_A8.2_T1.js
|
|
// test/built-ins/Promise/all/S25.4.4.1_A8.2_T2.js
|
|
const result = await value(`
|
|
const first = { id: 1 }
|
|
const second = { id: 2 }
|
|
const values = await Promise.all([Promise.resolve(3), first, Promise.resolve(second)])
|
|
const observe = async (promise) => {
|
|
try { await promise; return "fulfilled" } catch (reason) { return reason }
|
|
}
|
|
return [
|
|
values.length,
|
|
values[0],
|
|
values[1] === first,
|
|
values[2] === second,
|
|
await observe(Promise.all([Promise.reject(1), Promise.resolve(2)])),
|
|
await observe(Promise.all([Promise.resolve(1), Promise.reject(2)])),
|
|
]
|
|
`)
|
|
expect(result).toEqual([3, 3, true, true, 1, 2])
|
|
})
|
|
|
|
test("Promise.allSettled returns fresh arrays and ordered outcome records", async () => {
|
|
// Sources:
|
|
// test/built-ins/Promise/allSettled/resolves-empty-array.js
|
|
// test/built-ins/Promise/allSettled/resolves-to-array.js
|
|
// test/built-ins/Promise/allSettled/resolved-all-fulfilled.js
|
|
// test/built-ins/Promise/allSettled/resolved-all-rejected.js
|
|
// test/built-ins/Promise/allSettled/resolved-all-mixed.js
|
|
// test/built-ins/Promise/allSettled/resolve-non-thenable.js
|
|
expect(
|
|
await value(`
|
|
const input = []
|
|
const empty = await Promise.allSettled(input)
|
|
const reason = { id: 4 }
|
|
const object = { id: 5 }
|
|
const outcomes = await Promise.allSettled([
|
|
Promise.resolve(1),
|
|
Promise.reject(2),
|
|
3,
|
|
Promise.reject(reason),
|
|
object,
|
|
])
|
|
return [
|
|
empty instanceof Array,
|
|
empty.length,
|
|
empty !== input,
|
|
outcomes,
|
|
outcomes[4].value === object,
|
|
outcomes.map((item) => Object.keys(item)),
|
|
]
|
|
`),
|
|
).toEqual([
|
|
true,
|
|
0,
|
|
true,
|
|
[
|
|
{ status: "fulfilled", value: 1 },
|
|
{ status: "rejected", reason: 2 },
|
|
{ status: "fulfilled", value: 3 },
|
|
{ status: "rejected", reason: { id: 4 } },
|
|
{ status: "fulfilled", value: { id: 5 } },
|
|
],
|
|
true,
|
|
[
|
|
["status", "value"],
|
|
["status", "reason"],
|
|
["status", "value"],
|
|
["status", "reason"],
|
|
["status", "value"],
|
|
],
|
|
])
|
|
})
|
|
|
|
test("Promise.race preserves fulfillment, rejection, and iterable order", async () => {
|
|
// Sources:
|
|
// test/built-ins/Promise/race/S25.4.4.3_A6.2_T1.js
|
|
// test/built-ins/Promise/race/S25.4.4.3_A7.1_T1.js
|
|
// test/built-ins/Promise/race/S25.4.4.3_A7.2_T1.js
|
|
// test/built-ins/Promise/race/S25.4.4.3_A7.3_T1.js
|
|
// test/built-ins/Promise/race/S25.4.4.3_A7.3_T2.js
|
|
expect(
|
|
await value(`
|
|
const observe = async (promise) => {
|
|
try { return ["fulfilled", await promise] } catch (reason) { return ["rejected", reason] }
|
|
}
|
|
return await Promise.all([
|
|
observe(Promise.race([23])),
|
|
observe(Promise.race([Promise.reject(7)])),
|
|
observe(Promise.race([Promise.resolve(1), Promise.resolve(2)])),
|
|
observe(Promise.race([Promise.reject(3), Promise.resolve(4)])),
|
|
])
|
|
`),
|
|
).toEqual([
|
|
["fulfilled", 23],
|
|
["rejected", 7],
|
|
["fulfilled", 1],
|
|
["rejected", 3],
|
|
])
|
|
})
|
|
|
|
test("combinators consume supported string iterables", async () => {
|
|
// Sources:
|
|
// test/built-ins/Promise/all/iter-arg-is-string-resolve.js
|
|
// test/built-ins/Promise/allSettled/iter-arg-is-string-resolve.js
|
|
// test/built-ins/Promise/race/iter-arg-is-string-resolve.js
|
|
expect(
|
|
await value(`
|
|
return [
|
|
await Promise.all("abc"),
|
|
await Promise.allSettled("ab"),
|
|
await Promise.race("abc"),
|
|
]
|
|
`),
|
|
).toEqual([
|
|
["a", "b", "c"],
|
|
[
|
|
{ status: "fulfilled", value: "a" },
|
|
{ status: "fulfilled", value: "b" },
|
|
],
|
|
"a",
|
|
])
|
|
})
|
|
|
|
test("Promise.resolve adopts values and preserves CodeMode-promise identity", async () => {
|
|
// Sources:
|
|
// test/built-ins/Promise/resolve/S25.4.4.5_A2.1_T1.js
|
|
// test/built-ins/Promise/resolve/resolve-non-obj.js
|
|
// test/built-ins/Promise/resolve/resolve-non-thenable.js
|
|
expect(
|
|
await value(`
|
|
const object = { id: 1 }
|
|
const promise = Promise.resolve(1)
|
|
return [
|
|
await Promise.resolve(23),
|
|
await Promise.resolve(Promise.resolve(24)),
|
|
(await Promise.resolve(object)) === object,
|
|
[promise].includes(Promise.resolve(promise)),
|
|
]
|
|
`),
|
|
).toEqual([23, 24, true, true])
|
|
})
|
|
|
|
test("Promise.reject preserves primitive and object reasons", async () => {
|
|
// Sources:
|
|
// test/built-ins/Promise/reject/S25.4.4.4_A2.1_T1.js
|
|
const result = await value(`
|
|
const object = { reason: true }
|
|
const reasons = [undefined, null, false, true, 0, "", 42, object]
|
|
const observe = async (reason) => {
|
|
try { await Promise.reject(reason); return false } catch (caught) { return caught === reason }
|
|
}
|
|
return await Promise.all(reasons.map(observe))
|
|
`)
|
|
expect(result).toEqual([true, true, true, true, true, true, true, true])
|
|
})
|
|
|
|
test("Promise.all resolves duplicate members into every slot", async () => {
|
|
// Sources:
|
|
// test/built-ins/Promise/all/invoke-resolve-on-promises-every-iteration-of-promise.js
|
|
// test/built-ins/Promise/all/invoke-resolve-on-values-every-iteration-of-promise.js
|
|
// (adapted: CodeMode has no observable Promise.resolve hook, so per-iteration
|
|
// handling of a repeated member is asserted through the resolved slots)
|
|
expect(
|
|
await value(`
|
|
const settled = Promise.resolve(3)
|
|
const computed = (async () => "computed")()
|
|
return [
|
|
await Promise.all([settled, settled, settled]),
|
|
await Promise.all([computed, "plain", computed]),
|
|
]
|
|
`),
|
|
).toEqual([
|
|
[3, 3, 3],
|
|
["computed", "plain", "computed"],
|
|
])
|
|
})
|
|
|
|
test("Promise.allSettled records duplicate members independently", async () => {
|
|
// Source: test/built-ins/Promise/allSettled/invoke-resolve-on-promises-every-iteration-of-promise.js
|
|
// (adapted: per-iteration handling of a repeated member is asserted through the
|
|
// outcome records instead of a Promise.resolve hook)
|
|
expect(
|
|
await value(`
|
|
const good = Promise.resolve(1)
|
|
const bad = Promise.reject(2)
|
|
return await Promise.allSettled([good, bad, good, bad])
|
|
`),
|
|
).toEqual([
|
|
{ status: "fulfilled", value: 1 },
|
|
{ status: "rejected", reason: 2 },
|
|
{ status: "fulfilled", value: 1 },
|
|
{ status: "rejected", reason: 2 },
|
|
])
|
|
})
|
|
|
|
test("combinators adopt members that settled before the call", async () => {
|
|
// Sources:
|
|
// test/built-ins/Promise/all/reject-immed.js
|
|
// test/built-ins/Promise/allSettled/reject-immed.js
|
|
// test/built-ins/Promise/race/reject-immed.js
|
|
// (adapted: immediately-rejecting thenables become CodeMode promises that settled,
|
|
// and were even observed, before the combinator call)
|
|
expect(
|
|
await value(`
|
|
const fulfilled = Promise.resolve("done")
|
|
const rejected = Promise.reject("failed")
|
|
try { await rejected } catch {}
|
|
const observe = async (promise) => {
|
|
try { return ["fulfilled", await promise] } catch (reason) { return ["rejected", reason] }
|
|
}
|
|
return [
|
|
await observe(Promise.all([fulfilled, rejected])),
|
|
await Promise.allSettled([rejected, fulfilled]),
|
|
await observe(Promise.race([rejected, fulfilled])),
|
|
]
|
|
`),
|
|
).toEqual([
|
|
["rejected", "failed"],
|
|
[
|
|
{ status: "rejected", reason: "failed" },
|
|
{ status: "fulfilled", value: "done" },
|
|
],
|
|
["rejected", "failed"],
|
|
])
|
|
})
|
|
|
|
test("combinator results follow input order, not settlement order", async () => {
|
|
// Sources:
|
|
// test/built-ins/Promise/all/resolve-non-thenable.js
|
|
// test/built-ins/Promise/allSettled/resolved-all-mixed.js
|
|
// (adapted: members are created, and therefore settle, in reverse of input order;
|
|
// deferred settlement is not expressible without host-async work in this corpus)
|
|
expect(
|
|
await value(`
|
|
const third = Promise.resolve("c")
|
|
const failing = (async () => { throw "b" })()
|
|
try { await failing } catch {}
|
|
const second = (async () => "b")()
|
|
const first = Promise.resolve("a")
|
|
return [
|
|
await Promise.all([first, second, third]),
|
|
await Promise.allSettled([first, failing, third]),
|
|
]
|
|
`),
|
|
).toEqual([
|
|
["a", "b", "c"],
|
|
[
|
|
{ status: "fulfilled", value: "a" },
|
|
{ status: "rejected", reason: "b" },
|
|
{ status: "fulfilled", value: "c" },
|
|
],
|
|
])
|
|
})
|
|
|
|
test("Promise.race ignores a rejected loser once the first contender wins", async () => {
|
|
// Source: test/built-ins/Promise/race/reject-ignored-immed.js
|
|
// (adapted: the losing rejection comes from an async function instead of a thenable;
|
|
// the exact-equality check also asserts the loser leaves no unhandled-rejection warning)
|
|
expect(
|
|
await execute(`
|
|
const loser = (async () => { throw "lost" })()
|
|
return await Promise.race([Promise.resolve("won"), loser])
|
|
`),
|
|
).toEqual({ ok: true, value: "won", toolCalls: [] })
|
|
})
|
|
|
|
test("Promise.race([]) returns a promise whose CodeMode failure is catchable", async () => {
|
|
// Sources:
|
|
// test/built-ins/Promise/race/S25.4.4.3_A2.1_T1.js
|
|
// test/built-ins/Promise/race/S25.4.4.3_A5.1_T1.js
|
|
// (adapted: upstream requires Promise.race([]) to never settle; CodeMode intentionally
|
|
// rejects with a catchable diagnostic instead of hanging, so this asserts CodeMode
|
|
// divergence rather than the spec never-settles behavior)
|
|
expect(
|
|
await value(`
|
|
const empty = Promise.race([])
|
|
try {
|
|
await empty
|
|
return "settled"
|
|
} catch (error) {
|
|
return [empty instanceof Promise, error instanceof Error]
|
|
}
|
|
`),
|
|
).toEqual([true, true])
|
|
})
|
|
|
|
test("Promise.resolve passes the same CodeMode promise through nested chains", async () => {
|
|
// Source: test/built-ins/Promise/resolve/S25.4.4.5_A2.2_T1.js
|
|
// (adapted: no executor construction, and identity is observed with Array includes
|
|
// because promises are not comparable data values in CodeMode)
|
|
expect(
|
|
await value(`
|
|
const promise = Promise.resolve({ id: 1 })
|
|
return [
|
|
[promise].includes(Promise.resolve(promise)),
|
|
[promise].includes(Promise.resolve(Promise.resolve(promise))),
|
|
(await Promise.resolve(Promise.resolve(promise))).id,
|
|
]
|
|
`),
|
|
).toEqual([true, true, 1])
|
|
})
|
|
|
|
test("Promise.resolve of a rejected promise preserves identity and reason", async () => {
|
|
// Source: test/built-ins/Promise/resolve/S25.4.4.5_A2.3_T1.js
|
|
// (adapted: the source promise is already rejected instead of rejected later)
|
|
expect(
|
|
await value(`
|
|
const rejected = Promise.reject("oops")
|
|
const adopted = Promise.resolve(rejected)
|
|
const identity = [rejected].includes(adopted)
|
|
try {
|
|
await adopted
|
|
return "fulfilled"
|
|
} catch (reason) {
|
|
return [identity, reason]
|
|
}
|
|
`),
|
|
).toEqual([true, "oops"])
|
|
})
|
|
|
|
test("Promise.reject uses a promise reason without flattening it", async () => {
|
|
// Sources:
|
|
// test/built-ins/Promise/reject-via-fn-immed.js
|
|
// test/built-ins/Promise/reject-via-fn-deferred.js
|
|
// (adapted: the promise reason goes through Promise.reject instead of executor reject)
|
|
expect(
|
|
await value(`
|
|
const observe = async (reason) => {
|
|
try {
|
|
await Promise.reject(reason)
|
|
return "fulfilled"
|
|
} catch (caught) {
|
|
const identity = [reason].includes(caught)
|
|
try { return [identity, caught instanceof Promise, await caught] }
|
|
catch (inner) { return [identity, caught instanceof Promise, "rethrew " + inner] }
|
|
}
|
|
}
|
|
return [await observe(Promise.resolve(1)), await observe(Promise.reject("inner"))]
|
|
`),
|
|
).toEqual([
|
|
[true, true, 1],
|
|
[true, true, "rethrew inner"],
|
|
])
|
|
})
|
|
})
|
|
|
|
describe("Test262 async functions and await", () => {
|
|
test("declaration, expression, and arrow forms return promises", async () => {
|
|
// Sources:
|
|
// test/language/statements/async-function/declaration-returns-promise.js
|
|
// test/language/expressions/async-function/expression-returns-promise.js
|
|
// test/language/expressions/async-arrow-function/arrow-returns-promise.js
|
|
expect(
|
|
await value(`
|
|
async function declaration() { return 1 }
|
|
const expression = async function() { return 2 }
|
|
const arrow = async () => 3
|
|
const promises = [declaration(), expression(), arrow()]
|
|
return [promises.map((item) => item instanceof Promise), await Promise.all(promises)]
|
|
`),
|
|
).toEqual([
|
|
[true, true, true],
|
|
[1, 2, 3],
|
|
])
|
|
})
|
|
|
|
test("async bodies adopt returns and reject throws before and after await", async () => {
|
|
// Sources:
|
|
// test/language/statements/async-function/evaluation-body.js
|
|
// test/language/statements/async-function/evaluation-body-that-returns.js
|
|
// test/language/statements/async-function/evaluation-body-that-returns-after-await.js
|
|
// test/language/statements/async-function/evaluation-body-that-throws.js
|
|
// test/language/statements/async-function/evaluation-body-that-throws-after-await.js
|
|
expect(
|
|
await value(`
|
|
const order = []
|
|
const plain = async () => { order.push("body"); return 42 }
|
|
const afterAwait = async () => { await Promise.resolve(); return 43 }
|
|
const throwsBefore = async () => { throw 1 }
|
|
const throwsAfter = async () => { await Promise.resolve(); throw 2 }
|
|
const observe = async (promise) => {
|
|
try { return ["fulfilled", await promise] } catch (reason) { return ["rejected", reason] }
|
|
}
|
|
const first = plain()
|
|
return [
|
|
order,
|
|
await observe(first),
|
|
await observe(afterAwait()),
|
|
await observe(throwsBefore()),
|
|
await observe(throwsAfter()),
|
|
]
|
|
`),
|
|
).toEqual([["body"], ["fulfilled", 42], ["fulfilled", 43], ["rejected", 1], ["rejected", 2]])
|
|
})
|
|
|
|
test("default-parameter throws reject instead of escaping the call", async () => {
|
|
// Source: test/language/statements/async-function/evaluation-default-that-throws.js
|
|
expect(
|
|
await value(`
|
|
const fail = () => { throw new Error("default") }
|
|
const run = async (value = fail()) => value
|
|
let returned = false
|
|
try {
|
|
const promise = run()
|
|
returned = promise instanceof Promise
|
|
await promise
|
|
return [returned, "fulfilled"]
|
|
} catch (error) {
|
|
return [returned, error.message]
|
|
}
|
|
`),
|
|
).toEqual([true, "default"])
|
|
})
|
|
|
|
test("async try/finally completion records override earlier completion", async () => {
|
|
// Sources: the try-{return,throw,reject}-finally-{return,throw,reject}.js matrix under
|
|
// test/language/statements/async-function, test/language/expressions/async-function,
|
|
// and test/language/expressions/async-arrow-function.
|
|
expect(
|
|
await value(`
|
|
const observe = async (promise) => {
|
|
try { return ["fulfilled", await promise] } catch (reason) { return ["rejected", reason] }
|
|
}
|
|
const returnReturn = async () => { try { return "early" } finally { return await Promise.resolve("override") } }
|
|
const returnThrow = async () => { try { return "early" } finally { throw "override" } }
|
|
const returnReject = async () => { try { return "early" } finally { await Promise.reject("override") } }
|
|
const throwReturn = async () => { try { throw "early" } finally { return await Promise.resolve("override") } }
|
|
const throwThrow = async () => { try { throw "early" } finally { throw "override" } }
|
|
const throwReject = async () => { try { throw "early" } finally { await Promise.reject("override") } }
|
|
const rejectReturn = async () => { try { await Promise.reject("early") } finally { return await Promise.resolve("override") } }
|
|
const rejectThrow = async () => { try { await Promise.reject("early") } finally { throw "override" } }
|
|
const rejectReject = async () => { try { await Promise.reject("early") } finally { await Promise.reject("override") } }
|
|
return await Promise.all([
|
|
observe(returnReturn()), observe(returnThrow()), observe(returnReject()),
|
|
observe(throwReturn()), observe(throwThrow()), observe(throwReject()),
|
|
observe(rejectReturn()), observe(rejectThrow()), observe(rejectReject()),
|
|
])
|
|
`),
|
|
).toEqual([
|
|
["fulfilled", "override"],
|
|
["rejected", "override"],
|
|
["rejected", "override"],
|
|
["fulfilled", "override"],
|
|
["rejected", "override"],
|
|
["rejected", "override"],
|
|
["fulfilled", "override"],
|
|
["rejected", "override"],
|
|
["rejected", "override"],
|
|
])
|
|
})
|
|
|
|
test("await preserves an object whose then property is not callable", async () => {
|
|
// Source: test/language/expressions/await/await-awaits-thenable-not-callable.js
|
|
expect(
|
|
await value(`
|
|
const thenable = { then: 42 }
|
|
return (await thenable) === thenable
|
|
`),
|
|
).toBe(true)
|
|
})
|
|
|
|
test("await returns non-promise operands unchanged", async () => {
|
|
// Source: test/language/expressions/await/await-non-promise.js
|
|
// (adapted: only value pass-through is asserted here; the spec tick ordering around
|
|
// await of non-promises is covered by the failing interleaving test below)
|
|
expect(
|
|
await value(`
|
|
const object = { id: 1 }
|
|
const array = [1, 2]
|
|
return [
|
|
await 1,
|
|
await "text",
|
|
await true,
|
|
(await null) === null,
|
|
(await undefined) === undefined,
|
|
(await object) === object,
|
|
(await array) === array,
|
|
]
|
|
`),
|
|
).toEqual([1, "text", true, true, true, true, true])
|
|
})
|
|
})
|
|
|
|
describe("Test262 expected Promise conformance", () => {
|
|
for (const name of ["all", "allSettled", "race", "any"] as const) {
|
|
test(`Promise.${name} rejects invalid input with TypeError`, async () => {
|
|
// Sources:
|
|
// test/built-ins/Promise/all/S25.4.4.1_A3.1_T1.js
|
|
// test/built-ins/Promise/all/S25.4.4.1_A3.1_T2.js
|
|
// test/built-ins/Promise/allSettled/iter-arg-is-number-reject.js
|
|
// test/built-ins/Promise/race/iter-arg-is-number-reject.js
|
|
// test/built-ins/Promise/any/iter-arg-is-number-reject.js
|
|
expect(
|
|
await value(`
|
|
try {
|
|
const promise = Promise.${name}(42)
|
|
const returned = promise instanceof Promise
|
|
await promise
|
|
return [returned, "fulfilled"]
|
|
} catch (error) {
|
|
return [true, error.name]
|
|
}
|
|
`),
|
|
).toEqual([true, "TypeError"])
|
|
})
|
|
}
|
|
|
|
test("Promise.all consumes sparse positions as undefined", async () => {
|
|
// Source: test/built-ins/Array/from/from-array.js (array iterator hole behavior)
|
|
expect(
|
|
await value(`
|
|
const input = []
|
|
input[1] = 1
|
|
const result = await Promise.all(input)
|
|
return [result.length, result[0] === undefined, result[1]]
|
|
`),
|
|
).toEqual([2, true, 1])
|
|
})
|
|
|
|
test("Promise.allSettled consumes sparse positions as undefined", async () => {
|
|
// Source: test/built-ins/Array/from/from-array.js (array iterator hole behavior)
|
|
expect(
|
|
await value(`
|
|
const input = []
|
|
input[1] = 1
|
|
const result = await Promise.allSettled(input)
|
|
return [result.length, result[0].status, result[0].value === undefined, result[1]]
|
|
`),
|
|
).toEqual([2, "fulfilled", true, { status: "fulfilled", value: 1 }])
|
|
})
|
|
|
|
test("Promise.race consumes a sparse first position as undefined", async () => {
|
|
// Source: test/built-ins/Array/from/from-array.js (array iterator hole behavior)
|
|
expect(
|
|
await value(`
|
|
const input = []
|
|
input[1] = 1
|
|
return (await Promise.race(input)) === undefined
|
|
`),
|
|
).toBe(true)
|
|
})
|
|
|
|
test("Promise.any consumes a sparse first position as an undefined fulfillment", async () => {
|
|
// Source: test/built-ins/Array/from/from-array.js (array iterator hole behavior)
|
|
expect(
|
|
await value(`
|
|
const input = []
|
|
input[1] = Promise.reject("loses")
|
|
return (await Promise.any(input)) === undefined
|
|
`),
|
|
).toBe(true)
|
|
})
|
|
|
|
test("Promise.all settles after reactions attached to its inputs", async () => {
|
|
// Sources:
|
|
// test/built-ins/Promise/all/S25.4.4.1_A7.2_T1.js
|
|
// test/built-ins/Promise/all/S25.4.4.1_A8.1_T1.js
|
|
expect(
|
|
await value(`
|
|
const sequence = [1]
|
|
const input = Promise.resolve(1)
|
|
const aggregate = Promise.all([input])
|
|
aggregate.then(() => sequence.push(4))
|
|
input.then(() => sequence.push(3)).then(() => sequence.push(5))
|
|
sequence.push(2)
|
|
await aggregate
|
|
await Promise.resolve()
|
|
return sequence
|
|
`),
|
|
).toEqual([1, 2, 3, 4, 5])
|
|
})
|
|
|
|
test("Promise.allSettled settles after reactions attached to its inputs", async () => {
|
|
// Sources:
|
|
// test/built-ins/Promise/allSettled/resolved-sequence.js
|
|
// test/built-ins/Promise/allSettled/resolved-sequence-extra-ticks.js
|
|
// test/built-ins/Promise/allSettled/resolved-sequence-mixed.js
|
|
// test/built-ins/Promise/allSettled/resolved-sequence-with-rejections.js
|
|
expect(
|
|
await value(`
|
|
const sequence = [1]
|
|
const input = Promise.resolve(1)
|
|
const aggregate = Promise.allSettled([input])
|
|
aggregate.then(() => sequence.push(4))
|
|
input.then(() => sequence.push(3)).then(() => sequence.push(5))
|
|
sequence.push(2)
|
|
await aggregate
|
|
await Promise.resolve()
|
|
return sequence
|
|
`),
|
|
).toEqual([1, 2, 3, 4, 5])
|
|
})
|
|
|
|
test("Promise.race settles in a reaction after its winning input", async () => {
|
|
// Sources:
|
|
// test/built-ins/Promise/race/S25.4.4.3_A6.1_T1.js
|
|
// test/built-ins/Promise/race/resolved-sequence-extra-ticks.js
|
|
expect(
|
|
await value(`
|
|
const sequence = [1]
|
|
const race = Promise.race([1])
|
|
race.then(() => sequence.push(4))
|
|
Promise.resolve().then(() => sequence.push(3)).then(() => sequence.push(5))
|
|
sequence.push(2)
|
|
await race
|
|
await Promise.resolve()
|
|
return sequence
|
|
`),
|
|
).toEqual([1, 2, 3, 4, 5])
|
|
})
|
|
|
|
test("then reactions route and propagate fulfillment and rejection", async () => {
|
|
// Sources:
|
|
// test/built-ins/Promise/prototype/then/prfm-fulfilled.js
|
|
// test/built-ins/Promise/prototype/then/prfm-rejected.js
|
|
// test/built-ins/Promise/prototype/then/rxn-handler-identity.js
|
|
// test/built-ins/Promise/prototype/then/rxn-handler-thrower.js
|
|
// test/built-ins/Promise/prototype/then/rxn-handler-fulfilled-return-normal.js
|
|
// test/built-ins/Promise/prototype/then/rxn-handler-fulfilled-return-abrupt.js
|
|
// test/built-ins/Promise/prototype/then/rxn-handler-rejected-return-normal.js
|
|
// test/built-ins/Promise/prototype/then/rxn-handler-rejected-return-abrupt.js
|
|
expect(
|
|
await value(`
|
|
const observe = async (promise) => {
|
|
try { return ["fulfilled", await promise] } catch (reason) { return ["rejected", reason] }
|
|
}
|
|
return await Promise.all([
|
|
observe(Promise.resolve(1).then((value) => value + 1)),
|
|
observe(Promise.reject(2).then(undefined, (reason) => reason + 1)),
|
|
observe(Promise.resolve(3).then(undefined)),
|
|
observe(Promise.reject(4).then(undefined)),
|
|
observe(Promise.resolve(5).then(() => { throw 6 })),
|
|
observe(Promise.reject(7).then(undefined, () => { throw 8 })),
|
|
])
|
|
`),
|
|
).toEqual([
|
|
["fulfilled", 2],
|
|
["fulfilled", 3],
|
|
["fulfilled", 3],
|
|
["rejected", 4],
|
|
["rejected", 6],
|
|
["rejected", 8],
|
|
])
|
|
})
|
|
|
|
test("then reactions preserve breadth-first queue order", async () => {
|
|
// Source: test/built-ins/Promise/prototype/then/S25.4.4_A1.1_T1.js
|
|
expect(
|
|
await value(`
|
|
const sequence = [1]
|
|
const promise = Promise.resolve()
|
|
const first = promise.then(() => sequence.push(3)).then(() => sequence.push(5)).then(() => sequence.push(7))
|
|
const second = promise.then(() => sequence.push(4)).then(() => sequence.push(6)).then(() => sequence.push(8))
|
|
sequence.push(2)
|
|
await Promise.all([first, second])
|
|
return sequence
|
|
`),
|
|
).toEqual([1, 2, 3, 4, 5, 6, 7, 8])
|
|
})
|
|
|
|
test("then rejects direct self-resolution for fulfilled and rejected sources", async () => {
|
|
// Sources:
|
|
// test/built-ins/Promise/prototype/then/resolve-settled-fulfilled-self.js
|
|
// test/built-ins/Promise/prototype/then/resolve-settled-rejected-self.js
|
|
// test/built-ins/Promise/prototype/then/resolve-pending-fulfilled-self.js
|
|
// test/built-ins/Promise/prototype/then/resolve-pending-rejected-self.js
|
|
expect(
|
|
await value(`
|
|
const observe = async (promise) => {
|
|
try { await promise; return "fulfilled" } catch (reason) { return reason.name }
|
|
}
|
|
let fulfilled
|
|
let rejected
|
|
fulfilled = Promise.resolve().then(() => fulfilled)
|
|
rejected = Promise.reject().then(undefined, () => rejected)
|
|
return await Promise.all([observe(fulfilled), observe(rejected)])
|
|
`),
|
|
).toEqual(["TypeError", "TypeError"])
|
|
})
|
|
|
|
test("catch delegates rejection handling and preserves fulfillment", async () => {
|
|
// Sources:
|
|
// test/built-ins/Promise/prototype/catch/S25.4.5.1_A2.1_T1.js
|
|
// test/built-ins/Promise/prototype/catch/S25.4.5.1_A3.1_T1.js
|
|
// test/built-ins/Promise/prototype/catch/S25.4.5.1_A3.1_T2.js
|
|
expect(
|
|
await value(`
|
|
return [
|
|
await Promise.resolve(1).catch(() => 2),
|
|
await Promise.reject(3).catch((reason) => reason + 1),
|
|
]
|
|
`),
|
|
).toEqual([1, 4])
|
|
})
|
|
|
|
test("finally preserves or replaces the original settlement", async () => {
|
|
// Sources:
|
|
// test/built-ins/Promise/prototype/finally/resolution-value-no-override.js
|
|
// test/built-ins/Promise/prototype/finally/rejection-reason-no-fulfill.js
|
|
// test/built-ins/Promise/prototype/finally/rejection-reason-override-with-throw.js
|
|
expect(
|
|
await value(`
|
|
const observe = async (promise) => {
|
|
try { return ["fulfilled", await promise] } catch (reason) { return ["rejected", reason] }
|
|
}
|
|
return await Promise.all([
|
|
observe(Promise.resolve(1).finally(() => 2)),
|
|
observe(Promise.reject(3).finally(() => 4)),
|
|
observe(Promise.reject(5).finally(() => { throw 6 })),
|
|
])
|
|
`),
|
|
).toEqual([
|
|
["fulfilled", 1],
|
|
["rejected", 3],
|
|
["rejected", 6],
|
|
])
|
|
})
|
|
|
|
test("then ignores non-callable handlers", async () => {
|
|
// Sources:
|
|
// test/built-ins/Promise/prototype/then/S25.4.5.3_A4.1_T1.js
|
|
// test/built-ins/Promise/prototype/then/S25.4.5.3_A4.1_T2.js
|
|
// test/built-ins/Promise/prototype/then/S25.4.5.3_A5.1_T1.js
|
|
// test/built-ins/Promise/prototype/then/S25.4.5.3_A5.2_T1.js
|
|
// (adapted: only non-callable handlers are probed; callables that are not plain
|
|
// functions, such as tool references, intentionally throw in CodeMode)
|
|
expect(
|
|
await value(`
|
|
const observe = async (promise) => {
|
|
try { return ["fulfilled", await promise] } catch (reason) { return ["rejected", reason] }
|
|
}
|
|
return await Promise.all([
|
|
observe(Promise.resolve(1).then(2)),
|
|
observe(Promise.resolve(4).then(null, null)),
|
|
observe(Promise.resolve(5).then({}, "x")),
|
|
observe(Promise.reject(3).then(null, "x")),
|
|
observe(Promise.reject(6).then(7, {})),
|
|
])
|
|
`),
|
|
).toEqual([
|
|
["fulfilled", 1],
|
|
["fulfilled", 4],
|
|
["fulfilled", 5],
|
|
["rejected", 3],
|
|
["rejected", 6],
|
|
])
|
|
})
|
|
|
|
test("finally waits for a returned promise and preserves or replaces settlement", async () => {
|
|
// Sources:
|
|
// test/built-ins/Promise/prototype/finally/resolved-observable-then-calls.js
|
|
// test/built-ins/Promise/prototype/finally/rejected-observable-then-calls.js
|
|
// test/built-ins/Promise/prototype/finally/resolution-value-no-override.js
|
|
expect(
|
|
await value(`
|
|
const observe = async (promise) => {
|
|
try { return ["fulfilled", await promise] } catch (reason) { return ["rejected", reason] }
|
|
}
|
|
const order = []
|
|
const cleanup = async () => {
|
|
await Promise.resolve()
|
|
order.push("cleanup")
|
|
}
|
|
const settled = await Promise.resolve("kept").finally(() => cleanup())
|
|
order.push("settled:" + settled)
|
|
return [
|
|
await observe(Promise.resolve(1).finally(() => Promise.resolve(99))),
|
|
order,
|
|
await observe(Promise.resolve(2).finally(() => Promise.reject(3))),
|
|
await observe(Promise.reject(4).finally(() => Promise.resolve(99))),
|
|
]
|
|
`),
|
|
).toEqual([
|
|
["fulfilled", 1],
|
|
["cleanup", "settled:kept"],
|
|
["rejected", 3],
|
|
["rejected", 4],
|
|
])
|
|
})
|
|
|
|
test("then adopts a returned rejected promise", async () => {
|
|
// Sources:
|
|
// test/built-ins/Promise/prototype/then/rxn-handler-fulfilled-return-abrupt.js
|
|
// test/built-ins/Promise/resolve/resolve-promise.js
|
|
// (adapted: the fulfillment handler returns an already-rejected promise instead of
|
|
// throwing, and the rejection handler recovers with a fulfilled promise)
|
|
expect(
|
|
await value(`
|
|
const observe = async (promise) => {
|
|
try { return ["fulfilled", await promise] } catch (reason) { return ["rejected", reason] }
|
|
}
|
|
return await Promise.all([
|
|
observe(Promise.resolve(1).then(() => Promise.reject("bad"))),
|
|
observe(Promise.reject(2).then(undefined, () => Promise.resolve("ok"))),
|
|
])
|
|
`),
|
|
).toEqual([
|
|
["rejected", "bad"],
|
|
["fulfilled", "ok"],
|
|
])
|
|
})
|
|
|
|
test("independent reactions on one source each observe the same settlement", async () => {
|
|
// Source: test/built-ins/Promise/prototype/then/S25.4.4_A2.1_T1.js
|
|
// (adapted: the multiple-reactions family is asserted through the values every
|
|
// reaction returns instead of a shared completion counter)
|
|
expect(
|
|
await value(`
|
|
const fulfilled = Promise.resolve(7)
|
|
const rejected = Promise.reject(8)
|
|
return await Promise.all([
|
|
fulfilled.then((value) => "first:" + value),
|
|
fulfilled.then((value) => "second:" + value),
|
|
rejected.catch((reason) => "first:" + reason),
|
|
rejected.catch((reason) => "second:" + reason),
|
|
])
|
|
`),
|
|
).toEqual(["first:7", "second:7", "first:8", "second:8"])
|
|
})
|
|
|
|
test("await always resumes in a later reaction and interleaves async functions", async () => {
|
|
// Sources:
|
|
// test/language/expressions/await/async-await-interleaved.js
|
|
// test/language/expressions/await/await-non-promise.js
|
|
expect(
|
|
await value(`
|
|
const sequence = []
|
|
const first = async () => { sequence.push("first:1"); await 0; sequence.push("first:2") }
|
|
const second = async () => { sequence.push("second:1"); await 0; sequence.push("second:2") }
|
|
await Promise.all([first(), second()])
|
|
return sequence
|
|
`),
|
|
).toEqual(["first:1", "second:1", "first:2", "second:2"])
|
|
})
|
|
|
|
test("an async function rejects when it resolves with its own promise", async () => {
|
|
// Adapted from the self-resolution requirement represented by:
|
|
// test/built-ins/Promise/resolve-self.js
|
|
// test/built-ins/Promise/resolve/S25.4.4.5_A4.1_T1.js
|
|
expect(
|
|
await value(`
|
|
let promise
|
|
const run = async () => {
|
|
await Promise.resolve()
|
|
return promise
|
|
}
|
|
promise = run()
|
|
try {
|
|
await promise
|
|
return "fulfilled"
|
|
} catch (error) {
|
|
return error.name
|
|
}
|
|
`),
|
|
).toBe("TypeError")
|
|
})
|
|
|
|
test("Promise.resolve recursively assimilates callable thenables", async () => {
|
|
// Source: test/built-ins/Promise/resolve/resolve-thenable.js
|
|
expect(
|
|
await value(`
|
|
const value = { id: 1 }
|
|
const nested = { then: (resolve) => resolve(value) }
|
|
const thenable = { then: (resolve) => resolve(nested) }
|
|
return (await Promise.resolve(thenable)) === value
|
|
`),
|
|
).toBe(true)
|
|
})
|
|
|
|
test("Promise combinators assimilate callable thenable inputs", async () => {
|
|
// Sources:
|
|
// test/built-ins/Promise/all/reject-immed.js
|
|
// test/built-ins/Promise/all/reject-ignored-immed.js
|
|
// test/built-ins/Promise/allSettled/reject-ignored-immed.js
|
|
// test/built-ins/Promise/race/resolve-thenable.js
|
|
expect(
|
|
await value(`
|
|
const fulfills = { then: (resolve) => resolve(1) }
|
|
const rejects = { then: (_, reject) => reject(2) }
|
|
const resolvesFirst = { then: (resolve, reject) => { resolve(3); reject(4) } }
|
|
const observe = async (promise) => {
|
|
try { return ["fulfilled", await promise] } catch (reason) { return ["rejected", reason] }
|
|
}
|
|
return [
|
|
await observe(Promise.all([fulfills, rejects])),
|
|
await Promise.allSettled([fulfills, resolvesFirst]),
|
|
await observe(Promise.race([rejects])),
|
|
]
|
|
`),
|
|
).toEqual([
|
|
["rejected", 2],
|
|
[
|
|
{ status: "fulfilled", value: 1 },
|
|
{ status: "fulfilled", value: 3 },
|
|
],
|
|
["rejected", 2],
|
|
])
|
|
})
|
|
|
|
test("await assimilates callable thenables", async () => {
|
|
// Source: test/language/expressions/await/await-awaits-thenables.js
|
|
expect(
|
|
await value(`
|
|
const thenable = { then: (resolve) => resolve(42) }
|
|
return await thenable
|
|
`),
|
|
).toBe(42)
|
|
})
|
|
|
|
test("await rejects when a callable thenable throws", async () => {
|
|
// Source: test/language/expressions/await/await-awaits-thenables-that-throw.js
|
|
expect(
|
|
await value(`
|
|
const error = { id: 1 }
|
|
const thenable = { then: () => { throw error } }
|
|
try {
|
|
await thenable
|
|
return false
|
|
} catch (caught) {
|
|
return caught === error
|
|
}
|
|
`),
|
|
).toBe(true)
|
|
})
|
|
|
|
test("thenable resolution is deferred and settles only once", async () => {
|
|
// Sources:
|
|
// test/built-ins/Promise/resolve/S25.Promise_resolve_foreign_thenable_2.js
|
|
// test/built-ins/Promise/exception-after-resolve-in-thenable-job.js
|
|
expect(
|
|
await value(`
|
|
const sequence = []
|
|
const thenable = {
|
|
then: (resolve, reject) => {
|
|
sequence.push("then")
|
|
resolve(1)
|
|
reject(2)
|
|
throw 3
|
|
}
|
|
}
|
|
const promise = Promise.resolve(thenable)
|
|
sequence.push("after resolve")
|
|
const result = await promise
|
|
sequence.push("after await")
|
|
return [result, sequence]
|
|
`),
|
|
).toEqual([1, ["after resolve", "then", "after await"]])
|
|
})
|
|
|
|
test("the first thenable rejection wins over later resolution and throws", async () => {
|
|
// Source: test/built-ins/Promise/exception-after-resolve-in-thenable-job.js
|
|
expect(
|
|
await value(`
|
|
const thenable = {
|
|
then: (resolve, reject) => {
|
|
reject("first")
|
|
resolve("second")
|
|
throw "third"
|
|
}
|
|
}
|
|
try {
|
|
await thenable
|
|
return "fulfilled"
|
|
} catch (reason) {
|
|
return reason
|
|
}
|
|
`),
|
|
).toBe("first")
|
|
})
|
|
|
|
test("constructors, reactions, finally, and async returns assimilate thenables", async () => {
|
|
// Sources:
|
|
// test/built-ins/Promise/resolve-thenable-immed.js
|
|
// test/built-ins/Promise/prototype/then/resolve-settled-fulfilled-thenable.js
|
|
// test/built-ins/Promise/prototype/finally/resolved-observable-then-calls.js
|
|
const thenable = (value: string) => `({ then: (resolve) => resolve(${JSON.stringify(value)}) })`
|
|
expect(
|
|
await value(`
|
|
const fromAsync = async () => ${thenable("async")}
|
|
const cleanup = []
|
|
return await Promise.all([
|
|
new Promise((resolve) => resolve(${thenable("constructor")})),
|
|
Promise.resolve().then(() => ${thenable("reaction")}),
|
|
Promise.resolve("kept").finally(() => {
|
|
cleanup.push("ran")
|
|
return ${thenable("ignored")}
|
|
}),
|
|
fromAsync(),
|
|
]).then((values) => [values, cleanup])
|
|
`),
|
|
).toEqual([["constructor", "reaction", "kept", "async"], ["ran"]])
|
|
})
|
|
|
|
test("finally settles after its cleanup thenable reactions", async () => {
|
|
// Source: test/built-ins/Promise/prototype/finally/resolved-observable-then-calls-PromiseResolve.js
|
|
expect(
|
|
await value(`
|
|
const sequence = []
|
|
const cleanup = { then: (resolve) => { sequence.push("then"); resolve() } }
|
|
const result = Promise.resolve("kept").finally(() => cleanup)
|
|
result.then(() => sequence.push("finally"))
|
|
Promise.resolve()
|
|
.then(() => sequence.push("tick1"))
|
|
.then(() => sequence.push("tick2"))
|
|
.then(() => sequence.push("tick3"))
|
|
.then(() => sequence.push("tick4"))
|
|
await result
|
|
sequence.push("await")
|
|
return sequence
|
|
`),
|
|
).toEqual(["tick1", "then", "tick2", "tick3", "tick4", "finally", "await"])
|
|
})
|
|
})
|
|
|
|
describe("Test262 Promise.any", () => {
|
|
test("is a callable static that returns a promise", async () => {
|
|
// Sources:
|
|
// test/built-ins/Promise/any/is-function.js
|
|
// test/built-ins/Promise/any/returns-promise.js
|
|
expect(
|
|
await value(`
|
|
const promise = Promise.any([1])
|
|
return [typeof Promise.any, promise instanceof Promise, await promise]
|
|
`),
|
|
).toEqual(["function", true, 1])
|
|
})
|
|
|
|
test("fulfills with the first fulfilled member, ignoring rejections", async () => {
|
|
// Sources:
|
|
// test/built-ins/Promise/any/resolved-sequence-mixed.js
|
|
// test/built-ins/Promise/any/resolved-sequence-with-rejections.js
|
|
// test/built-ins/Promise/any/reject-ignored-immed.js
|
|
expect(
|
|
await value(`
|
|
return [
|
|
await Promise.any([Promise.reject("a"), Promise.resolve(1), Promise.resolve(2)]),
|
|
await Promise.any([Promise.reject("a"), "plain", Promise.reject("b")]),
|
|
]
|
|
`),
|
|
).toEqual([1, "plain"])
|
|
})
|
|
|
|
test("a fulfillment wins over a later rejection of another member", async () => {
|
|
// Sources:
|
|
// test/built-ins/Promise/any/resolve-ignores-late-rejection.js
|
|
// test/built-ins/Promise/any/resolve-ignores-late-rejection-deferred.js
|
|
expect(
|
|
await value(`
|
|
let rejectLate
|
|
const late = new Promise((_, reject) => { rejectLate = reject })
|
|
const result = await Promise.any([late, Promise.resolve("won")])
|
|
rejectLate("too late")
|
|
return result
|
|
`),
|
|
).toBe("won")
|
|
})
|
|
|
|
test("rejects with an AggregateError carrying the reasons in input order", async () => {
|
|
// Sources:
|
|
// test/built-ins/Promise/any/reject-all-mixed.js
|
|
// test/built-ins/Promise/any/reject-immed.js
|
|
// test/built-ins/Promise/any/reject-deferred.js
|
|
expect(
|
|
await value(`
|
|
let rejectLate
|
|
const late = new Promise((_, reject) => { rejectLate = reject })
|
|
const aggregate = Promise.any([Promise.reject("first"), late, Promise.reject("third")])
|
|
rejectLate("second")
|
|
try {
|
|
await aggregate
|
|
return "fulfilled"
|
|
} catch (error) {
|
|
return {
|
|
isAggregate: error instanceof AggregateError,
|
|
isError: error instanceof Error,
|
|
name: error.name,
|
|
message: error.message,
|
|
errors: error.errors,
|
|
}
|
|
}
|
|
`),
|
|
).toEqual({
|
|
isAggregate: true,
|
|
isError: true,
|
|
name: "AggregateError",
|
|
message: "All promises were rejected",
|
|
errors: ["first", "second", "third"],
|
|
})
|
|
})
|
|
|
|
test("rejects an empty input with an empty AggregateError", async () => {
|
|
// Source: test/built-ins/Promise/any/iter-arg-is-empty-iterable-reject.js
|
|
expect(
|
|
await value(`
|
|
try {
|
|
await Promise.any([])
|
|
return "fulfilled"
|
|
} catch (error) {
|
|
return [error instanceof AggregateError, error.errors.length]
|
|
}
|
|
`),
|
|
).toEqual([true, 0])
|
|
})
|
|
|
|
test("consumes a string input as its characters", async () => {
|
|
// Source: test/built-ins/Promise/any/iter-arg-is-string-resolve.js
|
|
expect(await value(`return await Promise.any("abc")`)).toBe("a")
|
|
})
|
|
|
|
test("rejects an empty string input with an empty AggregateError", async () => {
|
|
// Source: test/built-ins/Promise/any/iter-arg-is-empty-string-reject.js
|
|
expect(
|
|
await value(`
|
|
try {
|
|
await Promise.any("")
|
|
return "fulfilled"
|
|
} catch (error) {
|
|
return [error instanceof AggregateError, error.errors.length]
|
|
}
|
|
`),
|
|
).toEqual([true, 0])
|
|
})
|
|
|
|
test("fulfills with the first member that does not reject", async () => {
|
|
// Sources:
|
|
// test/built-ins/Promise/any/resolve-from-reject-catch.js
|
|
// test/built-ins/Promise/any/resolve-from-resolve-reject-catch.js
|
|
expect(
|
|
await value(`
|
|
return await Promise.any([
|
|
Promise.reject("a"),
|
|
new Promise((resolve, reject) => reject("b")),
|
|
Promise.all([Promise.reject("c")]),
|
|
Promise.resolve(Promise.reject("d").catch((reason) => reason)),
|
|
])
|
|
`),
|
|
).toBe("d")
|
|
})
|
|
|
|
test("settles after reactions attached to its inputs", async () => {
|
|
// Source: test/built-ins/Promise/any/resolved-sequence.js
|
|
expect(
|
|
await value(`
|
|
const sequence = [1]
|
|
const input = Promise.resolve(1)
|
|
const aggregate = Promise.any([input])
|
|
aggregate.then(() => sequence.push(4))
|
|
input.then(() => sequence.push(3)).then(() => sequence.push(5))
|
|
sequence.push(2)
|
|
await aggregate
|
|
await Promise.resolve()
|
|
return sequence
|
|
`),
|
|
).toEqual([1, 2, 3, 4, 5])
|
|
})
|
|
})
|
|
|
|
describe("Test262 AggregateError", () => {
|
|
test("constructs from an errors collection and an optional message", async () => {
|
|
// Sources:
|
|
// test/built-ins/AggregateError/errors-iterabletolist.js
|
|
// test/built-ins/AggregateError/message-undefined-no-prop.js
|
|
expect(
|
|
await value(`
|
|
const input = ["x", "y"]
|
|
const withMessage = new AggregateError(input, "msg")
|
|
const bare = new AggregateError([])
|
|
return [
|
|
withMessage.name,
|
|
withMessage.message,
|
|
withMessage.errors,
|
|
withMessage.errors !== input,
|
|
withMessage instanceof AggregateError,
|
|
withMessage instanceof Error,
|
|
bare.message,
|
|
bare.errors,
|
|
]
|
|
`),
|
|
).toEqual(["AggregateError", "msg", ["x", "y"], true, true, true, "", []])
|
|
})
|
|
|
|
test("rejects a non-collection errors argument with TypeError", async () => {
|
|
// Source: test/built-ins/AggregateError/errors-iterabletolist-failures.js
|
|
expect(
|
|
await value(`
|
|
try {
|
|
new AggregateError(42)
|
|
return "constructed"
|
|
} catch (error) {
|
|
return error.name
|
|
}
|
|
`),
|
|
).toBe("TypeError")
|
|
})
|
|
|
|
test("is callable without new", async () => {
|
|
// Source: test/built-ins/AggregateError/newtarget-is-undefined.js
|
|
expect(
|
|
await value(`
|
|
const error = AggregateError(["x"], "m")
|
|
return [error instanceof AggregateError, error instanceof Error, error.name, error.message, error.errors]
|
|
`),
|
|
).toEqual([true, true, "AggregateError", "m", ["x"]])
|
|
})
|
|
|
|
test("coerces a non-string message to a string", async () => {
|
|
// Source: test/built-ins/AggregateError/message-method-prop-cast.js (value coercion only; the
|
|
// upstream object-with-toString case is omitted because CodeMode has no user toString dispatch)
|
|
expect(
|
|
await value(`
|
|
return [
|
|
new AggregateError([], 42).message,
|
|
new AggregateError([], false).message,
|
|
new AggregateError([], true).message,
|
|
new AggregateError([], null).message,
|
|
]
|
|
`),
|
|
).toEqual(["42", "false", "true", "null"])
|
|
})
|
|
})
|
|
|
|
describe("Test262 Promise constructor", () => {
|
|
test("constructs a promise, handing the executor callable resolve/reject", async () => {
|
|
// Sources:
|
|
// test/built-ins/Promise/constructor.js
|
|
// test/built-ins/Promise/exec-args.js
|
|
expect(
|
|
await value(`
|
|
let observed
|
|
const promise = new Promise((resolve, reject) => {
|
|
observed = [typeof resolve, typeof reject]
|
|
resolve("done")
|
|
})
|
|
return [promise instanceof Promise, observed, await promise]
|
|
`),
|
|
).toEqual([true, ["function", "function"], "done"])
|
|
})
|
|
|
|
test("a missing or non-callable executor is a TypeError", async () => {
|
|
// Source: test/built-ins/Promise/executor-not-callable.js
|
|
expect(
|
|
await value(`
|
|
const outcomes = []
|
|
for (const make of [() => new Promise(), () => new Promise(1), () => new Promise({})]) {
|
|
try {
|
|
make()
|
|
outcomes.push("constructed")
|
|
} catch (error) {
|
|
outcomes.push(error.name)
|
|
}
|
|
}
|
|
return outcomes
|
|
`),
|
|
).toEqual(["TypeError", "TypeError", "TypeError"])
|
|
})
|
|
|
|
test("resolves immediately or later through an escaping resolver", async () => {
|
|
// Sources:
|
|
// test/built-ins/Promise/resolve-non-thenable-immed.js
|
|
// test/built-ins/Promise/resolve-non-thenable-deferred.js
|
|
// test/built-ins/Promise/create-resolving-functions-resolve.js
|
|
expect(
|
|
await value(`
|
|
let settle
|
|
const deferred = new Promise((resolve) => { settle = resolve })
|
|
const immediate = new Promise((resolve) => resolve("now"))
|
|
settle("later")
|
|
return [await immediate, await deferred]
|
|
`),
|
|
).toEqual(["now", "later"])
|
|
})
|
|
|
|
test("rejects through reject and through an abrupt executor completion", async () => {
|
|
// Sources:
|
|
// test/built-ins/Promise/reject-via-fn-immed.js
|
|
// test/built-ins/Promise/reject-via-abrupt.js
|
|
expect(
|
|
await value(`
|
|
const observe = async (promise) => {
|
|
try { return ["fulfilled", await promise] } catch (reason) { return ["rejected", reason.message ?? reason] }
|
|
}
|
|
return [
|
|
await observe(new Promise((_, reject) => reject("nope"))),
|
|
await observe(new Promise(() => { throw new Error("boom") })),
|
|
]
|
|
`),
|
|
).toEqual([
|
|
["rejected", "nope"],
|
|
["rejected", "boom"],
|
|
])
|
|
})
|
|
|
|
test("only the first settlement counts", async () => {
|
|
// Sources:
|
|
// test/built-ins/Promise/reject-ignored-via-fn-immed.js
|
|
// test/built-ins/Promise/resolve-ignored-via-fn-immed.js
|
|
expect(
|
|
await value(`
|
|
return [
|
|
await new Promise((resolve, reject) => { resolve("first"); reject("second"); resolve("third") }),
|
|
await new Promise((resolve) => { resolve(resolve("inner") === undefined ? "unreached" : "also unreached") }),
|
|
]
|
|
`),
|
|
).toEqual(["first", "inner"])
|
|
})
|
|
|
|
test("escaped resolvers keep first-settle-wins in both directions", async () => {
|
|
// Sources:
|
|
// test/built-ins/Promise/reject-ignored-via-fn-deferred.js
|
|
// test/built-ins/Promise/resolve-ignored-via-fn-deferred.js
|
|
expect(
|
|
await value(`
|
|
let resolveRejected, rejectRejected
|
|
const rejected = new Promise((resolve, reject) => { resolveRejected = resolve; rejectRejected = reject })
|
|
rejectRejected("first")
|
|
const lateResolve = resolveRejected("late")
|
|
let resolveFulfilled, rejectFulfilled
|
|
const fulfilled = new Promise((resolve, reject) => { resolveFulfilled = resolve; rejectFulfilled = reject })
|
|
resolveFulfilled()
|
|
const lateReject = rejectFulfilled(new Promise(() => {}))
|
|
try {
|
|
await rejected
|
|
return "fulfilled"
|
|
} catch (reason) {
|
|
return [reason, lateResolve === undefined, (await fulfilled) === undefined, lateReject === undefined]
|
|
}
|
|
`),
|
|
).toEqual(["first", true, true, true])
|
|
})
|
|
|
|
test("a queued reaction chain observes a later rejection through a handler-less then", async () => {
|
|
// Sources:
|
|
// test/built-ins/Promise/reject-via-fn-immed-queue.js
|
|
// test/built-ins/Promise/reject-via-fn-deferred-queue.js
|
|
// test/built-ins/Promise/reject-via-abrupt-queue.js
|
|
expect(
|
|
await value(`
|
|
const observe = (promise) => promise.then(() => "wrong").then(() => "also wrong", (reason) => "caught:" + reason)
|
|
let reject
|
|
const deferred = new Promise((_, r) => { reject = r })
|
|
const chained = observe(deferred)
|
|
reject("boom")
|
|
return [
|
|
await observe(new Promise((_, r) => r("immed"))),
|
|
await chained,
|
|
await observe(new Promise(() => { throw "abrupt" })),
|
|
]
|
|
`),
|
|
).toEqual(["caught:immed", "caught:boom", "caught:abrupt"])
|
|
})
|
|
|
|
test("an exception after resolve is ignored", async () => {
|
|
// Source: test/built-ins/Promise/exception-after-resolve-in-executor.js
|
|
expect(await value(`return await new Promise((resolve) => { resolve("kept"); throw new Error("dropped") })`)).toBe(
|
|
"kept",
|
|
)
|
|
})
|
|
|
|
test("resolving with a promise adopts its settlement", async () => {
|
|
// Sources:
|
|
// test/built-ins/Promise/resolve-thenable-immed.js (promise-adoption portion)
|
|
// test/built-ins/Promise/all/S25.4.4.1_A2.3_T1.js (resolution adoption semantics)
|
|
expect(
|
|
await value(`
|
|
const adoptedValue = await new Promise((resolve) => resolve(Promise.resolve("adopted")))
|
|
try {
|
|
await new Promise((resolve) => resolve(Promise.reject("bad")))
|
|
return [adoptedValue, "fulfilled"]
|
|
} catch (reason) {
|
|
return [adoptedValue, reason]
|
|
}
|
|
`),
|
|
).toEqual(["adopted", "bad"])
|
|
})
|
|
|
|
test("resolving with the promise itself rejects with TypeError", async () => {
|
|
// Source: test/built-ins/Promise/resolve-self.js
|
|
expect(
|
|
await value(`
|
|
let settle
|
|
const promise = new Promise((resolve) => { settle = resolve })
|
|
settle(promise)
|
|
try {
|
|
await promise
|
|
return "fulfilled"
|
|
} catch (error) {
|
|
return error.name
|
|
}
|
|
`),
|
|
).toBe("TypeError")
|
|
})
|
|
|
|
test("executor runs synchronously before the constructor returns", async () => {
|
|
// Source: test/built-ins/Promise/executor-call-context-strict.js (synchronous Call(executor) step)
|
|
expect(
|
|
await value(`
|
|
const sequence = []
|
|
sequence.push("before")
|
|
new Promise((resolve) => { sequence.push("executor"); resolve() })
|
|
sequence.push("after")
|
|
return sequence
|
|
`),
|
|
).toEqual(["before", "executor", "after"])
|
|
})
|
|
|
|
test("calling Promise without new throws TypeError", async () => {
|
|
// Source: test/built-ins/Promise/undefined-newtarget.js
|
|
expect(
|
|
await value(`
|
|
try {
|
|
Promise(() => {})
|
|
return "called"
|
|
} catch (error) {
|
|
return error.name
|
|
}
|
|
`),
|
|
).toBe("TypeError")
|
|
})
|
|
})
|