import { enableAutoUnmount, flushPromises, mount, type VueWrapper } from '@vue/test-utils' import { nextTick, reactive } from 'vue' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import GeneratedPlotlyResult from '../../components/GeneratedPlotlyResult.vue' const plotly = vi.hoisted(() => ({ Plots: { resize: vi.fn() }, purge: vi.fn(), react: vi.fn(), })) vi.mock('plotly.js-dist-min', () => ({ default: plotly })) enableAutoUnmount(afterEach) async function mountAndRecover( fallback?: { media_type: string; data: string }, ): Promise { const wrapper = mount(GeneratedPlotlyResult, { props: { spec: { data: [] }, ...(fallback ? { fallback } : {}), }, }) await flushPromises() plotly.react.mockResolvedValue(undefined) await wrapper.setProps({ spec: { data: [{ type: 'scatter', x: [1], y: [2] }] }, }) await flushPromises() return wrapper } describe('GeneratedPlotlyResult', () => { beforeEach(() => { vi.clearAllMocks() plotly.react.mockRejectedValue(new Error('')) }) it('should render a safe error when Plotly fails without a fallback', async () => { const wrapper = mount(GeneratedPlotlyResult, { props: { spec: { data: [] } }, }) await flushPromises() expect(wrapper.get('[role="alert"]').text()).toBe('Unable to display this chart.') }) it('should render the static image when Plotly fails with a fallback', async () => { const wrapper = mount(GeneratedPlotlyResult, { props: { spec: { data: [] }, fallback: { media_type: 'image/png', data: 'cG5n' }, }, }) await flushPromises() expect(wrapper.get('img').attributes('src')).toBe('data:image/png;base64,cG5n') }) it('should render a static fallback without the error presentation', async () => { const wrapper = mount(GeneratedPlotlyResult, { props: { spec: { data: [] }, fallback: { media_type: 'image/png', data: 'cG5n' }, }, }) await flushPromises() expect(wrapper.find('.mint-plotly-chart__error').exists()).toBe(false) }) it('should not redraw an immutable result for nested spec mutations', async () => { plotly.react.mockResolvedValue(undefined) const wrapper = mount(GeneratedPlotlyResult, { props: { spec: { data: [{ x: [1] }] } }, }) await flushPromises() const spec = wrapper.props('spec') as { data: Array<{ x: number[] }> } spec.data[0]!.x.push(2) await nextTick() await flushPromises() expect(plotly.react).toHaveBeenCalledTimes(1) }) it('should redraw a replaced spec even when its native arrays are reused', async () => { plotly.react.mockResolvedValue(undefined) const spec = reactive({ data: [{ x: [1], y: [2] }], layout: {}, config: { displayModeBar: true } }) const wrapper = mount(GeneratedPlotlyResult, { props: { spec } }) await flushPromises() spec.data[0]!.x.push(3) spec.data[0]!.y.push(4) await wrapper.setProps({ spec: { ...spec } }) await flushPromises() expect(plotly.react).toHaveBeenCalledTimes(2) expect(plotly.react.mock.lastCall?.[1]).toEqual([{ x: [1, 3], y: [2, 4] }]) }) it.each([ ['Heatmap', { type: 'heatmap', z: [[1, 2], [3, 4]] }], ['Scatter3d', { type: 'scatter3d', x: [1], y: [2], z: [3] }], ])('should pass a %s trace to the Plotly renderer', async (_name, trace) => { plotly.react.mockResolvedValue(undefined) mount(GeneratedPlotlyResult, { props: { spec: { data: [trace] } }, }) await flushPromises() expect(plotly.react).toHaveBeenCalledWith( expect.any(HTMLElement), [trace], expect.objectContaining({ autosize: true }), { responsive: true, displaylogo: false, displayModeBar: false, showSendToCloud: false }, ) }) it('should retry Plotly when a new immutable spec follows a render failure', async () => { await mountAndRecover() expect(plotly.react).toHaveBeenCalledTimes(2) }) it('should remove the fallback after a new immutable spec renders successfully', async () => { const wrapper = await mountAndRecover({ media_type: 'image/png', data: 'cG5n' }) expect(wrapper.find('img').exists()).toBe(false) }) it('should remove the error after a new immutable spec renders successfully', async () => { const wrapper = await mountAndRecover() expect(wrapper.find('[role="alert"]').exists()).toBe(false) }) it('should restore the interactive chart after a new immutable spec renders successfully', async () => { const wrapper = await mountAndRecover() expect(wrapper.get('.mint-plotly-chart__plot').isVisible()).toBe(true) }) it('should reveal the interactive canvas before retrying Plotly', async () => { const wrapper = mount(GeneratedPlotlyResult, { props: { spec: { data: [] } }, }) await flushPromises() let displayDuringRetry: string | undefined plotly.react.mockImplementation(async (element: HTMLElement) => { displayDuringRetry = element.style.display }) await wrapper.setProps({ spec: { data: [{ type: 'scatter', x: [1], y: [2] }] }, }) await flushPromises() expect(displayDuringRetry).not.toBe('none') }) })