feat(codemode): add groupBy parity (#37992)
This commit is contained in:
parent
17c1e9b083
commit
0c18d06c9f
7 changed files with 289 additions and 9 deletions
184
packages/codemode/test/groupby-test262.test.ts
Normal file
184
packages/codemode/test/groupby-test262.test.ts
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
/*
|
||||
* Portions adapted from Test262 at revision 250f204f23a9249ff204be2baec29600faae7b75:
|
||||
* - test/built-ins/Object/groupBy/evenOdd.js
|
||||
* - test/built-ins/Object/groupBy/groupLength.js
|
||||
* - test/built-ins/Object/groupBy/callback-arg.js
|
||||
* - test/built-ins/Object/groupBy/string.js
|
||||
* - test/built-ins/Object/groupBy/emptyList.js
|
||||
* - test/built-ins/Object/groupBy/toPropertyKey.js
|
||||
* - test/built-ins/Object/groupBy/invalid-property-key.js
|
||||
* - test/built-ins/Object/groupBy/invalid-iterable.js
|
||||
* - test/built-ins/Object/groupBy/invalid-callback.js
|
||||
* - test/built-ins/Object/groupBy/callback-throws.js
|
||||
* - test/built-ins/Object/groupBy/null-prototype.js
|
||||
* - test/built-ins/Map/groupBy/evenOdd.js
|
||||
* - test/built-ins/Map/groupBy/groupLength.js
|
||||
* - test/built-ins/Map/groupBy/callback-arg.js
|
||||
* - test/built-ins/Map/groupBy/string.js
|
||||
* - test/built-ins/Map/groupBy/emptyList.js
|
||||
* - test/built-ins/Map/groupBy/toPropertyKey.js
|
||||
* - test/built-ins/Map/groupBy/negativeZero.js
|
||||
* - test/built-ins/Map/groupBy/invalid-iterable.js
|
||||
* - test/built-ins/Map/groupBy/invalid-callback.js
|
||||
* - test/built-ins/Map/groupBy/callback-throws.js
|
||||
* - test/built-ins/Map/groupBy/map-instance.js
|
||||
*
|
||||
* Copyright (c) 2023 Ecma International. All rights reserved.
|
||||
* Test262 portions are governed by the BSD license in LICENSE.test262.
|
||||
* Invalid-iterable cases use a plain object, equivalent to the upstream object's absent
|
||||
* Symbol.iterator; CodeMode does not support symbol-keyed properties.
|
||||
*/
|
||||
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, tools: {} }))
|
||||
|
||||
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("Object.groupBy Test262 parity", () => {
|
||||
test("groups values by coerced property keys", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
const values = [1, 2, 3]
|
||||
const grouped = Object.groupBy(values, (value) => value % 2 === 0 ? "even" : "odd")
|
||||
const stringable = { toString: () => 1 }
|
||||
const coerced = Object.groupBy([1, "1", stringable], (value) => value)
|
||||
const lengths = Object.groupBy(["hello", "test", "world"], (value) => value.length)
|
||||
return [
|
||||
Object.keys(grouped), grouped.even, grouped.odd, grouped.toString === undefined,
|
||||
Object.keys(coerced), coerced[1].length,
|
||||
Object.keys(lengths), lengths[5], lengths[4],
|
||||
]
|
||||
`),
|
||||
).toEqual([["odd", "even"], [2], [1, 3], true, ["1"], 3, ["4", "5"], ["hello", "world"], ["test"]])
|
||||
})
|
||||
|
||||
test("passes each value and index to the callback", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
const values = [-0, 0, 1, 2, 3]
|
||||
const seen = []
|
||||
Object.groupBy(values, (value, index, ...extra) => {
|
||||
seen.push([value === values[index], index, extra.length])
|
||||
return null
|
||||
})
|
||||
return seen
|
||||
`),
|
||||
).toEqual([
|
||||
[true, 0, 0],
|
||||
[true, 1, 0],
|
||||
[true, 2, 0],
|
||||
[true, 3, 0],
|
||||
[true, 4, 0],
|
||||
])
|
||||
})
|
||||
|
||||
test("supports strings and empty collections", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
const grouped = Object.groupBy("🥰💩🙏😈", (char) => char < "🙏" ? "before" : "after")
|
||||
const empty = Object.groupBy([], () => { throw new Error("not called") })
|
||||
return [Object.keys(grouped), grouped.before, grouped.after, Object.keys(empty)]
|
||||
`),
|
||||
).toEqual([["after", "before"], ["💩", "😈"], ["🥰", "🙏"], []])
|
||||
})
|
||||
|
||||
test("rejects invalid inputs and propagates callback and key-coercion failures", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
const messages = []
|
||||
try { Object.groupBy({}, () => { throw new Error("not called") }) } catch (error) { messages.push(error.name) }
|
||||
for (const callback of [null, undefined, {}]) {
|
||||
try { Object.groupBy([], callback) } catch (error) { messages.push(error.name) }
|
||||
}
|
||||
try { Object.groupBy([1], () => { throw new Error("callback") }) } catch (error) { messages.push(error.message) }
|
||||
try {
|
||||
Object.groupBy([1], () => ({
|
||||
toString: () => { throw new Error("property key") },
|
||||
}))
|
||||
} catch (error) { messages.push(error.message) }
|
||||
return messages
|
||||
`),
|
||||
).toEqual(["TypeError", "TypeError", "TypeError", "TypeError", "callback", "property key"])
|
||||
})
|
||||
})
|
||||
|
||||
describe("Map.groupBy Test262 parity", () => {
|
||||
test("returns a Map with identity-preserving groups", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
const stringable = { toString: () => 1 }
|
||||
const grouped = Map.groupBy([1, "1", stringable], (value) => value)
|
||||
const parity = Map.groupBy([1, 2, 3], (value) => value % 2 === 0 ? "even" : "odd")
|
||||
const lengths = Map.groupBy(["hello", "test", "world"], (value) => value.length)
|
||||
return [
|
||||
grouped instanceof Map,
|
||||
grouped.size,
|
||||
grouped.get(1),
|
||||
grouped.get("1"),
|
||||
grouped.has(stringable),
|
||||
grouped.keys().length,
|
||||
parity.get("even"),
|
||||
parity.get("odd"),
|
||||
lengths.keys(),
|
||||
lengths.get(5),
|
||||
lengths.get(4),
|
||||
]
|
||||
`),
|
||||
).toEqual([true, 3, [1], ["1"], true, 3, [2], [1, 3], [5, 4], ["hello", "world"], ["test"]])
|
||||
})
|
||||
|
||||
test("normalizes negative zero and passes each value and index", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
const values = [-0, 0, 1, 2, 3]
|
||||
const seen = []
|
||||
const grouped = Map.groupBy(values, (value, index, ...extra) => {
|
||||
seen.push([value === values[index], index, extra.length])
|
||||
return value
|
||||
})
|
||||
return [grouped.size, grouped.get(0), seen]
|
||||
`),
|
||||
).toEqual([
|
||||
4,
|
||||
[-0, 0],
|
||||
[
|
||||
[true, 0, 0],
|
||||
[true, 1, 0],
|
||||
[true, 2, 0],
|
||||
[true, 3, 0],
|
||||
[true, 4, 0],
|
||||
],
|
||||
])
|
||||
})
|
||||
|
||||
test("supports strings and empty collections", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
const grouped = Map.groupBy("🥰💩🙏😈", (char) => char < "🙏" ? "before" : "after")
|
||||
const empty = Map.groupBy([], () => { throw new Error("not called") })
|
||||
return [grouped.keys(), grouped.get("before"), grouped.get("after"), empty.size]
|
||||
`),
|
||||
).toEqual([["after", "before"], ["💩", "😈"], ["🥰", "🙏"], 0])
|
||||
})
|
||||
|
||||
test("rejects invalid inputs and propagates callback failures", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
const results = []
|
||||
try { Map.groupBy({}, () => { throw new Error("not called") }) } catch (error) { results.push(error.name) }
|
||||
for (const callback of [null, undefined, {}]) {
|
||||
try { Map.groupBy([], callback) } catch (error) { results.push(error.name) }
|
||||
}
|
||||
try { Map.groupBy([1], () => { throw new Error("callback") }) }
|
||||
catch (error) { results.push(error.message) }
|
||||
return results
|
||||
`),
|
||||
).toEqual(["TypeError", "TypeError", "TypeError", "TypeError", "callback"])
|
||||
})
|
||||
})
|
||||
|
|
@ -857,7 +857,6 @@ describe("coercion parity: ++ and -- use CodeMode numeric coercion", () => {
|
|||
describe("coercion parity: unknown static members read as undefined", () => {
|
||||
test("feature detection on missing statics works like native JS", async () => {
|
||||
expect(await value(`return typeof Math.sum`)).toBe("undefined")
|
||||
expect(await value(`return Object.groupBy === undefined`)).toBe(true)
|
||||
expect(await value(`return RegExp.quote === undefined`)).toBe(true)
|
||||
expect(await value(`return Number.range === undefined`)).toBe(true)
|
||||
expect(await value(`return String.raw === undefined`)).toBe(true)
|
||||
|
|
@ -866,7 +865,6 @@ describe("coercion parity: unknown static members read as undefined", () => {
|
|||
expect(await value(`return Date.moment === undefined`)).toBe(true)
|
||||
expect(await value(`return JSON.rawJSON === undefined`)).toBe(true)
|
||||
expect(await value(`return URL.createObjectURL === undefined`)).toBe(true)
|
||||
expect(await value(`return Map.groupBy === undefined`)).toBe(true)
|
||||
expect(await value(`return Math.sum?.([1]) ?? "fallback"`)).toBe("fallback")
|
||||
})
|
||||
|
||||
|
|
@ -876,6 +874,8 @@ describe("coercion parity: unknown static members read as undefined", () => {
|
|||
expect(await value(`return typeof Date.now`)).toBe("function")
|
||||
expect(await value(`return typeof Math.sumPrecise`)).toBe("function")
|
||||
expect(await value(`return typeof RegExp.escape`)).toBe("function")
|
||||
expect(await value(`return typeof Object.groupBy`)).toBe("function")
|
||||
expect(await value(`return typeof Map.groupBy`)).toBe("function")
|
||||
expect(await value(`return Math.max(1, 2)`)).toBe(2)
|
||||
expect(await value(`return Math.sumPrecise([1, 2])`)).toBe(3)
|
||||
expect(await value(`return RegExp.escape("a.b")`)).toBe("\\x61\\.b")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue