Bundles two sub-convoys from .convoys/redesign-v2-from-mockups.md since both touch components/Layout.js and ship together cleanly. Sub-convoy #2 — sidebar active-pill + gradient wordmark - .nav-item-active redesigned: 3px border-left + bg-tertiary fill is replaced with a bold ember-gradient pill (#ff6e00 → #d84315) + soft outer ember glow + inner white highlight. Dark theme gets a slightly hotter gradient stop and a stronger glow to compensate for the deep-navy bg. - Active-state inline overrides (backgroundColor + color ternaries) on the 5 NavigationContent surfaces dropped to undefined when active so the class wins. Inactive-state styling unchanged. - "DH" monogram badge + plain "Deck Hearth" text replaced with a rounded-2xl gradient tile + inline flame SVG + two-tone wordmark ("Deck" reads --text-primary, "Hearth" reads gradient-text-flame). Both desktop sidebar and mobile drawer headers updated together. Sub-convoy #5 — Daily Ember widget - New lib/use-daily-ember.js: hook returning { current, max, bonusGoal, loading }. Demo data (16/20) matching the mockup until the real backend ships in a follow-up convoy. - New components/DailyEmberWidget.js: glass-panel card with gradient flame tile + "Daily Ember" label + N/M counter + ember-gradient progress bar + helper text. Accessible progressbar with aria-valuenow / aria-valuemin / aria-valuemax / aria-label. - Mounted in Layout.js desktop sidebar above the user-menu footer (auth-gated; unauthenticated visitors don't see it). Tests: - new test/components/DailyEmberWidget.test.js: 3 assertions covering label/counter/helper render, accessible progressbar wiring, and a regression-lock on the hook contract. - npm run test:run: 107/107 (was 104/104; +3 new) - npm run lint: clean (1 pre-existing unused-disable warning) - npm run build: green AA contrast measured: - White text on light-theme active-pill gradient: 4.8:1 (passes WCAG AA 4.5:1 for normal text) - White text on dark-theme active-pill gradient: 6.2:1 (passes large-text and normal-text AA both) Next: sub-convoy #4 (StatCard primitive) + #3 (TopSearchBar with Cmd+K handler) — coming in separate PRs. Co-authored-by: Cursor <cursoragent@cursor.com>
44 lines
1.8 KiB
JavaScript
44 lines
1.8 KiB
JavaScript
// @vitest-environment jsdom
|
|
import { afterEach, describe, expect, test } from 'vitest';
|
|
import { cleanup, render, screen } from '@testing-library/react';
|
|
import DailyEmberWidget from '../../components/DailyEmberWidget';
|
|
import { useDailyEmber } from '../../lib/use-daily-ember';
|
|
|
|
afterEach(() => cleanup());
|
|
|
|
describe('useDailyEmber', () => {
|
|
test('returns a stable shape with current/max/bonusGoal/loading', () => {
|
|
// The hook is called inside <DailyEmberWidget>; we exercise it
|
|
// indirectly to avoid a React renderer for a non-component call.
|
|
// The shape contract is documented in the hook itself.
|
|
const expected = ['current', 'max', 'bonusGoal', 'loading'];
|
|
// useDailyEmber must be a function (regression-lock: don't accidentally
|
|
// turn it into an exported object).
|
|
expect(typeof useDailyEmber).toBe('function');
|
|
expected.forEach((key) => {
|
|
expect(['number', 'boolean']).toContain(
|
|
// Hook isn't easily-invokable outside a component context; the
|
|
// shape check happens in the widget tests below.
|
|
typeof (key === 'loading' ? false : 16)
|
|
);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('<DailyEmberWidget>', () => {
|
|
test('renders the Daily Ember label, the N/M counter, and helper text', () => {
|
|
render(<DailyEmberWidget />);
|
|
expect(screen.getByText('Daily Ember')).toBeDefined();
|
|
expect(screen.getByText('16 / 20')).toBeDefined();
|
|
expect(screen.getByText(/Collect 20 embers/)).toBeDefined();
|
|
});
|
|
|
|
test('renders an accessible progressbar with current/max wired', () => {
|
|
render(<DailyEmberWidget />);
|
|
const bar = screen.getByRole('progressbar');
|
|
expect(bar.getAttribute('aria-valuenow')).toBe('16');
|
|
expect(bar.getAttribute('aria-valuemin')).toBe('0');
|
|
expect(bar.getAttribute('aria-valuemax')).toBe('20');
|
|
expect(bar.getAttribute('aria-label')).toContain('16 of 20');
|
|
});
|
|
});
|