--- /dev/null
+// 📧🏷️ É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",
+ );
+ });
+ });
+});