X-Git-Url: https://git.ladys.computer/Pisces/blobdiff_plain/8c273de93ce5adb58bb90c3fe676730a451b3c89..07762ac4c632a6436d43d50d58c8d91760e81e44:/collection.test.js?ds=inline diff --git a/collection.test.js b/collection.test.js index 7c242cc..079ef4b 100644 --- a/collection.test.js +++ b/collection.test.js @@ -1,60 +1,1048 @@ -// ♓🌟 Piscēs ∷ collection.test.js -// ==================================================================== -// -// Copyright © 2022 Lady [@ Lady’s Computer]. -// -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at . +// SPDX-FileCopyrightText: 2022, 2023, 2025 Lady +// SPDX-License-Identifier: MPL-2.0 +/** + * ⁌ ♓🧩 Piscēs ∷ collection.js + * + * Copyright © 2022–2023, 2025 Lady [@ Ladys Computer]. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at . + */ import { assertEquals, assertSpyCall, assertSpyCalls, assertStrictEquals, + assertThrows, describe, it, spy, } from "./dev-deps.js"; import { - findIndexedEntry, - isArrayIndexString, - isArraylikeObject, + array, + concatSpreadableCatenate, + copyWithin, + denseProxy, + fill, + filter, + findFirstIndex, + findFirstIndexedEntry, + findFirstItem, + findLastIndex, + findLastIndexedEntry, + findLastItem, + flatmap, + flatten as _flatten /* TK */, + getFirstIndex as _getFirstIndex /* TK */, + getItem as _getItem /* TK */, + getLastIndex as _getLastIndex /* TK */, + hasEvery as _hasEvery /* TK */, + hasSome as _hasSome /* TK */, + includes as _includes /* TK */, + indexedEntries as _indexedEntries /* TK */, + indices as _indices /* TK */, + isArray as _isArray /* TK */, isCollection, - isConcatSpreadable, + isDenseProxy, + items as _items /* TK */, + map as _map /* TK */, + pop as _pop /* TK */, + push as _push /* TK */, + reduce as _reduce /* TK */, + reverse as _reverse /* TK */, + shift as _shift /* TK */, + slice as _slice /* TK */, + sort as _sort /* TK */, + splice as _splice /* TK */, + toArray, + toDenseArray, + unshift as _unshift /* TK */, } from "./collection.js"; -describe("findIndexedEntry", () => { +describe("array", () => { + it("[[Call]] returns an array of the provided values", () => { + assertEquals(array("etaoin", [], true), ["etaoin", [], true]); + }); + + it("[[Call]] returns an empty array with no arguments", () => { + assertEquals(array(), []); + }); + + it("[[Construct]] throws an error", () => { + assertThrows(() => new array()); + }); + + describe(".length", () => { + it("[[Get]] returns the correct length", () => { + assertStrictEquals(array.length, 0); + }); + }); + + describe(".name", () => { + it("[[Get]] returns the correct name", () => { + assertStrictEquals(array.name, "array"); + }); + }); +}); + +describe("concatSpreadableCatenate", () => { + it("[[Call]] catenates concat spreadable values", () => { + assertEquals( + concatSpreadableCatenate([1, 2], [2, [3]], { + length: 1, + [Symbol.isConcatSpreadable]: true, + }), + [1, 2, 2, [3], ,], + ); + }); + + it("[[Call]] does not catenate other values", () => { + assertEquals(concatSpreadableCatenate({}, "etaoin"), [ + {}, + "etaoin", + ]); + }); + + it("[[Call]] allows a nullish first argument", () => { + assertEquals(concatSpreadableCatenate(null), [null]); + assertEquals(concatSpreadableCatenate(undefined), [undefined]); + }); + + it("[[Call]] returns an empty array with no arguments", () => { + assertEquals(concatSpreadableCatenate(), []); + }); + + it("[[Construct]] throws an error", () => { + assertThrows(() => new concatSpreadableCatenate()); + }); + + describe(".length", () => { + it("[[Get]] returns the correct length", () => { + assertStrictEquals(concatSpreadableCatenate.length, 2); + }); + }); + + describe(".name", () => { + it("[[Get]] returns the correct name", () => { + assertStrictEquals( + concatSpreadableCatenate.name, + "concatSpreadableCatenate", + ); + }); + }); +}); + +describe("copyWithin", () => { + it("[[Call]] copies within", () => { + assertEquals( + copyWithin(["a", "b", "c", , "e"], 0, 3, 4), + [, "b", "c", , "e"], + ); + assertEquals( + copyWithin(["a", "b", "c", , "e"], 1, 3), + ["a", , "e", , "e"], + ); + }); + + it("[[Construct]] throws an error", () => { + assertThrows(() => new copyWithin([], 0, 0)); + }); + + describe(".length", () => { + it("[[Get]] returns the correct length", () => { + assertStrictEquals(copyWithin.length, 3); + }); + }); + + describe(".name", () => { + it("[[Get]] returns the correct name", () => { + assertStrictEquals(copyWithin.name, "copyWithin"); + }); + }); +}); + +describe("denseProxy", () => { + const makeUnderlying = () => + Object.create({ 2: "inherited" }, { + // 0 is not present + 1: { value: "static", configurable: true, writable: true }, + // 2 is not an own property, but is present on the prototype + 3: { configurable: true, get: () => "dynamic" }, + length: { value: "4", configurable: true, writable: true }, + }); + + it("[[Call]] returns an object which inherits from the provided object", () => { + const underlying = makeUnderlying(); + const proxy = denseProxy(underlying); + assertStrictEquals( + Object.getPrototypeOf(proxy), + underlying, + ); + }); + + it("[[Construct]] throws an error", () => { + assertThrows(() => new denseProxy([])); + }); + + it("[[OwnPropertyKeys]] lists integer indices, then the length, then other keys", () => { + const underlying = makeUnderlying(); + const proxy = denseProxy(underlying); + const sym = Symbol(); + proxy[sym] = "shrdlu"; + proxy["1.2.3"] = "etaion"; + assertEquals( + Reflect.ownKeys(proxy), + ["0", "1", "2", "3", "length", "1.2.3", sym], + ); + }); + + it("[[PreventExtensions]] fails if the underlying object is extensible", () => { + const underlying = makeUnderlying(); + const proxy = denseProxy(underlying); + assertStrictEquals( + Reflect.preventExtensions(proxy), + false, + ); + }); + + it("[[PreventExtensions]] fails if the underlying object has a nonconstant length", () => { + const underlying = makeUnderlying(); + const proxy = denseProxy(underlying); + Object.defineProperty(underlying, "length", { get: () => 4 }); + Object.freeze(underlying); + assertStrictEquals( + Reflect.preventExtensions(proxy), + false, + ); + }); + + it("[[PreventExtensions]] succeeds if the underlying object is non·extensible and has a constant length", () => { + const underlying = makeUnderlying(); + const proxy = denseProxy(underlying); + Object.defineProperty(underlying, "length", { + configurable: false, + writable: false, + }); + Object.preventExtensions(underlying); + assertStrictEquals( + Reflect.preventExtensions(proxy), + true, + ); + }); + + it("[[SetPrototypeOf]] fails to change the prototype", () => { + const underlying = makeUnderlying(); + const proxy = denseProxy(underlying); + assertStrictEquals( + Reflect.setPrototypeOf(proxy, {}), + false, + ); + }); + + it("[[SetPrototypeOf]] succeeds keeping the prototype the same", () => { + const underlying = makeUnderlying(); + const proxy = denseProxy(underlying); + assertStrictEquals( + Reflect.setPrototypeOf(proxy, underlying), + true, + ); + }); + + describe(".length", () => { + it("[[Get]] returns the correct length", () => { + assertStrictEquals(denseProxy.length, 1); + }); + }); + + describe(".name", () => { + it("[[Get]] returns the correct name", () => { + assertStrictEquals(denseProxy.name, "denseProxy"); + }); + }); + + describe("~[]", () => { + it("[[DefineProperty]] allows changes when the property is not an index property or length", () => { + const underlying = makeUnderlying(); + const proxy = denseProxy(underlying); + const newValue = Symbol(); + proxy.etaoin = newValue; + assertStrictEquals( + proxy.etaoin, + newValue, + ); + }); + + it("[[DefineProperty]] allows changing nothing when the property is not an own property", () => { + const underlying = makeUnderlying(); + const proxy = denseProxy(underlying); + assertStrictEquals( + Reflect.defineProperty( + proxy, + "0", + { + configurable: true, + enumerable: true, + writable: false, + value: undefined, + }, + ), + true, + ); + assertStrictEquals( + Reflect.defineProperty( + proxy, + "2", + { + configurable: true, + enumerable: true, + writable: false, + value: undefined, + }, + ), + true, + ); + /* test nonconfigurable versions too */ + Object.freeze(underlying); + Object.defineProperty(proxy, "0", { configurable: false }); + Object.defineProperty(proxy, "2", { configurable: false }); + assertStrictEquals( + Reflect.defineProperty( + proxy, + "0", + Object.getOwnPropertyDescriptor(proxy, "0"), + ), + true, + ); + assertStrictEquals( + Reflect.defineProperty( + proxy, + "2", + Object.getOwnPropertyDescriptor(proxy, "2"), + ), + true, + ); + }); + + it("[[DefineProperty]] allows changing nothing when the property is a data index property", () => { + const underlying = makeUnderlying(); + const proxy = denseProxy(underlying); + assertStrictEquals( + Reflect.defineProperty( + proxy, + "1", + { + configurable: true, + enumerable: true, + writable: false, + value: underlying[1], + }, + ), + true, + ); + /* test nonconfigurable versions too */ + Object.freeze(underlying); + Object.defineProperty(proxy, "1", { configurable: false }); + assertStrictEquals( + Reflect.defineProperty( + proxy, + "1", + Object.getOwnPropertyDescriptor(proxy, "1"), + ), + true, + ); + }); + + it("[[DefineProperty]] allows changing nothing when the property is an accessor index property", () => { + const underlying = makeUnderlying(); + const proxy = denseProxy(underlying); + assertStrictEquals( + Reflect.defineProperty( + proxy, + "3", + { + configurable: true, + enumerable: true, + writable: false, + value: underlying[3], + }, + ), + true, + ); + /* test nonconfigurable versions too */ + Object.freeze(underlying); + Object.defineProperty(proxy, "1", { configurable: false }); + assertStrictEquals( + Reflect.defineProperty( + proxy, + "1", + Object.getOwnPropertyDescriptor(proxy, "1"), + ), + true, + ); + }); + + it("[[DefineProperty]] does not allow a change in enumerablility on index properties", () => { + const underlying = makeUnderlying(); + const proxy = denseProxy(underlying); + for (let i = 0; i < 4; ++i) { + assertStrictEquals( + Reflect.defineProperty(proxy, i, { enumerable: false }), + false, + ); + } + }); + + it("[[DefineProperty]] does not allow a change in value on index properties", () => { + const underlying = makeUnderlying(); + const proxy = denseProxy(underlying); + for (let i = 0; i < 4; ++i) { + assertStrictEquals( + Reflect.defineProperty(proxy, i, { value: "new value" }) + && (() => { + throw i; + })(), + false, + ); + } + }); + + it("[[DefineProperty]] does not allow a change in getter on index properties", () => { + const underlying = makeUnderlying(); + const proxy = denseProxy(underlying); + for (let i = 0; i < 4; ++i) { + assertStrictEquals( + Reflect.defineProperty(proxy, i, { + get: () => underlying[i], + }) && (() => { + throw i; + })(), + false, + ); + } + }); + + it("[[DefineProperty]] does not allow a change in setter on index properties", () => { + const underlying = makeUnderlying(); + const proxy = denseProxy(underlying); + for (let i = 0; i < 4; ++i) { + assertStrictEquals( + Reflect.defineProperty(proxy, i, { set: () => undefined }), + false, + ); + } + }); + + it("[[DefineProperty]] does not allow a change in configurability on index properties if the property in the underlying object may change", () => { + const underlying = makeUnderlying(); + const proxy = denseProxy(underlying); + Object.defineProperty(underlying, "length", { + configurable: false, + writable: false, + }); + for (let i = 0; i < 4; ++i) { + assertStrictEquals( + Reflect.defineProperty(proxy, i, { configurable: false }), + false, + ); + } + }); + + it("[[DefineProperty]] does not allow a change in configurability on index properties if the length of the underlying object may change", () => { + const underlying = makeUnderlying(); + const proxy = denseProxy(underlying); + Object.seal(underlying); + for (let i = 0; i < 4; ++i) { + assertStrictEquals( + Reflect.defineProperty(proxy, i, { configurable: false }), + false, + ); + } + }); + + it("[[DefineProperty]] allows a change in configurability on index properties when it is safe to do so", () => { + const underlying = makeUnderlying(); + const proxy = denseProxy(underlying); + Object.defineProperty(underlying, "length", { + configurable: false, + writable: false, + }); + Object.defineProperty(underlying, "1", { + configurable: false, + writable: false, + }); + Object.defineProperty(underlying, "3", { configurable: false }); + Object.preventExtensions(underlying); + for (let i = 0; i < 4; ++i) { + assertStrictEquals( + Reflect.defineProperty(proxy, i, { configurable: false }), + true, + ); + } + }); + + it("[[Delete]] is allowed when the property is not an index property or length", () => { + const underlying = makeUnderlying(); + const proxy = denseProxy(underlying); + proxy.a = "etaoin"; + assertStrictEquals( + Reflect.deleteProperty(proxy, "a"), + true, + ); + assertStrictEquals( + Reflect.has(proxy, "a"), + false, + ); + }); + + it("[[Delete]] is forbidden for index properties less than the length", () => { + const underlying = makeUnderlying(); + const proxy = denseProxy(underlying); + for (let i = 0; i < 4; ++i) { + assertStrictEquals( + Reflect.deleteProperty(proxy, i), + false, + ); + } + }); + + it("[[Delete]] is allowed for index properties greater than or equal to the length", () => { + const underlying = makeUnderlying(); + const proxy = denseProxy(underlying); + assertStrictEquals( + Reflect.deleteProperty(proxy, "4"), + true, + ); + assertStrictEquals( + Reflect.deleteProperty(proxy, "5"), + true, + ); + }); + + it("[[GetOwnProperty]] gives the value of an index property as a data property by default", () => { + const underlying = makeUnderlying(); + const proxy = denseProxy(underlying); + assertEquals( + Object.getOwnPropertyDescriptor(proxy, 1), + { + configurable: true, + enumerable: true, + value: underlying[1], + writable: false, + }, + ); + assertEquals( + Object.getOwnPropertyDescriptor(proxy, 3), + { + configurable: true, + enumerable: true, + value: underlying[3], + writable: false, + }, + ); + /* test nonconfigurable data properties too */ + Object.freeze(underlying); + Object.defineProperty(proxy, "1", { configurable: false }); + assertEquals( + Object.getOwnPropertyDescriptor(proxy, 1), + { + configurable: false, + enumerable: true, + value: underlying[1], + writable: false, + }, + ); + }); + + it("[[GetOwnProperty]] gives a value of undefined if the underlying object does not have an index property as an own property", () => { + const underlying = makeUnderlying(); + const proxy = denseProxy(underlying); + assertEquals( + Object.getOwnPropertyDescriptor(proxy, 0), + { + configurable: true, + enumerable: true, + value: undefined, + writable: false, + }, + ); + assertEquals( + Object.getOwnPropertyDescriptor(proxy, 2), + { + configurable: true, + enumerable: true, + value: undefined, + writable: false, + }, + ); + }); + + it("[[GetOwnProperty]] gives a value of undefined for index properties less than the length", () => { + const underlying = makeUnderlying(); + underlying.length = 0; + const proxy = denseProxy(underlying); + for (let i = 0; i < 4; ++i) { + assertStrictEquals( + Object.getOwnPropertyDescriptor(proxy, i), + undefined, + ); + } + }); + + it("[[GetOwnProperty]] gives a getter when the underlying object has a getter and an index property is not configurable", () => { + const underlying = makeUnderlying(); + const proxy = denseProxy(underlying); + Object.freeze(underlying); + Object.defineProperty(proxy, "3", { configurable: false }); + const descriptor = Object.getOwnPropertyDescriptor(proxy, 3); + assertEquals( + descriptor, + { + configurable: false, + enumerable: true, + get: descriptor.get, + set: undefined, + }, + ); + }); + + describe("[[GetOwnProperty]].get.length", () => { + it("[[Get]] returns the correct length", () => { + const underlying = makeUnderlying(); + const proxy = denseProxy(underlying); + Object.freeze(underlying); + Object.defineProperty(proxy, "3", { configurable: false }); + assertStrictEquals( + Object.getOwnPropertyDescriptor( + proxy, + "3", + ).get.length, + 0, + ); + }); + }); + + describe("[[GetOwnProperty]].get.name", () => { + it("[[Get]] returns the correct name", () => { + const underlying = makeUnderlying(); + const proxy = denseProxy(underlying); + Object.freeze(underlying); + Object.defineProperty(proxy, "3", { configurable: false }); + assertStrictEquals( + Object.getOwnPropertyDescriptor( + proxy, + "3", + ).get.name, + "get 3", + ); + }); + }); + + it("[[HasProperty]] works when the property is not an index property or length", () => { + const underlying = makeUnderlying(); + const proxy = denseProxy(underlying); + proxy.a = "etaoin"; + Object.getPrototypeOf(underlying).b = "shrdlu"; + assertStrictEquals( + Reflect.has(proxy, "a"), + true, + ); + assertStrictEquals( + Reflect.has(proxy, "b"), + true, + ); + assertStrictEquals( + Reflect.has(proxy, "z"), + false, + ); + }); + + it("[[HasProperty]] works for index properties less than the length", () => { + const underlying = makeUnderlying(); + const proxy = denseProxy(underlying); + for (let i = 0; i < 4; ++i) { + assertStrictEquals( + Reflect.has(proxy, i), + true, + ); + } + delete underlying.length; + for (let i = 0; i < 4; ++i) { + assertStrictEquals( + Reflect.has(proxy, i), + Reflect.has(underlying, i), + ); + } + }); + + it("[[HasProperty]] works for index properties greater than or equal to the length", () => { + const underlying = makeUnderlying(); + const proxy = denseProxy(underlying); + assertStrictEquals( + Reflect.has(proxy, "4"), + false, + ); + assertStrictEquals( + Reflect.has(proxy, "5"), + false, + ); + underlying[4] = "inherited now"; + assertStrictEquals( + Reflect.has(proxy, "4"), + true, + ); + }); + }); + + describe("~length", () => { + it("[[DefineProperty]] allows changing nothing", () => { + const underlying = makeUnderlying(); + const proxy = denseProxy(underlying); + assertStrictEquals( + Reflect.defineProperty( + proxy, + "length", + { + configurable: true, + enumerable: false, + writable: false, + value: underlying.length >>> 0, + }, + ), + true, + ); + /* test nonconfigurable versions too */ + Object.freeze(underlying); + Object.defineProperty(proxy, "length", { configurable: false }); + assertStrictEquals( + Reflect.defineProperty( + proxy, + "length", + Object.getOwnPropertyDescriptor(proxy, "length"), + ), + true, + ); + }); + + it("[[DefineProperty]] does not allow a change in enumerablility", () => { + const underlying = makeUnderlying(); + const proxy = denseProxy(underlying); + assertStrictEquals( + Reflect.defineProperty(proxy, "length", { enumerable: true }), + false, + ); + }); + + it("[[DefineProperty]] does not allow a change in value", () => { + const underlying = makeUnderlying(); + const proxy = denseProxy(underlying); + assertStrictEquals( + Reflect.defineProperty(proxy, "length", { value: 0 }), + false, + ); + }); + + it("[[DefineProperty]] does not allow a change in getter", () => { + const underlying = makeUnderlying(); + const proxy = denseProxy(underlying); + assertStrictEquals( + Reflect.defineProperty(proxy, "length", { + get: () => underlying.length >>> 0, + }), + false, + ); + }); + + it("[[DefineProperty]] does not allow a change in setter", () => { + const underlying = makeUnderlying(); + const proxy = denseProxy(underlying); + assertStrictEquals( + Reflect.defineProperty(proxy, "length", { set: () => {} }), + false, + ); + }); + + it("[[DefineProperty]] does not allow a change in configurability if the length of the underlying object may change", () => { + const underlying = makeUnderlying(); + const proxy = denseProxy(underlying); + assertStrictEquals( + Reflect.defineProperty(proxy, "length", { + configurable: false, + }), + false, + ); + Object.defineProperty(underlying, "length", { + configurable: false, + get: () => 0, + }); + assertStrictEquals( + Reflect.defineProperty(proxy, "length", { + configurable: false, + }), + false, + ); + }); + + it("[[DefineProperty]] allows a change in configurability when it is safe to do so", () => { + const underlying = makeUnderlying(); + const proxy = denseProxy(underlying); + Object.defineProperty(underlying, "length", { + configurable: false, + writable: false, + }); + assertStrictEquals( + Reflect.defineProperty(proxy, "length", { + configurable: false, + }), + true, + ); + }); + + it("[[Delete]] is forbidden", () => { + const underlying = makeUnderlying(); + const proxy = denseProxy(underlying); + assertStrictEquals( + Reflect.deleteProperty( + proxy, + "length", + ), + false, + ); + }); + + it("[[GetOwnProperty]] gives the value as a data property", () => { + const underlying = makeUnderlying(); + const proxy = denseProxy(underlying); + assertEquals( + Object.getOwnPropertyDescriptor(proxy, "length"), + { + configurable: true, + enumerable: false, + value: underlying.length >>> 0, + writable: false, + }, + ); + /* test nonconfigurable data properties too */ + Object.freeze(underlying); + Object.defineProperty(proxy, "length", { configurable: false }); + assertEquals( + Object.getOwnPropertyDescriptor(proxy, "length"), + { + configurable: false, + enumerable: false, + value: underlying.length >>> 0, + writable: false, + }, + ); + }); + + it("[[GetOwnProperty]] gives 0 if the underlying object does not have the property as an own property", () => { + const underlying = makeUnderlying(); + const proxy = denseProxy(underlying); + delete underlying.length; + Object.getPrototypeOf(underlying).length = 3; + assertEquals( + Object.getOwnPropertyDescriptor(proxy, "length"), + { + configurable: true, + enumerable: false, + value: 0, + writable: false, + }, + ); + }); + + it("[[HasProperty]] is always true", () => { + const underlying = makeUnderlying(); + const proxy = denseProxy(underlying); + assertStrictEquals( + Reflect.has(proxy, "length"), + true, + ); + delete underlying.length; + assertStrictEquals( + Reflect.has(proxy, "length"), + true, + ); + }); + }); +}); + +describe("fill", () => { + it("[[Call]] fills", () => { + assertEquals( + fill({ 1: "failure", length: 3 }, "success"), + { 0: "success", 1: "success", 2: "success", length: 3 }, + ); + }); + + it("[[Call]] can fill a range", () => { + assertEquals( + fill({ 1: "failure", length: 4 }, "success", 1, 3), + { 1: "success", 2: "success", length: 4 }, + ); + }); + + it("[[Construct]] throws an error", () => { + assertThrows(() => new fill([], null)); + }); + + describe(".length", () => { + it("[[Get]] returns the correct length", () => { + assertStrictEquals(fill.length, 2); + }); + }); + + describe(".name", () => { + it("[[Get]] returns the correct name", () => { + assertStrictEquals(fill.name, "fill"); + }); + }); +}); + +describe("filter", () => { + it("[[Call]] filters", () => { + assertEquals( + filter(["failure", "success", ,], function ($) { + return !$ || $ == this; + }, "success"), + ["success"], + ); + }); + + it("[[Construct]] throws an error", () => { + assertThrows(() => new filter([], () => {})); + }); + + describe(".length", () => { + it("[[Get]] returns the correct length", () => { + assertStrictEquals(filter.length, 2); + }); + }); + + describe(".name", () => { + it("[[Get]] returns the correct name", () => { + assertStrictEquals(filter.name, "filter"); + }); + }); +}); + +describe("findFirstIndex", () => { + it("[[Call]] returns undefined if no matching entry exists", () => { + assertStrictEquals(findFirstIndex([], () => true), undefined); + assertStrictEquals(findFirstIndex([1], () => false), undefined); + }); + + it("[[Call]] returns an index for the first match", () => { + assertStrictEquals( + findFirstIndex([, true, false], ($) => $ ?? true), + 1, + ); + assertStrictEquals( + findFirstIndex( + ["failure", "success", "success"], + ($) => $ == "success", + ), + 1, + ); + }); + + it("[[Call]] works on arraylike objects", () => { + assertStrictEquals( + findFirstIndex({ 1: "success", length: 2 }, ($) => $), + 1, + ); + assertStrictEquals( + findFirstIndex({ 1: "failure", length: 1 }, ($) => $), + undefined, + ); + }); + + it("[[Call]] only gets the value once", () => { + const get1 = spy(() => true); + findFirstIndex({ + get 1() { + return get1(); + }, + length: 2, + }, ($) => $); + assertSpyCalls(get1, 1); + }); + + it("[[Call]] passes the value, index, and this value to the callback", () => { + const arr = [, "failure", "success", "success"]; + const callback = spy(($) => $ === "success"); + const thisArg = {}; + findFirstIndex(arr, callback, thisArg); + assertSpyCalls(callback, 2); + assertSpyCall(callback, 0, { + args: ["failure", 1, arr], + self: thisArg, + }); + assertSpyCall(callback, 1, { + args: ["success", 2, arr], + self: thisArg, + }); + }); + + it("[[Construct]] throws an error", () => { + assertThrows(() => new findFirstIndex([], () => {})); + }); + + describe(".length", () => { + it("[[Get]] returns the correct length", () => { + assertStrictEquals(findFirstIndex.length, 2); + }); + }); + + describe(".name", () => { + it("[[Get]] returns the correct name", () => { + assertStrictEquals(findFirstIndex.name, "findFirstIndex"); + }); + }); +}); + +describe("findFirstIndexedEntry", () => { it("[[Call]] returns undefined if no matching entry exists", () => { - assertStrictEquals(findIndexedEntry([], () => true), void {}); - assertStrictEquals(findIndexedEntry([1], () => false), void {}); + assertStrictEquals( + findFirstIndexedEntry([], () => true), + undefined, + ); + assertStrictEquals( + findFirstIndexedEntry([1], () => false), + undefined, + ); }); it("[[Call]] returns an entry for the first match", () => { assertEquals( - findIndexedEntry([, true, false], ($) => $ ?? true), - [0, void {}], + findFirstIndexedEntry([, true, false], ($) => $ ?? true), + [1, true], ); assertEquals( - findIndexedEntry(["failure", "success"], ($) => $ == "success"), + findFirstIndexedEntry( + ["failure", "success", "success"], + ($) => $ == "success", + ), [1, "success"], ); }); it("[[Call]] works on arraylike objects", () => { assertEquals( - findIndexedEntry({ 1: "success", length: 2 }, ($) => $), + findFirstIndexedEntry({ 1: "success", length: 2 }, ($) => $), [1, "success"], ); assertEquals( - findIndexedEntry({ 1: "failure", length: 1 }, ($) => $), - void {}, + findFirstIndexedEntry({ 1: "failure", length: 1 }, ($) => $), + undefined, ); }); it("[[Call]] only gets the value once", () => { const get1 = spy(() => true); - findIndexedEntry({ + findFirstIndexedEntry({ get 1() { return get1(); }, @@ -64,96 +1052,372 @@ describe("findIndexedEntry", () => { }); it("[[Call]] passes the value, index, and this value to the callback", () => { - const arr = ["failure", "success", "success"]; + const arr = [, "failure", "success", "success"]; const callback = spy(($) => $ === "success"); const thisArg = {}; - findIndexedEntry(arr, callback, thisArg); + findFirstIndexedEntry(arr, callback, thisArg); assertSpyCalls(callback, 2); assertSpyCall(callback, 0, { - args: ["failure", 0, arr], + args: ["failure", 1, arr], self: thisArg, }); assertSpyCall(callback, 1, { - args: ["success", 1, arr], + args: ["success", 2, arr], self: thisArg, }); }); + + it("[[Construct]] throws an error", () => { + assertThrows(() => new findFirstIndexedEntry([], () => {})); + }); + + describe(".length", () => { + it("[[Get]] returns the correct length", () => { + assertStrictEquals(findFirstIndexedEntry.length, 2); + }); + }); + + describe(".name", () => { + it("[[Get]] returns the correct name", () => { + assertStrictEquals( + findFirstIndexedEntry.name, + "findFirstIndexedEntry", + ); + }); + }); }); -describe("isArrayIndexString", () => { - it("[[Call]] returns false for nonstrings", () => { - assertStrictEquals(isArrayIndexString(1), false); +describe("findFirstItem", () => { + it("[[Call]] returns undefined if no matching item exists", () => { + assertStrictEquals(findFirstItem([], () => true), undefined); + assertStrictEquals(findFirstItem([1], () => false), undefined); }); - it("[[Call]] returns false for noncanonical strings", () => { - assertStrictEquals(isArrayIndexString(""), false); - assertStrictEquals(isArrayIndexString("01"), false); - assertStrictEquals(isArrayIndexString("9007199254740993"), false); + it("[[Call]] returns the first match", () => { + assertStrictEquals( + findFirstItem([, true, false], ($) => $ ?? true), + true, + ); + assertStrictEquals( + findFirstItem( + ["failure", "success", "success"], + ($) => $ == "success", + ), + "success", + ); }); - it("[[Call]] returns false for nonfinite numbers", () => { - assertStrictEquals(isArrayIndexString("NaN"), false); - assertStrictEquals(isArrayIndexString("Infinity"), false); - assertStrictEquals(isArrayIndexString("-Infinity"), false); + it("[[Call]] works on arraylike objects", () => { + assertStrictEquals( + findFirstItem({ 1: "success", length: 2 }, ($) => $), + "success", + ); + assertStrictEquals( + findFirstItem({ 1: "failure", length: 1 }, ($) => $), + undefined, + ); + }); + + it("[[Call]] only gets the value once", () => { + const get1 = spy(() => true); + findFirstItem({ + get 1() { + return get1(); + }, + length: 2, + }, ($) => $); + assertSpyCalls(get1, 1); + }); + + it("[[Call]] passes the value, index, and this value to the callback", () => { + const arr = [, "failure", "success", "success"]; + const callback = spy(($) => $ === "success"); + const thisArg = {}; + findFirstItem(arr, callback, thisArg); + assertSpyCalls(callback, 2); + assertSpyCall(callback, 0, { + args: ["failure", 1, arr], + self: thisArg, + }); + assertSpyCall(callback, 1, { + args: ["success", 2, arr], + self: thisArg, + }); }); - it("[[Call]] returns false for negative numbers", () => { - assertStrictEquals(isArrayIndexString("-0"), false); - assertStrictEquals(isArrayIndexString("-1"), false); + it("[[Construct]] throws an error", () => { + assertThrows(() => new findFirstItem([], () => {})); }); - it("[[Call]] returns false for nonintegers", () => { - assertStrictEquals(isArrayIndexString("0.25"), false); - assertStrictEquals(isArrayIndexString("1.1"), false); + describe(".length", () => { + it("[[Get]] returns the correct length", () => { + assertStrictEquals(findFirstItem.length, 2); + }); }); - it("[[Call]] returns false for numbers greater than or equal to -1 >>> 0", () => { - assertStrictEquals(isArrayIndexString(String(-1 >>> 0)), false); + describe(".name", () => { + it("[[Get]] returns the correct name", () => { + assertStrictEquals(findFirstItem.name, "findFirstItem"); + }); + }); +}); + +describe("findLastIndex", () => { + it("[[Call]] returns undefined if no matching entry exists", () => { + assertStrictEquals(findLastIndex([], () => true), undefined); + assertStrictEquals(findLastIndex([1], () => false), undefined); + }); + + it("[[Call]] returns an index for the first match", () => { assertStrictEquals( - isArrayIndexString(String((-1 >>> 0) + 1)), - false, + findLastIndex([true, false, ,], ($) => $ ?? true), + 0, + ); + assertStrictEquals( + findLastIndex( + ["success", "success", "failure"], + ($) => $ == "success", + ), + 1, ); }); - it("[[Call]] returns true for array lengths less than -1 >>> 0", () => { - assertStrictEquals(isArrayIndexString("0"), true); + it("[[Call]] works on arraylike objects", () => { assertStrictEquals( - isArrayIndexString(String((-1 >>> 0) - 1)), - true, + findLastIndex({ 1: "success", length: 2 }, ($) => $), + 1, + ); + assertStrictEquals( + findLastIndex({ 1: "failure", length: 1 }, ($) => $), + undefined, ); }); -}); -describe("isArraylikeObject", () => { - it("[[Call]] returns false for primitives", () => { - assertStrictEquals(isArraylikeObject("failure"), false); + it("[[Call]] only gets the value once", () => { + const get1 = spy(() => true); + findLastIndex({ + get 1() { + return get1(); + }, + length: 2, + }, ($) => $); + assertSpyCalls(get1, 1); }); - it("[[Call]] returns false if length throws", () => { + it("[[Call]] passes the value, index, and this value to the callback", () => { + const arr = ["success", "success", "failure", ,]; + const callback = spy(($) => $ === "success"); + const thisArg = {}; + findLastIndex(arr, callback, thisArg); + assertSpyCalls(callback, 2); + assertSpyCall(callback, 0, { + args: ["failure", 2, arr], + self: thisArg, + }); + assertSpyCall(callback, 1, { + args: ["success", 1, arr], + self: thisArg, + }); + }); + + it("[[Construct]] throws an error", () => { + assertThrows(() => new findLastIndex([], () => {})); + }); + + describe(".length", () => { + it("[[Get]] returns the correct length", () => { + assertStrictEquals(findLastIndex.length, 2); + }); + }); + + describe(".name", () => { + it("[[Get]] returns the correct name", () => { + assertStrictEquals(findLastIndex.name, "findLastIndex"); + }); + }); +}); + +describe("findLastIndexedEntry", () => { + it("[[Call]] returns undefined if no matching entry exists", () => { assertStrictEquals( - isArraylikeObject({ - get length() { - throw void {}; - }, - }), - false, + findLastIndexedEntry([], () => true), + undefined, + ); + assertStrictEquals( + findLastIndexedEntry([1], () => false), + undefined, + ); + }); + + it("[[Call]] returns an index for the first match", () => { + assertEquals( + findLastIndexedEntry([true, false, ,], ($) => $ ?? true), + [0, true], + ); + assertEquals( + findLastIndexedEntry( + ["success", "success", "failure"], + ($) => $ == "success", + ), + [1, "success"], ); }); - it("[[Call]] returns false if length is not a number and cannot be converted to one", () => { - assertStrictEquals(isArraylikeObject({ length: 1n }), false); + it("[[Call]] works on arraylike objects", () => { + assertEquals( + findLastIndexedEntry({ 1: "success", length: 2 }, ($) => $), + [1, "success"], + ); + assertEquals( + findLastIndexedEntry({ 1: "failure", length: 1 }, ($) => $), + undefined, + ); + }); + + it("[[Call]] only gets the value once", () => { + const get1 = spy(() => true); + findLastIndexedEntry({ + get 1() { + return get1(); + }, + length: 2, + }, ($) => $); + assertSpyCalls(get1, 1); + }); + + it("[[Call]] passes the value, index, and this value to the callback", () => { + const arr = ["success", "success", "failure", ,]; + const callback = spy(($) => $ === "success"); + const thisArg = {}; + findLastIndexedEntry(arr, callback, thisArg); + assertSpyCalls(callback, 2); + assertSpyCall(callback, 0, { + args: ["failure", 2, arr], + self: thisArg, + }); + assertSpyCall(callback, 1, { + args: ["success", 1, arr], + self: thisArg, + }); + }); + + it("[[Construct]] throws an error", () => { + assertThrows(() => new findLastIndexedEntry([], () => {})); }); - it("[[Call]] returns true if length is convertable to a number", () => { - assertStrictEquals(isArraylikeObject({ length: -0 }), true); - assertStrictEquals(isArraylikeObject({ length: 1 }), true); - assertStrictEquals(isArraylikeObject({ length: -1.25 }), true); + describe(".length", () => { + it("[[Get]] returns the correct length", () => { + assertStrictEquals(findLastIndexedEntry.length, 2); + }); + }); + + describe(".name", () => { + it("[[Get]] returns the correct name", () => { + assertStrictEquals( + findLastIndexedEntry.name, + "findLastIndexedEntry", + ); + }); + }); +}); + +describe("findLastItem", () => { + it("[[Call]] returns undefined if no matching entry exists", () => { + assertStrictEquals(findLastItem([], () => true), undefined); + assertStrictEquals(findLastItem([1], () => false), undefined); + }); + + it("[[Call]] returns an index for the first match", () => { assertStrictEquals( - isArraylikeObject({ length: 9007199254740992 }), + findLastItem([true, false, ,], ($) => $ ?? true), true, ); - assertStrictEquals(isArraylikeObject({ length: Infinity }), true); - assertStrictEquals(isArraylikeObject({ length: "success" }), true); + assertStrictEquals( + findLastItem( + ["success", "success", "failure"], + ($) => $ == "success", + ), + "success", + ); + }); + + it("[[Call]] works on arraylike objects", () => { + assertStrictEquals( + findLastItem({ 1: "success", length: 2 }, ($) => $), + "success", + ); + assertStrictEquals( + findLastItem({ 1: "failure", length: 1 }, ($) => $), + undefined, + ); + }); + + it("[[Call]] only gets the value once", () => { + const get1 = spy(() => true); + findLastItem({ + get 1() { + return get1(); + }, + length: 2, + }, ($) => $); + assertSpyCalls(get1, 1); + }); + + it("[[Call]] passes the value, index, and this value to the callback", () => { + const arr = ["success", "success", "failure", ,]; + const callback = spy(($) => $ === "success"); + const thisArg = {}; + findLastItem(arr, callback, thisArg); + assertSpyCalls(callback, 2); + assertSpyCall(callback, 0, { + args: ["failure", 2, arr], + self: thisArg, + }); + assertSpyCall(callback, 1, { + args: ["success", 1, arr], + self: thisArg, + }); + }); + + it("[[Construct]] throws an error", () => { + assertThrows(() => new findLastItem([], () => {})); + }); + + describe(".length", () => { + it("[[Get]] returns the correct length", () => { + assertStrictEquals(findLastItem.length, 2); + }); + }); + + describe(".name", () => { + it("[[Get]] returns the correct name", () => { + assertStrictEquals(findLastItem.name, "findLastItem"); + }); + }); +}); + +describe("flatmap", () => { + it("[[Call]] flatmaps", () => { + assertEquals( + flatmap([, "a", ["b"], [["c"]]], ($) => $), + ["a", "b", ["c"]], + ); + }); + + it("[[Construct]] throws an error", () => { + assertThrows(() => new flatmap([], () => {})); + }); + + describe(".length", () => { + it("[[Get]] returns the correct length", () => { + assertStrictEquals(flatmap.length, 2); + }); + }); + + describe(".name", () => { + it("[[Get]] returns the correct name", () => { + assertStrictEquals(flatmap.name, "flatmap"); + }); }); }); @@ -224,39 +1488,54 @@ describe("isCollection", () => { }); }); -describe("isConcatSpreadable", () => { - it("[[Call]] returns false for primitives", () => { - assertStrictEquals(isConcatSpreadable("failure"), false); - }); - - it("[[Call]] returns false if [Symbol.isConcatSpreadable] is null or false", () => { +describe("isDenseProxy", () => { + it("[[Call]] returns true for dense proxies", () => { + const underlying = []; + const proxy = denseProxy(underlying); assertStrictEquals( - isConcatSpreadable( - Object.assign([], { [Symbol.isConcatSpreadable]: null }), - ), - false, + isDenseProxy(proxy), + true, ); + }); + + it("[[Call]] returns false for others", () => { assertStrictEquals( - isConcatSpreadable( - Object.assign([], { [Symbol.isConcatSpreadable]: false }), - ), + isDenseProxy([]), false, ); }); - it("[[Call]] returns true if [Symbol.isConcatSpreadable] is undefined and the object is an array", () => { - assertStrictEquals( - isConcatSpreadable( - Object.assign([], { [Symbol.isConcatSpreadable]: undefined }), - ), - true, + it("[[Construct]] throws an error", () => { + assertThrows(() => new isDenseProxy([])); + }); + + describe(".length", () => { + it("[[Get]] returns the correct length", () => { + assertStrictEquals(isDenseProxy.length, 1); + }); + }); + + describe(".name", () => { + it("[[Get]] returns the correct name", () => { + assertStrictEquals(isDenseProxy.name, "isDenseProxy"); + }); + }); +}); + +describe("toArray", () => { + it("[[Call]] returns an array of the values in a provided arraylike", () => { + assertEquals( + toArray({ 1: "success", length: 3 }), + [, "success", ,], ); }); +}); - it("[[Call]] returns true if [Symbol.isConcatSpreadable] is true", () => { - assertStrictEquals( - isConcatSpreadable({ [Symbol.isConcatSpreadable]: true }), - true, +describe("toDenseArray", () => { + it("[[Call]] returns a dense array of the values in a provided arraylike", () => { + assertEquals( + toDenseArray({ 1: "success", length: 3 }), + [undefined, "success", undefined], ); }); });