]> Lady’s Gitweb - Etiquette/blob - model.test.js
Enable application of activities onto tag systems
[Etiquette] / model.test.js
1 // 📧🏷️ Étiquette ∷ model.test.js
2 // ====================================================================
3 //
4 // Copyright © 2023 Lady [@ Lady’s Computer].
5 //
6 // This Source Code Form is subject to the terms of the Mozilla Public
7 // License, v. 2.0. If a copy of the MPL was not distributed with this
8 // file, You can obtain one at <https://mozilla.org/MPL/2.0/>.
9
10 import {
11 assert,
12 assertArrayIncludes,
13 assertEquals,
14 assertFalse,
15 assertObjectMatch,
16 assertStrictEquals,
17 assertThrows,
18 beforeEach,
19 describe,
20 it,
21 } from "./dev-deps.js";
22 import { TagSystem } from "./model.js";
23
24 describe("TagSystem", () => {
25 it("[[Call]] throws", () => {
26 assertThrows(() => {
27 TagSystem();
28 });
29 });
30
31 it("[[Construct]] creates a new TagSystem", () => {
32 assertStrictEquals(
33 Object.getPrototypeOf(new TagSystem("example", "1972-12-31")),
34 TagSystem.prototype,
35 );
36 });
37
38 it("[[Construct]] uses the identifier if provided", () => {
39 assertStrictEquals(
40 new TagSystem("example", "1972-12-31", "etaoin").identifier,
41 "etaoin",
42 );
43 });
44
45 it("[[Construct]] uses an empty identifier if none is provided", () => {
46 assertStrictEquals(
47 new TagSystem("example", "1972-12-31").identifier,
48 "",
49 );
50 });
51
52 it("[[Construct]] throws if provided an invalid domain", () => {
53 assertThrows(() => {
54 new TagSystem("example@example", "1972-12-31");
55 });
56 assertThrows(() => {
57 new TagSystem("0.0.0.0", "1972-12-31");
58 });
59 });
60
61 it("[[Construct]] throws if provided an invalid date", () => {
62 assertThrows(() => {
63 new TagSystem("example", "1969");
64 });
65 assertThrows(() => {
66 new TagSystem("example", "1972-12-31T00:00:00Z");
67 });
68 });
69
70 describe("::Tag", () => {
71 let Tag;
72 let system;
73
74 beforeEach(() => {
75 system = new TagSystem("example", "1972-12-31");
76 Tag = system.Tag;
77 });
78
79 it("[[Get]] returns the same value every time", () => {
80 assertStrictEquals(Tag, system.Tag);
81 });
82
83 it("[[Call]] throws", () => {
84 assertThrows(() => {
85 Tag();
86 });
87 });
88
89 it("[[Construct]] returns a new Tag", () => {
90 assertStrictEquals(
91 Object.getPrototypeOf(new Tag()),
92 Tag.prototype,
93 );
94 });
95
96 it('[[Construct]] defaults the kind to "Tag"', () => {
97 assertStrictEquals(new Tag().kind, "Tag");
98 });
99
100 it("[[Construct]] correctly sets the tag kind", () => {
101 assertStrictEquals(
102 new Tag("RelationshipTag").kind,
103 "RelationshipTag",
104 );
105 });
106
107 it("[[Construct]] defaults the preferred label to the empty string", () => {
108 assertEquals({ ...new Tag().prefLabel }, { "@value": "" });
109 });
110
111 it("[[Construct]] correctly sets the preferred label to a simple string", () => {
112 assertEquals(
113 { ...new Tag("RelationshipTag", "Shadow, Me").prefLabel },
114 { "@value": "Shadow, Me" },
115 );
116 });
117
118 it("[[Construct]] initializes tag identifiers to null", () => {
119 assertStrictEquals(
120 new Tag().identifier,
121 null,
122 );
123 });
124
125 it("[[Construct]] correctly sets the preferred label to a language‐tagged string", () => {
126 assertEquals(
127 {
128 ...new Tag("RelationshipTag", {
129 "@value": "Shadow, Me",
130 "@language": "en",
131 }).prefLabel,
132 },
133 { "@value": "Shadow, Me", "@language": "en" },
134 );
135 });
136
137 it("[[Construct]] throws if the tag kind is not recognized", () => {
138 assertThrows(() => {
139 new Tag("NotATag");
140 });
141 });
142
143 describe(".all", () => {
144 it("[[Call]] yields all the persisted tags", () => {
145 const tags = new Set(function* () {
146 let i = 0;
147 while (i++ < 5) {
148 // Generate 5 tags and remember their identifiers.
149 const tag = new Tag();
150 tag.persist();
151 yield tag.identifier;
152 }
153 }());
154 for (const tag of Tag.all()) {
155 assertStrictEquals(
156 Object.getPrototypeOf(tag),
157 Tag.prototype,
158 );
159 }
160 assertEquals(
161 new Set(Array.from(Tag.all(), (tag) => tag.identifier)),
162 tags,
163 );
164 });
165 });
166
167 describe(".constructor", () => {
168 it("[[Get]] is `Function`", () => {
169 assertStrictEquals(Tag.constructor, Function);
170 });
171 });
172
173 describe(".fromIRI", () => {
174 it("[[Call]] returns the persisted tag with the given I·R·I", () => {
175 const tag = new Tag();
176 tag.persist();
177 const { identifier, iri } = tag;
178 const retrieved = Tag.fromIRI(iri);
179 assertStrictEquals(
180 Object.getPrototypeOf(retrieved),
181 Tag.prototype,
182 );
183 assertStrictEquals(retrieved.identifier, identifier);
184 });
185
186 it("[[Call]] returns undefined if no tag with the given I·R·I has been persisted", () => {
187 assertStrictEquals(
188 Tag.fromIRI(
189 `https://${system.authorityName}/tag:${system.taggingEntity}:000-0000`,
190 ),
191 undefined,
192 );
193 });
194
195 it("[[Call]] throws if passed an invalid I·R·I", () => {
196 assertThrows(() => {
197 Tag.fromIRI(`bad iri`);
198 });
199 });
200 });
201
202 describe(".fromIdentifier", () => {
203 it("[[Call]] returns the persisted tag with the given identifier", () => {
204 const tag = new Tag();
205 tag.persist();
206 const { identifier } = tag;
207 const retrieved = Tag.fromIdentifier(identifier);
208 assertStrictEquals(
209 Object.getPrototypeOf(retrieved),
210 Tag.prototype,
211 );
212 assertStrictEquals(retrieved.identifier, identifier);
213 });
214
215 it("[[Call]] returns undefined if no tag with the given identifier has been persisted", () => {
216 assertStrictEquals(Tag.fromIdentifier("000-0000"), undefined);
217 });
218
219 it("[[Call]] throws if passed an invalid identifier", () => {
220 assertThrows(() => {
221 Tag.fromIdentifier(""); // wrong format
222 });
223 assertThrows(() => {
224 Tag.fromIdentifier("100-0000"); // bad checksum
225 });
226 });
227 });
228
229 describe(".fromTagURI", () => {
230 it("[[Call]] returns the persisted tag with the given Tag U·R·I", () => {
231 const tag = new Tag();
232 tag.persist();
233 const { identifier, tagURI } = tag;
234 const retrieved = Tag.fromTagURI(tagURI);
235 assertStrictEquals(
236 Object.getPrototypeOf(retrieved),
237 Tag.prototype,
238 );
239 assertStrictEquals(retrieved.identifier, identifier);
240 });
241
242 it("[[Call]] returns undefined if no tag with the given Tag U·R·I has been persisted", () => {
243 assertStrictEquals(
244 Tag.fromTagURI(`tag:${system.taggingEntity}:`),
245 undefined,
246 );
247 assertStrictEquals(
248 Tag.fromTagURI(`tag:${system.taggingEntity}:000-0000`),
249 undefined,
250 );
251 });
252
253 it("[[Call]] throws if passed an invalid Tag U·R·I", () => {
254 assertThrows(() => {
255 Tag.fromTagURI(""); // wrong format
256 });
257 assertThrows(() => {
258 Tag.fromTagURI(
259 "tag:unexample,1970-01-01:Z", // incorrect tagging entity
260 );
261 });
262 });
263 });
264
265 describe(".identifiers", () => {
266 it("[[Call]] yields all the persisted identifiers", () => {
267 const tags = new Set(function* () {
268 let i = 0;
269 while (i++ < 5) {
270 // Generate 5 tags and remember their identifiers.
271 const tag = new Tag();
272 tag.persist();
273 yield tag.identifier;
274 }
275 }());
276 assertEquals(
277 new Set(Tag.identifiers()),
278 tags,
279 );
280 });
281 });
282
283 describe(".system", () => {
284 it("[[Call]] returns the `TagSystem`", () => {
285 assertEquals(Tag.system, system);
286 });
287 });
288
289 // `.[Storage.toInstance]` is tested by `.fromIdentifier`.
290
291 describe("::addAltLabel", () => {
292 it("[[Call]] does nothing if called with no arguments", () => {
293 const tag = new Tag();
294 tag.addAltLabel();
295 assertEquals([...tag.altLabels()], []);
296 });
297
298 it("[[Call]] adds the provided alternative labels", () => {
299 const tag = new Tag();
300 tag.addAltLabel(
301 "one",
302 { "@value": "two" },
303 { "@value": "three", "@language": "en" },
304 );
305 assertEquals(
306 Array.from(tag.altLabels(), ($) => ({ ...$ })),
307 [
308 { "@value": "one" },
309 { "@value": "two" },
310 { "@value": "three", "@language": "en" },
311 ],
312 );
313 });
314
315 it("[[Call]] returns this", () => {
316 const tag = new Tag();
317 assertStrictEquals(tag.addAltLabel(), tag);
318 });
319 });
320
321 describe("::addBroaderTag", () => {
322 it("[[Call]] does nothing if called with no arguments", () => {
323 const tag = new Tag();
324 tag.addBroaderTag();
325 assertEquals([...tag.broaderTags()], []);
326 });
327
328 it("[[Call]] adds the provided broader tags", () => {
329 const broader = new Tag();
330 broader.persist();
331 const broader2 = new Tag();
332 broader2.persist();
333 const tag = new Tag();
334 tag.addBroaderTag(broader, broader2);
335 assertEquals(
336 Array.from(tag.broaderTags(), ($) => $.identifier),
337 [broader.identifier, broader2.identifier],
338 );
339 });
340
341 it("[[Call]] returns this", () => {
342 const tag = new Tag();
343 assertStrictEquals(tag.addBroaderTag(), tag);
344 });
345
346 it("[[Call]] throws when adding a non‐persisted tag", () => {
347 const tag = new Tag();
348 assertThrows(() => {
349 tag.addBroaderTag(new Tag());
350 });
351 });
352
353 it("[[Call]] throws when adding an unrecognized identifier", () => {
354 const tag = new Tag();
355 assertThrows(() => {
356 tag.addBroaderTag("000-0000"); // not persisted
357 });
358 assertThrows(() => {
359 tag.addBroaderTag(""); // bad format
360 });
361 });
362 });
363
364 describe("::addHiddenLabel", () => {
365 it("[[Call]] does nothing if called with no arguments", () => {
366 const tag = new Tag();
367 tag.addHiddenLabel();
368 assertEquals([...tag.hiddenLabels()], []);
369 });
370
371 it("[[Call]] adds the provided hidden labels", () => {
372 const tag = new Tag();
373 tag.addHiddenLabel(
374 "one",
375 { "@value": "two" },
376 { "@value": "three", "@language": "en" },
377 );
378 assertEquals(
379 Array.from(tag.hiddenLabels(), ($) => ({ ...$ })),
380 [
381 { "@value": "one" },
382 { "@value": "two" },
383 { "@value": "three", "@language": "en" },
384 ],
385 );
386 });
387
388 it("[[Call]] returns this", () => {
389 const tag = new Tag();
390 assertStrictEquals(tag.addHiddenLabel(), tag);
391 });
392 });
393
394 describe("::addInCanonTag", () => {
395 it("[[Call]] does nothing if called with no arguments", () => {
396 const tag = new Tag("EntityTag");
397 tag.addInCanonTag();
398 assertEquals([...tag.inCanonTags()], []);
399 });
400
401 it("[[Call]] adds the provided canon tags", () => {
402 const canon = new Tag("CanonTag");
403 canon.persist();
404 const canon2 = new Tag("CanonTag");
405 canon2.persist();
406 const tag = new Tag("EntityTag");
407 tag.addInCanonTag(canon, canon2);
408 assertEquals(
409 Array.from(tag.inCanonTags(), ($) => $.identifier),
410 [canon.identifier, canon2.identifier],
411 );
412 });
413
414 it("[[Call]] returns this", () => {
415 const tag = new Tag("EntityTag");
416 assertStrictEquals(tag.addInCanonTag(), tag);
417 });
418
419 it("[[Call]] throws when this is not a tag which can be placed in canon", () => {
420 const canon = new Tag("CanonTag");
421 canon.persist();
422 assertThrows(() => {
423 new Tag().addInCanonTag(canon);
424 });
425 });
426
427 it("[[Call]] throws when provided with a non‐canon tag", () => {
428 const notCanon = new Tag();
429 notCanon.persist();
430 const tag = new Tag("EntityTag");
431 assertThrows(() => {
432 tag.addInCanonTag(notCanon);
433 });
434 });
435
436 it("[[Call]] throws when adding a non‐persisted tag", () => {
437 const tag = new Tag("EntityTag");
438 assertThrows(() => {
439 tag.addInCanonTag(new Tag("CanonTag"));
440 });
441 });
442
443 it("[[Call]] throws when adding an unrecognized identifier", () => {
444 const tag = new Tag("EntityTag");
445 assertThrows(() => {
446 tag.addInCanonTag("000-0000"); // not persisted
447 });
448 assertThrows(() => {
449 tag.addInCanonTag(""); // bad format
450 });
451 });
452 });
453
454 describe("::addInvolvesTag", () => {
455 it("[[Call]] does nothing if called with no arguments", () => {
456 const tag = new Tag("ConceptualTag");
457 tag.addInvolvesTag();
458 assertEquals([...tag.involvesTags()], []);
459 });
460
461 it("[[Call]] adds the provided tags", () => {
462 const involved = new Tag();
463 involved.persist();
464 const involved2 = new Tag();
465 involved2.persist();
466 const tag = new Tag("ConceptualTag");
467 tag.addInvolvesTag(involved, involved2);
468 assertEquals(
469 Array.from(tag.involvesTags(), ($) => $.identifier),
470 [involved.identifier, involved2.identifier],
471 );
472 });
473
474 it("[[Call]] returns this", () => {
475 const tag = new Tag("ConceptualTag");
476 assertStrictEquals(tag.addInvolvesTag(), tag);
477 });
478
479 it("[[Call]] throws when this is not a conceptual tag", () => {
480 const involved = new Tag();
481 involved.persist();
482 assertThrows(() => {
483 new Tag().addInvolvesTag(involved);
484 });
485 });
486
487 it("[[Call]] throws when this is a relationship tag and provided with a non‐involvable tag", () => {
488 const notInvolved = new Tag();
489 notInvolved.persist();
490 const tag = new Tag("RelationshipTag");
491 assertThrows(() => {
492 tag.addInvolvesTag(notInvolved);
493 });
494 });
495
496 it("[[Call]] throws when adding a non‐persisted tag", () => {
497 const tag = new Tag("ConceptualTag");
498 assertThrows(() => {
499 tag.addInvolvesTag(new Tag());
500 });
501 });
502
503 it("[[Call]] throws when adding an unrecognized identifier", () => {
504 const tag = new Tag("ConceptualTag");
505 assertThrows(() => {
506 tag.addInvolvesTag("000-0000"); // not persisted
507 });
508 assertThrows(() => {
509 tag.addInvolvesTag(""); // bad format
510 });
511 });
512 });
513
514 // `::altLabels` is tested by `::addAltLabel`.
515
516 describe("::authorityName", () => {
517 it("[[Get]] returns the authority name of the tag system", () => {
518 assertStrictEquals(
519 new Tag().authorityName,
520 system.authorityName,
521 );
522 });
523 });
524
525 // `::broaderTags` is tested by `::addBroaderTag`.
526
527 describe("::broaderTransitiveTags", () => {
528 it("[[Call]] returns broader tags transitively", () => {
529 const superBroad = new Tag();
530 superBroad.persist();
531 const broad = new Tag();
532 broad.addBroaderTag(superBroad);
533 broad.persist();
534 const tag = new Tag();
535 tag.addBroaderTag(broad);
536 assertEquals(
537 Array.from(tag.broaderTransitiveTags(), ($) => $.identifier),
538 [broad.identifier, superBroad.identifier],
539 );
540 });
541
542 it("[[Call]] cannot recurse infinitely", () => {
543 const tag = new Tag();
544 tag.persist();
545 const broad = new Tag();
546 broad.addBroaderTag(tag);
547 broad.persist();
548 tag.addBroaderTag(broad);
549 tag.persist();
550 assertEquals(
551 Array.from(tag.broaderTransitiveTags(), ($) => $.identifier),
552 [broad.identifier, tag.identifier],
553 );
554 });
555 });
556
557 describe("::deleteAltLabel", () => {
558 it("[[Call]] does nothing if called with no arguments", () => {
559 const tag = new Tag();
560 tag.addAltLabel("etaoin");
561 tag.deleteAltLabel();
562 assertEquals(
563 Array.from(tag.altLabels(), ($) => ({ ...$ })),
564 [{ "@value": "etaoin" }],
565 );
566 });
567
568 it("[[Call]] deletes only the provided hidden labels", () => {
569 const tag = new Tag();
570 tag.addAltLabel(
571 "one",
572 "two",
573 { "@value": "three", "@language": "en" },
574 "four",
575 );
576 tag.deleteAltLabel(
577 "one",
578 { "@value": "two" },
579 { "@value": "three", "@language": "en" },
580 { "@value": "four", "@language": "en" },
581 );
582 assertEquals(
583 Array.from(tag.altLabels(), ($) => ({ ...$ })),
584 [{ "@value": "four" }],
585 );
586 });
587
588 it("[[Call]] returns this", () => {
589 const tag = new Tag();
590 assertStrictEquals(tag.deleteAltLabel(), tag);
591 });
592 });
593
594 describe("::deleteBroaderTag", () => {
595 it("[[Call]] does nothing if called with no arguments", () => {
596 const broader = new Tag();
597 broader.persist();
598 const tag = new Tag();
599 tag.addBroaderTag(broader);
600 tag.deleteBroaderTag();
601 assertEquals(
602 Array.from(tag.broaderTags(), ($) => $.identifier),
603 [broader.identifier],
604 );
605 });
606
607 it("[[Call]] deletes only the provided broader tags", () => {
608 const superBroader = new Tag();
609 superBroader.persist();
610 const broader = new Tag();
611 broader.addBroaderTag(superBroader);
612 broader.persist();
613 const broader2 = new Tag();
614 broader2.addBroaderTag(superBroader);
615 broader2.persist();
616 const tag = new Tag();
617 tag.addBroaderTag(broader, broader2);
618 tag.deleteBroaderTag(broader, superBroader, "000-0000", "");
619 assertEquals(
620 Array.from(tag.broaderTags(), ($) => $.identifier),
621 [broader2.identifier],
622 );
623 });
624
625 it("[[Call]] returns this", () => {
626 const tag = new Tag();
627 assertStrictEquals(tag.deleteBroaderTag(), tag);
628 });
629 });
630
631 describe("::deleteHiddenLabel", () => {
632 it("[[Call]] does nothing if called with no arguments", () => {
633 const tag = new Tag();
634 tag.addHiddenLabel("etaoin");
635 tag.deleteHiddenLabel();
636 assertEquals(
637 Array.from(tag.hiddenLabels(), ($) => ({ ...$ })),
638 [{ "@value": "etaoin" }],
639 );
640 });
641
642 it("[[Call]] deletes only the provided alternative labels", () => {
643 const tag = new Tag();
644 tag.addHiddenLabel(
645 "one",
646 "two",
647 { "@value": "three", "@language": "en" },
648 "four",
649 );
650 tag.deleteHiddenLabel(
651 "one",
652 { "@value": "two" },
653 { "@value": "three", "@language": "en" },
654 { "@value": "four", "@language": "en" },
655 );
656 assertEquals(
657 Array.from(tag.hiddenLabels(), ($) => ({ ...$ })),
658 [{ "@value": "four" }],
659 );
660 });
661
662 it("[[Call]] returns this", () => {
663 const tag = new Tag();
664 assertStrictEquals(tag.deleteHiddenLabel(), tag);
665 });
666 });
667
668 describe("::deleteInCanonTag", () => {
669 it("[[Call]] does nothing if called with no arguments", () => {
670 const canon = new Tag("CanonTag");
671 canon.persist();
672 const tag = new Tag("EntityTag");
673 tag.addInCanonTag(canon);
674 tag.deleteInCanonTag();
675 assertEquals(
676 Array.from(tag.inCanonTags(), ($) => $.identifier),
677 [canon.identifier],
678 );
679 });
680
681 it("[[Call]] deletes only the provided canon tags", () => {
682 const canon = new Tag("CanonTag");
683 canon.persist();
684 const canon2 = new Tag("CanonTag");
685 canon2.persist();
686 const tag = new Tag("EntityTag");
687 tag.addInCanonTag(canon, canon2);
688 tag.deleteInCanonTag(canon, "000-0000", "");
689 assertEquals(
690 Array.from(tag.inCanonTags(), ($) => $.identifier),
691 [canon2.identifier],
692 );
693 });
694
695 it("[[Call]] returns this", () => {
696 const tag = new Tag("EntityTag");
697 assertStrictEquals(tag.deleteInCanonTag(), tag);
698 });
699 });
700
701 describe("::deleteInvolvesTag", () => {
702 it("[[Call]] does nothing if called with no arguments", () => {
703 const involved = new Tag();
704 involved.persist();
705 const tag = new Tag("ConceptualTag");
706 tag.addInvolvesTag(involved);
707 tag.deleteInvolvesTag();
708 assertEquals(
709 Array.from(tag.involvesTags(), ($) => $.identifier),
710 [involved.identifier],
711 );
712 });
713
714 it("[[Call]] deletes only the provided involved tags", () => {
715 const character = new Tag("CharacterTag");
716 character.persist();
717 const involved = new Tag("RelationshipTag");
718 involved.addInvolvesTag(character);
719 involved.persist();
720 const involved2 = new Tag("RelationshipTag");
721 involved2.addInvolvesTag(character);
722 involved2.persist();
723 const tag = new Tag("RelationshipTag");
724 tag.addInvolvesTag(involved, involved2);
725 tag.deleteInvolvesTag(involved, character, "000-0000", "");
726 assertEquals(
727 Array.from(tag.involvesTags(), ($) => $.identifier),
728 [involved2.identifier],
729 );
730 });
731
732 it("[[Call]] returns this", () => {
733 const tag = new Tag("ConceptualTag");
734 assertStrictEquals(tag.deleteInvolvesTag(), tag);
735 });
736 });
737
738 describe("::hasInCanonTags", () => {
739 it("[[Call]] yields the persisted tags which have this tag in canon", () => {
740 const canon = new Tag("CanonTag");
741 canon.persist();
742 const entity = new Tag("EntityTag");
743 entity.addInCanonTag(canon);
744 entity.persist();
745 const entity2 = new Tag("EntityTag");
746 entity2.addInCanonTag(canon);
747 entity2.persist();
748 const tag = Tag.fromIdentifier(canon.identifier); // reload
749 assertEquals(
750 Array.from(tag.hasInCanonTags(), ($) => $.identifier),
751 [entity.identifier, entity2.identifier],
752 );
753 });
754 });
755
756 // `::hiddenLabels` is tested by `::addHiddenLabel`.
757
758 // `::identifier` is tested by a `.fromIdentifier`.
759
760 // `::inCanonTags` is tested by `::addInCanonTag`.
761
762 describe("::involvedInTags", () => {
763 it("[[Call]] yields the persisted tags which involve this tag", () => {
764 const involved = new Tag();
765 involved.persist();
766 const conceptual = new Tag("ConceptualTag");
767 conceptual.addInvolvesTag(involved);
768 conceptual.persist();
769 const conceptual2 = new Tag("ConceptualTag");
770 conceptual2.addInvolvesTag(involved);
771 conceptual2.persist();
772 const tag = Tag.fromIdentifier(involved.identifier); // reload
773 assertEquals(
774 Array.from(tag.involvedInTags(), ($) => $.identifier),
775 [conceptual.identifier, conceptual2.identifier],
776 );
777 });
778 });
779
780 // `::involvesTags` is tested by `::addInvolvesTag`.
781
782 // `::iri` is tested by a `.fromIRI`.
783
784 // `::iriSpace` is tested by a `.fromIRI`.
785
786 // `::kind` is tested by the constructor.
787
788 describe("::narrowerTags", () => {
789 it("[[Call]] yields the persisted tags which are narrower than this tag", () => {
790 const broader = new Tag();
791 broader.persist();
792 const narrower = new Tag();
793 narrower.addBroaderTag(broader);
794 narrower.persist();
795 const narrower2 = new Tag();
796 narrower2.addBroaderTag(broader);
797 narrower2.persist();
798 const tag = Tag.fromIdentifier(broader.identifier); // reload
799 assertEquals(
800 Array.from(tag.narrowerTags(), ($) => $.identifier),
801 [narrower.identifier, narrower2.identifier],
802 );
803 });
804 });
805
806 describe("::narrowerTransitiveTags", () => {
807 it("[[Call]] returns narrower tags transitively", () => {
808 const broad = new Tag();
809 broad.persist();
810 const narrow = new Tag();
811 narrow.addBroaderTag(broad);
812 narrow.persist();
813 const superNarrow = new Tag();
814 superNarrow.addBroaderTag(narrow);
815 superNarrow.persist();
816 const tag = Tag.fromIdentifier(broad.identifier); // reload
817 assertEquals(
818 Array.from(
819 tag.narrowerTransitiveTags(),
820 ($) => $.identifier,
821 ),
822 [narrow.identifier, superNarrow.identifier],
823 );
824 });
825
826 it("[[Call]] cannot recurse infinitely", () => {
827 const tag = new Tag();
828 tag.persist();
829 const broad = new Tag();
830 broad.addBroaderTag(tag);
831 broad.persist();
832 tag.addBroaderTag(broad);
833 tag.persist();
834 assertEquals(
835 Array.from(tag.broaderTransitiveTags(), ($) => $.identifier),
836 [broad.identifier, tag.identifier],
837 );
838 });
839 });
840
841 describe("::persist", () => {
842 it("[[Call]] returns an object with expected properties if there were changes", () => {
843 const tag = new Tag();
844 const activity = tag.persist();
845 assertObjectMatch(
846 activity,
847 {
848 "@context":
849 "https://ns.1024.gdn/Tagging/discovery.context.jsonld",
850 context: system.iri,
851 object: tag.iri,
852 },
853 );
854 assertArrayIncludes(activity["@type"], ["TagActivity"]);
855 assert("endTime" in activity);
856 });
857
858 it("[[Call]] returns a Create activity with a type predicate for new objects", () => {
859 const activity = new Tag().persist();
860 assertEquals(activity["@type"], ["TagActivity", "Create"]);
861 assertArrayIncludes(activity.states, [{
862 predicate: "a",
863 object: "Tag",
864 }]);
865 });
866
867 it("[[Call]] returns an Update activity for old objects", () => {
868 const tag = new Tag();
869 tag.persist();
870 tag.prefLabel = "etaoin";
871 const activity = tag.persist();
872 assertEquals(activity["@type"], ["TagActivity", "Update"]);
873 });
874
875 it("[[Call]] states and unstates changes", () => {
876 const broader1 = new Tag();
877 broader1.persist();
878 const broader2 = new Tag();
879 broader2.persist();
880 const tag = new Tag();
881 tag.addBroaderTag(broader1);
882 tag.persist();
883 tag.prefLabel = "etaoin";
884 tag.deleteBroaderTag(broader1);
885 tag.addBroaderTag(broader2);
886 const activity = tag.persist();
887 assertObjectMatch(activity, {
888 unstates: [
889 { predicate: "prefLabel", object: { "@value": "" } },
890 { predicate: "broader", object: broader1.iri },
891 ],
892 states: [
893 { predicate: "prefLabel", object: { "@value": "etaoin" } },
894 { predicate: "broader", object: broader2.iri },
895 ],
896 });
897 });
898
899 it("[[Call]] doesn’t state if there are no additions", () => {
900 const tag = new Tag();
901 tag.addAltLabel("etaoin");
902 tag.persist();
903 tag.deleteAltLabel("etaoin");
904 const activity = tag.persist();
905 assertFalse("state" in activity);
906 });
907
908 it("[[Call]] doesn’t unstate if there are no removals", () => {
909 const tag = new Tag();
910 tag.persist();
911 tag.addAltLabel("etaoin");
912 const activity = tag.persist();
913 assertFalse("unstate" in activity);
914 });
915
916 it("[[Call]] returns null if no meaningful changes were made", () => {
917 const tag = new Tag();
918 tag.persist();
919 const activity = tag.persist();
920 assertStrictEquals(activity, null);
921 });
922
923 it("[[Call]] returns undefined for a silent persist", () => {
924 const broader = new Tag();
925 broader.persist();
926 const tag = new Tag();
927 tag.prefLabel = "etaoin";
928 tag.addBroaderTag(broader);
929 assertStrictEquals(tag.persist(true), undefined);
930 });
931 });
932
933 describe("::prefLabel", () => {
934 it("[[Set]] sets the preferred label", () => {
935 const tag = new Tag();
936 tag.prefLabel = "one";
937 assertEquals({ ...tag.prefLabel }, { "@value": "one" });
938 tag.prefLabel = { "@value": "two" };
939 assertEquals({ ...tag.prefLabel }, { "@value": "two" });
940 tag.prefLabel = { "@value": "three", "@language": "en" };
941 assertEquals(
942 { ...tag.prefLabel },
943 { "@value": "three", "@language": "en" },
944 );
945 });
946 });
947
948 describe("::system", () => {
949 it("[[Get]] returns the tag system", () => {
950 assertStrictEquals(new Tag().system, system);
951 });
952 });
953
954 // `::tagURI` is tested by a `.fromTagURI`.
955
956 describe("::taggingEntity", () => {
957 it("[[Get]] returns the tagging entity of the tag system", () => {
958 assertStrictEquals(
959 new Tag().taggingEntity,
960 system.taggingEntity,
961 );
962 });
963 });
964
965 describe("::toString", () => {
966 it("[[Get]] returns the string value of the preferred label", () => {
967 const tag = new Tag();
968 tag.prefLabel = { "@value": "etaoin", "@language": "zxx" };
969 assertStrictEquals(tag.toString(), "etaoin");
970 });
971 });
972
973 // `::[Storage.toObject]` is tested by `::persist`.
974 });
975
976 describe("::apply", () => {
977 let system;
978 let Tag;
979
980 beforeEach(() => {
981 system = new TagSystem("example", "1972-12-31");
982 Tag = system.Tag;
983 });
984
985 it("[[Call]] throws if no activity is provided", () => {
986 assertThrows(() => {
987 system.apply();
988 });
989 });
990
991 it("[[Call]] throws with an invalid activity", () => {
992 assertThrows(() => {
993 system.apply({});
994 });
995 });
996
997 it("[[Call]] throws when specifying an invalid object", () => {
998 assertThrows(() => {
999 system.apply({
1000 object: "",
1001 });
1002 });
1003 assertThrows(() => {
1004 system.apply({
1005 object: `${system.iriSpace}000-0000`,
1006 });
1007 });
1008 });
1009
1010 it("[[Call]] returns the tag being modified", () => {
1011 const tag = new Tag();
1012 tag.persist(true);
1013 const applied = system.apply({ object: tag.iri });
1014 assertStrictEquals(
1015 Object.getPrototypeOf(applied),
1016 system.Tag.prototype,
1017 );
1018 assertStrictEquals(tag.identifier, applied.identifier);
1019 });
1020
1021 it("[[Call]] applies the changes", () => {
1022 const broaderTag = new Tag();
1023 const broaderActivity = broaderTag.persist();
1024 const otherBroaderTag = new Tag();
1025 const otherBroaderActivity = otherBroaderTag.persist();
1026 const tag = new Tag("EntityTag", "my pref label");
1027 tag.addHiddenLabel("label");
1028 tag.addBroaderTag(broaderTag);
1029 const createActivity = tag.persist();
1030 tag.prefLabel = "new pref label";
1031 tag.addAltLabel("alternative label");
1032 tag.deleteHiddenLabel("label");
1033 tag.addBroaderTag(otherBroaderTag);
1034 tag.deleteBroaderTag(broaderTag);
1035 const updateActivity = tag.persist();
1036 const otherSystem = new TagSystem(
1037 system.authorityName,
1038 system.date,
1039 );
1040 otherSystem.apply(broaderActivity);
1041 otherSystem.apply(otherBroaderActivity);
1042 const appliedCreate = otherSystem.apply(createActivity);
1043 assertStrictEquals(appliedCreate.kind, "EntityTag");
1044 assertStrictEquals(appliedCreate.identifier, tag.identifier);
1045 assertEquals(
1046 { ...appliedCreate.prefLabel },
1047 { "@value": "my pref label" },
1048 );
1049 assertEquals(
1050 [...appliedCreate.hiddenLabels()].map(($) => ({ ...$ })),
1051 [{ "@value": "label" }],
1052 );
1053 assertEquals(
1054 [...appliedCreate.broaderTags()].map(($) => $.identifier),
1055 [broaderTag.identifier],
1056 );
1057 const appliedUpdate = otherSystem.apply(updateActivity);
1058 assertEquals(
1059 { ...appliedUpdate.prefLabel },
1060 { "@value": "new pref label" },
1061 );
1062 assertEquals(
1063 [...appliedUpdate.altLabels()].map(($) => ({ ...$ })),
1064 [{ "@value": "alternative label" }],
1065 );
1066 assertEquals([...appliedUpdate.hiddenLabels()], []);
1067 assertEquals(
1068 [...appliedUpdate.broaderTags()].map(($) => $.identifier),
1069 [otherBroaderTag.identifier],
1070 );
1071 });
1072
1073 it("[[Call]] silently fails deleting preflabels", () => {
1074 const tag = new system.Tag("Tag", "my pref label");
1075 tag.persist(true);
1076 const applied = system.apply({
1077 object: tag.iri,
1078 unstates: [{
1079 predicate: "prefLabel",
1080 object: "my pref label",
1081 }],
1082 });
1083 assertEquals(
1084 { ...applied.prefLabel },
1085 { "@value": "my pref label" },
1086 );
1087 });
1088
1089 it("[[Call]] silently fails deleting unrecognized statements", () => {
1090 const tag = new Tag();
1091 tag.persist(true);
1092 const otherTag = new Tag();
1093 otherTag.persist(true);
1094 const applied = system.apply({
1095 object: tag.iri,
1096 unstates: [{
1097 predicate: "bad_statement",
1098 object: otherTag.iri,
1099 }],
1100 });
1101 assert(applied);
1102 });
1103
1104 it("[[Call]] silently fails deleting immutable statements", () => {
1105 const tag = new Tag();
1106 tag.persist(true);
1107 const applied = system.apply({
1108 object: tag.iri,
1109 unstates: [{ predicate: "a", object: "Tag" }],
1110 });
1111 assertStrictEquals(applied.kind, "Tag");
1112 });
1113
1114 it("[[Call]] silently fails deleting inverse statements", () => {
1115 const tag = new Tag();
1116 tag.persist(true);
1117 const otherTag = new Tag();
1118 otherTag.addBroaderTag(tag);
1119 otherTag.persist(true);
1120 const applied = system.apply({
1121 object: tag.iri,
1122 unstates: [{ predicate: "narrower", object: otherTag.iri }],
1123 });
1124 assertStrictEquals(
1125 [...applied.narrowerTags()][0].identifier,
1126 otherTag.identifier,
1127 );
1128 });
1129
1130 it("[[Call]] sets preflabels", () => {
1131 const tag = new Tag("Tag", "my pref label");
1132 tag.persist(true);
1133 const applied = system.apply({
1134 object: tag.iri,
1135 states: [{ predicate: "prefLabel", object: "new pref label" }],
1136 });
1137 assertEquals(
1138 { ...applied.prefLabel },
1139 { "@value": "new pref label" },
1140 );
1141 });
1142
1143 it("[[Call]] silently fails setting unrecognized statements", () => {
1144 const tag = new Tag();
1145 tag.persist(true);
1146 const otherTag = new Tag();
1147 otherTag.persist(true);
1148 const applied = system.apply({
1149 object: tag.iri,
1150 states: [{ predicate: "bad_statement", object: otherTag.iri }],
1151 });
1152 assert(applied);
1153 });
1154
1155 it("[[Call]] silently fails setting immutable statements", () => {
1156 const tag = new Tag();
1157 tag.persist(true);
1158 const applied = system.apply({
1159 object: tag.iri,
1160 states: [{ predicate: "a", object: "RelationshipTag" }],
1161 });
1162 assertStrictEquals(applied.kind, "Tag");
1163 });
1164
1165 it("[[Call]] silently fails setting inverse statements", () => {
1166 const tag = new Tag();
1167 tag.persist(true);
1168 const otherTag = new Tag();
1169 otherTag.persist(true);
1170 const applied = system.apply({
1171 object: tag.iri,
1172 unstates: [{ predicate: "narrower", object: otherTag.iri }],
1173 });
1174 assertEquals([...applied.narrowerTags()], []);
1175 });
1176 });
1177
1178 describe("::authorityName", () => {
1179 it("[[Get]] returns the authority name", () => {
1180 const system = new TagSystem("etaoin.example", "1972-12-31");
1181 assertStrictEquals(system.authorityName, "etaoin.example");
1182 });
1183 });
1184
1185 describe("::authorityName", () => {
1186 it("[[Get]] returns the date", () => {
1187 const system = new TagSystem("etaoin.example", "1972-12-31");
1188 assertStrictEquals(system.date, "1972-12-31");
1189 });
1190 });
1191
1192 describe("::identifiers", () => {
1193 it("[[Get]] yields the extant entities", () => {
1194 const system = new TagSystem("etaoin.example", "1972-12-31");
1195 const tags = new Set(function* () {
1196 let i = 0;
1197 while (i++ < 5) {
1198 // Generate 5 tags and remember their identifiers.
1199 const tag = new system.Tag();
1200 tag.persist();
1201 yield tag.identifier;
1202 }
1203 }());
1204 assertEquals(
1205 new Set(Array.from(system.entities(), ($) => $.identifier)),
1206 tags,
1207 );
1208 });
1209 });
1210
1211 describe("::identifier", () => {
1212 it("[[Get]] returns the identifier", () => {
1213 const system = new TagSystem("etaoin.example", "1972-12-31");
1214 assertStrictEquals(system.identifier, "");
1215 const system2 = new TagSystem(
1216 "etaoin.example",
1217 "1972-12-31",
1218 "etaoin",
1219 );
1220 assertStrictEquals(system2.identifier, "etaoin");
1221 });
1222 });
1223
1224 describe("::identifiers", () => {
1225 it("[[Get]] yields the identifiers in use", () => {
1226 const system = new TagSystem("etaoin.example", "1972-12-31");
1227 const tags = new Set(function* () {
1228 let i = 0;
1229 while (i++ < 5) {
1230 // Generate 5 tags and remember their identifiers.
1231 const tag = new system.Tag();
1232 tag.persist();
1233 yield tag.identifier;
1234 }
1235 }());
1236 assertEquals(new Set(system.identifiers()), tags);
1237 });
1238 });
1239
1240 describe("::iri", () => {
1241 it("[[Get]] returns the I·R·I", () => {
1242 const system = new TagSystem("etaoin.example", "1972-12-31");
1243 assertStrictEquals(
1244 system.iri,
1245 "https://etaoin.example/tag:etaoin.example,1972-12-31:",
1246 );
1247 const system2 = new TagSystem(
1248 "etaoin.example",
1249 "1972-12-31",
1250 "etaoin",
1251 );
1252 assertStrictEquals(
1253 system2.iri,
1254 "https://etaoin.example/tag:etaoin.example,1972-12-31:etaoin",
1255 );
1256 });
1257 });
1258
1259 describe("::iriSpace", () => {
1260 it("[[Get]] returns the I·R·I space", () => {
1261 const system = new TagSystem("etaoin.example", "1972-12-31");
1262 assertStrictEquals(
1263 system.iriSpace,
1264 "https://etaoin.example/tag:etaoin.example,1972-12-31:",
1265 );
1266 const system2 = new TagSystem(
1267 "etaoin.example",
1268 "1972-12-31",
1269 "etaoin",
1270 );
1271 assertStrictEquals(
1272 system2.iriSpace,
1273 "https://etaoin.example/tag:etaoin.example,1972-12-31:",
1274 );
1275 });
1276 });
1277
1278 describe("::tagURI", () => {
1279 it("[[Get]] returns the Tag U·R·I", () => {
1280 const system = new TagSystem("etaoin.example", "1972-12-31");
1281 assertStrictEquals(
1282 system.tagURI,
1283 "tag:etaoin.example,1972-12-31:",
1284 );
1285 const system2 = new TagSystem(
1286 "etaoin.example",
1287 "1972-12-31",
1288 "etaoin",
1289 );
1290 assertStrictEquals(
1291 system2.tagURI,
1292 "tag:etaoin.example,1972-12-31:etaoin",
1293 );
1294 });
1295 });
1296
1297 describe("::taggingEntity", () => {
1298 it("[[Get]] returns the tagging entity", () => {
1299 const system = new TagSystem("etaoin.example", "1972-12-31");
1300 assertStrictEquals(
1301 system.taggingEntity,
1302 "etaoin.example,1972-12-31",
1303 );
1304 });
1305 });
1306 });
This page took 0.181748 seconds and 5 git commands to generate.