Packages added: - @bytelyst/referral-client — referral API client + share helpers - @bytelyst/subscription-client — subscription/plan API client + cache - @bytelyst/celebrations — milestone triggers, confetti, positive messages - @bytelyst/gentle-notifications — ND-friendly messaging, forbidden phrases - @bytelyst/accessibility — VoiceOver/TalkBack label generators - @bytelyst/quick-actions — progressive disclosure, smart defaults - @bytelyst/time-references — familiar duration references - @bytelyst/org-client — org/workspace/membership/license API client - @bytelyst/marketplace-client — listing/review/install API client All packages: pure TS, ESM, globalThis.fetch, no Node.js deps. 99 Vitest tests across 9 packages, 79/79 public methods covered. Review fixes applied: - time-references: fix module-level mutable state leak + add clearCustomReferences() - accessibility: fix parameter reassignment in formatDurationForA11y/numberToWords - subscription-client: fix flaky daysRemaining test (ms boundary race)
155 lines
5.1 KiB
TypeScript
155 lines
5.1 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import {
|
|
buttonLabel,
|
|
timerLabel,
|
|
progressLabel,
|
|
sliderLabel,
|
|
alertLabel,
|
|
achievementLabel,
|
|
streakLabel,
|
|
listItemLabel,
|
|
formatDurationForA11y,
|
|
formatNumberForA11y,
|
|
buildAnnouncement,
|
|
getPositiveBreakMessage,
|
|
} from './client.js';
|
|
|
|
describe('accessibility label generators', () => {
|
|
it('buttonLabel returns correct props', () => {
|
|
const props = buttonLabel('Start Timer', 'Begins a new timer');
|
|
expect(props.accessible).toBe(true);
|
|
expect(props.accessibilityLabel).toBe('Start Timer');
|
|
expect(props.accessibilityHint).toBe('Begins a new timer');
|
|
expect(props.accessibilityRole).toBe('button');
|
|
});
|
|
|
|
it('timerLabel with context', () => {
|
|
const props = timerLabel('running', '2 hours 30 minutes', 'Intermittent fasting');
|
|
expect(props.accessibilityLabel).toContain('running');
|
|
expect(props.accessibilityLabel).toContain('2 hours 30 minutes');
|
|
expect(props.accessibilityLabel).toContain('Intermittent fasting');
|
|
expect(props.accessibilityRole).toBe('timer');
|
|
});
|
|
|
|
it('timerLabel without context', () => {
|
|
const props = timerLabel('paused', '10 minutes');
|
|
expect(props.accessibilityLabel).toBe('Timer paused, 10 minutes');
|
|
});
|
|
|
|
it('progressLabel with description', () => {
|
|
const props = progressLabel('Fasting', 75, 'On track');
|
|
expect(props.accessibilityLabel).toContain('75 percent');
|
|
expect(props.accessibilityLabel).toContain('On track');
|
|
expect(props.accessibilityRole).toBe('progressbar');
|
|
expect(props.accessibilityValue?.now).toBe(75);
|
|
});
|
|
|
|
it('progressLabel clamps to 0-100', () => {
|
|
const neg = progressLabel('Test', -10);
|
|
expect(neg.accessibilityValue?.now).toBe(0);
|
|
|
|
const over = progressLabel('Test', 150);
|
|
expect(over.accessibilityValue?.now).toBe(100);
|
|
});
|
|
|
|
it('sliderLabel with max', () => {
|
|
const props = sliderLabel('Volume', 7, 10);
|
|
expect(props.accessibilityLabel).toBe('Volume: 7 of 10');
|
|
expect(props.accessibilityRole).toBe('adjustable');
|
|
});
|
|
|
|
it('sliderLabel without max', () => {
|
|
const props = sliderLabel('Score', 42);
|
|
expect(props.accessibilityLabel).toBe('Score: 42');
|
|
});
|
|
|
|
it('alertLabel', () => {
|
|
const props = alertLabel('Warning', 'High heart rate detected');
|
|
expect(props.accessibilityLabel).toBe('Warning alert: High heart rate detected');
|
|
expect(props.accessibilityRole).toBe('alert');
|
|
});
|
|
|
|
it('achievementLabel earned', () => {
|
|
const props = achievementLabel('Early Bird', 'Complete 5 morning sessions', true);
|
|
expect(props.accessibilityLabel).toContain('Earned');
|
|
expect(props.accessibilityLabel).toContain('Early Bird');
|
|
expect(props.accessibilityState?.selected).toBe(true);
|
|
});
|
|
|
|
it('achievementLabel locked', () => {
|
|
const props = achievementLabel('Night Owl', 'Complete 5 late sessions', false);
|
|
expect(props.accessibilityLabel).toContain('Locked');
|
|
expect(props.accessibilityState?.selected).toBe(false);
|
|
});
|
|
|
|
it('streakLabel', () => {
|
|
const props = streakLabel(7, 14);
|
|
expect(props.accessibilityLabel).toContain('7 days');
|
|
expect(props.accessibilityLabel).toContain('14 days');
|
|
});
|
|
|
|
it('listItemLabel with badge', () => {
|
|
const props = listItemLabel('16:8 Protocol', '16 hours fasting', 'Popular');
|
|
expect(props.accessibilityLabel).toBe('16:8 Protocol, 16 hours fasting, Popular');
|
|
});
|
|
|
|
it('listItemLabel minimal', () => {
|
|
const props = listItemLabel('Simple Item');
|
|
expect(props.accessibilityLabel).toBe('Simple Item');
|
|
});
|
|
});
|
|
|
|
describe('formatDurationForA11y', () => {
|
|
it('formats hours and minutes', () => {
|
|
const result = formatDurationForA11y(16.5 * 60 * 60 * 1000);
|
|
expect(result).toBe('16 hours 30 minutes');
|
|
});
|
|
|
|
it('formats zero', () => {
|
|
expect(formatDurationForA11y(0)).toBe('0 seconds');
|
|
});
|
|
|
|
it('formats singular units', () => {
|
|
const result = formatDurationForA11y(3661000); // 1h 1m 1s
|
|
expect(result).toBe('1 hour 1 minute 1 second');
|
|
});
|
|
|
|
it('handles negative as zero', () => {
|
|
expect(formatDurationForA11y(-1000)).toBe('0 seconds');
|
|
});
|
|
});
|
|
|
|
describe('formatNumberForA11y', () => {
|
|
it('formats small numbers', () => {
|
|
expect(formatNumberForA11y(5)).toBe('five');
|
|
expect(formatNumberForA11y(0)).toBe('zero');
|
|
expect(formatNumberForA11y(13)).toBe('thirteen');
|
|
});
|
|
|
|
it('formats larger numbers', () => {
|
|
expect(formatNumberForA11y(42)).toBe('forty two');
|
|
expect(formatNumberForA11y(100)).toBe('one hundred');
|
|
expect(formatNumberForA11y(1234)).toBe('one thousand two hundred thirty four');
|
|
});
|
|
});
|
|
|
|
describe('buildAnnouncement', () => {
|
|
it('combines headline and detail', () => {
|
|
const result = buildAnnouncement('Fast Complete', 'You fasted for 16 hours');
|
|
expect(result).toBe('Fast Complete. You fasted for 16 hours');
|
|
});
|
|
});
|
|
|
|
describe('getPositiveBreakMessage', () => {
|
|
it('returns a positive message', () => {
|
|
const msg = getPositiveBreakMessage(50);
|
|
expect(msg.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('clamps to valid range', () => {
|
|
const msgNeg = getPositiveBreakMessage(-10);
|
|
const msg0 = getPositiveBreakMessage(0);
|
|
expect(msgNeg).toBe(msg0);
|
|
});
|
|
});
|