import { describe, it, expect, beforeEach, afterEach } from 'vitest' import { fireEvent, render, screen } from '@testing-library/react' import { Provider } from '../provider/widget-provider' import { clearAllWidgetStores, useTransformEnabled, useWidgetId, } from '../stores' import { Category } from './category' import { RelativeData } from '../actions/relative-data/relative-data' import type { CategoryWidgetData } from './types' beforeEach(() => clearAllWidgetStores()) afterEach(() => clearAllWidgetStores()) // Replicates `CategoryActionWiring` from the storybook composer // (apps/storybook/stories/modules/widgets/category/category.tsx). The // composer-level wiring isn't testable in `apps/storybook` (no vitest // infra), so we re-implement just the relative-data branch here and // assert the bar-width math end-to-end through Provider → action → // bridge → UI. function Wired({ maxOverride }: { maxOverride?: number }) { const id = useWidgetId() const relativeDataActive = useTransformEnabled(id, 'relative-data') const effectiveMaxOverride = relativeDataActive ? 100 : maxOverride return } // Single-series dataset where the max value is 1000 so percent values // map to round numbers we can match exactly against bar widths. const DATA: CategoryWidgetData = [ [ { name: 'A', value: 1000 }, { name: 'B', value: 500 }, { name: 'C', value: 250 }, ], ] function getBarFills(): HTMLElement[] { return Array.from( document.querySelectorAll('[data-bar-fill="true"]'), ) } describe('Category + RelativeData composer wiring', () => { it('default (RelativeData off) → bars scale to data max', () => { render( , ) const fills = getBarFills() expect(fills).toHaveLength(3) // 1000/1000, 500/1000, 250/1000 expect(fills[0]!.style.width).toBe('100%') expect(fills[1]!.style.width).toBe('50%') expect(fills[2]!.style.width).toBe('25%') }) it('clicking the % action flips bars to a 100-denominator', () => { render( , ) // Toggle ON. RelativeData rewrites data values into percentages // (A=1000/1750 ≈ 57.14, B=500/1750 ≈ 28.57, C=250/1750 ≈ 14.29) AND // the composer wiring forces maxOverride=100 so the bar widths // match those percent values directly. fireEvent.click(screen.getByRole('button', { name: /relative|percent/i })) const fills = getBarFills() expect(fills).toHaveLength(3) // Allow a small rounding tolerance — the percent transform produces // floats, the bar math multiplies by 100 again. const a = parseFloat(fills[0]!.style.width) const b = parseFloat(fills[1]!.style.width) const c = parseFloat(fills[2]!.style.width) expect(a).toBeGreaterThan(55) expect(a).toBeLessThan(60) expect(b).toBeGreaterThan(27) expect(b).toBeLessThan(30) expect(c).toBeGreaterThan(13) expect(c).toBeLessThan(16) // The largest bar must be NOT 100% — that's the whole point of the // fix. Without the wiring, A would have been 100% (largest of the // percent series). expect(a).toBeLessThan(100) }) it('toggling off again restores data-max scaling', () => { render( , ) const button = screen.getByRole('button', { name: /relative|percent/i }) fireEvent.click(button) // on fireEvent.click(button) // off const fills = getBarFills() expect(fills[0]!.style.width).toBe('100%') expect(fills[1]!.style.width).toBe('50%') expect(fills[2]!.style.width).toBe('25%') }) })