X-Git-Url: https://git.ladys.computer/Etiquette/blobdiff_plain/2889a6becfdfadc5b2ddce0b4b4f281a756b0c9e..ce40db353c27887f4345c88fc70a7251bf688bbb:/memory.test.js diff --git a/memory.test.js b/memory.test.js new file mode 100644 index 0000000..8187635 --- /dev/null +++ b/memory.test.js @@ -0,0 +1,267 @@ +// 📧🏷️ É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 . + +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; + }); + }); + }); +});