diff --git a/src/components/charts/area/area.spec.js b/src/components/charts/area/area.spec.js index e3477a1d5302552cd17f864cc63b0e46c21d8693..3699f071a4b2051af5693d30d7bf13593784a27d 100644 --- a/src/components/charts/area/area.spec.js +++ b/src/components/charts/area/area.spec.js @@ -3,7 +3,7 @@ import { shallowMount } from '@vue/test-utils'; import { LEGEND_LAYOUT_INLINE, LEGEND_LAYOUT_TABLE } from '~/utils/charts/constants'; import { - createMockChartInstance, + mockCreateChartInstance, ChartTooltipStub, chartTooltipStubData, } from '~helpers/chart_stubs'; @@ -40,7 +40,7 @@ describe('area component', () => { }; beforeEach(() => { - mockChartInstance = createMockChartInstance(); + mockChartInstance = mockCreateChartInstance(); }); it('emits `created`, with the chart instance', async () => { diff --git a/src/components/charts/bar/bar.spec.js b/src/components/charts/bar/bar.spec.js index b4229ff274289cd751acb0b1122dde3f3207fa4c..0446b679c6bcd003cfab7da35b1e89c0ddbdc4dc 100644 --- a/src/components/charts/bar/bar.spec.js +++ b/src/components/charts/bar/bar.spec.js @@ -1,5 +1,5 @@ import { shallowMount } from '@vue/test-utils'; -import { createMockChartInstance } from '~helpers/chart_stubs'; +import { mockCreateChartInstance } from '~helpers/chart_stubs'; import { expectHeightAutoClasses } from '~helpers/chart_height'; import Chart from '../chart/chart.vue'; import ChartTooltip from '../shared/tooltip/tooltip.vue'; @@ -39,7 +39,7 @@ describe('Bar chart component', () => { }; beforeEach(() => { - mockChartInstance = createMockChartInstance(); + mockChartInstance = mockCreateChartInstance(); }); describe('when mounted', () => { diff --git a/src/components/charts/chart/chart.spec.js b/src/components/charts/chart/chart.spec.js index 0bb0435f17d86066cce1586921e3c9719e172163..cf1744c8af3adb4cb22c85f55c04450a07919db1 100644 --- a/src/components/charts/chart/chart.spec.js +++ b/src/components/charts/chart/chart.spec.js @@ -1,38 +1,45 @@ +import { nextTick } from 'vue'; import { shallowMount } from '@vue/test-utils'; import * as echarts from 'echarts'; import { toolboxHeight } from '~/utils/charts/config'; import { createTheme } from '~/utils/charts/theme'; import { waitForAnimationFrame } from '~/utils/test_utils'; import { useMockResizeObserver } from '~helpers/mock_dom_observer'; +import { mockCreateChartInstance } from '~helpers/chart_stubs'; import Chart from './chart.vue'; -const defaultHeight = 400; +let mockChartInstance; -jest.mock('echarts', () => ({ - init: jest.fn(() => ({ - setOption: jest.fn(), - resize: jest.fn(), - on: jest.fn(), - off: jest.fn(), - })), - registerTheme: jest.fn(), -})); +jest.mock('echarts', () => { + return { + init: jest.fn(() => { + mockChartInstance = mockCreateChartInstance(); + return mockChartInstance; + }), + registerTheme: jest.fn(), + }; +}); describe('chart component', () => { - const options = { - aria: { - enabled: true, - }, - }; - const mountArgs = [Chart, { propsData: { options: {} } }]; const themeName = 'gitlab'; + const defaultHeight = 400; + let wrapper; + const createWrapper = (props) => { + wrapper = shallowMount(Chart, { + propsData: { + options: {}, + ...props, + }, + }); + }; + const { trigger: triggerResize } = useMockResizeObserver(); it('initializes chart using $refs.chart', async () => { - wrapper = shallowMount(...mountArgs); - await wrapper.vm.$nextTick(); + createWrapper(); + await nextTick(); expect(echarts.init).toHaveBeenCalledWith( wrapper.findComponent({ ref: 'chart' }).element, @@ -46,8 +53,10 @@ describe('chart component', () => { }); it('does not resize the chart when responsive = false', async () => { - wrapper = shallowMount(Chart, { propsData: { options: {}, responsive: false } }); - await wrapper.vm.$nextTick(); + createWrapper({ + responsive: false, + }); + await nextTick(); // initial call when chart gets created expect(wrapper.vm.chart.resize).toHaveBeenCalledTimes(1); @@ -58,8 +67,9 @@ describe('chart component', () => { }); it('resizes the chart only once per animation frame when responsive = true', async () => { - wrapper = shallowMount(...mountArgs); - await wrapper.vm.$nextTick(); + createWrapper(); + await nextTick(); + expect(wrapper.vm.chart.resize).toHaveBeenCalledTimes(1); triggerResize(wrapper.element); @@ -70,87 +80,128 @@ describe('chart component', () => { }); it('emits "created" after initializing chart', async () => { - wrapper = shallowMount(...mountArgs); - await wrapper.vm.$nextTick(); + createWrapper(); + await nextTick(); + expect(wrapper.emitted('created')).toEqual([[wrapper.vm.chart]]); }); it('uses GitLab theme', () => { - wrapper = shallowMount(...mountArgs); + createWrapper(); + const [firstRegisterThemeCall] = echarts.registerTheme.mock.calls; expect(firstRegisterThemeCall[0]).toBe(themeName); expect(JSON.stringify(firstRegisterThemeCall[1])).toEqual(JSON.stringify(createTheme())); }); it('waits a tick before creating the chart', async () => { - wrapper = shallowMount(...mountArgs); + createWrapper(); expect(wrapper.vm.chart).toBe(null); - await wrapper.vm.$nextTick(); + await nextTick(); expect(wrapper.vm.chart).toBeDefined(); }); - describe('custom sizing', () => { - const width = 1234; - const height = 123; - - it('sets chart to custom width and height if specified', async () => { - wrapper = shallowMount(Chart, { propsData: { options: {}, width, height } }); - - await wrapper.vm.$nextTick(); - - expect(wrapper.vm.chart.resize).toHaveBeenCalledWith({ width, height }); + describe('sets chart options', () => { + beforeEach(async () => { + createWrapper({ + options: { + title: { text: 'My chart title' }, + }, + }); + await nextTick(); }); - it('sets chart to custom width if specified', async () => { - wrapper = shallowMount(Chart, { propsData: { options: {}, width } }); - - await wrapper.vm.$nextTick(); + it('inits chart options, adding aria.enabled', async () => { + expect(wrapper.vm.chart.setOption).toHaveBeenCalledTimes(1); + expect(wrapper.vm.chart.setOption).toHaveBeenCalledWith({ + title: { text: 'My chart title' }, + aria: { enabled: true }, + }); - expect(wrapper.vm.chart.resize).toHaveBeenCalledWith({ width, height: defaultHeight }); + expect(wrapper.emitted('updated').length).toEqual(1); }); - it('sets chart to custom height if specified', async () => { - wrapper = shallowMount(Chart, { propsData: { options: {}, height } }); - - await wrapper.vm.$nextTick(); + it('reacts to options changes by merging options', async () => { + wrapper.setProps({ + options: { + title: { text: 'My new chart title' }, + xAxis: { show: true }, + }, + }); + await nextTick(); - expect(wrapper.vm.chart.resize).toHaveBeenCalledWith({ width: 'auto', height }); + expect(wrapper.vm.chart.setOption).toHaveBeenCalledTimes(2); + expect(wrapper.vm.chart.setOption).toHaveBeenLastCalledWith({ + title: { text: 'My new chart title' }, + xAxis: { show: true }, + aria: { enabled: true }, + }); + expect(wrapper.emitted('updated').length).toEqual(2); }); }); - describe('methods', () => { - beforeEach(() => { - wrapper = shallowMount(...mountArgs); - }); + describe('sets chart dimensions', () => { + it('sets dimensions', async () => { + createWrapper(); + await nextTick(); - describe('draw method', () => { - it('sets chart options', () => { - expect(wrapper.vm.chart.setOption).toHaveBeenCalledTimes(1); + expect(wrapper.vm.chart.resize).toHaveBeenCalledTimes(1); + expect(wrapper.vm.chart.resize).toHaveBeenLastCalledWith({ + height: 400, + width: 'auto', + }); + }); - wrapper.vm.draw(); + it('sets custom dimensions', async () => { + const width = 1234; + const height = 123; - expect(wrapper.vm.chart.setOption).toHaveBeenCalledTimes(2); - expect(wrapper.vm.chart.setOption).toHaveBeenCalledWith(options); + createWrapper({ + width, + height, }); - it('emits chart updated after drawing', () => { - wrapper.vm.draw(); - const { chart } = wrapper.vm; + await nextTick(); - expect(wrapper.emitted('updated')).toEqual([[chart], [chart]]); + expect(wrapper.vm.chart.resize).toHaveBeenCalledTimes(1); + expect(wrapper.vm.chart.resize).toHaveBeenLastCalledWith({ + width, + height, }); }); - describe('setChartSize method', () => { - it('sets chart size', () => { - wrapper.vm.setChartSize(); + describe('reacts to dimension changes', () => { + beforeEach(async () => { + createWrapper(); + await nextTick(); + }); + + it('reacts to width changes', async () => { + wrapper.setProps({ + width: 301, + }); + await nextTick(); - expect(wrapper.vm.chart.resize).toHaveBeenCalledWith({ + expect(wrapper.vm.chart.resize).toHaveBeenCalledTimes(2); + expect(wrapper.vm.chart.resize).toHaveBeenLastCalledWith({ + width: 301, height: defaultHeight, + }); + }); + + it('reacts to height changes', async () => { + wrapper.setProps({ + height: 401, + }); + await nextTick(); + + expect(wrapper.vm.chart.resize).toHaveBeenCalledTimes(2); + expect(wrapper.vm.chart.resize).toHaveBeenCalledWith({ width: 'auto', + height: 401, }); }); }); @@ -160,7 +211,7 @@ describe('chart component', () => { it('increases grid top by `toolboxHeight`', async () => { const optionsWithToolbox = { toolbox: { show: true } }; wrapper = shallowMount(Chart, { propsData: { options: optionsWithToolbox } }); - await wrapper.vm.$nextTick(); + await nextTick(); expect(wrapper.vm.chart.setOption).toHaveBeenCalledWith({ ...optionsWithToolbox, diff --git a/src/components/charts/chart/chart.stories.js b/src/components/charts/chart/chart.stories.js index 492c702a1c60e4009795795e7d093d0648af4053..ef998678c7aa9d8d2c4ddf65f265810b9fcc866c 100644 --- a/src/components/charts/chart/chart.stories.js +++ b/src/components/charts/chart/chart.stories.js @@ -3,58 +3,54 @@ import GlTab from '../../base/tabs/tab/tab.vue'; import GlChart from './chart.vue'; import readme from './chart.md'; +const generateProps = ({ options } = {}) => ({ + options: { + xAxis: { + type: 'category', + data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'], + }, + yAxis: { + type: 'value', + }, + series: [ + { + data: [820, 932, 901, 934, 1290, 1330, 1320], + type: 'bar', + }, + ], + ...options, + }, +}); + const Template = (args, { argTypes = {} }) => ({ + components: { GlChart }, + props: Object.keys(argTypes), + template: '', + ...args, +}); + +export const Default = Template.bind({}); +Default.args = generateProps(); + +export const Tab = (args, { argTypes }) => ({ components: { GlChart, GlTabs, GlTab, }, props: Object.keys(argTypes), - data() { - return { - options: { - xAxis: { - type: 'category', - data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'], - }, - yAxis: { - type: 'value', - }, - series: [ - { - data: [820, 932, 901, 934, 1290, 1330, 1320], - type: 'bar', - }, - ], - }, - }; - }, - ...args, + template: ` + + + + + + `, }); +Tab.args = generateProps(); -export const Default = Template.bind( - {}, - { - template: ` - - `, - } -); - -export const Tab = Template.bind( - {}, - { - template: ` - - - - - - `, - } -); +export const WithToolbox = Template.bind({}); +WithToolbox.args = generateProps({ options: { toolbox: { show: true } } }); export const Connected = Template.bind( {}, diff --git a/src/components/charts/column/column_chart.spec.js b/src/components/charts/column/column_chart.spec.js index 37e731a4aaabd4085b2c899041a380735356db5b..54193a64dc28790e0ec8a43b1dd67cab3d0d7f90 100644 --- a/src/components/charts/column/column_chart.spec.js +++ b/src/components/charts/column/column_chart.spec.js @@ -1,7 +1,7 @@ import { nextTick } from 'vue'; import { shallowMount } from '@vue/test-utils'; import { - createMockChartInstance, + mockCreateChartInstance, ChartTooltipStub, chartTooltipStubData, } from '~helpers/chart_stubs'; @@ -50,7 +50,7 @@ describe('column chart component', () => { beforeEach(() => { factory(); - mockChartInstance = createMockChartInstance(); + mockChartInstance = mockCreateChartInstance(); emitChartCreated(); }); @@ -132,7 +132,7 @@ describe('column chart component', () => { 'chart-tooltip': ChartTooltipStub, }, }); - mockChartInstance = createMockChartInstance(); + mockChartInstance = mockCreateChartInstance(); emitChartCreated(); await nextTick(); @@ -157,7 +157,7 @@ describe('column chart component', () => { }, }); - mockChartInstance = createMockChartInstance(); + mockChartInstance = mockCreateChartInstance(); emitChartCreated(); await nextTick(); @@ -177,7 +177,7 @@ describe('column chart component', () => { }, }); - mockChartInstance = createMockChartInstance(); + mockChartInstance = mockCreateChartInstance(); emitChartCreated(); await nextTick(); @@ -200,7 +200,7 @@ describe('column chart component', () => { }, }); - mockChartInstance = createMockChartInstance(); + mockChartInstance = mockCreateChartInstance(); emitChartCreated(); await nextTick(); diff --git a/src/components/charts/discrete_scatter/discrete_scatter.spec.js b/src/components/charts/discrete_scatter/discrete_scatter.spec.js index e5bd5cda1b5e131453d6c725fbb6008f0e0a9c11..c2ade4ebffc16929c5f0ae7a677690e86fdc067b 100644 --- a/src/components/charts/discrete_scatter/discrete_scatter.spec.js +++ b/src/components/charts/discrete_scatter/discrete_scatter.spec.js @@ -1,6 +1,6 @@ import { shallowMount } from '@vue/test-utils'; import { nextTick } from 'vue'; -import { createMockChartInstance } from '~helpers/chart_stubs'; +import { mockCreateChartInstance } from '~helpers/chart_stubs'; import { expectHeightAutoClasses } from '~helpers/chart_height'; import Chart from '../chart/chart.vue'; import ChartTooltip from '../shared/tooltip/tooltip.vue'; @@ -41,7 +41,7 @@ describe('column chart component', () => { }; beforeEach(() => { - mockChartInstance = createMockChartInstance(); + mockChartInstance = mockCreateChartInstance(); mockChartInstance.convertToPixel.mockReturnValue([mockDataPixel.x, mockDataPixel.y]); }); diff --git a/src/components/charts/gauge/gauge.spec.js b/src/components/charts/gauge/gauge.spec.js index 2dc778bec3ece6cbf5c6a8bb82c73e32359d1a60..f43e00fcca36cd5b9a3aee78d2cca8d8412a473f 100644 --- a/src/components/charts/gauge/gauge.spec.js +++ b/src/components/charts/gauge/gauge.spec.js @@ -1,5 +1,5 @@ import { shallowMount } from '@vue/test-utils'; -import { createMockChartInstance } from '~helpers/chart_stubs'; +import { mockCreateChartInstance } from '~helpers/chart_stubs'; import { gaugeSafeHues, gaugeWarningHue } from '../../../utils/charts/theme'; import Chart from '../chart/chart.vue'; import GlGauge from './gauge.vue'; @@ -37,7 +37,7 @@ describe('gauge component', () => { }; beforeEach(() => { - mockChartInstance = createMockChartInstance(); + mockChartInstance = mockCreateChartInstance(); }); it('emits `created`, with the chart instance', () => { diff --git a/src/components/charts/heatmap/heatmap.spec.js b/src/components/charts/heatmap/heatmap.spec.js index fe4270bdf93d0eb7e899c950b819c8ff5b1714ce..273b2e33b797ad83b1207b6ad387a3e57e9c335f 100644 --- a/src/components/charts/heatmap/heatmap.spec.js +++ b/src/components/charts/heatmap/heatmap.spec.js @@ -1,7 +1,7 @@ import { shallowMount } from '@vue/test-utils'; import { nextTick } from 'vue'; -import { createMockChartInstance } from '~helpers/chart_stubs'; +import { mockCreateChartInstance } from '~helpers/chart_stubs'; import { expectHeightAutoClasses } from '~helpers/chart_height'; import Chart from '../chart/chart.vue'; import ChartTooltip from '../shared/tooltip/tooltip.vue'; @@ -35,7 +35,7 @@ describe('heatmap component', () => { }; beforeEach(async () => { - mockChartInstance = createMockChartInstance(); + mockChartInstance = mockCreateChartInstance(); }); it('emits `created`, with the chart instance', () => { diff --git a/src/components/charts/legend/legend.spec.js b/src/components/charts/legend/legend.spec.js index aa8813acc36c5f7a9572cc094441c0682496acb2..f5f5918d4f744da93cc25a65cf39a277b9b9571f 100644 --- a/src/components/charts/legend/legend.spec.js +++ b/src/components/charts/legend/legend.spec.js @@ -1,5 +1,5 @@ import { shallowMount } from '@vue/test-utils'; -import { createMockChartInstance } from '~helpers/chart_stubs'; +import { mockCreateChartInstance } from '~helpers/chart_stubs'; import Chart from '../chart/chart.vue'; import GlChartSeriesLabel from '../series_label/series_label.vue'; import Legend from './legend.vue'; @@ -71,7 +71,7 @@ describe('chart legend component', () => { }; beforeEach(async () => { - mockChartInstance = createMockChartInstance(); + mockChartInstance = mockCreateChartInstance(); chartWrapper = shallowMount(...chartArgs); await chartWrapper.vm.$nextTick(); diff --git a/src/components/charts/line/line.spec.js b/src/components/charts/line/line.spec.js index 8e2ce32c5e3dc8ea261bf806d7d5db149b5b7c4e..13b2f41094c7e8e9921e49781120c7afb518d869 100644 --- a/src/components/charts/line/line.spec.js +++ b/src/components/charts/line/line.spec.js @@ -3,7 +3,7 @@ import { shallowMount } from '@vue/test-utils'; import { LEGEND_LAYOUT_INLINE, LEGEND_LAYOUT_TABLE } from '~/utils/charts/constants'; import { - createMockChartInstance, + mockCreateChartInstance, ChartTooltipStub, chartTooltipStubData, } from '~helpers/chart_stubs'; @@ -43,7 +43,7 @@ describe('line component', () => { }; beforeEach(() => { - mockChartInstance = createMockChartInstance(); + mockChartInstance = mockCreateChartInstance(); }); it('emits `created`, with the chart instance', async () => { diff --git a/src/components/charts/shared/tooltip/tooltip.spec.js b/src/components/charts/shared/tooltip/tooltip.spec.js index 1f0a09cb6234611aee9d1187e6a32e8c2ffeb569..fcf8b9ae6f7b681f52f0fbfa68735d911e6302ea 100644 --- a/src/components/charts/shared/tooltip/tooltip.spec.js +++ b/src/components/charts/shared/tooltip/tooltip.spec.js @@ -1,6 +1,6 @@ import { nextTick } from 'vue'; import { shallowMount } from '@vue/test-utils'; -import { createMockChartInstance } from '~helpers/chart_stubs'; +import { mockCreateChartInstance } from '~helpers/chart_stubs'; import GlPopover from '../../../base/popover/popover.vue'; import { popoverPlacements } from '../../../../utils/constants'; import { waitForAnimationFrame } from '../../../../utils/test_utils'; @@ -31,7 +31,7 @@ describe('ChartTooltip', () => { const createWrapper = (props = {}, options) => { mockChartInstance = { - ...createMockChartInstance(), + ...mockCreateChartInstance(), containPixel: mockContainPixel, }; diff --git a/src/components/charts/sparkline/sparkline.spec.js b/src/components/charts/sparkline/sparkline.spec.js index 7cc096670fd93c367bfa227355d9530fe617ef7b..d839e0379dbcd602f97da274d32f81283be5eef7 100644 --- a/src/components/charts/sparkline/sparkline.spec.js +++ b/src/components/charts/sparkline/sparkline.spec.js @@ -1,7 +1,7 @@ import { shallowMount } from '@vue/test-utils'; import { waitForAnimationFrame } from '~/utils/test_utils'; import { HEIGHT_AUTO_HORIZONTAL_LAYOUT_CLASSES } from '~/utils/charts/constants'; -import { createMockChartInstance, ChartTooltipStub } from '~helpers/chart_stubs'; +import { mockCreateChartInstance, ChartTooltipStub } from '~helpers/chart_stubs'; import { expectHeightAutoClasses } from '~helpers/chart_height'; import Chart from '../chart/chart.vue'; import SparklineChart from './sparkline.vue'; @@ -72,7 +72,7 @@ describe('sparkline chart component', () => { const emitChartCreated = () => getChart().vm.$emit('created', mockChartInstance); beforeEach(() => { - mockChartInstance = createMockChartInstance(); + mockChartInstance = mockCreateChartInstance(); factory(); // needs to run after every mount, or the chart-instance is `null` and `beforeDestroy` throws emitChartCreated(); diff --git a/src/components/charts/stacked_column/stacked_column.spec.js b/src/components/charts/stacked_column/stacked_column.spec.js index f4c11528defadeb52ebc92352e5c7edda020d34f..562b7b796f72a35a80916a1004eeb70866f96d02 100644 --- a/src/components/charts/stacked_column/stacked_column.spec.js +++ b/src/components/charts/stacked_column/stacked_column.spec.js @@ -2,7 +2,7 @@ import { nextTick } from 'vue'; import { shallowMount } from '@vue/test-utils'; import { - createMockChartInstance, + mockCreateChartInstance, ChartTooltipStub, chartTooltipStubData, } from '~helpers/chart_stubs'; @@ -58,7 +58,7 @@ describe('stacked column chart component', () => { }; beforeEach(() => { - mockChartInstance = createMockChartInstance(); + mockChartInstance = mockCreateChartInstance(); }); afterEach(() => { @@ -99,7 +99,7 @@ describe('stacked column chart component', () => { describe('legend', () => { beforeEach(() => { mockChartInstance = { - ...createMockChartInstance(), + ...mockCreateChartInstance(), getOption: jest.fn().mockReturnValueOnce({ series: [ { diff --git a/tests/__helpers__/chart_stubs.js b/tests/__helpers__/chart_stubs.js index b57962a641d3d3b432bd65ece163e0042a41718f..5251e68637c6c27acc46cf67b60c93fe1e3487de 100644 --- a/tests/__helpers__/chart_stubs.js +++ b/tests/__helpers__/chart_stubs.js @@ -1,6 +1,6 @@ import ChartTooltip from '~/components/charts/shared/tooltip/tooltip.vue'; -export const createMockChartInstance = () => { +export const mockCreateChartInstance = () => { const dom = { addEventListener: jest.fn(), removeEventListener: jest.fn(), @@ -10,20 +10,26 @@ export const createMockChartInstance = () => { on: jest.fn(), off: jest.fn(), }; + let opts = null; return { // temporary workaround to ensure compatibility with @vue/compat __v_isReadonly: true, + setOption: jest.fn((o) => { + opts = o; + }), + getOption: jest.fn(() => ({ + series: [], // mocks empty, but present data as if added by echarts + ...opts, + })), + dispatchAction: jest.fn(), - setOption: jest.fn(), on: jest.fn(), off: jest.fn(), resize: jest.fn(), convertToPixel: jest.fn().mockReturnValue([]), - getOption: jest.fn().mockReturnValue({ - series: [], - }), + getDom: () => { return dom; }, diff --git a/tests/__image_snapshots__/storyshots-spec-js-image-storyshots-charts-chart-with-toolbox-1-snap.png b/tests/__image_snapshots__/storyshots-spec-js-image-storyshots-charts-chart-with-toolbox-1-snap.png new file mode 100644 index 0000000000000000000000000000000000000000..1e4660ce842625ccba5c6147423daf052eb58a76 Binary files /dev/null and b/tests/__image_snapshots__/storyshots-spec-js-image-storyshots-charts-chart-with-toolbox-1-snap.png differ