/**
* 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)
}
})
})