--- /dev/null
+// 📧🏷️ Étiquette ∷ memory.test.js
+// ====================================================================
+//
+// Copyright © 2023 Lady [@ Lady’s Computer].
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at <https://mozilla.org/MPL/2.0/>.
+
+import {
+ assert,
+ assertEquals,
+ assertStrictEquals,
+ assertThrows,
+ beforeEach,
+ describe,
+ it,
+} from "./dev-deps.js";
+import { Storage } from "./memory.js";
+
+/** A simple storable class for use in testing. */
+class Storable {
+ /** Constructs a new instance with the provided data. */
+ constructor(data) {
+ this.data = data;
+ this.id = null;
+ }
+
+ /** Returns a new instance with the provided data and id. */
+ static [Storage.toInstance](data, id) {
+ const result = new Storable(data);
+ result.id = id;
+ return result;
+ }
+
+ /** Returns the data of this instance. */
+ [Storage.toObject]() {
+ return this.data;
+ }
+}
+
+describe("Storage", () => {
+ it("[[Construct]] creates a new Storage", () => {
+ assertStrictEquals(
+ Object.getPrototypeOf(new Storage()),
+ Storage.prototype,
+ );
+ });
+
+ describe(".toInstance", () => {
+ it("[[Get]] returns a symbol", () => {
+ assertStrictEquals(typeof Storage.toInstance, "symbol");
+ });
+
+ it("[[Set]] throws", () => {
+ assertThrows(() => Storage.toInstance = null);
+ });
+ });
+
+ describe(".toObject", () => {
+ it("[[Get]] returns a symbol", () => {
+ assertStrictEquals(typeof Storage.toObject, "symbol");
+ });
+
+ it("[[Set]] throws", () => {
+ assertThrows(() => Storage.toObject = null);
+ });
+ });
+
+ describe("::add", () => {
+ let instance;
+
+ beforeEach(() => {
+ instance = new Storage();
+ });
+
+ it("[[Call]] returns an id", () => {
+ const result = instance.add(new Storable());
+ assertStrictEquals(typeof result, "string");
+ assert(
+ /^[0-9A-Z*~$=][0-9A-TV-Z]{2}-[0-9A-TV-Z]{4}$/u.test(result),
+ );
+ });
+
+ it("[[Call]] stores data for retrieval later", () => {
+ const data = { my: "data" };
+ const storable = new Storable(data);
+ const newID = instance.add(storable);
+ assertEquals(instance.get(newID).data, data);
+ });
+
+ it("[[Call]] does not store non‐enumerable properties", () => {
+ const data = Object.create(null, {
+ gone: { enumerable: false },
+ });
+ const storable = new Storable(data);
+ const newID = instance.add(storable);
+ assert(!("gone" in instance.get(newID).data));
+ });
+
+ it("[[Call]] does not store prototype properties", () => {
+ const data = Object.create({ gone: true });
+ const storable = new Storable(data);
+ const newID = instance.add(storable);
+ assert(!("gone" in instance.get(newID).data));
+ });
+
+ it("[[Call]] throws if the provided value is not an object", () => {
+ assertThrows(() => {
+ instance.add("");
+ });
+ });
+
+ it("[[Call]] throws if the provided value does not implement `[Storage.toObject]`", () => {
+ assertThrows(() => {
+ instance.add(Object.create(null));
+ });
+ });
+ });
+
+ describe("::delete", () => {
+ let instance;
+
+ beforeEach(() => {
+ instance = new Storage();
+ });
+
+ it("[[Call]] returns whether the value was assigned", () => {
+ assertStrictEquals(instance.delete("000-0000"), false);
+ const newID = instance.add(new Storable());
+ assertStrictEquals(instance.delete(newID), true);
+ });
+
+ it("[[Call]] deletes the value", () => {
+ const newID = instance.add(new Storable());
+ instance.delete(newID);
+ assertStrictEquals(instance.get(newID), null);
+ });
+
+ it("[[Call]] throws if the provided identifier is invalid", () => {
+ assertThrows(() => {
+ instance.delete("");
+ });
+ });
+ });
+
+ describe("::has", () => {
+ let instance;
+
+ beforeEach(() => {
+ instance = new Storage();
+ });
+
+ it("[[Call]] returns whether the value was assigned", () => {
+ assertStrictEquals(instance.has("000-0000"), false);
+ instance.set("000-0000", new Storable());
+ assertStrictEquals(instance.has("000-0000"), true);
+ instance.delete("000-0000")
+ assertStrictEquals(instance.has("000-0000"), false);
+ });
+
+ it("[[Call]] throws if the provided identifier is invalid", () => {
+ assertThrows(() => {
+ instance.has("");
+ });
+ });
+ });
+
+ describe("::set", () => {
+ let instance;
+ let newID;
+
+ beforeEach(() => {
+ instance = new Storage();
+ newID = new Storage().add(new Storable());
+ });
+
+ it("[[Call]] returns the instance", () => {
+ const result = instance.set(newID, new Storable());
+ assertStrictEquals(result, instance);
+ });
+
+ it("[[Call]] stores data for retrieval later", () => {
+ const data = { my: "data" };
+ const storable = new Storable(data);
+ instance.set(newID, storable);
+ assertEquals(instance.get(newID).data, data);
+ });
+
+ it("[[Call]] updates existing data", () => {
+ const data = { my: "data" };
+ const storable = new Storable(data);
+ instance.set(newID, storable);
+ data.my = "new data"
+ instance.set(newID, storable);
+ assertEquals(instance.get(newID).data, data);
+ });
+
+ it("[[Call]] does not store non‐enumerable properties", () => {
+ const data = Object.create(null, {
+ gone: { enumerable: false },
+ });
+ const storable = new Storable(data);
+ instance.set(newID, storable);
+ assert(!("gone" in instance.get(newID).data));
+ });
+
+ it("[[Call]] does not store prototype properties", () => {
+ const data = Object.create({ gone: true });
+ const storable = new Storable(data);
+ instance.set(newID, storable);
+ assert(!("gone" in instance.get(newID).data));
+ });
+
+ it("[[Call]] throws if the provided identifier is invalid", () => {
+ assertThrows(() => {
+ instance.set("", new Storable());
+ });
+ });
+
+ it("[[Call]] throws if the provided value is not an object", () => {
+ assertThrows(() => {
+ instance.set(newID, "");
+ });
+ });
+
+ it("[[Call]] throws if the provided value does not implement `[Storage.toObject]`", () => {
+ assertThrows(() => {
+ instance.set(newID, Object.create(null));
+ });
+ });
+ });
+
+ describe("::size", () => {
+ let instance;
+
+ beforeEach(() => {
+ instance = new Storage();
+ });
+
+ it("[[Get]] returns the number of stored values", () => {
+ assertStrictEquals(instance.size, 0);
+ instance.add(new Storable());
+ assertStrictEquals(instance.size, 1);
+ const newID = instance.add(new Storable());
+ assertStrictEquals(instance.size, 2);
+ instance.set(newID, new Storable());
+ assertStrictEquals(instance.size, 2);
+ });
+
+ it("[[Get]] does not count deleted values", () => {
+ assertStrictEquals(instance.size, 0);
+ instance.add(new Storable());
+ assertStrictEquals(instance.size, 1);
+ const newID = instance.add(new Storable());
+ assertStrictEquals(instance.size, 2);
+ instance.delete(newID);
+ assertStrictEquals(instance.size, 1);
+ });
+
+ it("[[Set]] throws when setting", () => {
+ assertThrows(() => {
+ instance.size = 1;
+ });
+ });
+ });
+});