From: Lady Date: Sun, 17 Jul 2022 20:45:08 +0000 (-0700) Subject: Split some object code into value and unit test X-Git-Tag: 0.1.0~8 X-Git-Url: https://git.ladys.computer/Pisces/commitdiff_plain/7c3a2eda590637af9463e5a59ab798f802d274a9 Split some object code into value and unit test Also adds tests for module exports. --- diff --git a/dev-deps.js b/dev-deps.js index 49044c1..96858a6 100644 --- a/dev-deps.js +++ b/dev-deps.js @@ -12,4 +12,13 @@ export { assertEquals, assertStrictEquals, assertThrows, -} from "https://deno.land/std@0.134.0/testing/asserts.ts"; +} from "https://deno.land/std@0.148.0/testing/asserts.ts"; +export { + describe, + it, +} from "https://deno.land/std@0.148.0/testing/bdd.ts"; +export { + assertSpyCall, + assertSpyCalls, + spy, +} from "https://deno.land/std@0.148.0/testing/mock.ts"; diff --git a/mod.js b/mod.js index d036cbc..42b421c 100644 --- a/mod.js +++ b/mod.js @@ -14,3 +14,4 @@ export * from "./iri.js"; export * from "./numeric.js"; export * from "./object.js"; export * from "./string.js"; +export * from "./value.js"; diff --git a/mod.test.js b/mod.test.js new file mode 100644 index 0000000..59a7cbf --- /dev/null +++ b/mod.test.js @@ -0,0 +1,25 @@ +// ♓🌟 Piscēs ∷ mod.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, describe, it } from "./dev-deps.js"; +import * as Piscēs from "./mod.js"; + +describe("Piscēs", () => { + it("exports everything", async () => { + for await (const { name, isFile } of Deno.readDir(".")) { + if (isFile && /(? { + for (const exported of Object.keys(module)) { + assert(exported in Piscēs); + } + }); + } + } + }); +}); diff --git a/object.js b/object.js index c0a9081..578f84f 100644 --- a/object.js +++ b/object.js @@ -7,40 +7,8 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at . -import { call } from "./function.js"; - -export const { - assign: assignProperties, - defineProperty: defineOwnProperty, - defineProperties: defineOwnProperties, - freeze, - getOwnPropertyDescriptor, - getOwnPropertyDescriptors, - getOwnPropertyNames: getOwnPropertyStrings, - getOwnPropertySymbols, - getPrototypeOf: getPrototype, - hasOwn: hasOwnProperty, - isExtensible, - isFrozen, - isSealed, - entries: namedEntries, - keys: namedKeys, - values: namedValues, - create: objectCreate, - fromEntries: objectFromEntries, - preventExtensions, - is: sameValue, - seal, - setPrototypeOf: setPrototype, -} = Object; - -export const { - delete: deleteOwnProperty, - keys: getOwnPropertyKeys, - get: getPropertyValue, - has: hasProperty, - set: setPropertyValue, -} = Reflect; +import { bind, call } from "./function.js"; +import { toPrimitive, type } from "./value.js"; /** * A property descriptor object. @@ -53,7 +21,7 @@ export const { * * Otherwise, the instance properties and methods are generic. */ -export const PropertyDescriptor = (() => { +export const { PropertyDescriptor } = (() => { class PropertyDescriptor extends null { /** * Constructs a new property descriptor object from the provided @@ -64,8 +32,8 @@ export const PropertyDescriptor = (() => { * boolean). */ //deno-lint-ignore constructor-super - constructor(Obj) { - if (!isObject(Obj)) { + constructor(O) { + if (type(O) !== "object") { // The provided value is not an object. throw new TypeError( "Piscēs: Cannot convert primitive to property descriptor.", @@ -73,38 +41,38 @@ export const PropertyDescriptor = (() => { } else { // The provided value is an object. const desc = objectCreate(propertyDescriptorPrototype); - if ("enumerable" in Obj) { + if ("enumerable" in O) { // An enumerable property is specified. - desc.enumerable = !!Obj.enumerable; + desc.enumerable = !!O.enumerable; } else { // An enumerable property is not specified. /* do nothing */ } - if ("configurable" in Obj) { + if ("configurable" in O) { // A configurable property is specified. - desc.configurable = !!Obj.configurable; + desc.configurable = !!O.configurable; } else { // A configurable property is not specified. /* do nothing */ } - if ("value" in Obj) { + if ("value" in O) { // A value property is specified. - desc.value = Obj.value; + desc.value = O.value; } else { // A value property is not specified. /* do nothing */ } - if ("writable" in Obj) { + if ("writable" in O) { // A writable property is specified. - desc.writable = !!Obj.writable; + desc.writable = !!O.writable; } else { // A writable property is not specified. /* do nothing */ } - if ("get" in Obj) { + if ("get" in O) { // A get property is specified. - const getter = Obj.get; - if (typeof getter != "function") { + const getter = O.get; + if (getter !== undefined && typeof getter !== "function") { // The getter is not callable. throw new TypeError("Piscēs: Getters must be callable."); } else { @@ -115,10 +83,10 @@ export const PropertyDescriptor = (() => { // A get property is not specified. /* do nothing */ } - if ("set" in Obj) { + if ("set" in O) { // A set property is specified. - const setter = Obj.set; - if (typeof setter != "function") { + const setter = O.set; + if (setter !== undefined && typeof setter !== "function") { // The setter is not callable. throw new TypeError("Piscēs: Setters must be callable."); } else { @@ -200,20 +168,18 @@ export const PropertyDescriptor = (() => { } } - /** Returns whether this is an accessor descrtiptor. */ + /** Gets whether this is an accessor descrtiptor. */ get isAccessorDescriptor() { return this !== undefined && ("get" in this || "set" in this); } - /** Returns whether this is a data descrtiptor. */ + /** Gets whether this is a data descrtiptor. */ get isDataDescriptor() { return this !== undefined && ("value" in this || "writable" in this); } - /** - * Returns whether this is a fully‐populated property descriptor. - */ + /** Gets whether this is a fully‐populated property descriptor. */ get isFullyPopulated() { return this !== undefined && ("value" in this && "writable" in this || @@ -222,7 +188,7 @@ export const PropertyDescriptor = (() => { } /** - * Returns whether this is a generic (not accessor or data) + * Gets whether this is a generic (not accessor or data) * descrtiptor. */ get isGenericDescriptor() { @@ -232,7 +198,7 @@ export const PropertyDescriptor = (() => { } } - const coercePropretyDescriptorValue = (P, V) => { + const coercePropertyDescriptorValue = (P, V) => { switch (P) { case "configurable": case "enumerable": @@ -241,7 +207,7 @@ export const PropertyDescriptor = (() => { case "value": return V; case "get": - if (typeof V != "function") { + if (V !== undefined && typeof V !== "function") { throw new TypeError( "Piscēs: Getters must be callable.", ); @@ -249,7 +215,7 @@ export const PropertyDescriptor = (() => { return V; } case "set": - if (typeof V != "function") { + if (V !== undefined && typeof V !== "function") { throw new TypeError( "Piscēs: Setters must be callable.", ); @@ -261,10 +227,12 @@ export const PropertyDescriptor = (() => { } }; - const propertyDescriptorPrototype = PropertyDescriptor.prototype; + const { + prototype: propertyDescriptorPrototype, + } = PropertyDescriptor; - const propertyDescriptorProxyHandler = assignProperties( - objectCreate(null), + const propertyDescriptorProxyHandler = Object.assign( + Object.create(null), { defineProperty(O, P, Desc) { if ( @@ -279,9 +247,9 @@ export const PropertyDescriptor = (() => { throw new TypeError( "Piscēs: Property descriptor attributes must be data properties.", ); - } else if ("value" in desc) { - // Desc has a value. - desc.value = coercePropretyDescriptorValue(P, desc.value); + } else if ("value" in desc || !(P in O)) { + // Desc has a value or P does not already exist on O. + desc.value = coercePropertyDescriptorValue(P, desc.value); } else { // Desc is not an accessor property descriptor and has no // value. @@ -307,20 +275,36 @@ export const PropertyDescriptor = (() => { } }, set(O, P, V, Receiver) { - const newValue = coercePropertyDescriptorValue(P, V); - const isAccessorDescriptor = "get" === P || "set" === P || - "get" in O || "set" in O; - const isDataDescriptor = "value" === P || "writable" === P || - "value" in O || "writable" in O; - if (isAccessorDescriptor && isDataDescriptor) { - // Both accessor and data attributes will be present on O - // after defining P. - throw new TypeError( - "Piscēs: Property descriptors cannot specify both accessor and data attributes.", - ); + if ( + P === "configurable" || P === "enumerable" || + P === "writable" || P === "value" || + P === "get" || P === "set" + ) { + // P is a property descriptor attribute. + const newValue = coercePropertyDescriptorValue(P, V); + const isAccessorDescriptor = "get" === P || "set" === P || + "get" in O || "set" in O; + const isDataDescriptor = "value" === P || "writable" === P || + "value" in O || "writable" in O; + if (isAccessorDescriptor && isDataDescriptor) { + // Both accessor and data attributes will be present on O + // after defining P. + throw new TypeError( + "Piscēs: Property descriptors cannot specify both accessor and data attributes.", + ); + } else { + // P can be safely defined on O. + // + // ☡ Receiver will be the *proxied* object, so passing it + // through to setPropertyValue here would produce an + // infinite loop. + // + // ☡ This has implications on objects with a proxied + // PropertyDescriptor in their prototype. + return setPropertyValue(O, P, newValue, O); + } } else { - // P can be safely defined on O. - return setPropertyValue(O, prop, newValue, Receiver); + return setPropertyValue(O, P, V, Receiver); } }, setPrototypeOf(O, V) { @@ -335,160 +319,384 @@ export const PropertyDescriptor = (() => { }, ); - return PropertyDescriptor; + return { PropertyDescriptor }; })(); -/** - * Returns a new frozen shallow copy of the enumerable own properties - * of the provided object, according to the following rules :— - * - * - For data properties, create a nonconfigurable, nonwritable - * property with the same value. - * - * - For accessor properties, create a nonconfigurable accessor - * property with the same getter *and* setter. - * - * The prototype for the resulting object will be taken from the - * `prototype` property of the provided constructor, or the `prototype` - * of the `constructor` of the provided object if the provided - * constructor is undefined. If the used constructor has a nonnullish - * `Symbol.species`, that will be used instead. - */ -export const frozenCopy = (O, constructor = O?.constructor) => { - if (O == null) { - // O is null or undefined. - throw new TypeError( - "Piscēs: Cannot copy properties of null or undefined.", - ); - } else { - // O is not null or undefined. - // - // (If not provided, the constructor will be the value of getting - // the `constructor` property of O.) - const species = constructor?.[Symbol.species] ?? constructor; - return preventExtensions( - objectCreate( - species == null || !("prototype" in species) - ? null - : species.prototype, - objectFromEntries( - function* () { - for (const P of getOwnPropertyKeys(O)) { - const Desc = getOwnPropertyDescriptor(O, P); - if (Desc.enumerable) { - // P is an enumerable property. - yield [ - P, - "get" in Desc || "set" in Desc - ? { - configurable: false, - enumerable: true, - get: Desc.get, - set: Desc.set, - } - : { - configurable: false, - enumerable: true, - value: Desc.value, - writable: false, - }, - ]; - } else { - // P is not an enumerable property. - /* do nothing */ - } - } - }(), - ), - ), - ); - } -}; +export const { + /** + * Defines own properties on the provided object using the + * descriptors on the enumerable own properties of the provided + * additional objects. + * + * ※ This differs from Object.defineProperties in that it can take + * multiple source objects. + */ + defineOwnProperties, +} = (() => { + const { defineProperties } = Object; + const { forEach: arrayForEach } = Array.prototype; + return { + defineOwnProperties: (O, ...sources) => { + call( + arrayForEach, + sources, + [(source) => defineProperties(O, source)], + ); + return O; + }, + }; +})(); -/** Returns whether the provided value is an object. */ -export const isObject = ($) => { - return $ !== null && - (typeof $ == "function" || typeof $ == "object"); -}; +export const { + /** + * Defines an own property on the provided object on the provided + * property key using the provided property descriptor. + * + * ※ This is an alias for Object.defineProperty. + */ + defineProperty: defineOwnProperty, -/** - * Returns the primitive value of the provided object per its - * `toString` and `valueOf` methods. - * - * If the provided hint is "string", then `toString` takes precedence; - * otherwise, `valueOf` does. - * - * Throws an error if both of these methods are not callable or do not - * return a primitive. - */ -export const ordinaryToPrimitive = (O, hint) => { - for ( - const name of hint == "string" - ? ["toString", "valueOf"] - : ["valueOf", "toString"] - ) { - const method = O[name]; - if (typeof method == "function") { - // Method is callable. - const result = call(method, O, []); - if (!isObject(result)) { - // Method returns a primitive. - return result; + /** + * Marks the provided object as non·extensible and marks all its + * properties as nonconfigurable and (if data properties) + * nonwritable, and returns the object. + * + * ※ This is an alias for Object.freeze. + */ + freeze, + + /** + * Returns the property descriptor for the own property with the + * provided property key on the provided object, or null if none + * exists. + * + * ※ This is an alias for Object.getOwnPropertyDescriptor. + */ + getOwnPropertyDescriptor, + + /** + * Returns the property descriptors for the own properties on the + * provided object. + * + * ※ This is an alias for Object.getOwnPropertyDescriptors. + */ + getOwnPropertyDescriptors, + + /** + * Returns an array of string‐valued own property keys on the + * provided object. + * + * ☡ This includes both enumerable and non·enumerable properties. + * + * ※ This is an alias for Object.getOwnPropertyNames. + */ + getOwnPropertyNames: getOwnPropertyStrings, + + /** + * Returns an array of symbol‐valued own property keys on the + * provided object. + * + * ☡ This includes both enumerable and non·enumerable properties. + * + * ※ This is an alias for Object.getOwnPropertySymbols. + */ + getOwnPropertySymbols, + + /** + * Returns the prototype of the provided object. + * + * ※ This is an alias for Object.getPrototypeOf. + */ + getPrototypeOf: getPrototype, + + /** + * Returns whether the provided object has an own property with the + * provided property key. + * + * ※ This is an alias for Object.hasOwn. + */ + hasOwn: hasOwnProperty, + + /** + * Returns whether the provided object is extensible. + * + * ※ This is an alias for Object.isExtensible. + */ + isExtensible, + + /** + * Returns whether the provided object is frozen. + * + * ※ This is an alias for Object.isFrozen. + */ + isFrozen, + + /** + * Returns whether the provided object is sealed. + * + * ※ This is an alias for Object.isSealed. + */ + isSealed, + + /** + * Returns an array of key~value pairs for the enumerable, + * string‐valued property keys on the provided object. + * + * ※ This is an alias for Object.entries. + */ + entries: namedEntries, + + /** + * Returns an array of the enumerable, string‐valued property keys on + * the provided object. + * + * ※ This is an alias for Object.keys. + */ + keys: namedKeys, + + /** + * Returns an array of property values for the enumerable, + * string‐valued property keys on the provided object. + * + * ※ This is an alias for Object.values. + */ + values: namedValues, + + /** + * Returns a new object with the provided prototype and property + * descriptors. + * + * ※ This is an alias for Object.create. + */ + create: objectCreate, + + /** + * Returns a new object with the provided property keys and values. + * + * ※ This is an alias for Object.fromEntries. + */ + fromEntries: objectFromEntries, + + /** + * Marks the provided object as non·extensible, and returns the + * object. + * + * ※ This is an alias for Object.preventExtensions. + */ + preventExtensions, + + /** + * Marks the provided object as non·extensible and marks all its + * properties as nonconfigurable, and returns the object. + * + * ※ This is an alias for Object.seal. + */ + seal, + + /** + * Sets the values of the enumerable own properties of the provided + * additional objects on the provided object. + * + * ※ This is an alias for Object.assign. + */ + assign: setPropertyValues, + + /** + * Sets the prototype of the provided object to the provided value + * and returns the object. + * + * ※ This is an alias for Object.setPrototypeOf. + */ + setPrototypeOf: setPrototype, +} = Object; + +export const { + /** + * Removes the provided property key from the provided object and + * returns the object. + * + * ☡ This function differs from Reflect.deleteProperty and the + * `delete` operator in that it throws if the deletion is + * unsuccessful. + */ + deleteOwnProperty, + + /** + * Sets the provided property key to the provided value on the + * provided object and returns the object. + * + * ※ This function differs from Reflect.set in that it throws if the + * setting is unsuccessful. + */ + setPropertyValue, +} = (() => { + const { deleteProperty, set } = Reflect; + + return { + deleteOwnProperty: (O, P) => { + if (!deleteProperty(O, P)) { + throw new TypeError( + `Piscēs: Tried to delete property from object but [[Delete]] returned false: ${P}`, + ); } else { - // Method returns an object. - continue; + return O; } - } else { - // Method is not callable. - continue; - } - } - throw new TypeError("Piscēs: Unable to convert object to primitive"); -}; + }, + setPropertyValue: (O, P, V, Receiver = O) => { + if (!set(O, P, V, Receiver)) { + throw new TypeError( + `Piscēs: Tried to set property on object but [[Set]] returned false: ${P}`, + ); + } else { + return O; + } + }, + }; +})(); -/** - * Returns the provided value converted to a primitive, or throws if - * no such conversion is possible. - * - * The provided preferred type, if specified, should be "string", - * "number", or "default". If the provided input has a - * `Symbol.toPrimitive` method, this function will throw rather than - * calling that method with a preferred type other than one of the - * above. - */ -export const toPrimitive = ($, preferredType) => { - if (isObject($)) { - // The provided value is an object. - const exoticToPrim = $[Symbol.toPrimitive] ?? undefined; - if (exoticToPrim !== undefined) { - // The provided value has an exotic primitive conversion method. - if (typeof exoticToPrim != "function") { - // The method is not callable. +export const { + /** + * Returns a new frozen shallow copy of the enumerable own properties + * of the provided object, according to the following rules :— + * + * - For data properties, create a nonconfigurable, nonwritable + * property with the same value. + * + * - For accessor properties, create a nonconfigurable accessor + * property with the same getter *and* setter. + * + * The prototype for the resulting object will be taken from the + * `prototype` property of the provided constructor, or the + * `prototype` of the `constructor` of the provided object if the + * provided constructor is undefined. If the used constructor has a + * nonnullish `Symbol.species`, that will be used instead. If the + * used constructor or species is nullish or does not have a + * `prototype` property, the prototype is set to null. + */ + frozenCopy, +} = (() => { + const { + iterator: iteratorSymbol, + species: speciesSymbol, + } = Symbol; + const { + next: generatorIteratorNext, + } = getPrototype(function* () {}.prototype); + const propertyDescriptorEntryIterablePrototype = { + [iteratorSymbol]() { + return { + next: bind(generatorIteratorNext, this.generator(), []), + }; + }, + }; + return { + frozenCopy: (O, constructor = O?.constructor) => { + if (O == null) { + // O is null or undefined. throw new TypeError( - "Piscēs: Symbol.toPrimitive was neither nullish nor callable.", + "Piscēs: Cannot copy properties of null or undefined.", ); } else { - // The method is callable. - const hint = `${preferredType ?? "default"}`; - if (!["default", "string", "number"].includes(hint)) { - // An invalid preferred type was specified. - throw new TypeError( - `Piscēs: Invalid preferred type: ${preferredType}.`, - ); - } else { - // The resulting hint is either default, string, or number. - return call(exoticToPrim, $, [hint]); - } + // O is not null or undefined. + // + // (If not provided, the constructor will be the value of + // getting the `constructor` property of O.) + const species = constructor?.[speciesSymbol] ?? constructor; + return preventExtensions( + objectCreate( + species == null || !("prototype" in species) + ? null + : species.prototype, + objectFromEntries( + objectCreate( + propertyDescriptorEntryIterablePrototype, + { + generator: { + value: function* () { + const ownPropertyKeys = getOwnPropertyKeys(O); + for ( + let i = 0; + i < ownPropertyKeys.length; + ++i + ) { + const P = ownPropertyKeys[i]; + const Desc = getOwnPropertyDescriptor(O, P); + if (Desc.enumerable) { + // P is an enumerable property. + yield [ + P, + "get" in Desc || "set" in Desc + ? { + configurable: false, + enumerable: true, + get: Desc.get, + set: Desc.set, + } + : { + configurable: false, + enumerable: true, + value: Desc.value, + writable: false, + }, + ]; + } else { + // P is not an enumerable property. + /* do nothing */ + } + } + }, + }, + }, + ), + ), + ), + ); } - } else { - // Use the ordinary primitive conversion function. - ordinaryToPrimitive($, hint); - } - } else { - // The provided value is already a primitive. - return $; - } -}; + }, + }; +})(); + +export const { + /** + * Returns an array of property keys on the provided object. + * + * ※ This is an alias for Reflect.ownKeys. + */ + ownKeys: getOwnPropertyKeys, + + /** + * Returns the value of the provided property key on the provided + * object. + * + * ※ This is an alias for Reflect.get. + */ + get: getPropertyValue, + + /** + * Returns whether the provided property key exists on the provided + * object. + * + * ※ This is an alias for Reflect.has. + * + * ※ This includes properties present on the prototype chain. + */ + has: hasProperty, +} = Reflect; + +/** + * Returns the provided value converted to an object. + * + * Null and undefined are converted to a new, empty object. Other + * primitives are wrapped. Existing objects are returned with no + * modification. + * + * ※ This is effectively a nonconstructible version of the Object + * constructor. + */ +export const { toObject } = (() => { + const makeObject = Object; + return { toObject: ($) => makeObject($) }; +})(); /** * Returns the property key (symbol or string) corresponding to the 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", + ); + }); +}); diff --git a/value.js b/value.js new file mode 100644 index 0000000..77cb6b4 --- /dev/null +++ b/value.js @@ -0,0 +1,160 @@ +// ♓🌟 Piscēs ∷ value.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 { call } from "./function.js"; + +/** The null primitive. */ +export const NULL = null; + +/** The undefined primitive. */ +export const UNDEFINED = undefined; + +export const { + /** + * Returns the primitive value of the provided object per its + * `toString` and `valueOf` methods. + * + * If the provided hint is "string", then `toString` takes + * precedence; otherwise, `valueOf` does. + * + * Throws an error if both of these methods are not callable or do + * not return a primitive. + */ + ordinaryToPrimitive, + + /** + * Returns the provided value converted to a primitive, or throws if + * no such conversion is possible. + * + * The provided preferred type, if specified, should be "string", + * "number", or "default". If the provided input has a + * `[Symbol.toPrimitive]` method, this function will throw rather + * than calling that method with a preferred type other than one of + * the above. + */ + toPrimitive, +} = (() => { + const { toPrimitive: toPrimitiveSymbol } = Symbol; + + return { + ordinaryToPrimitive: (O, hint) => { + const methodNames = hint == "string" + ? ["toString", "valueOf"] + : ["valueOf", "toString"]; + for (let index = 0; index < methodNames.length; ++index) { + const method = O[methodNames[index]]; + if (typeof method === "function") { + // Method is callable. + const result = call(method, O, []); + if (type(result) !== "object") { + // Method returns a primitive. + return result; + } else { + // Method returns an object. + continue; + } + } else { + // Method is not callable. + continue; + } + } + throw new TypeError( + "Piscēs: Unable to convert object to primitive", + ); + }, + toPrimitive: ($, preferredType = "default") => { + const hint = `${preferredType}`; + if ( + "default" !== hint && "string" !== hint && + "number" !== hint + ) { + // An invalid preferred type was specified. + throw new TypeError( + `Piscēs: Invalid preferred type: ${preferredType}.`, + ); + } else if (type($) === "object") { + // The provided value is an object. + const exoticToPrim = $[toPrimitiveSymbol] ?? undefined; + if (exoticToPrim !== undefined) { + // The provided value has an exotic primitive conversion + // method. + if (typeof exoticToPrim !== "function") { + // The method is not callable. + throw new TypeError( + "Piscēs: `[Symbol.toPrimitive]` was neither nullish nor callable.", + ); + } else { + // The method is callable. + return call(exoticToPrim, $, [hint]); + } + } else { + // Use the ordinary primitive conversion function. + return ordinaryToPrimitive($, hint); + } + } else { + // The provided value is already a primitive. + return $; + } + }, + }; +})(); + +/** + * Returns whether the provided values are the same value. + * + * ※ This differs from `===` in the cases of nan and zero. + */ +export const sameValue = Object.is; + +export const { + /** + * Returns whether the provided values are either the same value or + * both zero (either positive or negative). + * + * ※ This differs from `===` in the case of nan. + */ + sameValueZero, +} = (() => { + const { isNaN: isNan } = Number; + return { + sameValueZero: ($1, $2) => { + const type1 = type($1); + const type2 = type($2); + if (type1 !== type2) { + // The provided values are not of the same type. + return false; + } else if (type1 === "number") { + // The provided values are numbers; check if they are nan and + // use strict equality otherwise. + return isNan($1) && isNan($2) || $1 === $2; + } else { + // The provided values are not numbers; use strict equality. + return $1 === $2; + } + }, + }; +})(); + +/** + * Returns a lowercase string identifying the type of the provided + * value. + * + * This differs from the value of the `typeof` operator only in the + * cases of objects and null. + */ +export const type = ($) => { + if ($ === null) { + // The provided value is null. + return "null"; + } else { + // The provided value is not null. + const type·of = typeof $; + return type·of === "function" ? "object" : type·of; + } +}; diff --git a/value.test.js b/value.test.js new file mode 100644 index 0000000..6729b85 --- /dev/null +++ b/value.test.js @@ -0,0 +1,315 @@ +// ♓🌟 Piscēs ∷ value.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, + assertStrictEquals, + assertThrows, + describe, + it, +} from "./dev-deps.js"; +import { + NULL, + ordinaryToPrimitive, + sameValue, + sameValueZero, + toPrimitive, + type, + UNDEFINED, +} from "./value.js"; + +describe("NULL", () => { + it("[[Get]] is null", () => { + assertStrictEquals(NULL, null); + }); +}); + +describe("UNDEFINED", () => { + it("[[Get]] is undefined", () => { + assertStrictEquals(UNDEFINED, void {}); + }); +}); + +describe("ordinaryToPrimitive", () => { + it("[[Call]] prefers `valueOf` by default", () => { + const obj = { + toString() { + return "failure"; + }, + valueOf() { + return "success"; + }, + }; + assertStrictEquals(ordinaryToPrimitive(obj), "success"); + assertStrictEquals(ordinaryToPrimitive(obj, "default"), "success"); + }); + + it('[[Call]] prefers `valueOf` for a "number" hint', () => { + const obj = { + toString() { + return "failure"; + }, + valueOf() { + return "success"; + }, + }; + assertStrictEquals(ordinaryToPrimitive(obj, "number"), "success"); + }); + + it('[[Call]] prefers `toString` for a "string" hint', () => { + const obj = { + toString() { + return "success"; + }, + valueOf() { + return "failure"; + }, + }; + assertStrictEquals(ordinaryToPrimitive(obj, "string"), "success"); + }); + + it("[[Call]] falls back to the other method if the first isn’t callable", () => { + const obj = { + toString() { + return "success"; + }, + valueOf: "failure", + }; + assertStrictEquals(ordinaryToPrimitive(obj), "success"); + }); + + it("[[Call]] falls back to the other method if the first returns an object", () => { + const obj = { + toString() { + return "success"; + }, + valueOf() { + return new String("failure"); + }, + }; + assertStrictEquals(ordinaryToPrimitive(obj), "success"); + }); + + it("[[Call]] throws an error if neither method is callable", () => { + const obj = { + toString: "failure", + valueOf: "failure", + }; + assertThrows(() => ordinaryToPrimitive(obj)); + }); + + it("[[Call]] throws an error if neither method returns an object", () => { + const obj = { + toString() { + return new String("failure"); + }, + valueOf() { + return new String("failure"); + }, + }; + assertThrows(() => ordinaryToPrimitive(obj)); + }); +}); + +describe("sameValue", () => { + it("[[Call]] returns false for null 🆚 undefined", () => { + assert(!sameValue(null, void {})); + }); + + it("[[Call]] returns false for null 🆚 an object", () => { + assert(!sameValue(null, {})); + }); + + it("[[Call]] returns true for null 🆚 null", () => { + assert(sameValue(null, null)); + }); + + it("[[Call]] returns false for two different objects", () => { + assert(!sameValue({}, {})); + }); + + it("[[Call]] returns true for the same object", () => { + const obj = {}; + assert(sameValue(obj, obj)); + }); + + it("[[Call]] returns false for ±0", () => { + assert(!sameValue(0, -0)); + }); + + it("[[Call]] returns true for -0", () => { + assert(sameValue(-0, -0)); + }); + + it("[[Call]] returns true for nan", () => { + assert(sameValue(0 / 0, 0 / 0)); + }); + + it("[[Call]] returns false for a primitive and its wrapped object", () => { + assert(!sameValue(false, new Boolean(false))); + }); +}); + +describe("sameValueZero", () => { + it("[[Call]] returns false for null 🆚 undefined", () => { + assert(!sameValueZero(null, void {})); + }); + + it("[[Call]] returns false for null 🆚 an object", () => { + assert(!sameValueZero(null, {})); + }); + + it("[[Call]] returns true for null 🆚 null", () => { + assert(sameValueZero(null, null)); + }); + + it("[[Call]] returns false for two different objects", () => { + assert(!sameValueZero({}, {})); + }); + + it("[[Call]] returns true for the same object", () => { + const obj = {}; + assert(sameValueZero(obj, obj)); + }); + + it("[[Call]] returns true for ±0", () => { + assert(sameValueZero(0, -0)); + }); + + it("[[Call]] returns true for -0", () => { + assert(sameValueZero(-0, -0)); + }); + + it("[[Call]] returns true for nan", () => { + assert(sameValueZero(0 / 0, 0 / 0)); + }); + + it("[[Call]] returns false for a primitive and its wrapped object", () => { + assert(!sameValueZero(false, new Boolean(false))); + }); +}); + +describe("toPrimitive", () => { + it("[[Call]] returns the argument when passed a primitive", () => { + const value = Symbol(); + assertStrictEquals(toPrimitive(value), value); + }); + + it("[[Call]] works with nullish values", () => { + assertStrictEquals(toPrimitive(null), null); + assertStrictEquals(toPrimitive(), void {}); + }); + + it("[[Call]] calls ordinaryToPrimitive by default", () => { + const value = Object.assign( + Object.create(null), + { + valueOf() { + return "success"; + }, + }, + ); + assertStrictEquals(toPrimitive(value), "success"); + }); + + it("[[Call]] accepts a hint", () => { + const value = Object.assign( + Object.create(null), + { + toString() { + return "success"; + }, + valueOf() { + return "failure"; + }, + }, + ); + assertStrictEquals(toPrimitive(value, "string"), "success"); + }); + + it("[[Call]] uses the exotic toPrimitive method if available", () => { + const value = Object.assign( + Object.create(null), + { + [Symbol.toPrimitive]() { + return "success"; + }, + }, + ); + assertStrictEquals(toPrimitive(value), "success"); + }); + + it("[[Call]] passes the hint to the exotic toPrimitive", () => { + const value = Object.assign( + Object.create(null), + { + [Symbol.toPrimitive](hint) { + return hint === "string" ? "success" : "failure"; + }, + }, + ); + assertStrictEquals(toPrimitive(value, "string"), "success"); + }); + + it('[[Call]] passes a "default" hint by default', () => { + const value = Object.assign( + Object.create(null), + { + [Symbol.toPrimitive](hint) { + return hint === "default" ? "success" : "failure"; + }, + }, + ); + assertStrictEquals(toPrimitive(value, "default"), "success"); + }); + + it("[[Call]] throws for an invalid hint", () => { + const value1 = Object.assign( + Object.create(null), + { + [Symbol.toPrimitive]() { + return "success"; + }, + }, + ); + const value2 = Object.assign( + Object.create(null), + { + valueOf() { + return true; + }, + }, + ); + assertThrows(() => toPrimitive(value1, "badhint")); + assertThrows(() => toPrimitive(value2, "badhint")); + assertThrows(() => toPrimitive(true, "badhint")); + }); +}); + +describe("type", () => { + it('[[Call]] returns "null" for null', () => { + assertStrictEquals(type(null), "null"); + }); + + it('[[Call]] returns "undefined" for undefined', () => { + assertStrictEquals(type(void {}), "undefined"); + }); + + it('[[Call]] returns "object" for non‐callable objects', () => { + assertStrictEquals(type(Object.create(null)), "object"); + }); + + it('[[Call]] returns "object" for callable objects', () => { + assertStrictEquals(type(() => {}), "object"); + }); + + it('[[Call]] returns "object" for constructable objects', () => { + assertStrictEquals(type(class {}), "object"); + }); +});