diff options
Diffstat (limited to 'test/unit/data/things')
-rw-r--r-- | test/unit/data/things/album.js | 451 | ||||
-rw-r--r-- | test/unit/data/things/art-tag.js | 71 | ||||
-rw-r--r-- | test/unit/data/things/flash.js | 55 | ||||
-rw-r--r-- | test/unit/data/things/track.js | 759 | ||||
-rw-r--r-- | test/unit/data/things/validators.js | 440 |
5 files changed, 1776 insertions, 0 deletions
diff --git a/test/unit/data/things/album.js b/test/unit/data/things/album.js new file mode 100644 index 0000000..46ea83b --- /dev/null +++ b/test/unit/data/things/album.js @@ -0,0 +1,451 @@ +import t from 'tap'; + +import {linkAndBindWikiData} from '#test-lib'; +import thingConstructors from '#things'; + +const { + Album, + ArtTag, + Artist, + Track, +} = 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 = [{who: `Test Artist`, what: null}]; + const badContribs = [{who: `Figment of Your Imagination`, what: null}]; + + return {artist, contribs, badContribs}; +} + +function stubTrack(directory = 'foo') { + const track = new Track(); + track.directory = directory; + + return track; +} + +t.test(`Album.artTags`, t => { + t.plan(3); + + const {artist, contribs} = stubArtistAndContribs(); + const album = new Album(); + const tag1 = stubArtTag(`Tag 1`); + const tag2 = stubArtTag(`Tag 2`); + + const {XXX_decacheWikiData} = linkAndBindWikiData({ + albumData: [album], + artistData: [artist], + artTagData: [tag1, tag2], + }); + + t.same(album.artTags, [], + `artTags #1: defaults to empty array`); + + album.artTags = [`Tag 1`, `Tag 2`]; + + t.same(album.artTags, [], + `artTags #2: is empty if album doesn't have cover artists`); + + album.coverArtistContribs = contribs; + + t.same(album.artTags, [tag1, tag2], + `artTags #3: resolves if album has cover artists`); +}); + +t.test(`Album.bannerDimensions`, t => { + t.plan(4); + + const album = new Album(); + const {artist, contribs, badContribs} = stubArtistAndContribs(); + + linkAndBindWikiData({ + albumData: [album], + artistData: [artist], + }); + + t.equal(album.bannerDimensions, null, + `Album.bannerDimensions #1: defaults to null`); + + album.bannerDimensions = [1200, 275]; + + t.equal(album.bannerDimensions, null, + `Album.bannerDimensions #2: is null if bannerArtistContribs empty`); + + album.bannerArtistContribs = badContribs; + + t.equal(album.bannerDimensions, null, + `Album.bannerDimensions #3: is null if bannerArtistContribs resolves empty`); + + album.bannerArtistContribs = contribs; + + t.same(album.bannerDimensions, [1200, 275], + `Album.bannerDimensions #4: is own value`); +}); + +t.test(`Album.bannerFileExtension`, t => { + t.plan(5); + + const album = new Album(); + const {artist, contribs, badContribs} = stubArtistAndContribs(); + + linkAndBindWikiData({ + albumData: [album], + artistData: [artist], + }); + + t.equal(album.bannerFileExtension, null, + `Album.bannerFileExtension #1: defaults to null`); + + album.bannerFileExtension = 'png'; + + t.equal(album.bannerFileExtension, null, + `Album.bannerFileExtension #2: is null if bannerArtistContribs empty`); + + album.bannerArtistContribs = badContribs; + + t.equal(album.bannerFileExtension, null, + `Album.bannerFileExtension #3: is null if bannerArtistContribs resolves empty`); + + album.bannerArtistContribs = contribs; + + t.equal(album.bannerFileExtension, 'png', + `Album.bannerFileExtension #4: is own value`); + + album.bannerFileExtension = null; + + t.equal(album.bannerFileExtension, 'jpg', + `Album.bannerFileExtension #5: defaults to jpg`); +}); + +t.test(`Album.bannerStyle`, t => { + t.plan(4); + + const album = new Album(); + const {artist, contribs, badContribs} = stubArtistAndContribs(); + + linkAndBindWikiData({ + albumData: [album], + artistData: [artist], + }); + + t.equal(album.bannerStyle, null, + `Album.bannerStyle #1: defaults to null`); + + album.bannerStyle = `opacity: 0.5`; + + t.equal(album.bannerStyle, null, + `Album.bannerStyle #2: is null if bannerArtistContribs empty`); + + album.bannerArtistContribs = badContribs; + + t.equal(album.bannerStyle, null, + `Album.bannerStyle #3: is null if bannerArtistContribs resolves empty`); + + album.bannerArtistContribs = contribs; + + t.equal(album.bannerStyle, `opacity: 0.5`, + `Album.bannerStyle #4: is own value`); +}); + +t.test(`Album.coverArtDate`, t => { + t.plan(6); + + const album = new Album(); + const {artist, contribs, badContribs} = stubArtistAndContribs(); + + linkAndBindWikiData({ + albumData: [album], + artistData: [artist], + }); + + t.equal(album.coverArtDate, null, + `Album.coverArtDate #1: defaults to null`); + + album.date = new Date('2012-10-25'); + + t.equal(album.coverArtDate, null, + `Album.coverArtDate #2: is null if coverArtistContribs empty (1/2)`); + + album.coverArtDate = new Date('2011-04-13'); + + t.equal(album.coverArtDate, null, + `Album.coverArtDate #3: is null if coverArtistContribs empty (2/2)`); + + album.coverArtistContribs = contribs; + + t.same(album.coverArtDate, new Date('2011-04-13'), + `Album.coverArtDate #4: is own value`); + + album.coverArtDate = null; + + t.same(album.coverArtDate, new Date(`2012-10-25`), + `Album.coverArtDate #5: inherits album release date`); + + album.coverArtistContribs = badContribs; + + t.equal(album.coverArtDate, null, + `Album.coverArtDate #6: is null if coverArtistContribs resolves empty`); +}); + +t.test(`Album.coverArtFileExtension`, t => { + t.plan(5); + + const album = new Album(); + const {artist, contribs, badContribs} = stubArtistAndContribs(); + + linkAndBindWikiData({ + albumData: [album], + artistData: [artist], + }); + + t.equal(album.coverArtFileExtension, null, + `Album.coverArtFileExtension #1: is null if coverArtistContribs empty (1/2)`); + + album.coverArtFileExtension = 'png'; + + t.equal(album.coverArtFileExtension, null, + `Album.coverArtFileExtension #2: is null if coverArtistContribs empty (2/2)`); + + album.coverArtFileExtension = null; + album.coverArtistContribs = contribs; + + t.equal(album.coverArtFileExtension, 'jpg', + `Album.coverArtFileExtension #3: defaults to jpg`); + + album.coverArtFileExtension = 'png'; + + t.equal(album.coverArtFileExtension, 'png', + `Album.coverArtFileExtension #4: is own value`); + + album.coverArtistContribs = badContribs; + + t.equal(album.coverArtFileExtension, null, + `Album.coverArtFileExtension #5: is null if coverArtistContribs resolves empty`); +}); + +t.test(`Album.tracks`, t => { + t.plan(5); + + const album = new Album(); + const track1 = stubTrack('track1'); + const track2 = stubTrack('track2'); + const track3 = stubTrack('track3'); + const tracks = [track1, track2, track3]; + + album.ownTrackData = tracks; + + for (const track of tracks) { + track.albumData = [album]; + } + + t.same(album.tracks, [], + `Album.tracks #1: defaults to empty array`); + + album.trackSections = [ + {tracks: ['track:track1', 'track:track2', 'track:track3']}, + ]; + + 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']}, + ]; + + 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']}, + ]; + + 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']}, + ]; + + t.same(album.tracks, [track1, track2, track3], + `Album.tracks #5: skips missing tracks property`); +}); + +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; + + for (const track of tracks) { + track.albumData = [album]; + } + + album.trackSections = [ + {tracks: ['track:track1', 'track:track2']}, + {tracks: ['track:track3', 'track:track4']}, + ]; + + t.match(album.trackSections, [ + {tracks: [track1, track2]}, + {tracks: [track3, track4]}, + ], `Album.trackSections #1: exposes tracks`); + + t.match(album.trackSections, [ + {tracks: [track1, track2], startIndex: 0}, + {tracks: [track3, track4], startIndex: 2}, + ], `Album.trackSections #2: exposes startIndex`); + + album.trackSections = [ + {name: 'First section', tracks: ['track:track1']}, + {name: 'Second section', tracks: ['track:track2']}, + {tracks: ['track:track3']}, + ]; + + t.match(album.trackSections, [ + {name: 'First section', tracks: [track1]}, + {name: 'Second section', tracks: [track2]}, + {name: 'Unnamed Track Section', tracks: [track3]}, + ], `Album.trackSections #3: exposes name, with fallback value`); + + album.color = '#123456'; + + album.trackSections = [ + {tracks: ['track:track1'], color: null}, + {tracks: ['track:track2'], color: '#abcdef'}, + {tracks: ['track:track3'], color: null}, + ]; + + t.match(album.trackSections, [ + {tracks: [track1], color: '#123456'}, + {tracks: [track2], color: '#abcdef'}, + {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}, + ]; + + t.match(album.trackSections, [ + {tracks: [track1], dateOriginallyReleased: null}, + {tracks: [track2], dateOriginallyReleased: new Date('2009-04-11')}, + {tracks: [track3], dateOriginallyReleased: null}, + ], `Album.trackSections #5: exposes dateOriginallyReleased, if present`); + + album.trackSections = [ + {tracks: ['track:track1'], isDefaultTrackSection: true}, + {tracks: ['track:track2'], isDefaultTrackSection: false}, + {tracks: ['track:track3'], isDefaultTrackSection: null}, + ]; + + t.match(album.trackSections, [ + {tracks: [track1], isDefaultTrackSection: true}, + {tracks: [track2], isDefaultTrackSection: false}, + {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'}, + ]; + + t.match(album.trackSections, [ + {tracks: [track1, track2], color: '#112233'}, + {tracks: [track3], color: '#334455'}, + {tracks: [track4], color: '#778899'}, + ], `Album.trackSections #7: filters out references without matches & empty sections`); +}); + +t.test(`Album.wallpaperFileExtension`, t => { + t.plan(5); + + const album = new Album(); + const {artist, contribs, badContribs} = stubArtistAndContribs(); + + linkAndBindWikiData({ + albumData: [album], + artistData: [artist], + }); + + t.equal(album.wallpaperFileExtension, null, + `Album.wallpaperFileExtension #1: defaults to null`); + + album.wallpaperFileExtension = 'png'; + + t.equal(album.wallpaperFileExtension, null, + `Album.wallpaperFileExtension #2: is null if wallpaperArtistContribs empty`); + + album.wallpaperArtistContribs = contribs; + + t.equal(album.wallpaperFileExtension, 'png', + `Album.wallpaperFileExtension #3: is own value`); + + album.wallpaperFileExtension = null; + + t.equal(album.wallpaperFileExtension, 'jpg', + `Album.wallpaperFileExtension #4: defaults to jpg`); + + album.wallpaperArtistContribs = badContribs; + + t.equal(album.wallpaperFileExtension, null, + `Album.wallpaperFileExtension #5: is null if wallpaperArtistContribs resolves empty`); +}); + +t.test(`Album.wallpaperStyle`, t => { + t.plan(4); + + const album = new Album(); + const {artist, contribs, badContribs} = stubArtistAndContribs(); + + linkAndBindWikiData({ + albumData: [album], + artistData: [artist], + }); + + t.equal(album.wallpaperStyle, null, + `Album.wallpaperStyle #1: defaults to null`); + + album.wallpaperStyle = `opacity: 0.5`; + + t.equal(album.wallpaperStyle, null, + `Album.wallpaperStyle #2: is null if wallpaperArtistContribs empty`); + + album.wallpaperArtistContribs = badContribs; + + t.equal(album.wallpaperStyle, null, + `Album.wallpaperStyle #3: is null if wallpaperArtistContribs resolves empty`); + + album.wallpaperArtistContribs = contribs; + + t.equal(album.wallpaperStyle, `opacity: 0.5`, + `Album.wallpaperStyle #4: is own value`); +}); diff --git a/test/unit/data/things/art-tag.js b/test/unit/data/things/art-tag.js new file mode 100644 index 0000000..561c93e --- /dev/null +++ b/test/unit/data/things/art-tag.js @@ -0,0 +1,71 @@ +import t from 'tap'; + +import {linkAndBindWikiData} from '#test-lib'; +import thingConstructors from '#things'; + +const { + Album, + Artist, + ArtTag, + Track, +} = 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}]; + + return album; +} + +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 = [{who: artistName, what: null}]; + const badContribs = [{who: `Figment of Your Imagination`, what: null}]; + + return {artist, contribs, badContribs}; +} + +t.test(`ArtTag.nameShort`, t => { + t.plan(3); + + const artTag = new ArtTag(); + + artTag.name = `Dave Strider`; + + t.equal(artTag.nameShort, `Dave Strider`, + `ArtTag #1: defaults to name`); + + artTag.name = `Dave Strider (Homestuck)`; + + t.equal(artTag.nameShort, `Dave Strider`, + `ArtTag #2: trims parenthical part at end`); + + artTag.name = `This (And) That (Then)`; + + t.equal(artTag.nameShort, `This (And) That`, + `ArtTag #2: doesn't trim midlde parenthical part`); +}); diff --git a/test/unit/data/things/flash.js b/test/unit/data/things/flash.js new file mode 100644 index 0000000..6205960 --- /dev/null +++ b/test/unit/data/things/flash.js @@ -0,0 +1,55 @@ +import t from 'tap'; + +import {linkAndBindWikiData} from '#test-lib'; +import thingConstructors from '#things'; + +const { + Flash, + FlashAct, + Thing, +} = thingConstructors; + +function stubFlash(directory = 'foo') { + const flash = new Flash(); + flash.directory = directory; + + return flash; +} + +function stubFlashAct(flashes, directory = 'bar') { + const flashAct = new FlashAct(); + flashAct.directory = directory; + flashAct.flashes = flashes.map(flash => Thing.getReference(flash)); + + return flashAct; +} + +t.test(`Flash.color`, t => { + t.plan(4); + + const flash = stubFlash(); + const flashAct = stubFlashAct([flash]); + + const {XXX_decacheWikiData} = linkAndBindWikiData({ + flashData: [flash], + flashActData: [flashAct], + }); + + t.equal(flash.color, null, + `color #1: defaults to null`); + + flashAct.color = '#abcdef'; + XXX_decacheWikiData(); + + t.equal(flash.color, '#abcdef', + `color #2: inherits from flash act`); + + flash.color = '#123456'; + + t.equal(flash.color, '#123456', + `color #3: is own value`); + + t.throws(() => { flash.color = '#aeiouw'; }, + {cause: TypeError}, + `color #4: must be set to valid color`); +}); diff --git a/test/unit/data/things/track.js b/test/unit/data/things/track.js new file mode 100644 index 0000000..b1c1611 --- /dev/null +++ b/test/unit/data/things/track.js @@ -0,0 +1,759 @@ +import t from 'tap'; + +import {linkAndBindWikiData} from '#test-lib'; +import thingConstructors from '#things'; + +const { + Album, + ArtTag, + Artist, + Flash, + FlashAct, + Thing, + Track, +} = 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}]; + + return album; +} + +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 = [{who: artistName, what: null}]; + const badContribs = [{who: `Figment of Your Imagination`, what: null}]; + + return {artist, contribs, badContribs}; +} + +function stubArtTag(tagName = `Test Art Tag`) { + const tag = new ArtTag(); + tag.name = tagName; + + return tag; +} + +function stubFlashAndAct(directory = 'zam') { + const flash = new Flash(); + flash.directory = directory; + + const flashAct = new FlashAct(); + flashAct.flashes = [Thing.getReference(flash)]; + + return {flash, flashAct}; +} + +t.test(`Track.album`, t => { + t.plan(6); + + // Note: These asserts use manual albumData/trackData relationships + // to illustrate more specifically the properties which are expected to + // be relevant for this case. Other properties use the same underlying + // get-album behavior as Track.album so aren't tested as aggressively. + + const track1 = stubTrack('track1'); + const track2 = stubTrack('track2'); + const album1 = new Album(); + const album2 = new Album(); + + 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']}]; + + t.equal(track1.album, album1, + `album #2: is album when album's trackSections matches track`); + + track1.albumData = [album2, album1]; + + t.equal(track1.album, album1, + `album #3: is album when albumData is in different order`); + + track1.albumData = []; + + t.equal(track1.album, null, + `album #4: is null when track missing albumData`); + + album1.ownTrackData = []; + track1.albumData = [album1, album2]; + + t.equal(track1.album, null, + `album #5: is null when album missing ownTrackData`); + + album1.ownTrackData = [track1, track2]; + album1.trackSections = [{tracks: ['track:track2']}]; + + // XXX_decacheWikiData + track1.albumData = []; + track1.albumData = [album1, album2]; + + t.equal(track1.album, null, + `album #6: is null when album's trackSections don't match track`); +}); + +t.test(`Track.artTags`, t => { + t.plan(6); + + const {track, album} = stubTrackAndAlbum(); + const {artist, contribs} = stubArtistAndContribs(); + const tag1 = stubArtTag(`Tag 1`); + const tag2 = stubArtTag(`Tag 2`); + + const {XXX_decacheWikiData} = linkAndBindWikiData({ + albumData: [album], + artistData: [artist], + artTagData: [tag1, tag2], + trackData: [track], + }); + + t.same(track.artTags, [], + `artTags #1: defaults to empty array`); + + track.artTags = [`Tag 1`, `Tag 2`]; + + t.same(track.artTags, [], + `artTags #2: is empty if track doesn't have cover artists`); + + track.coverArtistContribs = contribs; + + t.same(track.artTags, [tag1, tag2], + `artTags #3: resolves if track has cover artists`); + + track.coverArtistContribs = null; + album.trackCoverArtistContribs = contribs; + + XXX_decacheWikiData(); + + t.same(track.artTags, [tag1, tag2], + `artTags #4: resolves if track inherits cover artists`); + + track.disableUniqueCoverArt = true; + + t.same(track.artTags, [], + `artTags #5: is empty if track disables unique cover artwork`); + + album.coverArtistContribs = contribs; + album.artTags = [`Tag 2`]; + + XXX_decacheWikiData(); + + t.notSame(track.artTags, [tag2], + `artTags #6: doesn't inherit from album's art tags`); +}); + +t.test(`Track.artistContribs`, t => { + t.plan(4); + + const {track, album} = stubTrackAndAlbum(); + const artist1 = stubArtist(`Artist 1`); + const artist2 = stubArtist(`Artist 2`); + + const {XXX_decacheWikiData} = linkAndBindWikiData({ + albumData: [album], + artistData: [artist1, artist2], + trackData: [track], + }); + + t.same(track.artistContribs, [], + `artistContribs #1: defaults to empty array`); + + album.artistContribs = [ + {who: `Artist 1`, what: `composition`}, + {who: `Artist 2`, what: null}, + ]; + + XXX_decacheWikiData(); + + t.same(track.artistContribs, + [{who: artist1, what: `composition`}, {who: artist2, what: null}], + `artistContribs #2: inherits album artistContribs`); + + track.artistContribs = [ + {who: `Artist 1`, what: `arrangement`}, + ]; + + t.same(track.artistContribs, [{who: artist1, what: `arrangement`}], + `artistContribs #3: resolves from own value`); + + track.artistContribs = [ + {who: `Artist 1`, what: `snooping`}, + {who: `Artist 413`, what: `as`}, + {who: `Artist 2`, what: `usual`}, + ]; + + t.same(track.artistContribs, + [{who: artist1, what: `snooping`}, {who: artist2, what: `usual`}], + `artistContribs #4: filters out names without matches`); +}); + +t.test(`Track.color`, t => { + t.plan(5); + + const {track, album} = stubTrackAndAlbum(); + + const {XXX_decacheWikiData} = linkAndBindWikiData({ + albumData: [album], + trackData: [track], + }); + + t.equal(track.color, null, + `color #1: defaults to null`); + + album.color = '#abcdef'; + album.trackSections = [{ + color: '#beeeef', + tracks: [Thing.getReference(track)], + }]; + XXX_decacheWikiData(); + + t.equal(track.color, '#beeeef', + `color #2: inherits from track section before album`); + + // Replace the album with a completely fake one. This isn't realistic, since + // in correct data, Album.tracks depends on Albums.trackSections and so the + // track's album will always have a corresponding track section. But if that + // connection breaks for some future reason (with the album still present), + // Track.color should still inherit directly from the album. + track.albumData = [ + { + constructor: {[Thing.referenceType]: 'album'}, + color: '#abcdef', + tracks: [track], + trackSections: [ + {color: '#baaaad', tracks: []}, + ], + }, + ]; + + t.equal(track.color, '#abcdef', + `color #3: inherits from album without matching track section`); + + track.color = '#123456'; + + t.equal(track.color, '#123456', + `color #4: is own value`); + + t.throws(() => { track.color = '#aeiouw'; }, + {cause: TypeError}, + `color #5: must be set to valid color`); +}); + +t.test(`Track.commentatorArtists`, t => { + t.plan(8); + + const track = new Track(); + const artist1 = stubArtist(`SnooPING`); + const artist2 = stubArtist(`ASUsual`); + const artist3 = stubArtist(`Icy`); + + linkAndBindWikiData({ + trackData: [track], + artistData: [artist1, artist2, artist3], + }); + + // Keep track of the last commentary string in a separate value, since + // the track.commentary property exposes as a completely different format + // (i.e. an array of objects, one for each entry), and so isn't compatible + // with the += operator on its own. + let commentary; + + track.commentary = commentary = + `<i>SnooPING:</i>\n` + + `Wow.\n`; + + t.same(track.commentatorArtists, [artist1], + `Track.commentatorArtists #1: works with one commentator`); + + track.commentary = commentary += + `<i>ASUsual:</i>\n` + + `Yes!\n`; + + t.same(track.commentatorArtists, [artist1, artist2], + `Track.commentatorArtists #2: works with two commentators`); + + track.commentary = commentary += + `<i>Icy|<b>Icy What You Did There</b>:</i>\n` + + `Incredible.\n`; + + t.same(track.commentatorArtists, [artist1, artist2, artist3], + `Track.commentatorArtists #3: works with custom artist text`); + + track.commentary = commentary = + `<i>Icy:</i> (project manager)\n` + + `Very good track.\n`; + + t.same(track.commentatorArtists, [artist3], + `Track.commentatorArtists #4: works with annotation`); + + track.commentary = commentary = + `<i>Icy:</i> (project manager, 08/15/2023)\n` + + `Very very good track.\n`; + + t.same(track.commentatorArtists, [artist3], + `Track.commentatorArtists #5: works with date`); + + track.commentary = commentary += + `<i>Ohohohoho:</i>\n` + + `OHOHOHOHOHOHO...\n`; + + t.same(track.commentatorArtists, [artist3], + `Track.commentatorArtists #6: ignores artist names not found`); + + track.commentary = commentary += + `<i>Icy:</i>\n` + + `I'm back!\n`; + + t.same(track.commentatorArtists, [artist3], + `Track.commentatorArtists #7: ignores duplicate artist`); + + track.commentary = commentary += + `<i>SNooPING, ASUsual, Icy:</i>\n` + + `WITH ALL THREE POWERS COMBINED...`; + + t.same(track.commentatorArtists, [artist3, artist1, artist2], + `Track.commentatorArtists #8: works with more than one artist in one entry`); +}); + +t.test(`Track.coverArtistContribs`, t => { + t.plan(5); + + const {track, album} = stubTrackAndAlbum(); + const artist1 = stubArtist(`Artist 1`); + const artist2 = stubArtist(`Artist 2`); + + const {XXX_decacheWikiData} = linkAndBindWikiData({ + albumData: [album], + artistData: [artist1, artist2], + trackData: [track], + }); + + t.same(track.coverArtistContribs, [], + `coverArtistContribs #1: defaults to empty array`); + + album.trackCoverArtistContribs = [ + {who: `Artist 1`, what: `lines`}, + {who: `Artist 2`, what: null}, + ]; + + XXX_decacheWikiData(); + + t.same(track.coverArtistContribs, + [{who: artist1, what: `lines`}, {who: artist2, what: null}], + `coverArtistContribs #2: inherits album trackCoverArtistContribs`); + + track.coverArtistContribs = [ + {who: `Artist 1`, what: `collage`}, + ]; + + t.same(track.coverArtistContribs, [{who: artist1, what: `collage`}], + `coverArtistContribs #3: resolves from own value`); + + track.coverArtistContribs = [ + {who: `Artist 1`, what: `snooping`}, + {who: `Artist 413`, what: `as`}, + {who: `Artist 2`, what: `usual`}, + ]; + + t.same(track.coverArtistContribs, + [{who: artist1, what: `snooping`}, {who: artist2, what: `usual`}], + `coverArtistContribs #4: filters out names without matches`); + + track.disableUniqueCoverArt = true; + + t.same(track.coverArtistContribs, [], + `coverArtistContribs #5: is empty if track disables unique cover artwork`); +}); + +t.test(`Track.coverArtDate`, t => { + t.plan(8); + + const {track, album} = stubTrackAndAlbum(); + const {artist, contribs, badContribs} = stubArtistAndContribs(); + + const {XXX_decacheWikiData} = linkAndBindWikiData({ + albumData: [album], + artistData: [artist], + trackData: [track], + }); + + track.coverArtistContribs = contribs; + + t.equal(track.coverArtDate, null, + `coverArtDate #1: defaults to null`); + + album.trackArtDate = new Date('2012-12-12'); + + XXX_decacheWikiData(); + + t.same(track.coverArtDate, new Date('2012-12-12'), + `coverArtDate #2: inherits album trackArtDate`); + + track.coverArtDate = new Date('2009-09-09'); + + t.same(track.coverArtDate, new Date('2009-09-09'), + `coverArtDate #3: is own value`); + + track.coverArtistContribs = []; + + t.equal(track.coverArtDate, null, + `coverArtDate #4: is null if track coverArtistContribs empty`); + + album.trackCoverArtistContribs = contribs; + + XXX_decacheWikiData(); + + t.same(track.coverArtDate, new Date('2009-09-09'), + `coverArtDate #5: is not null if album trackCoverArtistContribs specified`); + + album.trackCoverArtistContribs = badContribs; + + XXX_decacheWikiData(); + + t.equal(track.coverArtDate, null, + `coverArtDate #6: is null if album trackCoverArtistContribs resolves empty`); + + track.coverArtistContribs = badContribs; + + t.equal(track.coverArtDate, null, + `coverArtDate #7: is null if track coverArtistContribs resolves empty`); + + track.coverArtistContribs = contribs; + track.disableUniqueCoverArt = true; + + t.equal(track.coverArtDate, null, + `coverArtDate #8: is null if track disables unique cover artwork`); +}); + +t.test(`Track.coverArtFileExtension`, t => { + t.plan(8); + + const {track, album} = stubTrackAndAlbum(); + const {artist, contribs} = stubArtistAndContribs(); + + const {XXX_decacheWikiData} = linkAndBindWikiData({ + albumData: [album], + artistData: [artist], + trackData: [track], + }); + + t.equal(track.coverArtFileExtension, null, + `coverArtFileExtension #1: defaults to null`); + + track.coverArtistContribs = contribs; + + t.equal(track.coverArtFileExtension, 'jpg', + `coverArtFileExtension #2: is jpg if has cover art and not further specified`); + + track.coverArtistContribs = []; + + album.coverArtistContribs = contribs; + XXX_decacheWikiData(); + + t.equal(track.coverArtFileExtension, null, + `coverArtFileExtension #3: only has value for unique cover art`); + + track.coverArtistContribs = contribs; + + album.trackCoverArtFileExtension = 'png'; + XXX_decacheWikiData(); + + t.equal(track.coverArtFileExtension, 'png', + `coverArtFileExtension #4: inherits album trackCoverArtFileExtension (1/2)`); + + track.coverArtFileExtension = 'gif'; + + t.equal(track.coverArtFileExtension, 'gif', + `coverArtFileExtension #5: is own value (1/2)`); + + track.coverArtistContribs = []; + + album.trackCoverArtistContribs = contribs; + XXX_decacheWikiData(); + + t.equal(track.coverArtFileExtension, 'gif', + `coverArtFileExtension #6: is own value (2/2)`); + + track.coverArtFileExtension = null; + + t.equal(track.coverArtFileExtension, 'png', + `coverArtFileExtension #7: inherits album trackCoverArtFileExtension (2/2)`); + + track.disableUniqueCoverArt = true; + + t.equal(track.coverArtFileExtension, null, + `coverArtFileExtension #8: is null if track disables unique cover art`); +}); + +t.test(`Track.date`, t => { + t.plan(3); + + const {track, album} = stubTrackAndAlbum(); + + const {XXX_decacheWikiData} = linkAndBindWikiData({ + albumData: [album], + trackData: [track], + }); + + t.equal(track.date, null, + `date #1: defaults to null`); + + album.date = new Date('2012-12-12'); + XXX_decacheWikiData(); + + t.same(track.date, album.date, + `date #2: inherits from album`); + + track.dateFirstReleased = new Date('2009-09-09'); + + t.same(track.date, new Date('2009-09-09'), + `date #3: is own dateFirstReleased`); +}); + +t.test(`Track.featuredInFlashes`, t => { + t.plan(2); + + const {track, album} = stubTrackAndAlbum('track1'); + + const {flash: flash1, flashAct: flashAct1} = stubFlashAndAct('flash1'); + const {flash: flash2, flashAct: flashAct2} = stubFlashAndAct('flash2'); + + const {XXX_decacheWikiData} = linkAndBindWikiData({ + albumData: [album], + trackData: [track], + flashData: [flash1, flash2], + flashActData: [flashAct1, flashAct2], + }); + + t.same(track.featuredInFlashes, [], + `featuredInFlashes #1: defaults to empty array`); + + flash1.featuredTracks = ['track:track1']; + flash2.featuredTracks = ['track:track1']; + XXX_decacheWikiData(); + + t.same(track.featuredInFlashes, [flash1, flash2], + `featuredInFlashes #2: matches flashes' featuredTracks`); +}); + +t.test(`Track.hasUniqueCoverArt`, t => { + t.plan(7); + + const {track, album} = stubTrackAndAlbum(); + const {artist, contribs, badContribs} = stubArtistAndContribs(); + + const {XXX_decacheWikiData} = linkAndBindWikiData({ + albumData: [album], + artistData: [artist], + trackData: [track], + }); + + t.equal(track.hasUniqueCoverArt, false, + `hasUniqueCoverArt #1: defaults to false`); + + album.trackCoverArtistContribs = contribs; + XXX_decacheWikiData(); + + t.equal(track.hasUniqueCoverArt, true, + `hasUniqueCoverArt #2: is true if album specifies trackCoverArtistContribs`); + + track.disableUniqueCoverArt = true; + + t.equal(track.hasUniqueCoverArt, false, + `hasUniqueCoverArt #3: is false if disableUniqueCoverArt is true (1/2)`); + + track.disableUniqueCoverArt = false; + + album.trackCoverArtistContribs = badContribs; + XXX_decacheWikiData(); + + t.equal(track.hasUniqueCoverArt, false, + `hasUniqueCoverArt #4: is false if album's trackCoverArtistContribs resolve empty`); + + track.coverArtistContribs = contribs; + + t.equal(track.hasUniqueCoverArt, true, + `hasUniqueCoverArt #5: is true if track specifies coverArtistContribs`); + + track.disableUniqueCoverArt = true; + + t.equal(track.hasUniqueCoverArt, false, + `hasUniqueCoverArt #6: is false if disableUniqueCoverArt is true (2/2)`); + + track.disableUniqueCoverArt = false; + + track.coverArtistContribs = badContribs; + + t.equal(track.hasUniqueCoverArt, false, + `hasUniqueCoverArt #7: is false if track's coverArtistContribs resolve empty`); +}); + +t.test(`Track.originalReleaseTrack`, t => { + t.plan(3); + + const {track: track1, album: album1} = stubTrackAndAlbum('track1'); + const {track: track2, album: album2} = stubTrackAndAlbum('track2'); + + const {wikiData, linkWikiDataArrays, XXX_decacheWikiData} = linkAndBindWikiData({ + albumData: [album1, album2], + trackData: [track1, track2], + }); + + t.equal(track2.originalReleaseTrack, null, + `originalReleaseTrack #1: defaults to null`); + + track2.originalReleaseTrack = 'track:track1'; + + t.equal(track2.originalReleaseTrack, track1, + `originalReleaseTrack #2: is resolved from own value`); + + track2.trackData = []; + + t.equal(track2.originalReleaseTrack, null, + `originalReleaseTrack #3: is null when track missing trackData`); +}); + +t.test(`Track.otherReleases`, t => { + t.plan(6); + + const {track: track1, album: album1} = stubTrackAndAlbum('track1'); + const {track: track2, album: album2} = stubTrackAndAlbum('track2'); + const {track: track3, album: album3} = stubTrackAndAlbum('track3'); + const {track: track4, album: album4} = stubTrackAndAlbum('track4'); + + const {wikiData, linkWikiDataArrays, XXX_decacheWikiData} = linkAndBindWikiData({ + albumData: [album1, album2, album3, album4], + trackData: [track1, track2, track3, track4], + }); + + t.same(track1.otherReleases, [], + `otherReleases #1: defaults to empty array`); + + track2.originalReleaseTrack = 'track:track1'; + track3.originalReleaseTrack = 'track:track1'; + track4.originalReleaseTrack = 'track:track1'; + XXX_decacheWikiData(); + + t.same(track1.otherReleases, [track2, track3, track4], + `otherReleases #2: otherReleases of original release are its rereleases`); + + wikiData.trackData = [track1, track3, track2, track4]; + linkWikiDataArrays(); + + t.same(track1.otherReleases, [track3, track2, track4], + `otherReleases #3: otherReleases matches trackData order`); + + wikiData.trackData = [track3, track2, track1, track4]; + linkWikiDataArrays(); + + t.same(track2.otherReleases, [track1, track3, track4], + `otherReleases #4: otherReleases of rerelease are original track then other rereleases (1/3)`); + + t.same(track3.otherReleases, [track1, track2, track4], + `otherReleases #5: otherReleases of rerelease are original track then other rereleases (2/3)`); + + t.same(track4.otherReleases, [track1, track3, track2], + `otherReleases #6: otherReleases of rerelease are original track then other rereleases (3/3)`); +}); + +t.test(`Track.referencedByTracks`, t => { + t.plan(4); + + const {track: track1, album: album1} = stubTrackAndAlbum('track1'); + const {track: track2, album: album2} = stubTrackAndAlbum('track2'); + const {track: track3, album: album3} = stubTrackAndAlbum('track3'); + const {track: track4, album: album4} = stubTrackAndAlbum('track4'); + + const {XXX_decacheWikiData} = linkAndBindWikiData({ + albumData: [album1, album2, album3, album4], + trackData: [track1, track2, track3, track4], + }); + + t.same(track1.referencedByTracks, [], + `referencedByTracks #1: defaults to empty array`); + + track2.referencedTracks = ['track:track1']; + track3.referencedTracks = ['track:track1']; + XXX_decacheWikiData(); + + t.same(track1.referencedByTracks, [track2, track3], + `referencedByTracks #2: matches tracks' referencedTracks`); + + track4.sampledTracks = ['track:track1']; + XXX_decacheWikiData(); + + t.same(track1.referencedByTracks, [track2, track3], + `referencedByTracks #3: doesn't match tracks' sampledTracks`); + + track3.originalReleaseTrack = 'track:track2'; + XXX_decacheWikiData(); + + t.same(track1.referencedByTracks, [track2], + `referencedByTracks #4: doesn't include rereleases`); +}); + +t.test(`Track.sampledByTracks`, t => { + t.plan(4); + + const {track: track1, album: album1} = stubTrackAndAlbum('track1'); + const {track: track2, album: album2} = stubTrackAndAlbum('track2'); + const {track: track3, album: album3} = stubTrackAndAlbum('track3'); + const {track: track4, album: album4} = stubTrackAndAlbum('track4'); + + const {XXX_decacheWikiData} = linkAndBindWikiData({ + albumData: [album1, album2, album3, album4], + trackData: [track1, track2, track3, track4], + }); + + t.same(track1.sampledByTracks, [], + `sampledByTracks #1: defaults to empty array`); + + track2.sampledTracks = ['track:track1']; + track3.sampledTracks = ['track:track1']; + XXX_decacheWikiData(); + + t.same(track1.sampledByTracks, [track2, track3], + `sampledByTracks #2: matches tracks' sampledTracks`); + + track4.referencedTracks = ['track:track1']; + XXX_decacheWikiData(); + + t.same(track1.sampledByTracks, [track2, track3], + `sampledByTracks #3: doesn't match tracks' referencedTracks`); + + track3.originalReleaseTrack = 'track:track2'; + XXX_decacheWikiData(); + + t.same(track1.sampledByTracks, [track2], + `sampledByTracks #4: doesn't include rereleases`); +}); diff --git a/test/unit/data/things/validators.js b/test/unit/data/things/validators.js new file mode 100644 index 0000000..11134a9 --- /dev/null +++ b/test/unit/data/things/validators.js @@ -0,0 +1,440 @@ +import t from 'tap'; +import {showAggregate} from '#sugar'; + +import { + // Basic types + isBoolean, + isCountingNumber, + isDate, + isNumber, + isString, + isStringNonEmpty, + + // Complex types + isArray, + isObject, + validateArrayItems, + + // Wiki data + isColor, + isCommentary, + isContentString, + isContribution, + isContributionList, + isDimensions, + isDirectory, + isDuration, + isFileExtension, + isName, + isURL, + validateReference, + validateReferenceList, + + // Compositional utilities + anyOf, +} from '#validators'; + +function test(t, msg, fn) { + t.test(msg, t => { + try { + fn(t); + } catch (error) { + if (error instanceof AggregateError) { + showAggregate(error); + } + throw error; + } + }); +} + +// Basic types + +test(t, 'isBoolean', t => { + t.plan(4); + t.ok(isBoolean(true)); + t.ok(isBoolean(false)); + t.throws(() => isBoolean(1), TypeError); + t.throws(() => isBoolean('yes'), TypeError); +}); + +test(t, 'isNumber', t => { + t.plan(6); + t.ok(isNumber(123)); + t.ok(isNumber(0.05)); + t.ok(isNumber(0)); + t.ok(isNumber(-10)); + t.throws(() => isNumber('413'), TypeError); + t.throws(() => isNumber(true), TypeError); +}); + +test(t, 'isCountingNumber', t => { + t.plan(6); + t.ok(isCountingNumber(3)); + t.ok(isCountingNumber(1)); + t.throws(() => isCountingNumber(1.75), TypeError); + t.throws(() => isCountingNumber(0), TypeError); + t.throws(() => isCountingNumber(-1), TypeError); + t.throws(() => isCountingNumber('612'), TypeError); +}); + +test(t, 'isString', t => { + t.plan(3); + t.ok(isString('hello!')); + t.ok(isString('')); + t.throws(() => isString(100), TypeError); +}); + +test(t, 'isStringNonEmpty', t => { + t.plan(4); + t.ok(isStringNonEmpty('hello!')); + t.throws(() => isStringNonEmpty(''), TypeError); + t.throws(() => isStringNonEmpty(' '), TypeError); + t.throws(() => isStringNonEmpty(100), TypeError); +}); + +// Complex types + +test(t, 'isArray', t => { + t.plan(3); + t.ok(isArray([])); + t.throws(() => isArray({}), TypeError); + t.throws(() => isArray('1, 2, 3'), TypeError); +}); + +test(t, 'isDate', t => { + t.plan(3); + t.ok(isDate(new Date('2023-03-27 09:24:15'))); + t.throws(() => isDate(new Date(Infinity)), TypeError); + t.throws(() => isDimensions('2023-03-27 09:24:15'), TypeError); +}); + +test(t, 'isObject', t => { + t.plan(3); + t.ok(isObject({})); + t.ok(isObject([])); + t.throws(() => isObject(null), TypeError); +}); + +test(t, 'validateArrayItems', t => { + t.plan(9); + + t.ok(validateArrayItems(isNumber)([3, 4, 5])); + t.ok(validateArrayItems(validateArrayItems(isNumber))([[3, 4], [4, 5], [6, 7]])); + + let caughtError = null; + try { + validateArrayItems(isNumber)([10, 20, 'one hundred million consorts', 30]); + } catch (err) { + caughtError = err; + } + + t.not(caughtError, null); + t.ok(caughtError instanceof AggregateError); + t.equal(caughtError.errors.length, 1); + t.ok(caughtError.errors[0] instanceof Error); + t.equal(caughtError.errors[0][Symbol.for('hsmusic.annotateError.indexInSourceArray')], 2); + t.not(caughtError.errors[0].cause, null); + t.ok(caughtError.errors[0].cause instanceof TypeError); +}); + +// Wiki data + +t.test('isColor', t => { + t.plan(9); + t.ok(isColor('#123')); + t.ok(isColor('#1234')); + t.ok(isColor('#112233')); + t.ok(isColor('#11223344')); + t.ok(isColor('#abcdef00')); + t.ok(isColor('#ABCDEF')); + t.throws(() => isColor('#ggg'), TypeError); + t.throws(() => isColor('red'), TypeError); + t.throws(() => isColor('hsl(150deg 30% 60%)'), TypeError); +}); + +t.test('isCommentary', t => { + t.plan(9); + + // TODO: Test specific error messages. + t.ok(isCommentary(`<i>Toby Fox:</i>\ndogsong.mp3`)); + t.ok(isCommentary(`<i>Toby Fox:</i> (music)\ndogsong.mp3`)); + t.throws(() => isCommentary(`dogsong.mp3\n<i>Toby Fox:</i>\ndogsong.mp3`)); + t.throws(() => isCommentary(`<i>Toby Fox:</i> dogsong.mp3`)); + t.throws(() => isCommentary(`<i>Toby Fox:</i> (music) dogsong.mp3`)); + t.throws(() => isCommentary(`<i>I Have Nothing To Say:</i>`)); + t.throws(() => isCommentary(123)); + t.throws(() => isCommentary(``)); + t.throws(() => isCommentary(`Technically, ah, er:</i>\nCorrect`)); +}); + +t.test('isContentString', t => { + t.plan(12); + + t.ok(isContentString(`Hello, world!`)); + t.ok(isContentString(`Hello...\nWorld!`)); + + const quickThrows = (string, description) => + t.throws(() => isContentString(string), description); + + quickThrows( + `Snooping\xa0as usual, I\xa0\xa0\xa0SEE.`, + Object.assign( + new AggregateError([ + new AggregateError([ + new TypeError(`Replace "\xa0" (non-breaking space) with " " (normal space) between "ing" and "as " (pos: 9)`), + new TypeError(`Replace "\xa0\xa0\xa0" (non-breaking space) with " " (normal space) between ", I" and "SEE" (pos: 21)`), + ], `Illegal characters found in content string`), + ], `Errors validating content string`), + {[Symbol.for(`hsmusic.aggregate.translucent`)]: 'single'})); + + quickThrows( + `Oh\u200bdear,\n` + + `Oh dear,\n` + + `oh-dear-oh-dear-oh\u200bdear.`, + new AggregateError([ + new AggregateError([ + new TypeError(`Delete "\u200b" (zero-width space) between "Oh" and "dea" (line: 1, col: 3)`), + new TypeError(`Delete "\u200b" (zero-width space) between "-oh" and "dea" (line: 3, col: 19)`), + ]), + ])); + + quickThrows( + `Well the days start comin'\xa0\xa0\xa0\xa0\u200b\u200b\xa0\xa0\xa0\u200b\u200b\u200band they don't stop comin'`, + new AggregateError([ + new AggregateError([ + new TypeError(`Replace "\xa0\xa0\xa0\xa0" (non-breaking space) with " " (normal space) after "in'" (pos: 27)`), + new TypeError(`Delete "\u200b\u200b" (zero-width space) (pos: 31)`), + new TypeError(`Replace "\xa0\xa0\xa0" (non-breaking space) with " " (normal space) (pos: 33)`), + new TypeError(`Delete "\u200b\u200b\u200b" (zero-width space) before "and" (pos: 36)`), + ]), + ])); + + quickThrows( + `It's go-\u200bin',\n` + + `\u200bIt's goin',\u200b\n` + + `\u200b\u200bIt's going!`, + new AggregateError([ + new AggregateError([ + new TypeError(`Delete "\u200b" (zero-width space) between "go-" and "in'" (line: 1, col: 9)`), + new TypeError(`Delete "\u200b" (zero-width space) before "It'" (line: 2, col: 1)`), + new TypeError(`Delete "\u200b" (zero-width space) after "n'," (line: 2, col: 13)`), + new TypeError(`Delete "\u200b\u200b" (zero-width space) before "It'" (line: 3, col: 1)`), + ]), + ])); + + quickThrows( + ` Room at the start.`, + new AggregateError([ + new AggregateError([ + new TypeError(`Matched " " at start`), + ], `Whitespace found at start or end`), + ])); + + quickThrows( + `Room at the end. `, + new AggregateError([ + new AggregateError([ + new TypeError(`Matched " " at end`), + ], `Whitespace found at start or end`), + ])); + + quickThrows( + ` Room on both sides. `, + new AggregateError([ + new AggregateError([ + new TypeError(`Matched " " at start`), + new TypeError(`Matched " " at end`), + ], `Whitespace found at start or end`), + ])); + + quickThrows( + `We're going multiline! \n` + + `That we are, aye. \n` + + ` \n`, + `Yessir.`, + new AggregateError([ + new AggregateError([ + new TypeError(`Matched " " at end of line 1`), + new TypeError(`Matched " " at end of line 2`), + new TypeError(`Matched " " as all of line 3`), + ], `Whitespace found at end of line`), + ])); + + t.doesNotThrow(() => + isContentString( + `It's cool.\n` + + ` It's cool.\n` + + ` It's cool.\n` + + ` It's so cool.`)); + + t.doesNotThrow(() => + isContentString( + `\n` + + `\n` + + `It's okay for\n` + + `blank lines\n` + + `\n` + + `just about anywhere.\n` + + ``)); +}); + +t.test('isContribution', t => { + t.plan(4); + t.ok(isContribution({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.test('isContributionList', t => { + t.plan(4); + t.ok(isContributionList([{who: 'Beavis'}, {who: 'Butthead', what: '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); +}); |