« get me outta code hell

hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
path: root/test
diff options
context:
space:
mode:
Diffstat (limited to 'test')
-rw-r--r--test/lib/wiki-data.js98
-rw-r--r--test/snapshot/generateAlbumCoverArtwork.js37
-rw-r--r--test/snapshot/generateAlbumReleaseInfo.js12
-rw-r--r--test/snapshot/generateAlbumSecondaryNav.js45
-rw-r--r--test/snapshot/generateAlbumTrackList.js24
-rw-r--r--test/snapshot/generateCoverArtwork.js31
-rw-r--r--test/snapshot/generatePreviousNextLinks.js35
-rw-r--r--test/snapshot/generateTrackAdditionalNamesBox.js107
-rw-r--r--test/snapshot/generateTrackCoverArtwork.js63
-rw-r--r--test/snapshot/generateTrackReleaseInfo.js4
-rw-r--r--test/snapshot/linkContribution.js53
-rw-r--r--test/snapshot/linkThing.js2
-rw-r--r--test/snapshot/transformContent.js10
-rw-r--r--test/unit/content/dependencies/generateAlbumTrackList.js3
-rw-r--r--test/unit/content/dependencies/linkContribution.js45
-rw-r--r--test/unit/data/composite/data/withPropertyFromObject.js84
-rw-r--r--test/unit/data/composite/wiki-data/withParsedCommentaryEntries.js275
-rw-r--r--test/unit/data/things/album.js321
-rw-r--r--test/unit/data/things/art-tag.js60
-rw-r--r--test/unit/data/things/flash.js39
-rw-r--r--test/unit/data/things/track.js402
-rw-r--r--test/unit/data/validators.js (renamed from test/unit/data/things/validators.js)0
22 files changed, 870 insertions, 880 deletions
diff --git a/test/lib/wiki-data.js b/test/lib/wiki-data.js
index c373aadd..7c3d2147 100644
--- a/test/lib/wiki-data.js
+++ b/test/lib/wiki-data.js
@@ -1,6 +1,8 @@
 import CacheableObject from '#cacheable-object';
 import find from '#find';
 import {withEntries} from '#sugar';
+import Thing from '#thing';
+import thingConstructors from '#things';
 import {linkWikiDataArrays} from '#yaml';
 
 export function linkAndBindWikiData(wikiData, {
@@ -53,3 +55,99 @@ export function linkAndBindWikiData(wikiData, {
         .bind(null, wikiData, {XXX_decacheWikiData: true}),
   };
 }
+
+export function stubWikiData() {
+  return {
+    albumData: [],
+    artistData: [],
+    artTagData: [],
+    flashData: [],
+    flashActData: [],
+    flashSideData: [],
+    groupData: [],
+    groupCategoryData: [],
+    newsData: [],
+    staticPageData: [],
+    trackData: [],
+    trackSectionData: [],
+  };
+}
+
+export function stubThing(wikiData, constructor, properties = {}) {
+  const thing = Reflect.construct(constructor, []);
+  Object.assign(thing, properties);
+
+  const wikiDataSpec = {
+    Album: 'albumData',
+    Artist: 'artistData',
+    ArtTag: 'artTagData',
+    Flash: 'flashData',
+    FlashAct: 'flashActData',
+    FlashSide: 'flashSideData',
+    Group: 'groupData',
+    GroupCategory: 'groupCategoryData',
+    NewsEntry: 'newsData',
+    StaticPage: 'staticPageData',
+    Track: 'trackData',
+    TrackSection: 'trackSectionData',
+  };
+
+  const wikiDataMap =
+    new Map(
+      Object.entries(wikiDataSpec)
+        .map(([thingKey, wikiDataKey]) => [
+          thingConstructors[thingKey],
+          wikiData[wikiDataKey],
+        ]));
+
+  const wikiDataArray =
+    wikiDataMap.get(constructor);
+
+  wikiDataArray.push(thing);
+
+  return thing;
+}
+
+export function stubTrackAndAlbum(wikiData, trackDirectory = null, albumDirectory = null) {
+  const {Track, TrackSection, Album} = thingConstructors;
+
+  const track =
+    stubThing(wikiData, Track, {directory: trackDirectory});
+
+  const section =
+    stubThing(wikiData, TrackSection, {tracks: [track]});
+
+  const album =
+    stubThing(wikiData, Album, {directory: albumDirectory, trackSections: [section]});
+
+  return {track, album, section};
+}
+
+export function stubArtistAndContribs(wikiData, artistName = `Test Artist`) {
+  const {Artist} = thingConstructors;
+
+  const artist =
+    stubThing(wikiData, Artist, {name: artistName});
+
+  const contribs =
+    [{artist: artistName, annotation: null}];
+
+  const badContribs =
+    [{artist: `Figment of Your Imagination`, annotation: null}];
+
+  return {artist, contribs, badContribs};
+}
+
+export function stubFlashAndAct(wikiData, flashDirectory = null) {
+  const {Flash, FlashAct} = thingConstructors;
+
+  const flash =
+    stubThing(wikiData, Flash, {directory: flashDirectory});
+
+  const flashAct =
+    stubThing(wikiData, FlashAct, {
+      flashes: [Thing.getReference(flash)],
+    });
+
+  return {flash, flashAct};
+}
diff --git a/test/snapshot/generateAlbumCoverArtwork.js b/test/snapshot/generateAlbumCoverArtwork.js
deleted file mode 100644
index 939c6e19..00000000
--- a/test/snapshot/generateAlbumCoverArtwork.js
+++ /dev/null
@@ -1,37 +0,0 @@
-import t from 'tap';
-
-import contentFunction from '#content-function';
-import {testContentFunctions} from '#test-lib';
-
-testContentFunctions(t, 'generateAlbumCoverArtwork (snapshot)', async (t, evaluate) => {
-  await evaluate.load({
-    mock: {
-      image: evaluate.stubContentFunction('image'),
-    },
-  });
-
-  const album = {
-    directory: 'bee-forus-seatbelt-safebee',
-    coverArtFileExtension: 'png',
-    coverArtDimensions: [400, 300],
-    color: '#f28514',
-    artTags: [
-      {name: 'Damara', directory: 'damara', isContentWarning: false},
-      {name: 'Cronus', directory: 'cronus', isContentWarning: false},
-      {name: 'Bees', directory: 'bees', isContentWarning: false},
-      {name: 'creepy crawlies', isContentWarning: true},
-    ],
-  };
-
-  evaluate.snapshot('display: primary', {
-    name: 'generateAlbumCoverArtwork',
-    args: [album],
-    slots: {mode: 'primary'},
-  });
-
-  evaluate.snapshot('display: thumbnail', {
-    name: 'generateAlbumCoverArtwork',
-    args: [album],
-    slots: {mode: 'thumbnail'},
-  });
-});
diff --git a/test/snapshot/generateAlbumReleaseInfo.js b/test/snapshot/generateAlbumReleaseInfo.js
index a109912f..f41e502d 100644
--- a/test/snapshot/generateAlbumReleaseInfo.js
+++ b/test/snapshot/generateAlbumReleaseInfo.js
@@ -8,22 +8,22 @@ testContentFunctions(t, 'generateAlbumReleaseInfo (snapshot)', async (t, evaluat
     name: 'generateAlbumReleaseInfo',
     args: [{
       artistContribs: [
-        {artist: {name: 'Toby Fox', directory: 'toby-fox', urls: null}, annotation: 'music probably'},
+        {artist: {name: 'Toby Fox', directory: 'toby-fox', urls: []}, annotation: 'music probably'},
         {artist: {name: 'Tensei', directory: 'tensei', urls: ['https://tenseimusic.bandcamp.com/']}, annotation: 'hot jams'},
       ],
 
       coverArtistContribs: [
-        {artist: {name: 'Hanni Brosh', directory: 'hb', urls: null}, annotation: null},
+        {artist: {name: 'Hanni Brosh', directory: 'hb', urls: []}, annotation: null},
       ],
 
       wallpaperArtistContribs: [
-        {artist: {name: 'Hanni Brosh', directory: 'hb', urls: null}, annotation: null},
-        {artist: {name: 'Niklink', directory: 'niklink', urls: null}, annotation: 'edits'},
+        {artist: {name: 'Hanni Brosh', directory: 'hb', urls: []}, annotation: null},
+        {artist: {name: 'Niklink', directory: 'niklink', urls: []}, annotation: 'edits'},
       ],
 
       bannerArtistContribs: [
-        {artist: {name: 'Hanni Brosh', directory: 'hb', urls: null}, annotation: null},
-        {artist: {name: 'Niklink', directory: 'niklink', urls: null}, annotation: 'edits'},
+        {artist: {name: 'Hanni Brosh', directory: 'hb', urls: []}, annotation: null},
+        {artist: {name: 'Niklink', directory: 'niklink', urls: []}, annotation: 'edits'},
       ],
 
       name: 'AlterniaBound',
diff --git a/test/snapshot/generateAlbumSecondaryNav.js b/test/snapshot/generateAlbumSecondaryNav.js
index 57618f2f..2495bc4a 100644
--- a/test/snapshot/generateAlbumSecondaryNav.js
+++ b/test/snapshot/generateAlbumSecondaryNav.js
@@ -4,10 +4,21 @@ import {testContentFunctions} from '#test-lib';
 testContentFunctions(t, 'generateAlbumSecondaryNav (snapshot)', async (t, evaluate) => {
   await evaluate.load();
 
-  let album, group1, group2;
+  let album, anotherAlbum, group1, group2;
 
-  group1 = {name: 'VCG', directory: 'vcg', color: '#abcdef'};
-  group2 = {name: 'Bepis', directory: 'bepis', color: '#123456'};
+  group1 = {
+    name: 'VCG',
+    directory: 'vcg',
+    color: '#abcdef',
+    serieses: [],
+  };
+
+  group2 = {
+    name: 'Bepis',
+    directory: 'bepis',
+    color: '#123456',
+    serieses: [],
+  };
 
   album = {
     name: 'Album',
@@ -16,15 +27,37 @@ testContentFunctions(t, 'generateAlbumSecondaryNav (snapshot)', async (t, evalua
     groups: [group1, group2],
   };
 
+  anotherAlbum = {
+    name: 'Last',
+    directory: 'last',
+    date: new Date('2010-06-12'),
+  };
+
   group1.albums = [
-    {name: 'First', directory: 'first', date: new Date('2010-04-10')},
+    {
+      name: 'First',
+      directory: 'first',
+      date: new Date('2010-04-10'),
+    },
     album,
-    {name: 'Last', directory: 'last', date: new Date('2010-06-12')},
+    anotherAlbum,
+  ];
+
+  group1.serieses = [
+    {
+      name: 'Series',
+      albums: [album, anotherAlbum],
+      group: group1,
+    },
   ];
 
   group2.albums = [
     album,
-    {name: 'Second', directory: 'second', date: new Date('2011-04-13')},
+    {
+      name: 'Second',
+      directory: 'second',
+      date: new Date('2011-04-13'),
+    },
   ];
 
   evaluate.snapshot('basic behavior, mode: album', {
diff --git a/test/snapshot/generateAlbumTrackList.js b/test/snapshot/generateAlbumTrackList.js
index 08b31902..e09f1c4a 100644
--- a/test/snapshot/generateAlbumTrackList.js
+++ b/test/snapshot/generateAlbumTrackList.js
@@ -6,17 +6,20 @@ testContentFunctions(t, 'generateAlbumTrackList (snapshot)', async (t, evaluate)
     mock: {
       generateAlbumTrackListMissingDuration:
         evaluate.stubContentFunction('generateAlbumTrackListMissingDuration'),
+
+      image:
+        evaluate.stubContentFunction('image'),
     },
   });
 
   const contribs1 = [
-    {artist: {name: 'Apricot', directory: 'apricot', urls: null}},
+    {artist: {name: 'Apricot', directory: 'apricot', urls: []}},
   ];
 
   const contribs2 = [
-    {artist: {name: 'Apricot', directory: 'apricot', urls: null}},
+    {artist: {name: 'Apricot', directory: 'apricot', urls: []}},
     {artist: {name: 'Peach', directory: 'peach', urls: ['https://peach.bandcamp.com/']}},
-    {artist: {name: 'Cerise', directory: 'cerise', urls: null}},
+    {artist: {name: 'Cerise', directory: 'cerise', urls: []}},
   ];
 
   const color1 = '#fb07ff';
@@ -46,6 +49,16 @@ testContentFunctions(t, 'generateAlbumTrackList (snapshot)', async (t, evaluate)
     tracks,
   };
 
+  const albumWithTrackSectionDescriptions = {
+    color: color1,
+    artistContribs: contribs1,
+    trackSections: [
+      {name: 'First section', tracks: tracks.slice(0, 3), description: `Why yes!`},
+      {name: 'Second section', tracks: tracks.slice(3), description: `How *contentful,* this is.`},
+    ],
+    tracks,
+  };
+
   const albumWithNoDuration = {
     color: color1,
     artistContribs: contribs1,
@@ -63,6 +76,11 @@ testContentFunctions(t, 'generateAlbumTrackList (snapshot)', async (t, evaluate)
     args: [albumWithoutTrackSections],
   });
 
+  evaluate.snapshot(`basic behavior, with descriptions`, {
+    name: 'generateAlbumTrackList',
+    args: [albumWithTrackSectionDescriptions],
+  });
+
   evaluate.snapshot(`collapseDurationScope: never`, {
     name: 'generateAlbumTrackList',
     slots: {collapseDurationScope: 'never'},
diff --git a/test/snapshot/generateCoverArtwork.js b/test/snapshot/generateCoverArtwork.js
deleted file mode 100644
index e35dd8d0..00000000
--- a/test/snapshot/generateCoverArtwork.js
+++ /dev/null
@@ -1,31 +0,0 @@
-import t from 'tap';
-import {testContentFunctions} from '#test-lib';
-
-testContentFunctions(t, 'generateCoverArtwork (snapshot)', async (t, evaluate) => {
-  await evaluate.load({
-    mock: {
-      image: evaluate.stubContentFunction('image', {mock: true}),
-    },
-  });
-
-  const artTags = [
-    {name: 'Damara', directory: 'damara', isContentWarning: false},
-    {name: 'Cronus', directory: 'cronus', isContentWarning: false},
-    {name: 'Bees', directory: 'bees', isContentWarning: false},
-    {name: 'creepy crawlies', isContentWarning: true},
-  ];
-
-  const path = ['media.albumCover', 'bee-forus-seatbelt-safebee', 'png'];
-
-  evaluate.snapshot('display: primary', {
-    name: 'generateCoverArtwork',
-    args: [artTags],
-    slots: {path, mode: 'primary'},
-  });
-
-  evaluate.snapshot('display: thumbnail', {
-    name: 'generateCoverArtwork',
-    args: [artTags],
-    slots: {path, mode: 'thumbnail'},
-  });
-});
diff --git a/test/snapshot/generatePreviousNextLinks.js b/test/snapshot/generatePreviousNextLinks.js
deleted file mode 100644
index 0d952f59..00000000
--- a/test/snapshot/generatePreviousNextLinks.js
+++ /dev/null
@@ -1,35 +0,0 @@
-import t from 'tap';
-import * as html from '#html';
-import {testContentFunctions} from '#test-lib';
-
-testContentFunctions(t, 'generatePreviousNextLinks (snapshot)', async (t, evaluate) => {
-  await evaluate.load();
-
-  const quickSnapshot = (message, slots) =>
-    evaluate.snapshot(message, {
-      name: 'generatePreviousNextLinks',
-      slots,
-      postprocess: template => template.content.join('\n'),
-    });
-
-  quickSnapshot('basic behavior', {
-    previousLink: evaluate.stubTemplate('previous'),
-    nextLink: evaluate.stubTemplate('next'),
-  });
-
-  quickSnapshot('previous missing', {
-    nextLink: evaluate.stubTemplate('next'),
-  });
-
-  quickSnapshot('next missing', {
-    previousLink: evaluate.stubTemplate('previous'),
-  });
-
-  quickSnapshot('neither link present', {});
-
-  quickSnapshot('disable id', {
-    previousLink: evaluate.stubTemplate('previous'),
-    nextLink: evaluate.stubTemplate('next'),
-    id: false,
-  });
-});
diff --git a/test/snapshot/generateTrackAdditionalNamesBox.js b/test/snapshot/generateTrackAdditionalNamesBox.js
deleted file mode 100644
index 9c1e3598..00000000
--- a/test/snapshot/generateTrackAdditionalNamesBox.js
+++ /dev/null
@@ -1,107 +0,0 @@
-import t from 'tap';
-
-import contentFunction from '#content-function';
-import {testContentFunctions} from '#test-lib';
-
-testContentFunctions(t, 'generateTrackAdditionalNamesBox (snapshot)', async (t, evaluate) => {
-  await evaluate.load({
-    mock: {
-      generateAdditionalNamesBox:
-        evaluate.stubContentFunction('generateAdditionalNamesBox'),
-    },
-  });
-
-  const stubTrack = {
-    additionalNames: [],
-    sharedAdditionalNames: [],
-    inferredAdditionalNames: [],
-  };
-
-  const quickSnapshot = (message, trackProperties) =>
-    evaluate.snapshot(message, {
-      name: 'generateTrackAdditionalNamesBox',
-      args: [{...stubTrack, ...trackProperties}],
-    });
-
-  quickSnapshot(`no additional names`, {});
-
-  quickSnapshot(`own additional names only`, {
-    additionalNames: [
-      {name: `Foo Bar`, annotation: `the Alps`},
-    ],
-  });
-
-  quickSnapshot(`shared additional names only`, {
-    sharedAdditionalNames: [
-      {name: `Bar Foo`, annotation: `the Rockies`},
-    ],
-  });
-
-  quickSnapshot(`inferred additional names only`, {
-    inferredAdditionalNames: [
-      {name: `Baz Baz`, from: [{directory: `the-pyrenees`}]},
-    ],
-  });
-
-  quickSnapshot(`multiple own`, {
-    additionalNames: [
-      {name: `Apple Time!`},
-      {name: `Pterodactyl Time!`},
-      {name: `Banana Time!`},
-    ],
-  });
-
-  quickSnapshot(`own and shared, some overlap`, {
-    additionalNames: [
-      {name: `weed dreams..`, annotation: `own annotation`},
-      {name: `夜間のMOON汗`, annotation: `own annotation`},
-    ],
-    sharedAdditionalNames: [
-      {name: `weed dreams..`, annotation: `shared annotation`},
-      {name: `GAMINGブラザー96`, annotation: `shared annotation`},
-    ],
-  });
-
-  quickSnapshot(`shared and inferred, some overlap`, {
-    sharedAdditionalNames: [
-      {name: `Coruscate`, annotation: `shared annotation`},
-      {name: `Arbroath`, annotation: `shared annotation`},
-    ],
-    inferredAdditionalNames: [
-      {name: `Arbroath`, from: [{directory: `inferred-from`}]},
-      {name: `Prana Ferox`, from: [{directory: `inferred-from`}]},
-    ],
-  });
-
-  quickSnapshot(`own and inferred, some overlap`, {
-    additionalNames: [
-      {name: `Ke$halo Strike Back`, annotation: `own annotation`},
-      {name: `Ironic Mania`, annotation: `own annotation`},
-    ],
-    inferredAdditionalNames: [
-      {name: `Ironic Mania`, from: [{directory: `inferred-from`}]},
-      {name: `ANARCHY::MEGASTRIFE`, from: [{directory: `inferred-from`}]},
-    ],
-  });
-
-  quickSnapshot(`own and shared and inferred, various overlap`, {
-    additionalNames: [
-      {name: `Own!`, annotation: `own annotation`},
-      {name: `Own! Shared!`, annotation: `own annotation`},
-      {name: `Own! Inferred!`, annotation: `own annotation`},
-      {name: `Own! Shared! Inferred!`, annotation: `own annotation`},
-    ],
-    sharedAdditionalNames: [
-      {name: `Shared!`, annotation: `shared annotation`},
-      {name: `Own! Shared!`, annotation: `shared annotation`},
-      {name: `Shared! Inferred!`, annotation: `shared annotation`},
-      {name: `Own! Shared! Inferred!`, annotation: `shared annotation`},
-    ],
-    inferredAdditionalNames: [
-      {name: `Inferred!`, from: [{directory: `inferred-from`}]},
-      {name: `Own! Inferred!`, from: [{directory: `inferred-from`}]},
-      {name: `Shared! Inferred!`, from: [{directory: `inferred-from`}]},
-      {name: `Own! Shared! Inferred!`, from: [{directory: `inferred-from`}]},
-    ],
-  });
-});
diff --git a/test/snapshot/generateTrackCoverArtwork.js b/test/snapshot/generateTrackCoverArtwork.js
deleted file mode 100644
index 4d952119..00000000
--- a/test/snapshot/generateTrackCoverArtwork.js
+++ /dev/null
@@ -1,63 +0,0 @@
-import t from 'tap';
-import {testContentFunctions} from '#test-lib';
-
-testContentFunctions(t, 'generateTrackCoverArtwork (snapshot)', async (t, evaluate) => {
-  await evaluate.load({
-    mock: {
-      image: evaluate.stubContentFunction('image'),
-    },
-  });
-
-  const album = {
-    directory: 'bee-forus-seatbelt-safebee',
-    coverArtFileExtension: 'png',
-    coverArtDimensions: [400, 300],
-    artTags: [
-      {name: 'Damara', directory: 'damara', isContentWarning: false},
-      {name: 'Cronus', directory: 'cronus', isContentWarning: false},
-      {name: 'Bees', directory: 'bees', isContentWarning: false},
-      {name: 'creepy crawlies', isContentWarning: true},
-    ],
-  };
-
-  const track1 = {
-    directory: 'beesmp3',
-    hasUniqueCoverArt: true,
-    coverArtFileExtension: 'jpg',
-    coverArtDimensions: null,
-    color: '#f28514',
-    artTags: [{name: 'Bees', directory: 'bees', isContentWarning: false}],
-    album,
-  };
-
-  const track2 = {
-    directory: 'fake-bonus-track',
-    hasUniqueCoverArt: false,
-    color: '#abcdef',
-    album,
-  };
-
-  evaluate.snapshot('display: primary - unique art', {
-    name: 'generateTrackCoverArtwork',
-    args: [track1],
-    slots: {mode: 'primary'},
-  });
-
-  evaluate.snapshot('display: thumbnail - unique art', {
-    name: 'generateTrackCoverArtwork',
-    args: [track1],
-    slots: {mode: 'thumbnail'},
-  });
-
-  evaluate.snapshot('display: primary - no unique art', {
-    name: 'generateTrackCoverArtwork',
-    args: [track2],
-    slots: {mode: 'primary'},
-  });
-
-  evaluate.snapshot('display: thumbnail - no unique art', {
-    name: 'generateTrackCoverArtwork',
-    args: [track2],
-    slots: {mode: 'thumbnail'},
-  });
-});
diff --git a/test/snapshot/generateTrackReleaseInfo.js b/test/snapshot/generateTrackReleaseInfo.js
index 78f0fee7..931377c8 100644
--- a/test/snapshot/generateTrackReleaseInfo.js
+++ b/test/snapshot/generateTrackReleaseInfo.js
@@ -4,8 +4,8 @@ import {testContentFunctions} from '#test-lib';
 testContentFunctions(t, 'generateTrackReleaseInfo (snapshot)', async (t, evaluate) => {
   await evaluate.load();
 
-  const artistContribs = [{artist: {name: 'Toby Fox', directory: 'toby-fox', urls: null}, annotation: null}];
-  const coverArtistContribs = [{artist: {name: 'Alpaca', directory: 'alpaca', urls: null}, annotation: '🔥'}];
+  const artistContribs = [{artist: {name: 'Toby Fox', directory: 'toby-fox', urls: []}, annotation: null}];
+  const coverArtistContribs = [{artist: {name: 'Alpaca', directory: 'alpaca', urls: []}, annotation: '🔥'}];
 
   evaluate.snapshot('basic behavior', {
     name: 'generateTrackReleaseInfo',
diff --git a/test/snapshot/linkContribution.js b/test/snapshot/linkContribution.js
index 1043ddc6..47ef1503 100644
--- a/test/snapshot/linkContribution.js
+++ b/test/snapshot/linkContribution.js
@@ -33,53 +33,22 @@ testContentFunctions(t, 'linkContribution (snapshot)', async (t, evaluate) => {
       slots,
     });
 
-  quickSnapshot('showContribution & showIcons (inline)', {
-    showContribution: true,
-    showIcons: true,
-    iconMode: 'inline',
+  quickSnapshot('showAnnotation & showExternalLinks', {
+    showAnnotation: true,
+    showExternalLinks: true,
   });
 
-  quickSnapshot('showContribution & showIcons (tooltip)', {
-    showContribution: true,
-    showIcons: true,
-    iconMode: 'tooltip',
+  quickSnapshot('only showAnnotation', {
+    showAnnotation: true,
   });
 
-  quickSnapshot('only showContribution', {
-    showContribution: true,
-  });
-
-  quickSnapshot('only showIcons (inline)', {
-    showIcons: true,
-    iconMode: 'inline',
-  });
-
-  quickSnapshot('only showIcons (tooltip)', {
-    showContribution: true,
-    showIcons: true,
-    iconMode: 'tooltip',
+  quickSnapshot('only showExternalLinks', {
+    showExternalLinks: true,
   });
 
   quickSnapshot('no accents', {});
 
-  evaluate.snapshot('loads of links (inline)', {
-    name: 'linkContribution',
-    args: [
-      {artist: {name: 'Lorem Ipsum Lover', directory: 'lorem-ipsum-lover', urls: [
-        'https://loremipsum.io',
-        'https://loremipsum.io/generator/',
-        'https://loremipsum.io/#meaning',
-        'https://loremipsum.io/#usage-and-examples',
-        'https://loremipsum.io/#controversy',
-        'https://loremipsum.io/#when-to-use-lorem-ipsum',
-        'https://loremipsum.io/#lorem-ipsum-all-the-things',
-        'https://loremipsum.io/#original-source',
-      ]}, annotation: null},
-    ],
-    slots: {showIcons: true},
-  });
-
-  evaluate.snapshot('loads of links (tooltip)', {
+  evaluate.snapshot('loads of links', {
     name: 'linkContribution',
     args: [
       {artist: {name: 'Lorem Ipsum Lover', directory: 'lorem-ipsum-lover', urls: [
@@ -93,12 +62,12 @@ testContentFunctions(t, 'linkContribution (snapshot)', async (t, evaluate) => {
         'https://loremipsum.io/#original-source',
       ]}, annotation: null},
     ],
-    slots: {showIcons: true, iconMode: 'tooltip'},
+    slots: {showExternalLinks: true},
   });
 
   quickSnapshot('no preventWrapping', {
-    showContribution: true,
-    showIcons: true,
+    showAnnotation: true,
+    showExternalLinks: true,
     preventWrapping: false,
   });
 });
diff --git a/test/snapshot/linkThing.js b/test/snapshot/linkThing.js
index 502db6d7..9b5cff33 100644
--- a/test/snapshot/linkThing.js
+++ b/test/snapshot/linkThing.js
@@ -20,7 +20,7 @@ testContentFunctions(t, 'linkThing (snapshot)', async (t, evaluate) => {
   });
 
   quickSnapshot('preferShortName', {
-    args: ['localized.tag', {
+    args: ['localized.artTagGallery', {
       directory: 'five-oceanfalls',
       name: 'Five (Oceanfalls)',
       nameShort: 'Five',
diff --git a/test/snapshot/transformContent.js b/test/snapshot/transformContent.js
index 87e337e4..63391681 100644
--- a/test/snapshot/transformContent.js
+++ b/test/snapshot/transformContent.js
@@ -156,6 +156,16 @@ testContentFunctions(t, 'transformContent (snapshot)', async (t, evaluate) => {
       `[[date:13 April 2004]], and don't ye forget it`,
       {mode: 'lyrics'});
 
+  quickSnapshot(
+    'emails',
+      `Email cute dogs to qznebula@protonmail.com please.\n` +
+      `Just kidding... [unless?](mailto:qznebula@protonmail.com)`);
+
+  quickSnapshot(
+    `absorb punctuation`,
+      `Don't you think this is an [interesting pull request](https://github.com/hsmusic/hsmusic-wiki/pull/567), Steve?\n` +
+      `Aren't you [interested](https://github.com/hsmusic/hsmusic-wiki/pull/567)... in [checking it out](https://github.com/hsmusic/hsmusic-wiki/pull/567)?!!`);
+
   // TODO: Snapshots for mode: inline
   // TODO: Snapshots for mode: single-link
 });
diff --git a/test/unit/content/dependencies/generateAlbumTrackList.js b/test/unit/content/dependencies/generateAlbumTrackList.js
index 7b3ecd33..988f8505 100644
--- a/test/unit/content/dependencies/generateAlbumTrackList.js
+++ b/test/unit/content/dependencies/generateAlbumTrackList.js
@@ -10,6 +10,9 @@ testContentFunctions(t, 'generateAlbumTrackList (unit)', async (t, evaluate) =>
         generate: (name, {html}) =>
           html.tag('li', `Item: ${name}`),
       },
+
+      image:
+        evaluate.stubContentFunction('image'),
     },
   });
 
diff --git a/test/unit/content/dependencies/linkContribution.js b/test/unit/content/dependencies/linkContribution.js
index ab45b03a..3ffd71d2 100644
--- a/test/unit/content/dependencies/linkContribution.js
+++ b/test/unit/content/dependencies/linkContribution.js
@@ -26,19 +26,21 @@ t.test('generateContributionLinks (unit)', async t => {
 
   await testContentFunctions(t, 'generateContributionLinks (unit 1)', async (t, evaluate) => {
     const slots = {
-      showContribution: true,
-      showIcons: true,
+      showAnnotation: true,
+      showExternalLinks: true,
     };
 
     await evaluate.load({
       mock: evaluate.mock(mock => ({
         linkArtist: {
-          relations: mock.function('linkArtist.relations', () => ({}))
+          relations: mock
+            .function('linkArtist.relations', () => ({}))
             .args([undefined, artist1]).next()
             .args([undefined, artist2]).next()
             .args([undefined, artist3]),
 
-          data: mock.function('linkArtist.data', () => ({}))
+          data: mock
+            .function('linkArtist.data', () => ({}))
             .args([artist1]).next()
             .args([artist2]).next()
             .args([artist3]),
@@ -49,13 +51,18 @@ t.test('generateContributionLinks (unit)', async t => {
             .repeat(3),
         },
 
-        linkExternalAsIcon: {
-          data: mock.function('linkExternalAsIcon.data', () => ({}))
+        generateExternalIcon: {
+          data: mock
+            .function('generateExternalIcon.data', () => ({}))
             .args([artist1.urls[0]]).next()
             .args([artist3.urls[0]]).next()
             .args([artist3.urls[1]]),
 
-          generate: mock.function('linkExternalAsIcon.generate', () => 'icon')
+          generate: mock
+            .function('generateExternalIcon.generate', () => ({
+              toString: () => 'icon',
+              setSlot: () => {},
+            }))
             .repeat(3),
         }
       })),
@@ -74,24 +81,27 @@ t.test('generateContributionLinks (unit)', async t => {
 
   await testContentFunctions(t, 'generateContributionLinks (unit 2)', async (t, evaluate) => {
     const slots = {
-      showContribution: false,
-      showIcons: false,
+      showAnnotation: false,
+      showExternalLinks: false,
     };
 
     await evaluate.load({
       mock: evaluate.mock(mock => ({
         linkArtist: {
-          relations: mock.function('linkArtist.relations', () => ({}))
+          relations: mock
+            .function('linkArtist.relations', () => ({}))
             .args([undefined, artist1]).next()
             .args([undefined, artist2]).next()
             .args([undefined, artist3]),
 
-          data: mock.function('linkArtist.data', () => ({}))
+          data: mock
+            .function('linkArtist.data', () => ({}))
             .args([artist1]).next()
             .args([artist2]).next()
             .args([artist3]),
 
-          generate: mock.function(() => 'artist link')
+          generate: mock
+            .function(() => 'artist link')
             .repeat(3),
         },
 
@@ -99,11 +109,16 @@ t.test('generateContributionLinks (unit)', async t => {
         // tree is the same since whether or not the external icon links are
         // shown is dependent on a slot, which is undefined and arbitrary at
         // relations/data time (it might change on a whim at generate time).
-        linkExternalAsIcon: {
-          data: mock.function('linkExternalAsIcon.data', () => ({}))
+        generateExternalIcon: {
+          data: mock
+            .function('generateExternalIcon.data', () => ({}))
             .repeat(3),
 
-          generate: mock.function('linkExternalAsIcon.generate', () => 'icon')
+          generate: mock
+            .function('generateExternalIcon.generate', () => ({
+              toString: () => 'icon',
+              setSlot: () => {},
+            }))
             .repeat(3),
         },
       })),
diff --git a/test/unit/data/composite/data/withPropertyFromObject.js b/test/unit/data/composite/data/withPropertyFromObject.js
index 50ee835c..912c924c 100644
--- a/test/unit/data/composite/data/withPropertyFromObject.js
+++ b/test/unit/data/composite/data/withPropertyFromObject.js
@@ -1,6 +1,7 @@
 import t from 'tap';
 import {quickCheckCompositeOutputs} from '#test-lib';
 
+import CacheableObject from '#cacheable-object';
 import {compositeFrom, input} from '#composite';
 import {exposeDependency} from '#composite/control-flow';
 import {withPropertyFromObject} from '#composite/data';
@@ -43,6 +44,89 @@ t.test(`withPropertyFromObject: basic behavior`, t => {
   }), null);
 });
 
+t.test(`withPropertyFromObject: "internal" input`, t => {
+  t.plan(7);
+
+  const composite = compositeFrom({
+    compose: false,
+
+    steps: [
+      withPropertyFromObject({
+        object: 'object',
+        property: 'property',
+        internal: 'internal',
+      }),
+
+      exposeDependency({dependency: '#value'}),
+    ],
+  });
+
+  const thing = new (class extends CacheableObject {
+    static [CacheableObject.propertyDescriptors] = {
+      foo: {
+        flags: {update: true, expose: false},
+      },
+
+      bar: {
+        flags: {update: true, expose: true},
+      },
+
+      baz: {
+        flags: {update: true, expose: true},
+        expose: {
+          transform: baz => baz * 2,
+        },
+      },
+    };
+  });
+
+  thing.foo = 100;
+  thing.bar = 200;
+  thing.baz = 300;
+
+  t.match(composite, {
+    expose: {
+      dependencies: ['object', 'property', 'internal'],
+    },
+  });
+
+  t.equal(composite.expose.compute({
+    object: thing,
+    property: 'foo',
+    internal: true,
+  }), 100);
+
+  t.equal(composite.expose.compute({
+    object: thing,
+    property: 'bar',
+    internal: true,
+  }), 200);
+
+  t.equal(composite.expose.compute({
+    object: thing,
+    property: 'baz',
+    internal: true,
+  }), 300);
+
+  t.equal(composite.expose.compute({
+    object: thing,
+    property: 'baz',
+    internal: false,
+  }), 600);
+
+  t.equal(composite.expose.compute({
+    object: thing,
+    property: 'bimbam',
+    internal: false,
+  }), null);
+
+  t.equal(composite.expose.compute({
+    object: null,
+    property: 'bambim',
+    internal: false,
+  }), null);
+});
+
 t.test(`withPropertyFromObject: output shapes & values`, t => {
   t.plan(2 * 3 ** 2);
 
diff --git a/test/unit/data/composite/wiki-data/withParsedCommentaryEntries.js b/test/unit/data/composite/wiki-data/withParsedCommentaryEntries.js
index babe4fae..411fd11d 100644
--- a/test/unit/data/composite/wiki-data/withParsedCommentaryEntries.js
+++ b/test/unit/data/composite/wiki-data/withParsedCommentaryEntries.js
@@ -28,12 +28,13 @@ function stubArtist(artistName = `Test Artist`) {
 }
 
 t.test(`withParsedCommentaryEntries: basic behavior`, t => {
-  t.plan(3);
+  t.plan(7);
 
   const artist1 = stubArtist(`Mobius Trip`);
   const artist2 = stubArtist(`Hadron Kaleido`);
+  const artist3 = stubArtist('Homestuck');
 
-  const artistData = [artist1, artist2];
+  const artistData = [artist1, artist2, artist3];
 
   t.match(composite, {
     expose: {
@@ -53,6 +54,10 @@ t.test(`withParsedCommentaryEntries: basic behavior`, t => {
       artistDisplayText: null,
       annotation: null,
       date: null,
+      accessDate: null,
+      accessKind: null,
+      secondDate: null,
+      dateKind: null,
       body: `Some commentary.\nVery cool.`,
     },
   ]);
@@ -76,6 +81,10 @@ t.test(`withParsedCommentaryEntries: basic behavior`, t => {
       annotation: `music, art`,
       date: new Date('12 January 2015'),
       body: `First commentary entry.\nVery cool.`,
+      secondDate: null,
+      dateKind: null,
+      accessDate: null,
+      accessKind: null,
     },
     {
       artists: [artist2],
@@ -83,6 +92,10 @@ t.test(`withParsedCommentaryEntries: basic behavior`, t => {
       annotation: `moral support`,
       date: new Date('4 April 2022'),
       body: `Second commentary entry. Yes. So cool.`,
+      secondDate: null,
+      dateKind: null,
+      accessDate: null,
+      accessKind: null,
     },
     {
       artists: [],
@@ -90,6 +103,10 @@ t.test(`withParsedCommentaryEntries: basic behavior`, t => {
       annotation: `pingas`,
       date: new Date('25 August 2023'),
       body: `Oh no.. Oh dear...`,
+      secondDate: null,
+      dateKind: null,
+      accessDate: null,
+      accessKind: null,
     },
     {
       artists: [artist1, artist2],
@@ -97,6 +114,260 @@ t.test(`withParsedCommentaryEntries: basic behavior`, t => {
       annotation: null,
       date: null,
       body: `And back around we go.`,
+      secondDate: null,
+      dateKind: null,
+      accessDate: null,
+      accessKind: null,
+    },
+  ]);
+
+  t.same(composite.expose.compute({
+    artistData,
+    from:
+      `<i>Homestuck:</i> ([Bandcamp credits blurb](https://web.archive.org/web/20201024170202/https://homestuck.bandcamp.com/track/sburban-countdown-3) on "Homestuck Vol. 1-4 (with Midnight Crew: Drawing Dead)", 10/25/2019)\n` +
+      `\n` +
+      `Written by [[artist:michael-guy-bowman|Michael Guy Bowman]]<br>\n` +
+      `Arrangement by [[artist:mark-j-hadley|Mark Hadley]]\n` +
+      `\n` +
+      `<i>Homestuck:</i> ([fake](https://web.archive.org/web/20201024170202/https://homestuck.bandcamp.com/fake), 7/20/2019 captured 4/13/2024)\n` +
+      `This isn't real!\n` +
+      `\n` +
+      `<i>Homestuck:</i> ([fake](https://homestuck.com/fake), 10/25/2011 accessed 10/27/2011)\n` +
+      `This isn't real either!\n` +
+      `\n` +
+      `<i>Homestuck:</i> ([fake](https://web.archive.org/web/20201024170202/https://homestuck.bandcamp.com/fake), 7/20/2019 accessed 4/13/2024)\n` +
+      `Not this one, neither!\n`
+  }), [
+    {
+      artists: [artist3],
+      artistDisplayText: null,
+      annotation: `[Bandcamp credits blurb](https://web.archive.org/web/20201024170202/https://homestuck.bandcamp.com/track/sburban-countdown-3) on "Homestuck Vol. 1-4 (with Midnight Crew: Drawing Dead)"`,
+      date: new Date('10/25/2019'),
+      body:
+        `Written by [[artist:michael-guy-bowman|Michael Guy Bowman]]<br>\n` +
+        `Arrangement by [[artist:mark-j-hadley|Mark Hadley]]`,
+      secondDate: null,
+      dateKind: null,
+      accessDate: new Date('10/24/2020'),
+      accessKind: 'captured',
+    },
+    {
+      artists: [artist3],
+      artistDisplayText: null,
+      annotation: `[fake](https://web.archive.org/web/20201024170202/https://homestuck.bandcamp.com/fake)`,
+      date: new Date('7/20/2019'),
+      body: `This isn't real!`,
+      secondDate: null,
+      dateKind: null,
+      accessDate: new Date('4/13/2024'),
+      accessKind: 'captured',
+    },
+    {
+      artists: [artist3],
+      artistDisplayText: null,
+      annotation: `[fake](https://homestuck.com/fake)`,
+      date: new Date('10/25/2011'),
+      body: `This isn't real either!`,
+      secondDate: null,
+      dateKind: null,
+      accessDate: new Date('10/27/2011'),
+      accessKind: 'accessed',
+    },
+    {
+      artists: [artist3],
+      artistDisplayText: null,
+      annotation: `[fake](https://web.archive.org/web/20201024170202/https://homestuck.bandcamp.com/fake)`,
+      date: new Date('7/20/2019'),
+      body: `Not this one, neither!`,
+      secondDate: null,
+      dateKind: null,
+      accessDate: new Date('4/13/2024'),
+      accessKind: 'accessed',
+    },
+  ]);
+
+  t.same(composite.expose.compute({
+    artistData,
+    from:
+      `<i>Homestuck:</i> ([MSPA sound credits](https://web.archive.org/web/20120805031705/http://www.mspaintadventures.com:80/soundcredits.html), sometime 6/21/2012 - 8/5/2012)\n` +
+      `\n` +
+      `[[flash:246|Page 2146]] - <b>"Sburban Countdown"</b><br>\n` +
+      `Available on Bandcamp in [[album:homestuck-vol-1-4|Homestuck Vol. 1-4]]<br>\n` +
+      `Written by [[artist:michael-guy-bowman|Michael Guy Bowman]]<br>\n` +
+      `Arrangement by [[artist:mark-j-hadley|Mark Hadley]]\n` +
+      `\n` +
+      `<i>Homestuck:</i> ([fake](https://web.archive.org/web/20201024170202/https://homestuck.bandcamp.com/fake), 7/20/2019 - 7/20/2022 captured 4/13/2024)\n` +
+      `It's goin' once.\n` +
+      `\n` +
+      `<i>Homestuck:</i> (10/25/2011 - 10/28/2011 accessed 10/27/2011)\n` +
+      `It's goin' twice.\n` +
+      `\n` +
+      `<i>Homestuck:</i> ([fake](https://web.archive.org/web/20201024170202/https://homestuck.bandcamp.com/fake), 7/20/2019 - 7/20/2022 accessed 4/13/2024)\n` +
+      `It's goin' thrice!\n`
+  }), [
+    {
+      artists: [artist3],
+      artistDisplayText: null,
+      annotation: `[MSPA sound credits](https://web.archive.org/web/20120805031705/http://www.mspaintadventures.com:80/soundcredits.html)`,
+      body:
+        `[[flash:246|Page 2146]] - <b>"Sburban Countdown"</b><br>\n` +
+        `Available on Bandcamp in [[album:homestuck-vol-1-4|Homestuck Vol. 1-4]]<br>\n` +
+        `Written by [[artist:michael-guy-bowman|Michael Guy Bowman]]<br>\n` +
+        `Arrangement by [[artist:mark-j-hadley|Mark Hadley]]`,
+      date: new Date('6/21/2012'),
+      secondDate: new Date('8/5/2012'),
+      dateKind: 'sometime',
+      accessDate: new Date('8/5/2012'),
+      accessKind: 'captured',
+    },
+    {
+      artists: [artist3],
+      artistDisplayText: null,
+      annotation: `[fake](https://web.archive.org/web/20201024170202/https://homestuck.bandcamp.com/fake)`,
+      body: `It's goin' once.`,
+      date: new Date('7/20/2019'),
+      secondDate: new Date('7/20/2022'),
+      dateKind: null,
+      accessDate: new Date('4/13/2024'),
+      accessKind: 'captured',
+    },
+    {
+      artists: [artist3],
+      artistDisplayText: null,
+      annotation: '', // TODO: This should be null, but the regex isn't structured for that, at the moment.
+      body: `It's goin' twice.`,
+      date: new Date('10/25/2011'),
+      secondDate: new Date('10/28/2011'),
+      dateKind: null,
+      accessDate: new Date('10/27/2011'),
+      accessKind: 'accessed',
+
+    },
+    {
+      artists: [artist3],
+      artistDisplayText: null,
+      annotation: `[fake](https://web.archive.org/web/20201024170202/https://homestuck.bandcamp.com/fake)`,
+      body: `It's goin' thrice!`,
+      date: new Date('7/20/2019'),
+      secondDate: new Date('7/20/2022'),
+      dateKind: null,
+      accessDate: new Date('4/13/2024'),
+      accessKind: 'accessed',
+    },
+  ]);
+
+  t.same(composite.expose.compute({
+    artistData,
+    from:
+      `<i>Homestuck:</i> ([MSPA sound credits](https://web.archive.org/web/20120805031705/http://www.mspaintadventures.com:80/soundcredits.html), sometime 6/21/2012 - 8/5/2012)\n` +
+      `\n` +
+      `[[flash:246|Page 2146]] - <b>"Sburban Countdown"</b><br>\n` +
+      `Available on Bandcamp in [[album:homestuck-vol-1-4|Homestuck Vol. 1-4]]<br>\n` +
+      `Written by [[artist:michael-guy-bowman|Michael Guy Bowman]]<br>\n` +
+      `Arrangement by [[artist:mark-j-hadley|Mark Hadley]]\n` +
+      `\n` +
+      `<i>Homestuck:</i> ([fake](https://web.archive.org/web/20201024170202/https://homestuck.bandcamp.com/fake), 7/20/2019 - 7/20/2022 captured 4/13/2024)\n` +
+      `It's goin' once.\n` +
+      `\n` +
+      `<i>Homestuck:</i> (10/25/2011 - 10/28/2011 accessed 10/27/2011)\n` +
+      `It's goin' twice.\n` +
+      `\n` +
+      `<i>Homestuck:</i> ([fake](https://web.archive.org/web/20201024170202/https://homestuck.bandcamp.com/fake), 7/20/2019 - 7/20/2022 accessed 4/13/2024)\n` +
+      `It's goin' thrice!\n`
+  }), [
+    {
+      artists: [artist3],
+      artistDisplayText: null,
+      annotation: `[MSPA sound credits](https://web.archive.org/web/20120805031705/http://www.mspaintadventures.com:80/soundcredits.html)`,
+      body:
+        `[[flash:246|Page 2146]] - <b>"Sburban Countdown"</b><br>\n` +
+        `Available on Bandcamp in [[album:homestuck-vol-1-4|Homestuck Vol. 1-4]]<br>\n` +
+        `Written by [[artist:michael-guy-bowman|Michael Guy Bowman]]<br>\n` +
+        `Arrangement by [[artist:mark-j-hadley|Mark Hadley]]`,
+      date: new Date('6/21/2012'),
+      secondDate: new Date('8/5/2012'),
+      dateKind: 'sometime',
+      accessDate: new Date('8/5/2012'),
+      accessKind: 'captured',
+    },
+    {
+      artists: [artist3],
+      artistDisplayText: null,
+      annotation: `[fake](https://web.archive.org/web/20201024170202/https://homestuck.bandcamp.com/fake)`,
+      body: `It's goin' once.`,
+      date: new Date('7/20/2019'),
+      secondDate: new Date('7/20/2022'),
+      dateKind: null,
+      accessDate: new Date('4/13/2024'),
+      accessKind: 'captured',
+    },
+    {
+      artists: [artist3],
+      artistDisplayText: null,
+      annotation: '', // TODO: This should be null, but the regex isn't structured for that, at the moment.
+      body: `It's goin' twice.`,
+      date: new Date('10/25/2011'),
+      secondDate: new Date('10/28/2011'),
+      dateKind: null,
+      accessDate: new Date('10/27/2011'),
+      accessKind: 'accessed',
+
+    },
+    {
+      artists: [artist3],
+      artistDisplayText: null,
+      annotation: `[fake](https://web.archive.org/web/20201024170202/https://homestuck.bandcamp.com/fake)`,
+      body: `It's goin' thrice!`,
+      date: new Date('7/20/2019'),
+      secondDate: new Date('7/20/2022'),
+      dateKind: null,
+      accessDate: new Date('4/13/2024'),
+      accessKind: 'accessed',
+    },
+  ]);
+
+  t.same(composite.expose.compute({
+    artistData,
+    from:
+      `<i>Homestuck:</i> ([Homestuck sound credits](https://web.archive.org/web/20180717171235/https://www.homestuck.com/credits/sound), excerpt, around 4/3/2018)\n` +
+      `blablabla\n` +
+      `<i>Homestuck:</i> ([fake](https://web.archive.org/web/20201024170202/https://homestuck.bandcamp.com/fake), around 7/20/2019 - 7/20/2022 captured 4/13/2024)\n` +
+      `Snoopin', snoopin', snoo,\n` +
+      `<i>Homestuck:</i> ([fake](https://web.archive.org/web/20201024170202/https://homestuck.bandcamp.com/fake), throughout 7/20/2019 - 7/20/2022 accessed 4/13/2024)\n` +
+      `~ pingas ~\n`
+  }), [
+    {
+      artists: [artist3],
+      artistDisplayText: null,
+      annotation: `[Homestuck sound credits](https://web.archive.org/web/20180717171235/https://www.homestuck.com/credits/sound), excerpt`,
+      body: `blablabla`,
+      date: new Date('4/3/2018'),
+      secondDate: null,
+      dateKind: 'around',
+      accessDate: new Date('7/17/2018'),
+      accessKind: 'captured',
+    },
+    {
+      artists: [artist3],
+      artistDisplayText: null,
+      annotation: `[fake](https://web.archive.org/web/20201024170202/https://homestuck.bandcamp.com/fake)`,
+      body: `Snoopin', snoopin', snoo,`,
+      date: new Date('7/20/2019'),
+      secondDate: new Date('7/20/2022'),
+      dateKind: 'around',
+      accessDate: new Date('4/13/2024'),
+      accessKind: 'captured',
+    },
+    {
+      artists: [artist3],
+      artistDisplayText: null,
+      annotation: `[fake](https://web.archive.org/web/20201024170202/https://homestuck.bandcamp.com/fake)`,
+      body: `~ pingas ~`,
+      date: new Date('7/20/2019'),
+      secondDate: new Date('7/20/2022'),
+      dateKind: 'throughout',
+      accessDate: new Date('4/13/2024'),
+      accessKind: 'accessed',
     },
   ]);
 });
diff --git a/test/unit/data/things/album.js b/test/unit/data/things/album.js
index 06265b09..a64488f7 100644
--- a/test/unit/data/things/album.js
+++ b/test/unit/data/things/album.js
@@ -1,62 +1,27 @@
 import t from 'tap';
 
-import {linkAndBindWikiData} from '#test-lib';
 import thingConstructors from '#things';
 
-const {
-  Album,
-  ArtTag,
-  Artist,
-  Track,
-  TrackSection,
-} = thingConstructors;
-
-function stubArtTag(tagName = `Test Art Tag`) {
-  const tag = new ArtTag();
-  tag.name = tagName;
-
-  return tag;
-}
-
-function stubArtistAndContribs() {
-  const artist = new Artist();
-  artist.name = `Test Artist`;
-
-  const contribs = [{artist: `Test Artist`, annotation: null}];
-  const badContribs = [{artist: `Figment of Your Imagination`, annotation: null}];
-
-  return {artist, contribs, badContribs};
-}
-
-function stubTrack(directory = 'foo') {
-  const track = new Track();
-  track.directory = directory;
-
-  return track;
-}
-
-function stubTrackSection(album, tracks, directory = 'baz') {
-  const trackSection = new TrackSection();
-  trackSection.unqualifiedDirectory = directory;
-  trackSection.tracks = tracks.map(t => Thing.getReference(t));
-  trackSection.ownTrackData = tracks;
-  trackSection.ownAlbumData = [album];
-  return trackSection;
-}
+import {
+  linkAndBindWikiData,
+  stubArtistAndContribs,
+  stubThing,
+  stubWikiData,
+} from '#test-lib';
 
 t.test(`Album.artTags`, t => {
+  const {Album, ArtTag} = thingConstructors;
+
   t.plan(3);
 
-  const {artist, contribs} = stubArtistAndContribs();
-  const album = new Album();
-  const tag1 = stubArtTag(`Tag 1`);
-  const tag2 = stubArtTag(`Tag 2`);
+  const wikiData = stubWikiData();
 
-  const {XXX_decacheWikiData} = linkAndBindWikiData({
-    albumData: [album],
-    artistData: [artist],
-    artTagData: [tag1, tag2],
-  });
+  const {contribs} = stubArtistAndContribs(wikiData);
+  const album = stubThing(wikiData, Album);
+  const tag1 = stubThing(wikiData, ArtTag, {name: `Tag 1`});
+  const tag2 = stubThing(wikiData, ArtTag, {name: `Tag 2`});
+
+  linkAndBindWikiData(wikiData);
 
   t.same(album.artTags, [],
     `artTags #1: defaults to empty array`);
@@ -73,15 +38,16 @@ t.test(`Album.artTags`, t => {
 });
 
 t.test(`Album.bannerDimensions`, t => {
+  const {Album} = thingConstructors;
+
   t.plan(4);
 
-  const album = new Album();
-  const {artist, contribs, badContribs} = stubArtistAndContribs();
+  const wikiData = stubWikiData();
 
-  linkAndBindWikiData({
-    albumData: [album],
-    artistData: [artist],
-  });
+  const album = stubThing(wikiData, Album);
+  const {contribs, badContribs} = stubArtistAndContribs(wikiData);
+
+  linkAndBindWikiData(wikiData);
 
   t.equal(album.bannerDimensions, null,
     `Album.bannerDimensions #1: defaults to null`);
@@ -103,15 +69,16 @@ t.test(`Album.bannerDimensions`, t => {
 });
 
 t.test(`Album.bannerFileExtension`, t => {
+  const {Album} = thingConstructors;
+
   t.plan(5);
 
-  const album = new Album();
-  const {artist, contribs, badContribs} = stubArtistAndContribs();
+  const wikiData = stubWikiData();
+
+  const album = stubThing(wikiData, Album);
+  const {contribs, badContribs} = stubArtistAndContribs(wikiData);
 
-  linkAndBindWikiData({
-    albumData: [album],
-    artistData: [artist],
-  });
+  linkAndBindWikiData(wikiData);
 
   t.equal(album.bannerFileExtension, null,
     `Album.bannerFileExtension #1: defaults to null`);
@@ -138,15 +105,16 @@ t.test(`Album.bannerFileExtension`, t => {
 });
 
 t.test(`Album.bannerStyle`, t => {
+  const {Album} = thingConstructors;
+
   t.plan(4);
 
-  const album = new Album();
-  const {artist, contribs, badContribs} = stubArtistAndContribs();
+  const wikiData = stubWikiData();
+
+  const album = stubThing(wikiData, Album);
+  const {contribs, badContribs} = stubArtistAndContribs(wikiData);
 
-  linkAndBindWikiData({
-    albumData: [album],
-    artistData: [artist],
-  });
+  linkAndBindWikiData(wikiData);
 
   t.equal(album.bannerStyle, null,
     `Album.bannerStyle #1: defaults to null`);
@@ -168,15 +136,16 @@ t.test(`Album.bannerStyle`, t => {
 });
 
 t.test(`Album.coverArtDate`, t => {
+  const {Album} = thingConstructors;
+
   t.plan(6);
 
-  const album = new Album();
-  const {artist, contribs, badContribs} = stubArtistAndContribs();
+  const wikiData = stubWikiData();
+
+  const album = stubThing(wikiData, Album);
+  const {contribs, badContribs} = stubArtistAndContribs(wikiData);
 
-  linkAndBindWikiData({
-    albumData: [album],
-    artistData: [artist],
-  });
+  linkAndBindWikiData(wikiData);
 
   t.equal(album.coverArtDate, null,
     `Album.coverArtDate #1: defaults to null`);
@@ -208,15 +177,16 @@ t.test(`Album.coverArtDate`, t => {
 });
 
 t.test(`Album.coverArtFileExtension`, t => {
+  const {Album} = thingConstructors;
+
   t.plan(5);
 
-  const album = new Album();
-  const {artist, contribs, badContribs} = stubArtistAndContribs();
+  const wikiData = stubWikiData();
 
-  linkAndBindWikiData({
-    albumData: [album],
-    artistData: [artist],
-  });
+  const album = stubThing(wikiData, Album);
+  const {contribs, badContribs} = stubArtistAndContribs(wikiData);
+
+  linkAndBindWikiData(wikiData);
 
   t.equal(album.coverArtFileExtension, null,
     `Album.coverArtFileExtension #1: is null if coverArtistContribs empty (1/2)`);
@@ -244,136 +214,105 @@ t.test(`Album.coverArtFileExtension`, t => {
 });
 
 t.test(`Album.tracks`, t => {
-  t.plan(5);
+  const {Album, Track, TrackSection} = thingConstructors;
 
-  const album = new Album();
+  t.plan(4);
+
+  let wikiData = stubWikiData();
+
+  const album = stubThing(wikiData, Album);
   album.directory = 'foo';
 
-  const track1 = stubTrack('track1');
-  const track2 = stubTrack('track2');
-  const track3 = stubTrack('track3');
+  const track1 = stubThing(wikiData, Track, {directory: 'track1'});
+  const track2 = stubThing(wikiData, Track, {directory: 'track2'});
+  const track3 = stubThing(wikiData, Track, {directory: 'track3'});
   const tracks = [track1, track2, track3];
 
-  const section1 = stubTrackSection(album, [], 'section1');
-  const section2 = stubTrackSection(album, [], 'section2');
-  const section3 = stubTrackSection(album, [], 'section3');
-  const section4 = stubTrackSection(album, [], 'section4');
-  const section5 = stubTrackSection(album, [], 'section5');
-  const section6 = stubTrackSection(album, [], 'section6');
+  const section1 = stubThing(wikiData, TrackSection, {unqualifiedDirectory: 'section1'});
+  const section2 = stubThing(wikiData, TrackSection, {unqualifiedDirectory: 'section2'});
+  const section3 = stubThing(wikiData, TrackSection, {unqualifiedDirectory: 'section3'});
+  const section4 = stubThing(wikiData, TrackSection, {unqualifiedDirectory: 'section4'});
+  const section5 = stubThing(wikiData, TrackSection, {unqualifiedDirectory: 'section5'});
+  const section6 = stubThing(wikiData, TrackSection, {unqualifiedDirectory: 'section6'});
   const sections = [section1, section2, section3, section4, section5, section6];
 
-  const section1_ref = `unqualified-track-section:section1`;
-  const section2_ref = `unqualified-track-section:section2`;
-  const section3_ref = `unqualified-track-section:section3`;
-  const section4_ref = `unqualified-track-section:section4`;
-  const section5_ref = `unqualified-track-section:section5`;
-  const section6_ref = `unqualified-track-section:section6`;
+  wikiData = null;
 
   for (const track of tracks) {
     track.albumData = [album];
   }
 
   for (const section of sections) {
-    section.ownAlbumData = [album];
+    section.albumData = [album];
   }
 
   t.same(album.tracks, [],
     `Album.tracks #1: defaults to empty array`);
 
-  section1.tracks = ['track:track1', 'track:track2', 'track:track3'];
-  section1.ownTrackData = [track1, track2, track3];
+  section1.tracks = [track1, track2, track3];
 
-  album.trackSections = [section1_ref];
-  album.ownTrackSectionData = [section1];
+  album.trackSections = [section1];
 
   t.same(album.tracks, [track1, track2, track3],
     `Album.tracks #2: pulls tracks from one track section`);
 
-  section1.tracks = ['track:track1'];
-  section2.tracks = ['track:track2', 'track:track3'];
+  section1.tracks = [track1];
+  section2.tracks = [track2, track3];
 
-  section1.ownTrackData = [track1];
-  section2.ownTrackData = [track2, track3];
-
-  album.trackSections = [section1_ref, section2_ref];
-  album.ownTrackSectionData = [section1, section2];
+  album.trackSections = [section1, section2];
 
   t.same(album.tracks, [track1, track2, track3],
     `Album.tracks #3: pulls tracks from multiple track sections`);
 
-  section1.tracks = ['track:track1', 'track:does-not-exist'];
-  section2.tracks = ['track:this-one-neither', 'track:track2'];
-  section3.tracks = ['track:effectively-empty-section'];
-  section4.tracks = ['track:track3'];
-
-  section1.ownTrackData = [track1];
-  section2.ownTrackData = [track2];
-  section3.ownTrackData = [];
-  section4.ownTrackData = [track3];
-
-  album.trackSections = [section1_ref, section2_ref, section3_ref, section4_ref];
-  album.ownTrackSectionData = [section1, section2, section3, section4];
-
-  t.same(album.tracks, [track1, track2, track3],
-    `Album.tracks #4: filters out references without matches`);
-
-  section1.tracks = ['track:track1'];
+  section1.tracks = [track1];
   section2.tracks = [];
-  section3.tracks = ['track:track2'];
+  section3.tracks = [track2];
   section4.tracks = [];
   section5.tracks = [];
-  section6.tracks = ['track:track3'];
+  section6.tracks = [track3];
 
-  section1.ownTrackData = [track1];
-  section2.ownTrackData = [];
-  section3.ownTrackData = [track2];
-  section4.ownTrackData = [];
-  section5.ownTrackData = [];
-  section6.ownTrackData = [track3];
-
-  album.trackSections = [section1_ref, section2_ref, section3_ref, section4_ref, section5_ref, section6_ref];
-  album.ownTrackSectionData = [section1, section2, section3, section4, section5, section6];
+  album.trackSections = [section1, section2, section3, section4, section5, section6];
 
   t.same(album.tracks, [track1, track2, track3],
-    `Album.tracks #5: skips empty track sections`);
+    `Album.tracks #4: skips empty track sections`);
 });
 
 t.test(`Album.trackSections`, t => {
+  const {Album, Track, TrackSection} = thingConstructors;
+
   t.plan(7);
 
-  const album = new Album();
+  let wikiData = stubWikiData();
 
-  const track1 = stubTrack('track1');
-  const track2 = stubTrack('track2');
-  const track3 = stubTrack('track3');
-  const track4 = stubTrack('track4');
+  const album = stubThing(wikiData, Album);
+
+  const track1 = stubThing(wikiData, Track, {directory: 'track1'});
+  const track2 = stubThing(wikiData, Track, {directory: 'track2'});
+  const track3 = stubThing(wikiData, Track, {directory: 'track3'});
+  const track4 = stubThing(wikiData, Track, {directory: 'track4'});
   const tracks = [track1, track2, track3, track4];
 
-  const section1 = stubTrackSection(album, [], 'section1');
-  const section2 = stubTrackSection(album, [], 'section2');
-  const section3 = stubTrackSection(album, [], 'section3');
-  const section4 = stubTrackSection(album, [], 'section4');
-  const section5 = stubTrackSection(album, [], 'section5');
+  const section1 = stubThing(wikiData, TrackSection, {unqualifiedDirectory: 'section1'});
+  const section2 = stubThing(wikiData, TrackSection, {unqualifiedDirectory: 'section2'});
+  const section3 = stubThing(wikiData, TrackSection, {unqualifiedDirectory: 'section3'});
+  const section4 = stubThing(wikiData, TrackSection, {unqualifiedDirectory: 'section4'});
+  const section5 = stubThing(wikiData, TrackSection, {unqualifiedDirectory: 'section5'});
   const sections = [section1, section2, section3, section4, section5];
 
-  const section1_ref = `unqualified-track-section:section1`;
-  const section2_ref = `unqualified-track-section:section2`;
-  const section3_ref = `unqualified-track-section:section3`;
-  const section4_ref = `unqualified-track-section:section4`;
-  const section5_ref = `unqualified-track-section:section5`;
+  wikiData = null;
 
   for (const track of tracks) {
     track.albumData = [album];
   }
 
-  section1.tracks = ['track:track1', 'track:track2'];
-  section2.tracks = ['track:track3', 'track:track4'];
+  for (const section of sections) {
+    section.albumData = [album];
+  }
 
-  section1.ownTrackData = [track1, track2];
-  section2.ownTrackData = [track3, track4];
+  section1.tracks = [track1, track2];
+  section2.tracks = [track3, track4];
 
-  album.trackSections = [section1_ref, section2_ref];
-  album.ownTrackSectionData = [section1, section2];
+  album.trackSections = [section1, section2];
 
   t.match(album.trackSections, [
     {tracks: [track1, track2]},
@@ -385,19 +324,14 @@ t.test(`Album.trackSections`, t => {
     {tracks: [track3, track4], startIndex: 2},
   ], `Album.trackSections #2: exposes startIndex`);
 
-  section1.tracks = ['track:track1'];
-  section2.tracks = ['track:track2'];
-  section3.tracks = ['track:track3'];
-
-  section1.ownTrackData = [track1];
-  section2.ownTrackData = [track2];
-  section3.ownTrackData = [track3];
+  section1.tracks = [track1];
+  section2.tracks = [track2];
+  section3.tracks = [track3];
 
   section1.name = 'First section';
   section2.name = 'Second section';
 
-  album.trackSections = [section1_ref, section2_ref, section3_ref];
-  album.ownTrackSectionData = [section1, section2, section3];
+  album.trackSections = [section1, section2, section3];
 
   t.match(album.trackSections, [
     {name: 'First section', tracks: [track1]},
@@ -411,7 +345,7 @@ t.test(`Album.trackSections`, t => {
 
   // XXX_decacheWikiData
   album.trackSections = [];
-  album.trackSections = [section1_ref, section2_ref, section3_ref];
+  album.trackSections = [section1, section2, section3];
 
   t.match(album.trackSections, [
     {tracks: [track1], color: '#123456'},
@@ -423,7 +357,7 @@ t.test(`Album.trackSections`, t => {
 
   // XXX_decacheWikiData
   album.trackSections = [];
-  album.trackSections = [section1_ref, section2_ref, section3_ref];
+  album.trackSections = [section1, section2, section3];
 
   t.match(album.trackSections, [
     {tracks: [track1], dateOriginallyReleased: null},
@@ -436,7 +370,7 @@ t.test(`Album.trackSections`, t => {
 
   // XXX_decacheWikiData
   album.trackSections = [];
-  album.trackSections = [section1_ref, section2_ref, section3_ref];
+  album.trackSections = [section1, section2, section3];
 
   t.match(album.trackSections, [
     {tracks: [track1], isDefaultTrackSection: true},
@@ -444,17 +378,11 @@ t.test(`Album.trackSections`, t => {
     {tracks: [track3], isDefaultTrackSection: false},
   ], `Album.trackSections #6: exposes isDefaultTrackSection, defaults to false`);
 
-  section1.tracks = ['track:track1', 'track:track2', 'track:snooping'];
-  section2.tracks = ['track:track3', 'track:as-usual'];
+  section1.tracks = [track1, track2];
+  section2.tracks = [track3];
   section3.tracks = [];
-  section4.tracks = ['track:icy', 'track:chilly', 'track:frigid'];
-  section5.tracks = ['track:track4'];
-
-  section1.ownTrackData = [track1, track2];
-  section2.ownTrackData = [track3];
-  section3.ownTrackData = [];
-  section4.ownTrackData = [];
-  section5.ownTrackData = [track4];
+  section4.tracks = [];
+  section5.tracks = [track4];
 
   section1.color = '#112233';
   section2.color = '#334455';
@@ -462,8 +390,7 @@ t.test(`Album.trackSections`, t => {
   section4.color = '#556677';
   section5.color = '#778899';
 
-  album.trackSections = [section1_ref, section2_ref, section3_ref, section4_ref, section5_ref];
-  album.ownTrackSectionData = [section1, section2, section3, section4, section5];
+  album.trackSections = [section1, section2, section3, section4, section5];
 
   t.match(album.trackSections, [
     {tracks: [track1, track2], color: '#112233'},
@@ -471,19 +398,20 @@ t.test(`Album.trackSections`, t => {
     {tracks: [],               color: '#bbbbba'},
     {tracks: [],               color: '#556677'},
     {tracks: [track4],         color: '#778899'},
-  ], `Album.trackSections #7: filters out references without matches, keeps empty sections`);
+  ], `Album.trackSections #7: keeps empty sections`);
 });
 
 t.test(`Album.wallpaperFileExtension`, t => {
+  const {Album} = thingConstructors;
+
   t.plan(5);
 
-  const album = new Album();
-  const {artist, contribs, badContribs} = stubArtistAndContribs();
+  const wikiData = stubWikiData();
+
+  const album = stubThing(wikiData, Album);
+  const {contribs, badContribs} = stubArtistAndContribs(wikiData);
 
-  linkAndBindWikiData({
-    albumData: [album],
-    artistData: [artist],
-  });
+  linkAndBindWikiData(wikiData);
 
   t.equal(album.wallpaperFileExtension, null,
     `Album.wallpaperFileExtension #1: defaults to null`);
@@ -510,15 +438,16 @@ t.test(`Album.wallpaperFileExtension`, t => {
 });
 
 t.test(`Album.wallpaperStyle`, t => {
+  const {Album} = thingConstructors;
+
   t.plan(4);
 
-  const album = new Album();
-  const {artist, contribs, badContribs} = stubArtistAndContribs();
+  const wikiData = stubWikiData();
+
+  const album = stubThing(wikiData, Album);
+  const {contribs, badContribs} = stubArtistAndContribs(wikiData);
 
-  linkAndBindWikiData({
-    albumData: [album],
-    artistData: [artist],
-  });
+  linkAndBindWikiData(wikiData);
 
   t.equal(album.wallpaperStyle, null,
     `Album.wallpaperStyle #1: defaults to null`);
diff --git a/test/unit/data/things/art-tag.js b/test/unit/data/things/art-tag.js
index cf2135c6..015acd0e 100644
--- a/test/unit/data/things/art-tag.js
+++ b/test/unit/data/things/art-tag.js
@@ -1,66 +1,10 @@
 import t from 'tap';
 
-import {linkAndBindWikiData} from '#test-lib';
 import thingConstructors from '#things';
 
-const {
-  Album,
-  Artist,
-  ArtTag,
-  Track,
-  trackSection,
-} = thingConstructors;
-
-function stubAlbum(tracks, directory = 'bar') {
-  const album = new Album();
-  album.directory = directory;
-
-  const trackSection = stubTrackSection(album, tracks);
-  album.trackSections = [`unqualified-track-section:${trackSection.unqualifiedDirectory}`];
-  album.ownTrackSectionData = [trackSection];
-
-  return album;
-}
-
-function stubTrackSection(album, tracks, directory = 'baz') {
-  const trackSection = new TrackSection();
-  trackSection.unqualifiedDirectory = directory;
-  trackSection.tracks = tracks.map(t => Thing.getReference(t));
-  trackSection.ownTrackData = tracks;
-  trackSection.ownAlbumData = [album];
-  return trackSection;
-}
-
-function stubTrack(directory = 'foo') {
-  const track = new Track();
-  track.directory = directory;
-
-  return track;
-}
-
-function stubTrackAndAlbum(trackDirectory = 'foo', albumDirectory = 'bar') {
-  const track = stubTrack(trackDirectory);
-  const album = stubAlbum([track], albumDirectory);
-
-  return {track, album};
-}
-
-function stubArtist(artistName = `Test Artist`) {
-  const artist = new Artist();
-  artist.name = artistName;
-
-  return artist;
-}
-
-function stubArtistAndContribs(artistName = `Test Artist`) {
-  const artist = stubArtist(artistName);
-  const contribs = [{artist: artistName, annotation: null}];
-  const badContribs = [{artist: `Figment of Your Imagination`, annotation: null}];
-
-  return {artist, contribs, badContribs};
-}
-
 t.test(`ArtTag.nameShort`, t => {
+  const {ArtTag} = thingConstructors;
+
   t.plan(3);
 
   const artTag = new ArtTag();
diff --git a/test/unit/data/things/flash.js b/test/unit/data/things/flash.js
index 62059604..de39e80d 100644
--- a/test/unit/data/things/flash.js
+++ b/test/unit/data/things/flash.js
@@ -1,39 +1,24 @@
 import t from 'tap';
 
-import {linkAndBindWikiData} from '#test-lib';
 import thingConstructors from '#things';
 
-const {
-  Flash,
-  FlashAct,
-  Thing,
-} = thingConstructors;
-
-function stubFlash(directory = 'foo') {
-  const flash = new Flash();
-  flash.directory = directory;
-
-  return flash;
-}
-
-function stubFlashAct(flashes, directory = 'bar') {
-  const flashAct = new FlashAct();
-  flashAct.directory = directory;
-  flashAct.flashes = flashes.map(flash => Thing.getReference(flash));
-
-  return flashAct;
-}
+import {
+  linkAndBindWikiData,
+  stubThing,
+  stubWikiData,
+} from '#test-lib';
 
 t.test(`Flash.color`, t => {
+  const {Flash, FlashAct} = thingConstructors;
+
   t.plan(4);
 
-  const flash = stubFlash();
-  const flashAct = stubFlashAct([flash]);
+  const wikiData = stubWikiData();
+
+  const flash = stubThing(wikiData, Flash, {directory: 'my-flash'});
+  const flashAct = stubThing(wikiData, FlashAct, {flashes: ['flash:my-flash']});
 
-  const {XXX_decacheWikiData} = linkAndBindWikiData({
-    flashData: [flash],
-    flashActData: [flashAct],
-  });
+  const {XXX_decacheWikiData} = linkAndBindWikiData(wikiData);
 
   t.equal(flash.color, null,
     `color #1: defaults to null`);
diff --git a/test/unit/data/things/track.js b/test/unit/data/things/track.js
index c6695b6f..403dc064 100644
--- a/test/unit/data/things/track.js
+++ b/test/unit/data/things/track.js
@@ -1,86 +1,20 @@
 import t from 'tap';
 
-import {linkAndBindWikiData} from '#test-lib';
+import {bindFind} from '#find';
 import thingConstructors from '#things';
 
-const {
-  Album,
-  ArtTag,
-  Artist,
-  Flash,
-  FlashAct,
-  Thing,
-  Track,
-  TrackSection,
-} = thingConstructors;
-
-function stubAlbum(tracks, directory = 'bar') {
-  const album = new Album();
-  album.directory = directory;
-
-  const trackSection = stubTrackSection(album, tracks);
-  album.trackSections = [`unqualified-track-section:${trackSection.unqualifiedDirectory}`];
-  album.ownTrackSectionData = [trackSection];
-
-  return album;
-}
-
-function stubTrackSection(album, tracks, directory = 'baz') {
-  const trackSection = new TrackSection();
-  trackSection.unqualifiedDirectory = directory;
-  trackSection.tracks = tracks.map(t => Thing.getReference(t));
-  trackSection.ownTrackData = tracks;
-  trackSection.ownAlbumData = [album];
-  return trackSection;
-}
-
-function stubTrack(directory = 'foo') {
-  const track = new Track();
-  track.directory = directory;
-
-  return track;
-}
-
-function stubTrackAndAlbum(trackDirectory = 'foo', albumDirectory = 'bar') {
-  const track = stubTrack(trackDirectory);
-  const album = stubAlbum([track], albumDirectory);
-
-  return {track, album};
-}
-
-function stubArtist(artistName = `Test Artist`) {
-  const artist = new Artist();
-  artist.name = artistName;
-
-  return artist;
-}
-
-function stubArtistAndContribs(artistName = `Test Artist`) {
-  const artist = stubArtist(artistName);
-  const contribs = [{artist: artistName, annotation: null}];
-  const badContribs = [{artist: `Figment of Your Imagination`, annotation: null}];
-
-  return {artist, contribs, badContribs};
-}
-
-function stubArtTag(tagName = `Test Art Tag`) {
-  const tag = new ArtTag();
-  tag.name = tagName;
-
-  return tag;
-}
-
-function stubFlashAndAct(directory = 'zam') {
-  const flash = new Flash();
-  flash.directory = directory;
-
-  const flashAct = new FlashAct();
-  flashAct.flashes = [Thing.getReference(flash)];
-
-  return {flash, flashAct};
-}
+import {
+  linkAndBindWikiData,
+  stubArtistAndContribs,
+  stubFlashAndAct,
+  stubThing,
+  stubTrackAndAlbum,
+  stubWikiData,
+} from '#test-lib';
 
 t.test(`Track.album`, t => {
+  const {Album, Track, TrackSection} = thingConstructors;
+
   t.plan(6);
 
   // Note: These asserts use manual albumData/trackData relationships
@@ -88,32 +22,28 @@ t.test(`Track.album`, t => {
   // be relevant for this case. Other properties use the same underlying
   // get-album behavior as Track.album so aren't tested as aggressively.
 
-  const track1 = stubTrack('track1');
-  const track2 = stubTrack('track2');
-  const album1 = new Album();
-  const album2 = new Album();
-  const section1 = new TrackSection();
-  const section2 = new TrackSection();
-  section1.unqualifiedDirectory = 'section1';
-  section2.unqualifiedDirectory = 'section2';
-  const section1_ref = `unqualified-track-section:section1`;
-  const section2_ref = `unqualified-track-section:section2`;
+  let wikiData = stubWikiData();
+
+  const track1 = stubThing(wikiData, Track, {directory: 'track1'});
+  const track2 = stubThing(wikiData, Track, {directory: 'track2'});
+  const album1 = stubThing(wikiData, Album);
+  const album2 = stubThing(wikiData, Album);
+  const section1 = stubThing(wikiData, TrackSection, {unqualifiedDirectory: 'section1'});
+  const section2 = stubThing(wikiData, TrackSection, {unqualifiedDirectory: 'section2'});
+
+  wikiData = null;
 
   t.equal(track1.album, null,
     `album #1: defaults to null`);
 
   track1.albumData = [album1, album2];
   track2.albumData = [album1, album2];
-  section1.ownTrackData = [track1];
-  section2.ownTrackData = [track2];
-  section1.ownAlbumData = [album1];
-  section2.ownAlbumData = [album2];
-  section1.tracks = ['track:track1'];
-  section2.tracks = ['track:track2'];
-  album1.trackSections = [section1_ref];
-  album2.trackSections = [section2_ref];
-  album1.ownTrackSectionData = [section1];
-  album2.ownTrackSectionData = [section2];
+  section1.tracks = [track1];
+  section2.tracks = [track2];
+  section1.albumData = [album1];
+  section2.albumData = [album2];
+  album1.trackSections = [section1];
+  album2.trackSections = [section2];
 
   t.equal(track1.album, album1,
     `album #2: is album when album's trackSections matches track`);
@@ -128,23 +58,22 @@ t.test(`Track.album`, t => {
   t.equal(track1.album, null,
     `album #4: is null when track missing albumData`);
 
-  section1.ownTrackData = [];
+  section1.tracks = [];
 
   // XXX_decacheWikiData
   album1.trackSections = [];
-  album1.trackSections = [section1_ref];
+  album1.trackSections = [section1];
   track1.albumData = [];
   track1.albumData = [album2, album1];
 
   t.equal(track1.album, null,
-    `album #5: is null when album track section missing ownTrackData`);
+    `album #5: is null when album track section missing tracks`);
 
-  section1.ownTrackData = [track2];
-  section1.tracks = ['track:track2'];
+  section1.tracks = [track2];
 
   // XXX_decacheWikiData
   album1.trackSections = [];
-  album1.trackSections = [section1_ref];
+  album1.trackSections = [section1];
   track1.albumData = [];
   track1.albumData = [album2, album1];
 
@@ -155,11 +84,13 @@ t.test(`Track.album`, t => {
 t.test(`Track.alwaysReferenceByDirectory`, t => {
   t.plan(7);
 
-  const {track: originalTrack, album: originalAlbum} =
-    stubTrackAndAlbum('original-track', 'original-album');
+  const wikiData = stubWikiData();
+
+  const {track: originalTrack} =
+    stubTrackAndAlbum(wikiData, 'original-track', 'original-album');
 
   const {track: rereleaseTrack, album: rereleaseAlbum} =
-    stubTrackAndAlbum('rerelease-track', 'rerelease-album');
+    stubTrackAndAlbum(wikiData, 'rerelease-track', 'rerelease-album');
 
   originalTrack.name = 'Cowabunga';
   rereleaseTrack.name = 'Cowabunga';
@@ -169,10 +100,7 @@ t.test(`Track.alwaysReferenceByDirectory`, t => {
 
   rereleaseTrack.originalReleaseTrack = 'track:original-track';
 
-  const {XXX_decacheWikiData} = linkAndBindWikiData({
-    albumData: [originalAlbum, rereleaseAlbum],
-    trackData: [originalTrack, rereleaseTrack],
-  });
+  const {XXX_decacheWikiData} = linkAndBindWikiData(wikiData);
 
   t.equal(originalTrack.alwaysReferenceByDirectory, false,
     `alwaysReferenceByDirectory #1: defaults to false`);
@@ -208,19 +136,22 @@ t.test(`Track.alwaysReferenceByDirectory`, t => {
 });
 
 t.test(`Track.artTags`, t => {
+  const {ArtTag} = thingConstructors;
+
   t.plan(6);
 
-  const {track, album} = stubTrackAndAlbum();
-  const {artist, contribs} = stubArtistAndContribs();
-  const tag1 = stubArtTag(`Tag 1`);
-  const tag2 = stubArtTag(`Tag 2`);
+  const wikiData = stubWikiData();
+
+  const {track, album} = stubTrackAndAlbum(wikiData);
+  const {contribs} = stubArtistAndContribs(wikiData);
+
+  const tag1 =
+    stubThing(wikiData, ArtTag, {name: `Tag 1`});
+
+  const tag2 =
+    stubThing(wikiData, ArtTag, {name: `Tag 2`});
 
-  const {XXX_decacheWikiData} = linkAndBindWikiData({
-    albumData: [album],
-    artistData: [artist],
-    artTagData: [tag1, tag2],
-    trackData: [track],
-  });
+  const {XXX_decacheWikiData} = linkAndBindWikiData(wikiData);
 
   t.same(track.artTags, [],
     `artTags #1: defaults to empty array`);
@@ -258,17 +189,28 @@ t.test(`Track.artTags`, t => {
 });
 
 t.test(`Track.artistContribs`, t => {
+  const {Album, Artist, Track, TrackSection} = thingConstructors;
+
   t.plan(4);
 
-  const {track, album} = stubTrackAndAlbum();
-  const artist1 = stubArtist(`Artist 1`);
-  const artist2 = stubArtist(`Artist 2`);
+  const wikiData = stubWikiData();
+
+  const track =
+    stubThing(wikiData, Track);
+
+  const section =
+    stubThing(wikiData, TrackSection, {tracks: [track]});
+
+  const album =
+    stubThing(wikiData, Album, {trackSections: [section]});
 
-  const {XXX_decacheWikiData} = linkAndBindWikiData({
-    albumData: [album],
-    artistData: [artist1, artist2],
-    trackData: [track],
-  });
+  const artist1 =
+    stubThing(wikiData, Artist, {name: `Artist 1`});
+
+  const artist2 =
+    stubThing(wikiData, Artist, {name: `Artist 2`});
+
+  const {XXX_decacheWikiData} = linkAndBindWikiData(wikiData);
 
   t.same(track.artistContribs, [],
     `artistContribs #1: defaults to empty array`);
@@ -280,7 +222,7 @@ t.test(`Track.artistContribs`, t => {
 
   XXX_decacheWikiData();
 
-  t.same(track.artistContribs,
+  t.match(track.artistContribs,
     [{artist: artist1, annotation: `composition`}, {artist: artist2, annotation: null}],
     `artistContribs #2: inherits album artistContribs`);
 
@@ -288,7 +230,7 @@ t.test(`Track.artistContribs`, t => {
     {artist: `Artist 1`, annotation: `arrangement`},
   ];
 
-  t.same(track.artistContribs, [{artist: artist1, annotation: `arrangement`}],
+  t.match(track.artistContribs, [{artist: artist1, annotation: `arrangement`}],
     `artistContribs #3: resolves from own value`);
 
   track.artistContribs = [
@@ -297,79 +239,56 @@ t.test(`Track.artistContribs`, t => {
     {artist: `Artist 2`, annotation: `usual`},
   ];
 
-  t.same(track.artistContribs,
+  t.match(track.artistContribs,
     [{artist: artist1, annotation: `snooping`}, {artist: artist2, annotation: `usual`}],
     `artistContribs #4: filters out names without matches`);
 });
 
 t.test(`Track.color`, t => {
-  t.plan(5);
+  t.plan(4);
+
+  const wikiData = stubWikiData();
 
-  const {track, album} = stubTrackAndAlbum();
+  const {track, album, section} = stubTrackAndAlbum(wikiData);
 
-  const {XXX_decacheWikiData} = linkAndBindWikiData({
-    albumData: [album],
-    trackData: [track],
-  });
+  const {XXX_decacheWikiData} = linkAndBindWikiData(wikiData);
 
   t.equal(track.color, null,
     `color #1: defaults to null`);
 
-  const section = stubTrackSection(album, [track], 'section');
-
   album.color = '#abcdef';
   section.color = '#beeeef';
 
-  album.trackSections = [`unqualified-track-section:section`];
-  album.ownTrackSectionData = [section];
+  album.trackSections = [section];
 
   XXX_decacheWikiData();
 
   t.equal(track.color, '#beeeef',
     `color #2: inherits from track section before album`);
 
-  // Replace the album with a completely fake one. This isn't realistic, since
-  // in correct data, Album.tracks depends on Albums.trackSections and so the
-  // track's album will always have a corresponding track section. But if that
-  // connection breaks for some future reason (with the album still present),
-  // Track.color should still inherit directly from the album.
-  track.albumData = [
-    {
-      constructor: {[Thing.referenceType]: 'album'},
-      [Thing.isThing]: true,
-      color: '#abcdef',
-      tracks: [track],
-      trackSections: [
-        {color: '#baaaad', tracks: []},
-      ],
-    },
-  ];
-
-  t.equal(track.color, '#abcdef',
-    `color #3: inherits from album without matching track section`);
-
   track.color = '#123456';
 
   t.equal(track.color, '#123456',
-    `color #4: is own value`);
+    `color #3: is own value`);
 
   t.throws(() => { track.color = '#aeiouw'; },
     {cause: TypeError},
-    `color #5: must be set to valid color`);
+    `color #4: must be set to valid color`);
 });
 
 t.test(`Track.commentatorArtists`, t => {
+  const {Artist, Track} = thingConstructors;
+
   t.plan(8);
 
-  const track = new Track();
-  const artist1 = stubArtist(`SnooPING`);
-  const artist2 = stubArtist(`ASUsual`);
-  const artist3 = stubArtist(`Icy`);
+  const wikiData = stubWikiData();
+
+  const track = stubThing(wikiData, Track);
+  const artist1 = stubThing(wikiData, Artist, {name: `SnooPING`});
+  const artist2 = stubThing(wikiData, Artist, {name: `ASUsual`});
+  const artist3 = stubThing(wikiData, Artist, {name: `Icy`});
 
-  linkAndBindWikiData({
-    trackData: [track],
-    artistData: [artist1, artist2, artist3],
-  });
+  linkAndBindWikiData(wikiData);
 
   // Keep track of the last commentary string in a separate value, since
   // the track.commentary property exposes as a completely different format
@@ -435,17 +354,17 @@ t.test(`Track.commentatorArtists`, t => {
 });
 
 t.test(`Track.coverArtistContribs`, t => {
+  const {Artist} = thingConstructors;
+
   t.plan(5);
 
-  const {track, album} = stubTrackAndAlbum();
-  const artist1 = stubArtist(`Artist 1`);
-  const artist2 = stubArtist(`Artist 2`);
+  const wikiData = stubWikiData();
 
-  const {XXX_decacheWikiData} = linkAndBindWikiData({
-    albumData: [album],
-    artistData: [artist1, artist2],
-    trackData: [track],
-  });
+  const {track, album} = stubTrackAndAlbum(wikiData);
+  const artist1 = stubThing(wikiData, Artist, {name: `Artist 1`});
+  const artist2 = stubThing(wikiData, Artist, {name: `Artist 2`});
+
+  const {XXX_decacheWikiData} = linkAndBindWikiData(wikiData);
 
   t.same(track.coverArtistContribs, [],
     `coverArtistContribs #1: defaults to empty array`);
@@ -457,7 +376,7 @@ t.test(`Track.coverArtistContribs`, t => {
 
   XXX_decacheWikiData();
 
-  t.same(track.coverArtistContribs,
+  t.match(track.coverArtistContribs,
     [{artist: artist1, annotation: `lines`}, {artist: artist2, annotation: null}],
     `coverArtistContribs #2: inherits album trackCoverArtistContribs`);
 
@@ -465,7 +384,7 @@ t.test(`Track.coverArtistContribs`, t => {
     {artist: `Artist 1`, annotation: `collage`},
   ];
 
-  t.same(track.coverArtistContribs, [{artist: artist1, annotation: `collage`}],
+  t.match(track.coverArtistContribs, [{artist: artist1, annotation: `collage`}],
     `coverArtistContribs #3: resolves from own value`);
 
   track.coverArtistContribs = [
@@ -474,7 +393,7 @@ t.test(`Track.coverArtistContribs`, t => {
     {artist: `Artist 2`, annotation: `usual`},
   ];
 
-  t.same(track.coverArtistContribs,
+  t.match(track.coverArtistContribs,
     [{artist: artist1, annotation: `snooping`}, {artist: artist2, annotation: `usual`}],
     `coverArtistContribs #4: filters out names without matches`);
 
@@ -487,14 +406,12 @@ t.test(`Track.coverArtistContribs`, t => {
 t.test(`Track.coverArtDate`, t => {
   t.plan(8);
 
-  const {track, album} = stubTrackAndAlbum();
-  const {artist, contribs, badContribs} = stubArtistAndContribs();
+  const wikiData = stubWikiData();
+
+  const {track, album} = stubTrackAndAlbum(wikiData);
+  const {contribs, badContribs} = stubArtistAndContribs(wikiData);
 
-  const {XXX_decacheWikiData} = linkAndBindWikiData({
-    albumData: [album],
-    artistData: [artist],
-    trackData: [track],
-  });
+  const {XXX_decacheWikiData} = linkAndBindWikiData(wikiData);
 
   track.coverArtistContribs = contribs;
 
@@ -547,14 +464,12 @@ t.test(`Track.coverArtDate`, t => {
 t.test(`Track.coverArtFileExtension`, t => {
   t.plan(8);
 
-  const {track, album} = stubTrackAndAlbum();
-  const {artist, contribs} = stubArtistAndContribs();
+  const wikiData = stubWikiData();
+
+  const {track, album} = stubTrackAndAlbum(wikiData);
+  const {contribs} = stubArtistAndContribs(wikiData);
 
-  const {XXX_decacheWikiData} = linkAndBindWikiData({
-    albumData: [album],
-    artistData: [artist],
-    trackData: [track],
-  });
+  const {XXX_decacheWikiData} = linkAndBindWikiData(wikiData);
 
   t.equal(track.coverArtFileExtension, null,
     `coverArtFileExtension #1: defaults to null`);
@@ -607,12 +522,11 @@ t.test(`Track.coverArtFileExtension`, t => {
 t.test(`Track.date`, t => {
   t.plan(3);
 
-  const {track, album} = stubTrackAndAlbum();
+  const wikiData = stubWikiData();
 
-  const {XXX_decacheWikiData} = linkAndBindWikiData({
-    albumData: [album],
-    trackData: [track],
-  });
+  const {track, album} = stubTrackAndAlbum(wikiData);
+
+  const {XXX_decacheWikiData} = linkAndBindWikiData(wikiData);
 
   t.equal(track.date, null,
     `date #1: defaults to null`);
@@ -632,17 +546,13 @@ t.test(`Track.date`, t => {
 t.test(`Track.featuredInFlashes`, t => {
   t.plan(2);
 
-  const {track, album} = stubTrackAndAlbum('track1');
+  const wikiData = stubWikiData();
 
-  const {flash: flash1, flashAct: flashAct1} = stubFlashAndAct('flash1');
-  const {flash: flash2, flashAct: flashAct2} = stubFlashAndAct('flash2');
+  const {track} = stubTrackAndAlbum(wikiData, 'track1');
+  const {flash: flash1} = stubFlashAndAct(wikiData, 'flash1');
+  const {flash: flash2} = stubFlashAndAct(wikiData, 'flash2');
 
-  const {XXX_decacheWikiData} = linkAndBindWikiData({
-    albumData: [album],
-    trackData: [track],
-    flashData: [flash1, flash2],
-    flashActData: [flashAct1, flashAct2],
-  });
+  const {XXX_decacheWikiData} = linkAndBindWikiData(wikiData);
 
   t.same(track.featuredInFlashes, [],
     `featuredInFlashes #1: defaults to empty array`);
@@ -658,14 +568,12 @@ t.test(`Track.featuredInFlashes`, t => {
 t.test(`Track.hasUniqueCoverArt`, t => {
   t.plan(7);
 
-  const {track, album} = stubTrackAndAlbum();
-  const {artist, contribs, badContribs} = stubArtistAndContribs();
+  const wikiData = stubWikiData();
+
+  const {track, album} = stubTrackAndAlbum(wikiData);
+  const {contribs, badContribs} = stubArtistAndContribs(wikiData);
 
-  const {XXX_decacheWikiData} = linkAndBindWikiData({
-    albumData: [album],
-    artistData: [artist],
-    trackData: [track],
-  });
+  const {XXX_decacheWikiData} = linkAndBindWikiData(wikiData);
 
   t.equal(track.hasUniqueCoverArt, false,
     `hasUniqueCoverArt #1: defaults to false`);
@@ -710,13 +618,12 @@ t.test(`Track.hasUniqueCoverArt`, t => {
 t.test(`Track.originalReleaseTrack`, t => {
   t.plan(3);
 
-  const {track: track1, album: album1} = stubTrackAndAlbum('track1');
-  const {track: track2, album: album2} = stubTrackAndAlbum('track2');
+  const wikiData = stubWikiData();
+
+  const {track: track1} = stubTrackAndAlbum(wikiData, 'track1');
+  const {track: track2} = stubTrackAndAlbum(wikiData, 'track2');
 
-  const {wikiData, linkWikiDataArrays, XXX_decacheWikiData} = linkAndBindWikiData({
-    albumData: [album1, album2],
-    trackData: [track1, track2],
-  });
+  linkAndBindWikiData(wikiData);
 
   t.equal(track2.originalReleaseTrack, null,
     `originalReleaseTrack #1: defaults to null`);
@@ -735,15 +642,14 @@ t.test(`Track.originalReleaseTrack`, t => {
 t.test(`Track.otherReleases`, t => {
   t.plan(6);
 
-  const {track: track1, album: album1} = stubTrackAndAlbum('track1');
-  const {track: track2, album: album2} = stubTrackAndAlbum('track2');
-  const {track: track3, album: album3} = stubTrackAndAlbum('track3');
-  const {track: track4, album: album4} = stubTrackAndAlbum('track4');
+  const wikiData = stubWikiData();
 
-  const {wikiData, linkWikiDataArrays, XXX_decacheWikiData} = linkAndBindWikiData({
-    albumData: [album1, album2, album3, album4],
-    trackData: [track1, track2, track3, track4],
-  });
+  const {track: track1} = stubTrackAndAlbum(wikiData, 'track1');
+  const {track: track2} = stubTrackAndAlbum(wikiData, 'track2');
+  const {track: track3} = stubTrackAndAlbum(wikiData, 'track3');
+  const {track: track4} = stubTrackAndAlbum(wikiData, 'track4');
+
+  const {linkWikiDataArrays, XXX_decacheWikiData} = linkAndBindWikiData(wikiData);
 
   t.same(track1.otherReleases, [],
     `otherReleases #1: defaults to empty array`);
@@ -757,13 +663,13 @@ t.test(`Track.otherReleases`, t => {
     `otherReleases #2: otherReleases of original release are its rereleases`);
 
   wikiData.trackData = [track1, track3, track2, track4];
-  linkWikiDataArrays();
+  linkWikiDataArrays({bindFind});
 
   t.same(track1.otherReleases, [track3, track2, track4],
     `otherReleases #3: otherReleases matches trackData order`);
 
   wikiData.trackData = [track3, track2, track1, track4];
-  linkWikiDataArrays();
+  linkWikiDataArrays({bindFind});
 
   t.same(track2.otherReleases, [track1, track3, track4],
     `otherReleases #4: otherReleases of rerelease are original track then other rereleases (1/3)`);
@@ -778,15 +684,14 @@ t.test(`Track.otherReleases`, t => {
 t.test(`Track.referencedByTracks`, t => {
   t.plan(4);
 
-  const {track: track1, album: album1} = stubTrackAndAlbum('track1');
-  const {track: track2, album: album2} = stubTrackAndAlbum('track2');
-  const {track: track3, album: album3} = stubTrackAndAlbum('track3');
-  const {track: track4, album: album4} = stubTrackAndAlbum('track4');
+  const wikiData = stubWikiData();
+
+  const {track: track1} = stubTrackAndAlbum(wikiData, 'track1');
+  const {track: track2} = stubTrackAndAlbum(wikiData, 'track2');
+  const {track: track3} = stubTrackAndAlbum(wikiData, 'track3');
+  const {track: track4} = stubTrackAndAlbum(wikiData, 'track4');
 
-  const {XXX_decacheWikiData} = linkAndBindWikiData({
-    albumData: [album1, album2, album3, album4],
-    trackData: [track1, track2, track3, track4],
-  });
+  const {XXX_decacheWikiData} = linkAndBindWikiData(wikiData);
 
   t.same(track1.referencedByTracks, [],
     `referencedByTracks #1: defaults to empty array`);
@@ -814,15 +719,14 @@ t.test(`Track.referencedByTracks`, t => {
 t.test(`Track.sampledByTracks`, t => {
   t.plan(4);
 
-  const {track: track1, album: album1} = stubTrackAndAlbum('track1');
-  const {track: track2, album: album2} = stubTrackAndAlbum('track2');
-  const {track: track3, album: album3} = stubTrackAndAlbum('track3');
-  const {track: track4, album: album4} = stubTrackAndAlbum('track4');
+  const wikiData = stubWikiData();
+
+  const {track: track1} = stubTrackAndAlbum(wikiData, 'track1');
+  const {track: track2} = stubTrackAndAlbum(wikiData, 'track2');
+  const {track: track3} = stubTrackAndAlbum(wikiData, 'track3');
+  const {track: track4} = stubTrackAndAlbum(wikiData, 'track4');
 
-  const {XXX_decacheWikiData} = linkAndBindWikiData({
-    albumData: [album1, album2, album3, album4],
-    trackData: [track1, track2, track3, track4],
-  });
+  const {XXX_decacheWikiData} = linkAndBindWikiData(wikiData);
 
   t.same(track1.sampledByTracks, [],
     `sampledByTracks #1: defaults to empty array`);
diff --git a/test/unit/data/things/validators.js b/test/unit/data/validators.js
index 3a217d6f..3a217d6f 100644
--- a/test/unit/data/things/validators.js
+++ b/test/unit/data/validators.js