]> Lady’s Gitweb - Etiquette/blobdiff - model.test.js
Initial Tag model
[Etiquette] / model.test.js
diff --git a/model.test.js b/model.test.js
new file mode 100644 (file)
index 0000000..7944238
--- /dev/null
@@ -0,0 +1,1000 @@
+// 📧🏷️ Étiquette ∷ model.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,
+  assertArrayIncludes,
+  assertEquals,
+  assertFalse,
+  assertObjectMatch,
+  assertStrictEquals,
+  assertThrows,
+  beforeEach,
+  describe,
+  it,
+} from "./dev-deps.js";
+import { TagSystem } from "./model.js";
+
+describe("TagSystem", () => {
+  it("[[Call]] throws", () => {
+    assertThrows(() => {
+      TagSystem();
+    });
+  });
+
+  it("[[Construct]] creates a new TagSystem", () => {
+    assertStrictEquals(
+      Object.getPrototypeOf(new TagSystem("example", "1972-12-31")),
+      TagSystem.prototype,
+    );
+  });
+
+  it("[[Construct]] uses the identifier if provided", () => {
+    assertStrictEquals(
+      new TagSystem("example", "1972-12-31", "etaoin").identifier,
+      "etaoin",
+    );
+  });
+
+  it("[[Construct]] uses an empty identifier if none is provided", () => {
+    assertStrictEquals(
+      new TagSystem("example", "1972-12-31").identifier,
+      "",
+    );
+  });
+
+  it("[[Construct]] throws if provided an invalid domain", () => {
+    assertThrows(() => {
+      new TagSystem("example@example", "1972-12-31");
+    });
+    assertThrows(() => {
+      new TagSystem("0.0.0.0", "1972-12-31");
+    });
+  });
+
+  it("[[Construct]] throws if provided an invalid date", () => {
+    assertThrows(() => {
+      new TagSystem("example", "1969");
+    });
+    assertThrows(() => {
+      new TagSystem("example", "1972-12-31T00:00:00Z");
+    });
+  });
+
+  describe("::Tag", () => {
+    let Tag;
+    let system;
+
+    beforeEach(() => {
+      system = new TagSystem("example", "1972-12-31");
+      Tag = system.Tag;
+    });
+
+    it("[[Get]] returns the same value every time", () => {
+      assertStrictEquals(Tag, system.Tag);
+    });
+
+    it("[[Call]] throws", () => {
+      assertThrows(() => {
+        Tag();
+      });
+    });
+
+    it("[[Construct]] returns a new Tag", () => {
+      assertStrictEquals(
+        Object.getPrototypeOf(new Tag()),
+        Tag.prototype,
+      );
+    });
+
+    it('[[Construct]] defaults the kind to "Tag"', () => {
+      assertStrictEquals(new Tag().kind, "Tag");
+    });
+
+    it("[[Construct]] correctly sets the tag kind", () => {
+      assertStrictEquals(
+        new Tag("RelationshipTag").kind,
+        "RelationshipTag",
+      );
+    });
+
+    it("[[Construct]] defaults the preferred label to the empty string", () => {
+      assertStrictEquals(new Tag().prefLabel, "");
+    });
+
+    it("[[Construct]] correctly sets the preferred label to a simple string", () => {
+      assertStrictEquals(
+        new Tag("RelationshipTag", "Shadow, Me").prefLabel,
+        "Shadow, Me",
+      );
+    });
+
+    it("[[Construct]] initializes tag identifiers to null", () => {
+      assertStrictEquals(
+        new Tag().identifier,
+        null,
+      );
+    });
+
+    it("[[Construct]] correctly sets the preferred label to a language‐tagged string", () => {
+      assertEquals(
+        {
+          ...new Tag("RelationshipTag", {
+            "@value": "Shadow, Me",
+            "@language": "en",
+          }).prefLabel,
+        },
+        { "@value": "Shadow, Me", "@language": "en" },
+      );
+    });
+
+    it("[[Construct]] throws if the tag kind is not recognized", () => {
+      assertThrows(() => {
+        new Tag("NotATag");
+      });
+    });
+
+    describe(".all", () => {
+      it("[[Call]] yields all the persisted tags", () => {
+        const tags = new Set(function* () {
+          let i = 0;
+          while (i++ < 5) {
+            // Generate 5 tags and remember their identifiers.
+            const tag = new Tag();
+            tag.persist();
+            yield tag.identifier;
+          }
+        }());
+        for (const tag of Tag.all()) {
+          assertStrictEquals(
+            Object.getPrototypeOf(tag),
+            Tag.prototype,
+          );
+        }
+        assertEquals(
+          new Set(Array.from(Tag.all(), (tag) => tag.identifier)),
+          tags,
+        );
+      });
+    });
+
+    describe(".fromIRI", () => {
+      it("[[Call]] returns the persisted tag with the given I·R·I", () => {
+        const tag = new Tag();
+        tag.persist();
+        const { identifier, iri } = tag;
+        const retrieved = Tag.fromIRI(iri);
+        assertStrictEquals(
+          Object.getPrototypeOf(retrieved),
+          Tag.prototype,
+        );
+        assertStrictEquals(retrieved.identifier, identifier);
+      });
+
+      it("[[Call]] returns null if no tag with the given I·R·I has been persisted", () => {
+        assertStrictEquals(
+          Tag.fromIRI(
+            `https://${system.authorityName}/tag:${system.taggingEntity}:000-0000`,
+          ),
+          null,
+        );
+      });
+
+      it("[[Call]] returns null if passed an invalid I·R·I", () => {
+        assertStrictEquals(Tag.fromIRI(`bad iri`), null);
+      });
+    });
+
+    describe(".fromIdentifier", () => {
+      it("[[Call]] returns the persisted tag with the given identifier", () => {
+        const tag = new Tag();
+        tag.persist();
+        const { identifier } = tag;
+        const retrieved = Tag.fromIdentifier(identifier);
+        assertStrictEquals(
+          Object.getPrototypeOf(retrieved),
+          Tag.prototype,
+        );
+        assertStrictEquals(retrieved.identifier, identifier);
+      });
+
+      it("[[Call]] returns null if no tag with the given identifier has been persisted", () => {
+        assertStrictEquals(Tag.fromIdentifier("000-0000"), null);
+      });
+
+      it("[[Call]] throws if passed an invalid identifier", () => {
+        assertThrows(() => {
+          Tag.fromIdentifier(""); // wrong format
+        });
+        assertThrows(() => {
+          Tag.fromIdentifier("100-0000"); // bad checksum
+        });
+      });
+    });
+
+    describe(".fromTagURI", () => {
+      it("[[Call]] returns the persisted tag with the given Tag U·R·I", () => {
+        const tag = new Tag();
+        tag.persist();
+        const { identifier, tagURI } = tag;
+        const retrieved = Tag.fromTagURI(tagURI);
+        assertStrictEquals(
+          Object.getPrototypeOf(retrieved),
+          Tag.prototype,
+        );
+        assertStrictEquals(retrieved.identifier, identifier);
+      });
+
+      it("[[Call]] returns null if no tag with the given Tag U·R·I has been persisted", () => {
+        assertStrictEquals(
+          Tag.fromIRI(`tag:${system.taggingEntity}:`),
+          null,
+        );
+        assertStrictEquals(
+          Tag.fromIRI(`tag:${system.taggingEntity}:000-0000`),
+          null,
+        );
+      });
+
+      it("[[Call]] throws if passed an invalid Tag U·R·I", () => {
+        assertThrows(() => {
+          Tag.fromTagURI(""); // wrong format
+        });
+        assertThrows(() => {
+          Tag.fromTagURI(
+            "tag:unexample,1970-01-01:Z", // incorrect tagging entity
+          );
+        });
+      });
+    });
+
+    describe(".getSystem", () => {
+      it("[[Has]] is not present", () => {
+        assertFalse("getSystem" in Tag);
+      });
+    });
+
+    describe(".identifiers", () => {
+      it("[[Call]] yields all the persisted identifiers", () => {
+        const tags = new Set(function* () {
+          let i = 0;
+          while (i++ < 5) {
+            // Generate 5 tags and remember their identifiers.
+            const tag = new Tag();
+            tag.persist();
+            yield tag.identifier;
+          }
+        }());
+        assertEquals(
+          new Set(Tag.identifiers()),
+          tags,
+        );
+      });
+    });
+
+    // `.[Storage.toInstance]` is tested by `.fromIdentifier`.
+
+    describe("::addAltLabel", () => {
+      it("[[Call]] does nothing if called with no arguments", () => {
+        const tag = new Tag();
+        tag.addAltLabel();
+        assertEquals([...tag.altLabels()], []);
+      });
+
+      it("[[Call]] adds the provided alternative labels", () => {
+        const tag = new Tag();
+        tag.addAltLabel(
+          "one",
+          { "@value": "two" },
+          { "@value": "three", "@language": "en" },
+        );
+        assertEquals(
+          Array.from(
+            tag.altLabels(),
+            ($) => typeof $ == "string" ? $ : { ...$ },
+          ),
+          [
+            "one",
+            "two",
+            { "@value": "three", "@language": "en" },
+          ],
+        );
+      });
+    });
+
+    describe("::addBroaderTag", () => {
+      it("[[Call]] does nothing if called with no arguments", () => {
+        const tag = new Tag();
+        tag.addBroaderTag();
+        assertEquals([...tag.broaderTags()], []);
+      });
+
+      it("[[Call]] adds the provided broader tags", () => {
+        const broader = new Tag();
+        broader.persist();
+        const broader2 = new Tag();
+        broader2.persist();
+        const tag = new Tag();
+        tag.addBroaderTag(broader, broader2);
+        assertEquals(
+          Array.from(tag.broaderTags(), ($) => $.identifier),
+          [broader.identifier, broader2.identifier],
+        );
+      });
+
+      it("[[Call]] throws when adding a non‐persisted tag", () => {
+        const tag = new Tag();
+        assertThrows(() => {
+          tag.addBroaderTag(new Tag());
+        });
+      });
+
+      it("[[Call]] throws when adding an unrecognized identifier", () => {
+        const tag = new Tag();
+        assertThrows(() => {
+          tag.addBroaderTag("000-0000"); // not persisted
+        });
+        assertThrows(() => {
+          tag.addBroaderTag(""); // bad format
+        });
+      });
+    });
+
+    describe("::addHiddenLabel", () => {
+      it("[[Call]] does nothing if called with no arguments", () => {
+        const tag = new Tag();
+        tag.addHiddenLabel();
+        assertEquals([...tag.hiddenLabels()], []);
+      });
+
+      it("[[Call]] adds the provided hidden labels", () => {
+        const tag = new Tag();
+        tag.addHiddenLabel(
+          "one",
+          { "@value": "two" },
+          { "@value": "three", "@language": "en" },
+        );
+        assertEquals(
+          Array.from(
+            tag.hiddenLabels(),
+            ($) => typeof $ == "string" ? $ : { ...$ },
+          ),
+          [
+            "one",
+            "two",
+            { "@value": "three", "@language": "en" },
+          ],
+        );
+      });
+    });
+
+    describe("::addInCanonTag", () => {
+      it("[[Call]] does nothing if called with no arguments", () => {
+        const tag = new Tag("EntityTag");
+        tag.addInCanonTag();
+        assertEquals([...tag.inCanonTags()], []);
+      });
+
+      it("[[Call]] adds the provided canon tags", () => {
+        const canon = new Tag("CanonTag");
+        canon.persist();
+        const canon2 = new Tag("CanonTag");
+        canon2.persist();
+        const tag = new Tag("EntityTag");
+        tag.addInCanonTag(canon, canon2);
+        assertEquals(
+          Array.from(tag.inCanonTags(), ($) => $.identifier),
+          [canon.identifier, canon2.identifier],
+        );
+      });
+
+      it("[[Call]] throws when this is not a tag which can be placed in canon", () => {
+        assertThrows(() => {
+          new Tag().addInCanonTag();
+        });
+      });
+
+      it("[[Call]] throws when provided with a non‐canon tag", () => {
+        const notCanon = new Tag();
+        notCanon.persist();
+        const tag = new Tag("EntityTag");
+        assertThrows(() => {
+          tag.addInCanonTag(notCanon);
+        });
+      });
+
+      it("[[Call]] throws when adding a non‐persisted tag", () => {
+        const tag = new Tag("EntityTag");
+        assertThrows(() => {
+          tag.addInCanonTag(new Tag("CanonTag"));
+        });
+      });
+
+      it("[[Call]] throws when adding an unrecognized identifier", () => {
+        const tag = new Tag("EntityTag");
+        assertThrows(() => {
+          tag.addInCanonTag("000-0000"); // not persisted
+        });
+        assertThrows(() => {
+          tag.addInCanonTag(""); // bad format
+        });
+      });
+    });
+
+    describe("::addInvolvesTag", () => {
+      it("[[Call]] does nothing if called with no arguments", () => {
+        const tag = new Tag("ConceptualTag");
+        tag.addInvolvesTag();
+        assertEquals([...tag.involvesTags()], []);
+      });
+
+      it("[[Call]] adds the provided tags", () => {
+        const involved = new Tag();
+        involved.persist();
+        const involved2 = new Tag();
+        involved2.persist();
+        const tag = new Tag("ConceptualTag");
+        tag.addInvolvesTag(involved, involved2);
+        assertEquals(
+          Array.from(tag.involvesTags(), ($) => $.identifier),
+          [involved.identifier, involved2.identifier],
+        );
+      });
+
+      it("[[Call]] throws when this is not a conceptual tag", () => {
+        assertThrows(() => {
+          new Tag().addInvolvesTag();
+        });
+      });
+
+      it("[[Call]] throws when this is a relationship tag and provided with a non‐involvable tag", () => {
+        const notInvolved = new Tag();
+        notInvolved.persist();
+        const tag = new Tag("RelationshipTag");
+        assertThrows(() => {
+          tag.addInvolvesTag(notInvolved);
+        });
+      });
+
+      it("[[Call]] throws when adding a non‐persisted tag", () => {
+        const tag = new Tag("ConceptualTag");
+        assertThrows(() => {
+          tag.addInvolvesTag(new Tag());
+        });
+      });
+
+      it("[[Call]] throws when adding an unrecognized identifier", () => {
+        const tag = new Tag("ConceptualTag");
+        assertThrows(() => {
+          tag.addInvolvesTag("000-0000"); // not persisted
+        });
+        assertThrows(() => {
+          tag.addInvolvesTag(""); // bad format
+        });
+      });
+    });
+
+    // `::altLabels` is tested by `::addAltLabel`.
+
+    describe("::authorityName", () => {
+      it("[[Get]] returns the authority name of the tag system", () => {
+        assertStrictEquals(
+          new Tag().authorityName,
+          system.authorityName,
+        );
+      });
+    });
+
+    // `::broaderTags` is tested by `::addBroaderTag`.
+
+    describe("::broaderTransitiveTags", () => {
+      it("[[Call]] returns broader tags transitively", () => {
+        const superBroad = new Tag();
+        superBroad.persist();
+        const broad = new Tag();
+        broad.addBroaderTag(superBroad);
+        broad.persist();
+        const tag = new Tag();
+        tag.addBroaderTag(broad);
+        assertEquals(
+          Array.from(tag.broaderTransitiveTags(), ($) => $.identifier),
+          [broad.identifier, superBroad.identifier],
+        );
+      });
+
+      it("[[Call]] cannot recurse infinitely", () => {
+        const tag = new Tag();
+        tag.persist();
+        const broad = new Tag();
+        broad.addBroaderTag(tag);
+        broad.persist();
+        tag.addBroaderTag(broad);
+        tag.persist();
+        assertEquals(
+          Array.from(tag.broaderTransitiveTags(), ($) => $.identifier),
+          [broad.identifier, tag.identifier],
+        );
+      });
+    });
+
+    describe("::deleteAltLabel", () => {
+      it("[[Call]] does nothing if called with no arguments", () => {
+        const tag = new Tag();
+        tag.addAltLabel("etaoin");
+        tag.deleteAltLabel();
+        assertEquals([...tag.altLabels()], ["etaoin"]);
+      });
+
+      it("[[Call]] deletes only the provided hidden labels", () => {
+        const tag = new Tag();
+        tag.addAltLabel(
+          "one",
+          "two",
+          { "@value": "three", "@language": "en" },
+          "four",
+        );
+        tag.deleteAltLabel(
+          "one",
+          { "@value": "two" },
+          { "@value": "three", "@language": "en" },
+          { "@value": "four", "@language": "en" },
+        );
+        assertEquals([...tag.altLabels()], ["four"]);
+      });
+    });
+
+    describe("::deleteBroaderTag", () => {
+      it("[[Call]] does nothing if called with no arguments", () => {
+        const broader = new Tag();
+        broader.persist();
+        const tag = new Tag();
+        tag.addBroaderTag(broader);
+        tag.deleteBroaderTag();
+        assertEquals(
+          Array.from(tag.broaderTags(), ($) => $.identifier),
+          [broader.identifier],
+        );
+      });
+
+      it("[[Call]] deletes only the provided broader tags", () => {
+        const superBroader = new Tag();
+        superBroader.persist();
+        const broader = new Tag();
+        broader.addBroaderTag(superBroader);
+        broader.persist();
+        const broader2 = new Tag();
+        broader2.addBroaderTag(superBroader);
+        broader2.persist();
+        const tag = new Tag();
+        tag.addBroaderTag(broader, broader2);
+        tag.deleteBroaderTag(broader, superBroader, "000-0000", "");
+        assertEquals(
+          Array.from(tag.broaderTags(), ($) => $.identifier),
+          [broader2.identifier],
+        );
+      });
+    });
+
+    describe("::deleteHiddenLabel", () => {
+      it("[[Call]] does nothing if called with no arguments", () => {
+        const tag = new Tag();
+        tag.addHiddenLabel("etaoin");
+        tag.deleteHiddenLabel();
+        assertEquals([...tag.hiddenLabels()], ["etaoin"]);
+      });
+
+      it("[[Call]] deletes only the provided alternative labels", () => {
+        const tag = new Tag();
+        tag.addHiddenLabel(
+          "one",
+          "two",
+          { "@value": "three", "@language": "en" },
+          "four",
+        );
+        tag.deleteHiddenLabel(
+          "one",
+          { "@value": "two" },
+          { "@value": "three", "@language": "en" },
+          { "@value": "four", "@language": "en" },
+        );
+        assertEquals([...tag.hiddenLabels()], ["four"]);
+      });
+    });
+
+    describe("::deleteInCanonTag", () => {
+      it("[[Call]] does nothing if called with no arguments", () => {
+        const canon = new Tag("CanonTag");
+        canon.persist();
+        const tag = new Tag("EntityTag");
+        tag.addInCanonTag(canon);
+        tag.deleteInCanonTag();
+        assertEquals(
+          Array.from(tag.inCanonTags(), ($) => $.identifier),
+          [canon.identifier],
+        );
+      });
+
+      it("[[Call]] deletes only the provided canon tags", () => {
+        const canon = new Tag("CanonTag");
+        canon.persist();
+        const canon2 = new Tag("CanonTag");
+        canon2.persist();
+        const tag = new Tag("EntityTag");
+        tag.addInCanonTag(canon, canon2);
+        tag.deleteInCanonTag(canon, "000-0000", "");
+        assertEquals(
+          Array.from(tag.inCanonTags(), ($) => $.identifier),
+          [canon2.identifier],
+        );
+      });
+    });
+
+    describe("::deleteInvolvesTag", () => {
+      it("[[Call]] does nothing if called with no arguments", () => {
+        const involved = new Tag();
+        involved.persist();
+        const tag = new Tag("ConceptualTag");
+        tag.addInvolvesTag(involved);
+        tag.deleteInvolvesTag();
+        assertEquals(
+          Array.from(tag.involvesTags(), ($) => $.identifier),
+          [involved.identifier],
+        );
+      });
+
+      it("[[Call]] deletes only the provided involved tags", () => {
+        const character = new Tag("CharacterTag");
+        character.persist();
+        const involved = new Tag("RelationshipTag");
+        involved.addInvolvesTag(character);
+        involved.persist();
+        const involved2 = new Tag("RelationshipTag");
+        involved2.addInvolvesTag(character);
+        involved2.persist();
+        const tag = new Tag("RelationshipTag");
+        tag.addInvolvesTag(involved, involved2);
+        tag.deleteInvolvesTag(involved, character, "000-0000", "");
+        assertEquals(
+          Array.from(tag.involvesTags(), ($) => $.identifier),
+          [involved2.identifier],
+        );
+      });
+    });
+
+    describe("::hasInCanonTags", () => {
+      it("[[Call]] yields the persisted tags which have this tag in canon", () => {
+        const canon = new Tag("CanonTag");
+        canon.persist();
+        const entity = new Tag("EntityTag");
+        entity.addInCanonTag(canon);
+        entity.persist();
+        const entity2 = new Tag("EntityTag");
+        entity2.addInCanonTag(canon);
+        entity2.persist();
+        const tag = Tag.fromIdentifier(canon.identifier); // reload
+        assertEquals(
+          Array.from(tag.hasInCanonTags(), ($) => $.identifier),
+          [entity.identifier, entity2.identifier],
+        );
+      });
+    });
+
+    // `::hiddenLabels` is tested by `::addHiddenLabel`.
+
+    // `::identifier` is tested by a `.fromIdentifier`.
+
+    // `::inCanonTags` is tested by `::addInCanonTag`.
+
+    describe("::involvedInTags", () => {
+      it("[[Call]] yields the persisted tags which involve this tag", () => {
+        const involved = new Tag();
+        involved.persist();
+        const conceptual = new Tag("ConceptualTag");
+        conceptual.addInvolvesTag(involved);
+        conceptual.persist();
+        const conceptual2 = new Tag("ConceptualTag");
+        conceptual2.addInvolvesTag(involved);
+        conceptual2.persist();
+        const tag = Tag.fromIdentifier(involved.identifier); // reload
+        assertEquals(
+          Array.from(tag.involvedInTags(), ($) => $.identifier),
+          [conceptual.identifier, conceptual2.identifier],
+        );
+      });
+    });
+
+    // `::involvesTags` is tested by `::addInvolvesTag`.
+
+    // `::iri` is tested by a `.fromIRI`.
+
+    // `::kind` is tested by the constructor.
+
+    describe("::narrowerTags", () => {
+      it("[[Call]] yields the persisted tags which are narrower than this tag", () => {
+        const broader = new Tag();
+        broader.persist();
+        const narrower = new Tag();
+        narrower.addBroaderTag(broader);
+        narrower.persist();
+        const narrower2 = new Tag();
+        narrower2.addBroaderTag(broader);
+        narrower2.persist();
+        const tag = Tag.fromIdentifier(broader.identifier); // reload
+        assertEquals(
+          Array.from(tag.narrowerTags(), ($) => $.identifier),
+          [narrower.identifier, narrower2.identifier],
+        );
+      });
+    });
+
+    describe("::narrowerTransitiveTags", () => {
+      it("[[Call]] returns narrower tags transitively", () => {
+        const broad = new Tag();
+        broad.persist();
+        const narrow = new Tag();
+        narrow.addBroaderTag(broad);
+        narrow.persist();
+        const superNarrow = new Tag();
+        superNarrow.addBroaderTag(narrow);
+        superNarrow.persist();
+        const tag = Tag.fromIdentifier(broad.identifier); // reload
+        assertEquals(
+          Array.from(
+            tag.narrowerTransitiveTags(),
+            ($) => $.identifier,
+          ),
+          [narrow.identifier, superNarrow.identifier],
+        );
+      });
+
+      it("[[Call]] cannot recurse infinitely", () => {
+        const tag = new Tag();
+        tag.persist();
+        const broad = new Tag();
+        broad.addBroaderTag(tag);
+        broad.persist();
+        tag.addBroaderTag(broad);
+        tag.persist();
+        assertEquals(
+          Array.from(tag.broaderTransitiveTags(), ($) => $.identifier),
+          [broad.identifier, tag.identifier],
+        );
+      });
+    });
+
+    describe("::persist", () => {
+      it("[[Call]] returns an object with expected properties if there were changes", () => {
+        const tag = new Tag();
+        const activity = tag.persist();
+        assertObjectMatch(
+          activity,
+          {
+            "@context":
+              "https://ns.1024.gdn/Tagging/discovery.context.jsonld",
+            context: system.iri,
+            object: tag.iri,
+          },
+        );
+        assertArrayIncludes(activity["@type"], ["TagActivity"]);
+        assert("endTime" in activity);
+      });
+
+      it("[[Call]] returns a Create activity with a type predicate for new objects", () => {
+        const activity = new Tag().persist();
+        assertEquals(activity["@type"], ["TagActivity", "Create"]);
+        assertArrayIncludes(activity.states, [{
+          predicate: "a",
+          object: "Tag",
+        }]);
+      });
+
+      it("[[Call]] returns an Update activity for old objects", () => {
+        const tag = new Tag();
+        tag.persist();
+        tag.prefLabel = "etaoin";
+        const activity = tag.persist();
+        assertEquals(activity["@type"], ["TagActivity", "Update"]);
+      });
+
+      it("[[Call]] states and unstates changes", () => {
+        const broader1 = new Tag();
+        broader1.persist();
+        const broader2 = new Tag();
+        broader2.persist();
+        const tag = new Tag();
+        tag.addBroaderTag(broader1);
+        tag.persist();
+        tag.prefLabel = "etaoin";
+        tag.deleteBroaderTag(broader1);
+        tag.addBroaderTag(broader2);
+        const activity = tag.persist();
+        assertObjectMatch(activity, {
+          unstates: [
+            { predicate: "prefLabel", object: { "@value": "" } },
+            { predicate: "broader", object: broader1.iri },
+          ],
+          states: [
+            { predicate: "prefLabel", object: { "@value": "etaoin" } },
+            { predicate: "broader", object: broader2.iri },
+          ],
+        });
+      });
+
+      it("[[Call]] doesn’t state if there are no additions", () => {
+        const tag = new Tag();
+        tag.addAltLabel("etaoin");
+        tag.persist();
+        tag.deleteAltLabel("etaoin");
+        const activity = tag.persist();
+        assertFalse("state" in activity);
+      });
+
+      it("[[Call]] doesn’t unstate if there are no removals", () => {
+        const tag = new Tag();
+        tag.persist();
+        tag.addAltLabel("etaoin");
+        const activity = tag.persist();
+        assertFalse("unstate" in activity);
+      });
+
+      it("[[Call]] returns null if no meaningful changes were made", () => {
+        const tag = new Tag();
+        tag.persist();
+        const activity = tag.persist();
+        assertStrictEquals(activity, null);
+      });
+    });
+
+    describe("::prefLabel", () => {
+      it("[[Set]] sets the preferred label", () => {
+        const tag = new Tag();
+        tag.prefLabel = "one";
+        assertStrictEquals(tag.prefLabel, "one");
+        tag.prefLabel = { "@value": "two" };
+        assertStrictEquals(tag.prefLabel, "two");
+        tag.prefLabel = { "@value": "three", "@language": "en" };
+        assertEquals(
+          { ...tag.prefLabel },
+          { "@value": "three", "@language": "en" },
+        );
+      });
+    });
+
+    // `::tagURI` is tested by a `.fromTagURI`.
+
+    describe("::taggingEntity", () => {
+      it("[[Get]] returns the tagging entity of the tag system", () => {
+        assertStrictEquals(
+          new Tag().taggingEntity,
+          system.taggingEntity,
+        );
+      });
+    });
+
+    describe("::toString", () => {
+      it("[[Get]] returns the string value of the preferred label", () => {
+        const tag = new Tag();
+        tag.prefLabel = { "@value": "etaoin", "@language": "zxx" };
+        assertStrictEquals(tag.toString(), "etaoin");
+      });
+    });
+
+    // `::[Storage.toObject]` is tested by `::persist`.
+  });
+
+  describe("::authorityName", () => {
+    it("[[Get]] returns the authority name", () => {
+      const system = new TagSystem("etaoin.example", "1972-12-31");
+      assertStrictEquals(system.authorityName, "etaoin.example");
+    });
+  });
+
+  describe("::authorityName", () => {
+    it("[[Get]] returns the date", () => {
+      const system = new TagSystem("etaoin.example", "1972-12-31");
+      assertStrictEquals(system.date, "1972-12-31");
+    });
+  });
+
+  describe("::identifiers", () => {
+    it("[[Get]] yields the extant entities", () => {
+      const system = new TagSystem("etaoin.example", "1972-12-31");
+      const tags = new Set(function* () {
+        let i = 0;
+        while (i++ < 5) {
+          // Generate 5 tags and remember their identifiers.
+          const tag = new system.Tag();
+          tag.persist();
+          yield tag.identifier;
+        }
+      }());
+      assertEquals(
+        new Set(Array.from(system.entities(), ($) => $.identifier)),
+        tags,
+      );
+    });
+  });
+
+  describe("::identifier", () => {
+    it("[[Get]] returns the identifier", () => {
+      const system = new TagSystem("etaoin.example", "1972-12-31");
+      assertStrictEquals(system.identifier, "");
+      const system2 = new TagSystem(
+        "etaoin.example",
+        "1972-12-31",
+        "etaoin",
+      );
+      assertStrictEquals(system2.identifier, "etaoin");
+    });
+  });
+
+  describe("::identifiers", () => {
+    it("[[Get]] yields the identifiers in use", () => {
+      const system = new TagSystem("etaoin.example", "1972-12-31");
+      const tags = new Set(function* () {
+        let i = 0;
+        while (i++ < 5) {
+          // Generate 5 tags and remember their identifiers.
+          const tag = new system.Tag();
+          tag.persist();
+          yield tag.identifier;
+        }
+      }());
+      assertEquals(new Set(system.identifiers()), tags);
+    });
+  });
+
+  describe("::iri", () => {
+    it("[[Get]] returns the I·R·I", () => {
+      const system = new TagSystem("etaoin.example", "1972-12-31");
+      assertStrictEquals(
+        system.iri,
+        "https://etaoin.example/tag:etaoin.example,1972-12-31:",
+      );
+      const system2 = new TagSystem(
+        "etaoin.example",
+        "1972-12-31",
+        "etaoin",
+      );
+      assertStrictEquals(
+        system2.iri,
+        "https://etaoin.example/tag:etaoin.example,1972-12-31:etaoin",
+      );
+    });
+  });
+
+  describe("::tagURI", () => {
+    it("[[Get]] returns the Tag U·R·I", () => {
+      const system = new TagSystem("etaoin.example", "1972-12-31");
+      assertStrictEquals(
+        system.tagURI,
+        "tag:etaoin.example,1972-12-31:",
+      );
+      const system2 = new TagSystem(
+        "etaoin.example",
+        "1972-12-31",
+        "etaoin",
+      );
+      assertStrictEquals(
+        system2.tagURI,
+        "tag:etaoin.example,1972-12-31:etaoin",
+      );
+    });
+  });
+
+  describe("::taggingEntity", () => {
+    it("[[Get]] returns the tagging entity", () => {
+      const system = new TagSystem("etaoin.example", "1972-12-31");
+      assertStrictEquals(
+        system.taggingEntity,
+        "etaoin.example,1972-12-31",
+      );
+    });
+  });
+});
This page took 0.420118 seconds and 4 git commands to generate.