+// ♓🌟 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 <https://mozilla.org/MPL/2.0/>.
+
+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",
+ );
+ });
+});