« get me outta code hell

test: general refactor instantiating things & managing wikiData - hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
path: root/test/unit/data/things
diff options
context:
space:
mode:
author(quasar) nebula <qznebula@protonmail.com>2024-08-05 20:39:44 -0300
committer(quasar) nebula <qznebula@protonmail.com>2024-08-05 20:42:09 -0300
commit91469b51fe10fbedaece83dafb07e1fd6730e11c (patch)
tree22a0b2e82f891ce4f0f4c5309be2166190aabe2e /test/unit/data/things
parent4cb5d1d0c231afad0175b2824320d0612f0eceef (diff)
test: general refactor instantiating things & managing wikiData
Diffstat (limited to 'test/unit/data/things')
-rw-r--r--test/unit/data/things/album.js211
-rw-r--r--test/unit/data/things/art-tag.js59
-rw-r--r--test/unit/data/things/flash.js39
-rw-r--r--test/unit/data/things/track.js351
-rw-r--r--test/unit/data/things/validators.js440
5 files changed, 246 insertions, 854 deletions
diff --git a/test/unit/data/things/album.js b/test/unit/data/things/album.js
index 8be9d9ee..a64488f7 100644
--- a/test/unit/data/things/album.js
+++ b/test/unit/data/things/album.js
@@ -1,61 +1,27 @@
 import t from 'tap';
 
-import {linkAndBindWikiData} from '#test-lib';
 import thingConstructors from '#things';
 
-const {
-  Album,
-  ArtTag,
-  Artist,
-  Track,
-  TrackSection,
-} = thingConstructors;
-
-function stubArtTag(tagName = `Test Art Tag`) {
-  const tag = new ArtTag();
-  tag.name = tagName;
-
-  return tag;
-}
-
-function stubArtistAndContribs() {
-  const artist = new Artist();
-  artist.name = `Test Artist`;
-
-  const contribs = [{artist: `Test Artist`, annotation: null}];
-  const badContribs = [{artist: `Figment of Your Imagination`, annotation: null}];
-
-  return {artist, contribs, badContribs};
-}
-
-function stubTrack(directory = 'foo') {
-  const track = new Track();
-  track.directory = directory;
-
-  return track;
-}
-
-function stubTrackSection(album, tracks, directory = 'baz') {
-  const trackSection = new TrackSection();
-  trackSection.unqualifiedDirectory = directory;
-  trackSection.tracks = tracks;
-  trackSection.albumData = [album];
-  return trackSection;
-}
+import {
+  linkAndBindWikiData,
+  stubArtistAndContribs,
+  stubThing,
+  stubWikiData,
+} from '#test-lib';
 
 t.test(`Album.artTags`, t => {
+  const {Album, ArtTag} = thingConstructors;
+
   t.plan(3);
 
-  const {artist, contribs} = stubArtistAndContribs();
-  const album = new Album();
-  const tag1 = stubArtTag(`Tag 1`);
-  const tag2 = stubArtTag(`Tag 2`);
+  const wikiData = stubWikiData();
 
-  const {XXX_decacheWikiData} = linkAndBindWikiData({
-    albumData: [album],
-    artistData: [artist],
-    artTagData: [tag1, tag2],
-  });
+  const {contribs} = stubArtistAndContribs(wikiData);
+  const album = stubThing(wikiData, Album);
+  const tag1 = stubThing(wikiData, ArtTag, {name: `Tag 1`});
+  const tag2 = stubThing(wikiData, ArtTag, {name: `Tag 2`});
+
+  linkAndBindWikiData(wikiData);
 
   t.same(album.artTags, [],
     `artTags #1: defaults to empty array`);
@@ -72,15 +38,16 @@ t.test(`Album.artTags`, t => {
 });
 
 t.test(`Album.bannerDimensions`, t => {
+  const {Album} = thingConstructors;
+
   t.plan(4);
 
-  const album = new Album();
-  const {artist, contribs, badContribs} = stubArtistAndContribs();
+  const wikiData = stubWikiData();
 
-  linkAndBindWikiData({
-    albumData: [album],
-    artistData: [artist],
-  });
+  const album = stubThing(wikiData, Album);
+  const {contribs, badContribs} = stubArtistAndContribs(wikiData);
+
+  linkAndBindWikiData(wikiData);
 
   t.equal(album.bannerDimensions, null,
     `Album.bannerDimensions #1: defaults to null`);
@@ -102,15 +69,16 @@ t.test(`Album.bannerDimensions`, t => {
 });
 
 t.test(`Album.bannerFileExtension`, t => {
+  const {Album} = thingConstructors;
+
   t.plan(5);
 
-  const album = new Album();
-  const {artist, contribs, badContribs} = stubArtistAndContribs();
+  const wikiData = stubWikiData();
 
-  linkAndBindWikiData({
-    albumData: [album],
-    artistData: [artist],
-  });
+  const album = stubThing(wikiData, Album);
+  const {contribs, badContribs} = stubArtistAndContribs(wikiData);
+
+  linkAndBindWikiData(wikiData);
 
   t.equal(album.bannerFileExtension, null,
     `Album.bannerFileExtension #1: defaults to null`);
@@ -137,15 +105,16 @@ t.test(`Album.bannerFileExtension`, t => {
 });
 
 t.test(`Album.bannerStyle`, t => {
+  const {Album} = thingConstructors;
+
   t.plan(4);
 
-  const album = new Album();
-  const {artist, contribs, badContribs} = stubArtistAndContribs();
+  const wikiData = stubWikiData();
 
-  linkAndBindWikiData({
-    albumData: [album],
-    artistData: [artist],
-  });
+  const album = stubThing(wikiData, Album);
+  const {contribs, badContribs} = stubArtistAndContribs(wikiData);
+
+  linkAndBindWikiData(wikiData);
 
   t.equal(album.bannerStyle, null,
     `Album.bannerStyle #1: defaults to null`);
@@ -167,15 +136,16 @@ t.test(`Album.bannerStyle`, t => {
 });
 
 t.test(`Album.coverArtDate`, t => {
+  const {Album} = thingConstructors;
+
   t.plan(6);
 
-  const album = new Album();
-  const {artist, contribs, badContribs} = stubArtistAndContribs();
+  const wikiData = stubWikiData();
 
-  linkAndBindWikiData({
-    albumData: [album],
-    artistData: [artist],
-  });
+  const album = stubThing(wikiData, Album);
+  const {contribs, badContribs} = stubArtistAndContribs(wikiData);
+
+  linkAndBindWikiData(wikiData);
 
   t.equal(album.coverArtDate, null,
     `Album.coverArtDate #1: defaults to null`);
@@ -207,15 +177,16 @@ t.test(`Album.coverArtDate`, t => {
 });
 
 t.test(`Album.coverArtFileExtension`, t => {
+  const {Album} = thingConstructors;
+
   t.plan(5);
 
-  const album = new Album();
-  const {artist, contribs, badContribs} = stubArtistAndContribs();
+  const wikiData = stubWikiData();
+
+  const album = stubThing(wikiData, Album);
+  const {contribs, badContribs} = stubArtistAndContribs(wikiData);
 
-  linkAndBindWikiData({
-    albumData: [album],
-    artistData: [artist],
-  });
+  linkAndBindWikiData(wikiData);
 
   t.equal(album.coverArtFileExtension, null,
     `Album.coverArtFileExtension #1: is null if coverArtistContribs empty (1/2)`);
@@ -243,24 +214,30 @@ t.test(`Album.coverArtFileExtension`, t => {
 });
 
 t.test(`Album.tracks`, t => {
+  const {Album, Track, TrackSection} = thingConstructors;
+
   t.plan(4);
 
-  const album = new Album();
+  let wikiData = stubWikiData();
+
+  const album = stubThing(wikiData, Album);
   album.directory = 'foo';
 
-  const track1 = stubTrack('track1');
-  const track2 = stubTrack('track2');
-  const track3 = stubTrack('track3');
+  const track1 = stubThing(wikiData, Track, {directory: 'track1'});
+  const track2 = stubThing(wikiData, Track, {directory: 'track2'});
+  const track3 = stubThing(wikiData, Track, {directory: 'track3'});
   const tracks = [track1, track2, track3];
 
-  const section1 = stubTrackSection(album, [], 'section1');
-  const section2 = stubTrackSection(album, [], 'section2');
-  const section3 = stubTrackSection(album, [], 'section3');
-  const section4 = stubTrackSection(album, [], 'section4');
-  const section5 = stubTrackSection(album, [], 'section5');
-  const section6 = stubTrackSection(album, [], 'section6');
+  const section1 = stubThing(wikiData, TrackSection, {unqualifiedDirectory: 'section1'});
+  const section2 = stubThing(wikiData, TrackSection, {unqualifiedDirectory: 'section2'});
+  const section3 = stubThing(wikiData, TrackSection, {unqualifiedDirectory: 'section3'});
+  const section4 = stubThing(wikiData, TrackSection, {unqualifiedDirectory: 'section4'});
+  const section5 = stubThing(wikiData, TrackSection, {unqualifiedDirectory: 'section5'});
+  const section6 = stubThing(wikiData, TrackSection, {unqualifiedDirectory: 'section6'});
   const sections = [section1, section2, section3, section4, section5, section6];
 
+  wikiData = null;
+
   for (const track of tracks) {
     track.albumData = [album];
   }
@@ -301,27 +278,37 @@ t.test(`Album.tracks`, t => {
 });
 
 t.test(`Album.trackSections`, t => {
+  const {Album, Track, TrackSection} = thingConstructors;
+
   t.plan(7);
 
-  const album = new Album();
+  let wikiData = stubWikiData();
 
-  const track1 = stubTrack('track1');
-  const track2 = stubTrack('track2');
-  const track3 = stubTrack('track3');
-  const track4 = stubTrack('track4');
+  const album = stubThing(wikiData, Album);
+
+  const track1 = stubThing(wikiData, Track, {directory: 'track1'});
+  const track2 = stubThing(wikiData, Track, {directory: 'track2'});
+  const track3 = stubThing(wikiData, Track, {directory: 'track3'});
+  const track4 = stubThing(wikiData, Track, {directory: 'track4'});
   const tracks = [track1, track2, track3, track4];
 
-  const section1 = stubTrackSection(album, [], 'section1');
-  const section2 = stubTrackSection(album, [], 'section2');
-  const section3 = stubTrackSection(album, [], 'section3');
-  const section4 = stubTrackSection(album, [], 'section4');
-  const section5 = stubTrackSection(album, [], 'section5');
+  const section1 = stubThing(wikiData, TrackSection, {unqualifiedDirectory: 'section1'});
+  const section2 = stubThing(wikiData, TrackSection, {unqualifiedDirectory: 'section2'});
+  const section3 = stubThing(wikiData, TrackSection, {unqualifiedDirectory: 'section3'});
+  const section4 = stubThing(wikiData, TrackSection, {unqualifiedDirectory: 'section4'});
+  const section5 = stubThing(wikiData, TrackSection, {unqualifiedDirectory: 'section5'});
   const sections = [section1, section2, section3, section4, section5];
 
+  wikiData = null;
+
   for (const track of tracks) {
     track.albumData = [album];
   }
 
+  for (const section of sections) {
+    section.albumData = [album];
+  }
+
   section1.tracks = [track1, track2];
   section2.tracks = [track3, track4];
 
@@ -415,15 +402,16 @@ t.test(`Album.trackSections`, t => {
 });
 
 t.test(`Album.wallpaperFileExtension`, t => {
+  const {Album} = thingConstructors;
+
   t.plan(5);
 
-  const album = new Album();
-  const {artist, contribs, badContribs} = stubArtistAndContribs();
+  const wikiData = stubWikiData();
+
+  const album = stubThing(wikiData, Album);
+  const {contribs, badContribs} = stubArtistAndContribs(wikiData);
 
-  linkAndBindWikiData({
-    albumData: [album],
-    artistData: [artist],
-  });
+  linkAndBindWikiData(wikiData);
 
   t.equal(album.wallpaperFileExtension, null,
     `Album.wallpaperFileExtension #1: defaults to null`);
@@ -450,15 +438,16 @@ t.test(`Album.wallpaperFileExtension`, t => {
 });
 
 t.test(`Album.wallpaperStyle`, t => {
+  const {Album} = thingConstructors;
+
   t.plan(4);
 
-  const album = new Album();
-  const {artist, contribs, badContribs} = stubArtistAndContribs();
+  const wikiData = stubWikiData();
+
+  const album = stubThing(wikiData, Album);
+  const {contribs, badContribs} = stubArtistAndContribs(wikiData);
 
-  linkAndBindWikiData({
-    albumData: [album],
-    artistData: [artist],
-  });
+  linkAndBindWikiData(wikiData);
 
   t.equal(album.wallpaperStyle, null,
     `Album.wallpaperStyle #1: defaults to null`);
diff --git a/test/unit/data/things/art-tag.js b/test/unit/data/things/art-tag.js
index e4879326..015acd0e 100644
--- a/test/unit/data/things/art-tag.js
+++ b/test/unit/data/things/art-tag.js
@@ -1,65 +1,10 @@
 import t from 'tap';
 
-import {linkAndBindWikiData} from '#test-lib';
 import thingConstructors from '#things';
 
-const {
-  Album,
-  Artist,
-  ArtTag,
-  Track,
-  trackSection,
-} = thingConstructors;
-
-function stubAlbum(tracks, directory = 'bar') {
-  const album = new Album();
-  album.directory = directory;
-
-  const trackSection = stubTrackSection(album, tracks);
-  album.trackSections = [`unqualified-track-section:${trackSection.unqualifiedDirectory}`];
-  album.ownTrackSectionData = [trackSection];
-
-  return album;
-}
-
-function stubTrackSection(album, tracks, directory = 'baz') {
-  const trackSection = new TrackSection();
-  trackSection.unqualifiedDirectory = directory;
-  trackSection.tracks = tracks;
-  trackSection.albumData = [album];
-  return trackSection;
-}
-
-function stubTrack(directory = 'foo') {
-  const track = new Track();
-  track.directory = directory;
-
-  return track;
-}
-
-function stubTrackAndAlbum(trackDirectory = 'foo', albumDirectory = 'bar') {
-  const track = stubTrack(trackDirectory);
-  const album = stubAlbum([track], albumDirectory);
-
-  return {track, album};
-}
-
-function stubArtist(artistName = `Test Artist`) {
-  const artist = new Artist();
-  artist.name = artistName;
-
-  return artist;
-}
-
-function stubArtistAndContribs(artistName = `Test Artist`) {
-  const artist = stubArtist(artistName);
-  const contribs = [{artist: artistName, annotation: null}];
-  const badContribs = [{artist: `Figment of Your Imagination`, annotation: null}];
-
-  return {artist, contribs, badContribs};
-}
-
 t.test(`ArtTag.nameShort`, t => {
+  const {ArtTag} = thingConstructors;
+
   t.plan(3);
 
   const artTag = new ArtTag();
diff --git a/test/unit/data/things/flash.js b/test/unit/data/things/flash.js
index 62059604..de39e80d 100644
--- a/test/unit/data/things/flash.js
+++ b/test/unit/data/things/flash.js
@@ -1,39 +1,24 @@
 import t from 'tap';
 
-import {linkAndBindWikiData} from '#test-lib';
 import thingConstructors from '#things';
 
-const {
-  Flash,
-  FlashAct,
-  Thing,
-} = thingConstructors;
-
-function stubFlash(directory = 'foo') {
-  const flash = new Flash();
-  flash.directory = directory;
-
-  return flash;
-}
-
-function stubFlashAct(flashes, directory = 'bar') {
-  const flashAct = new FlashAct();
-  flashAct.directory = directory;
-  flashAct.flashes = flashes.map(flash => Thing.getReference(flash));
-
-  return flashAct;
-}
+import {
+  linkAndBindWikiData,
+  stubThing,
+  stubWikiData,
+} from '#test-lib';
 
 t.test(`Flash.color`, t => {
+  const {Flash, FlashAct} = thingConstructors;
+
   t.plan(4);
 
-  const flash = stubFlash();
-  const flashAct = stubFlashAct([flash]);
+  const wikiData = stubWikiData();
+
+  const flash = stubThing(wikiData, Flash, {directory: 'my-flash'});
+  const flashAct = stubThing(wikiData, FlashAct, {flashes: ['flash:my-flash']});
 
-  const {XXX_decacheWikiData} = linkAndBindWikiData({
-    flashData: [flash],
-    flashActData: [flashAct],
-  });
+  const {XXX_decacheWikiData} = linkAndBindWikiData(wikiData);
 
   t.equal(flash.color, null,
     `color #1: defaults to null`);
diff --git a/test/unit/data/things/track.js b/test/unit/data/things/track.js
index 3fe4b4de..983a5537 100644
--- a/test/unit/data/things/track.js
+++ b/test/unit/data/things/track.js
@@ -1,84 +1,19 @@
 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;
-  trackSection.albumData = [album];
-  return trackSection;
-}
-
-function stubTrack(directory = 'foo') {
-  const track = new Track();
-  track.directory = directory;
-
-  return track;
-}
-
-function stubTrackAndAlbum(trackDirectory = 'foo', albumDirectory = 'bar') {
-  const track = stubTrack(trackDirectory);
-  const album = stubAlbum([track], albumDirectory);
-
-  return {track, album};
-}
-
-function stubArtist(artistName = `Test Artist`) {
-  const artist = new Artist();
-  artist.name = artistName;
-
-  return artist;
-}
-
-function stubArtistAndContribs(artistName = `Test Artist`) {
-  const artist = stubArtist(artistName);
-  const contribs = [{artist: artistName, annotation: null}];
-  const badContribs = [{artist: `Figment of Your Imagination`, annotation: null}];
-
-  return {artist, contribs, badContribs};
-}
-
-function stubArtTag(tagName = `Test Art Tag`) {
-  const tag = new ArtTag();
-  tag.name = tagName;
-
-  return tag;
-}
-
-function stubFlashAndAct(directory = 'zam') {
-  const flash = new Flash();
-  flash.directory = directory;
-
-  const flashAct = new FlashAct();
-  flashAct.flashes = [Thing.getReference(flash)];
-
-  return {flash, flashAct};
-}
+import {
+  linkAndBindWikiData,
+  stubArtistAndContribs,
+  stubFlashAndAct,
+  stubThing,
+  stubTrackAndAlbum,
+  stubWikiData,
+} from '#test-lib';
 
 t.test(`Track.album`, t => {
+  const {Album, Track, TrackSection} = thingConstructors;
+
   t.plan(6);
 
   // Note: These asserts use manual albumData/trackData relationships
@@ -86,14 +21,16 @@ t.test(`Track.album`, t => {
   // be relevant for this case. Other properties use the same underlying
   // get-album behavior as Track.album so aren't tested as aggressively.
 
-  const track1 = stubTrack('track1');
-  const track2 = stubTrack('track2');
-  const album1 = new Album();
-  const album2 = new Album();
-  const section1 = new TrackSection();
-  const section2 = new TrackSection();
-  section1.unqualifiedDirectory = 'section1';
-  section2.unqualifiedDirectory = 'section2';
+  let wikiData = stubWikiData();
+
+  const track1 = stubThing(wikiData, Track, {directory: 'track1'});
+  const track2 = stubThing(wikiData, Track, {directory: 'track2'});
+  const album1 = stubThing(wikiData, Album);
+  const album2 = stubThing(wikiData, Album);
+  const section1 = stubThing(wikiData, TrackSection, {unqualifiedDirectory: 'section1'});
+  const section2 = stubThing(wikiData, TrackSection, {unqualifiedDirectory: 'section2'});
+
+  wikiData = null;
 
   t.equal(track1.album, null,
     `album #1: defaults to null`);
@@ -146,11 +83,13 @@ t.test(`Track.album`, t => {
 t.test(`Track.alwaysReferenceByDirectory`, t => {
   t.plan(7);
 
-  const {track: originalTrack, album: originalAlbum} =
-    stubTrackAndAlbum('original-track', 'original-album');
+  const wikiData = stubWikiData();
+
+  const {track: originalTrack} =
+    stubTrackAndAlbum(wikiData, 'original-track', 'original-album');
 
   const {track: rereleaseTrack, album: rereleaseAlbum} =
-    stubTrackAndAlbum('rerelease-track', 'rerelease-album');
+    stubTrackAndAlbum(wikiData, 'rerelease-track', 'rerelease-album');
 
   originalTrack.name = 'Cowabunga';
   rereleaseTrack.name = 'Cowabunga';
@@ -160,10 +99,7 @@ t.test(`Track.alwaysReferenceByDirectory`, t => {
 
   rereleaseTrack.originalReleaseTrack = 'track:original-track';
 
-  const {XXX_decacheWikiData} = linkAndBindWikiData({
-    albumData: [originalAlbum, rereleaseAlbum],
-    trackData: [originalTrack, rereleaseTrack],
-  });
+  const {XXX_decacheWikiData} = linkAndBindWikiData(wikiData);
 
   t.equal(originalTrack.alwaysReferenceByDirectory, false,
     `alwaysReferenceByDirectory #1: defaults to false`);
@@ -199,19 +135,22 @@ t.test(`Track.alwaysReferenceByDirectory`, t => {
 });
 
 t.test(`Track.artTags`, t => {
+  const {ArtTag} = thingConstructors;
+
   t.plan(6);
 
-  const {track, album} = stubTrackAndAlbum();
-  const {artist, contribs} = stubArtistAndContribs();
-  const tag1 = stubArtTag(`Tag 1`);
-  const tag2 = stubArtTag(`Tag 2`);
+  const wikiData = stubWikiData();
+
+  const {track, album} = stubTrackAndAlbum(wikiData);
+  const {contribs} = stubArtistAndContribs(wikiData);
+
+  const tag1 =
+    stubThing(wikiData, ArtTag, {name: `Tag 1`});
+
+  const tag2 =
+    stubThing(wikiData, ArtTag, {name: `Tag 2`});
 
-  const {XXX_decacheWikiData} = linkAndBindWikiData({
-    albumData: [album],
-    artistData: [artist],
-    artTagData: [tag1, tag2],
-    trackData: [track],
-  });
+  const {XXX_decacheWikiData} = linkAndBindWikiData(wikiData);
 
   t.same(track.artTags, [],
     `artTags #1: defaults to empty array`);
@@ -249,17 +188,28 @@ t.test(`Track.artTags`, t => {
 });
 
 t.test(`Track.artistContribs`, t => {
+  const {Album, Artist, Track, TrackSection} = thingConstructors;
+
   t.plan(4);
 
-  const {track, album} = stubTrackAndAlbum();
-  const artist1 = stubArtist(`Artist 1`);
-  const artist2 = stubArtist(`Artist 2`);
+  const wikiData = stubWikiData();
+
+  const track =
+    stubThing(wikiData, Track);
+
+  const section =
+    stubThing(wikiData, TrackSection, {tracks: [track]});
+
+  const album =
+    stubThing(wikiData, Album, {trackSections: [section]});
 
-  const {XXX_decacheWikiData} = linkAndBindWikiData({
-    albumData: [album],
-    artistData: [artist1, artist2],
-    trackData: [track],
-  });
+  const artist1 =
+    stubThing(wikiData, Artist, {name: `Artist 1`});
+
+  const artist2 =
+    stubThing(wikiData, Artist, {name: `Artist 2`});
+
+  const {XXX_decacheWikiData} = linkAndBindWikiData(wikiData);
 
   t.same(track.artistContribs, [],
     `artistContribs #1: defaults to empty array`);
@@ -294,20 +244,17 @@ t.test(`Track.artistContribs`, t => {
 });
 
 t.test(`Track.color`, t => {
-  t.plan(5);
+  t.plan(4);
+
+  const wikiData = stubWikiData();
 
-  const {track, album} = stubTrackAndAlbum();
+  const {track, album, section} = stubTrackAndAlbum(wikiData);
 
-  const {XXX_decacheWikiData} = linkAndBindWikiData({
-    albumData: [album],
-    trackData: [track],
-  });
+  const {XXX_decacheWikiData} = linkAndBindWikiData(wikiData);
 
   t.equal(track.color, null,
     `color #1: defaults to null`);
 
-  const section = stubTrackSection(album, [track], 'section');
-
   album.color = '#abcdef';
   section.color = '#beeeef';
 
@@ -318,48 +265,29 @@ t.test(`Track.color`, t => {
   t.equal(track.color, '#beeeef',
     `color #2: inherits from track section before album`);
 
-  // Replace the album with a completely fake one. This isn't realistic, since
-  // in correct data, Album.tracks depends on Albums.trackSections and so the
-  // track's album will always have a corresponding track section. But if that
-  // connection breaks for some future reason (with the album still present),
-  // Track.color should still inherit directly from the album.
-  track.albumData = [
-    {
-      constructor: {[Thing.referenceType]: 'album'},
-      [Thing.isThing]: true,
-      color: '#abcdef',
-      tracks: [track],
-      trackSections: [
-        {color: '#baaaad', tracks: []},
-      ],
-    },
-  ];
-
-  t.equal(track.color, '#abcdef',
-    `color #3: inherits from album without matching track section`);
-
   track.color = '#123456';
 
   t.equal(track.color, '#123456',
-    `color #4: is own value`);
+    `color #3: is own value`);
 
   t.throws(() => { track.color = '#aeiouw'; },
     {cause: TypeError},
-    `color #5: must be set to valid color`);
+    `color #4: must be set to valid color`);
 });
 
 t.test(`Track.commentatorArtists`, t => {
+  const {Artist, Track} = thingConstructors;
+
   t.plan(8);
 
-  const track = new Track();
-  const artist1 = stubArtist(`SnooPING`);
-  const artist2 = stubArtist(`ASUsual`);
-  const artist3 = stubArtist(`Icy`);
+  const wikiData = stubWikiData();
+
+  const track = stubThing(wikiData, Track);
+  const artist1 = stubThing(wikiData, Artist, {name: `SnooPING`});
+  const artist2 = stubThing(wikiData, Artist, {name: `ASUsual`});
+  const artist3 = stubThing(wikiData, Artist, {name: `Icy`});
 
-  linkAndBindWikiData({
-    trackData: [track],
-    artistData: [artist1, artist2, artist3],
-  });
+  linkAndBindWikiData(wikiData);
 
   // Keep track of the last commentary string in a separate value, since
   // the track.commentary property exposes as a completely different format
@@ -425,17 +353,17 @@ t.test(`Track.commentatorArtists`, t => {
 });
 
 t.test(`Track.coverArtistContribs`, t => {
+  const {Artist} = thingConstructors;
+
   t.plan(5);
 
-  const {track, album} = stubTrackAndAlbum();
-  const artist1 = stubArtist(`Artist 1`);
-  const artist2 = stubArtist(`Artist 2`);
+  const wikiData = stubWikiData();
 
-  const {XXX_decacheWikiData} = linkAndBindWikiData({
-    albumData: [album],
-    artistData: [artist1, artist2],
-    trackData: [track],
-  });
+  const {track, album} = stubTrackAndAlbum(wikiData);
+  const artist1 = stubThing(wikiData, Artist, {name: `Artist 1`});
+  const artist2 = stubThing(wikiData, Artist, {name: `Artist 2`});
+
+  const {XXX_decacheWikiData} = linkAndBindWikiData(wikiData);
 
   t.same(track.coverArtistContribs, [],
     `coverArtistContribs #1: defaults to empty array`);
@@ -477,14 +405,12 @@ t.test(`Track.coverArtistContribs`, t => {
 t.test(`Track.coverArtDate`, t => {
   t.plan(8);
 
-  const {track, album} = stubTrackAndAlbum();
-  const {artist, contribs, badContribs} = stubArtistAndContribs();
+  const wikiData = stubWikiData();
+
+  const {track, album} = stubTrackAndAlbum(wikiData);
+  const {contribs, badContribs} = stubArtistAndContribs(wikiData);
 
-  const {XXX_decacheWikiData} = linkAndBindWikiData({
-    albumData: [album],
-    artistData: [artist],
-    trackData: [track],
-  });
+  const {XXX_decacheWikiData} = linkAndBindWikiData(wikiData);
 
   track.coverArtistContribs = contribs;
 
@@ -537,14 +463,12 @@ t.test(`Track.coverArtDate`, t => {
 t.test(`Track.coverArtFileExtension`, t => {
   t.plan(8);
 
-  const {track, album} = stubTrackAndAlbum();
-  const {artist, contribs} = stubArtistAndContribs();
+  const wikiData = stubWikiData();
+
+  const {track, album} = stubTrackAndAlbum(wikiData);
+  const {contribs} = stubArtistAndContribs(wikiData);
 
-  const {XXX_decacheWikiData} = linkAndBindWikiData({
-    albumData: [album],
-    artistData: [artist],
-    trackData: [track],
-  });
+  const {XXX_decacheWikiData} = linkAndBindWikiData(wikiData);
 
   t.equal(track.coverArtFileExtension, null,
     `coverArtFileExtension #1: defaults to null`);
@@ -597,12 +521,11 @@ t.test(`Track.coverArtFileExtension`, t => {
 t.test(`Track.date`, t => {
   t.plan(3);
 
-  const {track, album} = stubTrackAndAlbum();
+  const wikiData = stubWikiData();
 
-  const {XXX_decacheWikiData} = linkAndBindWikiData({
-    albumData: [album],
-    trackData: [track],
-  });
+  const {track, album} = stubTrackAndAlbum(wikiData);
+
+  const {XXX_decacheWikiData} = linkAndBindWikiData(wikiData);
 
   t.equal(track.date, null,
     `date #1: defaults to null`);
@@ -622,17 +545,13 @@ t.test(`Track.date`, t => {
 t.test(`Track.featuredInFlashes`, t => {
   t.plan(2);
 
-  const {track, album} = stubTrackAndAlbum('track1');
+  const wikiData = stubWikiData();
 
-  const {flash: flash1, flashAct: flashAct1} = stubFlashAndAct('flash1');
-  const {flash: flash2, flashAct: flashAct2} = stubFlashAndAct('flash2');
+  const {track} = stubTrackAndAlbum(wikiData, 'track1');
+  const {flash: flash1} = stubFlashAndAct(wikiData, 'flash1');
+  const {flash: flash2} = stubFlashAndAct(wikiData, 'flash2');
 
-  const {XXX_decacheWikiData} = linkAndBindWikiData({
-    albumData: [album],
-    trackData: [track],
-    flashData: [flash1, flash2],
-    flashActData: [flashAct1, flashAct2],
-  });
+  const {XXX_decacheWikiData} = linkAndBindWikiData(wikiData);
 
   t.same(track.featuredInFlashes, [],
     `featuredInFlashes #1: defaults to empty array`);
@@ -648,14 +567,12 @@ t.test(`Track.featuredInFlashes`, t => {
 t.test(`Track.hasUniqueCoverArt`, t => {
   t.plan(7);
 
-  const {track, album} = stubTrackAndAlbum();
-  const {artist, contribs, badContribs} = stubArtistAndContribs();
+  const wikiData = stubWikiData();
+
+  const {track, album} = stubTrackAndAlbum(wikiData);
+  const {contribs, badContribs} = stubArtistAndContribs(wikiData);
 
-  const {XXX_decacheWikiData} = linkAndBindWikiData({
-    albumData: [album],
-    artistData: [artist],
-    trackData: [track],
-  });
+  const {XXX_decacheWikiData} = linkAndBindWikiData(wikiData);
 
   t.equal(track.hasUniqueCoverArt, false,
     `hasUniqueCoverArt #1: defaults to false`);
@@ -700,13 +617,12 @@ t.test(`Track.hasUniqueCoverArt`, t => {
 t.test(`Track.originalReleaseTrack`, t => {
   t.plan(3);
 
-  const {track: track1, album: album1} = stubTrackAndAlbum('track1');
-  const {track: track2, album: album2} = stubTrackAndAlbum('track2');
+  const wikiData = stubWikiData();
+
+  const {track: track1} = stubTrackAndAlbum(wikiData, 'track1');
+  const {track: track2} = stubTrackAndAlbum(wikiData, 'track2');
 
-  const {wikiData, linkWikiDataArrays, XXX_decacheWikiData} = linkAndBindWikiData({
-    albumData: [album1, album2],
-    trackData: [track1, track2],
-  });
+  linkAndBindWikiData(wikiData);
 
   t.equal(track2.originalReleaseTrack, null,
     `originalReleaseTrack #1: defaults to null`);
@@ -725,15 +641,14 @@ t.test(`Track.originalReleaseTrack`, t => {
 t.test(`Track.otherReleases`, t => {
   t.plan(6);
 
-  const {track: track1, album: album1} = stubTrackAndAlbum('track1');
-  const {track: track2, album: album2} = stubTrackAndAlbum('track2');
-  const {track: track3, album: album3} = stubTrackAndAlbum('track3');
-  const {track: track4, album: album4} = stubTrackAndAlbum('track4');
+  const wikiData = stubWikiData();
 
-  const {wikiData, linkWikiDataArrays, XXX_decacheWikiData} = linkAndBindWikiData({
-    albumData: [album1, album2, album3, album4],
-    trackData: [track1, track2, track3, track4],
-  });
+  const {track: track1} = stubTrackAndAlbum(wikiData, 'track1');
+  const {track: track2} = stubTrackAndAlbum(wikiData, 'track2');
+  const {track: track3} = stubTrackAndAlbum(wikiData, 'track3');
+  const {track: track4} = stubTrackAndAlbum(wikiData, 'track4');
+
+  const {linkWikiDataArrays, XXX_decacheWikiData} = linkAndBindWikiData(wikiData);
 
   t.same(track1.otherReleases, [],
     `otherReleases #1: defaults to empty array`);
@@ -768,15 +683,14 @@ t.test(`Track.otherReleases`, t => {
 t.test(`Track.referencedByTracks`, t => {
   t.plan(4);
 
-  const {track: track1, album: album1} = stubTrackAndAlbum('track1');
-  const {track: track2, album: album2} = stubTrackAndAlbum('track2');
-  const {track: track3, album: album3} = stubTrackAndAlbum('track3');
-  const {track: track4, album: album4} = stubTrackAndAlbum('track4');
+  const wikiData = stubWikiData();
+
+  const {track: track1} = stubTrackAndAlbum(wikiData, 'track1');
+  const {track: track2} = stubTrackAndAlbum(wikiData, 'track2');
+  const {track: track3} = stubTrackAndAlbum(wikiData, 'track3');
+  const {track: track4} = stubTrackAndAlbum(wikiData, 'track4');
 
-  const {XXX_decacheWikiData} = linkAndBindWikiData({
-    albumData: [album1, album2, album3, album4],
-    trackData: [track1, track2, track3, track4],
-  });
+  const {XXX_decacheWikiData} = linkAndBindWikiData(wikiData);
 
   t.same(track1.referencedByTracks, [],
     `referencedByTracks #1: defaults to empty array`);
@@ -804,15 +718,14 @@ t.test(`Track.referencedByTracks`, t => {
 t.test(`Track.sampledByTracks`, t => {
   t.plan(4);
 
-  const {track: track1, album: album1} = stubTrackAndAlbum('track1');
-  const {track: track2, album: album2} = stubTrackAndAlbum('track2');
-  const {track: track3, album: album3} = stubTrackAndAlbum('track3');
-  const {track: track4, album: album4} = stubTrackAndAlbum('track4');
+  const wikiData = stubWikiData();
+
+  const {track: track1} = stubTrackAndAlbum(wikiData, 'track1');
+  const {track: track2} = stubTrackAndAlbum(wikiData, 'track2');
+  const {track: track3} = stubTrackAndAlbum(wikiData, 'track3');
+  const {track: track4} = stubTrackAndAlbum(wikiData, 'track4');
 
-  const {XXX_decacheWikiData} = linkAndBindWikiData({
-    albumData: [album1, album2, album3, album4],
-    trackData: [track1, track2, track3, track4],
-  });
+  const {XXX_decacheWikiData} = linkAndBindWikiData(wikiData);
 
   t.same(track1.sampledByTracks, [],
     `sampledByTracks #1: defaults to empty array`);
diff --git a/test/unit/data/things/validators.js b/test/unit/data/things/validators.js
deleted file mode 100644
index 3a217d6f..00000000
--- a/test/unit/data/things/validators.js
+++ /dev/null
@@ -1,440 +0,0 @@
-import t from 'tap';
-import {showAggregate} from '#aggregate';
-
-import {
-  // Basic types
-  isBoolean,
-  isCountingNumber,
-  isDate,
-  isNumber,
-  isString,
-  isStringNonEmpty,
-
-  // Complex types
-  isArray,
-  isObject,
-  validateArrayItems,
-
-  // Wiki data
-  isColor,
-  isCommentary,
-  isContentString,
-  isContribution,
-  isContributionList,
-  isDimensions,
-  isDirectory,
-  isDuration,
-  isFileExtension,
-  isName,
-  isURL,
-  validateReference,
-  validateReferenceList,
-
-  // Compositional utilities
-  anyOf,
-} from '#validators';
-
-function test(t, msg, fn) {
-  t.test(msg, t => {
-    try {
-      fn(t);
-    } catch (error) {
-      if (error instanceof AggregateError) {
-        showAggregate(error);
-      }
-      throw error;
-    }
-  });
-}
-
-// Basic types
-
-test(t, 'isBoolean', t => {
-  t.plan(4);
-  t.ok(isBoolean(true));
-  t.ok(isBoolean(false));
-  t.throws(() => isBoolean(1), TypeError);
-  t.throws(() => isBoolean('yes'), TypeError);
-});
-
-test(t, 'isNumber', t => {
-  t.plan(6);
-  t.ok(isNumber(123));
-  t.ok(isNumber(0.05));
-  t.ok(isNumber(0));
-  t.ok(isNumber(-10));
-  t.throws(() => isNumber('413'), TypeError);
-  t.throws(() => isNumber(true), TypeError);
-});
-
-test(t, 'isCountingNumber', t => {
-  t.plan(6);
-  t.ok(isCountingNumber(3));
-  t.ok(isCountingNumber(1));
-  t.throws(() => isCountingNumber(1.75), TypeError);
-  t.throws(() => isCountingNumber(0), TypeError);
-  t.throws(() => isCountingNumber(-1), TypeError);
-  t.throws(() => isCountingNumber('612'), TypeError);
-});
-
-test(t, 'isString', t => {
-  t.plan(3);
-  t.ok(isString('hello!'));
-  t.ok(isString(''));
-  t.throws(() => isString(100), TypeError);
-});
-
-test(t, 'isStringNonEmpty', t => {
-  t.plan(4);
-  t.ok(isStringNonEmpty('hello!'));
-  t.throws(() => isStringNonEmpty(''), TypeError);
-  t.throws(() => isStringNonEmpty('     '), TypeError);
-  t.throws(() => isStringNonEmpty(100), TypeError);
-});
-
-// Complex types
-
-test(t, 'isArray', t => {
-  t.plan(3);
-  t.ok(isArray([]));
-  t.throws(() => isArray({}), TypeError);
-  t.throws(() => isArray('1, 2, 3'), TypeError);
-});
-
-test(t, 'isDate', t => {
-  t.plan(3);
-  t.ok(isDate(new Date('2023-03-27 09:24:15')));
-  t.throws(() => isDate(new Date(Infinity)), TypeError);
-  t.throws(() => isDimensions('2023-03-27 09:24:15'), TypeError);
-});
-
-test(t, 'isObject', t => {
-  t.plan(3);
-  t.ok(isObject({}));
-  t.ok(isObject([]));
-  t.throws(() => isObject(null), TypeError);
-});
-
-test(t, 'validateArrayItems', t => {
-  t.plan(9);
-
-  t.ok(validateArrayItems(isNumber)([3, 4, 5]));
-  t.ok(validateArrayItems(validateArrayItems(isNumber))([[3, 4], [4, 5], [6, 7]]));
-
-  let caughtError = null;
-  try {
-    validateArrayItems(isNumber)([10, 20, 'one hundred million consorts', 30]);
-  } catch (err) {
-    caughtError = err;
-  }
-
-  t.not(caughtError, null);
-  t.ok(caughtError instanceof AggregateError);
-  t.equal(caughtError.errors.length, 1);
-  t.ok(caughtError.errors[0] instanceof Error);
-  t.equal(caughtError.errors[0][Symbol.for('hsmusic.annotateError.indexInSourceArray')], 2);
-  t.not(caughtError.errors[0].cause, null);
-  t.ok(caughtError.errors[0].cause instanceof TypeError);
-});
-
-// Wiki data
-
-t.test('isColor', t => {
-  t.plan(9);
-  t.ok(isColor('#123'));
-  t.ok(isColor('#1234'));
-  t.ok(isColor('#112233'));
-  t.ok(isColor('#11223344'));
-  t.ok(isColor('#abcdef00'));
-  t.ok(isColor('#ABCDEF'));
-  t.throws(() => isColor('#ggg'), TypeError);
-  t.throws(() => isColor('red'), TypeError);
-  t.throws(() => isColor('hsl(150deg 30% 60%)'), TypeError);
-});
-
-t.test('isCommentary', t => {
-  t.plan(9);
-
-  // TODO: Test specific error messages.
-  t.ok(isCommentary(`<i>Toby Fox:</i>\ndogsong.mp3`));
-  t.ok(isCommentary(`<i>Toby Fox:</i> (music)\ndogsong.mp3`));
-  t.throws(() => isCommentary(`dogsong.mp3\n<i>Toby Fox:</i>\ndogsong.mp3`));
-  t.throws(() => isCommentary(`<i>Toby Fox:</i> dogsong.mp3`));
-  t.throws(() => isCommentary(`<i>Toby Fox:</i> (music) dogsong.mp3`));
-  t.throws(() => isCommentary(`<i>I Have Nothing To Say:</i>`));
-  t.throws(() => isCommentary(123));
-  t.throws(() => isCommentary(``));
-  t.throws(() => isCommentary(`Technically, ah, er:</i>\nCorrect`));
-});
-
-t.test('isContentString', t => {
-  t.plan(12);
-
-  t.ok(isContentString(`Hello, world!`));
-  t.ok(isContentString(`Hello...\nWorld!`));
-
-  const quickThrows = (string, description) =>
-    t.throws(() => isContentString(string), description);
-
-  quickThrows(
-    `Snooping\xa0as usual, I\xa0\xa0\xa0SEE.`,
-    Object.assign(
-      new AggregateError([
-        new AggregateError([
-          new TypeError(`Replace "\xa0" (non-breaking space) with " " (normal space) between "ing" and "as " (pos: 9)`),
-          new TypeError(`Replace "\xa0\xa0\xa0" (non-breaking space) with "   " (normal space) between ", I" and "SEE" (pos: 21)`),
-        ], `Illegal characters found in content string`),
-      ], `Errors validating content string`),
-      {[Symbol.for(`hsmusic.aggregate.translucent`)]: 'single'}));
-
-  quickThrows(
-    `Oh\u200bdear,\n` +
-    `Oh dear,\n` +
-    `oh-dear-oh-dear-oh\u200bdear.`,
-    new AggregateError([
-      new AggregateError([
-        new TypeError(`Delete "\u200b" (zero-width space) between "Oh" and "dea" (line: 1, col: 3)`),
-        new TypeError(`Delete "\u200b" (zero-width space) between "-oh" and "dea" (line: 3, col: 19)`),
-      ]),
-    ]));
-
-  quickThrows(
-    `Well the days start comin'\xa0\xa0\xa0\xa0\u200b\u200b\xa0\xa0\xa0\u200b\u200b\u200band they don't stop comin'`,
-    new AggregateError([
-      new AggregateError([
-        new TypeError(`Replace "\xa0\xa0\xa0\xa0" (non-breaking space) with "    " (normal space) after "in'" (pos: 27)`),
-        new TypeError(`Delete "\u200b\u200b" (zero-width space) (pos: 31)`),
-        new TypeError(`Replace "\xa0\xa0\xa0" (non-breaking space) with "   " (normal space) (pos: 33)`),
-        new TypeError(`Delete "\u200b\u200b\u200b" (zero-width space) before "and" (pos: 36)`),
-      ]),
-    ]));
-
-  quickThrows(
-    `It's go-\u200bin',\n` +
-    `\u200bIt's goin',\u200b\n` +
-    `\u200b\u200bIt's going!`,
-    new AggregateError([
-      new AggregateError([
-        new TypeError(`Delete "\u200b" (zero-width space) between "go-" and "in'" (line: 1, col: 9)`),
-        new TypeError(`Delete "\u200b" (zero-width space) before "It'" (line: 2, col: 1)`),
-        new TypeError(`Delete "\u200b" (zero-width space) after "n'," (line: 2, col: 13)`),
-        new TypeError(`Delete "\u200b\u200b" (zero-width space) before "It'" (line: 3, col: 1)`),
-      ]),
-    ]));
-
-  quickThrows(
-    `  Room at the start.`,
-    new AggregateError([
-      new AggregateError([
-        new TypeError(`Matched "  " at start`),
-      ], `Whitespace found at start or end`),
-    ]));
-
-  quickThrows(
-    `Room at the end.      `,
-    new AggregateError([
-      new AggregateError([
-        new TypeError(`Matched "      " at end`),
-      ], `Whitespace found at start or end`),
-    ]));
-
-  quickThrows(
-    `      Room on both sides. `,
-    new AggregateError([
-      new AggregateError([
-        new TypeError(`Matched "      " at start`),
-        new TypeError(`Matched " " at end`),
-      ], `Whitespace found at start or end`),
-    ]));
-
-  quickThrows(
-    `We're going multiline! \n` +
-    `That we are, aye.    \n` +
-    `      \n`,
-    `Yessir.`,
-    new AggregateError([
-      new AggregateError([
-        new TypeError(`Matched " " at end of line 1`),
-        new TypeError(`Matched "    " at end of line 2`),
-        new TypeError(`Matched "      " as all of line 3`),
-      ], `Whitespace found at end of line`),
-    ]));
-
-  t.doesNotThrow(() =>
-    isContentString(
-      `It's cool.\n` +
-      `  It's cool.\n` +
-      `    It's cool.\n` +
-      `      It's so cool.`));
-
-  t.doesNotThrow(() =>
-    isContentString(
-      `\n` +
-      `\n` +
-      `It's okay for\n` +
-      `blank lines\n` +
-      `\n` +
-      `just about anywhere.\n` +
-      ``));
-});
-
-t.test('isContribution', t => {
-  t.plan(4);
-  t.ok(isContribution({artist: 'artist:toby-fox', annotation: 'Music'}));
-  t.ok(isContribution({artist: 'Toby Fox'}));
-  t.throws(() => isContribution(({artist: 'group:umspaf', annotation: 'Organizing'})),
-    {errors: /artist/});
-  t.throws(() => isContribution(({artist: 'artist:toby-fox', annotation: 123})),
-    {errors: /annotation/});
-});
-
-t.test('isContributionList', t => {
-  t.plan(4);
-  t.ok(isContributionList([{artist: 'Beavis'}, {artist: 'Butthead', annotation: 'Wrangling'}]));
-  t.ok(isContributionList([]));
-  t.throws(() => isContributionList(2));
-  t.throws(() => isContributionList(['Charlie', 'Woodstock']));
-});
-
-test(t, 'isDimensions', t => {
-  t.plan(6);
-  t.ok(isDimensions([1, 1]));
-  t.ok(isDimensions([50, 50]));
-  t.ok(isDimensions([5000, 1]));
-  t.throws(() => isDimensions([1]), TypeError);
-  t.throws(() => isDimensions([413, 612, 1025]), TypeError);
-  t.throws(() => isDimensions('800x200'), TypeError);
-});
-
-test(t, 'isDirectory', t => {
-  t.plan(6);
-  t.ok(isDirectory('savior-of-the-waking-world'));
-  t.ok(isDirectory('MeGaLoVania'));
-  t.ok(isDirectory('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'));
-  t.throws(() => isDirectory(123), TypeError);
-  t.throws(() => isDirectory(''), TypeError);
-  t.throws(() => isDirectory('troll saint nicholas and the quest for the holy pail'), TypeError);
-});
-
-test(t, 'isDuration', t => {
-  t.plan(5);
-  t.ok(isDuration(60));
-  t.ok(isDuration(0.02));
-  t.ok(isDuration(0));
-  t.throws(() => isDuration(-1), TypeError);
-  t.throws(() => isDuration('10:25'), TypeError);
-});
-
-test(t, 'isFileExtension', t => {
-  t.plan(6);
-  t.ok(isFileExtension('png'));
-  t.ok(isFileExtension('jpg'));
-  t.ok(isFileExtension('sub_loc'));
-  t.throws(() => isFileExtension(''), TypeError);
-  t.throws(() => isFileExtension('.jpg'), TypeError);
-  t.throws(() => isFileExtension('just an image bro!!!!'), TypeError);
-});
-
-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.throws(() => isName(612));
-});
-
-t.test('isURL', t => {
-  t.plan(4);
-  t.ok(isURL(`https://hsmusic.wiki/foo/bar/hi?baz=25#hash`));
-  t.throws(() => isURL(`/the/dog/zone/`));
-  t.throws(() => isURL(25));
-  t.throws(() => isURL(new URL(`https://hsmusic.wiki/perfectly/reasonable/`)));
-});
-
-test(t, 'validateReference', t => {
-  t.plan(16);
-
-  const typeless = validateReference();
-  const track = validateReference('track');
-  const album = validateReference('album');
-
-  t.ok(track('track:doctor'));
-  t.ok(track('track:MeGaLoVania'));
-  t.ok(track('Showtime (Imp Strife Mix)'));
-  t.throws(() => track('track:troll saint nic'), TypeError);
-  t.throws(() => track('track:'), TypeError);
-  t.throws(() => track('album:homestuck-vol-1'), TypeError);
-
-  t.ok(album('album:sburb'));
-  t.ok(album('album:the-wanderers'));
-  t.ok(album('Homestuck Vol. 8'));
-  t.throws(() => album('album:Hiveswap Friendsim'), TypeError);
-  t.throws(() => album('album:'), TypeError);
-  t.throws(() => album('track:showtime-piano-refrain'), TypeError);
-
-  t.ok(typeless('Hopes and Dreams'));
-  t.ok(typeless('track:snowdin-town'));
-  t.throws(() => typeless(''), TypeError);
-  t.throws(() => typeless('album:undertale-soundtrack'));
-});
-
-test(t, 'validateReferenceList', t => {
-  const track = validateReferenceList('track');
-  const artist = validateReferenceList('artist');
-
-  t.plan(11);
-
-  t.ok(track(['track:fallen-down', 'Once Upon a Time']));
-  t.ok(artist(['artist:toby-fox', 'Mark Hadley']));
-  t.ok(track(['track:amalgam']));
-  t.ok(track([]));
-
-  let caughtError = null;
-  try {
-    track(['Dog', 'album:vaporwave-2016', 'Cat', 'artist:john-madden']);
-  } catch (err) {
-    caughtError = err;
-  }
-
-  t.not(caughtError, null);
-  t.ok(caughtError instanceof AggregateError);
-  t.equal(caughtError.errors.length, 2);
-  t.ok(caughtError.errors[0] instanceof Error);
-  t.ok(caughtError.errors[0].cause instanceof TypeError);
-  t.ok(caughtError.errors[1] instanceof Error);
-  t.ok(caughtError.errors[0].cause instanceof TypeError);
-});
-
-test(t, 'anyOf', t => {
-  t.plan(11);
-
-  const isStringOrNumber = anyOf(isString, isNumber);
-
-  t.ok(isStringOrNumber('hello world'));
-  t.ok(isStringOrNumber(42));
-  t.throws(() => isStringOrNumber(false));
-
-  const mockError = new Error();
-  const neverSucceeds = () => {
-    throw mockError;
-  };
-
-  const isStringOrGetRekt = anyOf(isString, neverSucceeds);
-
-  t.ok(isStringOrGetRekt('phew!'));
-
-  let caughtError = null;
-  try {
-    isStringOrGetRekt(0xdeadbeef);
-  } catch (err) {
-    caughtError = err;
-  }
-
-  t.not(caughtError, null);
-  t.ok(caughtError instanceof AggregateError);
-  t.equal(caughtError.errors.length, 2);
-  t.ok(caughtError.errors[0] instanceof TypeError);
-  t.equal(caughtError.errors[0].check, isString);
-  t.equal(caughtError.errors[1], mockError);
-  t.equal(caughtError.errors[1].check, neverSucceeds);
-});