X-Git-Url: https://git.ladys.computer/Pisces/blobdiff_plain/29307967cb084aceaa9265fe22af4348c9e4376a..refs/heads/current:/function.test.js diff --git a/function.test.js b/function.test.js index 4cfa85f..bf0e907 100644 --- a/function.test.js +++ b/function.test.js @@ -1,7 +1,7 @@ // ♓🌟 Piscēs ∷ function.test.js // ==================================================================== // -// Copyright © 2022 Lady [@ Lady’s Computer]. +// Copyright © 2022–2023 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 @@ -10,18 +10,28 @@ import { assert, assertEquals, + assertNotStrictEquals, + assertSpyCall, + assertSpyCalls, assertStrictEquals, + assertThrows, describe, it, + spy, } from "./dev-deps.js"; import { bind, call, + completesNormally, construct, + createArrowFunction, + createCallableFunction, + createIllegalConstructor, + createProxyConstructor, identity, isCallable, isConstructor, - makeCallable, + maybe, ordinaryHasInstance, } from "./function.js"; @@ -68,6 +78,22 @@ describe("bind", () => { ["etaoin", undefined, "shrdlu", undefined, "cmfwyp"], ); }); + + it("[[Construct]] throws an error", () => { + assertThrows(() => new bind(function () {})); + }); + + describe(".length", () => { + it("[[Get]] returns the correct length", () => { + assertStrictEquals(bind.length, 3); + }); + }); + + describe(".name", () => { + it("[[Get]] returns the correct name", () => { + assertStrictEquals(bind.name, "bind"); + }); + }); }); describe("call", () => { @@ -114,6 +140,61 @@ describe("call", () => { ["etaoin", undefined, "shrdlu", undefined, "cmfwyp"], ); }); + + it("[[Construct]] throws an error", () => { + assertThrows(() => new call(function () {}, null, [])); + }); + + describe(".length", () => { + it("[[Get]] returns the correct length", () => { + assertStrictEquals(call.length, 3); + }); + }); + + describe(".name", () => { + it("[[Get]] returns the correct name", () => { + assertStrictEquals(call.name, "call"); + }); + }); +}); + +describe("completesNormally", () => { + it("[[Call]] returns true for functions which complete normally", () => { + assertStrictEquals(completesNormally(() => {}), true); + }); + + it("[[Call]] returns false for functions which throw", () => { + assertStrictEquals( + completesNormally(() => { + throw null; + }), + false, + ); + }); + + it("[[Call]] throws when the argument is not callable", () => { + assertThrows(() => completesNormally(null)); + }); + + it("[[Call]] throws when the argument is not provided", () => { + assertThrows(() => completesNormally()); + }); + + it("[[Construct]] throws an error", () => { + assertThrows(() => new completesNormally(function () {})); + }); + + describe(".length", () => { + it("[[Get]] returns the correct length", () => { + assertStrictEquals(completesNormally.length, 1); + }); + }); + + describe(".name", () => { + it("[[Get]] returns the correct name", () => { + assertStrictEquals(completesNormally.name, "completesNormally"); + }); + }); }); describe("construct", () => { @@ -172,6 +253,660 @@ describe("construct", () => { ["etaoin", undefined, "shrdlu", undefined, "cmfwyp"], ); }); + + it("[[Construct]] throws an error", () => { + assertThrows(() => new construct(function () {}, [])); + }); + + describe(".length", () => { + it("[[Get]] returns the correct length", () => { + assertStrictEquals(construct.length, 2); + }); + }); + + describe(".name", () => { + it("[[Get]] returns the correct name", () => { + assertStrictEquals(construct.name, "construct"); + }); + }); +}); + +describe("createArrowFunction", () => { + it("[[Call]] sets this to undefined", () => { + assertStrictEquals( + createArrowFunction( + function () { + return this; + }, + ).call("fail"), + undefined, + ); + }); + + it("[[Call]] transfers the provided arguments", () => { + assertEquals( + createArrowFunction( + function (...args) { + return [this, ...args]; + }, + ).call("failure", "etaoin", "shrdlu", "cmfwyp"), + [undefined, "etaoin", "shrdlu", "cmfwyp"], + ); + }); + + it("[[Call]] correctly sets the length", () => { + assertStrictEquals( + createArrowFunction( + function (_a, _b, _c) {}, + ).length, + 3, + ); + }); + + it("[[Call]] correctly sets the name", () => { + assertStrictEquals( + createArrowFunction( + function etaoin() {}, + ).name, + "etaoin", + ); + }); + + it("[[Call]] allows the length to be overridden", () => { + assertStrictEquals( + createArrowFunction( + function etaoin() {}, + { length: 100 }, + ).length, + 100, + ); + }); + + it("[[Call]] allows the name to be overridden", () => { + assertStrictEquals( + createArrowFunction( + function etaoin() {}, + { name: "shrdlu" }, + ).name, + "shrdlu", + ); + }); + + it("[[Call]] returns an arrow function", () => { + assertThrows(() => new (createArrowFunction(function () {}))()); + }); + + it("[[Call]] returns a function with no prototype property", () => { + assert( + !("prototype" in + createArrowFunction(function () {}, { prototype: {} })), + ); + }); + + it("[[Construct]] throws an error", () => { + assertThrows(() => new createArrowFunction(function () {})); + }); + + describe(".length", () => { + it("[[Get]] returns the correct length", () => { + assertStrictEquals(createArrowFunction.length, 1); + }); + }); + + describe(".name", () => { + it("[[Get]] returns the correct name", () => { + assertStrictEquals( + createArrowFunction.name, + "createArrowFunction", + ); + }); + }); +}); + +describe("createCallableFunction", () => { + it("[[Call]] transfers the first argument to this", () => { + assertStrictEquals( + createCallableFunction( + function () { + return this; + }, + ).call("fail", "pass"), + "pass", + ); + }); + + it("[[Call]] transfers the remaining arguments", () => { + assertEquals( + createCallableFunction( + function (...args) { + return [this, ...args]; + }, + ).call("failure", "etaoin", "shrdlu", "cmfwyp"), + ["etaoin", "shrdlu", "cmfwyp"], + ); + }); + + it("[[Call]] correctly sets the length", () => { + assertStrictEquals( + createCallableFunction( + function (_a, _b, _c) {}, + ).length, + 4, + ); + }); + + it("[[Call]] correctly sets the name", () => { + assertStrictEquals( + createCallableFunction( + function etaoin() {}, + ).name, + "etaoin", + ); + }); + + it("[[Call]] allows the length to be overridden", () => { + assertStrictEquals( + createCallableFunction( + function etaoin() {}, + { length: 100 }, + ).length, + 100, + ); + }); + + it("[[Call]] allows the name to be overridden", () => { + assertStrictEquals( + createCallableFunction( + function etaoin() {}, + { name: "shrdlu" }, + ).name, + "shrdlu", + ); + }); + + it("[[Call]] returns an arrow function", () => { + assertThrows(() => new (createCallableFunction(function () {}))()); + }); + + it("[[Call]] returns a function with no prototype property", () => { + assert( + !("prototype" in + createCallableFunction(function () {}, { prototype: {} })), + ); + }); + + it("[[Construct]] throws an error", () => { + assertThrows(() => new createCallableFunction(function () {})); + }); + + describe(".length", () => { + it("[[Get]] returns the correct length", () => { + assertStrictEquals(createCallableFunction.length, 1); + }); + }); + + describe(".name", () => { + it("[[Get]] returns the correct name", () => { + assertStrictEquals( + createCallableFunction.name, + "createCallableFunction", + ); + }); + }); +}); + +describe("createIllegalConstructor", () => { + it("[[Call]] returns a constructor", () => { + assert(isConstructor(createIllegalConstructor())); + }); + + it("[[Call]] works as expected when provided with no arguments", () => { + const constructor = createIllegalConstructor(); + assertStrictEquals( + Object.getPrototypeOf(constructor), + Function.prototype, + ); + assertStrictEquals( + Object.getPrototypeOf(constructor.prototype), + Object.prototype, + ); + assertEquals( + constructor.prototype, + Object.create(Object.prototype, { + constructor: { + configurable: true, + enumerable: false, + value: constructor, + writable: true, + }, + }), + ); + assert( + !Object.getOwnPropertyDescriptor(constructor, "prototype") + .writable, + ); + assertStrictEquals(constructor.name, ""); + }); + + it("[[Call]] returns a correctly‐formed constructor when provided one argument", () => { + const constructorPrototype = Object.create(null, { + name: { value: "etaoin" }, + length: { value: "3" }, + prototype: { value: {} }, + }); + const constructor = createIllegalConstructor( + Object.create(constructorPrototype), + ); + assert(isConstructor(constructor)); + assertStrictEquals( + Object.getPrototypeOf(constructor), + constructorPrototype, + ); + assert(Object.hasOwn(constructor, "prototype")); + assertStrictEquals( + constructor.prototype, + constructorPrototype.prototype, + ); + assert( + !Object.getOwnPropertyDescriptor(constructor, "prototype") + .writable, + ); + assertStrictEquals(constructor.name, "etaoin"); + assertStrictEquals(constructor.length, 3); + }); + + it("[[Call]] allows the length to be overridden", () => { + assertStrictEquals( + createIllegalConstructor( + function etaoin() {}, + { length: 100 }, + ).length, + 100, + ); + }); + + it("[[Call]] allows the name to be overridden", () => { + assertStrictEquals( + createIllegalConstructor( + function etaoin() {}, + { name: "shrdlu" }, + ).name, + "shrdlu", + ); + }); + + it("[[Call]] allows the prototype to be overridden", () => { + const obj = {}; + assertStrictEquals( + createIllegalConstructor( + function etaoin() {}, + { prototype: obj }, + ).prototype, + obj, + ); + }); + + it("[[Construct]] throws an error", () => { + assertThrows(() => new createIllegalConstructor(function () {})); + }); + + describe(".length", () => { + it("[[Get]] returns the correct length", () => { + assertStrictEquals(createIllegalConstructor.length, 1); + }); + }); + + describe(".name", () => { + it("[[Get]] returns the correct name", () => { + assertStrictEquals( + createIllegalConstructor.name, + "createIllegalConstructor", + ); + }); + }); + + describe("~", () => { + it("[[Call]] throws an error", () => { + assertThrows(() => { + createIllegalConstructor(function () {})(); + }); + }); + + it("[[Construct]] throws an error", () => { + assertThrows(() => { + createIllegalConstructor(function () {})(); + }); + }); + }); +}); + +describe("createProxyConstructor", () => { + it("[[Call]] returns a constructor", () => { + assert(isConstructor(createProxyConstructor({}))); + }); + + it("[[Call]] throws with no arguments", () => { + assertThrows(() => createProxyConstructor()); + }); + + it("[[Call]] throws if the second argument is not a constructor or undefined", () => { + assertThrows(() => createProxyConstructor({}, () => {})); + }); + + it("[[Call]] throws if the third argument is not a constructor or undefined", () => { + assertThrows(() => + createProxyConstructor({}, undefined, () => {}) + ); + }); + + it("[[Call]] creates a proper proxy constructor", () => { + const constructorPrototype = function etaoin(_) {}; + const constructor = class Constructor + extends constructorPrototype { + constructor(_1, _2, _3) {} + }; + const proxyConstructor = createProxyConstructor( + {}, + constructor, + ); + assert(isConstructor(proxyConstructor)); + assertStrictEquals( + Object.getPrototypeOf(proxyConstructor), + Proxy, + ); + assertStrictEquals(proxyConstructor.prototype, undefined); + assertStrictEquals(proxyConstructor.name, "ConstructorProxy"); + assertStrictEquals(proxyConstructor.length, 3); + }); + + it("[[Call]] allows the length to be overridden", () => { + assertStrictEquals( + createProxyConstructor({}, undefined, undefined, { + length: 100, + }).length, + 100, + ); + }); + + it("[[Call]] allows the name to be overridden", () => { + assertStrictEquals( + createProxyConstructor({}, function etaoin() {}, undefined, { + name: "shrdlu", + }).name, + "shrdlu", + ); + }); + + it("[[Call]] does not allow the prototype to be overridden", () => { + assertStrictEquals( + createProxyConstructor({}, undefined, undefined, { + prototype: {}, + }).prototype, + undefined, + ); + }); + + it("[[Construct]] throws an error", () => { + assertThrows(() => new createProxyConstructor({})); + }); + + describe(".length", () => { + it("[[Get]] returns the correct length", () => { + assertStrictEquals(createProxyConstructor.length, 2); + }); + }); + + describe(".name", () => { + it("[[Get]] returns the correct name", () => { + assertStrictEquals( + createProxyConstructor.name, + "createProxyConstructor", + ); + }); + }); + + describe("~", () => { + it("[[Call]] throws an error", () => { + assertThrows(() => { + createProxyConstructor({})(); + }); + }); + + it("[[Construct]] produces a proxy", () => { + const obj = {}; + const proxyConstructor = createProxyConstructor({ + get(O, P, Receiver) { + if (P === "etaoin") { + return Reflect.get(O, P, Receiver) ?? "success"; + } else { + return Reflect.get(O, P, Receiver); + } + }, + }, function () { + return obj; + }); + const proxy = new proxyConstructor(); + assertStrictEquals(proxy.etaoin, "success"); + obj.etaoin = "shrdlu"; + assertStrictEquals(proxy.etaoin, "shrdlu"); + }); + + it("[[Construct]] receives the expected new.target", () => { + const constructor = function Constructor() { + return { name: new.target.name }; + }; + assertStrictEquals( + new (createProxyConstructor({}, constructor))().name, + "Constructor", + ); + assertStrictEquals( + new (createProxyConstructor( + {}, + constructor, + function NewTarget() {}, + ))().name, + "NewTarget", + ); + }); + }); + + describe("~is[[.name]]", () => { + it("[[GetOwnProperty]] defines the appropriate method", () => { + assertNotStrictEquals( + Object.getOwnPropertyDescriptor( + createProxyConstructor({}), + "isProxy", + ), + undefined, + ); + assertNotStrictEquals( + Object.getOwnPropertyDescriptor( + createProxyConstructor({}, function Base() {}), + "isBaseProxy", + ), + undefined, + ); + assertNotStrictEquals( + Object.getOwnPropertyDescriptor( + createProxyConstructor({}, function Bad() {}, undefined, { + name: "⸺", + }), + "is⸺", + ), + undefined, + ); + }); + + it("[[GetOwnProperty]] has the correct descriptor", () => { + const proxyConstructor = createProxyConstructor({}); + assertEquals( + Object.getOwnPropertyDescriptor( + proxyConstructor, + "isProxy", + ), + { + configurable: true, + enumerable: false, + value: proxyConstructor.isProxy, + writable: true, + }, + ); + }); + + it("[[Call]] returns true for created proxies", () => { + const proxyConstructor = createProxyConstructor({}); + const proxy = new proxyConstructor(); + assertStrictEquals( + proxyConstructor.isProxy(proxy), + true, + ); + }); + + it("[[Call]] returns false for nonproxies", () => { + const constructor = function Base() {}; + const proxyConstructor = createProxyConstructor({}, constructor); + assertStrictEquals( + proxyConstructor.isBaseProxy(new constructor()), + false, + ); + }); + + it("[[Construct]] throws an error", () => { + const proxyConstructor = createProxyConstructor({}); + assertThrows(() => new proxyConstructor.isProxy({})); + }); + + describe(".length", () => { + it("[[Get]] returns the correct length", () => { + const proxyConstructor = createProxyConstructor({}); + assertStrictEquals(proxyConstructor.isProxy.length, 1); + }); + }); + + describe(".name", () => { + it("[[Get]] returns the correct name", () => { + const proxyConstructor = createProxyConstructor({}); + assertStrictEquals(proxyConstructor.isProxy.name, "isProxy"); + const otherProxyConstructor = createProxyConstructor( + {}, + function Base() {}, + ); + assertStrictEquals( + otherProxyConstructor.isBaseProxy.name, + "isBaseProxy", + ); + }); + }); + }); + + describe("~length", () => { + it("[[GetOwnProperty]] has the correct descriptor", () => { + assertEquals( + Object.getOwnPropertyDescriptor( + createProxyConstructor({}), + "length", + ), + { + configurable: true, + enumerable: false, + value: 1, + writable: false, + }, + ); + }); + }); + + describe("~name", () => { + it("[[GetOwnProperty]] has the correct descriptor", () => { + assertEquals( + Object.getOwnPropertyDescriptor( + createProxyConstructor({}), + "name", + ), + { + configurable: true, + enumerable: false, + value: "Proxy", + writable: false, + }, + ); + }); + }); + + describe("~prototype", () => { + it("[[GetOwnProperty]] has the correct descriptor", () => { + assertEquals( + Object.getOwnPropertyDescriptor( + createProxyConstructor({}), + "prototype", + ), + { + configurable: false, + enumerable: false, + value: undefined, + writable: false, + }, + ); + }); + }); + + describe("~revocable", () => { + it("[[Call]] produces a revocable proxy", () => { + const obj = {}; + const proxyConstructor = createProxyConstructor({ + get(O, P, Receiver) { + if (P === "etaoin") { + return Reflect.get(O, P, Receiver) ?? "success"; + } else { + return Reflect.get(O, P, Receiver); + } + }, + }, function () { + return obj; + }); + const { proxy, revoke } = proxyConstructor.revocable(); + assertStrictEquals(proxy.etaoin, "success"); + obj.etaoin = "shrdlu"; + assertStrictEquals(proxy.etaoin, "shrdlu"); + revoke(); + assertThrows(() => proxy.etaoin); + }); + + it("[[Call]] receives the expected new.target", () => { + const constructor = function Constructor() { + return { name: new.target.name }; + }; + assertStrictEquals( + createProxyConstructor({}, constructor).revocable().proxy.name, + "Constructor", + ); + assertStrictEquals( + createProxyConstructor( + {}, + constructor, + function NewTarget() {}, + ).revocable().proxy.name, + "NewTarget", + ); + }); + + it("[[Construct]] throws an error", () => { + assertThrows(() => new (createProxyConstructor({})).revocable()); + }); + + it("[[GetOwnProperty]] has the correct descriptor", () => { + const proxyConstructor = createProxyConstructor({}); + assertEquals( + Object.getOwnPropertyDescriptor(proxyConstructor, "revocable"), + { + configurable: true, + enumerable: false, + value: proxyConstructor.revocable, + writable: true, + }, + ); + }); + }); }); describe("identity", () => { @@ -189,89 +924,197 @@ describe("identity", () => { const value = {}; assertStrictEquals(new class extends identity {}(value), value); }); + + describe(".length", () => { + it("[[Get]] returns the correct length", () => { + assertStrictEquals(identity.length, 1); + }); + }); + + describe(".name", () => { + it("[[Get]] returns the correct name", () => { + assertStrictEquals(identity.name, "identity"); + }); + }); }); describe("isCallable", () => { it("[[Call]] returns true for ordinary functions", () => { - assert(isCallable(function () {})); + assertStrictEquals(isCallable(function () {}), true); }); it("[[Call]] returns true for arrow functions", () => { - assert(isCallable(() => {})); + assertStrictEquals(isCallable(() => {}), true); }); it("[[Call]] returns true for generator functions", () => { - assert(isCallable(function* () {})); + assertStrictEquals(isCallable(function* () {}), true); }); it("[[Call]] returns true for classes", () => { - assert(isCallable(class {})); + assertStrictEquals(isCallable(class {}), true); }); it("[[Call]] returns true for builtin functions", () => { - assert(isCallable(Math.ceil)); + assertStrictEquals(isCallable(Math.ceil), true); + }); + + it("[[Call]] returns true for getters", () => { + assertStrictEquals( + isCallable( + Object.getOwnPropertyDescriptor({ + get foo() { + return undefined; + }, + }, "foo").get, + ), + true, + ); + }); + + it("[[Call]] returns true for setters", () => { + assertStrictEquals( + isCallable( + Object.getOwnPropertyDescriptor({ + set foo($) { + /* do nothing */ + }, + }, "foo").set, + ), + true, + ); }); it("[[Call]] returns false for null", () => { - assert(!isCallable(null)); + assertStrictEquals(isCallable(null), false); }); it("[[Call]] returns false for objects", () => { - assert(!isCallable({})); + assertStrictEquals(isCallable({}), false); + }); + + it("[[Construct]] throws an error", () => { + assertThrows(() => new isCallable(function () {})); + }); + + describe(".length", () => { + it("[[Get]] returns the correct length", () => { + assertStrictEquals(isCallable.length, 1); + }); + }); + + describe(".name", () => { + it("[[Get]] returns the correct name", () => { + assertStrictEquals(isCallable.name, "isCallable"); + }); }); }); describe("isConstructor", () => { it("[[Call]] returns true for ordinary functions", () => { - assert(isConstructor(function () {})); + assertStrictEquals(isConstructor(function () {}), true); }); it("[[Call]] returns false for arrow functions", () => { - assert(!isConstructor(() => {})); + assertStrictEquals(isConstructor(() => {}), false); }); it("[[Call]] returns false for generator functions", () => { - assert(!isConstructor(function* () {})); + assertStrictEquals(isConstructor(function* () {}), false); }); it("[[Call]] returns true for classes", () => { - assert(isConstructor(class {})); + assertStrictEquals(isConstructor(class {}), true); }); it("[[Call]] returns false for builtin functions", () => { - assert(!isConstructor(Math.ceil)); + assertStrictEquals(isConstructor(Math.ceil), false); + }); + + it("[[Call]] returns false for getters", () => { + assertStrictEquals( + isConstructor( + Object.getOwnPropertyDescriptor({ + get foo() { + return undefined; + }, + }, "foo").get, + ), + false, + ); + }); + + it("[[Call]] returns false for setters", () => { + assertStrictEquals( + isConstructor( + Object.getOwnPropertyDescriptor({ + set foo($) { + /* do nothing */ + }, + }, "foo").set, + ), + false, + ); }); it("[[Call]] returns false for null", () => { - assert(!isConstructor(null)); + assertStrictEquals(isConstructor(null), false); }); it("[[Call]] returns false for objects", () => { - assert(!isConstructor({})); + assertStrictEquals(isConstructor({}), false); + }); + + it("[[Construct]] throws an error", () => { + assertThrows(() => new isConstructor(function () {})); + }); + + describe(".length", () => { + it("[[Get]] returns the correct length", () => { + assertStrictEquals(isConstructor.length, 1); + }); + }); + + describe(".name", () => { + it("[[Get]] returns the correct name", () => { + assertStrictEquals(isConstructor.name, "isConstructor"); + }); }); }); -describe("makeCallable", () => { - it("[[Call]] transfers the first argument to this", () => { - assertStrictEquals( - makeCallable( - function () { - return this; - }, - ).call("fail", "pass"), - "pass", - ); +describe("maybe", () => { + it("[[Call]] calls if not nullish", () => { + const wrapper = spy(() => "success"); + assertStrictEquals(maybe(0, wrapper), "success"); + assertSpyCalls(wrapper, 1); + assertSpyCall(wrapper, 0, { + args: [0], + self: undefined, + returned: "success", + }); }); - it("[[Call]] transfers the remaining arguments", () => { - assertEquals( - makeCallable( - function (...args) { - return [this, ...args]; - }, - ).call("failure", "etaoin", "shrdlu", "cmfwyp"), - ["etaoin", "shrdlu", "cmfwyp"], - ); + it("[[Call]] does not call if nullish", () => { + const wrapper = spy(() => "success"); + assertStrictEquals(maybe(null, wrapper), null); + assertStrictEquals(maybe(undefined, wrapper), undefined); + assertSpyCalls(wrapper, 0); + }); + + it("[[Construct]] throws an error", () => { + assertThrows(() => new maybe(true, ($) => $)); + }); + + describe(".length", () => { + it("[[Get]] returns the correct length", () => { + assertStrictEquals(maybe.length, 2); + }); + }); + + describe(".name", () => { + it("[[Get]] returns the correct name", () => { + assertStrictEquals(maybe.name, "maybe"); + }); }); }); @@ -282,11 +1125,31 @@ describe("ordinaryHasInstance", () => { return false; } }; - assert( + assertStrictEquals( ordinaryHasInstance( constructor, new class extends constructor {}(), ), + true, ); }); + + it("[[Construct]] throws an error", () => { + assertThrows(() => new ordinaryHasInstance(function () {}, {})); + }); + + describe(".length", () => { + it("[[Get]] returns the correct length", () => { + assertStrictEquals(ordinaryHasInstance.length, 2); + }); + }); + + describe(".name", () => { + it("[[Get]] returns the correct name", () => { + assertStrictEquals( + ordinaryHasInstance.name, + "ordinaryHasInstance", + ); + }); + }); });