From 89882b486308613456fca7eeb24ee578efa7ce0b Mon Sep 17 00:00:00 2001 From: Logan Date: Tue, 18 Aug 2026 21:18:36 -0400 Subject: [PATCH] refactor(ui): give the icons one vocabulary and sweep the call sites `plus` meant "add to the queue", "add to a playlist", "make a new playlist" and "you do not own this" -- the first two adjacent in the same context menu, so two neighbouring items were the same glyph doing different things. `list` meant the queue (the button that opens it), the Playlists destination, and adding to the queue in `queue-panel` alone. Two icons carrying seven meanings is not a vocabulary, and nothing catches it: a wrong-but-real icon renders perfectly. `utils/icon-language.ts` is the table, beside `library-status.ts` as the issue suggested. The rule it is built on is that an icon names the **noun** it acts on, not the verb: "add to queue" and "add to playlist" are one verb on two nouns, so the noun is what differs -- which is why adding to a playlist wears the Playlists destination's own icon, and why the queue took `bars-staggered` and stopped wearing Playlists'. `plus` keeps the one meaning it is unambiguous about, making something that is not there yet, which covers New Playlist and the drop zones. `bars-staggered` is the only new glyph, vendored through names.txt and fetch-icons.mjs after confirming it is in Font Awesome **Free** 7.3.1. Two things this found rather than changed: - The request toggle's outline/solid pair was already in the app and already right -- `explore-album-details`'s "Request this" button has used `regular/bookmark` -> `solid/bookmark` since it was written -- while the badge forty pixels away showed a **plus** for the same state. That is `utils/library-status.ts`'s fault one layer down: it made the two surfaces agree on what wanting *means* and left them disagreeing on what it looks like. - `explore-artist-details`'s Follow button was `bookmark-check`, which is Font Awesome **Pro** and has never been bundled, so it has drawn the missing-icon fallback -- a circled question mark -- for every followed artist since it was written. `requested-badge.spec.ts` was written for exactly this bug on the album button and says so in its docstring; this is the same bug one component over, still live, because `offline-icons.spec.ts` sweeps `__yjIconMisses` and no spec had ever followed an artist. So the test does what reaching the state cannot. `icon-language.test.ts` reads every `src/**/*.ts` as raw text and fails on a governed name written outside the table, and separately asserts every `ICON_*` is a *bundled* name -- which is what makes a Pro name a failing test rather than a runtime report from a state something has to reach first. Its first assertion is that it read any source at all, because a sweep over an empty glob passes. `chrome.test.ts` asserted `['check', 'bookmark', 'plus']` and so pinned the badge's glyphs against the vocabulary they were meant to follow; it names them from the table now, and keeps the assertion that the three differ, which is the property the states actually need. Downloads keeps the solid bookmark on purpose. That is one word twice, not two words: the badge says the entity is on your list and the nav item is that list. Closes #34 --- CLAUDE.md | 46 ++++++ frontend/index.html | 5 +- .../assets/icons/fa/solid/bars-staggered.svg | 1 + .../components/artists-view/artists-view.ts | 8 +- .../src/components/bottom-nav/bottom-nav.ts | 3 +- .../src/components/cover-grid/cover-grid.ts | 8 +- .../explore-album-details.ts | 14 +- .../explore-artist-details.ts | 22 ++- .../components/explore-view/explore-view.ts | 3 +- .../src/components/genres-view/genres-view.ts | 8 +- .../library-status-indicator.ts | 24 ++- .../now-playing-view/now-playing-view.ts | 3 +- .../playlist-details/playlist-details.ts | 10 +- .../playlist-picker/playlist-picker.ts | 3 +- .../components/playlist-view/playlist-view.ts | 14 +- .../src/components/queue-panel/queue-panel.ts | 13 +- .../src/components/sidebar/app-sidebar.ts | 8 +- .../smart-playlist-details.ts | 8 +- .../src/components/track-list/track-list.ts | 8 +- frontend/src/icons/names.txt | 1 + frontend/src/utils/icon-language.ts | 100 +++++++++++++ frontend/test/components/chrome.test.ts | 21 ++- .../test/components/icon-language.test.ts | 140 ++++++++++++++++++ 23 files changed, 425 insertions(+), 46 deletions(-) create mode 100644 frontend/src/assets/icons/fa/solid/bars-staggered.svg create mode 100644 frontend/src/utils/icon-language.ts create mode 100644 frontend/test/components/icon-language.test.ts diff --git a/CLAUDE.md b/CLAUDE.md index 0e603b7..a9a1f1a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1448,6 +1448,52 @@ is therefore **reported at runtime** to `window.__yjIconMisses` and drawn as a fallback — an e2e sweep asserts there are none — since a missing icon used to be impossible, the CDN having had everything. +**What each icon *means* is a second table, and it is +`utils/icon-language.ts`.** Bundling answers "does this name resolve"; +nothing answered "does this name mean what the one next to it means", +and a wrong-but-real icon renders perfectly. So `plus` came to mean add +to the queue, add to a playlist, make a new playlist **and** you do not +own this — the first two *adjacent in the same context menu* — while +`list` meant the queue, the Playlists destination and adding to the +queue. + +The rule the table is built on: **an icon names the noun it acts on, +not the verb.** "Add to queue" and "add to playlist" are one verb on +two nouns, so the noun is what has to differ — which is why adding to a +playlist wears the Playlists destination's own icon, and why the queue +got `bars-staggered` and stopped wearing Playlists'. `plus` keeps the +one meaning it is unambiguous about, making something that is not there +yet. + +Four things about it are load-bearing: + +- **The request toggle is one glyph in two weights** + (`regular/bookmark` → `solid/bookmark`), because two states of a + toggle have to read as each other's opposite and a plus against a + bookmark does not. The pair was *already in the app and already + right* on `explore-album-details`'s "Request this" button while the + badge forty pixels away showed a plus — `utils/library-status.ts`'s + fault one layer down, having made the two agree on what wanting means + and left them disagreeing on what it looks like. +- **Downloads keeps the solid bookmark, deliberately.** That is the + same word twice, not two words: the badge says "this is on your + list" and the nav item is that list. +- **`icon-language.test.ts` sweeps the source**, because the rule is + about every call site and checking one checks nothing — the same + shape as `TestNoDirectRuntimeEmits`. It reads every `src/**/*.ts` as + raw text and fails on a literal `name="plus"` or `icon: 'list'` + outside the table, and its **first assertion is that it read + anything at all**, since a sweep over an empty glob passes. +- **It also asserts every `ICON_*` is bundled**, which closes the loop + the runtime cannot: `bookmark-check` is Font Awesome **Pro** and sat + on `explore-artist-details`'s Follow button, drawn for every followed + artist as a circled question mark. `offline-icons.spec.ts` sweeps + `__yjIconMisses` and could not see it, because no spec had ever + followed an artist — the same fault `requested-badge.spec.ts` was + written for, one component over, still live. A name computed from + state was only checkable from the state; now it is checkable from the + table. + **An album page says how much of the album is yours.** `explore-album-details` is a *catalog* page and there is no library-side album detail page at all, so the album on it may be diff --git a/frontend/index.html b/frontend/index.html index 14b3786..cb14257 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -39,7 +39,10 @@ \ No newline at end of file diff --git a/frontend/src/components/artists-view/artists-view.ts b/frontend/src/components/artists-view/artists-view.ts index 209dc49..16659af 100644 --- a/frontend/src/components/artists-view/artists-view.ts +++ b/frontend/src/components/artists-view/artists-view.ts @@ -37,6 +37,10 @@ import type WaPopup from '@awesome.me/webawesome/dist/components/popup/popup.js' import '@awesome.me/webawesome/dist/components/dropdown-item/dropdown-item.js'; import '@components/playlist-picker/playlist-picker.js'; import { dict, list } from '@utils/binding'; +import { + ICON_PLAYLIST, + ICON_QUEUE, +} from '@utils/icon-language'; /** Pixels to change card width per scroll tick. */ const ZOOM_STEP = 16; @@ -1371,7 +1375,7 @@ export class ArtistsView > Add to Queue @@ -1407,7 +1411,7 @@ export class ArtistsView > Add to Playlist Add to Queue @@ -2156,7 +2160,7 @@ export class CoverGrid > Add to Playlist this.queueOwned()} > - + Add to queue ${partial @@ -2740,7 +2746,7 @@ export class ExploreAlbumDetails extends LitElement implements ContextMenuHost { of the same Free glyph carry the toggle instead. --> ${this.isRequested ? 'Requested' : 'Request this'} @@ -3338,7 +3344,7 @@ export class ExploreAlbumDetails extends LitElement implements ContextMenuHost { @click=${() => this.onContextMenuAction('add-to-queue')} @mouseenter=${() => this.ctxMenu.closePlaylistSubmenu()} > - + Add to Queue - + Add to Playlist diff --git a/frontend/src/components/explore-artist-details/explore-artist-details.ts b/frontend/src/components/explore-artist-details/explore-artist-details.ts index c18ca92..9c2389e 100644 --- a/frontend/src/components/explore-artist-details/explore-artist-details.ts +++ b/frontend/src/components/explore-artist-details/explore-artist-details.ts @@ -59,6 +59,12 @@ import { dict, dictByName } from '@utils/binding'; import type { TrackDetails } from '@components/track-details/track-details.js'; import { showTrackDetailsForPath } from '@utils/track-details-opener.js'; import '@components/playlist-picker/playlist-picker.js'; +import { + ICON_CAN_REQUEST, + ICON_PLAYLIST, + ICON_QUEUE, + ICON_REQUESTED, +} from '@utils/icon-language'; /* ── Constants ── */ @@ -2674,7 +2680,7 @@ export class ExploreArtistDetails extends LitElement implements ContextMenuHost @click=${() => this.onContextMenuAction('add-to-queue')} @mouseenter=${() => this.ctxMenu.closePlaylistSubmenu()} > - + Add to Queue - + Add to Playlist @@ -2737,7 +2743,7 @@ export class ExploreArtistDetails extends LitElement implements ContextMenuHost Play void this.onReleaseAction('add-to-queue')}> - + Add to Queue void this.onReleaseAction('play-next')}> @@ -2751,7 +2757,7 @@ export class ExploreArtistDetails extends LitElement implements ContextMenuHost void this.onReleaseRequestToggle()}> ${requested ? 'Cancel Request' : 'Request This'} @@ -2789,9 +2795,15 @@ export class ExploreArtistDetails extends LitElement implements ContextMenuHost appearance=${request ? 'filled' : 'outlined'} @click=${() => void this.toggleFollow(request?.id)} > + ${request ? 'Following' : 'Follow for new releases'} diff --git a/frontend/src/components/explore-view/explore-view.ts b/frontend/src/components/explore-view/explore-view.ts index b9b1257..b67e003 100644 --- a/frontend/src/components/explore-view/explore-view.ts +++ b/frontend/src/components/explore-view/explore-view.ts @@ -36,6 +36,7 @@ import '@awesome.me/webawesome/dist/components/popup/popup.js'; import type WaPopup from '@awesome.me/webawesome/dist/components/popup/popup.js'; import '@awesome.me/webawesome/dist/components/dropdown-item/dropdown-item.js'; import { dict, dictByName } from '@utils/binding'; +import { ICON_QUEUE } from '@utils/icon-language'; /** The region explore's own action failures (play/queue) are rendered in. */ export const ExploreRegion = 'explore'; @@ -1342,7 +1343,7 @@ export class ExploreView extends ViewLifecycleMixin(LitElement) implements Conte Play this.onContextMenuAction('add-to-queue')}> - + Add to Queue this.onContextMenuAction('play-next')}> diff --git a/frontend/src/components/genres-view/genres-view.ts b/frontend/src/components/genres-view/genres-view.ts index 92e45f7..8c0a6f7 100644 --- a/frontend/src/components/genres-view/genres-view.ts +++ b/frontend/src/components/genres-view/genres-view.ts @@ -35,6 +35,10 @@ import type WaPopup from '@awesome.me/webawesome/dist/components/popup/popup.js' import '@awesome.me/webawesome/dist/components/dropdown-item/dropdown-item.js'; import '@components/playlist-picker/playlist-picker.js'; import { dictByName } from '@utils/binding'; +import { + ICON_PLAYLIST, + ICON_QUEUE, +} from '@utils/icon-language'; /** Pixels to change card width per scroll tick. */ const ZOOM_STEP = 16; @@ -1211,7 +1215,7 @@ export class GenresView > Add to Queue @@ -1257,7 +1261,7 @@ export class GenresView > Add to Playlist - + `; diff --git a/frontend/src/components/playlist-details/playlist-details.ts b/frontend/src/components/playlist-details/playlist-details.ts index e188099..c2e5d75 100644 --- a/frontend/src/components/playlist-details/playlist-details.ts +++ b/frontend/src/components/playlist-details/playlist-details.ts @@ -70,6 +70,10 @@ import { } from '@utils/explore-link'; import { designTokens } from '../../styles/tokens.css'; import { list } from '@utils/binding'; +import { + ICON_PLAYLIST, + ICON_QUEUE, +} from '@utils/icon-language'; /** One playlist row: the track and its position in the *playlist*, * which is not its position in the filtered view. */ @@ -1358,7 +1362,7 @@ export class PlaylistDetails
@@ -1665,7 +1669,7 @@ export class PlaylistDetails > Add to Queue @@ -1720,7 +1724,7 @@ export class PlaylistDetails > Add to Playlist - + New Playlist
diff --git a/frontend/src/components/playlist-view/playlist-view.ts b/frontend/src/components/playlist-view/playlist-view.ts index 4e7aa62..8ffe1cd 100644 --- a/frontend/src/components/playlist-view/playlist-view.ts +++ b/frontend/src/components/playlist-view/playlist-view.ts @@ -37,6 +37,10 @@ import { ViewLifecycleMixin } from '@utils/view-lifecycle'; import { FavoritesController } from '@store/controllers/favorites-controller'; import '@components/duplicate-tracks-dialog/duplicate-tracks-dialog.js'; import type { DuplicateTracksDialog } from '@components/duplicate-tracks-dialog/duplicate-tracks-dialog.js'; +import { + ICON_NEW, + ICON_PLAYLIST, +} from '@utils/icon-language'; const SCROLL_DEBOUNCE_MS = 100; @@ -1496,7 +1500,7 @@ export class PlaylistView extends ViewLifecycleMixin(LitElement) { @dragleave=${this.onNewButtonDragLeave} @drop=${this.onNewButtonDrop} > - + New Playlist @@ -1794,11 +1799,11 @@ export class QueuePanel ? html`

Queue is empty

@@ -1875,7 +1880,7 @@ export class QueuePanel > Add to Playlist Add to Queue @@ -1521,7 +1525,7 @@ export class SmartPlaylistDetails > Add to Playlist this.onContextMenuAction('add-to-queue')} @mouseenter=${() => this.ctxMenu.closePlaylistSubmenu()} > - + Add to Queue - + Add to Playlist diff --git a/frontend/src/icons/names.txt b/frontend/src/icons/names.txt index 81ce440..309e42b 100644 --- a/frontend/src/icons/names.txt +++ b/frontend/src/icons/names.txt @@ -24,6 +24,7 @@ solid/arrows-rotate solid/arrow-up-short-wide solid/backward-step solid/bars +solid/bars-staggered regular/bookmark solid/bookmark solid/box-open diff --git a/frontend/src/utils/icon-language.ts b/frontend/src/utils/icon-language.ts new file mode 100644 index 0000000..28ffc94 --- /dev/null +++ b/frontend/src/utils/icon-language.ts @@ -0,0 +1,100 @@ +/** + * What each icon in this app means, once. + * + * The set was a mix: `plus` meant "add to the queue", "add to a + * playlist", "make a new playlist" and "you do not own this" — the + * first two *adjacent in the same context menu* — while `list` meant + * the queue, the Playlists destination, and (in `queue-panel` alone) + * adding to the queue. Two icons carrying seven meanings between them + * is not a vocabulary, and a user cannot learn one that says four + * things. + * + * The rule these are chosen by: **an icon names the noun it acts on, + * not the verb.** "Add to queue" and "add to playlist" are the same + * verb on different nouns, so the noun is what has to differ — which is + * also why adding to a playlist wears the Playlists destination's own + * icon rather than a generic plus. `plus` survives for exactly the one + * thing it is unambiguous about, making something that did not exist. + * + * Import these rather than writing a name inline. A literal string is + * how the last set drifted, and nothing catches it: a wrong-but-real + * icon renders perfectly. + */ + +/** Start playing this now. */ +export const ICON_PLAY = 'play'; + +/** Start playing this now, in a shuffled order. */ +export const ICON_SHUFFLE = 'shuffle'; + +/** + * The queue, and putting something into it. + * + * One glyph for the noun and the action, so the button that opens the + * queue and the menu item that adds to it are visibly the same subject. + * The queue used to wear `list`, which is the Playlists destination. + */ +export const ICON_QUEUE = 'bars-staggered'; + +/** Put this next in the queue rather than at the end. */ +export const ICON_PLAY_NEXT = 'forward-step'; + +/** + * A playlist, and adding something to one. + * + * The same icon as the Playlists destination in the sidebar, which is + * the point: the menu item says where the thing is going. + */ +export const ICON_PLAYLIST = 'list'; + +/** + * Make a new thing that did not exist — a playlist, a rule, a library. + * + * This is the only meaning `plus` keeps. It used to carry four. + */ +export const ICON_NEW = 'plus'; + +/** + * The request ("want") toggle, as an outline/solid pair. + * + * Two states of one control have to read as each other's opposite, + * which a plus and a bookmark do not. The pair was already in the app + * and already correct — `explore-album-details`'s "Want this" button + * has used it since it was written, and `favorites-controller` uses the + * same shape for `regular/heart` → `heart` — while the badge forty + * pixels away showed a plus for the same state. + * + * That is `utils/library-status.ts`'s fault one layer down: it made the + * two surfaces agree on *what wanting means* and left them disagreeing + * on what it looks like. + */ +export const ICON_CAN_REQUEST = 'regular/bookmark'; +export const ICON_REQUESTED = 'solid/bookmark'; + +/** + * You have this. + * + * Deliberately not drawn on the common case — see the tracklist, where + * absence is what gets marked. This is for the places that answer the + * question directly, like the badge on a catalog card. + */ +export const ICON_IN_LIBRARY = 'check'; + +/** + * Something is being fetched right now. + * + * Distinct from `ICON_REQUESTED`: a request may sit on the list + * forever without anything happening, which is exactly why the badge's + * "queued" state stopped being an hourglass. + */ +export const ICON_DOWNLOADING = 'download'; + +/** + * Take this away. + * + * One icon for removing from a playlist, from the queue and from the + * library, because the difference that matters is stated in the words + * beside it and in the confirmation — "Remove from Library" says in its + * impact line that the files are not deleted. + */ +export const ICON_REMOVE = 'trash'; diff --git a/frontend/test/components/chrome.test.ts b/frontend/test/components/chrome.test.ts index 4d11c12..1384145 100644 --- a/frontend/test/components/chrome.test.ts +++ b/frontend/test/components/chrome.test.ts @@ -19,6 +19,11 @@ import { update, visual, } from '@test/support/render'; +import { + ICON_CAN_REQUEST, + ICON_IN_LIBRARY, + ICON_REQUESTED, +} from '@utils/icon-language'; describe('', () => { it('renders a testid per destination, which is how e2e navigates', async () => { @@ -154,9 +159,20 @@ describe('', () => { it('defaults to "not in library"', async () => { const el = await fixture('library-status-indicator'); - expect(shadow(el, 'wa-icon')?.getAttribute('name')).toBe('plus'); + expect(shadow(el, 'wa-icon')?.getAttribute('name')).toBe(ICON_CAN_REQUEST); }); + /** + * Named from the vocabulary rather than written out, or this test + * pins the glyphs *against* the table it is supposed to follow — + * which is what it did: it asserted `plus` for the un-owned state, + * the same glyph two adjacent menu items were using for two other + * meanings, and passing was the reason nobody looked. + * + * What is still worth asserting is that the three differ, which is + * the property the states need and the one the table cannot state + * about itself here. + */ it('uses a distinct glyph per state', async () => { const glyphs: (string | null | undefined)[] = []; @@ -166,7 +182,8 @@ describe('', () => { glyphs.push(shadow(el, 'wa-icon')?.getAttribute('name')); } - expect(glyphs).toEqual(['check', 'bookmark', 'plus']); + expect(glyphs).toEqual([ICON_IN_LIBRARY, ICON_REQUESTED, ICON_CAN_REQUEST]); + expect(new Set(glyphs).size).toBe(3); }); it('phrases its label around the entity it describes', async () => { diff --git a/frontend/test/components/icon-language.test.ts b/frontend/test/components/icon-language.test.ts new file mode 100644 index 0000000..1c14fe5 --- /dev/null +++ b/frontend/test/components/icon-language.test.ts @@ -0,0 +1,140 @@ +/** + * The icon vocabulary is one table, and nothing writes around it. + * + * A wrong-but-real icon name renders perfectly: no error, no fallback, + * no failing assertion anywhere. That is how `plus` came to mean "add + * to the queue", "add to a playlist", "make a new playlist" and "you do + * not own this" — the first two adjacent in the same context menu — + * while `list` meant the queue, the Playlists destination *and* adding + * to the queue. + * + * `src/icons/index.ts` catches a name that is not *bundled*. Nothing + * catches a name that is bundled and means something else, so this + * sweeps the source for the governed ones. It is the same shape as + * `TestNoDirectRuntimeEmits` and `TestNoWritesOnTheReadPool` in the + * backend, and exists for the same reason: the rule is about every call + * site, so checking one is checking nothing. + */ +import { describe, expect, it } from 'vitest'; + +import { bundledIconNames } from '../../src/icons'; +import * as icons from '@utils/icon-language'; + +/** Every component source, as text. */ +const SOURCES = import.meta.glob('../../src/**/*.ts', { + eager: true, + query: '?raw', + import: 'default', +}); + +/** + * The names that carry a meaning the table owns. + * + * Deliberately not every bundled name. `check` is `ICON_IN_LIBRARY` + * here and also the "Copied" confirmation in `job-log-view`, which is + * a different, perfectly good meaning — governing it would force a + * false rename. What belongs on this list is a name that was actually + * overloaded. + */ +const GOVERNED = [ + 'plus', + 'list', + 'bookmark', + 'solid/bookmark', + 'regular/bookmark', + 'bars-staggered', +]; + +/** The one file allowed to say them, plus its own test. */ +const DEFINITION = /icon-language\.(ts|test\.ts)$/; + +describe('the icon vocabulary', () => { + /** + * A sweep over nothing passes. This is the assertion that makes the + * rest of the file mean something, and it is the first thing that + * breaks if the glob pattern stops matching after a move. + */ + it('actually reads the source', () => { + const paths = Object.keys(SOURCES); + + expect(paths.length).toBeGreaterThan(100); + expect(paths.some((p) => p.endsWith('/track-list.ts'))).toBe(true); + expect(SOURCES[paths[0]!]).toContain('import'); + }); + + it.each(GOVERNED)('is not written around for %s', (name) => { + const offenders: string[] = []; + + for (const [path, source] of Object.entries(SOURCES)) { + if (DEFINITION.test(path)) continue; + + // Both spellings: an icon in a template, and an icon name in a + // data table (which is how the sidebar and bottom-nav carry + // theirs). + const literal = new RegExp( + `(name="${name}"|icon: '${name}'|name=\\$\\{[^}]*'${name}')`, + ); + + if (literal.test(source)) offenders.push(path); + } + + expect(offenders).toEqual([]); + }); + + /** + * A meaning with no icon behind it is the state the badge's `queued` + * spent a year in — declared, styled, and produced by nothing. + */ + it('gives every meaning a name', () => { + const values = Object.entries(icons).filter(([k]) => k.startsWith('ICON_')); + + expect(values.length).toBeGreaterThan(0); + + for (const [key, value] of values) { + expect(`${key}=${value}`).toMatch(/^ICON_[A-Z_]+=[a-z]+[a-z/-]*$/); + } + }); + + /** + * Every name in the table is a name the app actually ships. + * + * This is the loop the vocabulary closes. A name that is not bundled + * renders a circled question mark and reports itself to + * `__yjIconMisses` — at *runtime*, from a state something has to + * reach first. `bookmark-check` is Font Awesome **Pro**, and it was + * on `explore-artist-details`'s Follow button, drawn for every + * followed artist, invisible to `offline-icons.spec.ts` because no + * spec had ever followed one. Reaching the state is no longer how + * this is found. + */ + it('names only icons that are bundled', () => { + const bundled = new Set(bundledIconNames()); + const missing = Object.entries(icons) + .filter(([k]) => k.startsWith('ICON_')) + .filter(([, v]) => !bundled.has(v as string)) + .map(([k, v]) => `${k} (${v})`); + + expect(missing).toEqual([]); + }); + + /** + * The two states of the request toggle have to be the same glyph in + * two weights, or they do not read as each other's opposite — which + * is what a plus against a bookmark was. + */ + it('makes the request toggle an outline/solid pair', () => { + expect(icons.ICON_CAN_REQUEST).toBe(`regular/${icons.ICON_REQUESTED.replace('solid/', '')}`); + }); + + /** + * The queue and the Playlists destination wore the same icon, and + * "add to queue" and "add to playlist" sat next to each other wearing + * a third same one. Whatever the table says, these three have to + * differ from each other. + */ + it('keeps the queue, playlists and creating something apart', () => { + const three = [icons.ICON_QUEUE, icons.ICON_PLAYLIST, icons.ICON_NEW]; + + expect(new Set(three).size).toBe(3); + }); +});