Also adds tests for module exports.
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";
export * from "./numeric.js";
export * from "./object.js";
export * from "./string.js";
+export * from "./value.js";
--- /dev/null
+// ♓🌟 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 <https://mozilla.org/MPL/2.0/>.
+
+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 && /(?<!^mod|\.test|(?:^|-)deps)\.js$/u.test(name)) {
+ await import(`./${name}`).then((module) => {
+ for (const exported of Object.keys(module)) {
+ assert(exported in Piscēs);
+ }
+ });
+ }
+ }
+ });
+});
// 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 { 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.
*
* 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
* 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.",
} 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 {
// 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 {
}
}
- /** 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 ||
}
/**
- * Returns whether this is a generic (not accessor or data)
+ * Gets whether this is a generic (not accessor or data)
* descrtiptor.
*/
get isGenericDescriptor() {
}
}
- const coercePropretyDescriptorValue = (P, V) => {
+ const coercePropertyDescriptorValue = (P, V) => {
switch (P) {
case "configurable":
case "enumerable":
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.",
);
return V;
}
case "set":
- if (typeof V != "function") {
+ if (V !== undefined && typeof V !== "function") {
throw new TypeError(
"Piscēs: Setters must be callable.",
);
}
};
- 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 (
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.
}
},
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) {
},
);
- 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
--- /dev/null
+// ♓🌟 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",
+ );
+ });
+});
--- /dev/null
+// ♓🌟 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 <https://mozilla.org/MPL/2.0/>.
+
+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;
+ }
+};
--- /dev/null
+// ♓🌟 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 <https://mozilla.org/MPL/2.0/>.
+
+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");
+ });
+});