« get me outta code hell

hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
path: root/test
diff options
context:
space:
mode:
Diffstat (limited to 'test')
-rw-r--r--test/lib/wiki-data.js18
-rw-r--r--test/snapshot/generateAdditionalFilesList.js64
-rw-r--r--test/snapshot/generateAdditionalFilesShortcut.js36
-rw-r--r--test/snapshot/generateAlbumAdditionalFilesList.js84
-rw-r--r--test/snapshot/generateAlbumCoverArtwork.js1
-rw-r--r--test/snapshot/generateAlbumReleaseInfo.js14
-rw-r--r--test/snapshot/generateAlbumSecondaryNav.js2
-rw-r--r--test/snapshot/generateAlbumSidebarGroupBox.js2
-rw-r--r--test/snapshot/generateAlbumTrackList.js7
-rw-r--r--test/snapshot/generateTrackCoverArtwork.js2
-rw-r--r--test/snapshot/generateTrackReleaseInfo.js4
-rw-r--r--test/snapshot/image.js13
-rw-r--r--test/snapshot/linkContribution.js20
-rw-r--r--test/snapshot/linkExternal.js171
-rw-r--r--test/unit/content/dependencies/linkContribution.js54
-rw-r--r--test/unit/data/cacheable-object.js2
-rw-r--r--test/unit/data/composite/things/track/withAlbum.js95
-rw-r--r--test/unit/data/things/album.js185
-rw-r--r--test/unit/data/things/art-tag.js18
-rw-r--r--test/unit/data/things/track.js157
-rw-r--r--test/unit/data/things/validators.js16
21 files changed, 619 insertions, 346 deletions
diff --git a/test/lib/wiki-data.js b/test/lib/wiki-data.js
index d2d860c..75b1170 100644
--- a/test/lib/wiki-data.js
+++ b/test/lib/wiki-data.js
@@ -13,20 +13,20 @@ export function linkAndBindWikiData(wikiData, {
             .map(([key, value]) => [key, value.slice()]))
         : wikiData));
 
-    // If albumData is present, automatically set albums' ownTrackData values
-    // by resolving track sections' references against the full array. This is
-    // just a nicety for working with albums throughout tests.
+    // 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');
 
-        const trackRefs =
-          trackSections.flatMap(section => section.tracks);
-
-        album.ownTrackData =
-          trackRefs.map(ref =>
-            find.track(ref, wikiData.trackData, {mode: 'error'}));
+        for (const trackSection of trackSections) {
+          trackSection.ownTrackData =
+            CacheableObject.getUpdateValue(trackSection, 'tracks')
+              .map(ref =>
+                find.track(ref, wikiData.trackData, {mode: 'error'}));
+        }
       }
     }
   }
diff --git a/test/snapshot/generateAdditionalFilesList.js b/test/snapshot/generateAdditionalFilesList.js
deleted file mode 100644
index 3ea1c37..0000000
--- a/test/snapshot/generateAdditionalFilesList.js
+++ /dev/null
@@ -1,64 +0,0 @@
-import t from 'tap';
-import {testContentFunctions} from '#test-lib';
-
-testContentFunctions(t, 'generateAdditionalFilesList (snapshot)', async (t, evaluate) => {
-  await evaluate.load();
-
-  evaluate.snapshot('no additional files', {
-    name: 'generateAdditionalFilesList',
-    args: [[]],
-  });
-
-  evaluate.snapshot('basic behavior', {
-    name: 'generateAdditionalFilesList',
-    args: [
-      [
-        {
-          title: 'SBURB Wallpaper',
-          files: [
-            'sburbwp_1280x1024.jpg',
-            'sburbwp_1440x900.jpg',
-            'sburbwp_1920x1080.jpg',
-          ],
-        },
-        {
-          title: 'Fake Section',
-          description: 'Ooo, what happens if there are NO file links provided?',
-          files: [
-            'oops.mp3',
-            'Internet Explorer.gif',
-            'daisy.mp3',
-          ],
-        },
-        {
-          title: 'Alternate Covers',
-          description: 'This is just an example description.',
-          files: [
-            'Homestuck_Vol4_alt1.jpg',
-            'Homestuck_Vol4_alt2.jpg',
-            'Homestuck_Vol4_alt3.jpg',
-          ],
-        },
-      ],
-    ],
-    slots: {
-      fileLinks: {
-        'sburbwp_1280x1024.jpg': 'link to 1280x1024',
-        'sburbwp_1440x900.jpg': 'link to 1440x900',
-        'sburbwp_1920x1080.jpg': null,
-        'Homestuck_Vol4_alt1.jpg': 'link to alt1',
-        'Homestuck_Vol4_alt2.jpg': null,
-        'Homestuck_Vol4_alt3.jpg': 'link to alt3',
-      },
-      fileSizes: {
-        '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,
-      }
-    },
-  });
-});
diff --git a/test/snapshot/generateAdditionalFilesShortcut.js b/test/snapshot/generateAdditionalFilesShortcut.js
deleted file mode 100644
index 9825efa..0000000
--- a/test/snapshot/generateAdditionalFilesShortcut.js
+++ /dev/null
@@ -1,36 +0,0 @@
-import t from 'tap';
-import {testContentFunctions} from '#test-lib';
-
-testContentFunctions(t, 'generateAdditionalFilesShortcut (snapshot)', async (t, evaluate) => {
-  await evaluate.load();
-
-  evaluate.snapshot('no additional files', {
-    name: 'generateAdditionalFilesShortcut',
-    args: [[]],
-  });
-
-  evaluate.snapshot('basic behavior', {
-    name: 'generateAdditionalFilesShortcut',
-    args: [
-      [
-        {
-          title: 'SBURB Wallpaper',
-          files: [
-            'sburbwp_1280x1024.jpg',
-            'sburbwp_1440x900.jpg',
-            'sburbwp_1920x1080.jpg',
-          ],
-        },
-        {
-          title: 'Alternate Covers',
-          description: 'This is just an example description.',
-          files: [
-            'Homestuck_Vol4_alt1.jpg',
-            'Homestuck_Vol4_alt2.jpg',
-            'Homestuck_Vol4_alt3.jpg',
-          ],
-        },
-      ],
-    ],
-  });
-});
diff --git a/test/snapshot/generateAlbumAdditionalFilesList.js b/test/snapshot/generateAlbumAdditionalFilesList.js
new file mode 100644
index 0000000..c25e568
--- /dev/null
+++ b/test/snapshot/generateAlbumAdditionalFilesList.js
@@ -0,0 +1,84 @@
+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/generateAlbumCoverArtwork.js b/test/snapshot/generateAlbumCoverArtwork.js
index 9244c03..939c6e1 100644
--- a/test/snapshot/generateAlbumCoverArtwork.js
+++ b/test/snapshot/generateAlbumCoverArtwork.js
@@ -13,6 +13,7 @@ testContentFunctions(t, 'generateAlbumCoverArtwork (snapshot)', async (t, evalua
   const album = {
     directory: 'bee-forus-seatbelt-safebee',
     coverArtFileExtension: 'png',
+    coverArtDimensions: [400, 300],
     color: '#f28514',
     artTags: [
       {name: 'Damara', directory: 'damara', isContentWarning: false},
diff --git a/test/snapshot/generateAlbumReleaseInfo.js b/test/snapshot/generateAlbumReleaseInfo.js
index 3dea119..a109912 100644
--- a/test/snapshot/generateAlbumReleaseInfo.js
+++ b/test/snapshot/generateAlbumReleaseInfo.js
@@ -8,22 +8,22 @@ testContentFunctions(t, 'generateAlbumReleaseInfo (snapshot)', async (t, evaluat
     name: 'generateAlbumReleaseInfo',
     args: [{
       artistContribs: [
-        {who: {name: 'Toby Fox', directory: 'toby-fox', urls: null}, what: 'music probably'},
-        {who: {name: 'Tensei', directory: 'tensei', urls: ['https://tenseimusic.bandcamp.com/']}, what: 'hot jams'},
+        {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: [
-        {who: {name: 'Hanni Brosh', directory: 'hb', urls: null}, what: null},
+        {artist: {name: 'Hanni Brosh', directory: 'hb', urls: null}, annotation: null},
       ],
 
       wallpaperArtistContribs: [
-        {who: {name: 'Hanni Brosh', directory: 'hb', urls: null}, what: null},
-        {who: {name: 'Niklink', directory: 'niklink', urls: null}, what: 'edits'},
+        {artist: {name: 'Hanni Brosh', directory: 'hb', urls: null}, annotation: null},
+        {artist: {name: 'Niklink', directory: 'niklink', urls: null}, annotation: 'edits'},
       ],
 
       bannerArtistContribs: [
-        {who: {name: 'Hanni Brosh', directory: 'hb', urls: null}, what: null},
-        {who: {name: 'Niklink', directory: 'niklink', urls: null}, what: 'edits'},
+        {artist: {name: 'Hanni Brosh', directory: 'hb', urls: null}, annotation: null},
+        {artist: {name: 'Niklink', directory: 'niklink', urls: null}, annotation: 'edits'},
       ],
 
       name: 'AlterniaBound',
diff --git a/test/snapshot/generateAlbumSecondaryNav.js b/test/snapshot/generateAlbumSecondaryNav.js
index 709b062..57618f2 100644
--- a/test/snapshot/generateAlbumSecondaryNav.js
+++ b/test/snapshot/generateAlbumSecondaryNav.js
@@ -10,6 +10,8 @@ testContentFunctions(t, 'generateAlbumSecondaryNav (snapshot)', async (t, evalua
   group2 = {name: 'Bepis', directory: 'bepis', color: '#123456'};
 
   album = {
+    name: 'Album',
+    directory: 'album',
     date: new Date('2010-04-13'),
     groups: [group1, group2],
   };
diff --git a/test/snapshot/generateAlbumSidebarGroupBox.js b/test/snapshot/generateAlbumSidebarGroupBox.js
index 8785051..f920bd9 100644
--- a/test/snapshot/generateAlbumSidebarGroupBox.js
+++ b/test/snapshot/generateAlbumSidebarGroupBox.js
@@ -11,6 +11,8 @@ testContentFunctions(t, 'generateAlbumSidebarGroupBox (snapshot)', async (t, eva
   let album, group;
 
   album = {
+    name: 'Middle',
+    directory: 'middle',
     date: new Date('2010-04-13'),
   };
 
diff --git a/test/snapshot/generateAlbumTrackList.js b/test/snapshot/generateAlbumTrackList.js
index 181cc1d..08b3190 100644
--- a/test/snapshot/generateAlbumTrackList.js
+++ b/test/snapshot/generateAlbumTrackList.js
@@ -10,12 +10,13 @@ testContentFunctions(t, 'generateAlbumTrackList (snapshot)', async (t, evaluate)
   });
 
   const contribs1 = [
-    {who: {name: 'Apricot', directory: 'apricot', urls: null}},
+    {artist: {name: 'Apricot', directory: 'apricot', urls: null}},
   ];
 
   const contribs2 = [
-    {who: {name: 'Apricot', directory: 'apricot', urls: null}},
-    {who: {name: 'Peach', directory: 'peach', urls: ['https://peach.bandcamp.com/']}},
+    {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';
diff --git a/test/snapshot/generateTrackCoverArtwork.js b/test/snapshot/generateTrackCoverArtwork.js
index 1e651eb..4d95211 100644
--- a/test/snapshot/generateTrackCoverArtwork.js
+++ b/test/snapshot/generateTrackCoverArtwork.js
@@ -11,6 +11,7 @@ testContentFunctions(t, 'generateTrackCoverArtwork (snapshot)', async (t, evalua
   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},
@@ -23,6 +24,7 @@ testContentFunctions(t, 'generateTrackCoverArtwork (snapshot)', async (t, evalua
     directory: 'beesmp3',
     hasUniqueCoverArt: true,
     coverArtFileExtension: 'jpg',
+    coverArtDimensions: null,
     color: '#f28514',
     artTags: [{name: 'Bees', directory: 'bees', isContentWarning: false}],
     album,
diff --git a/test/snapshot/generateTrackReleaseInfo.js b/test/snapshot/generateTrackReleaseInfo.js
index c72344b..78f0fee 100644
--- a/test/snapshot/generateTrackReleaseInfo.js
+++ b/test/snapshot/generateTrackReleaseInfo.js
@@ -4,8 +4,8 @@ import {testContentFunctions} from '#test-lib';
 testContentFunctions(t, 'generateTrackReleaseInfo (snapshot)', async (t, evaluate) => {
   await evaluate.load();
 
-  const artistContribs = [{who: {name: 'Toby Fox', directory: 'toby-fox', urls: null}, what: null}];
-  const coverArtistContribs = [{who: {name: 'Alpaca', directory: 'alpaca', urls: null}, what: '🔥'}];
+  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',
diff --git a/test/snapshot/image.js b/test/snapshot/image.js
index 447e7fa..1985211 100644
--- a/test/snapshot/image.js
+++ b/test/snapshot/image.js
@@ -38,11 +38,10 @@ testContentFunctions(t, 'image (snapshot)', async (t, evaluate) => {
     },
   });
 
-  quickSnapshot('width & height', {
+  quickSnapshot('dimensions', {
     slots: {
       src: 'foobar',
-      width: 600,
-      height: 400,
+      dimensions: [600, 400],
     },
   });
 
@@ -53,6 +52,14 @@ testContentFunctions(t, 'image (snapshot)', async (t, evaluate) => {
     },
   });
 
+  quickSnapshot('dimensions with square', {
+    slots: {
+      src: 'foobar',
+      dimensions: [600, 400],
+      square: true,
+    },
+  });
+
   quickSnapshot('lazy with square', {
     slots: {
       src: 'foobar',
diff --git a/test/snapshot/linkContribution.js b/test/snapshot/linkContribution.js
index ebd3be5..1043ddc 100644
--- a/test/snapshot/linkContribution.js
+++ b/test/snapshot/linkContribution.js
@@ -9,25 +9,25 @@ testContentFunctions(t, 'linkContribution (snapshot)', async (t, evaluate) => {
       name: 'linkContribution',
       multiple: [
         {args: [
-          {who: {
+          {artist: {
             name: 'Clark Powell',
             directory: 'clark-powell',
             urls: ['https://soundcloud.com/plazmataz'],
-          }, what: null},
+          }, annotation: null},
         ]},
         {args: [
-          {who: {
+          {artist: {
             name: 'Grounder & Scratch',
             directory: 'the-big-baddies',
             urls: [],
-          }, what: 'Snooping'},
+          }, annotation: 'Snooping'},
         ]},
         {args: [
-          {who: {
+          {artist: {
             name: 'Toby Fox',
             directory: 'toby-fox',
             urls: ['https://tobyfox.bandcamp.com/', 'https://toby.fox/'],
-          }, what: 'Arrangement'},
+          }, annotation: 'Arrangement'},
         ]},
       ],
       slots,
@@ -65,7 +65,7 @@ testContentFunctions(t, 'linkContribution (snapshot)', async (t, evaluate) => {
   evaluate.snapshot('loads of links (inline)', {
     name: 'linkContribution',
     args: [
-      {who: {name: 'Lorem Ipsum Lover', directory: 'lorem-ipsum-lover', urls: [
+      {artist: {name: 'Lorem Ipsum Lover', directory: 'lorem-ipsum-lover', urls: [
         'https://loremipsum.io',
         'https://loremipsum.io/generator/',
         'https://loremipsum.io/#meaning',
@@ -74,7 +74,7 @@ testContentFunctions(t, 'linkContribution (snapshot)', async (t, evaluate) => {
         'https://loremipsum.io/#when-to-use-lorem-ipsum',
         'https://loremipsum.io/#lorem-ipsum-all-the-things',
         'https://loremipsum.io/#original-source',
-      ]}, what: null},
+      ]}, annotation: null},
     ],
     slots: {showIcons: true},
   });
@@ -82,7 +82,7 @@ testContentFunctions(t, 'linkContribution (snapshot)', async (t, evaluate) => {
   evaluate.snapshot('loads of links (tooltip)', {
     name: 'linkContribution',
     args: [
-      {who: {name: 'Lorem Ipsum Lover', directory: 'lorem-ipsum-lover', urls: [
+      {artist: {name: 'Lorem Ipsum Lover', directory: 'lorem-ipsum-lover', urls: [
         'https://loremipsum.io',
         'https://loremipsum.io/generator/',
         'https://loremipsum.io/#meaning',
@@ -91,7 +91,7 @@ testContentFunctions(t, 'linkContribution (snapshot)', async (t, evaluate) => {
         'https://loremipsum.io/#when-to-use-lorem-ipsum',
         'https://loremipsum.io/#lorem-ipsum-all-the-things',
         'https://loremipsum.io/#original-source',
-      ]}, what: null},
+      ]}, annotation: null},
     ],
     slots: {showIcons: true, iconMode: 'tooltip'},
   });
diff --git a/test/snapshot/linkExternal.js b/test/snapshot/linkExternal.js
index f413863..90c98f4 100644
--- a/test/snapshot/linkExternal.js
+++ b/test/snapshot/linkExternal.js
@@ -20,35 +20,174 @@ testContentFunctions(t, 'linkExternal (snapshot)', async (t, evaluate) => {
     });
 
   const quickSnapshotAllStyles = (context, urls) => {
-    for (const style of ['platform', 'normal', 'compact']) {
+    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/',
-    'https://soundcloud.com/plazmataz',
-    'https://aeritus.tumblr.com/',
-    'https://twitter.com/awkwarddoesart',
+
+    // 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://en.wikipedia.org/wiki/Haydn_Quartet_(vocal_ensemble)',
-    'https://www.poetryfoundation.org/poets/christina-rossetti',
-    'https://www.instagram.com/levc_egm/',
-    'https://www.patreon.com/CecilyRenns',
-    'https://open.spotify.com/artist/63SNNpNOicDzG3LY82G4q3',
-    'https://buzinkai.newgrounds.com/',
+    'https://www.deviantart.com/shilloshilloh/art/Homestuck-Jake-English-268874606',
 
-    // Just one custom domain of each platform is OK here
-    'https://music.solatrus.com/',
-    'https://types.pl/',
+    // 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',
-    'https://mspaintadventures.fandom.com/',
-    'https://mspaintadventures.fandom.com/wiki/',
-    'https://mspaintadventures.fandom.com/wiki/Draconian_Dignitary',
+
+    // 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', [
diff --git a/test/unit/content/dependencies/linkContribution.js b/test/unit/content/dependencies/linkContribution.js
index 9490890..ab45b03 100644
--- a/test/unit/content/dependencies/linkContribution.js
+++ b/test/unit/content/dependencies/linkContribution.js
@@ -2,27 +2,27 @@ import t from 'tap';
 import {testContentFunctions} from '#test-lib';
 
 t.test('generateContributionLinks (unit)', async t => {
-  const who1 = {
+  const artist1 = {
     name: 'Clark Powell',
     directory: 'clark-powell',
     urls: ['https://soundcloud.com/plazmataz'],
   };
 
-  const who2 = {
+  const artist2 = {
     name: 'Grounder & Scratch',
     directory: 'the-big-baddies',
     urls: [],
   };
 
-  const who3 = {
+  const artist3 = {
     name: 'Toby Fox',
     directory: 'toby-fox',
     urls: ['https://tobyfox.bandcamp.com/', 'https://toby.fox/'],
   };
 
-  const what1 = null;
-  const what2 = 'Snooping';
-  const what3 = 'Arrangement';
+  const annotation1 = null;
+  const annotation2 = 'Snooping';
+  const annotation3 = 'Arrangement';
 
   await testContentFunctions(t, 'generateContributionLinks (unit 1)', async (t, evaluate) => {
     const slots = {
@@ -34,14 +34,14 @@ t.test('generateContributionLinks (unit)', async t => {
       mock: evaluate.mock(mock => ({
         linkArtist: {
           relations: mock.function('linkArtist.relations', () => ({}))
-            .args([undefined, who1]).next()
-            .args([undefined, who2]).next()
-            .args([undefined, who3]),
+            .args([undefined, artist1]).next()
+            .args([undefined, artist2]).next()
+            .args([undefined, artist3]),
 
           data: mock.function('linkArtist.data', () => ({}))
-            .args([who1]).next()
-            .args([who2]).next()
-            .args([who3]),
+            .args([artist1]).next()
+            .args([artist2]).next()
+            .args([artist3]),
 
           // This can be tweaked to return a specific (mocked) template
           // for each artist if we need to test for slots in the future.
@@ -51,9 +51,9 @@ t.test('generateContributionLinks (unit)', async t => {
 
         linkExternalAsIcon: {
           data: mock.function('linkExternalAsIcon.data', () => ({}))
-            .args([who1.urls[0]]).next()
-            .args([who3.urls[0]]).next()
-            .args([who3.urls[1]]),
+            .args([artist1.urls[0]]).next()
+            .args([artist3.urls[0]]).next()
+            .args([artist3.urls[1]]),
 
           generate: mock.function('linkExternalAsIcon.generate', () => 'icon')
             .repeat(3),
@@ -64,9 +64,9 @@ t.test('generateContributionLinks (unit)', async t => {
     evaluate({
       name: 'linkContribution',
       multiple: [
-        {args: [{who: who1, what: what1}]},
-        {args: [{who: who2, what: what2}]},
-        {args: [{who: who3, what: what3}]},
+        {args: [{artist: artist1, annotation: annotation1}]},
+        {args: [{artist: artist2, annotation: annotation2}]},
+        {args: [{artist: artist3, annotation: annotation3}]},
       ],
       slots,
     });
@@ -82,14 +82,14 @@ t.test('generateContributionLinks (unit)', async t => {
       mock: evaluate.mock(mock => ({
         linkArtist: {
           relations: mock.function('linkArtist.relations', () => ({}))
-            .args([undefined, who1]).next()
-            .args([undefined, who2]).next()
-            .args([undefined, who3]),
+            .args([undefined, artist1]).next()
+            .args([undefined, artist2]).next()
+            .args([undefined, artist3]),
 
           data: mock.function('linkArtist.data', () => ({}))
-            .args([who1]).next()
-            .args([who2]).next()
-            .args([who3]),
+            .args([artist1]).next()
+            .args([artist2]).next()
+            .args([artist3]),
 
           generate: mock.function(() => 'artist link')
             .repeat(3),
@@ -112,9 +112,9 @@ t.test('generateContributionLinks (unit)', async t => {
     evaluate({
       name: 'linkContribution',
       multiple: [
-        {args: [{who: who1, what: what1}]},
-        {args: [{who: who2, what: what2}]},
-        {args: [{who: who3, what: what3}]},
+        {args: [{artist: artist1, annotation: annotation1}]},
+        {args: [{artist: artist2, annotation: annotation2}]},
+        {args: [{artist: artist3, annotation: annotation3}]},
       ],
       slots,
     });
diff --git a/test/unit/data/cacheable-object.js b/test/unit/data/cacheable-object.js
index 8c31a5b..4b92724 100644
--- a/test/unit/data/cacheable-object.js
+++ b/test/unit/data/cacheable-object.js
@@ -4,7 +4,7 @@ import CacheableObject from '#cacheable-object';
 
 function newCacheableObject(PD) {
   return new (class extends CacheableObject {
-    static propertyDescriptors = PD;
+    static [CacheableObject.propertyDescriptors] = PD;
   });
 }
 
diff --git a/test/unit/data/composite/things/track/withAlbum.js b/test/unit/data/composite/things/track/withAlbum.js
index 30f8cc5..6f50776 100644
--- a/test/unit/data/composite/things/track/withAlbum.js
+++ b/test/unit/data/composite/things/track/withAlbum.js
@@ -1,5 +1,9 @@
 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';
@@ -21,9 +25,21 @@ t.test(`withAlbum: basic behavior`, t => {
     },
   });
 
-  const fakeTrack1 = {directory: 'foo'};
-  const fakeTrack2 = {directory: 'bar'};
-  const fakeAlbum = {directory: 'baz', tracks: [fakeTrack1]};
+  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({
@@ -40,7 +56,7 @@ t.test(`withAlbum: basic behavior`, t => {
     null);
 });
 
-t.test(`withAlbum: early exit conditions (notFoundMode: null)`, t => {
+t.test(`withAlbum: early exit conditions`, t => {
   t.plan(4);
 
   const composite = compositeFrom({
@@ -53,9 +69,21 @@ t.test(`withAlbum: early exit conditions (notFoundMode: null)`, t => {
     ],
   });
 
-  const fakeTrack1 = {directory: 'foo'};
-  const fakeTrack2 = {directory: 'bar'};
-  const fakeAlbum = {directory: 'baz', tracks: [fakeTrack1]};
+  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({
@@ -89,56 +117,3 @@ t.test(`withAlbum: early exit conditions (notFoundMode: null)`, t => {
     null,
     `early exits if albumData is null`);
 });
-
-t.test(`withAlbum: early exit conditions (notFoundMode: exit)`, t => {
-  t.plan(4);
-
-  const composite = compositeFrom({
-    compose: false,
-    steps: [
-      withAlbum({
-        notFoundMode: input.value('exit'),
-      }),
-
-      exposeConstant({
-        value: input.value('bimbam'),
-      }),
-    ],
-  });
-
-  const fakeTrack1 = {directory: 'foo'};
-  const fakeTrack2 = {directory: 'bar'};
-  const fakeAlbum = {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,
-    }),
-    null,
-    `early exits if albumData is present and does not contain the track`);
-
-  t.equal(
-    composite.expose.compute({
-      albumData: [],
-      this: fakeTrack1,
-    }),
-    null,
-    `early exits 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/things/album.js b/test/unit/data/things/album.js
index 46ea83b..d28ab70 100644
--- a/test/unit/data/things/album.js
+++ b/test/unit/data/things/album.js
@@ -8,6 +8,7 @@ const {
   ArtTag,
   Artist,
   Track,
+  TrackSection,
 } = thingConstructors;
 
 function stubArtTag(tagName = `Test Art Tag`) {
@@ -21,8 +22,8 @@ function stubArtistAndContribs() {
   const artist = new Artist();
   artist.name = `Test Artist`;
 
-  const contribs = [{who: `Test Artist`, what: null}];
-  const badContribs = [{who: `Figment of Your Imagination`, what: null}];
+  const contribs = [{artist: `Test Artist`, annotation: null}];
+  const badContribs = [{artist: `Figment of Your Imagination`, annotation: null}];
 
   return {artist, contribs, badContribs};
 }
@@ -34,6 +35,15 @@ function stubTrack(directory = 'foo') {
   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);
 
@@ -237,78 +247,115 @@ 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];
 
-  album.ownTrackData = tracks;
+  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`);
 
-  album.trackSections = [
-    {tracks: ['track:track1', 'track:track2', 'track:track3']},
-  ];
+  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`);
 
-  album.trackSections = [
-    {tracks: ['track:track1']},
-    {tracks: ['track:track2', 'track:track3']},
-  ];
+  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`);
 
-  album.trackSections = [
-    {tracks: ['track:track1', 'track:does-not-exist']},
-    {tracks: ['track:this-one-neither', 'track:track2']},
-    {tracks: ['track:effectively-empty-section']},
-    {tracks: ['track:track3']},
-  ];
+  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`);
 
-  album.trackSections = [
-    {tracks: ['track:track1']},
-    {},
-    {tracks: ['track:track2']},
-    {},
-    {},
-    {tracks: ['track:track3']},
-  ];
+  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 missing tracks property`);
+    `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];
 
-  album.ownTrackData = tracks;
+  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];
   }
 
-  album.trackSections = [
-    {tracks: ['track:track1', 'track:track2']},
-    {tracks: ['track:track3', 'track:track4']},
-  ];
+  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]},
@@ -320,11 +367,18 @@ t.test(`Album.trackSections`, t => {
     {tracks: [track3, track4], startIndex: 2},
   ], `Album.trackSections #2: exposes startIndex`);
 
-  album.trackSections = [
-    {name: 'First section', tracks: ['track:track1']},
-    {name: 'Second section', tracks: ['track:track2']},
-    {tracks: ['track:track3']},
-  ];
+  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]},
@@ -334,11 +388,11 @@ t.test(`Album.trackSections`, t => {
 
   album.color = '#123456';
 
-  album.trackSections = [
-    {tracks: ['track:track1'], color: null},
-    {tracks: ['track:track2'], color: '#abcdef'},
-    {tracks: ['track:track3'], color: null},
-  ];
+  section2.color = '#abcdef';
+
+  // XXX_decacheWikiData
+  album.trackSections = [];
+  album.trackSections = [section1, section2, section3];
 
   t.match(album.trackSections, [
     {tracks: [track1], color: '#123456'},
@@ -346,11 +400,11 @@ t.test(`Album.trackSections`, t => {
     {tracks: [track3], color: '#123456'},
   ], `Album.trackSections #4: exposes color, inherited from album`);
 
-  album.trackSections = [
-    {tracks: ['track:track1'], dateOriginallyReleased: null},
-    {tracks: ['track:track2'], dateOriginallyReleased: new Date('2009-04-11')},
-    {tracks: ['track:track3'], dateOriginallyReleased: null},
-  ];
+  section2.dateOriginallyReleased = new Date('2009-04-11');
+
+  // XXX_decacheWikiData
+  album.trackSections = [];
+  album.trackSections = [section1, section2, section3];
 
   t.match(album.trackSections, [
     {tracks: [track1], dateOriginallyReleased: null},
@@ -358,11 +412,12 @@ t.test(`Album.trackSections`, t => {
     {tracks: [track3], dateOriginallyReleased: null},
   ], `Album.trackSections #5: exposes dateOriginallyReleased, if present`);
 
-  album.trackSections = [
-    {tracks: ['track:track1'], isDefaultTrackSection: true},
-    {tracks: ['track:track2'], isDefaultTrackSection: false},
-    {tracks: ['track:track3'], isDefaultTrackSection: null},
-  ];
+  section1.isDefaultTrackSection = true;
+  section2.isDefaultTrackSection = false;
+
+  // XXX_decacheWikiData
+  album.trackSections = [];
+  album.trackSections = [section1, section2, section3];
 
   t.match(album.trackSections, [
     {tracks: [track1], isDefaultTrackSection: true},
@@ -370,19 +425,33 @@ t.test(`Album.trackSections`, t => {
     {tracks: [track3], isDefaultTrackSection: false},
   ], `Album.trackSections #6: exposes isDefaultTrackSection, defaults to false`);
 
-  album.trackSections = [
-    {tracks: ['track:track1', 'track:track2', 'track:snooping'], color: '#112233'},
-    {tracks: ['track:track3', 'track:as-usual'],                 color: '#334455'},
-    {tracks: [],                                                 color: '#bbbbba'},
-    {tracks: ['track:icy', 'track:chilly', 'track:frigid'],      color: '#556677'},
-    {tracks: ['track:track4'],                                   color: '#778899'},
-  ];
+  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 & empty sections`);
+  ], `Album.trackSections #7: filters out references without matches, keeps empty sections`);
 });
 
 t.test(`Album.wallpaperFileExtension`, t => {
diff --git a/test/unit/data/things/art-tag.js b/test/unit/data/things/art-tag.js
index 561c93e..427b357 100644
--- a/test/unit/data/things/art-tag.js
+++ b/test/unit/data/things/art-tag.js
@@ -8,18 +8,28 @@ const {
   Artist,
   ArtTag,
   Track,
+  trackSection,
 } = thingConstructors;
 
 function stubAlbum(tracks, directory = 'bar') {
   const album = new Album();
   album.directory = directory;
 
-  const trackRefs = tracks.map(t => Thing.getReference(t));
-  album.trackSections = [{tracks: trackRefs}];
+  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;
@@ -43,8 +53,8 @@ function stubArtist(artistName = `Test Artist`) {
 
 function stubArtistAndContribs(artistName = `Test Artist`) {
   const artist = stubArtist(artistName);
-  const contribs = [{who: artistName, what: null}];
-  const badContribs = [{who: `Figment of Your Imagination`, what: null}];
+  const contribs = [{artist: artistName, annotation: null}];
+  const badContribs = [{artist: `Figment of Your Imagination`, annotation: null}];
 
   return {artist, contribs, badContribs};
 }
diff --git a/test/unit/data/things/track.js b/test/unit/data/things/track.js
index b1c1611..644d21c 100644
--- a/test/unit/data/things/track.js
+++ b/test/unit/data/things/track.js
@@ -11,18 +11,28 @@ const {
   FlashAct,
   Thing,
   Track,
+  TrackSection,
 } = thingConstructors;
 
 function stubAlbum(tracks, directory = 'bar') {
   const album = new Album();
   album.directory = directory;
 
-  const trackRefs = tracks.map(t => Thing.getReference(t));
-  album.trackSections = [{tracks: trackRefs}];
+  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;
@@ -46,8 +56,8 @@ function stubArtist(artistName = `Test Artist`) {
 
 function stubArtistAndContribs(artistName = `Test Artist`) {
   const artist = stubArtist(artistName);
-  const contribs = [{who: artistName, what: null}];
-  const badContribs = [{who: `Figment of Your Imagination`, what: null}];
+  const contribs = [{artist: artistName, annotation: null}];
+  const badContribs = [{artist: `Figment of Your Imagination`, annotation: null}];
 
   return {artist, contribs, badContribs};
 }
@@ -81,16 +91,22 @@ t.test(`Track.album`, t => {
   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];
-  album1.ownTrackData = [track1, track2];
-  album2.ownTrackData = [track1, track2];
-  album1.trackSections = [{tracks: ['track:track1']}];
-  album2.trackSections = [{tracks: ['track:track2']}];
+  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`);
@@ -105,21 +121,83 @@ t.test(`Track.album`, t => {
   t.equal(track1.album, null,
     `album #4: is null when track missing albumData`);
 
-  album1.ownTrackData = [];
-  track1.albumData = [album1, album2];
+  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 missing ownTrackData`);
+    `album #5: is null when album track section missing ownTrackData`);
 
-  album1.ownTrackData = [track1, track2];
-  album1.trackSections = [{tracks: ['track:track2']}];
+  section1.ownTrackData = [track2];
+  section1.tracks = ['track:track2'];
 
   // XXX_decacheWikiData
+  album1.trackSections = [];
+  album1.trackSections = [section1];
   track1.albumData = [];
-  track1.albumData = [album1, album2];
+  track1.albumData = [album2, album1];
 
   t.equal(track1.album, null,
-    `album #6: is null when album's trackSections don't match track`);
+    `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 => {
@@ -189,31 +267,31 @@ t.test(`Track.artistContribs`, t => {
     `artistContribs #1: defaults to empty array`);
 
   album.artistContribs = [
-    {who: `Artist 1`, what: `composition`},
-    {who: `Artist 2`, what: null},
+    {artist: `Artist 1`, annotation: `composition`},
+    {artist: `Artist 2`, annotation: null},
   ];
 
   XXX_decacheWikiData();
 
   t.same(track.artistContribs,
-    [{who: artist1, what: `composition`}, {who: artist2, what: null}],
+    [{artist: artist1, annotation: `composition`}, {artist: artist2, annotation: null}],
     `artistContribs #2: inherits album artistContribs`);
 
   track.artistContribs = [
-    {who: `Artist 1`, what: `arrangement`},
+    {artist: `Artist 1`, annotation: `arrangement`},
   ];
 
-  t.same(track.artistContribs, [{who: artist1, what: `arrangement`}],
+  t.same(track.artistContribs, [{artist: artist1, annotation: `arrangement`}],
     `artistContribs #3: resolves from own value`);
 
   track.artistContribs = [
-    {who: `Artist 1`, what: `snooping`},
-    {who: `Artist 413`, what: `as`},
-    {who: `Artist 2`, what: `usual`},
+    {artist: `Artist 1`, annotation: `snooping`},
+    {artist: `Artist 413`, annotation: `as`},
+    {artist: `Artist 2`, annotation: `usual`},
   ];
 
   t.same(track.artistContribs,
-    [{who: artist1, what: `snooping`}, {who: artist2, what: `usual`}],
+    [{artist: artist1, annotation: `snooping`}, {artist: artist2, annotation: `usual`}],
     `artistContribs #4: filters out names without matches`);
 });
 
@@ -230,11 +308,13 @@ t.test(`Track.color`, t => {
   t.equal(track.color, null,
     `color #1: defaults to null`);
 
+  const section = stubTrackSection(album, [track]);
+
   album.color = '#abcdef';
-  album.trackSections = [{
-    color: '#beeeef',
-    tracks: [Thing.getReference(track)],
-  }];
+  section.color = '#beeeef';
+
+  album.trackSections = [section];
+
   XXX_decacheWikiData();
 
   t.equal(track.color, '#beeeef',
@@ -248,6 +328,7 @@ t.test(`Track.color`, t => {
   track.albumData = [
     {
       constructor: {[Thing.referenceType]: 'album'},
+      [Thing.isThing]: true,
       color: '#abcdef',
       tracks: [track],
       trackSections: [
@@ -303,7 +384,7 @@ t.test(`Track.commentatorArtists`, t => {
     `Track.commentatorArtists #2: works with two commentators`);
 
   track.commentary = commentary +=
-    `<i>Icy|<b>Icy What You Did There</b>:</i>\n` +
+    `<i>Icy|<b>Icy annotation You Did There</b>:</i>\n` +
     `Incredible.\n`;
 
   t.same(track.commentatorArtists, [artist1, artist2, artist3],
@@ -362,31 +443,31 @@ t.test(`Track.coverArtistContribs`, t => {
     `coverArtistContribs #1: defaults to empty array`);
 
   album.trackCoverArtistContribs = [
-    {who: `Artist 1`, what: `lines`},
-    {who: `Artist 2`, what: null},
+    {artist: `Artist 1`, annotation: `lines`},
+    {artist: `Artist 2`, annotation: null},
   ];
 
   XXX_decacheWikiData();
 
   t.same(track.coverArtistContribs,
-    [{who: artist1, what: `lines`}, {who: artist2, what: null}],
+    [{artist: artist1, annotation: `lines`}, {artist: artist2, annotation: null}],
     `coverArtistContribs #2: inherits album trackCoverArtistContribs`);
 
   track.coverArtistContribs = [
-    {who: `Artist 1`, what: `collage`},
+    {artist: `Artist 1`, annotation: `collage`},
   ];
 
-  t.same(track.coverArtistContribs, [{who: artist1, what: `collage`}],
+  t.same(track.coverArtistContribs, [{artist: artist1, annotation: `collage`}],
     `coverArtistContribs #3: resolves from own value`);
 
   track.coverArtistContribs = [
-    {who: `Artist 1`, what: `snooping`},
-    {who: `Artist 413`, what: `as`},
-    {who: `Artist 2`, what: `usual`},
+    {artist: `Artist 1`, annotation: `snooping`},
+    {artist: `Artist 413`, annotation: `as`},
+    {artist: `Artist 2`, annotation: `usual`},
   ];
 
   t.same(track.coverArtistContribs,
-    [{who: artist1, what: `snooping`}, {who: artist2, what: `usual`}],
+    [{artist: artist1, annotation: `snooping`}, {artist: artist2, annotation: `usual`}],
     `coverArtistContribs #4: filters out names without matches`);
 
   track.disableUniqueCoverArt = true;
diff --git a/test/unit/data/things/validators.js b/test/unit/data/things/validators.js
index 11134a9..3a217d6 100644
--- a/test/unit/data/things/validators.js
+++ b/test/unit/data/things/validators.js
@@ -1,5 +1,5 @@
 import t from 'tap';
-import {showAggregate} from '#sugar';
+import {showAggregate} from '#aggregate';
 
 import {
   // Basic types
@@ -280,17 +280,17 @@ t.test('isContentString', t => {
 
 t.test('isContribution', t => {
   t.plan(4);
-  t.ok(isContribution({who: 'artist:toby-fox', what: 'Music'}));
-  t.ok(isContribution({who: 'Toby Fox'}));
-  t.throws(() => isContribution(({who: 'group:umspaf', what: 'Organizing'})),
-    {errors: /who/});
-  t.throws(() => isContribution(({who: 'artist:toby-fox', what: 123})),
-    {errors: /what/});
+  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([{who: 'Beavis'}, {who: 'Butthead', what: 'Wrangling'}]));
+  t.ok(isContributionList([{artist: 'Beavis'}, {artist: 'Butthead', annotation: 'Wrangling'}]));
   t.ok(isContributionList([]));
   t.throws(() => isContributionList(2));
   t.throws(() => isContributionList(['Charlie', 'Woodstock']));