Files
yellowjacket/frontend/test/components/library-status-indicator.test.ts
T
logan 19c68d73a7 fix(ui): keep the count in a partial badge that can act
A control is named after what activating it does, so an actionable
badge said "Request album X" — and `partial` is actionable, because an
album you hold nine of twelve tracks of has three left to ask for.
That made the one state the ring exists for the one state whose name
did not mention it.

The argument the `partial` branch already carries does not stop
applying because the badge became clickable: a ring says "some" to a
sighted user and nothing to anyone else. The name is now the action and
the count.
2026-08-19 00:38:05 -04:00

107 lines
3.3 KiB
TypeScript

/**
* The badge on cards, rows and the album title.
*
* The `partial` state was added so an album you hold nine tracks of
* looks different from one you hold all twelve of. The risk it carries
* is that a ring is a *claim about a total*, and most of an untagged
* library has no total — so the rules under test are that the arc
* reflects the real fraction, that extras do not overfill it, and that
* the count reaches a screen reader rather than only an eye.
*/
import { describe, expect, it } from 'vitest';
import '@components/library-status-indicator/library-status-indicator';
import { fixture, shadow } from '@test/support/render';
/** The stroke-dashoffset the arc was drawn with, as a fraction filled. */
function filledFraction(el: Element): number {
const arc = shadow(el, '.ring-fill');
const dash = Number(arc?.getAttribute('stroke-dasharray'));
const offset = Number(arc?.getAttribute('stroke-dashoffset'));
return (dash - offset) / dash;
}
describe('the library status badge', () => {
it('draws no ring unless it is partial', async () => {
const el = await fixture('library-status-indicator', {
status: 'in-library',
});
expect(shadow(el, '.ring-fill')).toBeNull();
expect(shadow(el, 'wa-icon')?.getAttribute('name')).toBe('check');
});
it('fills the arc to the held fraction', async () => {
const el = await fixture('library-status-indicator', {
status: 'partial',
owned: 9,
expected: 12,
});
expect(filledFraction(el)).toBeCloseTo(0.75, 5);
});
it('does not overfill on bonus tracks', async () => {
const el = await fixture('library-status-indicator', {
status: 'partial',
owned: 13,
expected: 12,
});
expect(filledFraction(el)).toBeCloseTo(1, 5);
});
it('does not divide by a total it was never given', async () => {
const el = await fixture('library-status-indicator', {
status: 'partial',
owned: 3,
expected: 0,
});
expect(filledFraction(el)).toBe(0);
});
it('says the count, not just the shape', async () => {
const el = await fixture('library-status-indicator', {
status: 'partial',
owned: 9,
expected: 12,
entityType: 'album',
label: 'Glass Harbour',
});
const name = shadow(el, '.badge')?.getAttribute('aria-label') ?? '';
expect(name).toContain('9 of 12');
expect(name).toContain('Glass Harbour');
});
});
/**
* A partly-held album is the one state that is *actionable and
* counted*: there are tracks left to ask for, so the badge is a button
* — and a control is named after what activating it does, which is how
* the count came to be dropped from exactly the state the ring exists
* for. Both, or the ring says "some" to an eye and nothing to anyone
* else.
*/
describe('a partial badge that can act', () => {
it('names the action and keeps the count', async () => {
const el = await fixture('library-status-indicator', {
status: 'partial',
owned: 9,
expected: 12,
entityType: 'album',
label: 'Glass Harbour',
requestMbid: 'rg-1',
});
const name = shadow(el, '.badge')?.getAttribute('aria-label') ?? '';
expect(shadow(el, 'button.badge')).not.toBeNull();
expect(name).toContain('Request the rest of');
expect(name).toContain('9 of 12');
});
});