X-Git-Url: https://git.ladys.computer/Etiquette/blobdiff_plain/9f6c704b8d1ebe70e97b0d934d04c71e8ae0ed5f..6623cbf39b85515219a3ab012e27d97884ea8aa3:/model.test.js diff --git a/model.test.js b/model.test.js new file mode 100644 index 0000000..7944238 --- /dev/null +++ b/model.test.js @@ -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 . + +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", + ); + }); + }); +});