import { shallowMount, mount } from '@vue/test-utils'; import Tracking from '~/tracking'; import { ESC_KEY, ESC_KEY_IE11 } from '~/lib/utils/keys'; import { GlModal, GlDropdownItem, GlDeprecatedButton, GlIcon } from '@gitlab/ui'; import { objectToQuery } from '~/lib/utils/url_utility'; import VueDraggable from 'vuedraggable'; import MockAdapter from 'axios-mock-adapter'; import axios from '~/lib/utils/axios_utils'; import statusCodes from '~/lib/utils/http_status'; import { metricStates } from '~/monitoring/constants'; import Dashboard from '~/monitoring/components/dashboard.vue'; import DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_picker.vue'; import CustomMetricsFormFields from '~/custom_metrics/components/custom_metrics_form_fields.vue'; import DashboardsDropdown from '~/monitoring/components/dashboards_dropdown.vue'; import EmptyState from '~/monitoring/components/empty_state.vue'; import GroupEmptyState from '~/monitoring/components/group_empty_state.vue'; import DashboardPanel from '~/monitoring/components/dashboard_panel.vue'; import { createStore } from '~/monitoring/stores'; import * as types from '~/monitoring/stores/mutation_types'; import { setupAllDashboards, setupStoreWithDashboard, setMetricResult, setupStoreWithData, setupStoreWithVariable, } from '../store_utils'; import { environmentData, dashboardGitResponse, propsData } from '../mock_data'; import { metricsDashboardViewModel, metricsDashboardPanelCount } from '../fixture_data'; import createFlash from '~/flash'; jest.mock('~/flash'); describe('Dashboard', () => { let store; let wrapper; let mock; const findEnvironmentsDropdown = () => wrapper.find({ ref: 'monitorEnvironmentsDropdown' }); const findAllEnvironmentsDropdownItems = () => findEnvironmentsDropdown().findAll(GlDropdownItem); const setSearchTerm = searchTerm => { store.commit(`monitoringDashboard/${types.SET_ENVIRONMENTS_FILTER}`, searchTerm); }; const createShallowWrapper = (props = {}, options = {}) => { wrapper = shallowMount(Dashboard, { propsData: { ...propsData, ...props }, store, ...options, }); }; const createMountedWrapper = (props = {}, options = {}) => { wrapper = mount(Dashboard, { propsData: { ...propsData, ...props }, store, stubs: ['graph-group', 'dashboard-panel'], ...options, }); }; beforeEach(() => { store = createStore(); mock = new MockAdapter(axios); jest.spyOn(store, 'dispatch').mockResolvedValue(); }); afterEach(() => { mock.restore(); if (store.dispatch.mockReset) { store.dispatch.mockReset(); } }); describe('no metrics are available yet', () => { beforeEach(() => { createShallowWrapper(); }); it('shows the environment selector', () => { expect(findEnvironmentsDropdown().exists()).toBe(true); }); it('sets initial state', () => { expect(store.dispatch).toHaveBeenCalledWith('monitoringDashboard/setInitialState', { currentDashboard: '', currentEnvironmentName: 'production', dashboardEndpoint: 'https://invalid', dashboardsEndpoint: 'https://invalid', deploymentsEndpoint: null, logsPath: '/path/to/logs', metricsEndpoint: 'http://test.host/monitoring/mock', projectPath: '/path/to/project', }); }); }); describe('no data found', () => { beforeEach(() => { createShallowWrapper(); return wrapper.vm.$nextTick(); }); it('shows the environment selector dropdown', () => { expect(findEnvironmentsDropdown().exists()).toBe(true); }); }); describe('request information to the server', () => { it('calls to set time range and fetch data', () => { createShallowWrapper({ hasMetrics: true }); return wrapper.vm.$nextTick().then(() => { expect(store.dispatch).toHaveBeenCalledWith( 'monitoringDashboard/setTimeRange', expect.any(Object), ); expect(store.dispatch).toHaveBeenCalledWith('monitoringDashboard/fetchData', undefined); }); }); it('shows up a loading state', () => { store.state.monitoringDashboard.emptyState = 'loading'; createShallowWrapper({ hasMetrics: true }); return wrapper.vm.$nextTick().then(() => { expect(wrapper.find(EmptyState).exists()).toBe(true); expect(wrapper.find(EmptyState).props('selectedState')).toBe('loading'); }); }); it('hides the group panels when showPanels is false', () => { createMountedWrapper({ hasMetrics: true, showPanels: false }); setupStoreWithData(store); return wrapper.vm.$nextTick().then(() => { expect(wrapper.vm.showEmptyState).toEqual(false); expect(wrapper.findAll('.prometheus-panel')).toHaveLength(0); }); }); it('fetches the metrics data with proper time window', () => { jest.spyOn(store, 'dispatch'); createMountedWrapper({ hasMetrics: true }); store.commit( `monitoringDashboard/${types.RECEIVE_ENVIRONMENTS_DATA_SUCCESS}`, environmentData, ); return wrapper.vm.$nextTick().then(() => { expect(store.dispatch).toHaveBeenCalled(); }); }); }); describe('when the URL contains a reference to a panel', () => { let location; const setSearch = search => { window.location = { ...location, search }; }; beforeEach(() => { location = window.location; delete window.location; }); afterEach(() => { window.location = location; }); it('when the URL points to a panel it expands', () => { const panelGroup = metricsDashboardViewModel.panelGroups[0]; const panel = panelGroup.panels[0]; setSearch( objectToQuery({ group: panelGroup.group, title: panel.title, y_label: panel.y_label, }), ); createMountedWrapper({ hasMetrics: true }); setupStoreWithData(store); return wrapper.vm.$nextTick().then(() => { expect(store.dispatch).toHaveBeenCalledWith('monitoringDashboard/setExpandedPanel', { group: panelGroup.group, panel: expect.objectContaining({ title: panel.title, y_label: panel.y_label, }), }); }); }); it('when the URL does not link to any panel, no panel is expanded', () => { setSearch(''); createMountedWrapper({ hasMetrics: true }); setupStoreWithData(store); return wrapper.vm.$nextTick().then(() => { expect(store.dispatch).not.toHaveBeenCalledWith( 'monitoringDashboard/setExpandedPanel', expect.anything(), ); }); }); it('when the URL points to an incorrect panel it shows an error', () => { const panelGroup = metricsDashboardViewModel.panelGroups[0]; const panel = panelGroup.panels[0]; setSearch( objectToQuery({ group: panelGroup.group, title: 'incorrect', y_label: panel.y_label, }), ); createMountedWrapper({ hasMetrics: true }); setupStoreWithData(store); return wrapper.vm.$nextTick().then(() => { expect(createFlash).toHaveBeenCalled(); expect(store.dispatch).not.toHaveBeenCalledWith( 'monitoringDashboard/setExpandedPanel', expect.anything(), ); }); }); }); describe('when the panel is expanded', () => { let group; let panel; const expandPanel = (mockGroup, mockPanel) => { store.commit(`monitoringDashboard/${types.SET_EXPANDED_PANEL}`, { group: mockGroup, panel: mockPanel, }); }; beforeEach(() => { setupStoreWithData(store); const { panelGroups } = store.state.monitoringDashboard.dashboard; group = panelGroups[0].group; [panel] = panelGroups[0].panels; jest.spyOn(window.history, 'pushState').mockImplementation(); }); afterEach(() => { window.history.pushState.mockRestore(); }); it('URL is updated with panel parameters', () => { createMountedWrapper({ hasMetrics: true }); expandPanel(group, panel); const expectedSearch = objectToQuery({ group, title: panel.title, y_label: panel.y_label, }); return wrapper.vm.$nextTick(() => { expect(window.history.pushState).toHaveBeenCalledTimes(1); expect(window.history.pushState).toHaveBeenCalledWith( expect.anything(), // state expect.any(String), // document title expect.stringContaining(`${expectedSearch}`), ); }); }); it('URL is updated with panel parameters and custom dashboard', () => { const dashboard = 'dashboard.yml'; createMountedWrapper({ hasMetrics: true, currentDashboard: dashboard }); expandPanel(group, panel); const expectedSearch = objectToQuery({ dashboard, group, title: panel.title, y_label: panel.y_label, }); return wrapper.vm.$nextTick(() => { expect(window.history.pushState).toHaveBeenCalledTimes(1); expect(window.history.pushState).toHaveBeenCalledWith( expect.anything(), // state expect.any(String), // document title expect.stringContaining(`${expectedSearch}`), ); }); }); it('URL is updated with no parameters', () => { expandPanel(group, panel); createMountedWrapper({ hasMetrics: true }); expandPanel(null, null); return wrapper.vm.$nextTick(() => { expect(window.history.pushState).toHaveBeenCalledTimes(1); expect(window.history.pushState).toHaveBeenCalledWith( expect.anything(), // state expect.any(String), // document title expect.not.stringMatching(/group|title|y_label/), // no panel params ); }); }); }); describe('when all requests have been commited by the store', () => { beforeEach(() => { createMountedWrapper({ hasMetrics: true }); setupStoreWithData(store); return wrapper.vm.$nextTick(); }); it('renders the environments dropdown with a number of environments', () => { expect(findAllEnvironmentsDropdownItems().length).toEqual(environmentData.length); findAllEnvironmentsDropdownItems().wrappers.forEach((itemWrapper, index) => { const anchorEl = itemWrapper.find('a'); if (anchorEl.exists() && environmentData[index].metrics_path) { const href = anchorEl.attributes('href'); expect(href).toBe(environmentData[index].metrics_path); } }); }); it('renders the environments dropdown with a single active element', () => { const activeItem = findAllEnvironmentsDropdownItems().wrappers.filter(itemWrapper => itemWrapper.find('.active').exists(), ); expect(activeItem.length).toBe(1); }); }); describe('star dashboards', () => { const findToggleStar = () => wrapper.find({ ref: 'toggleStarBtn' }); const findToggleStarIcon = () => findToggleStar().find(GlIcon); beforeEach(() => { createShallowWrapper(); setupAllDashboards(store); }); it('toggle star button is shown', () => { expect(findToggleStar().exists()).toBe(true); expect(findToggleStar().props('disabled')).toBe(false); }); it('toggle star button is disabled when starring is taking place', () => { store.commit(`monitoringDashboard/${types.REQUEST_DASHBOARD_STARRING}`); return wrapper.vm.$nextTick(() => { expect(findToggleStar().exists()).toBe(true); expect(findToggleStar().props('disabled')).toBe(true); }); }); describe('when the dashboard list is loaded', () => { // Tooltip element should wrap directly const getToggleTooltip = () => findToggleStar().element.parentElement.getAttribute('title'); beforeEach(() => { setupAllDashboards(store); jest.spyOn(store, 'dispatch'); }); it('dispatches a toggle star action', () => { findToggleStar().vm.$emit('click'); return wrapper.vm.$nextTick().then(() => { expect(store.dispatch).toHaveBeenCalledWith( 'monitoringDashboard/toggleStarredValue', undefined, ); }); }); describe('when dashboard is not starred', () => { beforeEach(() => { store.commit(`monitoringDashboard/${types.SET_INITIAL_STATE}`, { currentDashboard: dashboardGitResponse[0].path, }); return wrapper.vm.$nextTick(); }); it('toggle star button shows "Star dashboard"', () => { expect(getToggleTooltip()).toBe('Star dashboard'); }); it('toggle star button shows an unstarred state', () => { expect(findToggleStarIcon().attributes('name')).toBe('star-o'); }); }); describe('when dashboard is starred', () => { beforeEach(() => { store.commit(`monitoringDashboard/${types.SET_INITIAL_STATE}`, { currentDashboard: dashboardGitResponse[1].path, }); return wrapper.vm.$nextTick(); }); it('toggle star button shows "Star dashboard"', () => { expect(getToggleTooltip()).toBe('Unstar dashboard'); }); it('toggle star button shows a starred state', () => { expect(findToggleStarIcon().attributes('name')).toBe('star'); }); }); }); }); it('hides the environments dropdown list when there is no environments', () => { createMountedWrapper({ hasMetrics: true }); setupStoreWithDashboard(store); return wrapper.vm.$nextTick().then(() => { expect(findAllEnvironmentsDropdownItems()).toHaveLength(0); }); }); it('renders the datetimepicker dropdown', () => { createMountedWrapper({ hasMetrics: true }); setupStoreWithData(store); return wrapper.vm.$nextTick().then(() => { expect(wrapper.find(DateTimePicker).exists()).toBe(true); }); }); it('renders the refresh dashboard button', () => { createMountedWrapper({ hasMetrics: true }); setupStoreWithData(store); return wrapper.vm.$nextTick().then(() => { const refreshBtn = wrapper.findAll({ ref: 'refreshDashboardBtn' }); expect(refreshBtn).toHaveLength(1); expect(refreshBtn.is(GlDeprecatedButton)).toBe(true); }); }); describe('variables section', () => { beforeEach(() => { createShallowWrapper({ hasMetrics: true }); setupStoreWithData(store); setupStoreWithVariable(store); return wrapper.vm.$nextTick(); }); it('shows the variables section', () => { expect(wrapper.vm.shouldShowVariablesSection).toBe(true); }); }); describe('single panel expands to "full screen" mode', () => { const findExpandedPanel = () => wrapper.find({ ref: 'expandedPanel' }); describe('when the panel is not expanded', () => { beforeEach(() => { createShallowWrapper({ hasMetrics: true }); setupStoreWithData(store); return wrapper.vm.$nextTick(); }); it('expanded panel is not visible', () => { expect(findExpandedPanel().isVisible()).toBe(false); }); it('can set a panel as expanded', () => { const panel = wrapper.findAll(DashboardPanel).at(1); jest.spyOn(store, 'dispatch'); panel.vm.$emit('expand'); const groupData = metricsDashboardViewModel.panelGroups[0]; expect(store.dispatch).toHaveBeenCalledWith('monitoringDashboard/setExpandedPanel', { group: groupData.group, panel: expect.objectContaining({ id: groupData.panels[0].id, }), }); }); }); describe('when the panel is expanded', () => { let group; let panel; const mockKeyup = key => window.dispatchEvent(new KeyboardEvent('keyup', { key })); const MockPanel = { template: `