import { describe, test, expect, beforeEach } from 'vitest' import { render, screen, fireEvent, waitFor } from '@testing-library/react' import { StackToggle, STACK_TOGGLE_TOOL_ID } from './stack-toggle' import { useWidgetStore } from '../../stores/widget-store' import { DEFAULT_STACK_GROUP } from '../../echart/const' describe('StackToggle', () => { const widgetId = 'test-stack-widget' // Helper to set up widget with multiple series (required for component to render) function setupMultiSeriesWidget(overrides = {}) { useWidgetStore.getState().setWidget(widgetId, { option: { series: [ { name: 'Series 1', type: 'bar' }, { name: 'Series 2', type: 'bar' }, ], }, ...overrides, }) } beforeEach(() => { useWidgetStore.getState().clearWidgets() setupMultiSeriesWidget() }) test('renders stack toggle button', () => { render() const button = screen.getByRole('button') expect(button).toBeTruthy() }) test('shows stacked tooltip when in unstacked mode', () => { render() const button = screen.getByRole('button', { name: 'Enable stacking' }) expect(button).toBeTruthy() }) test('toggles to stacked mode and updates tool config', () => { render() const button = screen.getByRole('button') fireEvent.click(button) const widget = useWidgetStore.getState().getWidget(widgetId) const tool = widget?.registeredTools?.find( (t) => t.id === STACK_TOGGLE_TOOL_ID, ) expect(tool?.enabled).toBe(true) }) test('toggles back to unstacked mode', () => { render() const button = screen.getByRole('button') // Toggle to stacked fireEvent.click(button) // Toggle back to unstacked fireEvent.click(button) const widget = useWidgetStore.getState().getWidget(widgetId) const tool = widget?.registeredTools?.find( (t) => t.id === STACK_TOGGLE_TOOL_ID, ) expect(tool?.enabled).toBe(false) }) test('has active state when in stacked mode', () => { render() const button = screen.getByRole('button') expect(button.getAttribute('data-active')).toBe('false') fireEvent.click(button) expect(button.getAttribute('data-active')).toBe('true') }) test('starts in stacked mode when defaultIsStacked is true', () => { render() const button = screen.getByRole('button') expect(button.getAttribute('data-active')).toBe('true') // Should show unstacked tooltip when in stacked mode expect(button.getAttribute('aria-label')).toBe('Disable stacking') }) test('uses custom labels when provided', () => { const customLabels = { stacked: 'Stack bars', unstacked: 'Unstack bars', ariaLabel: 'Toggle bar stacking', } render() const button = screen.getByRole('button', { name: 'Toggle bar stacking' }) expect(button).toBeTruthy() }) test('initializes tool config with default values on mount', () => { render() const widget = useWidgetStore.getState().getWidget(widgetId) const tool = widget?.registeredTools?.find( (t) => t.id === STACK_TOGGLE_TOOL_ID, ) expect(tool?.enabled).toBe(false) }) test('initializes tool config with stacked values when defaultIsStacked is true', () => { render() const widget = useWidgetStore.getState().getWidget(widgetId) const tool = widget?.registeredTools?.find( (t) => t.id === STACK_TOGGLE_TOOL_ID, ) expect(tool?.enabled).toBe(true) }) test('applies stack to EChart series via config pipeline when toggling to stacked', async () => { render() const button = screen.getByRole('button') fireEvent.click(button) // Run the config pipeline to apply the stack tool's transformation await useWidgetStore.getState().executeConfigPipeline(widgetId, { option: { series: [ { name: 'Series 1', type: 'bar' }, { name: 'Series 2', type: 'bar' }, ], }, }) const widget = useWidgetStore.getState().getWidget(widgetId) const series = (widget as { option?: { series?: { stack?: string }[] } }) ?.option?.series?.[0] expect(series?.stack).toBe(DEFAULT_STACK_GROUP) }) test('removes stack from EChart series via config pipeline when toggling to unstacked', async () => { useWidgetStore.getState().setWidget(widgetId, { option: { series: [ { name: 'Series 1', type: 'bar', stack: DEFAULT_STACK_GROUP }, { name: 'Series 2', type: 'bar', stack: DEFAULT_STACK_GROUP }, ], }, }) render() const button = screen.getByRole('button') // First click unstacks (since series has stack, it starts stacked) fireEvent.click(button) // Run the config pipeline — tool is disabled when unstacked, so stack is removed await useWidgetStore.getState().executeConfigPipeline(widgetId, { option: { series: [ { name: 'Series 1', type: 'bar' }, { name: 'Series 2', type: 'bar' }, ], }, }) const widget = useWidgetStore.getState().getWidget(widgetId) const series = (widget as { option?: { series?: { stack?: string }[] } }) ?.option?.series?.[0] expect(series?.stack).toBeUndefined() }) test('applies stack to multiple series via config pipeline', async () => { useWidgetStore.getState().setWidget(widgetId, { option: { series: [ { name: 'Series 1', type: 'bar' }, { name: 'Series 2', type: 'bar' }, { name: 'Series 3', type: 'bar' }, ], }, }) render() const button = screen.getByRole('button') fireEvent.click(button) await useWidgetStore.getState().executeConfigPipeline(widgetId, { option: { series: [ { name: 'Series 1', type: 'bar' }, { name: 'Series 2', type: 'bar' }, { name: 'Series 3', type: 'bar' }, ], }, }) const widget = useWidgetStore.getState().getWidget(widgetId) const seriesArray = ( widget as { option?: { series?: { stack?: string }[] } } )?.option?.series expect(seriesArray).toHaveLength(3) seriesArray?.forEach((s) => { expect(s.stack).toBe(DEFAULT_STACK_GROUP) }) }) test('preserves custom stack group via config pipeline', async () => { const customStackGroup = 'custom-group' useWidgetStore.getState().setWidget(widgetId, { option: { series: [ { name: 'Series 1', type: 'bar', stack: customStackGroup }, { name: 'Series 2', type: 'bar', stack: customStackGroup }, ], }, }) render() const button = screen.getByRole('button') // Toggle off then on fireEvent.click(button) // unstacked fireEvent.click(button) // stacked again await useWidgetStore.getState().executeConfigPipeline(widgetId, { option: { series: [ { name: 'Series 1', type: 'bar', stack: customStackGroup }, { name: 'Series 2', type: 'bar', stack: customStackGroup }, ], }, }) await waitFor(() => { const widget = useWidgetStore.getState().getWidget(widgetId) const series = (widget as { option?: { series?: { stack?: string }[] } }) ?.option?.series?.[0] expect(series?.stack).toBe(customStackGroup) }) }) test('detects existing stack in series and defaults to stacked mode', () => { useWidgetStore.getState().setWidget(widgetId, { option: { series: [ { name: 'Series 1', type: 'bar', stack: 'existing-stack' }, { name: 'Series 2', type: 'bar', stack: 'existing-stack' }, ], }, }) render() const button = screen.getByRole('button') expect(button.getAttribute('data-active')).toBe('true') expect(button.getAttribute('aria-label')).toBe('Disable stacking') }) test('returns null for single series (stacking not applicable)', () => { useWidgetStore.getState().setWidget(widgetId, { option: { series: { name: 'Single Series', type: 'bar' }, }, }) const { container } = render() // Component returns null when there's only one series expect(container.firstChild).toBeNull() }) test('returns null when widget has no option defined', () => { // Clear the widget set up in beforeEach and create one without option useWidgetStore.getState().clearWidgets() useWidgetStore.getState().setWidget(widgetId, {}) const { container } = render() // Component returns null when there's no series data expect(container.firstChild).toBeNull() }) test('config tool re-applies stack when config pipeline re-runs', async () => { // Start with stacked widget useWidgetStore.getState().setWidget(widgetId, { option: { series: [ { name: 'Series 1', type: 'bar', stack: DEFAULT_STACK_GROUP }, { name: 'Series 2', type: 'bar', stack: DEFAULT_STACK_GROUP }, ], }, }) render() // Simulate loader running config pipeline with base config (no stack) await useWidgetStore.getState().executeConfigPipeline(widgetId, { option: { series: [ { name: 'Series 1', type: 'bar' }, { name: 'Series 2', type: 'bar' }, ], }, }) // Stack should be re-applied by the config tool await waitFor(() => { const widget = useWidgetStore.getState().getWidget(widgetId) const series = (widget as { option?: { series?: { stack?: string }[] } }) ?.option?.series expect(series?.[0]?.stack).toBe(DEFAULT_STACK_GROUP) expect(series?.[1]?.stack).toBe(DEFAULT_STACK_GROUP) }) }) test('config tool does not apply stack when unstacked and pipeline re-runs', async () => { // Start with unstacked widget useWidgetStore.getState().setWidget(widgetId, { option: { series: [ { name: 'Series 1', type: 'bar' }, { name: 'Series 2', type: 'bar' }, ], }, }) render() // Run config pipeline with base config await useWidgetStore.getState().executeConfigPipeline(widgetId, { option: { series: [ { name: 'Series 1', type: 'bar' }, { name: 'Series 2', type: 'bar' }, ], }, }) // Stack should NOT be applied since isStacked is false (tool disabled) await waitFor(() => { const widget = useWidgetStore.getState().getWidget(widgetId) const series = (widget as { option?: { series?: { stack?: string }[] } }) ?.option?.series expect(series?.[0]?.stack).toBeUndefined() expect(series?.[1]?.stack).toBeUndefined() }) }) test('preserves other option properties via config pipeline', async () => { useWidgetStore.getState().setWidget(widgetId, { option: { title: { text: 'My Chart' }, xAxis: { type: 'category' }, series: [ { name: 'Series 1', type: 'bar' }, { name: 'Series 2', type: 'bar' }, ], }, }) render() const button = screen.getByRole('button') fireEvent.click(button) await useWidgetStore.getState().executeConfigPipeline(widgetId, { option: { title: { text: 'My Chart' }, xAxis: { type: 'category' }, series: [ { name: 'Series 1', type: 'bar' }, { name: 'Series 2', type: 'bar' }, ], }, }) const widget = useWidgetStore.getState().getWidget(widgetId) const option = ( widget as { option?: { title?: { text?: string } xAxis?: { type?: string } } } )?.option expect(option?.title?.text).toBe('My Chart') expect(option?.xAxis?.type).toBe('category') }) test('registers config tool on mount and unregisters on unmount', () => { const { unmount } = render() const widget = useWidgetStore.getState().getWidget(widgetId) const tools = widget?.registeredTools ?? [] const stackTool = tools.find((t) => t.id === STACK_TOGGLE_TOOL_ID) expect(stackTool).toBeDefined() expect(stackTool?.type).toBe('config') unmount() const widgetAfter = useWidgetStore.getState().getWidget(widgetId) const toolsAfter = widgetAfter?.registeredTools ?? [] const stackToolAfter = toolsAfter.find((t) => t.id === STACK_TOGGLE_TOOL_ID) expect(stackToolAfter).toBeUndefined() }) })