diff --git a/e2e/specs/queue-toggle-state.spec.ts b/e2e/specs/queue-toggle-state.spec.ts new file mode 100644 index 0000000..78ac663 --- /dev/null +++ b/e2e/specs/queue-toggle-state.spec.ts @@ -0,0 +1,66 @@ +import { test, expect } from '../support/fixtures.js'; + +/** + * The queue button says whether the queue is open. + * + * It used to look identical in both states, so the only way to tell + * what pressing it would do was to look at the other side of the window + * and infer it — and for anyone not looking at all there was nothing to + * infer from: no `aria-expanded`, no `aria-controls`, no pressed state. + * + * The state is reflected *from the panel*, not kept beside the click, + * because the button is not the only thing that opens the queue — + * `now-playing-view` sets the same attribute, since it hides the bar + * this button lives in. A flag maintained by the click handler would be + * right until something else opened the panel and then quietly wrong, + * which is the second test here. + */ +test.describe('the queue toggle', () => { + test('reports open and closed, and names what it controls', async ({ + app, + }) => { + const toggle = app.locator('#queue-button'); + + await expect(toggle).toHaveAttribute('aria-controls', 'queue-panel'); + await expect(toggle).toHaveAttribute('aria-expanded', 'false'); + + await toggle.click(); + await expect(toggle).toHaveAttribute('aria-expanded', 'true'); + + // The state is not only in the accessibility tree: a control that + // announces a state it does not draw is half a fix. + // + // Background rather than colour, because the pointer is still on + // the button after the click and `:hover` paints it the same accent + // the open state does -- so a colour comparison here passes on the + // broken build and proves nothing. + const [open, closed] = await toggle.evaluate((el) => { + const now = getComputedStyle(el).backgroundColor; + + el.setAttribute('aria-expanded', 'false'); + const shut = getComputedStyle(el).backgroundColor; + + el.setAttribute('aria-expanded', 'true'); + + return [now, shut]; + }); + + expect(open).not.toBe(closed); + + await toggle.click(); + await expect(toggle).toHaveAttribute('aria-expanded', 'false'); + }); + + test('follows the panel when something else opens it', async ({ app }) => { + const toggle = app.locator('#queue-button'); + + await expect(toggle).toHaveAttribute('aria-expanded', 'false'); + + // Exactly what `now-playing-view`'s queue button does. + await app.evaluate(() => + document.getElementById('queue-panel')?.setAttribute('open', ''), + ); + + await expect(toggle).toHaveAttribute('aria-expanded', 'true'); + }); +}); diff --git a/frontend/index.css b/frontend/index.css index 5d5fe0a..7549e03 100644 --- a/frontend/index.css +++ b/frontend/index.css @@ -207,6 +207,17 @@ body div.sidebar { color: var(--yj-accent, #ffd43b); } + /* An open queue is a state this button can be in, and it used to + look exactly like the closed one -- so the only way to tell what + pressing it would do was to look at the other side of the window + and infer it. `aria-expanded` is the same fact for anyone not + looking at all, and it points at the panel it controls. */ + #queue-button[aria-expanded='true'] { + color: var(--yj-accent, #ffd43b); + background: var(--yj-bg-overlay, #404040); + border-radius: 4px; + } + #queue-button.drag-over { color: var(--yj-accent, #ffd43b); outline: 2px dashed var(--yj-accent, #ffd43b); diff --git a/frontend/index.html b/frontend/index.html index 7af95aa..14b3786 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -37,7 +37,8 @@ diff --git a/frontend/index.ts b/frontend/index.ts index 0b05564..2431050 100644 --- a/frontend/index.ts +++ b/frontend/index.ts @@ -521,6 +521,28 @@ if (queueButton && queuePanel) { } }); + // The button says whether the panel is open, and it learns that + // from the panel rather than from its own click handler. + // + // It is not the only thing that opens the queue -- `now-playing-view` + // sets the same attribute, because it hides the bar this button + // lives in -- so a state kept beside the click would be right until + // something else opened the panel and then quietly wrong. The panel's + // `open` attribute is the one fact; this reflects it. + const reflectQueueState = () => { + queueButton.setAttribute( + 'aria-expanded', + String(queuePanel.hasAttribute('open')), + ); + }; + + new MutationObserver(reflectQueueState).observe(queuePanel, { + attributes: true, + attributeFilter: ['open'], + }); + + reflectQueueState(); + // --------------------------------------------------------------- // Queue button as drop target (when queue panel is closed) // ---------------------------------------------------------------