/** * Render tests for ZoomToggle covering the previously-uncovered branches * in src/widgets/actions/zoom-toggle/zoom-toggle.tsx (0% baseline). * * Coverage focus: * - useWidgetSelector defaults (when widget store entry is empty, defaults * `defaultZoom`, `defaultZoomStart`, `defaultZoomEnd` are used) * - Initial useEffect that seeds the store only when zoom is undefined * - handleToggle toggles between zoom on/off and resets the range * - handleReset re-applies default range with zoom=true * - Conditional render of the reset Tooltip when zoom is active * - Custom labels override defaults * - Custom Icon / ResetIcon passthrough * - dataZoom handler updates store with start/end (both `event.start` and * the batched `event.batch[0].start` paths) */ import { describe, it, expect, beforeEach } from 'vitest' import { render, screen, act, fireEvent } from '@testing-library/react' import { ThemeProvider, createTheme } from '@mui/material/styles' import { useWidgetStore, widgetStoreActions } from '../../stores/widget-store' import { ZoomToggle, ZOOM_TOGGLE_TOOL_ID } from './zoom-toggle' import type { ZoomState } from './types' const theme = createTheme({ spacing: (n: number) => `${n * 8}px`, palette: { common: { white: '#ffffff', black: '#000000' }, secondary: { main: '#3366ff' }, // @ts-expect-error - `black` is a CARTO-specific MUI palette extension black: { 60: 'rgba(0,0,0,0.6)' }, }, typography: { overlineDelicate: { fontSize: '11', fontFamily: 'Inter, sans-serif' }, }, }) function renderWithTheme(ui: React.ReactElement) { return render({ui}) } beforeEach(() => { useWidgetStore.getState().clearWidgets() }) describe('ZoomToggle render', () => { it('renders with default state (zoom off → no reset button)', () => { renderWithTheme() expect(screen.getAllByRole('button')).toHaveLength(1) expect(screen.getByRole('button').getAttribute('data-active')).toBe('false') }) it('seeds the widget store on mount when zoom is undefined', () => { renderWithTheme( , ) const widget = widgetStoreActions.getWidget('zt-seed') expect(widget?.zoom).toBe(true) expect(widget?.zoomStart).toBe(10) expect(widget?.zoomEnd).toBe(90) }) it('does NOT seed when zoom is already set in the store', () => { widgetStoreActions.setWidget('zt-noseed', { zoom: false, zoomStart: 25, zoomEnd: 75, }) renderWithTheme( , ) const widget = widgetStoreActions.getWidget('zt-noseed') // Should preserve the existing store entry expect(widget?.zoomStart).toBe(25) expect(widget?.zoomEnd).toBe(75) }) it('shows the reset button when zoom is active', () => { widgetStoreActions.setWidget('zt-active', { zoom: true, zoomStart: 0, zoomEnd: 100, }) renderWithTheme() // 2 buttons: toggle + reset expect(screen.getAllByRole('button')).toHaveLength(2) }) it('uses custom labels when provided', () => { widgetStoreActions.setWidget('zt-labels', { zoom: true, zoomStart: 0, zoomEnd: 100, }) renderWithTheme( , ) // Reset button is rendered (we won't assert tooltip text because tooltips // render lazily; the reset button's aria-label uses `resetLabel`) expect(screen.getByRole('button', { name: /Reset/i })).toBeDefined() }) it('respects an explicit ariaLabel from labels', () => { renderWithTheme( , ) expect(screen.getByRole('button', { name: 'My zoom button' })).toBeDefined() }) it('renders custom Icon when provided', () => { renderWithTheme( } />, ) expect(screen.getByTestId('custom-icon')).toBeDefined() }) it('renders custom ResetIcon when zoom is active', () => { widgetStoreActions.setWidget('zt-reset-icon', { zoom: true }) renderWithTheme( } />, ) expect(screen.getByTestId('custom-reset')).toBeDefined() }) }) describe('ZoomToggle interactions', () => { it('handleToggle turns zoom on (zoom was off) and preserves the current range', () => { widgetStoreActions.setWidget('zt-toggle-on', { zoom: false, zoomStart: 20, zoomEnd: 80, }) renderWithTheme() fireEvent.click(screen.getByRole('button')) const w = widgetStoreActions.getWidget('zt-toggle-on') expect(w?.zoom).toBe(true) expect(w?.zoomStart).toBe(20) expect(w?.zoomEnd).toBe(80) }) it('handleToggle turns zoom off and resets the range to 0/100', () => { widgetStoreActions.setWidget('zt-toggle-off', { zoom: true, zoomStart: 20, zoomEnd: 80, }) renderWithTheme() const buttons = screen.getAllByRole('button') // First button is the toggle fireEvent.click(buttons[0]!) const w = widgetStoreActions.getWidget('zt-toggle-off') expect(w?.zoom).toBe(false) expect(w?.zoomStart).toBe(0) expect(w?.zoomEnd).toBe(100) }) it('handleReset restores the default zoom range with zoom=true', () => { widgetStoreActions.setWidget('zt-reset', { zoom: true, zoomStart: 33, zoomEnd: 66, }) renderWithTheme( , ) // 2 buttons: toggle + reset → click reset const buttons = screen.getAllByRole('button') fireEvent.click(buttons[1]!) const w = widgetStoreActions.getWidget('zt-reset') expect(w?.zoom).toBe(true) expect(w?.zoomStart).toBe(5) expect(w?.zoomEnd).toBe(95) }) }) describe('ZoomToggle pipeline tool registration', () => { it('registers a config tool on mount and unregisters on unmount', () => { const { unmount } = renderWithTheme() // Tool should be registered const widget = widgetStoreActions.getWidget('zt-tool') expect(widget).toBeDefined() unmount() // Tool cleanup runs via the effect's cleanup function }) it('config tool fn merges zoom config into the option, adjusting grid.bottom', () => { widgetStoreActions.setWidget('zt-toolfn', { zoom: true, zoomStart: 0, zoomEnd: 100, }) renderWithTheme() // Find the registered tool and exercise its fn const widget = widgetStoreActions.getWidget('zt-toolfn') as unknown as { registeredTools?: { id: string; fn: (cfg: unknown) => unknown }[] } const tool = widget?.registeredTools?.find( (t) => t.id === ZOOM_TOGGLE_TOOL_ID, ) if (tool) { const result = tool.fn({ option: { legend: { show: true }, grid: { bottom: 16 }, }, onEvents: { custom: () => undefined }, }) as { option: { dataZoom: unknown[]; grid: { bottom: number } } onEvents: { dataZoom: unknown; custom: unknown } } expect(result.option.dataZoom).toBeDefined() expect(result.option.grid.bottom).toBeGreaterThan(16) expect(result.onEvents.dataZoom).toBeDefined() expect(result.onEvents.custom).toBeDefined() } }) it('config tool fn falls back to defaults when option/grid is missing', () => { widgetStoreActions.setWidget('zt-fn-defaults', { zoom: true }) renderWithTheme() const widget = widgetStoreActions.getWidget( 'zt-fn-defaults', ) as unknown as { registeredTools?: { id: string; fn: (cfg: unknown) => unknown }[] } const tool = widget?.registeredTools?.find( (t) => t.id === ZOOM_TOGGLE_TOOL_ID, ) if (tool) { // No option, no onEvents — exercises the `?? {}` and default branches const result = tool.fn({}) as { option: { grid: { bottom: number } } } expect(result.option.grid.bottom).toBeGreaterThan(0) } }) it('handleDataZoom updates the store from `event.start/end`', () => { widgetStoreActions.setWidget('zt-dz-direct', { zoom: true }) renderWithTheme() const widget = widgetStoreActions.getWidget('zt-dz-direct') as unknown as { registeredTools?: { id: string; fn: (cfg: unknown) => unknown }[] } const tool = widget?.registeredTools?.find( (t) => t.id === ZOOM_TOGGLE_TOOL_ID, ) if (tool) { const result = tool.fn({ option: {} }) as { onEvents: { dataZoom: (event: unknown) => void } } act(() => { result.onEvents.dataZoom({ start: 10, end: 90 }) }) const w = widgetStoreActions.getWidget('zt-dz-direct') expect(w?.zoomStart).toBe(10) expect(w?.zoomEnd).toBe(90) } }) it('handleDataZoom falls back to `event.batch[0]` when start/end are missing', () => { widgetStoreActions.setWidget('zt-dz-batch', { zoom: true }) renderWithTheme() const widget = widgetStoreActions.getWidget('zt-dz-batch') as unknown as { registeredTools?: { id: string; fn: (cfg: unknown) => unknown }[] } const tool = widget?.registeredTools?.find( (t) => t.id === ZOOM_TOGGLE_TOOL_ID, ) if (tool) { const result = tool.fn({ option: {} }) as { onEvents: { dataZoom: (event: unknown) => void } } act(() => { result.onEvents.dataZoom({ batch: [{ start: 25, end: 75 }] }) }) const w = widgetStoreActions.getWidget('zt-dz-batch') expect(w?.zoomStart).toBe(25) expect(w?.zoomEnd).toBe(75) } }) it('handleDataZoom ignores events where start/end are still undefined', () => { widgetStoreActions.setWidget('zt-dz-noop', { zoom: true, zoomStart: 5, zoomEnd: 95, }) renderWithTheme() const widget = widgetStoreActions.getWidget('zt-dz-noop') as unknown as { registeredTools?: { id: string; fn: (cfg: unknown) => unknown }[] } const tool = widget?.registeredTools?.find( (t) => t.id === ZOOM_TOGGLE_TOOL_ID, ) if (tool) { const result = tool.fn({ option: {} }) as { onEvents: { dataZoom: (event: unknown) => void } } act(() => { result.onEvents.dataZoom({}) }) const w = widgetStoreActions.getWidget('zt-dz-noop') // unchanged expect(w?.zoomStart).toBe(5) expect(w?.zoomEnd).toBe(95) } }) })