From 91469b51fe10fbedaece83dafb07e1fd6730e11c Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Mon, 5 Aug 2024 20:39:44 -0300 Subject: test: general refactor instantiating things & managing wikiData --- test/lib/wiki-data.js | 98 ++++++++ test/unit/data/things/album.js | 211 ++++++++--------- test/unit/data/things/art-tag.js | 59 +---- test/unit/data/things/flash.js | 39 +--- test/unit/data/things/track.js | 351 +++++++++++----------------- test/unit/data/things/validators.js | 440 ------------------------------------ test/unit/data/validators.js | 440 ++++++++++++++++++++++++++++++++++++ 7 files changed, 784 insertions(+), 854 deletions(-) delete mode 100644 test/unit/data/things/validators.js create mode 100644 test/unit/data/validators.js (limited to 'test') diff --git a/test/lib/wiki-data.js b/test/lib/wiki-data.js index c373aadd..7c3d2147 100644 --- a/test/lib/wiki-data.js +++ b/test/lib/wiki-data.js @@ -1,6 +1,8 @@ import CacheableObject from '#cacheable-object'; import find from '#find'; import {withEntries} from '#sugar'; +import Thing from '#thing'; +import thingConstructors from '#things'; import {linkWikiDataArrays} from '#yaml'; export function linkAndBindWikiData(wikiData, { @@ -53,3 +55,99 @@ export function linkAndBindWikiData(wikiData, { .bind(null, wikiData, {XXX_decacheWikiData: true}), }; } + +export function stubWikiData() { + return { + albumData: [], + artistData: [], + artTagData: [], + flashData: [], + flashActData: [], + flashSideData: [], + groupData: [], + groupCategoryData: [], + newsData: [], + staticPageData: [], + trackData: [], + trackSectionData: [], + }; +} + +export function stubThing(wikiData, constructor, properties = {}) { + const thing = Reflect.construct(constructor, []); + Object.assign(thing, properties); + + const wikiDataSpec = { + Album: 'albumData', + Artist: 'artistData', + ArtTag: 'artTagData', + Flash: 'flashData', + FlashAct: 'flashActData', + FlashSide: 'flashSideData', + Group: 'groupData', + GroupCategory: 'groupCategoryData', + NewsEntry: 'newsData', + StaticPage: 'staticPageData', + Track: 'trackData', + TrackSection: 'trackSectionData', + }; + + const wikiDataMap = + new Map( + Object.entries(wikiDataSpec) + .map(([thingKey, wikiDataKey]) => [ + thingConstructors[thingKey], + wikiData[wikiDataKey], + ])); + + const wikiDataArray = + wikiDataMap.get(constructor); + + wikiDataArray.push(thing); + + return thing; +} + +export function stubTrackAndAlbum(wikiData, trackDirectory = null, albumDirectory = null) { + const {Track, TrackSection, Album} = thingConstructors; + + const track = + stubThing(wikiData, Track, {directory: trackDirectory}); + + const section = + stubThing(wikiData, TrackSection, {tracks: [track]}); + + const album = + stubThing(wikiData, Album, {directory: albumDirectory, trackSections: [section]}); + + return {track, album, section}; +} + +export function stubArtistAndContribs(wikiData, artistName = `Test Artist`) { + const {Artist} = thingConstructors; + + const artist = + stubThing(wikiData, Artist, {name: artistName}); + + const contribs = + [{artist: artistName, annotation: null}]; + + const badContribs = + [{artist: `Figment of Your Imagination`, annotation: null}]; + + return {artist, contribs, badContribs}; +} + +export function stubFlashAndAct(wikiData, flashDirectory = null) { + const {Flash, FlashAct} = thingConstructors; + + const flash = + stubThing(wikiData, Flash, {directory: flashDirectory}); + + const flashAct = + stubThing(wikiData, FlashAct, { + flashes: [Thing.getReference(flash)], + }); + + return {flash, flashAct}; +} diff --git a/test/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(`Toby Fox:\ndogsong.mp3`)); - t.ok(isCommentary(`Toby Fox: (music)\ndogsong.mp3`)); - t.throws(() => isCommentary(`dogsong.mp3\nToby Fox:\ndogsong.mp3`)); - t.throws(() => isCommentary(`Toby Fox: dogsong.mp3`)); - t.throws(() => isCommentary(`Toby Fox: (music) dogsong.mp3`)); - t.throws(() => isCommentary(`I Have Nothing To Say:`)); - t.throws(() => isCommentary(123)); - t.throws(() => isCommentary(``)); - t.throws(() => isCommentary(`Technically, ah, er:\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); -}); diff --git a/test/unit/data/validators.js b/test/unit/data/validators.js new file mode 100644 index 00000000..3a217d6f --- /dev/null +++ b/test/unit/data/validators.js @@ -0,0 +1,440 @@ +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(`Toby Fox:\ndogsong.mp3`)); + t.ok(isCommentary(`Toby Fox: (music)\ndogsong.mp3`)); + t.throws(() => isCommentary(`dogsong.mp3\nToby Fox:\ndogsong.mp3`)); + t.throws(() => isCommentary(`Toby Fox: dogsong.mp3`)); + t.throws(() => isCommentary(`Toby Fox: (music) dogsong.mp3`)); + t.throws(() => isCommentary(`I Have Nothing To Say:`)); + t.throws(() => isCommentary(123)); + t.throws(() => isCommentary(``)); + t.throws(() => isCommentary(`Technically, ah, er:\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); +}); -- cgit 1.3.0-6-gf8a5