« 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/composite.js33
-rw-r--r--test/lib/content-function.js36
-rw-r--r--test/lib/index.js1
-rw-r--r--test/lib/wiki-data.js128
-rw-r--r--test/snapshot/generateAlbumAdditionalFilesList.js84
-rw-r--r--test/snapshot/generateAlbumBanner.js34
-rw-r--r--test/snapshot/generateAlbumCoverArtwork.js37
-rw-r--r--test/snapshot/generateAlbumReleaseInfo.js74
-rw-r--r--test/snapshot/generateAlbumSecondaryNav.js57
-rw-r--r--test/snapshot/generateAlbumSidebarGroupBox.js57
-rw-r--r--test/snapshot/generateAlbumTrackList.js105
-rw-r--r--test/snapshot/generateBanner.js22
-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.js51
-rw-r--r--test/snapshot/image.js132
-rw-r--r--test/snapshot/linkArtist.js30
-rw-r--r--test/snapshot/linkContribution.js104
-rw-r--r--test/snapshot/linkExternal.js225
-rw-r--r--test/snapshot/linkTemplate.js63
-rw-r--r--test/snapshot/linkThing.js94
-rw-r--r--test/snapshot/transformContent.js161
-rw-r--r--test/unit/content/dependencies/generateAlbumTrackList.js3
-rw-r--r--test/unit/content/dependencies/linkContribution.js71
-rw-r--r--test/unit/data/cacheable-object.js12
-rw-r--r--test/unit/data/composite/control-flow/withResultOfAvailabilityCheck.js18
-rw-r--r--test/unit/data/composite/data/withPropertiesFromObject.js59
-rw-r--r--test/unit/data/composite/data/withPropertyFromObject.js111
-rw-r--r--test/unit/data/composite/data/withUniqueItemsOnly.js23
-rw-r--r--test/unit/data/composite/things/track/withAlbum.js119
-rw-r--r--test/unit/data/composite/wiki-data/withParsedCommentaryEntries.js102
-rw-r--r--test/unit/data/things/album.js520
-rw-r--r--test/unit/data/things/art-tag.js81
-rw-r--r--test/unit/data/things/flash.js55
-rw-r--r--test/unit/data/things/track.js840
-rw-r--r--test/unit/data/validators.js (renamed from test/unit/data/things/validators.js)4
-rw-r--r--test/unit/util/html.js4
39 files changed, 364 insertions, 3422 deletions
diff --git a/test/lib/composite.js b/test/lib/composite.js
new file mode 100644
index 00000000..359d364d
--- /dev/null
+++ b/test/lib/composite.js
@@ -0,0 +1,33 @@
+import {compositeFrom} from '#composite';
+
+export function quickCheckCompositeOutputs(t, dependencies) {
+  return (step, outputDict) => {
+    t.same(
+      Object.keys(step.toDescription().outputs),
+      Object.keys(outputDict));
+
+    const composite = compositeFrom({
+      compose: false,
+      steps: [
+        step,
+
+        {
+          dependencies: Object.keys(outputDict),
+
+          // Access all dependencies by their expected keys -
+          // the composition runner actually provides a proxy
+          // and is checking that *we* access the dependencies
+          // we've specified.
+          compute: dependencies =>
+            Object.fromEntries(
+              Object.keys(outputDict)
+                .map(key => [key, dependencies[key]])),
+        },
+      ],
+    });
+
+    t.same(
+      composite.expose.compute(dependencies),
+      outputDict);
+  };
+}
diff --git a/test/lib/content-function.js b/test/lib/content-function.js
index 7bc62139..49fe5c95 100644
--- a/test/lib/content-function.js
+++ b/test/lib/content-function.js
@@ -11,14 +11,45 @@ import {quickEvaluate} from '#content-function';
 import * as html from '#html';
 import {internalDefaultStringsFile, processLanguageFile} from '#language';
 import {empty} from '#sugar';
-import {generateURLs, thumb, urlSpec} from '#urls';
+
+import {
+  applyLocalizedWithBaseDirectory,
+  generateURLs,
+  internalDefaultURLSpecFile,
+  processURLSpecFromFileSync,
+  thumb,
+} from '#urls';
 
 import mock from './generic-mock.js';
 
 const __dirname = path.dirname(fileURLToPath(import.meta.url));
 
+function cleanURLSpec(urlSpec) {
+  for (const spec of Object.values(urlSpec)) {
+    if (spec.prefix) {
+      // Strip out STATIC_VERSION. This updates fairly regularly and we
+      // don't want it to affect snapshot tests.
+      spec.prefix = spec.prefix
+        .replace(/static-\d+[a-z]\d+/i, 'static');
+    }
+  }
+}
+
+function urlsPlease() {
+  const {aggregate: urlsAggregate, result: urlSpec} =
+    processURLSpecFromFileSync(internalDefaultURLSpecFile);
+
+  urlsAggregate.close();
+
+  applyLocalizedWithBaseDirectory(urlSpec);
+
+  cleanURLSpec(urlSpec);
+
+  return generateURLs(urlSpec);
+}
+
 export function testContentFunctions(t, message, fn) {
-  const urls = generateURLs(urlSpec);
+  const urls = urlsPlease();
 
   t.test(message, async t => {
     let loadedContentDependencies;
@@ -52,7 +83,6 @@ export function testContentFunctions(t, message, fn) {
             to,
             urls,
 
-            cachebust: 413,
             pagePath: ['home'],
             appendIndexHTML: false,
             getColors: c => getColors(c, {chroma}),
diff --git a/test/lib/index.js b/test/lib/index.js
index 5fb5bf78..4c9ee23f 100644
--- a/test/lib/index.js
+++ b/test/lib/index.js
@@ -1,5 +1,6 @@
 Error.stackTraceLimit = Infinity;
 
+export * from './composite.js';
 export * from './content-function.js';
 export * from './generic-mock.js';
 export * from './wiki-data.js';
diff --git a/test/lib/wiki-data.js b/test/lib/wiki-data.js
index 75b1170a..f0ee0ef5 100644
--- a/test/lib/wiki-data.js
+++ b/test/lib/wiki-data.js
@@ -1,34 +1,22 @@
 import CacheableObject from '#cacheable-object';
-import find from '#find';
+import find, {bindFind} from '#find';
+import {bindReverse} from '#reverse';
 import {withEntries} from '#sugar';
+import Thing from '#thing';
+import thingConstructors from '#things';
 import {linkWikiDataArrays} from '#yaml';
 
 export function linkAndBindWikiData(wikiData, {
   inferAlbumsOwnTrackData = true,
 } = {}) {
   function customLinkWikiDataArrays(wikiData, options = {}) {
-    linkWikiDataArrays(
-      (options.XXX_decacheWikiData
-        ? withEntries(wikiData, entries => entries
-            .map(([key, value]) => [key, value.slice()]))
-        : wikiData));
-
-    // If albumData is present, automatically set their sections' ownTrackData
-    // by resolving references against the full array. This is just a nicety
-    // for working with albums throughout tests.
-    if (inferAlbumsOwnTrackData && wikiData.albumData && wikiData.trackData) {
-      for (const album of wikiData.albumData) {
-        const trackSections =
-          CacheableObject.getUpdateValue(album, 'trackSections');
-
-        for (const trackSection of trackSections) {
-          trackSection.ownTrackData =
-            CacheableObject.getUpdateValue(trackSection, 'tracks')
-              .map(ref =>
-                find.track(ref, wikiData.trackData, {mode: 'error'}));
-        }
-      }
+    if (options.XXX_decacheWikiData) {
+      wikiData =
+        withEntries(wikiData, entries => entries
+          .map(([key, value]) => [key, value.slice()]));
     }
+
+    linkWikiDataArrays(wikiData, {bindFind, bindReverse});
   }
 
   customLinkWikiDataArrays(wikiData);
@@ -70,3 +58,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/generateAlbumAdditionalFilesList.js b/test/snapshot/generateAlbumAdditionalFilesList.js
deleted file mode 100644
index c25e5682..00000000
--- a/test/snapshot/generateAlbumAdditionalFilesList.js
+++ /dev/null
@@ -1,84 +0,0 @@
-import t from 'tap';
-
-import {testContentFunctions} from '#test-lib';
-import thingConstructors from '#things';
-
-const {Album} = thingConstructors;
-
-testContentFunctions(t, 'generateAlbumAdditionalFilesList (snapshot)', async (t, evaluate) => {
-  const sizeMap = {
-    'sburbwp_1280x1024.jpg': 2500,
-    'sburbwp_1440x900.jpg': null,
-    'sburbwp_1920x1080.jpg': null,
-    'Internet Explorer.gif': 1,
-    'Homestuck_Vol4_alt1.jpg': 1234567,
-    'Homestuck_Vol4_alt2.jpg': 1234567,
-    'Homestuck_Vol4_alt3.jpg': 1234567,
-  };
-
-  const extraDependencies = {
-    getSizeOfAdditionalFile: file =>
-      Object.entries(sizeMap)
-        .find(key => file.includes(key))
-        ?.at(1) ?? null,
-  };
-
-  await evaluate.load({
-    mock: {
-      image: evaluate.stubContentFunction('image'),
-    },
-  });
-
-  const album = new Album();
-  album.directory = 'exciting-album';
-
-  evaluate.snapshot('no additional files', {
-    extraDependencies,
-    name: 'generateAlbumAdditionalFilesList',
-    args: [album, []],
-  });
-
-  try {
-    evaluate.snapshot('basic behavior', {
-      extraDependencies,
-      name: 'generateAlbumAdditionalFilesList',
-      args: [
-        album,
-        [
-          {
-            title: 'SBURB Wallpaper',
-            files: [
-              'sburbwp_1280x1024.jpg',
-              'sburbwp_1440x900.jpg',
-              'sburbwp_1920x1080.jpg',
-            ],
-          },
-          {
-            title: 'Fake Section',
-            description: 'No sizes for these files',
-            files: [
-              'oops.mp3',
-              'Internet Explorer.gif',
-              'daisy.mp3',
-            ],
-          },
-          {
-            title: `Empty Section`,
-            description: `These files haven't been made available.`,
-          },
-          {
-            title: 'Alternate Covers',
-            description: 'This is just an example description.',
-            files: [
-              'Homestuck_Vol4_alt1.jpg',
-              'Homestuck_Vol4_alt2.jpg',
-              'Homestuck_Vol4_alt3.jpg',
-            ],
-          },
-        ],
-      ],
-    });
-  } catch (error) {
-    console.log(error);
-  }
-});
diff --git a/test/snapshot/generateAlbumBanner.js b/test/snapshot/generateAlbumBanner.js
deleted file mode 100644
index 8e63308f..00000000
--- a/test/snapshot/generateAlbumBanner.js
+++ /dev/null
@@ -1,34 +0,0 @@
-import t from 'tap';
-import {testContentFunctions} from '#test-lib';
-
-testContentFunctions(t, 'generateAlbumBanner (snapshot)', async (t, evaluate) => {
-  await evaluate.load();
-
-  evaluate.snapshot('basic behavior', {
-    name: 'generateAlbumBanner',
-    args: [{
-      directory: 'cool-album',
-      hasBannerArt: true,
-      bannerDimensions: [800, 200],
-      bannerFileExtension: 'png',
-    }],
-  });
-
-  evaluate.snapshot('no dimensions', {
-    name: 'generateAlbumBanner',
-    args: [{
-      directory: 'cool-album',
-      hasBannerArt: true,
-      bannerDimensions: null,
-      bannerFileExtension: 'png',
-    }],
-  });
-
-  evaluate.snapshot('no banner', {
-    name: 'generateAlbumBanner',
-    args: [{
-      directory: 'cool-album',
-      hasBannerArt: false,
-    }],
-  });
-});
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
deleted file mode 100644
index a109912f..00000000
--- a/test/snapshot/generateAlbumReleaseInfo.js
+++ /dev/null
@@ -1,74 +0,0 @@
-import t from 'tap';
-import {testContentFunctions} from '#test-lib';
-
-testContentFunctions(t, 'generateAlbumReleaseInfo (snapshot)', async (t, evaluate) => {
-  await evaluate.load();
-
-  evaluate.snapshot('basic behavior', {
-    name: 'generateAlbumReleaseInfo',
-    args: [{
-      artistContribs: [
-        {artist: {name: 'Toby Fox', directory: 'toby-fox', urls: null}, 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},
-      ],
-
-      wallpaperArtistContribs: [
-        {artist: {name: 'Hanni Brosh', directory: 'hb', urls: null}, annotation: null},
-        {artist: {name: 'Niklink', directory: 'niklink', urls: null}, annotation: 'edits'},
-      ],
-
-      bannerArtistContribs: [
-        {artist: {name: 'Hanni Brosh', directory: 'hb', urls: null}, annotation: null},
-        {artist: {name: 'Niklink', directory: 'niklink', urls: null}, annotation: 'edits'},
-      ],
-
-      name: 'AlterniaBound',
-      date: new Date('March 14, 2011'),
-      coverArtDate: new Date('April 1, 1991'),
-      urls: [
-        'https://homestuck.bandcamp.com/album/alterniabound-with-alternia',
-        'https://www.youtube.com/playlist?list=PLnVpmehyaOFZWO9QOZmD6A3TIK0wZ6xE2',
-        'https://www.youtube.com/watch?v=HO5V2uogkYc',
-      ],
-
-      tracks: [{duration: 253}, {duration: 372}],
-    }],
-  });
-
-  const sparse = {
-    artistContribs: [],
-    coverArtistContribs: [],
-    wallpaperArtistContribs: [],
-    bannerArtistContribs: [],
-
-    name: 'Suspicious Album',
-    urls: [],
-    tracks: [],
-  };
-
-  evaluate.snapshot('reduced details', {
-    name: 'generateAlbumReleaseInfo',
-    args: [sparse],
-  });
-
-  evaluate.snapshot('URLs only', {
-    name: 'generateAlbumReleaseInfo',
-    args: [{
-      ...sparse,
-      urls: ['https://homestuck.bandcamp.com/foo', 'https://soundcloud.com/bar'],
-    }],
-  });
-
-  evaluate.snapshot('equal cover art date', {
-    name: 'generateAlbumReleaseInfo',
-    args: [{
-      ...sparse,
-      date: new Date('2020-04-13'),
-      coverArtDate: new Date('2020-04-13'),
-    }],
-  });
-});
diff --git a/test/snapshot/generateAlbumSecondaryNav.js b/test/snapshot/generateAlbumSecondaryNav.js
deleted file mode 100644
index 57618f2f..00000000
--- a/test/snapshot/generateAlbumSecondaryNav.js
+++ /dev/null
@@ -1,57 +0,0 @@
-import t from 'tap';
-import {testContentFunctions} from '#test-lib';
-
-testContentFunctions(t, 'generateAlbumSecondaryNav (snapshot)', async (t, evaluate) => {
-  await evaluate.load();
-
-  let album, group1, group2;
-
-  group1 = {name: 'VCG', directory: 'vcg', color: '#abcdef'};
-  group2 = {name: 'Bepis', directory: 'bepis', color: '#123456'};
-
-  album = {
-    name: 'Album',
-    directory: 'album',
-    date: new Date('2010-04-13'),
-    groups: [group1, group2],
-  };
-
-  group1.albums = [
-    {name: 'First', directory: 'first', date: new Date('2010-04-10')},
-    album,
-    {name: 'Last', directory: 'last', date: new Date('2010-06-12')},
-  ];
-
-  group2.albums = [
-    album,
-    {name: 'Second', directory: 'second', date: new Date('2011-04-13')},
-  ];
-
-  evaluate.snapshot('basic behavior, mode: album', {
-    name: 'generateAlbumSecondaryNav',
-    args: [album],
-    slots: {mode: 'album'},
-  });
-
-  evaluate.snapshot('basic behavior, mode: track', {
-    name: 'generateAlbumSecondaryNav',
-    args: [album],
-    slots: {mode: 'track'},
-  });
-
-  album = {
-    date: null,
-    groups: [group1, group2],
-  };
-
-  group1.albums = [
-    ...group1.albums,
-    album,
-  ];
-
-  evaluate.snapshot('dateless album in mixed group', {
-    name: 'generateAlbumSecondaryNav',
-    args: [album],
-    slots: {mode: 'album'},
-  });
-});
diff --git a/test/snapshot/generateAlbumSidebarGroupBox.js b/test/snapshot/generateAlbumSidebarGroupBox.js
deleted file mode 100644
index f920bd96..00000000
--- a/test/snapshot/generateAlbumSidebarGroupBox.js
+++ /dev/null
@@ -1,57 +0,0 @@
-import t from 'tap';
-import {testContentFunctions} from '#test-lib';
-
-testContentFunctions(t, 'generateAlbumSidebarGroupBox (snapshot)', async (t, evaluate) => {
-  await evaluate.load({
-    mock: {
-      ...evaluate.mock.transformContent,
-    },
-  });
-
-  let album, group;
-
-  album = {
-    name: 'Middle',
-    directory: 'middle',
-    date: new Date('2010-04-13'),
-  };
-
-  group = {
-    name: 'VCG',
-    directory: 'vcg',
-    descriptionShort: 'Very cool group.',
-    urls: ['https://vcg.bandcamp.com/', 'https://youtube.com/@vcg'],
-    albums: [
-      {name: 'First', directory: 'first', date: new Date('2010-04-10')},
-      album,
-      {name: 'Last', directory: 'last', date: new Date('2010-06-12')},
-    ],
-  };
-
-  evaluate.snapshot('basic behavior, mode: album', {
-    name: 'generateAlbumSidebarGroupBox',
-    args: [album, group],
-    slots: {mode: 'album'},
-  });
-
-  evaluate.snapshot('basic behavior, mode: track', {
-    name: 'generateAlbumSidebarGroupBox',
-    args: [album, group],
-    slots: {mode: 'track'},
-  });
-
-  album = {
-    date: null,
-  };
-
-  group.albums = [
-    ...group.albums,
-    album,
-  ];
-
-  evaluate.snapshot('dateless album in mixed group', {
-    name: 'generateAlbumSidebarGroupBox',
-    args: [album, group],
-    slots: {mode: 'album'},
-  });
-});
diff --git a/test/snapshot/generateAlbumTrackList.js b/test/snapshot/generateAlbumTrackList.js
deleted file mode 100644
index 08b31902..00000000
--- a/test/snapshot/generateAlbumTrackList.js
+++ /dev/null
@@ -1,105 +0,0 @@
-import t from 'tap';
-import {testContentFunctions} from '#test-lib';
-
-testContentFunctions(t, 'generateAlbumTrackList (snapshot)', async (t, evaluate) => {
-  await evaluate.load({
-    mock: {
-      generateAlbumTrackListMissingDuration:
-        evaluate.stubContentFunction('generateAlbumTrackListMissingDuration'),
-    },
-  });
-
-  const contribs1 = [
-    {artist: {name: 'Apricot', directory: 'apricot', urls: null}},
-  ];
-
-  const contribs2 = [
-    {artist: {name: 'Apricot', directory: 'apricot', urls: null}},
-    {artist: {name: 'Peach', directory: 'peach', urls: ['https://peach.bandcamp.com/']}},
-    {artist: {name: 'Cerise', directory: 'cerise', urls: null}},
-  ];
-
-  const color1 = '#fb07ff';
-  const color2 = '#ea2e83';
-
-  const tracks = [
-    {name: 'Track 1', directory: 't1', duration: 20, artistContribs: contribs1, color: color1},
-    {name: 'Track 2', directory: 't2', duration: 0, artistContribs: contribs1, color: color1},
-    {name: 'Track 3', directory: 't3', duration: 40, artistContribs: contribs1, color: color1},
-    {name: 'Track 4', directory: 't4', duration: 0, artistContribs: contribs2, color: color2},
-  ];
-
-  const albumWithTrackSections = {
-    color: color1,
-    artistContribs: contribs1,
-    trackSections: [
-      {name: 'First section', tracks: tracks.slice(0, 3)},
-      {name: 'Second section', tracks: tracks.slice(3)},
-    ],
-    tracks,
-  };
-
-  const albumWithoutTrackSections = {
-    color: color1,
-    artistContribs: contribs1,
-    trackSections: [{isDefaultTrackSection: true, tracks}],
-    tracks,
-  };
-
-  const albumWithNoDuration = {
-    color: color1,
-    artistContribs: contribs1,
-    trackSections: [{isDefaultTrackSection: true, tracks: [tracks[1], tracks[3]]}],
-    tracks: [tracks[1], tracks[3]],
-  };
-
-  evaluate.snapshot(`basic behavior, with track sections`, {
-    name: 'generateAlbumTrackList',
-    args: [albumWithTrackSections],
-  });
-
-  evaluate.snapshot(`basic behavior, default track section`, {
-    name: 'generateAlbumTrackList',
-    args: [albumWithoutTrackSections],
-  });
-
-  evaluate.snapshot(`collapseDurationScope: never`, {
-    name: 'generateAlbumTrackList',
-    slots: {collapseDurationScope: 'never'},
-    multiple: [
-      {args: [albumWithTrackSections]},
-      {args: [albumWithoutTrackSections]},
-      {args: [albumWithNoDuration]},
-    ],
-  });
-
-  evaluate.snapshot(`collapseDurationScope: track`, {
-    name: 'generateAlbumTrackList',
-    slots: {collapseDurationScope: 'track'},
-    multiple: [
-      {args: [albumWithTrackSections]},
-      {args: [albumWithoutTrackSections]},
-      {args: [albumWithNoDuration]},
-    ],
-  });
-
-  evaluate.snapshot(`collapseDurationScope: section`, {
-    name: 'generateAlbumTrackList',
-    slots: {collapseDurationScope: 'section'},
-    multiple: [
-      {args: [albumWithTrackSections]},
-      {args: [albumWithoutTrackSections]},
-      {args: [albumWithNoDuration]},
-    ],
-  });
-
-  evaluate.snapshot(`collapseDurationScope: album`, {
-    name: 'generateAlbumTrackList',
-    slots: {collapseDurationScope: 'album'},
-    multiple: [
-      {args: [albumWithTrackSections]},
-      {args: [albumWithoutTrackSections]},
-      {args: [albumWithNoDuration]},
-    ],
-  });
-});
diff --git a/test/snapshot/generateBanner.js b/test/snapshot/generateBanner.js
deleted file mode 100644
index ab57c3cc..00000000
--- a/test/snapshot/generateBanner.js
+++ /dev/null
@@ -1,22 +0,0 @@
-import t from 'tap';
-import {testContentFunctions} from '#test-lib';
-
-testContentFunctions(t, 'generateBanner (snapshot)', async (t, evaluate) => {
-  await evaluate.load();
-
-  evaluate.snapshot('basic behavior', {
-    name: 'generateBanner',
-    slots: {
-      path: ['media.albumBanner', 'cool-album', 'png'],
-      alt: 'Very cool banner art.',
-      dimensions: [800, 200],
-    },
-  });
-
-  evaluate.snapshot('no dimensions', {
-    name: 'generateBanner',
-    slots: {
-      path: ['media.albumBanner', 'cool-album', 'png'],
-    },
-  });
-});
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
deleted file mode 100644
index 78f0fee7..00000000
--- a/test/snapshot/generateTrackReleaseInfo.js
+++ /dev/null
@@ -1,51 +0,0 @@
-import t from 'tap';
-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: '🔥'}];
-
-  evaluate.snapshot('basic behavior', {
-    name: 'generateTrackReleaseInfo',
-    args: [{
-      artistContribs,
-      name: 'An Apple Disaster!!',
-      date: new Date('2011-11-30'),
-      duration: 58,
-      urls: ['https://soundcloud.com/foo', 'https://youtube.com/watch?v=bar'],
-    }],
-  });
-
-  const sparse = {
-    artistContribs,
-    name: 'Suspicious Track',
-    date: null,
-    duration: null,
-    urls: [],
-  };
-
-  evaluate.snapshot('reduced details', {
-    name: 'generateTrackReleaseInfo',
-    args: [sparse],
-  });
-
-  evaluate.snapshot('cover artist contribs, non-unique', {
-    name: 'generateTrackReleaseInfo',
-    args: [{
-      ...sparse,
-      coverArtistContribs,
-      hasUniqueCoverArt: false,
-    }],
-  });
-
-  evaluate.snapshot('cover artist contribs, unique', {
-    name: 'generateTrackReleaseInfo',
-    args: [{
-      ...sparse,
-      coverArtistContribs,
-      hasUniqueCoverArt: true,
-    }],
-  });
-});
diff --git a/test/snapshot/image.js b/test/snapshot/image.js
deleted file mode 100644
index 1985211f..00000000
--- a/test/snapshot/image.js
+++ /dev/null
@@ -1,132 +0,0 @@
-import t from 'tap';
-import {testContentFunctions} from '#test-lib';
-
-testContentFunctions(t, 'image (snapshot)', async (t, evaluate) => {
-  await evaluate.load();
-
-  const quickSnapshot = (message, {extraDependencies, ...opts}) =>
-    evaluate.snapshot(message, {
-      name: 'image',
-      extraDependencies: {
-        checkIfImagePathHasCachedThumbnails: path => !path.endsWith('.gif'),
-        getSizeOfImagePath: () => 0,
-        getDimensionsOfImagePath: () => [600, 600],
-        getThumbnailEqualOrSmaller: () => 'medium',
-        getThumbnailsAvailableForDimensions: () =>
-          [['large', 800], ['medium', 400], ['small', 250]],
-        missingImagePaths: ['album-art/missing/cover.png'],
-        ...extraDependencies,
-      },
-      ...opts,
-    });
-
-  quickSnapshot('source via path', {
-    slots: {
-      path: ['media.albumCover', 'beyond-canon', 'png'],
-    },
-  });
-
-  quickSnapshot('source via src', {
-    slots: {
-      src: 'https://example.com/bananas.gif',
-    },
-  });
-
-  quickSnapshot('source missing', {
-    slots: {
-      missingSourceContent: 'Example of missing source message.',
-    },
-  });
-
-  quickSnapshot('dimensions', {
-    slots: {
-      src: 'foobar',
-      dimensions: [600, 400],
-    },
-  });
-
-  quickSnapshot('square', {
-    slots: {
-      src: 'foobar',
-      square: true,
-    },
-  });
-
-  quickSnapshot('dimensions with square', {
-    slots: {
-      src: 'foobar',
-      dimensions: [600, 400],
-      square: true,
-    },
-  });
-
-  quickSnapshot('lazy with square', {
-    slots: {
-      src: 'foobar',
-      lazy: true,
-      square: true,
-    },
-  });
-
-  quickSnapshot('link with file size', {
-    extraDependencies: {
-      getSizeOfImagePath: () => 10 ** 6,
-    },
-    slots: {
-      path: ['media.albumCover', 'pingas', 'png'],
-      link: true,
-    },
-  });
-
-  quickSnapshot('content warnings via tags', {
-    args: [
-      [
-        {name: 'Dirk Strider', directory: 'dirk'},
-        {name: 'too cool for school', isContentWarning: true},
-      ],
-    ],
-    slots: {
-      path: ['media.albumCover', 'beyond-canon', 'png'],
-    },
-  });
-
-  evaluate.snapshot('thumbnail details', {
-    name: 'image',
-    extraDependencies: {
-      checkIfImagePathHasCachedThumbnails: () => true,
-      getSizeOfImagePath: () => 0,
-      getDimensionsOfImagePath: () => [900, 1200],
-      getThumbnailsAvailableForDimensions: () =>
-        [['voluminous', 1200], ['middling', 900], ['petite', 20]],
-      getThumbnailEqualOrSmaller: () => 'voluminous',
-      missingImagePaths: [],
-    },
-    slots: {
-      thumb: 'gargantuan',
-      path: ['media.albumCover', 'beyond-canon', 'png'],
-    },
-  });
-
-  quickSnapshot('thumb requested but source is gif', {
-    slots: {
-      thumb: 'medium',
-      path: ['media.flashArt', '5426', 'gif'],
-    },
-  });
-
-  quickSnapshot('missing image path', {
-    slots: {
-      thumb: 'medium',
-      path: ['media.albumCover', 'missing', 'png'],
-      link: true,
-    },
-  });
-
-  quickSnapshot('missing image path w/ missingSourceContent', {
-    slots: {
-      thumb: 'medium',
-      path: ['media.albumCover', 'missing', 'png'],
-      missingSourceContent: `Cover's missing, whoops`,
-    },
-  });
-});
diff --git a/test/snapshot/linkArtist.js b/test/snapshot/linkArtist.js
deleted file mode 100644
index 7b2114b5..00000000
--- a/test/snapshot/linkArtist.js
+++ /dev/null
@@ -1,30 +0,0 @@
-import t from 'tap';
-import {testContentFunctions} from '#test-lib';
-
-testContentFunctions(t, 'linkArtist (snapshot)', async (t, evaluate) => {
-  await evaluate.load();
-
-  evaluate.snapshot('basic behavior', {
-    name: 'linkArtist',
-    args: [
-      {
-        name: `Toby Fox`,
-        directory: `toby-fox`,
-      }
-    ],
-  });
-
-  evaluate.snapshot('prefer short name', {
-    name: 'linkArtist',
-    args: [
-      {
-        name: 'ICCTTCMDMIROTMCWMWFTPFTDDOTARHPOESWGBTWEATFCWSEBTSSFOFG',
-        nameShort: '55gore',
-        directory: '55gore',
-      },
-    ],
-    slots: {
-      preferShortName: true,
-    },
-  });
-});
diff --git a/test/snapshot/linkContribution.js b/test/snapshot/linkContribution.js
deleted file mode 100644
index 1043ddc6..00000000
--- a/test/snapshot/linkContribution.js
+++ /dev/null
@@ -1,104 +0,0 @@
-import t from 'tap';
-import {testContentFunctions} from '#test-lib';
-
-testContentFunctions(t, 'linkContribution (snapshot)', async (t, evaluate) => {
-  await evaluate.load();
-
-  const quickSnapshot = (message, slots) =>
-    evaluate.snapshot(message, {
-      name: 'linkContribution',
-      multiple: [
-        {args: [
-          {artist: {
-            name: 'Clark Powell',
-            directory: 'clark-powell',
-            urls: ['https://soundcloud.com/plazmataz'],
-          }, annotation: null},
-        ]},
-        {args: [
-          {artist: {
-            name: 'Grounder & Scratch',
-            directory: 'the-big-baddies',
-            urls: [],
-          }, annotation: 'Snooping'},
-        ]},
-        {args: [
-          {artist: {
-            name: 'Toby Fox',
-            directory: 'toby-fox',
-            urls: ['https://tobyfox.bandcamp.com/', 'https://toby.fox/'],
-          }, annotation: 'Arrangement'},
-        ]},
-      ],
-      slots,
-    });
-
-  quickSnapshot('showContribution & showIcons (inline)', {
-    showContribution: true,
-    showIcons: true,
-    iconMode: 'inline',
-  });
-
-  quickSnapshot('showContribution & showIcons (tooltip)', {
-    showContribution: true,
-    showIcons: true,
-    iconMode: 'tooltip',
-  });
-
-  quickSnapshot('only showContribution', {
-    showContribution: true,
-  });
-
-  quickSnapshot('only showIcons (inline)', {
-    showIcons: true,
-    iconMode: 'inline',
-  });
-
-  quickSnapshot('only showIcons (tooltip)', {
-    showContribution: true,
-    showIcons: true,
-    iconMode: 'tooltip',
-  });
-
-  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)', {
-    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, iconMode: 'tooltip'},
-  });
-
-  quickSnapshot('no preventWrapping', {
-    showContribution: true,
-    showIcons: true,
-    preventWrapping: false,
-  });
-});
diff --git a/test/snapshot/linkExternal.js b/test/snapshot/linkExternal.js
deleted file mode 100644
index 90c98f4b..00000000
--- a/test/snapshot/linkExternal.js
+++ /dev/null
@@ -1,225 +0,0 @@
-import t from 'tap';
-import {testContentFunctions} from '#test-lib';
-
-testContentFunctions(t, 'linkExternal (snapshot)', async (t, evaluate) => {
-  await evaluate.load();
-
-  evaluate.snapshot('unknown domain (arbitrary world wide web path)', {
-    name: 'linkExternal',
-    args: ['https://snoo.ping.as/usual/i/see/'],
-  });
-
-  const urlsToArgs = urls =>
-    urls.map(url => ({args: [url]}));
-
-  const quickSnapshot = (message, urls, slots) =>
-    evaluate.snapshot(message, {
-      name: 'linkExternal',
-      slots,
-      multiple: urlsToArgs(urls),
-    });
-
-  const quickSnapshotAllStyles = (context, urls) => {
-    for (const style of ['platform', 'handle']) {
-      const message = `context: ${context}, style: ${style}`;
-      quickSnapshot(message, urls, {context, style});
-    }
-  };
-
-  // Try to comprehensively test every regular expression
-  // (in `match` and extractions like `handle` or `details`).
-
-  // Try to *also* represent a reasonable variety of what kinds
-  // of URLs appear throughout the wiki. (This should serve to
-  // identify areas which #external-links is expected to
-  // accommodate, regardless whether or not there is special
-  // attention given in the actual descriptors.)
-
-  // For normal custom-domain matches (e.g. Mastodon),
-  // it's OK to just test one custom domain in the list.
-
-  // Generally match the sorting order in externalLinkSpec,
-  // so corresponding and missing test cases are easy to locate.
-
-  quickSnapshotAllStyles('generic', [
-    // platform: appleMusic
-    'https://music.apple.com/us/artist/system-of-a-down/462715',
-
-    // platform: artstation
-    'https://www.artstation.com/eevaningtea',
-    'https://witnesstheabsurd.artstation.com/',
-
-    // platform: bandcamp
-    'https://music.solatrus.com/',
-    'https://homestuck.bandcamp.com/',
-
-    // platform: bluesky
-    'https://bsky.app/profile/jacobtheloofah.bsky.social',
-
-    // platform: carrd
-    'https://aliceflare.carrd.co',
-    'https://bigchaslappa.carrd.co/',
-
-    // platform: cohost
-    'https://cohost.org/cosmoptera',
-
-    // platform: deconreconstruction.music
-    'https://music.deconreconstruction.com/albums/catch-322',
-    'https://music.deconreconstruction.com/albums/catch-322?track=arcjecs-theme',
-
-    // platform: deconreconstruction
-    'https://www.deconreconstruction.com/',
-
-    // platform: deviantart
-    'https://culdhira.deviantart.com',
-    'https://www.deviantart.com/chesswanderlust-sama',
-    'https://www.deviantart.com/shilloshilloh/art/Homestuck-Jake-English-268874606',
-
-    // platform: facebook
-    'https://www.facebook.com/DoomedCloud/',
-    'https://www.facebook.com/pages/WoodenToaster/280642235307371',
-    'https://www.facebook.com/Svixy/posts/400018786702633',
-
-    // platform: fandom.mspaintadventures
-    'https://mspaintadventures.fandom.com/wiki/Draconian_Dignitary',
-    'https://mspaintadventures.fandom.com/wiki/',
-    'https://mspaintadventures.fandom.com/',
-
-    // platform: fandom
-    'https://community.fandom.com/',
-    'https://community.fandom.com/wiki/',
-    'https://community.fandom.com/wiki/Community_Central',
-
-    // platform: gamebanana
-    'https://gamebanana.com/members/2028092',
-    'https://gamebanana.com/mods/459476',
-
-    // platform: homestuck
-    'https://homestuck.com/',
-
-    // platform: hsmusic.archive
-    'https://hsmusic.wiki/media/misc/archive/Firefly%20Cloud%20Remix.mp3',
-
-    // platform: hsmusic
-    'https://hsmusic.wiki/feedback/',
-
-    // platform: internetArchive
-    'https://archive.org/details/a-life-well-lived',
-    'https://archive.org/details/VastError_Volume1/11+Renaissance.mp3',
-
-    // platform: instagram
-    'https://instagram.com/bass.and.noises',
-    'https://www.instagram.com/levc_egm/',
-
-    // platform: itch
-    'https://tuyoki.itch.io/',
-    'https://itch.io/profile/bravelittletoreador',
-
-    // platform: ko-fi
-    'https://ko-fi.com/gnaach',
-
-    // platform: linktree
-    'https://linktr.ee/bbpanzu',
-
-    // platform: mastodon
-    'https://types.pl/',
-
-    // platform: mspfa
-    'https://canwc.mspfa.com/',
-    'https://mspfa.com/?s=12003&p=1045',
-    'https://mspfa.com/user/?u=103334508819793669241',
-
-    // platform: neocities
-    'https://wodaro.neocities.org',
-    'https://neomints.neocities.org/',
-
-    // platform: newgrounds
-    'https://buzinkai.newgrounds.com/',
-    'https://www.newgrounds.com/audio/listen/1256058',
-
-    // platform: patreon
-    'https://www.patreon.com/CecilyRenns',
-
-    // platform: poetryFoundation
-    'https://www.poetryfoundation.org/poets/christina-rossetti',
-    'https://www.poetryfoundation.org/poems/45000/remember-56d224509b7ae',
-
-    // platform: soundcloud
-    'https://soundcloud.com/plazmataz',
-    'https://soundcloud.com/worthikids/1-i-accidentally-broke-my',
-
-    // platform: spotify
-    'https://open.spotify.com/artist/63SNNpNOicDzG3LY82G4q3',
-    'https://open.spotify.com/album/0iHvPD8rM3hQa0qeVtPQ3t',
-    'https://open.spotify.com/track/6YEGQH32aAXb9vQQbBrPlw',
-
-    // platform: tiktok
-    'https://www.tiktok.com/@richaadeb',
-
-    // platform: toyhouse
-    'https://toyhou.se/ghastaboo',
-
-    // platform: tumblr
-    'https://aeritus.tumblr.com/',
-    'https://vol5anthology.tumblr.com/post/159528808107/hey-everyone-its-413-and-that-means-we-have',
-    'https://www.tumblr.com/electricwestern',
-    'https://www.tumblr.com/spellmynamewithabang/142767566733/happy-413-this-is-the-first-time-anyones-heard',
-
-    // platform: twitch
-    'https://www.twitch.tv/ajhebard',
-    'https://www.twitch.tv/vargskelethor/',
-
-    // platform: twitter
-    'https://twitter.com/awkwarddoesart',
-    'https://twitter.com/purenonsens/',
-    'https://twitter.com/circlejourney/status/1202265927183548416',
-
-    // platform: waybackMachine
-    'https://web.archive.org/web/20120405160556/https://homestuck.bandcamp.com/album/colours-and-mayhem-universe-a',
-    'https://web.archive.org/web/20160807111207/http://griffinspacejam.com:80/',
-
-    // platform: wikipedia
-    'https://en.wikipedia.org/wiki/Haydn_Quartet_(vocal_ensemble)',
-
-    // platform: youtube
-    'https://youtube.com/@bani-chan8949',
-    'https://www.youtube.com/@Razzie16',
-    'https://www.youtube.com/channel/UCQXfvlKkpbOqEz4BepHqK7g',
-    'https://www.youtube.com/watch?v=6ekVnZm29kw',
-    'https://youtu.be/WBkC038wSio',
-    'https://www.youtube.com/playlist?list=PLy5UGIMKOXpONMExgI7lVYFwQa54QFp_H',
-  ]);
-
-  quickSnapshotAllStyles('album', [
-    'https://youtu.be/abc',
-    'https://youtube.com/watch?v=abc',
-    'https://youtube.com/Playlist?list=kweh',
-  ]);
-
-  quickSnapshotAllStyles('albumNoTracks', [
-    'https://youtu.be/abc',
-    'https://youtube.com/watch?v=abc',
-    'https://youtube.com/Playlist?list=kweh',
-  ]);
-
-  quickSnapshotAllStyles('albumOneTrack', [
-    'https://youtu.be/abc',
-    'https://youtube.com/watch?v=abc',
-    'https://youtube.com/Playlist?list=kweh',
-  ]);
-
-  quickSnapshotAllStyles('albumMultipleTracks', [
-    'https://youtu.be/abc',
-    'https://youtube.com/watch?v=abc',
-    'https://youtube.com/Playlist?list=kweh',
-  ]);
-
-  quickSnapshotAllStyles('flash', [
-    'https://www.bgreco.net/hsflash/002238.html',
-    'https://homestuck.com/story/1234',
-    'https://homestuck.com/story/pony',
-    'https://www.youtube.com/watch?v=wKgOp3Kg2wI',
-    'https://youtu.be/IOcvkkklWmY',
-    'https://some.external.site/foo/bar/',
-  ]);
-});
diff --git a/test/snapshot/linkTemplate.js b/test/snapshot/linkTemplate.js
deleted file mode 100644
index 300065e2..00000000
--- a/test/snapshot/linkTemplate.js
+++ /dev/null
@@ -1,63 +0,0 @@
-import t from 'tap';
-import * as html from '#html';
-import {testContentFunctions} from '#test-lib';
-
-testContentFunctions(t, 'linkTemplate (snapshot)', async (t, evaluate) => {
-  await evaluate.load();
-
-  evaluate.snapshot('fill many slots', {
-    name: 'linkTemplate',
-
-    slots: {
-      'href': 'https://hsmusic.wiki/media/cool file.pdf',
-      'hash': 'fooey',
-      'attributes': {class: 'dog', id: 'cat1'},
-      'content': 'My Cool Link',
-    },
-  });
-
-  evaluate.snapshot('fill path slot & provide appendIndexHTML', {
-    name: 'linkTemplate',
-
-    extraDependencies: {
-      to: (...path) => '/c*lzone/' + path.join('/') + '/',
-      appendIndexHTML: true,
-    },
-
-    slots: {
-      path: ['myCoolPath', 'ham', 'pineapple', 'tomato'],
-      content: 'delish',
-    },
-  });
-
-  evaluate.snapshot('special characters in path argument', {
-    name: 'linkTemplate',
-    slots: {
-      path: [
-        'media.albumAdditionalFile',
-        'homestuck-vol-1',
-        'Showtime (Piano Refrain) - #xXxAwesomeSheetMusick?rxXx#.pdf',
-      ],
-      content: `Damn, that's some good sheet music`,
-    },
-  });
-
-  evaluate.snapshot('missing content', {
-    name: 'linkTemplate',
-    slots: {href: 'banana'},
-  });
-
-  evaluate.snapshot('link in content', {
-    name: 'linkTemplate',
-    slots: {
-      hash: 'the-more-ye-know',
-      content: [
-        `Oh geez oh heck`,
-        html.tag('a', {href: 'dogs'}, `There's a link in here!!`),
-        `But here's <b>a normal tag.</b>`,
-        html.tag('div', `Gotta keep them normal tags.`),
-        html.tag('div', `But not... <a href="#">NESTED LINKS, OOO.</a>`),
-      ],
-    },
-  });
-});
diff --git a/test/snapshot/linkThing.js b/test/snapshot/linkThing.js
deleted file mode 100644
index 502db6d7..00000000
--- a/test/snapshot/linkThing.js
+++ /dev/null
@@ -1,94 +0,0 @@
-import t from 'tap';
-import * as html from '#html';
-import {testContentFunctions} from '#test-lib';
-
-testContentFunctions(t, 'linkThing (snapshot)', async (t, evaluate) => {
-  await evaluate.load();
-
-  const quickSnapshot = (message, oneOrMultiple) =>
-    evaluate.snapshot(message,
-      (Array.isArray(oneOrMultiple)
-        ? {name: 'linkThing', multiple: oneOrMultiple}
-        : {name: 'linkThing', ...oneOrMultiple}));
-
-  quickSnapshot('basic behavior', {
-    args: ['localized.track', {
-      directory: 'foo',
-      color: '#abcdef',
-      name: `Cool track!`,
-    }],
-  });
-
-  quickSnapshot('preferShortName', {
-    args: ['localized.tag', {
-      directory: 'five-oceanfalls',
-      name: 'Five (Oceanfalls)',
-      nameShort: 'Five',
-    }],
-    slots: {preferShortName: true},
-  });
-
-  quickSnapshot('tooltip & content', {
-    args: ['localized.album', {
-      directory: 'beyond-canon',
-      name: 'Beyond Canon',
-      nameShort: 'BC',
-    }],
-    multiple: [
-      {slots: {tooltipStyle: 'none'}},
-      {slots: {tooltipStyle: 'browser'}},
-      {slots: {tooltipStyle: 'browser', content: 'Next'}},
-      {slots: {tooltipStyle: 'auto'}},
-      {slots: {tooltipStyle: 'auto', preferShortName: true}},
-      {slots: {tooltipStyle: 'auto', preferShortName: true, content: 'Next'}},
-      {slots: {tooltipStyle: 'auto', content: 'Next'}},
-      {slots: {tooltipStyle: 'wiki'}},
-      {slots: {tooltipStyle: 'wiki', content: 'Next'}},
-      {slots: {content: 'Banana'}},
-    ],
-  });
-
-  quickSnapshot('color', {
-    args: ['localized.track', {
-      directory: 'showtime-piano-refrain',
-      name: 'Showtime (Piano Refrain)',
-      color: '#38f43d',
-    }],
-    multiple: [
-      {slots: {color: false}},
-      {slots: {color: true}},
-      {slots: {color: '#aaccff'}},
-      {slots: {color: '#aaccff', tooltipStyle: 'wiki'}},
-    ],
-  });
-
-  quickSnapshot('tags in name escaped', [
-    {args: ['localized.track', {
-      directory: 'foo',
-      name: `<a href="SNOOPING">AS USUAL</a> I SEE`,
-    }]},
-    {args: ['localized.track', {
-      directory: 'bar',
-      name: `<b>boldface</b>`,
-    }]},
-    {args: ['localized.album', {
-      directory: 'exile',
-      name: '>Exile<',
-    }]},
-    {args: ['localized.track', {
-      directory: 'heart',
-      name: '<3',
-    }]},
-  ]);
-
-  quickSnapshot('nested links in content stripped', {
-    args: ['localized.staticPage', {directory: 'foo', name: 'Foo'}],
-    slots: {
-      content:
-        html.tag('b', {[html.joinChildren]: ''}, [
-          html.tag('a', {href: 'bar'}, `Oooo!`),
-          ` Very spooky.`,
-        ]),
-    },
-  });
-});
diff --git a/test/snapshot/transformContent.js b/test/snapshot/transformContent.js
deleted file mode 100644
index 87e337e4..00000000
--- a/test/snapshot/transformContent.js
+++ /dev/null
@@ -1,161 +0,0 @@
-import t from 'tap';
-import {testContentFunctions} from '#test-lib';
-
-testContentFunctions(t, 'transformContent (snapshot)', async (t, evaluate) => {
-  await evaluate.load({
-    mock: {
-      image: evaluate.stubContentFunction('image'),
-    },
-  });
-
-  const extraDependencies = {
-    wikiData: {
-      albumData: [
-        {directory: 'cool-album', name: 'Cool Album', color: '#123456'},
-      ],
-    },
-
-    to: (key, ...args) => `to-${key}/${args.join('/')}`,
-  };
-
-  const quickSnapshot = (message, content, slots) =>
-    evaluate.snapshot(message, {
-      name: 'transformContent',
-      args: [content],
-      extraDependencies,
-      slots,
-    });
-
-  quickSnapshot(
-    'two text paragraphs',
-      `Hello, world!\n` +
-      `Wow, this is very cool.`);
-
-  quickSnapshot(
-    'links to a thing',
-      `This is [[album:cool-album|my favorite album]].\n` +
-      `That's right, [[album:cool-album]]!`);
-
-  quickSnapshot(
-    'indent on a directly following line',
-      `<div>\n` +
-      `    <span>Wow!</span>\n` +
-      `</div>`);
-
-  quickSnapshot(
-    'indent on an indierctly following line',
-      `Some text.\n` +
-      `Yes, some more text.\n` +
-      `\n` +
-      `    I am hax0rz!!\n` +
-      `    All yor base r blong 2 us.\n` +
-      `\n` +
-      `Aye.\n` +
-      `Aye aye aye.`);
-
-  quickSnapshot(
-    'hanging indent list',
-      `Hello!\n` +
-      `\n` +
-      `* I am a list item and I\n` +
-      `  go on and on and on\n` +
-      `  and on and on and on.\n` +
-      `\n` +
-      `* I am another list item.\n` +
-      `  Yeah.\n` +
-      `\n` +
-      `In-between!\n` +
-      `\n` +
-      `* Spooky,\n` +
-      `  spooky, I say!\n` +
-      `* Following list item.\n` +
-      `  No empty line around me.\n` +
-      `* Very cool.\n` +
-      `  So, so cool.\n` +
-      `\n` +
-      `Goodbye!`);
-
-  quickSnapshot(
-    'inline images',
-      `<img src="snooping.png"> as USUAL...\n` +
-      `What do you know? <img src="cowabunga.png" width="24" height="32">\n` +
-      `[[album:cool-album|I'm on the left.]]<img src="im-on-the-right.jpg">\n` +
-      `<img src="im-on-the-left.jpg">[[album:cool-album|I'm on the right.]]\n` +
-      `Media time! <img src="media/misc/interesting.png"> Oh yeah!\n` +
-      `<img src="must.png"><img src="stick.png"><img src="together.png">\n` +
-      `And... all done! <img src="end-of-source.png">`);
-
-  quickSnapshot(
-    'non-inline image #1',
-      `<img src="spark.png">`);
-
-  quickSnapshot(
-    'non-inline image #2',
-      `Rad.\n` +
-      `<img src="spark.png">`);
-
-  quickSnapshot(
-    'non-inline image #3',
-      `<img src="spark.png">\n` +
-      `Baller.`);
-
-  quickSnapshot(
-    'dates',
-      `[[date:2023-04-13]] Yep!\n` +
-      `Very nice: [[date:25 October 2413]]`);
-
-  quickSnapshot(
-    'super basic string',
-      `Neat listing: [[string:listingPage.listAlbums.byDate.title]]`);
-
-  quickSnapshot(
-    'basic markdown',
-      `Hello *world!* This is **SO COOL.**`);
-
-  quickSnapshot(
-    'escape entire tag',
-      `\\[[album:cool-album|spooky]] [[album:cool-album|scary]]`);
-
-  quickSnapshot(
-    'escape end of tag',
-      `My favorite album is [[album:cool-album|[Tactical Omission\\]]].\n` +
-      `Your favorite album is [[album:cool-album|[Tactical Wha-Huh-Now]]].`);
-
-  quickSnapshot(
-    'escape markdown',
-      `What will it be, *ye fool?* \\*arr*`);
-
-  quickSnapshot(
-    'lyrics - basic line breaks',
-      `Hey, ho\n` +
-      `And away we go\n` +
-      `Truly, music\n` +
-      `\n` +
-      `(Oh yeah)\n` +
-      `(That's right)`,
-      {mode: 'lyrics'});
-
-  quickSnapshot(
-    'lyrics - repeated and edge line breaks',
-      `\n\nWell, you know\nHow it goes\n\n\nYessiree\n\n\n`,
-      {mode: 'lyrics'});
-
-  quickSnapshot(
-    'lyrics - line breaks around tags',
-      `The date be [[date:13 April 2004]]\n` +
-      `I say, the date be [[date:13 April 2004]]\n` +
-      `[[date:13 April 2004]]\n` +
-      `[[date:13 April 2004]][[date:13 April 2004]][[date:13 April 2004]]\n` +
-      `(Aye!)\n` +
-      `\n` +
-      `[[date:13 April 2004]]\n` +
-      `[[date:13 April 2004]][[date:13 April 2004]]\n` +
-      `[[date:13 April 2004]]\n` +
-      `\n` +
-      `[[date:13 April 2004]]\n` +
-      `[[date:13 April 2004]], and don't ye forget it`,
-      {mode: 'lyrics'});
-
-  // 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..1baa80f8 100644
--- a/test/unit/content/dependencies/linkContribution.js
+++ b/test/unit/content/dependencies/linkContribution.js
@@ -1,7 +1,7 @@
 import t from 'tap';
 import {testContentFunctions} from '#test-lib';
 
-t.test('generateContributionLinks (unit)', async t => {
+t.test('linkContribution (unit)', async t => {
   const artist1 = {
     name: 'Clark Powell',
     directory: 'clark-powell',
@@ -24,21 +24,31 @@ t.test('generateContributionLinks (unit)', async t => {
   const annotation2 = 'Snooping';
   const annotation3 = 'Arrangement';
 
-  await testContentFunctions(t, 'generateContributionLinks (unit 1)', async (t, evaluate) => {
+  const thing1 = {};
+  const thing2 = {};
+  const thing3 = {};
+
+  const contribution1 = {artist: artist1, annotation: annotation1, thing: thing1};
+  const contribution2 = {artist: artist2, annotation: annotation2, thing: thing2};
+  const contribution3 = {artist: artist3, annotation: annotation3, thing: thing3};
+
+  await testContentFunctions(t, 'linkContribution (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 +59,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),
         }
       })),
@@ -64,34 +79,37 @@ t.test('generateContributionLinks (unit)', async t => {
     evaluate({
       name: 'linkContribution',
       multiple: [
-        {args: [{artist: artist1, annotation: annotation1}]},
-        {args: [{artist: artist2, annotation: annotation2}]},
-        {args: [{artist: artist3, annotation: annotation3}]},
+        {args: [contribution1]},
+        {args: [contribution2]},
+        {args: [contribution3]},
       ],
       slots,
     });
   });
 
-  await testContentFunctions(t, 'generateContributionLinks (unit 2)', async (t, evaluate) => {
+  await testContentFunctions(t, 'linkContribution (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 +117,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),
         },
       })),
@@ -112,9 +135,9 @@ t.test('generateContributionLinks (unit)', async t => {
     evaluate({
       name: 'linkContribution',
       multiple: [
-        {args: [{artist: artist1, annotation: annotation1}]},
-        {args: [{artist: artist2, annotation: annotation2}]},
-        {args: [{artist: artist3, annotation: annotation3}]},
+        {args: [contribution1]},
+        {args: [contribution2]},
+        {args: [contribution3]},
       ],
       slots,
     });
diff --git a/test/unit/data/cacheable-object.js b/test/unit/data/cacheable-object.js
index 4b927248..4be31788 100644
--- a/test/unit/data/cacheable-object.js
+++ b/test/unit/data/cacheable-object.js
@@ -2,10 +2,14 @@ import t from 'tap';
 
 import CacheableObject from '#cacheable-object';
 
-function newCacheableObject(PD) {
-  return new (class extends CacheableObject {
-    static [CacheableObject.propertyDescriptors] = PD;
-  });
+function newCacheableObject(propertyDescriptors) {
+  const constructor = class extends CacheableObject {
+    static [CacheableObject.propertyDescriptors] = propertyDescriptors;
+  };
+
+  constructor.finalizeCacheableObjectPrototype();
+
+  return Reflect.construct(constructor, []);
 }
 
 t.test(`CacheableObject simple separate update & expose`, t => {
diff --git a/test/unit/data/composite/control-flow/withResultOfAvailabilityCheck.js b/test/unit/data/composite/control-flow/withResultOfAvailabilityCheck.js
index 2bcabb4f..9d588e4c 100644
--- a/test/unit/data/composite/control-flow/withResultOfAvailabilityCheck.js
+++ b/test/unit/data/composite/control-flow/withResultOfAvailabilityCheck.js
@@ -177,10 +177,11 @@ t.test(`withResultOfAvailabilityCheck: validate dynamic inputs`, t => {
       mode: 'banana',
     }),
     {message: `Error computing composition`, cause:
-      {message: `Error computing composition withResultOfAvailabilityCheck`, cause:
-        {message: `Errors in input values provided to withResultOfAvailabilityCheck`, errors: [
-          {message: `mode: Expected one of null empty falsy index, got banana`},
-        ]}}});
+      {message: `Error in step 1 of 2, withResultOfAvailabilityCheck`, cause:
+        {message: `Error computing composition withResultOfAvailabilityCheck`, cause:
+          {message: `Errors in input values provided to withResultOfAvailabilityCheck`, errors: [
+            {message: `mode: Expected one of null empty falsy index, got banana`},
+          ]}}}});
 
   t.throws(
     () => composite.expose.compute({
@@ -188,8 +189,9 @@ t.test(`withResultOfAvailabilityCheck: validate dynamic inputs`, t => {
       mode: null,
     }),
     {message: `Error computing composition`, cause:
-      {message: `Error computing composition withResultOfAvailabilityCheck`, cause:
-        {message: `Errors in input values provided to withResultOfAvailabilityCheck`, errors: [
-          {message: `mode: Expected a value, got null`},
-        ]}}});
+      {message: `Error in step 1 of 2, withResultOfAvailabilityCheck`, cause:
+        {message: `Error computing composition withResultOfAvailabilityCheck`, cause:
+          {message: `Errors in input values provided to withResultOfAvailabilityCheck`, errors: [
+            {message: `mode: Expected a value, got null`},
+          ]}}}});
 });
diff --git a/test/unit/data/composite/data/withPropertiesFromObject.js b/test/unit/data/composite/data/withPropertiesFromObject.js
index 750dc8c4..b81d51a5 100644
--- a/test/unit/data/composite/data/withPropertiesFromObject.js
+++ b/test/unit/data/composite/data/withPropertiesFromObject.js
@@ -1,4 +1,5 @@
 import t from 'tap';
+import {quickCheckCompositeOutputs} from '#test-lib';
 
 import {compositeFrom, input} from '#composite';
 import {exposeDependency} from '#composite/control-flow';
@@ -62,6 +63,8 @@ t.test(`withPropertiesFromObject: output shapes & values`, t => {
       ['foo', 'baz', 'missing3'],
   };
 
+  const qcco = quickCheckCompositeOutputs(t, dependencies);
+
   const mapLevel1 = [
     [input.value('prefix_value'), [
       ['object_dependency', [
@@ -153,28 +156,10 @@ t.test(`withPropertiesFromObject: output shapes & values`, t => {
           properties: propertiesInput,
         });
 
-        quickCheckOutputs(step, outputDict);
+        qcco(step, outputDict);
       }
     }
   }
-
-  function quickCheckOutputs(step, outputDict) {
-    t.same(
-      Object.keys(step.toDescription().outputs),
-      Object.keys(outputDict));
-
-    const composite = compositeFrom({
-      compose: false,
-      steps: [step, {
-        dependencies: Object.keys(outputDict),
-        compute: dependencies => dependencies,
-      }],
-    });
-
-    t.same(
-      composite.expose.compute(dependencies),
-      outputDict);
-  }
 });
 
 t.test(`withPropertiesFromObject: validate static inputs`, t => {
@@ -226,11 +211,12 @@ t.test(`withPropertiesFromObject: validate dynamic inputs`, t => {
       properties: 'onceMore',
     }),
     {message: `Error computing composition`, cause:
-      {message: `Error computing composition withPropertiesFromObject`, cause:
-        {message: `Errors in input values provided to withPropertiesFromObject`, errors: [
-          {message: `object: Expected an object, got string`},
-          {message: `properties: Expected an array, got string`},
-        ]}}});
+      {message: `Error in step 1 of 2, withPropertiesFromObject`, cause:
+        {message: `Error computing composition withPropertiesFromObject`, cause:
+          {message: `Errors in input values provided to withPropertiesFromObject`, errors: [
+            {message: `object: Expected an object, got string`},
+            {message: `properties: Expected an array, got string`},
+          ]}}}});
 
   t.throws(
     () => composite.expose.compute({
@@ -238,17 +224,18 @@ t.test(`withPropertiesFromObject: validate dynamic inputs`, t => {
       properties: ['abc', 'def', 123],
     }),
     {message: `Error computing composition`, cause:
-      {message: `Error computing composition withPropertiesFromObject`, cause:
-        {message: `Errors in input values provided to withPropertiesFromObject`, errors: [
-          {message: `object: Expected an object, got array`},
-          {message: `properties: Errors validating array items`, errors: [
-            {
-              [Symbol.for('hsmusic.annotateError.indexInSourceArray')]: 2,
-              message: `Error at zero-index 2: 123`,
-              cause: {
-                message: `Expected a string, got number`,
+      {message: `Error in step 1 of 2, withPropertiesFromObject`, cause:
+        {message: `Error computing composition withPropertiesFromObject`, cause:
+          {message: `Errors in input values provided to withPropertiesFromObject`, errors: [
+            {message: `object: Expected an object, got array`},
+            {message: `properties: Errors validating array items`, errors: [
+              {
+                [Symbol.for('hsmusic.annotateError.indexInSourceArray')]: 2,
+                message: `Error at zero-index 2: 123`,
+                cause: {
+                  message: `Expected a string, got number`,
+                },
               },
-            },
-          ]},
-        ]}}});
+            ]},
+          ]}}}});
 });
diff --git a/test/unit/data/composite/data/withPropertyFromObject.js b/test/unit/data/composite/data/withPropertyFromObject.js
index 6a772c36..068932e2 100644
--- a/test/unit/data/composite/data/withPropertyFromObject.js
+++ b/test/unit/data/composite/data/withPropertyFromObject.js
@@ -1,5 +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';
@@ -42,6 +44,93 @@ 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 constructor = 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,
+        },
+      },
+    };
+  };
+
+  constructor.finalizeCacheableObjectPrototype();
+
+  const thing = Reflect.construct(constructor, []);
+
+  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);
 
@@ -56,6 +145,8 @@ t.test(`withPropertyFromObject: output shapes & values`, t => {
       'baz',
   };
 
+  const qcco = quickCheckCompositeOutputs(t, dependencies);
+
   const mapLevel1 = [
     ['object_dependency', [
       ['property_dependency', {
@@ -98,25 +189,7 @@ t.test(`withPropertyFromObject: output shapes & values`, t => {
         property: propertyInput,
       });
 
-      quickCheckOutputs(step, outputDict);
+      qcco(step, outputDict);
     }
   }
-
-  function quickCheckOutputs(step, outputDict) {
-    t.same(
-      Object.keys(step.toDescription().outputs),
-      Object.keys(outputDict));
-
-    const composite = compositeFrom({
-      compose: false,
-      steps: [step, {
-        dependencies: Object.keys(outputDict),
-        compute: dependencies => dependencies,
-      }],
-    });
-
-    t.same(
-      composite.expose.compute(dependencies),
-      outputDict);
-  }
 });
diff --git a/test/unit/data/composite/data/withUniqueItemsOnly.js b/test/unit/data/composite/data/withUniqueItemsOnly.js
index 965b14b5..50b16f43 100644
--- a/test/unit/data/composite/data/withUniqueItemsOnly.js
+++ b/test/unit/data/composite/data/withUniqueItemsOnly.js
@@ -1,4 +1,5 @@
 import t from 'tap';
+import {quickCheckCompositeOutputs} from '#test-lib';
 
 import {compositeFrom, input} from '#composite';
 import {exposeDependency} from '#composite/control-flow';
@@ -44,6 +45,8 @@ t.test(`withUniqueItemsOnly: output shapes & values`, t => {
       [8, 8, 7, 6, 6, 5, 'bar', true, true, 5],
   };
 
+  const qcco = quickCheckCompositeOutputs(t, dependencies);
+
   const mapLevel1 = [
     ['list_dependency', {
       '#list_dependency': [1, 2, 3, 4, 'foo', false],
@@ -61,24 +64,6 @@ t.test(`withUniqueItemsOnly: output shapes & values`, t => {
       list: listInput,
     });
 
-    quickCheckOutputs(step, outputDict);
-  }
-
-  function quickCheckOutputs(step, outputDict) {
-    t.same(
-      Object.keys(step.toDescription().outputs),
-      Object.keys(outputDict));
-
-    const composite = compositeFrom({
-      compose: false,
-      steps: [step, {
-        dependencies: Object.keys(outputDict),
-        compute: dependencies => dependencies,
-      }],
-    });
-
-    t.same(
-      composite.expose.compute(dependencies),
-      outputDict);
+    qcco(step, outputDict);
   }
 });
diff --git a/test/unit/data/composite/things/track/withAlbum.js b/test/unit/data/composite/things/track/withAlbum.js
deleted file mode 100644
index 6f50776b..00000000
--- a/test/unit/data/composite/things/track/withAlbum.js
+++ /dev/null
@@ -1,119 +0,0 @@
-import t from 'tap';
-
-import '#import-heck';
-
-import Thing from '#thing';
-
-import {compositeFrom, input} from '#composite';
-import {exposeConstant, exposeDependency} from '#composite/control-flow';
-import {withAlbum} from '#composite/things/track';
-
-t.test(`withAlbum: basic behavior`, t => {
-  t.plan(3);
-
-  const composite = compositeFrom({
-    compose: false,
-    steps: [
-      withAlbum(),
-      exposeDependency({dependency: '#album'}),
-    ],
-  });
-
-  t.match(composite, {
-    expose: {
-      dependencies: ['albumData', 'this'],
-    },
-  });
-
-  const fakeTrack1 = {
-    [Thing.isThing]: true,
-    directory: 'foo',
-  };
-
-  const fakeTrack2 = {
-    [Thing.isThing]: true,
-    directory: 'bar',
-  };
-
-  const fakeAlbum = {
-    [Thing.isThing]: true,
-    directory: 'baz',
-    tracks: [fakeTrack1],
-  };
-
-  t.equal(
-    composite.expose.compute({
-      albumData: [fakeAlbum],
-      this: fakeTrack1,
-    }),
-    fakeAlbum);
-
-  t.equal(
-    composite.expose.compute({
-      albumData: [fakeAlbum],
-      this: fakeTrack2,
-    }),
-    null);
-});
-
-t.test(`withAlbum: early exit conditions`, t => {
-  t.plan(4);
-
-  const composite = compositeFrom({
-    compose: false,
-    steps: [
-      withAlbum(),
-      exposeConstant({
-        value: input.value('bimbam'),
-      }),
-    ],
-  });
-
-  const fakeTrack1 = {
-    [Thing.isThing]: true,
-    directory: 'foo',
-  };
-
-  const fakeTrack2 = {
-    [Thing.isThing]: true,
-    directory: 'bar',
-  };
-
-  const fakeAlbum = {
-    [Thing.isThing]: true,
-    directory: 'baz',
-    tracks: [fakeTrack1],
-  };
-
-  t.equal(
-    composite.expose.compute({
-      albumData: [fakeAlbum],
-      this: fakeTrack1,
-    }),
-    'bimbam',
-    `does not early exit if albumData is present and contains the track`);
-
-  t.equal(
-    composite.expose.compute({
-      albumData: [fakeAlbum],
-      this: fakeTrack2,
-    }),
-    'bimbam',
-    `does not early exit if albumData is present and does not contain the track`);
-
-  t.equal(
-    composite.expose.compute({
-      albumData: [],
-      this: fakeTrack1,
-    }),
-    'bimbam',
-    `does not early exit if albumData is empty array`);
-
-  t.equal(
-    composite.expose.compute({
-      albumData: null,
-      this: fakeTrack1,
-    }),
-    null,
-    `early exits if albumData is null`);
-});
diff --git a/test/unit/data/composite/wiki-data/withParsedCommentaryEntries.js b/test/unit/data/composite/wiki-data/withParsedCommentaryEntries.js
deleted file mode 100644
index babe4fae..00000000
--- a/test/unit/data/composite/wiki-data/withParsedCommentaryEntries.js
+++ /dev/null
@@ -1,102 +0,0 @@
-import t from 'tap';
-
-import {compositeFrom, input} from '#composite';
-import thingConstructors from '#things';
-
-import {exposeDependency} from '#composite/control-flow';
-import {withParsedCommentaryEntries} from '#composite/wiki-data';
-
-const {Artist} = thingConstructors;
-
-const composite = compositeFrom({
-  compose: false,
-
-  steps: [
-    withParsedCommentaryEntries({
-      from: 'from',
-    }),
-
-    exposeDependency({dependency: '#parsedCommentaryEntries'}),
-  ],
-});
-
-function stubArtist(artistName = `Test Artist`) {
-  const artist = new Artist();
-  artist.name = artistName;
-
-  return artist;
-}
-
-t.test(`withParsedCommentaryEntries: basic behavior`, t => {
-  t.plan(3);
-
-  const artist1 = stubArtist(`Mobius Trip`);
-  const artist2 = stubArtist(`Hadron Kaleido`);
-
-  const artistData = [artist1, artist2];
-
-  t.match(composite, {
-    expose: {
-      dependencies: ['from', 'artistData'],
-    },
-  });
-
-  t.same(composite.expose.compute({
-    artistData,
-    from:
-      `<i>Mobius Trip:</i>\n` +
-      `Some commentary.\n` +
-      `Very cool.\n`,
-  }), [
-    {
-      artists: [artist1],
-      artistDisplayText: null,
-      annotation: null,
-      date: null,
-      body: `Some commentary.\nVery cool.`,
-    },
-  ]);
-
-  t.same(composite.expose.compute({
-    artistData,
-    from:
-      `<i>Mobius Trip|Moo-bius Trip:</i> (music, art, 12 January 2015)\n` +
-      `First commentary entry.\n` +
-      `Very cool.\n` +
-      `<i>Hadron Kaleido|<b>[[artist:hadron-kaleido|The Ol' Hadron]]</b>:</i> (moral support, 4/4/2022)\n` +
-      `Second commentary entry. Yes. So cool.\n` +
-      `<i>Mystery Artist:</i> (pingas, August 25, 2023)\n` +
-      `Oh no.. Oh dear...\n` +
-      `<i>Mobius Trip, Hadron Kaleido:</i>\n` +
-      `And back around we go.`,
-  }), [
-    {
-      artists: [artist1],
-      artistDisplayText: `Moo-bius Trip`,
-      annotation: `music, art`,
-      date: new Date('12 January 2015'),
-      body: `First commentary entry.\nVery cool.`,
-    },
-    {
-      artists: [artist2],
-      artistDisplayText: `<b>[[artist:hadron-kaleido|The Ol' Hadron]]</b>`,
-      annotation: `moral support`,
-      date: new Date('4 April 2022'),
-      body: `Second commentary entry. Yes. So cool.`,
-    },
-    {
-      artists: [],
-      artistDisplayText: null,
-      annotation: `pingas`,
-      date: new Date('25 August 2023'),
-      body: `Oh no.. Oh dear...`,
-    },
-    {
-      artists: [artist1, artist2],
-      artistDisplayText: null,
-      annotation: null,
-      date: null,
-      body: `And back around we go.`,
-    },
-  ]);
-});
diff --git a/test/unit/data/things/album.js b/test/unit/data/things/album.js
deleted file mode 100644
index d28ab709..00000000
--- a/test/unit/data/things/album.js
+++ /dev/null
@@ -1,520 +0,0 @@
-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;
-}
-
-t.test(`Album.artTags`, t => {
-  t.plan(3);
-
-  const {artist, contribs} = stubArtistAndContribs();
-  const album = new Album();
-  const tag1 = stubArtTag(`Tag 1`);
-  const tag2 = stubArtTag(`Tag 2`);
-
-  const {XXX_decacheWikiData} = linkAndBindWikiData({
-    albumData: [album],
-    artistData: [artist],
-    artTagData: [tag1, tag2],
-  });
-
-  t.same(album.artTags, [],
-    `artTags #1: defaults to empty array`);
-
-  album.artTags = [`Tag 1`, `Tag 2`];
-
-  t.same(album.artTags, [],
-    `artTags #2: is empty if album doesn't have cover artists`);
-
-  album.coverArtistContribs = contribs;
-
-  t.same(album.artTags, [tag1, tag2],
-    `artTags #3: resolves if album has cover artists`);
-});
-
-t.test(`Album.bannerDimensions`, t => {
-  t.plan(4);
-
-  const album = new Album();
-  const {artist, contribs, badContribs} = stubArtistAndContribs();
-
-  linkAndBindWikiData({
-    albumData: [album],
-    artistData: [artist],
-  });
-
-  t.equal(album.bannerDimensions, null,
-    `Album.bannerDimensions #1: defaults to null`);
-
-  album.bannerDimensions = [1200, 275];
-
-  t.equal(album.bannerDimensions, null,
-    `Album.bannerDimensions #2: is null if bannerArtistContribs empty`);
-
-  album.bannerArtistContribs = badContribs;
-
-  t.equal(album.bannerDimensions, null,
-    `Album.bannerDimensions #3: is null if bannerArtistContribs resolves empty`);
-
-  album.bannerArtistContribs = contribs;
-
-  t.same(album.bannerDimensions, [1200, 275],
-    `Album.bannerDimensions #4: is own value`);
-});
-
-t.test(`Album.bannerFileExtension`, t => {
-  t.plan(5);
-
-  const album = new Album();
-  const {artist, contribs, badContribs} = stubArtistAndContribs();
-
-  linkAndBindWikiData({
-    albumData: [album],
-    artistData: [artist],
-  });
-
-  t.equal(album.bannerFileExtension, null,
-    `Album.bannerFileExtension #1: defaults to null`);
-
-  album.bannerFileExtension = 'png';
-
-  t.equal(album.bannerFileExtension, null,
-    `Album.bannerFileExtension #2: is null if bannerArtistContribs empty`);
-
-  album.bannerArtistContribs = badContribs;
-
-  t.equal(album.bannerFileExtension, null,
-    `Album.bannerFileExtension #3: is null if bannerArtistContribs resolves empty`);
-
-  album.bannerArtistContribs = contribs;
-
-  t.equal(album.bannerFileExtension, 'png',
-    `Album.bannerFileExtension #4: is own value`);
-
-  album.bannerFileExtension = null;
-
-  t.equal(album.bannerFileExtension, 'jpg',
-    `Album.bannerFileExtension #5: defaults to jpg`);
-});
-
-t.test(`Album.bannerStyle`, t => {
-  t.plan(4);
-
-  const album = new Album();
-  const {artist, contribs, badContribs} = stubArtistAndContribs();
-
-  linkAndBindWikiData({
-    albumData: [album],
-    artistData: [artist],
-  });
-
-  t.equal(album.bannerStyle, null,
-    `Album.bannerStyle #1: defaults to null`);
-
-  album.bannerStyle = `opacity: 0.5`;
-
-  t.equal(album.bannerStyle, null,
-    `Album.bannerStyle #2: is null if bannerArtistContribs empty`);
-
-  album.bannerArtistContribs = badContribs;
-
-  t.equal(album.bannerStyle, null,
-    `Album.bannerStyle #3: is null if bannerArtistContribs resolves empty`);
-
-  album.bannerArtistContribs = contribs;
-
-  t.equal(album.bannerStyle, `opacity: 0.5`,
-    `Album.bannerStyle #4: is own value`);
-});
-
-t.test(`Album.coverArtDate`, t => {
-  t.plan(6);
-
-  const album = new Album();
-  const {artist, contribs, badContribs} = stubArtistAndContribs();
-
-  linkAndBindWikiData({
-    albumData: [album],
-    artistData: [artist],
-  });
-
-  t.equal(album.coverArtDate, null,
-    `Album.coverArtDate #1: defaults to null`);
-
-  album.date = new Date('2012-10-25');
-
-  t.equal(album.coverArtDate, null,
-    `Album.coverArtDate #2: is null if coverArtistContribs empty (1/2)`);
-
-  album.coverArtDate = new Date('2011-04-13');
-
-  t.equal(album.coverArtDate, null,
-    `Album.coverArtDate #3: is null if coverArtistContribs empty (2/2)`);
-
-  album.coverArtistContribs = contribs;
-
-  t.same(album.coverArtDate, new Date('2011-04-13'),
-    `Album.coverArtDate #4: is own value`);
-
-  album.coverArtDate = null;
-
-  t.same(album.coverArtDate, new Date(`2012-10-25`),
-    `Album.coverArtDate #5: inherits album release date`);
-
-  album.coverArtistContribs = badContribs;
-
-  t.equal(album.coverArtDate, null,
-    `Album.coverArtDate #6: is null if coverArtistContribs resolves empty`);
-});
-
-t.test(`Album.coverArtFileExtension`, t => {
-  t.plan(5);
-
-  const album = new Album();
-  const {artist, contribs, badContribs} = stubArtistAndContribs();
-
-  linkAndBindWikiData({
-    albumData: [album],
-    artistData: [artist],
-  });
-
-  t.equal(album.coverArtFileExtension, null,
-    `Album.coverArtFileExtension #1: is null if coverArtistContribs empty (1/2)`);
-
-  album.coverArtFileExtension = 'png';
-
-  t.equal(album.coverArtFileExtension, null,
-    `Album.coverArtFileExtension #2: is null if coverArtistContribs empty (2/2)`);
-
-  album.coverArtFileExtension = null;
-  album.coverArtistContribs = contribs;
-
-  t.equal(album.coverArtFileExtension, 'jpg',
-    `Album.coverArtFileExtension #3: defaults to jpg`);
-
-  album.coverArtFileExtension = 'png';
-
-  t.equal(album.coverArtFileExtension, 'png',
-    `Album.coverArtFileExtension #4: is own value`);
-
-  album.coverArtistContribs = badContribs;
-
-  t.equal(album.coverArtFileExtension, null,
-    `Album.coverArtFileExtension #5: is null if coverArtistContribs resolves empty`);
-});
-
-t.test(`Album.tracks`, t => {
-  t.plan(5);
-
-  const album = new Album();
-  album.directory = 'foo';
-
-  const track1 = stubTrack('track1');
-  const track2 = stubTrack('track2');
-  const track3 = stubTrack('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 sections = [section1, section2, section3, section4, section5, section6];
-
-  for (const track of tracks) {
-    track.albumData = [album];
-  }
-
-  for (const section of sections) {
-    section.ownAlbumData = [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];
-
-  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.ownTrackData = [track1];
-  section2.ownTrackData = [track2, track3];
-
-  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, section2, section3, section4];
-
-  t.same(album.tracks, [track1, track2, track3],
-    `Album.tracks #4: filters out references without matches`);
-
-  section1.tracks = ['track:track1'];
-  section2.tracks = [];
-  section3.tracks = ['track:track2'];
-  section4.tracks = [];
-  section5.tracks = [];
-  section6.tracks = ['track:track3'];
-
-  section1.ownTrackData = [track1];
-  section2.ownTrackData = [];
-  section3.ownTrackData = [track2];
-  section4.ownTrackData = [];
-  section5.ownTrackData = [];
-  section6.ownTrackData = [track3];
-
-  album.trackSections = [section1, section2, section3, section4, section5, section6];
-
-  t.same(album.tracks, [track1, track2, track3],
-    `Album.tracks #5: skips empty track sections`);
-});
-
-t.test(`Album.trackSections`, t => {
-  t.plan(7);
-
-  const album = new Album();
-
-  const track1 = stubTrack('track1');
-  const track2 = stubTrack('track2');
-  const track3 = stubTrack('track3');
-  const track4 = stubTrack('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 sections = [section1, section2, section3, section4, section5];
-
-  for (const track of tracks) {
-    track.albumData = [album];
-  }
-
-  section1.tracks = ['track:track1', 'track:track2'];
-  section2.tracks = ['track:track3', 'track:track4'];
-
-  section1.ownTrackData = [track1, track2];
-  section2.ownTrackData = [track3, track4];
-
-  album.trackSections = [section1, section2];
-
-  t.match(album.trackSections, [
-    {tracks: [track1, track2]},
-    {tracks: [track3, track4]},
-  ], `Album.trackSections #1: exposes tracks`);
-
-  t.match(album.trackSections, [
-    {tracks: [track1, track2], startIndex: 0},
-    {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.name = 'First section';
-  section2.name = 'Second section';
-
-  album.trackSections = [section1, section2, section3];
-
-  t.match(album.trackSections, [
-    {name: 'First section', tracks: [track1]},
-    {name: 'Second section', tracks: [track2]},
-    {name: 'Unnamed Track Section', tracks: [track3]},
-  ], `Album.trackSections #3: exposes name, with fallback value`);
-
-  album.color = '#123456';
-
-  section2.color = '#abcdef';
-
-  // XXX_decacheWikiData
-  album.trackSections = [];
-  album.trackSections = [section1, section2, section3];
-
-  t.match(album.trackSections, [
-    {tracks: [track1], color: '#123456'},
-    {tracks: [track2], color: '#abcdef'},
-    {tracks: [track3], color: '#123456'},
-  ], `Album.trackSections #4: exposes color, inherited from album`);
-
-  section2.dateOriginallyReleased = new Date('2009-04-11');
-
-  // XXX_decacheWikiData
-  album.trackSections = [];
-  album.trackSections = [section1, section2, section3];
-
-  t.match(album.trackSections, [
-    {tracks: [track1], dateOriginallyReleased: null},
-    {tracks: [track2], dateOriginallyReleased: new Date('2009-04-11')},
-    {tracks: [track3], dateOriginallyReleased: null},
-  ], `Album.trackSections #5: exposes dateOriginallyReleased, if present`);
-
-  section1.isDefaultTrackSection = true;
-  section2.isDefaultTrackSection = false;
-
-  // XXX_decacheWikiData
-  album.trackSections = [];
-  album.trackSections = [section1, section2, section3];
-
-  t.match(album.trackSections, [
-    {tracks: [track1], isDefaultTrackSection: true},
-    {tracks: [track2], isDefaultTrackSection: false},
-    {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'];
-  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];
-
-  section1.color = '#112233';
-  section2.color = '#334455';
-  section3.color = '#bbbbba';
-  section4.color = '#556677';
-  section5.color = '#778899';
-
-  album.trackSections = [section1, section2, section3, section4, section5];
-
-  t.match(album.trackSections, [
-    {tracks: [track1, track2], color: '#112233'},
-    {tracks: [track3],         color: '#334455'},
-    {tracks: [],               color: '#bbbbba'},
-    {tracks: [],               color: '#556677'},
-    {tracks: [track4],         color: '#778899'},
-  ], `Album.trackSections #7: filters out references without matches, keeps empty sections`);
-});
-
-t.test(`Album.wallpaperFileExtension`, t => {
-  t.plan(5);
-
-  const album = new Album();
-  const {artist, contribs, badContribs} = stubArtistAndContribs();
-
-  linkAndBindWikiData({
-    albumData: [album],
-    artistData: [artist],
-  });
-
-  t.equal(album.wallpaperFileExtension, null,
-    `Album.wallpaperFileExtension #1: defaults to null`);
-
-  album.wallpaperFileExtension = 'png';
-
-  t.equal(album.wallpaperFileExtension, null,
-    `Album.wallpaperFileExtension #2: is null if wallpaperArtistContribs empty`);
-
-  album.wallpaperArtistContribs = contribs;
-
-  t.equal(album.wallpaperFileExtension, 'png',
-    `Album.wallpaperFileExtension #3: is own value`);
-
-  album.wallpaperFileExtension = null;
-
-  t.equal(album.wallpaperFileExtension, 'jpg',
-    `Album.wallpaperFileExtension #4: defaults to jpg`);
-
-  album.wallpaperArtistContribs = badContribs;
-
-  t.equal(album.wallpaperFileExtension, null,
-    `Album.wallpaperFileExtension #5: is null if wallpaperArtistContribs resolves empty`);
-});
-
-t.test(`Album.wallpaperStyle`, t => {
-  t.plan(4);
-
-  const album = new Album();
-  const {artist, contribs, badContribs} = stubArtistAndContribs();
-
-  linkAndBindWikiData({
-    albumData: [album],
-    artistData: [artist],
-  });
-
-  t.equal(album.wallpaperStyle, null,
-    `Album.wallpaperStyle #1: defaults to null`);
-
-  album.wallpaperStyle = `opacity: 0.5`;
-
-  t.equal(album.wallpaperStyle, null,
-    `Album.wallpaperStyle #2: is null if wallpaperArtistContribs empty`);
-
-  album.wallpaperArtistContribs = badContribs;
-
-  t.equal(album.wallpaperStyle, null,
-    `Album.wallpaperStyle #3: is null if wallpaperArtistContribs resolves empty`);
-
-  album.wallpaperArtistContribs = contribs;
-
-  t.equal(album.wallpaperStyle, `opacity: 0.5`,
-    `Album.wallpaperStyle #4: is own value`);
-});
diff --git a/test/unit/data/things/art-tag.js b/test/unit/data/things/art-tag.js
deleted file mode 100644
index 427b357b..00000000
--- a/test/unit/data/things/art-tag.js
+++ /dev/null
@@ -1,81 +0,0 @@
-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 = [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 => {
-  t.plan(3);
-
-  const artTag = new ArtTag();
-
-  artTag.name = `Dave Strider`;
-
-  t.equal(artTag.nameShort, `Dave Strider`,
-    `ArtTag #1: defaults to name`);
-
-  artTag.name = `Dave Strider (Homestuck)`;
-
-  t.equal(artTag.nameShort, `Dave Strider`,
-    `ArtTag #2: trims parenthical part at end`);
-
-  artTag.name = `This (And) That (Then)`;
-
-  t.equal(artTag.nameShort, `This (And) That`,
-    `ArtTag #2: doesn't trim midlde parenthical part`);
-});
diff --git a/test/unit/data/things/flash.js b/test/unit/data/things/flash.js
deleted file mode 100644
index 62059604..00000000
--- a/test/unit/data/things/flash.js
+++ /dev/null
@@ -1,55 +0,0 @@
-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;
-}
-
-t.test(`Flash.color`, t => {
-  t.plan(4);
-
-  const flash = stubFlash();
-  const flashAct = stubFlashAct([flash]);
-
-  const {XXX_decacheWikiData} = linkAndBindWikiData({
-    flashData: [flash],
-    flashActData: [flashAct],
-  });
-
-  t.equal(flash.color, null,
-    `color #1: defaults to null`);
-
-  flashAct.color = '#abcdef';
-  XXX_decacheWikiData();
-
-  t.equal(flash.color, '#abcdef',
-    `color #2: inherits from flash act`);
-
-  flash.color = '#123456';
-
-  t.equal(flash.color, '#123456',
-    `color #3: is own value`);
-
-  t.throws(() => { flash.color = '#aeiouw'; },
-    {cause: TypeError},
-    `color #4: must be set to valid color`);
-});
diff --git a/test/unit/data/things/track.js b/test/unit/data/things/track.js
deleted file mode 100644
index 644d21ce..00000000
--- a/test/unit/data/things/track.js
+++ /dev/null
@@ -1,840 +0,0 @@
-import t from 'tap';
-
-import {linkAndBindWikiData} from '#test-lib';
-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 = [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};
-}
-
-t.test(`Track.album`, t => {
-  t.plan(6);
-
-  // Note: These asserts use manual albumData/trackData relationships
-  // to illustrate more specifically the properties which are expected to
-  // 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();
-
-  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];
-  album2.trackSections = [section2];
-
-  t.equal(track1.album, album1,
-    `album #2: is album when album's trackSections matches track`);
-
-  track1.albumData = [album2, album1];
-
-  t.equal(track1.album, album1,
-    `album #3: is album when albumData is in different order`);
-
-  track1.albumData = [];
-
-  t.equal(track1.album, null,
-    `album #4: is null when track missing albumData`);
-
-  section1.ownTrackData = [];
-
-  // XXX_decacheWikiData
-  album1.trackSections = [];
-  album1.trackSections = [section1];
-  track1.albumData = [];
-  track1.albumData = [album2, album1];
-
-  t.equal(track1.album, null,
-    `album #5: is null when album track section missing ownTrackData`);
-
-  section1.ownTrackData = [track2];
-  section1.tracks = ['track:track2'];
-
-  // XXX_decacheWikiData
-  album1.trackSections = [];
-  album1.trackSections = [section1];
-  track1.albumData = [];
-  track1.albumData = [album2, album1];
-
-  t.equal(track1.album, null,
-    `album #6: is null when album track section doesn't match track`);
-});
-
-t.test(`Track.alwaysReferenceByDirectory`, t => {
-  t.plan(7);
-
-  const {track: originalTrack, album: originalAlbum} =
-    stubTrackAndAlbum('original-track', 'original-album');
-
-  const {track: rereleaseTrack, album: rereleaseAlbum} =
-    stubTrackAndAlbum('rerelease-track', 'rerelease-album');
-
-  originalTrack.name = 'Cowabunga';
-  rereleaseTrack.name = 'Cowabunga';
-
-  originalTrack.dataSourceAlbum = 'album:original-album';
-  rereleaseTrack.dataSourceAlbum = 'album:rerelease-album';
-
-  rereleaseTrack.originalReleaseTrack = 'track:original-track';
-
-  const {XXX_decacheWikiData} = linkAndBindWikiData({
-    albumData: [originalAlbum, rereleaseAlbum],
-    trackData: [originalTrack, rereleaseTrack],
-  });
-
-  t.equal(originalTrack.alwaysReferenceByDirectory, false,
-    `alwaysReferenceByDirectory #1: defaults to false`);
-
-  t.equal(rereleaseTrack.alwaysReferenceByDirectory, true,
-    `alwaysReferenceByDirectory #2: is true if rerelease name matches original`);
-
-  rereleaseTrack.name = 'Foo Dog!';
-
-  t.equal(rereleaseTrack.alwaysReferenceByDirectory, false,
-    `alwaysReferenceByDirectory #3: is false if rerelease name doesn't match original`);
-
-  rereleaseTrack.name = `COWabunga`;
-
-  t.equal(rereleaseTrack.alwaysReferenceByDirectory, false,
-    `alwaysReferenceByDirectory #4: is false if rerelease name doesn't match original exactly`);
-
-  rereleaseAlbum.alwaysReferenceTracksByDirectory = true;
-  XXX_decacheWikiData();
-
-  t.equal(rereleaseTrack.alwaysReferenceByDirectory, true,
-    `alwaysReferenceByDirectory #5: is true if album's alwaysReferenceTracksByDirectory is true`);
-
-  rereleaseTrack.alwaysReferenceByDirectory = false;
-
-  t.equal(rereleaseTrack.alwaysReferenceByDirectory, false,
-    `alwaysReferenceByDirectory #6: doesn't inherit from album if set to false`);
-
-  rereleaseTrack.name = 'Cowabunga';
-
-  t.equal(rereleaseTrack.alwaysReferenceByDirectory, false,
-    `alwaysReferenceByDirectory #7: doesn't compare original release name if set to false`);
-});
-
-t.test(`Track.artTags`, t => {
-  t.plan(6);
-
-  const {track, album} = stubTrackAndAlbum();
-  const {artist, contribs} = stubArtistAndContribs();
-  const tag1 = stubArtTag(`Tag 1`);
-  const tag2 = stubArtTag(`Tag 2`);
-
-  const {XXX_decacheWikiData} = linkAndBindWikiData({
-    albumData: [album],
-    artistData: [artist],
-    artTagData: [tag1, tag2],
-    trackData: [track],
-  });
-
-  t.same(track.artTags, [],
-    `artTags #1: defaults to empty array`);
-
-  track.artTags = [`Tag 1`, `Tag 2`];
-
-  t.same(track.artTags, [],
-    `artTags #2: is empty if track doesn't have cover artists`);
-
-  track.coverArtistContribs = contribs;
-
-  t.same(track.artTags, [tag1, tag2],
-    `artTags #3: resolves if track has cover artists`);
-
-  track.coverArtistContribs = null;
-  album.trackCoverArtistContribs = contribs;
-
-  XXX_decacheWikiData();
-
-  t.same(track.artTags, [tag1, tag2],
-    `artTags #4: resolves if track inherits cover artists`);
-
-  track.disableUniqueCoverArt = true;
-
-  t.same(track.artTags, [],
-    `artTags #5: is empty if track disables unique cover artwork`);
-
-  album.coverArtistContribs = contribs;
-  album.artTags = [`Tag 2`];
-
-  XXX_decacheWikiData();
-
-  t.notSame(track.artTags, [tag2],
-    `artTags #6: doesn't inherit from album's art tags`);
-});
-
-t.test(`Track.artistContribs`, t => {
-  t.plan(4);
-
-  const {track, album} = stubTrackAndAlbum();
-  const artist1 = stubArtist(`Artist 1`);
-  const artist2 = stubArtist(`Artist 2`);
-
-  const {XXX_decacheWikiData} = linkAndBindWikiData({
-    albumData: [album],
-    artistData: [artist1, artist2],
-    trackData: [track],
-  });
-
-  t.same(track.artistContribs, [],
-    `artistContribs #1: defaults to empty array`);
-
-  album.artistContribs = [
-    {artist: `Artist 1`, annotation: `composition`},
-    {artist: `Artist 2`, annotation: null},
-  ];
-
-  XXX_decacheWikiData();
-
-  t.same(track.artistContribs,
-    [{artist: artist1, annotation: `composition`}, {artist: artist2, annotation: null}],
-    `artistContribs #2: inherits album artistContribs`);
-
-  track.artistContribs = [
-    {artist: `Artist 1`, annotation: `arrangement`},
-  ];
-
-  t.same(track.artistContribs, [{artist: artist1, annotation: `arrangement`}],
-    `artistContribs #3: resolves from own value`);
-
-  track.artistContribs = [
-    {artist: `Artist 1`, annotation: `snooping`},
-    {artist: `Artist 413`, annotation: `as`},
-    {artist: `Artist 2`, annotation: `usual`},
-  ];
-
-  t.same(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);
-
-  const {track, album} = stubTrackAndAlbum();
-
-  const {XXX_decacheWikiData} = linkAndBindWikiData({
-    albumData: [album],
-    trackData: [track],
-  });
-
-  t.equal(track.color, null,
-    `color #1: defaults to null`);
-
-  const section = stubTrackSection(album, [track]);
-
-  album.color = '#abcdef';
-  section.color = '#beeeef';
-
-  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`);
-
-  t.throws(() => { track.color = '#aeiouw'; },
-    {cause: TypeError},
-    `color #5: must be set to valid color`);
-});
-
-t.test(`Track.commentatorArtists`, t => {
-  t.plan(8);
-
-  const track = new Track();
-  const artist1 = stubArtist(`SnooPING`);
-  const artist2 = stubArtist(`ASUsual`);
-  const artist3 = stubArtist(`Icy`);
-
-  linkAndBindWikiData({
-    trackData: [track],
-    artistData: [artist1, artist2, artist3],
-  });
-
-  // Keep track of the last commentary string in a separate value, since
-  // the track.commentary property exposes as a completely different format
-  // (i.e. an array of objects, one for each entry), and so isn't compatible
-  // with the += operator on its own.
-  let commentary;
-
-  track.commentary = commentary =
-    `<i>SnooPING:</i>\n` +
-    `Wow.\n`;
-
-  t.same(track.commentatorArtists, [artist1],
-    `Track.commentatorArtists #1: works with one commentator`);
-
-  track.commentary = commentary +=
-    `<i>ASUsual:</i>\n` +
-    `Yes!\n`;
-
-  t.same(track.commentatorArtists, [artist1, artist2],
-    `Track.commentatorArtists #2: works with two commentators`);
-
-  track.commentary = commentary +=
-    `<i>Icy|<b>Icy annotation You Did There</b>:</i>\n` +
-    `Incredible.\n`;
-
-  t.same(track.commentatorArtists, [artist1, artist2, artist3],
-    `Track.commentatorArtists #3: works with custom artist text`);
-
-  track.commentary = commentary =
-    `<i>Icy:</i> (project manager)\n` +
-    `Very good track.\n`;
-
-  t.same(track.commentatorArtists, [artist3],
-    `Track.commentatorArtists #4: works with annotation`);
-
-  track.commentary = commentary =
-    `<i>Icy:</i> (project manager, 08/15/2023)\n` +
-    `Very very good track.\n`;
-
-  t.same(track.commentatorArtists, [artist3],
-    `Track.commentatorArtists #5: works with date`);
-
-  track.commentary = commentary +=
-    `<i>Ohohohoho:</i>\n` +
-    `OHOHOHOHOHOHO...\n`;
-
-  t.same(track.commentatorArtists, [artist3],
-    `Track.commentatorArtists #6: ignores artist names not found`);
-
-  track.commentary = commentary +=
-    `<i>Icy:</i>\n` +
-    `I'm back!\n`;
-
-  t.same(track.commentatorArtists, [artist3],
-    `Track.commentatorArtists #7: ignores duplicate artist`);
-
-  track.commentary = commentary +=
-    `<i>SNooPING, ASUsual, Icy:</i>\n` +
-    `WITH ALL THREE POWERS COMBINED...`;
-
-  t.same(track.commentatorArtists, [artist3, artist1, artist2],
-    `Track.commentatorArtists #8: works with more than one artist in one entry`);
-});
-
-t.test(`Track.coverArtistContribs`, t => {
-  t.plan(5);
-
-  const {track, album} = stubTrackAndAlbum();
-  const artist1 = stubArtist(`Artist 1`);
-  const artist2 = stubArtist(`Artist 2`);
-
-  const {XXX_decacheWikiData} = linkAndBindWikiData({
-    albumData: [album],
-    artistData: [artist1, artist2],
-    trackData: [track],
-  });
-
-  t.same(track.coverArtistContribs, [],
-    `coverArtistContribs #1: defaults to empty array`);
-
-  album.trackCoverArtistContribs = [
-    {artist: `Artist 1`, annotation: `lines`},
-    {artist: `Artist 2`, annotation: null},
-  ];
-
-  XXX_decacheWikiData();
-
-  t.same(track.coverArtistContribs,
-    [{artist: artist1, annotation: `lines`}, {artist: artist2, annotation: null}],
-    `coverArtistContribs #2: inherits album trackCoverArtistContribs`);
-
-  track.coverArtistContribs = [
-    {artist: `Artist 1`, annotation: `collage`},
-  ];
-
-  t.same(track.coverArtistContribs, [{artist: artist1, annotation: `collage`}],
-    `coverArtistContribs #3: resolves from own value`);
-
-  track.coverArtistContribs = [
-    {artist: `Artist 1`, annotation: `snooping`},
-    {artist: `Artist 413`, annotation: `as`},
-    {artist: `Artist 2`, annotation: `usual`},
-  ];
-
-  t.same(track.coverArtistContribs,
-    [{artist: artist1, annotation: `snooping`}, {artist: artist2, annotation: `usual`}],
-    `coverArtistContribs #4: filters out names without matches`);
-
-  track.disableUniqueCoverArt = true;
-
-  t.same(track.coverArtistContribs, [],
-    `coverArtistContribs #5: is empty if track disables unique cover artwork`);
-});
-
-t.test(`Track.coverArtDate`, t => {
-  t.plan(8);
-
-  const {track, album} = stubTrackAndAlbum();
-  const {artist, contribs, badContribs} = stubArtistAndContribs();
-
-  const {XXX_decacheWikiData} = linkAndBindWikiData({
-    albumData: [album],
-    artistData: [artist],
-    trackData: [track],
-  });
-
-  track.coverArtistContribs = contribs;
-
-  t.equal(track.coverArtDate, null,
-    `coverArtDate #1: defaults to null`);
-
-  album.trackArtDate = new Date('2012-12-12');
-
-  XXX_decacheWikiData();
-
-  t.same(track.coverArtDate, new Date('2012-12-12'),
-    `coverArtDate #2: inherits album trackArtDate`);
-
-  track.coverArtDate = new Date('2009-09-09');
-
-  t.same(track.coverArtDate, new Date('2009-09-09'),
-    `coverArtDate #3: is own value`);
-
-  track.coverArtistContribs = [];
-
-  t.equal(track.coverArtDate, null,
-    `coverArtDate #4: is null if track coverArtistContribs empty`);
-
-  album.trackCoverArtistContribs = contribs;
-
-  XXX_decacheWikiData();
-
-  t.same(track.coverArtDate, new Date('2009-09-09'),
-    `coverArtDate #5: is not null if album trackCoverArtistContribs specified`);
-
-  album.trackCoverArtistContribs = badContribs;
-
-  XXX_decacheWikiData();
-
-  t.equal(track.coverArtDate, null,
-    `coverArtDate #6: is null if album trackCoverArtistContribs resolves empty`);
-
-  track.coverArtistContribs = badContribs;
-
-  t.equal(track.coverArtDate, null,
-    `coverArtDate #7: is null if track coverArtistContribs resolves empty`);
-
-  track.coverArtistContribs = contribs;
-  track.disableUniqueCoverArt = true;
-
-  t.equal(track.coverArtDate, null,
-    `coverArtDate #8: is null if track disables unique cover artwork`);
-});
-
-t.test(`Track.coverArtFileExtension`, t => {
-  t.plan(8);
-
-  const {track, album} = stubTrackAndAlbum();
-  const {artist, contribs} = stubArtistAndContribs();
-
-  const {XXX_decacheWikiData} = linkAndBindWikiData({
-    albumData: [album],
-    artistData: [artist],
-    trackData: [track],
-  });
-
-  t.equal(track.coverArtFileExtension, null,
-    `coverArtFileExtension #1: defaults to null`);
-
-  track.coverArtistContribs = contribs;
-
-  t.equal(track.coverArtFileExtension, 'jpg',
-    `coverArtFileExtension #2: is jpg if has cover art and not further specified`);
-
-  track.coverArtistContribs = [];
-
-  album.coverArtistContribs = contribs;
-  XXX_decacheWikiData();
-
-  t.equal(track.coverArtFileExtension, null,
-    `coverArtFileExtension #3: only has value for unique cover art`);
-
-  track.coverArtistContribs = contribs;
-
-  album.trackCoverArtFileExtension = 'png';
-  XXX_decacheWikiData();
-
-  t.equal(track.coverArtFileExtension, 'png',
-    `coverArtFileExtension #4: inherits album trackCoverArtFileExtension (1/2)`);
-
-  track.coverArtFileExtension = 'gif';
-
-  t.equal(track.coverArtFileExtension, 'gif',
-    `coverArtFileExtension #5: is own value (1/2)`);
-
-  track.coverArtistContribs = [];
-
-  album.trackCoverArtistContribs = contribs;
-  XXX_decacheWikiData();
-
-  t.equal(track.coverArtFileExtension, 'gif',
-    `coverArtFileExtension #6: is own value (2/2)`);
-
-  track.coverArtFileExtension = null;
-
-  t.equal(track.coverArtFileExtension, 'png',
-    `coverArtFileExtension #7: inherits album trackCoverArtFileExtension (2/2)`);
-
-  track.disableUniqueCoverArt = true;
-
-  t.equal(track.coverArtFileExtension, null,
-    `coverArtFileExtension #8: is null if track disables unique cover art`);
-});
-
-t.test(`Track.date`, t => {
-  t.plan(3);
-
-  const {track, album} = stubTrackAndAlbum();
-
-  const {XXX_decacheWikiData} = linkAndBindWikiData({
-    albumData: [album],
-    trackData: [track],
-  });
-
-  t.equal(track.date, null,
-    `date #1: defaults to null`);
-
-  album.date = new Date('2012-12-12');
-  XXX_decacheWikiData();
-
-  t.same(track.date, album.date,
-    `date #2: inherits from album`);
-
-  track.dateFirstReleased = new Date('2009-09-09');
-
-  t.same(track.date, new Date('2009-09-09'),
-    `date #3: is own dateFirstReleased`);
-});
-
-t.test(`Track.featuredInFlashes`, t => {
-  t.plan(2);
-
-  const {track, album} = stubTrackAndAlbum('track1');
-
-  const {flash: flash1, flashAct: flashAct1} = stubFlashAndAct('flash1');
-  const {flash: flash2, flashAct: flashAct2} = stubFlashAndAct('flash2');
-
-  const {XXX_decacheWikiData} = linkAndBindWikiData({
-    albumData: [album],
-    trackData: [track],
-    flashData: [flash1, flash2],
-    flashActData: [flashAct1, flashAct2],
-  });
-
-  t.same(track.featuredInFlashes, [],
-    `featuredInFlashes #1: defaults to empty array`);
-
-  flash1.featuredTracks = ['track:track1'];
-  flash2.featuredTracks = ['track:track1'];
-  XXX_decacheWikiData();
-
-  t.same(track.featuredInFlashes, [flash1, flash2],
-    `featuredInFlashes #2: matches flashes' featuredTracks`);
-});
-
-t.test(`Track.hasUniqueCoverArt`, t => {
-  t.plan(7);
-
-  const {track, album} = stubTrackAndAlbum();
-  const {artist, contribs, badContribs} = stubArtistAndContribs();
-
-  const {XXX_decacheWikiData} = linkAndBindWikiData({
-    albumData: [album],
-    artistData: [artist],
-    trackData: [track],
-  });
-
-  t.equal(track.hasUniqueCoverArt, false,
-    `hasUniqueCoverArt #1: defaults to false`);
-
-  album.trackCoverArtistContribs = contribs;
-  XXX_decacheWikiData();
-
-  t.equal(track.hasUniqueCoverArt, true,
-    `hasUniqueCoverArt #2: is true if album specifies trackCoverArtistContribs`);
-
-  track.disableUniqueCoverArt = true;
-
-  t.equal(track.hasUniqueCoverArt, false,
-    `hasUniqueCoverArt #3: is false if disableUniqueCoverArt is true (1/2)`);
-
-  track.disableUniqueCoverArt = false;
-
-  album.trackCoverArtistContribs = badContribs;
-  XXX_decacheWikiData();
-
-  t.equal(track.hasUniqueCoverArt, false,
-    `hasUniqueCoverArt #4: is false if album's trackCoverArtistContribs resolve empty`);
-
-  track.coverArtistContribs = contribs;
-
-  t.equal(track.hasUniqueCoverArt, true,
-    `hasUniqueCoverArt #5: is true if track specifies coverArtistContribs`);
-
-  track.disableUniqueCoverArt = true;
-
-  t.equal(track.hasUniqueCoverArt, false,
-    `hasUniqueCoverArt #6: is false if disableUniqueCoverArt is true (2/2)`);
-
-  track.disableUniqueCoverArt = false;
-
-  track.coverArtistContribs = badContribs;
-
-  t.equal(track.hasUniqueCoverArt, false,
-    `hasUniqueCoverArt #7: is false if track's coverArtistContribs resolve empty`);
-});
-
-t.test(`Track.originalReleaseTrack`, t => {
-  t.plan(3);
-
-  const {track: track1, album: album1} = stubTrackAndAlbum('track1');
-  const {track: track2, album: album2} = stubTrackAndAlbum('track2');
-
-  const {wikiData, linkWikiDataArrays, XXX_decacheWikiData} = linkAndBindWikiData({
-    albumData: [album1, album2],
-    trackData: [track1, track2],
-  });
-
-  t.equal(track2.originalReleaseTrack, null,
-    `originalReleaseTrack #1: defaults to null`);
-
-  track2.originalReleaseTrack = 'track:track1';
-
-  t.equal(track2.originalReleaseTrack, track1,
-    `originalReleaseTrack #2: is resolved from own value`);
-
-  track2.trackData = [];
-
-  t.equal(track2.originalReleaseTrack, null,
-    `originalReleaseTrack #3: is null when track missing trackData`);
-});
-
-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, linkWikiDataArrays, XXX_decacheWikiData} = linkAndBindWikiData({
-    albumData: [album1, album2, album3, album4],
-    trackData: [track1, track2, track3, track4],
-  });
-
-  t.same(track1.otherReleases, [],
-    `otherReleases #1: defaults to empty array`);
-
-  track2.originalReleaseTrack = 'track:track1';
-  track3.originalReleaseTrack = 'track:track1';
-  track4.originalReleaseTrack = 'track:track1';
-  XXX_decacheWikiData();
-
-  t.same(track1.otherReleases, [track2, track3, track4],
-    `otherReleases #2: otherReleases of original release are its rereleases`);
-
-  wikiData.trackData = [track1, track3, track2, track4];
-  linkWikiDataArrays();
-
-  t.same(track1.otherReleases, [track3, track2, track4],
-    `otherReleases #3: otherReleases matches trackData order`);
-
-  wikiData.trackData = [track3, track2, track1, track4];
-  linkWikiDataArrays();
-
-  t.same(track2.otherReleases, [track1, track3, track4],
-    `otherReleases #4: otherReleases of rerelease are original track then other rereleases (1/3)`);
-
-  t.same(track3.otherReleases, [track1, track2, track4],
-    `otherReleases #5: otherReleases of rerelease are original track then other rereleases (2/3)`);
-
-  t.same(track4.otherReleases, [track1, track3, track2],
-    `otherReleases #6: otherReleases of rerelease are original track then other rereleases (3/3)`);
-});
-
-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 {XXX_decacheWikiData} = linkAndBindWikiData({
-    albumData: [album1, album2, album3, album4],
-    trackData: [track1, track2, track3, track4],
-  });
-
-  t.same(track1.referencedByTracks, [],
-    `referencedByTracks #1: defaults to empty array`);
-
-  track2.referencedTracks = ['track:track1'];
-  track3.referencedTracks = ['track:track1'];
-  XXX_decacheWikiData();
-
-  t.same(track1.referencedByTracks, [track2, track3],
-    `referencedByTracks #2: matches tracks' referencedTracks`);
-
-  track4.sampledTracks = ['track:track1'];
-  XXX_decacheWikiData();
-
-  t.same(track1.referencedByTracks, [track2, track3],
-    `referencedByTracks #3: doesn't match tracks' sampledTracks`);
-
-  track3.originalReleaseTrack = 'track:track2';
-  XXX_decacheWikiData();
-
-  t.same(track1.referencedByTracks, [track2],
-    `referencedByTracks #4: doesn't include rereleases`);
-});
-
-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 {XXX_decacheWikiData} = linkAndBindWikiData({
-    albumData: [album1, album2, album3, album4],
-    trackData: [track1, track2, track3, track4],
-  });
-
-  t.same(track1.sampledByTracks, [],
-    `sampledByTracks #1: defaults to empty array`);
-
-  track2.sampledTracks = ['track:track1'];
-  track3.sampledTracks = ['track:track1'];
-  XXX_decacheWikiData();
-
-  t.same(track1.sampledByTracks, [track2, track3],
-    `sampledByTracks #2: matches tracks' sampledTracks`);
-
-  track4.referencedTracks = ['track:track1'];
-  XXX_decacheWikiData();
-
-  t.same(track1.sampledByTracks, [track2, track3],
-    `sampledByTracks #3: doesn't match tracks' referencedTracks`);
-
-  track3.originalReleaseTrack = 'track:track2';
-  XXX_decacheWikiData();
-
-  t.same(track1.sampledByTracks, [track2],
-    `sampledByTracks #4: doesn't include rereleases`);
-});
diff --git a/test/unit/data/things/validators.js b/test/unit/data/validators.js
index 3a217d6f..02f94866 100644
--- a/test/unit/data/things/validators.js
+++ b/test/unit/data/validators.js
@@ -339,7 +339,7 @@ t.test('isName', t => {
   t.plan(4);
   t.ok(isName('Dogz 2.0'));
   t.ok(isName('album:this-track-is-only-named-thusly-to-give-niklink-a-headache'));
-  t.throws(() => isName(''));
+  t.ok(() => isName(''));
   t.throws(() => isName(612));
 });
 
@@ -374,8 +374,8 @@ test(t, 'validateReference', t => {
 
   t.ok(typeless('Hopes and Dreams'));
   t.ok(typeless('track:snowdin-town'));
+  t.ok(typeless('album:undertale-soundtrack'));
   t.throws(() => typeless(''), TypeError);
-  t.throws(() => typeless('album:undertale-soundtrack'));
 });
 
 test(t, 'validateReferenceList', t => {
diff --git a/test/unit/util/html.js b/test/unit/util/html.js
index 1652aee2..126e36ff 100644
--- a/test/unit/util/html.js
+++ b/test/unit/util/html.js
@@ -545,7 +545,7 @@ t.test(`html.template`, t => {
         slots.slot1,
         slots.slot2,
         slots.slot3,
-        `(length: ${slots.slot4.length})`,
+        `(is null: ${slots.slot4 === null})`,
       ].join(' '));
     },
   });
@@ -557,7 +557,7 @@ t.test(`html.template`, t => {
     slot4: '',
   });
 
-  t.equal(template3.toString(), `<span>123 0 false (length: 0)</span>`);
+  t.equal(template3.toString(), `<span>123 0 false (is null: true)</span>`);
 });
 
 t.test(`Template - description errors`, t => {