X-Git-Url: https://git.ladys.computer/Pisces/blobdiff_plain/6f1ff895670d04034ef09faea4779923a85097fb..7c3a2eda590637af9463e5a59ab798f802d274a9:/object.test.js?ds=sidebyside diff --git a/object.test.js b/object.test.js new file mode 100644 index 0000000..da890d3 --- /dev/null +++ b/object.test.js @@ -0,0 +1,734 @@ +// ♓🌟 Piscēs ∷ object.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 . + +import { + assert, + assertEquals, + assertSpyCall, + assertSpyCalls, + assertStrictEquals, + assertThrows, + describe, + it, + spy, +} from "./dev-deps.js"; +import { + defineOwnProperties, + deleteOwnProperty, + frozenCopy, + PropertyDescriptor, + setPropertyValue, + toObject, + toPropertyKey, +} from "./object.js"; + +describe("PropertyDescriptor", () => { + it("[[Construct]] creates a new PropertyDescriptor", () => { + assertStrictEquals( + Object.getPrototypeOf(new PropertyDescriptor({})), + PropertyDescriptor.prototype, + ); + }); + + it("[[Construct]] throws for primitives", () => { + assertThrows(() => new PropertyDescriptor("failure")); + }); + + describe("::complete", () => { + it("[[Call]] completes a generic descriptor", () => { + const desc = {}; + PropertyDescriptor.prototype.complete.call(desc); + assertEquals(desc, { + configurable: false, + enumerable: false, + value: undefined, + writable: false, + }); + }); + + it("[[Call]] completes a data descriptor", () => { + const desc = { value: undefined }; + PropertyDescriptor.prototype.complete.call(desc); + assertEquals(desc, { + configurable: false, + enumerable: false, + value: undefined, + writable: false, + }); + }); + + it("[[Call]] completes an accessor descriptor", () => { + const desc = { get: undefined }; + PropertyDescriptor.prototype.complete.call(desc); + assertEquals(desc, { + configurable: false, + enumerable: false, + get: undefined, + set: undefined, + }); + }); + }); + + describe("::isAccessorDescriptor", () => { + it("[[Get]] returns false for a generic descriptor", () => { + assertStrictEquals( + Reflect.get( + PropertyDescriptor.prototype, + "isAccessorDescriptor", + {}, + ), + false, + ); + }); + + it("[[Get]] returns false for a data descriptor", () => { + assertStrictEquals( + Reflect.get( + PropertyDescriptor.prototype, + "isAccessorDescriptor", + { value: undefined }, + ), + false, + ); + }); + + it("[[Get]] returns true for an accessor descriptor", () => { + assertStrictEquals( + Reflect.get( + PropertyDescriptor.prototype, + "isAccessorDescriptor", + { get: undefined }, + ), + true, + ); + }); + }); + + describe("::isDataDescriptor", () => { + it("[[Get]] returns false for a generic descriptor", () => { + assertStrictEquals( + Reflect.get( + PropertyDescriptor.prototype, + "isDataDescriptor", + {}, + ), + false, + ); + }); + + it("[[Get]] returns true for a data descriptor", () => { + assertStrictEquals( + Reflect.get( + PropertyDescriptor.prototype, + "isDataDescriptor", + { value: undefined }, + ), + true, + ); + }); + + it("[[Get]] returns false for an accessor descriptor", () => { + assertStrictEquals( + Reflect.get( + PropertyDescriptor.prototype, + "isDataDescriptor", + { get: undefined }, + ), + false, + ); + }); + }); + + describe("::isFullyPopulated", () => { + it("[[Get]] returns false for a generic descriptor", () => { + assertStrictEquals( + Reflect.get( + PropertyDescriptor.prototype, + "isFullyPopulated", + {}, + ), + false, + ); + }); + + it("[[Get]] returns false for a non‐fully‐populated data descriptor", () => { + assertStrictEquals( + Reflect.get( + PropertyDescriptor.prototype, + "isFullyPopulated", + { value: undefined }, + ), + false, + ); + }); + + it("[[Get]] returns true for a fully‐populated data descriptor", () => { + assertStrictEquals( + Reflect.get(PropertyDescriptor.prototype, "isFullyPopulated", { + configurable: true, + enumerable: true, + value: undefined, + writable: true, + }), + true, + ); + }); + + it("[[Get]] returns false for a non‐fully‐populated accessor descriptor", () => { + assertStrictEquals( + Reflect.get( + PropertyDescriptor.prototype, + "isFullyPopulated", + { get: undefined }, + ), + false, + ); + }); + + it("[[Get]] returns true for a fully‐populated accessor descriptor", () => { + assertStrictEquals( + Reflect.get(PropertyDescriptor.prototype, "isFullyPopulated", { + configurable: true, + enumerable: true, + get: undefined, + set: undefined, + }), + true, + ); + }); + }); + + describe("::isGenericDescriptor", () => { + it("[[Get]] returns true for a generic descriptor", () => { + assertStrictEquals( + Reflect.get( + PropertyDescriptor.prototype, + "isGenericDescriptor", + {}, + ), + true, + ); + }); + + it("[[Get]] returns true for a data descriptor", () => { + assertStrictEquals( + Reflect.get( + PropertyDescriptor.prototype, + "isGenericDescriptor", + { value: undefined }, + ), + false, + ); + }); + + it("[[Get]] returns false for an accessor descriptor", () => { + assertStrictEquals( + Reflect.get( + PropertyDescriptor.prototype, + "isGenericDescriptor", + { get: undefined }, + ), + false, + ); + }); + }); + + describe("~configurable", () => { + it("[[DefineOwnProperty]] coerces to a boolean", () => { + const desc = new PropertyDescriptor({}); + Object.defineProperty(desc, "configurable", {}); + assertStrictEquals(desc.configurable, false); + }); + + it("[[DefineOwnProperty]] throws for accessor properties", () => { + const desc = new PropertyDescriptor({}); + assertThrows(() => + Object.defineProperty(desc, "configurable", { get: undefined }) + ); + }); + + it("[[Set]] coerces to a boolean", () => { + const desc = new PropertyDescriptor({}); + desc.configurable = undefined; + assertStrictEquals(desc.configurable, false); + }); + + it("[[Delete]] works", () => { + const desc = new PropertyDescriptor({ configurable: false }); + delete desc.configurable; + assert(!("configurable" in desc)); + }); + }); + + describe("~enumerable", () => { + it("[[DefineOwnProperty]] coerces to a boolean", () => { + const desc = new PropertyDescriptor({}); + Object.defineProperty(desc, "enumerable", {}); + assertStrictEquals(desc.enumerable, false); + }); + + it("[[DefineOwnProperty]] throws for accessor properties", () => { + const desc = new PropertyDescriptor({}); + assertThrows(() => + Object.defineProperty(desc, "enumerable", { get: undefined }) + ); + }); + + it("[[Set]] coerces to a boolean", () => { + const desc = new PropertyDescriptor({}); + desc.enumerable = undefined; + assertStrictEquals(desc.enumerable, false); + }); + + it("[[Delete]] works", () => { + const desc = new PropertyDescriptor({ enumerable: false }); + delete desc.enumerable; + assert(!("enumerable" in desc)); + }); + }); + + describe("~get", () => { + it("[[DefineOwnProperty]] works", () => { + const desc = new PropertyDescriptor({}); + Object.defineProperty(desc, "get", {}); + assertStrictEquals(desc.get, undefined); + }); + + it("[[DefineOwnProperty]] throws for accessor properties", () => { + const desc = new PropertyDescriptor({}); + assertThrows(() => + Object.defineProperty(desc, "get", { get: undefined }) + ); + }); + + it("[[DefineOwnProperty]] throws if not callable or undefined", () => { + const desc = new PropertyDescriptor({}); + assertThrows( + () => Object.defineProperty(desc, "get", { value: null }), + ); + }); + + it("[[DefineOwnProperty]] throws if a data property is defined", () => { + const desc = new PropertyDescriptor({ value: undefined }); + assertThrows(() => Object.defineProperty(desc, "get", {})); + }); + + it("[[Set]] works", () => { + const desc = new PropertyDescriptor({}); + const fn = () => {}; + desc.get = fn; + assertStrictEquals(desc.get, fn); + }); + + it("[[Set]] throws if not callable or undefined", () => { + const desc = new PropertyDescriptor({}); + assertThrows(() => desc.get = null); + }); + + it("[[Set]] throws if a data property is defined", () => { + const desc = new PropertyDescriptor({ value: undefined }); + assertThrows(() => desc.get = undefined); + }); + + it("[[Delete]] works", () => { + const desc = new PropertyDescriptor({ get: undefined }); + delete desc.get; + assert(!("get" in desc)); + }); + }); + + describe("~set", () => { + it("[[DefineOwnProperty]] works", () => { + const desc = new PropertyDescriptor({}); + Object.defineProperty(desc, "set", {}); + assertStrictEquals(desc.set, undefined); + }); + + it("[[DefineOwnProperty]] throws for accessor properties", () => { + const desc = new PropertyDescriptor({}); + assertThrows(() => + Object.defineProperty(desc, "set", { get: undefined }) + ); + }); + + it("[[DefineOwnProperty]] throws if not callable or undefined", () => { + const desc = new PropertyDescriptor({}); + assertThrows( + () => Object.defineProperty(desc, "set", { value: null }), + ); + }); + + it("[[DefineOwnProperty]] throws if a data property is defined", () => { + const desc = new PropertyDescriptor({ value: undefined }); + assertThrows(() => Object.defineProperty(desc, "set", {})); + }); + + it("[[Set]] works", () => { + const desc = new PropertyDescriptor({}); + const fn = (_) => {}; + desc.set = fn; + assertStrictEquals(desc.set, fn); + }); + + it("[[Set]] throws if not callable or undefined", () => { + const desc = new PropertyDescriptor({}); + assertThrows(() => desc.set = null); + }); + + it("[[Set]] throws if a data property is defined", () => { + const desc = new PropertyDescriptor({ value: undefined }); + assertThrows(() => desc.set = undefined); + }); + + it("[[Delete]] works", () => { + const desc = new PropertyDescriptor({ set: undefined }); + delete desc.set; + assert(!("set" in desc)); + }); + }); + + describe("~value", () => { + it("[[DefineOwnProperty]] works", () => { + const desc = new PropertyDescriptor({}); + Object.defineProperty(desc, "value", {}); + assertStrictEquals(desc.value, undefined); + }); + + it("[[DefineOwnProperty]] throws for accessor properties", () => { + const desc = new PropertyDescriptor({}); + assertThrows(() => + Object.defineProperty(desc, "value", { get: undefined }) + ); + }); + + it("[[DefineOwnProperty]] throws if an accessor property is defined", () => { + const desc = new PropertyDescriptor({ get: undefined }); + assertThrows(() => Object.defineProperty(desc, "value", {})); + }); + + it("[[Set]] works", () => { + const desc = new PropertyDescriptor({}); + desc.value = "success"; + assertStrictEquals(desc.value, "success"); + }); + + it("[[Set]] throws if an accessor property is defined", () => { + const desc = new PropertyDescriptor({ get: undefined }); + assertThrows(() => desc.value = null); + }); + + it("[[Delete]] works", () => { + const desc = new PropertyDescriptor({ value: undefined }); + delete desc.value; + assert(!("value" in desc)); + }); + }); + + describe("~writable", () => { + it("[[DefineOwnProperty]] coerces to a boolean", () => { + const desc = new PropertyDescriptor({}); + Object.defineProperty(desc, "writable", {}); + assertStrictEquals(desc.writable, false); + }); + + it("[[DefineOwnProperty]] throws for accessor properties", () => { + const desc = new PropertyDescriptor({}); + assertThrows(() => + Object.defineProperty(desc, "writable", { get: undefined }) + ); + }); + + it("[[DefineOwnProperty]] throws if an accessor property is defined", () => { + const desc = new PropertyDescriptor({ get: undefined }); + assertThrows(() => Object.defineProperty(desc, "writable", {})); + }); + + it("[[Set]] coerces to a boolean", () => { + const desc = new PropertyDescriptor({}); + desc.writable = undefined; + assertStrictEquals(desc.writable, false); + }); + + it("[[Set]] throws if an accessor property is defined", () => { + const desc = new PropertyDescriptor({ get: undefined }); + assertThrows(() => desc.writable = false); + }); + + it("[[Delete]] works", () => { + const desc = new PropertyDescriptor({ writable: false }); + delete desc.writable; + assert(!("writable" in desc)); + }); + }); +}); + +describe("defineOwnProperties", () => { + it("[[Call]] defines properties from the provided objects", () => { + const obj = {}; + defineOwnProperties(obj, { + etaoin: {}, + shrdlu: {}, + }, { cmfwyp: {} }); + assert("etaoin" in obj); + assert("shrdlu" in obj); + assert("cmfwyp" in obj); + }); + + it("[[Call]] overrides earlier declarations with later ones", () => { + const obj = { etaoin: undefined }; + defineOwnProperties(obj, { + etaoin: { value: "failure" }, + }, { + etaoin: { value: "success" }, + }); + assertStrictEquals(obj.etaoin, "success"); + }); + + it("[[Call]] returns the provided object", () => { + const obj = {}; + assertStrictEquals(defineOwnProperties(obj), obj); + }); +}); + +describe("deleteOwnProperty", () => { + it("[[Call]] deletes the provided property on the provided object", () => { + const obj = { failure: undefined }; + deleteOwnProperty(obj, "failure"); + assert(!("failure" in obj)); + }); + + it("[[Call]] does nothing if the property doesn’t exist", () => { + const obj = Object.freeze({}); + deleteOwnProperty(obj, "failure"); + assert(!("failure" in obj)); + }); + + it("[[Call]] throws if the property can’t be deleted", () => { + const obj = Object.seal({ failure: undefined }); + assertThrows(() => deleteOwnProperty(obj, "failure")); + }); + + it("[[Call]] returns the provided object", () => { + const obj = {}; + assertStrictEquals(deleteOwnProperty(obj, ""), obj); + }); +}); + +describe("frozenCopy", () => { + it("[[Call]] returns a frozen object", () => { + assert( + Object.isFrozen( + frozenCopy(Object.create(null), { + data: { + configurable: true, + enumerable: true, + value: undefined, + writable: true, + }, + accessor: { + configurable: true, + enumerable: true, + get: undefined, + }, + }), + ), + ); + }); + + it("[[Call]] ignores non·enumerable properties", () => { + assertEquals( + frozenCopy( + Object.create(null, { + data: { value: undefined }, + accessor: { get: undefined }, + }), + ), + {}, + ); + }); + + it("[[Call]] preserves accessor properties", () => { + const properties = { + both: { + configurable: false, + enumerable: true, + get: () => {}, + set: (_) => {}, + }, + empty: { + configurable: false, + enumerable: true, + get: undefined, + set: undefined, + }, + getter: { + configurable: false, + enumerable: true, + get: () => {}, + set: undefined, + }, + setter: { + configurable: false, + enumerable: true, + get: undefined, + set: (_) => {}, + }, + }; + assertEquals( + Object.getOwnPropertyDescriptors( + frozenCopy(Object.create(null, properties)), + ), + properties, + ); + }); + + it("[[Call]] does not copy properties on the prototype", () => { + assert( + !("failure" in + frozenCopy(Object.create({ failure: undefined }), { + data: { + configurable: true, + value: undefined, + writable: true, + }, + accessor: { configurable: true, get: undefined }, + })), + ); + }); + + it("[[Call]] uses the species of the constructor", () => { + const species = { prototype: {} }; + assertStrictEquals( + Object.getPrototypeOf( + frozenCopy({}, { [Symbol.species]: species }), + ), + species.prototype, + ); + }); + + it("[[Call]] uses constructor if no species is defined", () => { + const constructor = { [Symbol.species]: null, prototype: {} }; + assertStrictEquals( + Object.getPrototypeOf(frozenCopy({}, constructor)), + constructor.prototype, + ); + }); + + it("[[Call]] uses the constructor on the object if none is provided", () => { + const constructor = { [Symbol.species]: null, prototype: {} }; + assertStrictEquals( + Object.getPrototypeOf(frozenCopy({ constructor })), + constructor.prototype, + ); + }); + + it("[[Call]] allows a null constructor", () => { + assertStrictEquals( + Object.getPrototypeOf(frozenCopy({}, null)), + null, + ); + }); +}); + +describe("setPropertyValue", () => { + it("[[Call]] sets the provided property on the provided object", () => { + const obj = {}; + setPropertyValue(obj, "success", true); + assertStrictEquals(obj.success, true); + }); + + it("[[Call]] calls setters", () => { + const setter = spy((_) => {}); + const obj = Object.create(null, { success: { set: setter } }); + setPropertyValue(obj, "success", true); + assertSpyCalls(setter, 1); + assertSpyCall(setter, 0, { + args: [true], + self: obj, + }); + }); + + it("[[Call]] walks the prototype chain", () => { + const setter = spy((_) => {}); + const obj = Object.create( + Object.create(null, { success: { set: setter } }), + ); + setPropertyValue(obj, "success", true); + assertSpyCalls(setter, 1); + assertSpyCall(setter, 0, { + args: [true], + self: obj, + }); + }); + + it("[[Call]] uses the provided receiver", () => { + const setter = spy((_) => {}); + const obj = Object.create(null, { success: { set: setter } }); + const receiver = {}; + setPropertyValue(obj, "success", true, receiver); + assertSpyCalls(setter, 1); + assertSpyCall(setter, 0, { + args: [true], + self: receiver, + }); + }); + + it("[[Call]] throws if the property can’t be set", () => { + const obj = Object.freeze({ failure: undefined }); + assertThrows(() => setPropertyValue(obj, "failure", true)); + }); + + it("[[Call]] returns the provided object", () => { + const obj = {}; + assertStrictEquals(setPropertyValue(obj, "", undefined), obj); + }); +}); + +describe("toObject", () => { + it("returns the input for objects", () => { + const obj = {}; + assertStrictEquals(toObject(obj), obj); + }); + + it("returns a new object for nullish values", () => { + assertEquals(toObject(null), {}); + assertEquals(toObject(void {}), {}); + }); + + it("returns a wrapper object for other primitives", () => { + const sym = Symbol(); + assertStrictEquals(typeof toObject(sym), "object"); + assertStrictEquals(toObject(sym).valueOf(), sym); + }); +}); + +describe("toPropertyKey", () => { + it("returns a string or symbol", () => { + const sym = Symbol(); + assertStrictEquals(toPropertyKey(sym), sym); + assertStrictEquals( + toPropertyKey(new String("success")), + "success", + ); + }); + + it("favours the `toString` representation", () => { + assertStrictEquals( + toPropertyKey({ + toString() { + return "success"; + }, + valueOf() { + return "failure"; + }, + }), + "success", + ); + }); +});