diff options
Diffstat (limited to 'spec/frontend/vue_shared')
41 files changed, 1495 insertions, 667 deletions
diff --git a/spec/frontend/vue_shared/alert_details/alert_details_spec.js b/spec/frontend/vue_shared/alert_details/alert_details_spec.js index 7ee6e29e6de..7aa54a1c55a 100644 --- a/spec/frontend/vue_shared/alert_details/alert_details_spec.js +++ b/spec/frontend/vue_shared/alert_details/alert_details_spec.js @@ -12,12 +12,17 @@ import AlertSummaryRow from '~/vue_shared/alert_details/components/alert_summary import { PAGE_CONFIG, SEVERITY_LEVELS } from '~/vue_shared/alert_details/constants'; import createIssueMutation from '~/vue_shared/alert_details/graphql/mutations/alert_issue_create.mutation.graphql'; import AlertDetailsTable from '~/vue_shared/components/alert_details_table.vue'; +import MetricImagesTab from '~/vue_shared/components/metric_images/metric_images_tab.vue'; +import createStore from '~/vue_shared/components/metric_images/store/'; +import service from '~/vue_shared/alert_details/service'; import mockAlerts from './mocks/alerts.json'; const mockAlert = mockAlerts[0]; const environmentName = 'Production'; const environmentPath = '/fake/path'; +jest.mock('~/vue_shared/alert_details/service'); + describe('AlertDetails', () => { let environmentData = { name: environmentName, path: environmentPath }; let mock; @@ -67,9 +72,11 @@ describe('AlertDetails', () => { $route: { params: {} }, }, stubs: { - ...stubs, AlertSummaryRow, + 'metric-images-tab': true, + ...stubs, }, + store: createStore({}, service), }), ); } @@ -91,7 +98,7 @@ describe('AlertDetails', () => { const findEnvironmentName = () => wrapper.findByTestId('environmentName'); const findEnvironmentPath = () => wrapper.findByTestId('environmentPath'); const findDetailsTable = () => wrapper.findComponent(AlertDetailsTable); - const findMetricsTab = () => wrapper.findByTestId('metrics'); + const findMetricsTab = () => wrapper.findComponent(MetricImagesTab); describe('Alert details', () => { describe('when alert is null', () => { @@ -129,8 +136,21 @@ describe('AlertDetails', () => { expect(wrapper.findByTestId('startTimeItem').exists()).toBe(true); expect(wrapper.findByTestId('startTimeItem').props('time')).toBe(mockAlert.startedAt); }); + }); + + describe('Metrics tab', () => { + it('should mount without errors', () => { + mountComponent({ + mountMethod: mount, + provide: { + canUpdate: true, + iid: '1', + }, + stubs: { + MetricImagesTab, + }, + }); - it('renders the metrics tab', () => { expect(findMetricsTab().exists()).toBe(true); }); }); @@ -312,7 +332,9 @@ describe('AlertDetails', () => { describe('header', () => { const findHeader = () => wrapper.findByTestId('alert-header'); - const stubs = { TimeAgoTooltip: { template: '<span>now</span>' } }; + const stubs = { + TimeAgoTooltip: { template: '<span>now</span>' }, + }; describe('individual header fields', () => { describe.each` diff --git a/spec/frontend/vue_shared/alert_details/service_spec.js b/spec/frontend/vue_shared/alert_details/service_spec.js new file mode 100644 index 00000000000..790854d0ca7 --- /dev/null +++ b/spec/frontend/vue_shared/alert_details/service_spec.js @@ -0,0 +1,44 @@ +import { fileList, fileListRaw } from 'jest/vue_shared/components/metric_images/mock_data'; +import { + getMetricImages, + uploadMetricImage, + updateMetricImage, + deleteMetricImage, +} from '~/vue_shared/alert_details/service'; +import * as alertManagementAlertsApi from '~/api/alert_management_alerts_api'; + +jest.mock('~/api/alert_management_alerts_api'); + +describe('Alert details service', () => { + it('fetches metric images', async () => { + alertManagementAlertsApi.fetchAlertMetricImages.mockResolvedValue({ data: fileListRaw }); + const result = await getMetricImages(); + + expect(alertManagementAlertsApi.fetchAlertMetricImages).toHaveBeenCalled(); + expect(result).toEqual(fileList); + }); + + it('uploads a metric image', async () => { + alertManagementAlertsApi.uploadAlertMetricImage.mockResolvedValue({ data: fileListRaw[0] }); + const result = await uploadMetricImage(); + + expect(alertManagementAlertsApi.uploadAlertMetricImage).toHaveBeenCalled(); + expect(result).toEqual(fileList[0]); + }); + + it('updates a metric image', async () => { + alertManagementAlertsApi.updateAlertMetricImage.mockResolvedValue({ data: fileListRaw[0] }); + const result = await updateMetricImage(); + + expect(alertManagementAlertsApi.updateAlertMetricImage).toHaveBeenCalled(); + expect(result).toEqual(fileList[0]); + }); + + it('deletes a metric image', async () => { + alertManagementAlertsApi.deleteAlertMetricImage.mockResolvedValue({ data: '' }); + const result = await deleteMetricImage(); + + expect(alertManagementAlertsApi.deleteAlertMetricImage).toHaveBeenCalled(); + expect(result).toEqual({}); + }); +}); diff --git a/spec/frontend/vue_shared/components/__snapshots__/awards_list_spec.js.snap b/spec/frontend/vue_shared/components/__snapshots__/awards_list_spec.js.snap index c14cf0db370..bdf5ea23812 100644 --- a/spec/frontend/vue_shared/components/__snapshots__/awards_list_spec.js.snap +++ b/spec/frontend/vue_shared/components/__snapshots__/awards_list_spec.js.snap @@ -218,65 +218,88 @@ exports[`vue_shared/components/awards_list default matches snapshot 1`] = ` <div class="award-menu-holder gl-my-2" > - <button - aria-label="Add reaction" - class="btn add-reaction-button js-add-award btn-default btn-md gl-button js-test-add-button-class" + <div + class="emoji-picker" + data-testid="emoji-picker" title="Add reaction" - type="button" > - <!----> - - <!----> - - <span - class="gl-button-text" + <div + boundary="scrollParent" + class="dropdown b-dropdown gl-new-dropdown btn-group" + id="__BVID__13" + lazy="" + menu-class="dropdown-extended-height" + no-flip="" > - <span - class="reaction-control-icon reaction-control-icon-neutral" + <!----> + <button + aria-expanded="false" + aria-haspopup="true" + class="btn dropdown-toggle btn-default btn-md add-reaction-button btn-icon gl-relative! gl-button gl-dropdown-toggle btn-default-secondary" + id="__BVID__13__BV_toggle_" + type="button" > - <svg - aria-hidden="true" - class="gl-icon s16" - data-testid="slight-smile-icon" - role="img" + <span + class="gl-sr-only" > - <use - href="#slight-smile" - /> - </svg> - </span> - - <span - class="reaction-control-icon reaction-control-icon-positive" - > - <svg - aria-hidden="true" - class="gl-icon s16" - data-testid="smiley-icon" - role="img" + Add reaction + </span> + + <span + class="reaction-control-icon reaction-control-icon-neutral" > - <use - href="#smiley" - /> - </svg> - </span> - - <span - class="reaction-control-icon reaction-control-icon-super-positive" - > - <svg - aria-hidden="true" - class="gl-icon s16" - data-testid="smile-icon" - role="img" + <svg + aria-hidden="true" + class="gl-icon s16" + data-testid="slight-smile-icon" + role="img" + > + <use + href="#slight-smile" + /> + </svg> + </span> + + <span + class="reaction-control-icon reaction-control-icon-positive" > - <use - href="#smile" - /> - </svg> - </span> - </span> - </button> + <svg + aria-hidden="true" + class="gl-icon s16" + data-testid="smiley-icon" + role="img" + > + <use + href="#smiley" + /> + </svg> + </span> + + <span + class="reaction-control-icon reaction-control-icon-super-positive" + > + <svg + aria-hidden="true" + class="gl-icon s16" + data-testid="smile-icon" + role="img" + > + <use + href="#smile" + /> + </svg> + </span> + </button> + <ul + aria-labelledby="__BVID__13__BV_toggle_" + class="dropdown-menu dropdown-extended-height dropdown-menu-right" + role="menu" + tabindex="-1" + > + <!----> + </ul> + </div> + </div> </div> </div> `; diff --git a/spec/frontend/vue_shared/components/__snapshots__/identicon_spec.js.snap b/spec/frontend/vue_shared/components/__snapshots__/identicon_spec.js.snap deleted file mode 100644 index 1d8e04b83a3..00000000000 --- a/spec/frontend/vue_shared/components/__snapshots__/identicon_spec.js.snap +++ /dev/null @@ -1,21 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Identicon entity id is a GraphQL id matches snapshot 1`] = ` -<div - class="avatar identicon s40 bg2" -> - - E - -</div> -`; - -exports[`Identicon entity id is a number matches snapshot 1`] = ` -<div - class="avatar identicon s40 bg2" -> - - E - -</div> -`; diff --git a/spec/frontend/vue_shared/components/awards_list_spec.js b/spec/frontend/vue_shared/components/awards_list_spec.js index 95e9760c181..1c8cf726aca 100644 --- a/spec/frontend/vue_shared/components/awards_list_spec.js +++ b/spec/frontend/vue_shared/components/awards_list_spec.js @@ -76,7 +76,7 @@ describe('vue_shared/components/awards_list', () => { count: Number(x.find('.js-counter').text()), }; }); - const findAddAwardButton = () => wrapper.find('.js-add-award'); + const findAddAwardButton = () => wrapper.find('[data-testid="emoji-picker"]'); describe('default', () => { beforeEach(() => { @@ -151,7 +151,6 @@ describe('vue_shared/components/awards_list', () => { const btn = findAddAwardButton(); expect(btn.exists()).toBe(true); - expect(btn.classes(TEST_ADD_BUTTON_CLASS)).toBe(true); }); }); diff --git a/spec/frontend/vue_shared/components/blob_viewers/simple_viewer_spec.js b/spec/frontend/vue_shared/components/blob_viewers/simple_viewer_spec.js index 663ebd3e12f..4b44311b253 100644 --- a/spec/frontend/vue_shared/components/blob_viewers/simple_viewer_spec.js +++ b/spec/frontend/vue_shared/components/blob_viewers/simple_viewer_spec.js @@ -2,9 +2,6 @@ import { shallowMount } from '@vue/test-utils'; import { nextTick } from 'vue'; import { HIGHLIGHT_CLASS_NAME } from '~/vue_shared/components/blob_viewers/constants'; import SimpleViewer from '~/vue_shared/components/blob_viewers/simple_viewer.vue'; -import LineHighlighter from '~/blob/line_highlighter'; - -jest.mock('~/blob/line_highlighter'); describe('Blob Simple Viewer component', () => { let wrapper; @@ -30,20 +27,6 @@ describe('Blob Simple Viewer component', () => { wrapper.destroy(); }); - describe('refactorBlobViewer feature flag', () => { - it('loads the LineHighlighter if refactorBlobViewer is enabled', () => { - createComponent('', false, { refactorBlobViewer: true }); - - expect(LineHighlighter).toHaveBeenCalled(); - }); - - it('does not load the LineHighlighter if refactorBlobViewer is disabled', () => { - createComponent('', false, { refactorBlobViewer: false }); - - expect(LineHighlighter).not.toHaveBeenCalled(); - }); - }); - it('does not fail if content is empty', () => { const spy = jest.spyOn(window.console, 'error'); createComponent(''); diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js index 575e8a73050..b6a181e6a0b 100644 --- a/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js +++ b/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js @@ -26,7 +26,6 @@ import { tokenValueMilestone, tokenValueMembership, tokenValueConfidential, - tokenValueEmpty, } from './mock_data'; jest.mock('~/vue_shared/components/filtered_search_bar/filtered_search_utils', () => ({ @@ -207,33 +206,14 @@ describe('FilteredSearchBarRoot', () => { }); }); - describe('watchers', () => { - describe('filterValue', () => { - it('emits component event `onFilter` with empty array and false when filter was never selected', async () => { - wrapper = createComponent({ initialFilterValue: [tokenValueEmpty] }); - // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details - // eslint-disable-next-line no-restricted-syntax - wrapper.setData({ - initialRender: false, - filterValue: [tokenValueEmpty], - }); - - await nextTick(); - expect(wrapper.emitted('onFilter')[0]).toEqual([[], false]); - }); + describe('events', () => { + it('emits component event `onFilter` with empty array and true when initially selected filter value was cleared', async () => { + wrapper = createComponent({ initialFilterValue: [tokenValueLabel] }); - it('emits component event `onFilter` with empty array and true when initially selected filter value was cleared', async () => { - wrapper = createComponent({ initialFilterValue: [tokenValueLabel] }); - // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details - // eslint-disable-next-line no-restricted-syntax - wrapper.setData({ - initialRender: false, - filterValue: [tokenValueEmpty], - }); + wrapper.find(GlFilteredSearch).vm.$emit('clear'); - await nextTick(); - expect(wrapper.emitted('onFilter')[0]).toEqual([[], true]); - }); + await nextTick(); + expect(wrapper.emitted('onFilter')[0]).toEqual([[], true]); }); }); diff --git a/spec/frontend/vue_shared/components/form/input_copy_toggle_visibility_spec.js b/spec/frontend/vue_shared/components/form/input_copy_toggle_visibility_spec.js index b67385cc43e..e636f58d868 100644 --- a/spec/frontend/vue_shared/components/form/input_copy_toggle_visibility_spec.js +++ b/spec/frontend/vue_shared/components/form/input_copy_toggle_visibility_spec.js @@ -89,8 +89,11 @@ describe('InputCopyToggleVisibility', () => { }); describe('when clicked', () => { + let event; + beforeEach(async () => { - await findRevealButton().trigger('click'); + event = { stopPropagation: jest.fn() }; + await findRevealButton().trigger('click', event); }); it('displays value', () => { @@ -110,6 +113,11 @@ describe('InputCopyToggleVisibility', () => { it('emits `visibility-change` event', () => { expect(wrapper.emitted('visibility-change')[0]).toEqual([true]); }); + + it('stops propagation on click event', () => { + // in case the input is located in a dropdown or modal + expect(event.stopPropagation).toHaveBeenCalledTimes(1); + }); }); }); diff --git a/spec/frontend/vue_shared/components/help_popover_spec.js b/spec/frontend/vue_shared/components/help_popover_spec.js index 597fb63d95c..64dce194327 100644 --- a/spec/frontend/vue_shared/components/help_popover_spec.js +++ b/spec/frontend/vue_shared/components/help_popover_spec.js @@ -34,7 +34,7 @@ describe('HelpPopover', () => { it('renders a link button with an icon question', () => { expect(findQuestionButton().props()).toMatchObject({ - icon: 'question', + icon: 'question-o', variant: 'link', }); }); diff --git a/spec/frontend/vue_shared/components/identicon_spec.js b/spec/frontend/vue_shared/components/identicon_spec.js deleted file mode 100644 index 24fc3713e2b..00000000000 --- a/spec/frontend/vue_shared/components/identicon_spec.js +++ /dev/null @@ -1,50 +0,0 @@ -import { shallowMount } from '@vue/test-utils'; -import IdenticonComponent from '~/vue_shared/components/identicon.vue'; - -describe('Identicon', () => { - let wrapper; - - const defaultProps = { - entityId: 1, - entityName: 'entity-name', - sizeClass: 's40', - }; - - const createComponent = (props = {}) => { - wrapper = shallowMount(IdenticonComponent, { - propsData: { - ...defaultProps, - ...props, - }, - }); - }; - - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - - describe('entity id is a number', () => { - beforeEach(() => createComponent()); - - it('matches snapshot', () => { - expect(wrapper.element).toMatchSnapshot(); - }); - - it('adds a correct class to identicon', () => { - expect(wrapper.find({ ref: 'identicon' }).classes()).toContain('bg2'); - }); - }); - - describe('entity id is a GraphQL id', () => { - beforeEach(() => createComponent({ entityId: 'gid://gitlab/Project/8' })); - - it('matches snapshot', () => { - expect(wrapper.element).toMatchSnapshot(); - }); - - it('adds a correct class to identicon', () => { - expect(wrapper.find({ ref: 'identicon' }).classes()).toContain('bg2'); - }); - }); -}); diff --git a/spec/frontend/vue_shared/components/line_numbers_spec.js b/spec/frontend/vue_shared/components/line_numbers_spec.js deleted file mode 100644 index 38c26226863..00000000000 --- a/spec/frontend/vue_shared/components/line_numbers_spec.js +++ /dev/null @@ -1,37 +0,0 @@ -import { shallowMount } from '@vue/test-utils'; -import { GlIcon, GlLink } from '@gitlab/ui'; -import LineNumbers from '~/vue_shared/components/line_numbers.vue'; - -describe('Line Numbers component', () => { - let wrapper; - const lines = 10; - - const createComponent = () => { - wrapper = shallowMount(LineNumbers, { propsData: { lines } }); - }; - - const findGlIcon = () => wrapper.findComponent(GlIcon); - const findLineNumbers = () => wrapper.findAllComponents(GlLink); - const findFirstLineNumber = () => findLineNumbers().at(0); - - beforeEach(() => createComponent()); - - afterEach(() => wrapper.destroy()); - - describe('rendering', () => { - it('renders Line Numbers', () => { - expect(findLineNumbers().length).toBe(lines); - expect(findFirstLineNumber().attributes()).toMatchObject({ - id: 'L1', - to: '#LC1', - }); - }); - - it('renders a link icon', () => { - expect(findGlIcon().props()).toMatchObject({ - size: 12, - name: 'link', - }); - }); - }); -}); diff --git a/spec/frontend/vue_shared/components/local_storage_sync_spec.js b/spec/frontend/vue_shared/components/local_storage_sync_spec.js index dac633fe6c8..a80717a1aea 100644 --- a/spec/frontend/vue_shared/components/local_storage_sync_spec.js +++ b/spec/frontend/vue_shared/components/local_storage_sync_spec.js @@ -1,31 +1,29 @@ import { shallowMount } from '@vue/test-utils'; -import { nextTick } from 'vue'; import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; +const STORAGE_KEY = 'key'; + describe('Local Storage Sync', () => { let wrapper; - const createComponent = ({ props = {}, slots = {} } = {}) => { + const createComponent = ({ value, asString = false, slots = {} } = {}) => { wrapper = shallowMount(LocalStorageSync, { - propsData: props, + propsData: { storageKey: STORAGE_KEY, value, asString }, slots, }); }; + const setStorageValue = (value) => localStorage.setItem(STORAGE_KEY, value); + const getStorageValue = (value) => localStorage.getItem(STORAGE_KEY, value); + afterEach(() => { - if (wrapper) { - wrapper.destroy(); - } - wrapper = null; + wrapper.destroy(); localStorage.clear(); }); it('is a renderless component', () => { const html = '<div class="test-slot"></div>'; createComponent({ - props: { - storageKey: 'key', - }, slots: { default: html, }, @@ -35,233 +33,136 @@ describe('Local Storage Sync', () => { }); describe('localStorage empty', () => { - const storageKey = 'issue_list_order'; - it('does not emit input event', () => { - createComponent({ - props: { - storageKey, - value: 'ascending', - }, - }); - - expect(wrapper.emitted('input')).toBeFalsy(); - }); - - it.each('foo', 3, true, ['foo', 'bar'], { foo: 'bar' })( - 'saves updated value to localStorage', - async (newValue) => { - createComponent({ - props: { - storageKey, - value: 'initial', - }, - }); - - wrapper.setProps({ value: newValue }); + createComponent({ value: 'ascending' }); - await nextTick(); - expect(localStorage.getItem(storageKey)).toBe(String(newValue)); - }, - ); - - it('does not save default value', () => { - const value = 'ascending'; + expect(wrapper.emitted('input')).toBeUndefined(); + }); - createComponent({ - props: { - storageKey, - value, - }, - }); + it('does not save initial value if it did not change', () => { + createComponent({ value: 'ascending' }); - expect(localStorage.getItem(storageKey)).toBe(null); + expect(getStorageValue()).toBeNull(); }); }); describe('localStorage has saved value', () => { - const storageKey = 'issue_list_order_by'; const savedValue = 'last_updated'; beforeEach(() => { - localStorage.setItem(storageKey, savedValue); + setStorageValue(savedValue); + createComponent({ asString: true }); }); it('emits input event with saved value', () => { - createComponent({ - props: { - storageKey, - value: 'ascending', - }, - }); - expect(wrapper.emitted('input')[0][0]).toBe(savedValue); }); - it('does not overwrite localStorage with prop value', () => { - createComponent({ - props: { - storageKey, - value: 'created', - }, - }); - - expect(localStorage.getItem(storageKey)).toBe(savedValue); + it('does not overwrite localStorage with initial prop value', () => { + expect(getStorageValue()).toBe(savedValue); }); it('updating the value updates localStorage', async () => { - createComponent({ - props: { - storageKey, - value: 'created', - }, - }); - const newValue = 'last_updated'; - wrapper.setProps({ - value: newValue, - }); + await wrapper.setProps({ value: newValue }); - await nextTick(); - expect(localStorage.getItem(storageKey)).toBe(newValue); + expect(getStorageValue()).toBe(newValue); }); + }); + describe('persist prop', () => { it('persists the value by default', async () => { const persistedValue = 'persisted'; + createComponent({ asString: true }); + // Sanity check to make sure we start with nothing saved. + expect(getStorageValue()).toBeNull(); - createComponent({ - props: { - storageKey, - }, - }); + await wrapper.setProps({ value: persistedValue }); - wrapper.setProps({ value: persistedValue }); - await nextTick(); - expect(localStorage.getItem(storageKey)).toBe(persistedValue); + expect(getStorageValue()).toBe(persistedValue); }); it('does not save a value if persist is set to false', async () => { + const value = 'saved'; const notPersistedValue = 'notPersisted'; + createComponent({ asString: true }); + // Save some value so we can test that it's not overwritten. + await wrapper.setProps({ value }); - createComponent({ - props: { - storageKey, - }, - }); + expect(getStorageValue()).toBe(value); - wrapper.setProps({ persist: false, value: notPersistedValue }); - await nextTick(); - expect(localStorage.getItem(storageKey)).not.toBe(notPersistedValue); + await wrapper.setProps({ persist: false, value: notPersistedValue }); + + expect(getStorageValue()).toBe(value); }); }); - describe('with "asJson" prop set to "true"', () => { - const storageKey = 'testStorageKey'; - - describe.each` - value | serializedValue - ${null} | ${'null'} - ${''} | ${'""'} - ${true} | ${'true'} - ${false} | ${'false'} - ${42} | ${'42'} - ${'42'} | ${'"42"'} - ${'{ foo: '} | ${'"{ foo: "'} - ${['test']} | ${'["test"]'} - ${{ foo: 'bar' }} | ${'{"foo":"bar"}'} - `('given $value', ({ value, serializedValue }) => { - describe('is a new value', () => { - beforeEach(async () => { - createComponent({ - props: { - storageKey, - value: 'initial', - asJson: true, - }, - }); - - wrapper.setProps({ value }); - - await nextTick(); - }); - - it('serializes the value correctly to localStorage', () => { - expect(localStorage.getItem(storageKey)).toBe(serializedValue); - }); - }); - - describe('is already stored', () => { - beforeEach(() => { - localStorage.setItem(storageKey, serializedValue); - - createComponent({ - props: { - storageKey, - value: 'initial', - asJson: true, - }, - }); - }); - - it('emits an input event with the deserialized value', () => { - expect(wrapper.emitted('input')).toEqual([[value]]); - }); - }); + describe('saving and restoring', () => { + it.each` + value | asString + ${'foo'} | ${true} + ${'foo'} | ${false} + ${'{ a: 1 }'} | ${true} + ${'{ a: 1 }'} | ${false} + ${3} | ${false} + ${['foo', 'bar']} | ${false} + ${{ foo: 'bar' }} | ${false} + ${null} | ${false} + ${' '} | ${false} + ${true} | ${false} + ${false} | ${false} + ${42} | ${false} + ${'42'} | ${false} + ${'{ foo: '} | ${false} + `('saves and restores the same value', async ({ value, asString }) => { + // Create an initial component to save the value. + createComponent({ asString }); + await wrapper.setProps({ value }); + wrapper.destroy(); + // Create a second component to restore the value. Restore is only done once, when the + // component is first mounted. + createComponent({ asString }); + + expect(wrapper.emitted('input')[0][0]).toEqual(value); }); - describe('with bad JSON in storage', () => { - const badJSON = '{ badJSON'; - - beforeEach(() => { - jest.spyOn(console, 'warn').mockImplementation(); - localStorage.setItem(storageKey, badJSON); - - createComponent({ - props: { - storageKey, - value: 'initial', - asJson: true, - }, - }); - }); - - it('should console warn', () => { - // eslint-disable-next-line no-console - expect(console.warn).toHaveBeenCalledWith( - `[gitlab] Failed to deserialize value from localStorage (key=${storageKey})`, - badJSON, - ); - }); - - it('should not emit an input event', () => { - expect(wrapper.emitted('input')).toBeUndefined(); - }); + it('shows a warning when trying to save a non-string value when asString prop is true', async () => { + const spy = jest.spyOn(console, 'warn').mockImplementation(); + createComponent({ asString: true }); + await wrapper.setProps({ value: [] }); + + expect(spy).toHaveBeenCalled(); }); }); - it('clears localStorage when clear property is true', async () => { - const storageKey = 'key'; - const value = 'initial'; + describe('with bad JSON in storage', () => { + const badJSON = '{ badJSON'; + let spy; - createComponent({ - props: { - storageKey, - }, + beforeEach(() => { + spy = jest.spyOn(console, 'warn').mockImplementation(); + setStorageValue(badJSON); + createComponent(); }); - wrapper.setProps({ - value, + + it('should console warn', () => { + expect(spy).toHaveBeenCalled(); }); - await nextTick(); + it('should not emit an input event', () => { + expect(wrapper.emitted('input')).toBeUndefined(); + }); + }); - expect(localStorage.getItem(storageKey)).toBe(value); + it('clears localStorage when clear property is true', async () => { + const value = 'initial'; + createComponent({ asString: true }); + await wrapper.setProps({ value }); - wrapper.setProps({ - clear: true, - }); + expect(getStorageValue()).toBe(value); - await nextTick(); + await wrapper.setProps({ clear: true }); - expect(localStorage.getItem(storageKey)).toBe(null); + expect(getStorageValue()).toBeNull(); }); }); diff --git a/spec/frontend/vue_shared/components/markdown/apply_suggestion_spec.js b/spec/frontend/vue_shared/components/markdown/apply_suggestion_spec.js index c56628fcbcd..ecb2b37c3a5 100644 --- a/spec/frontend/vue_shared/components/markdown/apply_suggestion_spec.js +++ b/spec/frontend/vue_shared/components/markdown/apply_suggestion_spec.js @@ -1,4 +1,4 @@ -import { GlDropdown, GlFormTextarea, GlButton } from '@gitlab/ui'; +import { GlDropdown, GlFormTextarea, GlButton, GlAlert } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import ApplySuggestionComponent from '~/vue_shared/components/markdown/apply_suggestion.vue'; @@ -10,9 +10,10 @@ describe('Apply Suggestion component', () => { wrapper = shallowMount(ApplySuggestionComponent, { propsData: { ...propsData, ...props } }); }; - const findDropdown = () => wrapper.find(GlDropdown); - const findTextArea = () => wrapper.find(GlFormTextarea); - const findApplyButton = () => wrapper.find(GlButton); + const findDropdown = () => wrapper.findComponent(GlDropdown); + const findTextArea = () => wrapper.findComponent(GlFormTextarea); + const findApplyButton = () => wrapper.findComponent(GlButton); + const findAlert = () => wrapper.findComponent(GlAlert); beforeEach(() => createWrapper()); @@ -53,6 +54,20 @@ describe('Apply Suggestion component', () => { }); }); + describe('error', () => { + it('displays an error message', () => { + const errorMessage = 'Error message'; + createWrapper({ errorMessage }); + + const alert = findAlert(); + + expect(alert.exists()).toBe(true); + expect(alert.props('variant')).toBe('danger'); + expect(alert.props('dismissible')).toBe(false); + expect(alert.text()).toBe(errorMessage); + }); + }); + describe('apply suggestion', () => { it('emits an apply event with no message if no message was added', () => { findTextArea().vm.$emit('input', null); diff --git a/spec/frontend/vue_shared/components/markdown/field_spec.js b/spec/frontend/vue_shared/components/markdown/field_spec.js index b5daa389fc6..d1c4d777d44 100644 --- a/spec/frontend/vue_shared/components/markdown/field_spec.js +++ b/spec/frontend/vue_shared/components/markdown/field_spec.js @@ -85,7 +85,7 @@ describe('Markdown field component', () => { describe('mounted', () => { const previewHTML = ` <p>markdown preview</p> - <video src="${FIXTURES_PATH}/static/mock-video.mp4" muted="muted"></video> + <video src="${FIXTURES_PATH}/static/mock-video.mp4"></video> `; let previewLink; let writeLink; @@ -101,6 +101,21 @@ describe('Markdown field component', () => { expect(subject.find('.zen-backdrop textarea').element).not.toBeNull(); }); + it('renders referenced commands on markdown preview', async () => { + axiosMock + .onPost(markdownPreviewPath) + .reply(200, { references: { users: [], commands: 'test command' } }); + + previewLink = getPreviewLink(); + previewLink.vm.$emit('click', { target: {} }); + + await axios.waitFor(markdownPreviewPath); + const referencedCommands = subject.find('[data-testid="referenced-commands"]'); + + expect(referencedCommands.exists()).toBe(true); + expect(referencedCommands.text()).toContain('test command'); + }); + describe('markdown preview', () => { beforeEach(() => { axiosMock.onPost(markdownPreviewPath).reply(200, { body: previewHTML }); diff --git a/spec/frontend/vue_shared/components/markdown/header_spec.js b/spec/frontend/vue_shared/components/markdown/header_spec.js index 9ffb9c6a541..fa4ca63f910 100644 --- a/spec/frontend/vue_shared/components/markdown/header_spec.js +++ b/spec/frontend/vue_shared/components/markdown/header_spec.js @@ -95,7 +95,7 @@ describe('Markdown field header component', () => { it('hides toolbar in preview mode', () => { createWrapper({ previewMarkdown: true }); - expect(findToolbar().classes().includes('gl-display-none')).toBe(true); + expect(findToolbar().classes().includes('gl-display-none!')).toBe(true); }); it('emits toggle markdown event when clicking preview tab', async () => { diff --git a/spec/frontend/vue_shared/components/metric_images/__snapshots__/metric_images_table_spec.js.snap b/spec/frontend/vue_shared/components/metric_images/__snapshots__/metric_images_table_spec.js.snap new file mode 100644 index 00000000000..5dd12d9edf5 --- /dev/null +++ b/spec/frontend/vue_shared/components/metric_images/__snapshots__/metric_images_table_spec.js.snap @@ -0,0 +1,73 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Metrics upload item render the metrics image component 1`] = ` +<gl-card-stub + bodyclass="gl-border-1,gl-border-t-solid,gl-border-gray-100,[object Object]" + class="collapsible-card border gl-p-0 gl-mb-5" + footerclass="" + headerclass="gl-display-flex gl-align-items-center gl-border-b-0 gl-py-3" +> + <gl-modal-stub + actioncancel="[object Object]" + actionprimary="[object Object]" + body-class="gl-pb-0! gl-min-h-6!" + dismisslabel="Close" + modalclass="" + modalid="delete-metric-modal" + size="sm" + titletag="h4" + > + + <p> + Are you sure you wish to delete this image? + </p> + </gl-modal-stub> + + <gl-modal-stub + actioncancel="[object Object]" + actionprimary="[object Object]" + data-testid="metric-image-edit-modal" + dismisslabel="Close" + modalclass="" + modalid="edit-metric-modal" + size="sm" + titletag="h4" + > + + <gl-form-group-stub + label="Text (optional)" + label-for="upload-text-input" + labeldescription="" + optionaltext="(optional)" + > + <gl-form-input-stub + data-testid="metric-image-text-field" + id="upload-text-input" + /> + </gl-form-group-stub> + + <gl-form-group-stub + description="Must start with http or https" + label="Link (optional)" + label-for="upload-url-input" + labeldescription="" + optionaltext="(optional)" + > + <gl-form-input-stub + data-testid="metric-image-url-field" + id="upload-url-input" + /> + </gl-form-group-stub> + </gl-modal-stub> + + <div + class="gl-display-flex gl-flex-direction-column" + data-testid="metric-image-body" + > + <img + class="gl-max-w-full gl-align-self-center" + src="test_file_path" + /> + </div> +</gl-card-stub> +`; diff --git a/spec/frontend/vue_shared/components/metric_images/metric_images_tab_spec.js b/spec/frontend/vue_shared/components/metric_images/metric_images_tab_spec.js new file mode 100644 index 00000000000..2cefa77b72d --- /dev/null +++ b/spec/frontend/vue_shared/components/metric_images/metric_images_tab_spec.js @@ -0,0 +1,174 @@ +import { GlFormInput, GlModal } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import Vue from 'vue'; +import merge from 'lodash/merge'; +import Vuex from 'vuex'; +import MetricImagesTable from '~/vue_shared/components/metric_images/metric_images_table.vue'; +import MetricImagesTab from '~/vue_shared/components/metric_images/metric_images_tab.vue'; +import createStore from '~/vue_shared/components/metric_images/store'; +import waitForPromises from 'helpers/wait_for_promises'; +import UploadDropzone from '~/vue_shared/components/upload_dropzone/upload_dropzone.vue'; +import { fileList, initialData } from './mock_data'; + +const service = { + getMetricImages: jest.fn(), +}; + +const mockEvent = { preventDefault: jest.fn() }; + +Vue.use(Vuex); + +describe('Metric images tab', () => { + let wrapper; + let store; + + const mountComponent = (options = {}) => { + store = createStore({}, service); + + wrapper = shallowMount( + MetricImagesTab, + merge( + { + store, + provide: { + canUpdate: true, + iid: initialData.issueIid, + projectId: initialData.projectId, + }, + }, + options, + ), + ); + }; + + beforeEach(() => { + mountComponent(); + }); + + afterEach(() => { + if (wrapper) { + wrapper.destroy(); + wrapper = null; + } + }); + + const findUploadDropzone = () => wrapper.findComponent(UploadDropzone); + const findImages = () => wrapper.findAllComponents(MetricImagesTable); + const findModal = () => wrapper.findComponent(GlModal); + const submitModal = () => findModal().vm.$emit('primary', mockEvent); + const cancelModal = () => findModal().vm.$emit('hidden'); + + describe('empty state', () => { + beforeEach(() => { + mountComponent(); + }); + + it('renders the upload component', () => { + expect(findUploadDropzone().exists()).toBe(true); + }); + }); + + describe('permissions', () => { + beforeEach(() => { + mountComponent({ provide: { canUpdate: false } }); + }); + + it('hides the upload component when disallowed', () => { + expect(findUploadDropzone().exists()).toBe(false); + }); + }); + + describe('onLoad action', () => { + it('should load images', async () => { + service.getMetricImages.mockImplementation(() => Promise.resolve(fileList)); + + mountComponent(); + + await waitForPromises(); + + expect(findImages().length).toBe(1); + }); + }); + + describe('add metric dialog', () => { + const testUrl = 'test url'; + + it('should open the add metric dialog when clicked', async () => { + mountComponent(); + + findUploadDropzone().vm.$emit('change'); + + await waitForPromises(); + + expect(findModal().attributes('visible')).toBe('true'); + }); + + it('should close when cancelled', async () => { + mountComponent({ + data() { + return { modalVisible: true }; + }, + }); + + cancelModal(); + + await waitForPromises(); + + expect(findModal().attributes('visible')).toBeFalsy(); + }); + + it('should add files and url when selected', async () => { + mountComponent({ + data() { + return { modalVisible: true, modalUrl: testUrl, currentFiles: fileList }; + }, + }); + + const dispatchSpy = jest.spyOn(store, 'dispatch'); + + submitModal(); + + await waitForPromises(); + + expect(dispatchSpy).toHaveBeenCalledWith('uploadImage', { + files: fileList, + url: testUrl, + urlText: '', + }); + }); + + describe('url field', () => { + beforeEach(() => { + mountComponent({ + data() { + return { modalVisible: true, modalUrl: testUrl }; + }, + }); + }); + + it('should display the url field', () => { + expect(wrapper.find('#upload-url-input').attributes('value')).toBe(testUrl); + }); + + it('should display the url text field', () => { + expect(wrapper.find('#upload-text-input').attributes('value')).toBe(''); + }); + + it('should clear url when cancelled', async () => { + cancelModal(); + + await waitForPromises(); + + expect(wrapper.findComponent(GlFormInput).attributes('value')).toBe(''); + }); + + it('should clear url when submitted', async () => { + submitModal(); + + await waitForPromises(); + + expect(wrapper.findComponent(GlFormInput).attributes('value')).toBe(''); + }); + }); + }); +}); diff --git a/spec/frontend/vue_shared/components/metric_images/metric_images_table_spec.js b/spec/frontend/vue_shared/components/metric_images/metric_images_table_spec.js new file mode 100644 index 00000000000..d792bd46ccd --- /dev/null +++ b/spec/frontend/vue_shared/components/metric_images/metric_images_table_spec.js @@ -0,0 +1,230 @@ +import { GlLink, GlModal } from '@gitlab/ui'; +import { shallowMount, mount } from '@vue/test-utils'; +import Vue from 'vue'; +import merge from 'lodash/merge'; +import Vuex from 'vuex'; +import createStore from '~/vue_shared/components/metric_images/store'; +import MetricsImageTable from '~/vue_shared/components/metric_images/metric_images_table.vue'; +import waitForPromises from 'helpers/wait_for_promises'; + +const defaultProps = { + id: 1, + filePath: 'test_file_path', + filename: 'test_file_name', +}; + +const mockEvent = { preventDefault: jest.fn() }; + +Vue.use(Vuex); + +describe('Metrics upload item', () => { + let wrapper; + let store; + + const mountComponent = (options = {}, mountMethod = mount) => { + store = createStore(); + + wrapper = mountMethod( + MetricsImageTable, + merge( + { + store, + propsData: { + ...defaultProps, + }, + provide: { canUpdate: true }, + }, + options, + ), + ); + }; + + afterEach(() => { + if (wrapper) { + wrapper.destroy(); + wrapper = null; + } + }); + + const findImageLink = () => wrapper.findComponent(GlLink); + const findLabelTextSpan = () => wrapper.find('[data-testid="metric-image-label-span"]'); + const findCollapseButton = () => wrapper.find('[data-testid="collapse-button"]'); + const findMetricImageBody = () => wrapper.find('[data-testid="metric-image-body"]'); + const findModal = () => wrapper.findComponent(GlModal); + const findEditModal = () => wrapper.find('[data-testid="metric-image-edit-modal"]'); + const findDeleteButton = () => wrapper.find('[data-testid="delete-button"]'); + const findEditButton = () => wrapper.find('[data-testid="edit-button"]'); + const findImageTextInput = () => wrapper.find('[data-testid="metric-image-text-field"]'); + const findImageUrlInput = () => wrapper.find('[data-testid="metric-image-url-field"]'); + + const closeModal = () => findModal().vm.$emit('hidden'); + const submitModal = () => findModal().vm.$emit('primary', mockEvent); + const deleteImage = () => findDeleteButton().vm.$emit('click'); + const closeEditModal = () => findEditModal().vm.$emit('hidden'); + const submitEditModal = () => findEditModal().vm.$emit('primary', mockEvent); + const editImage = () => findEditButton().vm.$emit('click'); + + it('render the metrics image component', () => { + mountComponent({}, shallowMount); + + expect(wrapper.element).toMatchSnapshot(); + }); + + it('shows a link with the correct url', () => { + const testUrl = 'test_url'; + mountComponent({ propsData: { url: testUrl } }); + + expect(findImageLink().attributes('href')).toBe(testUrl); + expect(findImageLink().text()).toBe(defaultProps.filename); + }); + + it('shows a link with the url text, if url text is present', () => { + const testUrl = 'test_url'; + const testUrlText = 'test_url_text'; + mountComponent({ propsData: { url: testUrl, urlText: testUrlText } }); + + expect(findImageLink().attributes('href')).toBe(testUrl); + expect(findImageLink().text()).toBe(testUrlText); + }); + + it('shows the url text with no url, if no url is present', () => { + const testUrlText = 'test_url_text'; + mountComponent({ propsData: { urlText: testUrlText } }); + + expect(findLabelTextSpan().text()).toBe(testUrlText); + }); + + describe('expand and collapse', () => { + beforeEach(() => { + mountComponent(); + }); + + it('the card is expanded by default', () => { + expect(findMetricImageBody().isVisible()).toBe(true); + }); + + it('the card is collapsed when clicked', async () => { + findCollapseButton().trigger('click'); + + await waitForPromises(); + + expect(findMetricImageBody().isVisible()).toBe(false); + }); + }); + + describe('delete functionality', () => { + it('should open the delete modal when clicked', async () => { + mountComponent({ stubs: { GlModal: true } }); + + deleteImage(); + + await waitForPromises(); + + expect(findModal().attributes('visible')).toBe('true'); + }); + + describe('when the modal is open', () => { + beforeEach(() => { + mountComponent( + { + data() { + return { modalVisible: true }; + }, + }, + shallowMount, + ); + }); + + it('should close the modal when cancelled', async () => { + closeModal(); + + await waitForPromises(); + + expect(findModal().attributes('visible')).toBeFalsy(); + }); + + it('should delete the image when selected', async () => { + const dispatchSpy = jest.spyOn(store, 'dispatch').mockImplementation(jest.fn()); + + submitModal(); + + await waitForPromises(); + + expect(dispatchSpy).toHaveBeenCalledWith('deleteImage', defaultProps.id); + }); + }); + + describe('canUpdate permission', () => { + it('delete button is hidden when user lacks update permissions', () => { + mountComponent({ provide: { canUpdate: false } }); + + expect(findDeleteButton().exists()).toBe(false); + }); + }); + }); + + describe('edit functionality', () => { + it('should open the delete modal when clicked', async () => { + mountComponent({ stubs: { GlModal: true } }); + + editImage(); + + await waitForPromises(); + + expect(findEditModal().attributes('visible')).toBe('true'); + }); + + describe('when the modal is open', () => { + beforeEach(() => { + mountComponent({ + data() { + return { editModalVisible: true }; + }, + propsData: { urlText: 'test' }, + stubs: { GlModal: true }, + }); + }); + + it('should close the modal when cancelled', async () => { + closeEditModal(); + + await waitForPromises(); + + expect(findEditModal().attributes('visible')).toBeFalsy(); + }); + + it('should delete the image when selected', async () => { + const dispatchSpy = jest.spyOn(store, 'dispatch').mockImplementation(jest.fn()); + + submitEditModal(); + + await waitForPromises(); + + expect(dispatchSpy).toHaveBeenCalledWith('updateImage', { + imageId: defaultProps.id, + url: null, + urlText: 'test', + }); + }); + + it('should clear edits when the modal is closed', async () => { + await findImageTextInput().setValue('test value'); + await findImageUrlInput().setValue('http://www.gitlab.com'); + + expect(findImageTextInput().element.value).toBe('test value'); + expect(findImageUrlInput().element.value).toBe('http://www.gitlab.com'); + + closeEditModal(); + + await waitForPromises(); + + editImage(); + + await waitForPromises(); + + expect(findImageTextInput().element.value).toBe('test'); + expect(findImageUrlInput().element.value).toBe(''); + }); + }); + }); +}); diff --git a/spec/frontend/vue_shared/components/metric_images/mock_data.js b/spec/frontend/vue_shared/components/metric_images/mock_data.js new file mode 100644 index 00000000000..480491077fb --- /dev/null +++ b/spec/frontend/vue_shared/components/metric_images/mock_data.js @@ -0,0 +1,5 @@ +export const fileList = [{ filePath: 'test', filename: 'hello', id: 5, url: null }]; + +export const fileListRaw = [{ file_path: 'test', filename: 'hello', id: 5, url: null }]; + +export const initialData = { issueIid: '123', projectId: 456 }; diff --git a/spec/frontend/vue_shared/components/metric_images/store/actions_spec.js b/spec/frontend/vue_shared/components/metric_images/store/actions_spec.js new file mode 100644 index 00000000000..518cf354675 --- /dev/null +++ b/spec/frontend/vue_shared/components/metric_images/store/actions_spec.js @@ -0,0 +1,158 @@ +import Vue from 'vue'; +import Vuex from 'vuex'; +import actionsFactory from '~/vue_shared/components/metric_images/store/actions'; +import * as types from '~/vue_shared/components/metric_images/store/mutation_types'; +import createStore from '~/vue_shared/components/metric_images/store'; +import testAction from 'helpers/vuex_action_helper'; +import createFlash from '~/flash'; +import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; +import { fileList, initialData } from '../mock_data'; + +jest.mock('~/flash'); +const service = { + getMetricImages: jest.fn(), + uploadMetricImage: jest.fn(), + updateMetricImage: jest.fn(), + deleteMetricImage: jest.fn(), +}; + +const actions = actionsFactory(service); + +const defaultState = { + issueIid: 1, + projectId: '2', +}; + +Vue.use(Vuex); + +describe('Metrics tab store actions', () => { + let store; + let state; + + beforeEach(() => { + store = createStore(defaultState); + state = store.state; + }); + + afterEach(() => { + createFlash.mockClear(); + }); + + describe('fetching metric images', () => { + it('should call success action when fetching metric images', () => { + service.getMetricImages.mockImplementation(() => Promise.resolve(fileList)); + + testAction(actions.fetchImages, null, state, [ + { type: types.REQUEST_METRIC_IMAGES }, + { + type: types.RECEIVE_METRIC_IMAGES_SUCCESS, + payload: convertObjectPropsToCamelCase(fileList, { deep: true }), + }, + ]); + }); + + it('should call error action when fetching metric images with an error', async () => { + service.getMetricImages.mockImplementation(() => Promise.reject()); + + await testAction( + actions.fetchImages, + null, + state, + [{ type: types.REQUEST_METRIC_IMAGES }, { type: types.RECEIVE_METRIC_IMAGES_ERROR }], + [], + ); + expect(createFlash).toHaveBeenCalled(); + }); + }); + + describe('uploading metric images', () => { + const payload = { + // mock the FileList api + files: { + item() { + return fileList[0]; + }, + }, + url: 'test_url', + }; + + it('should call success action when uploading an image', () => { + service.uploadMetricImage.mockImplementation(() => Promise.resolve(fileList[0])); + + testAction(actions.uploadImage, payload, state, [ + { type: types.REQUEST_METRIC_UPLOAD }, + { + type: types.RECEIVE_METRIC_UPLOAD_SUCCESS, + payload: fileList[0], + }, + ]); + }); + + it('should call error action when failing to upload an image', async () => { + service.uploadMetricImage.mockImplementation(() => Promise.reject()); + + await testAction( + actions.uploadImage, + payload, + state, + [{ type: types.REQUEST_METRIC_UPLOAD }, { type: types.RECEIVE_METRIC_UPLOAD_ERROR }], + [], + ); + expect(createFlash).toHaveBeenCalled(); + }); + }); + + describe('updating metric images', () => { + const payload = { + url: 'test_url', + urlText: 'url text', + }; + + it('should call success action when updating an image', () => { + service.updateMetricImage.mockImplementation(() => Promise.resolve()); + + testAction(actions.updateImage, payload, state, [ + { type: types.REQUEST_METRIC_UPLOAD }, + { + type: types.RECEIVE_METRIC_UPDATE_SUCCESS, + }, + ]); + }); + + it('should call error action when failing to update an image', async () => { + service.updateMetricImage.mockImplementation(() => Promise.reject()); + + await testAction( + actions.updateImage, + payload, + state, + [{ type: types.REQUEST_METRIC_UPLOAD }, { type: types.RECEIVE_METRIC_UPLOAD_ERROR }], + [], + ); + expect(createFlash).toHaveBeenCalled(); + }); + }); + + describe('deleting a metric image', () => { + const payload = fileList[0].id; + + it('should call success action when deleting an image', () => { + service.deleteMetricImage.mockImplementation(() => Promise.resolve()); + + testAction(actions.deleteImage, payload, state, [ + { + type: types.RECEIVE_METRIC_DELETE_SUCCESS, + payload, + }, + ]); + }); + }); + + describe('initial data', () => { + it('should set the initial data correctly', () => { + testAction(actions.setInitialData, initialData, state, [ + { type: types.SET_INITIAL_DATA, payload: initialData }, + ]); + }); + }); +}); diff --git a/spec/frontend/vue_shared/components/metric_images/store/mutations_spec.js b/spec/frontend/vue_shared/components/metric_images/store/mutations_spec.js new file mode 100644 index 00000000000..754f729e657 --- /dev/null +++ b/spec/frontend/vue_shared/components/metric_images/store/mutations_spec.js @@ -0,0 +1,147 @@ +import { cloneDeep } from 'lodash'; +import * as types from '~/vue_shared/components/metric_images/store/mutation_types'; +import mutations from '~/vue_shared/components/metric_images/store/mutations'; +import { initialData } from '../mock_data'; + +const defaultState = { + metricImages: [], + isLoadingMetricImages: false, + isUploadingImage: false, +}; + +const testImages = [ + { filename: 'test.filename', id: 5, filePath: 'test/file/path', url: null }, + { filename: 'second.filename', id: 6, filePath: 'second/file/path', url: 'test/url' }, + { filename: 'third.filename', id: 7, filePath: 'third/file/path', url: 'test/url' }, +]; + +describe('Metric images mutations', () => { + let state; + + const createState = (customState = {}) => { + state = { + ...cloneDeep(defaultState), + ...customState, + }; + }; + + beforeEach(() => { + createState(); + }); + + describe('REQUEST_METRIC_IMAGES', () => { + beforeEach(() => { + mutations[types.REQUEST_METRIC_IMAGES](state); + }); + + it('should set the loading state', () => { + expect(state.isLoadingMetricImages).toBe(true); + }); + }); + + describe('RECEIVE_METRIC_IMAGES_SUCCESS', () => { + beforeEach(() => { + mutations[types.RECEIVE_METRIC_IMAGES_SUCCESS](state, testImages); + }); + + it('should unset the loading state', () => { + expect(state.isLoadingMetricImages).toBe(false); + }); + + it('should set the metric images', () => { + expect(state.metricImages).toEqual(testImages); + }); + }); + + describe('RECEIVE_METRIC_IMAGES_ERROR', () => { + beforeEach(() => { + mutations[types.RECEIVE_METRIC_IMAGES_ERROR](state); + }); + + it('should unset the loading state', () => { + expect(state.isLoadingMetricImages).toBe(false); + }); + }); + + describe('REQUEST_METRIC_UPLOAD', () => { + beforeEach(() => { + mutations[types.REQUEST_METRIC_UPLOAD](state); + }); + + it('should set the loading state', () => { + expect(state.isUploadingImage).toBe(true); + }); + }); + + describe('RECEIVE_METRIC_UPLOAD_SUCCESS', () => { + const initialImage = testImages[0]; + const newImage = testImages[1]; + + beforeEach(() => { + createState({ metricImages: [initialImage] }); + mutations[types.RECEIVE_METRIC_UPLOAD_SUCCESS](state, newImage); + }); + + it('should unset the loading state', () => { + expect(state.isUploadingImage).toBe(false); + }); + + it('should add the new metric image after the existing one', () => { + expect(state.metricImages).toMatchObject([initialImage, newImage]); + }); + }); + + describe('RECEIVE_METRIC_UPLOAD_ERROR', () => { + beforeEach(() => { + mutations[types.RECEIVE_METRIC_UPLOAD_ERROR](state); + }); + + it('should unset the loading state', () => { + expect(state.isUploadingImage).toBe(false); + }); + }); + + describe('RECEIVE_METRIC_UPDATE_SUCCESS', () => { + const initialImage = testImages[0]; + const newImage = testImages[0]; + newImage.url = 'https://www.gitlab.com'; + + beforeEach(() => { + createState({ metricImages: [initialImage] }); + mutations[types.RECEIVE_METRIC_UPDATE_SUCCESS](state, newImage); + }); + + it('should unset the loading state', () => { + expect(state.isUploadingImage).toBe(false); + }); + + it('should replace the existing image with the new one', () => { + expect(state.metricImages).toMatchObject([newImage]); + }); + }); + + describe('RECEIVE_METRIC_DELETE_SUCCESS', () => { + const deletedImageId = testImages[1].id; + const expectedResult = [testImages[0], testImages[2]]; + + beforeEach(() => { + createState({ metricImages: [...testImages] }); + mutations[types.RECEIVE_METRIC_DELETE_SUCCESS](state, deletedImageId); + }); + + it('should remove the correct metric image', () => { + expect(state.metricImages).toEqual(expectedResult); + }); + }); + + describe('SET_INITIAL_DATA', () => { + beforeEach(() => { + mutations[types.SET_INITIAL_DATA](state, initialData); + }); + + it('should unset the loading state', () => { + expect(state.modelIid).toBe(initialData.modelIid); + expect(state.projectId).toBe(initialData.projectId); + }); + }); +}); diff --git a/spec/frontend/vue_shared/components/notes/placeholder_note_spec.js b/spec/frontend/vue_shared/components/notes/placeholder_note_spec.js index c8dab0204d3..6881cb79740 100644 --- a/spec/frontend/vue_shared/components/notes/placeholder_note_spec.js +++ b/spec/frontend/vue_shared/components/notes/placeholder_note_spec.js @@ -3,7 +3,7 @@ import Vue from 'vue'; import Vuex from 'vuex'; import IssuePlaceholderNote from '~/vue_shared/components/notes/placeholder_note.vue'; import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; -import { userDataMock } from '../../../notes/mock_data'; +import { userDataMock } from 'jest/notes/mock_data'; Vue.use(Vuex); diff --git a/spec/frontend/vue_shared/components/project_avatar/default_spec.js b/spec/frontend/vue_shared/components/project_avatar/default_spec.js deleted file mode 100644 index d042db6051c..00000000000 --- a/spec/frontend/vue_shared/components/project_avatar/default_spec.js +++ /dev/null @@ -1,50 +0,0 @@ -import Vue, { nextTick } from 'vue'; -import mountComponent from 'helpers/vue_mount_component_helper'; -import { projectData } from 'jest/ide/mock_data'; -import { TEST_HOST } from 'spec/test_constants'; -import { getFirstCharacterCapitalized } from '~/lib/utils/text_utility'; -import ProjectAvatarDefault from '~/vue_shared/components/deprecated_project_avatar/default.vue'; - -describe('ProjectAvatarDefault component', () => { - const Component = Vue.extend(ProjectAvatarDefault); - let vm; - - beforeEach(() => { - vm = mountComponent(Component, { - project: projectData, - }); - }); - - afterEach(() => { - vm.$destroy(); - }); - - it('renders identicon if project has no avatar_url', async () => { - const expectedText = getFirstCharacterCapitalized(projectData.name); - - vm.project = { - ...vm.project, - avatar_url: null, - }; - - await nextTick(); - const identiconEl = vm.$el.querySelector('.identicon'); - - expect(identiconEl).not.toBe(null); - expect(identiconEl.textContent.trim()).toEqual(expectedText); - }); - - it('renders avatar image if project has avatar_url', async () => { - const avatarUrl = `${TEST_HOST}/images/home/nasa.svg`; - - vm.project = { - ...vm.project, - avatar_url: avatarUrl, - }; - - await nextTick(); - expect(vm.$el.querySelector('.avatar')).not.toBeNull(); - expect(vm.$el.querySelector('.identicon')).toBeNull(); - expect(vm.$el.querySelector('img')).toHaveAttr('src', avatarUrl); - }); -}); diff --git a/spec/frontend/vue_shared/components/project_selector/project_list_item_spec.js b/spec/frontend/vue_shared/components/project_selector/project_list_item_spec.js index 5afa017aa76..397ab2254b9 100644 --- a/spec/frontend/vue_shared/components/project_selector/project_list_item_spec.js +++ b/spec/frontend/vue_shared/components/project_selector/project_list_item_spec.js @@ -2,7 +2,7 @@ import { shallowMount } from '@vue/test-utils'; import Vue from 'vue'; import mockProjects from 'test_fixtures_static/projects.json'; import { trimText } from 'helpers/text_helper'; -import ProjectAvatar from '~/vue_shared/components/deprecated_project_avatar/default.vue'; +import ProjectAvatar from '~/vue_shared/components/project_avatar.vue'; import ProjectListItem from '~/vue_shared/components/project_selector/project_list_item.vue'; describe('ProjectListItem component', () => { @@ -52,8 +52,13 @@ describe('ProjectListItem component', () => { it(`renders the project avatar`, () => { wrapper = shallowMount(Component, options); + const avatar = wrapper.findComponent(ProjectAvatar); - expect(wrapper.findComponent(ProjectAvatar).exists()).toBe(true); + expect(avatar.exists()).toBe(true); + expect(avatar.props()).toMatchObject({ + projectAvatarUrl: '', + projectName: project.name_with_namespace, + }); }); it(`renders a simple namespace name with a trailing slash`, () => { diff --git a/spec/frontend/vue_shared/components/registry/persisted_dropdown_selection_spec.js b/spec/frontend/vue_shared/components/registry/persisted_dropdown_selection_spec.js index c65ded000d3..616fefe847e 100644 --- a/spec/frontend/vue_shared/components/registry/persisted_dropdown_selection_spec.js +++ b/spec/frontend/vue_shared/components/registry/persisted_dropdown_selection_spec.js @@ -36,10 +36,10 @@ describe('Persisted dropdown selection', () => { }); describe('local storage sync', () => { - it('uses the local storage sync component', () => { + it('uses the local storage sync component with the correct props', () => { createComponent(); - expect(findLocalStorageSync().exists()).toBe(true); + expect(findLocalStorageSync().props('asString')).toBe(true); }); it('passes the right props', () => { diff --git a/spec/frontend/vue_shared/components/runner_aws_deployments/__snapshots__/runner_aws_deployments_modal_spec.js.snap b/spec/frontend/vue_shared/components/runner_aws_deployments/__snapshots__/runner_aws_deployments_modal_spec.js.snap index 6954bd5ccff..ac313e556fc 100644 --- a/spec/frontend/vue_shared/components/runner_aws_deployments/__snapshots__/runner_aws_deployments_modal_spec.js.snap +++ b/spec/frontend/vue_shared/components/runner_aws_deployments/__snapshots__/runner_aws_deployments_modal_spec.js.snap @@ -42,7 +42,7 @@ exports[`RunnerAwsDeploymentsModal renders the modal 1`] = ` <gl-accordion-item-stub class="gl-font-weight-normal" title="More Details" - title-visible="Less Details" + titlevisible="Less Details" > <p class="gl-pt-2" @@ -76,7 +76,7 @@ exports[`RunnerAwsDeploymentsModal renders the modal 1`] = ` <gl-accordion-item-stub class="gl-font-weight-normal" title="More Details" - title-visible="Less Details" + titlevisible="Less Details" > <p class="gl-pt-2" @@ -110,7 +110,7 @@ exports[`RunnerAwsDeploymentsModal renders the modal 1`] = ` <gl-accordion-item-stub class="gl-font-weight-normal" title="More Details" - title-visible="Less Details" + titlevisible="Less Details" > <p class="gl-pt-2" @@ -144,7 +144,7 @@ exports[`RunnerAwsDeploymentsModal renders the modal 1`] = ` <gl-accordion-item-stub class="gl-font-weight-normal" title="More Details" - title-visible="Less Details" + titlevisible="Less Details" > <p class="gl-pt-2" diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/actions_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/actions_spec.js index 2e4c056df61..2bc513e87bf 100644 --- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/actions_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/actions_spec.js @@ -21,87 +21,81 @@ describe('LabelsSelect Actions', () => { }); describe('setInitialState', () => { - it('sets initial store state', (done) => { - testAction( + it('sets initial store state', () => { + return testAction( actions.setInitialState, mockInitialState, state, [{ type: types.SET_INITIAL_STATE, payload: mockInitialState }], [], - done, ); }); }); describe('toggleDropdownButton', () => { - it('toggles dropdown button', (done) => { - testAction( + it('toggles dropdown button', () => { + return testAction( actions.toggleDropdownButton, {}, state, [{ type: types.TOGGLE_DROPDOWN_BUTTON }], [], - done, ); }); }); describe('toggleDropdownContents', () => { - it('toggles dropdown contents', (done) => { - testAction( + it('toggles dropdown contents', () => { + return testAction( actions.toggleDropdownContents, {}, state, [{ type: types.TOGGLE_DROPDOWN_CONTENTS }], [], - done, ); }); }); describe('toggleDropdownContentsCreateView', () => { - it('toggles dropdown create view', (done) => { - testAction( + it('toggles dropdown create view', () => { + return testAction( actions.toggleDropdownContentsCreateView, {}, state, [{ type: types.TOGGLE_DROPDOWN_CONTENTS_CREATE_VIEW }], [], - done, ); }); }); describe('requestLabels', () => { - it('sets value of `state.labelsFetchInProgress` to `true`', (done) => { - testAction(actions.requestLabels, {}, state, [{ type: types.REQUEST_LABELS }], [], done); + it('sets value of `state.labelsFetchInProgress` to `true`', () => { + return testAction(actions.requestLabels, {}, state, [{ type: types.REQUEST_LABELS }], []); }); }); describe('receiveLabelsSuccess', () => { - it('sets provided labels to `state.labels`', (done) => { + it('sets provided labels to `state.labels`', () => { const labels = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }]; - testAction( + return testAction( actions.receiveLabelsSuccess, labels, state, [{ type: types.RECEIVE_SET_LABELS_SUCCESS, payload: labels }], [], - done, ); }); }); describe('receiveLabelsFailure', () => { - it('sets value `state.labelsFetchInProgress` to `false`', (done) => { - testAction( + it('sets value `state.labelsFetchInProgress` to `false`', () => { + return testAction( actions.receiveLabelsFailure, {}, state, [{ type: types.RECEIVE_SET_LABELS_FAILURE }], [], - done, ); }); @@ -125,72 +119,67 @@ describe('LabelsSelect Actions', () => { }); describe('on success', () => { - it('dispatches `requestLabels` & `receiveLabelsSuccess` actions', (done) => { + it('dispatches `requestLabels` & `receiveLabelsSuccess` actions', () => { const labels = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }]; mock.onGet(/labels.json/).replyOnce(200, labels); - testAction( + return testAction( actions.fetchLabels, {}, state, [], [{ type: 'requestLabels' }, { type: 'receiveLabelsSuccess', payload: labels }], - done, ); }); }); describe('on failure', () => { - it('dispatches `requestLabels` & `receiveLabelsFailure` actions', (done) => { + it('dispatches `requestLabels` & `receiveLabelsFailure` actions', () => { mock.onGet(/labels.json/).replyOnce(500, {}); - testAction( + return testAction( actions.fetchLabels, {}, state, [], [{ type: 'requestLabels' }, { type: 'receiveLabelsFailure' }], - done, ); }); }); }); describe('requestCreateLabel', () => { - it('sets value `state.labelCreateInProgress` to `true`', (done) => { - testAction( + it('sets value `state.labelCreateInProgress` to `true`', () => { + return testAction( actions.requestCreateLabel, {}, state, [{ type: types.REQUEST_CREATE_LABEL }], [], - done, ); }); }); describe('receiveCreateLabelSuccess', () => { - it('sets value `state.labelCreateInProgress` to `false`', (done) => { - testAction( + it('sets value `state.labelCreateInProgress` to `false`', () => { + return testAction( actions.receiveCreateLabelSuccess, {}, state, [{ type: types.RECEIVE_CREATE_LABEL_SUCCESS }], [], - done, ); }); }); describe('receiveCreateLabelFailure', () => { - it('sets value `state.labelCreateInProgress` to `false`', (done) => { - testAction( + it('sets value `state.labelCreateInProgress` to `false`', () => { + return testAction( actions.receiveCreateLabelFailure, {}, state, [{ type: types.RECEIVE_CREATE_LABEL_FAILURE }], [], - done, ); }); @@ -214,11 +203,11 @@ describe('LabelsSelect Actions', () => { }); describe('on success', () => { - it('dispatches `requestCreateLabel`, `fetchLabels` & `receiveCreateLabelSuccess` & `toggleDropdownContentsCreateView` actions', (done) => { + it('dispatches `requestCreateLabel`, `fetchLabels` & `receiveCreateLabelSuccess` & `toggleDropdownContentsCreateView` actions', () => { const label = { id: 1 }; mock.onPost(/labels.json/).replyOnce(200, label); - testAction( + return testAction( actions.createLabel, {}, state, @@ -229,38 +218,35 @@ describe('LabelsSelect Actions', () => { { type: 'receiveCreateLabelSuccess' }, { type: 'toggleDropdownContentsCreateView' }, ], - done, ); }); }); describe('on failure', () => { - it('dispatches `requestCreateLabel` & `receiveCreateLabelFailure` actions', (done) => { + it('dispatches `requestCreateLabel` & `receiveCreateLabelFailure` actions', () => { mock.onPost(/labels.json/).replyOnce(500, {}); - testAction( + return testAction( actions.createLabel, {}, state, [], [{ type: 'requestCreateLabel' }, { type: 'receiveCreateLabelFailure' }], - done, ); }); }); }); describe('updateSelectedLabels', () => { - it('updates `state.labels` based on provided `labels` param', (done) => { + it('updates `state.labels` based on provided `labels` param', () => { const labels = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }]; - testAction( + return testAction( actions.updateSelectedLabels, labels, state, [{ type: types.UPDATE_SELECTED_LABELS, payload: { labels } }], [], - done, ); }); }); diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js index 67e1a3ce932..1b27a294b90 100644 --- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js @@ -11,9 +11,15 @@ import DropdownValue from '~/vue_shared/components/sidebar/labels_select_widget/ import issueLabelsQuery from '~/vue_shared/components/sidebar/labels_select_widget/graphql/issue_labels.query.graphql'; import updateIssueLabelsMutation from '~/boards/graphql/issue_set_labels.mutation.graphql'; import updateMergeRequestLabelsMutation from '~/sidebar/queries/update_merge_request_labels.mutation.graphql'; +import issuableLabelsSubscription from 'ee_else_ce/sidebar/queries/issuable_labels.subscription.graphql'; import updateEpicLabelsMutation from '~/vue_shared/components/sidebar/labels_select_widget/graphql/epic_update_labels.mutation.graphql'; import LabelsSelectRoot from '~/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue'; -import { mockConfig, issuableLabelsQueryResponse, updateLabelsMutationResponse } from './mock_data'; +import { + mockConfig, + issuableLabelsQueryResponse, + updateLabelsMutationResponse, + issuableLabelsSubscriptionResponse, +} from './mock_data'; jest.mock('~/flash'); @@ -21,6 +27,7 @@ Vue.use(VueApollo); const successfulQueryHandler = jest.fn().mockResolvedValue(issuableLabelsQueryResponse); const successfulMutationHandler = jest.fn().mockResolvedValue(updateLabelsMutationResponse); +const subscriptionHandler = jest.fn().mockResolvedValue(issuableLabelsSubscriptionResponse); const errorQueryHandler = jest.fn().mockRejectedValue('Houston, we have a problem'); const updateLabelsMutation = { @@ -42,10 +49,12 @@ describe('LabelsSelectRoot', () => { issuableType = IssuableType.Issue, queryHandler = successfulQueryHandler, mutationHandler = successfulMutationHandler, + isRealtimeEnabled = false, } = {}) => { const mockApollo = createMockApollo([ [issueLabelsQuery, queryHandler], [updateLabelsMutation[issuableType], mutationHandler], + [issuableLabelsSubscription, subscriptionHandler], ]); wrapper = shallowMount(LabelsSelectRoot, { @@ -65,6 +74,9 @@ describe('LabelsSelectRoot', () => { allowLabelEdit: true, allowLabelCreate: true, labelsManagePath: 'test', + glFeatures: { + realtimeLabels: isRealtimeEnabled, + }, }, }); }; @@ -190,5 +202,26 @@ describe('LabelsSelectRoot', () => { message: 'An error occurred while updating labels.', }); }); + + it('does not emit `updateSelectedLabels` event when the subscription is triggered and FF is disabled', async () => { + createComponent(); + await waitForPromises(); + + expect(wrapper.emitted('updateSelectedLabels')).toBeUndefined(); + }); + + it('emits `updateSelectedLabels` event when the subscription is triggered and FF is enabled', async () => { + createComponent({ isRealtimeEnabled: true }); + await waitForPromises(); + + expect(wrapper.emitted('updateSelectedLabels')).toEqual([ + [ + { + id: '1', + labels: issuableLabelsSubscriptionResponse.data.issuableLabelsUpdated.labels.nodes, + }, + ], + ]); + }); }); }); diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/mock_data.js b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/mock_data.js index 49224fb915c..afad9314ace 100644 --- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/mock_data.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/mock_data.js @@ -141,6 +141,34 @@ export const issuableLabelsQueryResponse = { }, }; +export const issuableLabelsSubscriptionResponse = { + data: { + issuableLabelsUpdated: { + id: '1', + labels: { + nodes: [ + { + __typename: 'Label', + color: '#330066', + description: null, + id: 'gid://gitlab/ProjectLabel/1', + title: 'Label1', + textColor: '#000000', + }, + { + __typename: 'Label', + color: '#000000', + description: null, + id: 'gid://gitlab/ProjectLabel/2', + title: 'Label2', + textColor: '#ffffff', + }, + ], + }, + }, + }, +}; + export const updateLabelsMutationResponse = { data: { updateIssuableLabels: { diff --git a/spec/frontend/vue_shared/components/source_viewer/components/chunk_line_spec.js b/spec/frontend/vue_shared/components/source_viewer/components/chunk_line_spec.js new file mode 100644 index 00000000000..eb2eec92534 --- /dev/null +++ b/spec/frontend/vue_shared/components/source_viewer/components/chunk_line_spec.js @@ -0,0 +1,69 @@ +import { GlLink } from '@gitlab/ui'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import ChunkLine from '~/vue_shared/components/source_viewer/components/chunk_line.vue'; +import { + BIDI_CHARS, + BIDI_CHARS_CLASS_LIST, + BIDI_CHAR_TOOLTIP, +} from '~/vue_shared/components/source_viewer/constants'; + +const DEFAULT_PROPS = { + number: 2, + content: '// Line content', + language: 'javascript', +}; + +describe('Chunk Line component', () => { + let wrapper; + + const createComponent = (props = {}) => { + wrapper = shallowMountExtended(ChunkLine, { propsData: { ...DEFAULT_PROPS, ...props } }); + }; + + const findLink = () => wrapper.findComponent(GlLink); + const findContent = () => wrapper.findByTestId('content'); + const findWrappedBidiChars = () => wrapper.findAllByTestId('bidi-wrapper'); + + beforeEach(() => { + createComponent(); + }); + + afterEach(() => wrapper.destroy()); + + describe('rendering', () => { + it('wraps BiDi characters', () => { + const content = `// some content ${BIDI_CHARS.toString()} with BiDi chars`; + createComponent({ content }); + const wrappedBidiChars = findWrappedBidiChars(); + + expect(wrappedBidiChars.length).toBe(BIDI_CHARS.length); + + wrappedBidiChars.wrappers.forEach((_, i) => { + expect(wrappedBidiChars.at(i).text()).toBe(BIDI_CHARS[i]); + expect(wrappedBidiChars.at(i).attributes()).toMatchObject({ + class: BIDI_CHARS_CLASS_LIST, + title: BIDI_CHAR_TOOLTIP, + }); + }); + }); + + it('renders a line number', () => { + expect(findLink().attributes()).toMatchObject({ + 'data-line-number': `${DEFAULT_PROPS.number}`, + to: `#L${DEFAULT_PROPS.number}`, + id: `L${DEFAULT_PROPS.number}`, + }); + + expect(findLink().text()).toBe(DEFAULT_PROPS.number.toString()); + }); + + it('renders content', () => { + expect(findContent().attributes()).toMatchObject({ + id: `LC${DEFAULT_PROPS.number}`, + lang: DEFAULT_PROPS.language, + }); + + expect(findContent().text()).toBe(DEFAULT_PROPS.content); + }); + }); +}); diff --git a/spec/frontend/vue_shared/components/source_viewer/components/chunk_spec.js b/spec/frontend/vue_shared/components/source_viewer/components/chunk_spec.js new file mode 100644 index 00000000000..42c4f2eacb8 --- /dev/null +++ b/spec/frontend/vue_shared/components/source_viewer/components/chunk_spec.js @@ -0,0 +1,82 @@ +import { GlIntersectionObserver } from '@gitlab/ui'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import Chunk from '~/vue_shared/components/source_viewer/components/chunk.vue'; +import ChunkLine from '~/vue_shared/components/source_viewer/components/chunk_line.vue'; + +const DEFAULT_PROPS = { + chunkIndex: 2, + isHighlighted: false, + content: '// Line 1 content \n // Line 2 content', + startingFrom: 140, + totalLines: 50, + language: 'javascript', +}; + +describe('Chunk component', () => { + let wrapper; + + const createComponent = (props = {}) => { + wrapper = shallowMountExtended(Chunk, { propsData: { ...DEFAULT_PROPS, ...props } }); + }; + + const findIntersectionObserver = () => wrapper.findComponent(GlIntersectionObserver); + const findChunkLines = () => wrapper.findAllComponents(ChunkLine); + const findLineNumbers = () => wrapper.findAllByTestId('line-number'); + const findContent = () => wrapper.findByTestId('content'); + + beforeEach(() => { + createComponent(); + }); + + afterEach(() => wrapper.destroy()); + + describe('Intersection observer', () => { + it('renders an Intersection observer component', () => { + expect(findIntersectionObserver().exists()).toBe(true); + }); + + it('emits an appear event when intersection-observer appears', () => { + findIntersectionObserver().vm.$emit('appear'); + + expect(wrapper.emitted('appear')).toEqual([[DEFAULT_PROPS.chunkIndex]]); + }); + + it('does not emit an appear event is isHighlighted is true', () => { + createComponent({ isHighlighted: true }); + findIntersectionObserver().vm.$emit('appear'); + + expect(wrapper.emitted('appear')).toEqual(undefined); + }); + }); + + describe('rendering', () => { + it('does not render a Chunk Line component if isHighlighted is false', () => { + expect(findChunkLines().length).toBe(0); + }); + + it('renders simplified line numbers and content if isHighlighted is false', () => { + expect(findLineNumbers().length).toBe(DEFAULT_PROPS.totalLines); + + expect(findLineNumbers().at(0).attributes()).toMatchObject({ + 'data-line-number': `${DEFAULT_PROPS.startingFrom + 1}`, + href: `#L${DEFAULT_PROPS.startingFrom + 1}`, + id: `L${DEFAULT_PROPS.startingFrom + 1}`, + }); + + expect(findContent().text()).toBe(DEFAULT_PROPS.content); + }); + + it('renders Chunk Line components if isHighlighted is true', () => { + const splitContent = DEFAULT_PROPS.content.split('\n'); + createComponent({ isHighlighted: true }); + + expect(findChunkLines().length).toBe(splitContent.length); + + expect(findChunkLines().at(0).props()).toMatchObject({ + number: DEFAULT_PROPS.startingFrom + 1, + content: splitContent[0], + language: DEFAULT_PROPS.language, + }); + }); + }); +}); diff --git a/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js b/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js index ab579945e22..6a9ea75127d 100644 --- a/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js +++ b/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js @@ -1,24 +1,38 @@ import hljs from 'highlight.js/lib/core'; -import { GlLoadingIcon } from '@gitlab/ui'; -import Vue, { nextTick } from 'vue'; +import Vue from 'vue'; import VueRouter from 'vue-router'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import SourceViewer from '~/vue_shared/components/source_viewer/source_viewer.vue'; +import Chunk from '~/vue_shared/components/source_viewer/components/chunk.vue'; import { ROUGE_TO_HLJS_LANGUAGE_MAP } from '~/vue_shared/components/source_viewer/constants'; -import LineNumbers from '~/vue_shared/components/line_numbers.vue'; import waitForPromises from 'helpers/wait_for_promises'; -import * as sourceViewerUtils from '~/vue_shared/components/source_viewer/utils'; +import LineHighlighter from '~/blob/line_highlighter'; +import eventHub from '~/notes/event_hub'; +jest.mock('~/blob/line_highlighter'); jest.mock('highlight.js/lib/core'); Vue.use(VueRouter); const router = new VueRouter(); +const generateContent = (content, totalLines = 1) => { + let generatedContent = ''; + for (let i = 0; i < totalLines; i += 1) { + generatedContent += `Line: ${i + 1} = ${content}\n`; + } + return generatedContent; +}; + +const execImmediately = (callback) => callback(); + describe('Source Viewer component', () => { let wrapper; const language = 'docker'; const mappedLanguage = ROUGE_TO_HLJS_LANGUAGE_MAP[language]; - const content = `// Some source code`; - const DEFAULT_BLOB_DATA = { language, rawTextBlob: content }; + const chunk1 = generateContent('// Some source code 1', 70); + const chunk2 = generateContent('// Some source code 2', 70); + const content = chunk1 + chunk2; + const path = 'some/path.js'; + const DEFAULT_BLOB_DATA = { language, rawTextBlob: content, path }; const highlightedContent = `<span data-testid='test-highlighted' id='LC1'>${content}</span><span id='LC2'></span>`; const createComponent = async (blob = {}) => { @@ -29,15 +43,13 @@ describe('Source Viewer component', () => { await waitForPromises(); }; - const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); - const findLineNumbers = () => wrapper.findComponent(LineNumbers); - const findHighlightedContent = () => wrapper.findByTestId('test-highlighted'); - const findFirstLine = () => wrapper.find('#LC1'); + const findChunks = () => wrapper.findAllComponents(Chunk); beforeEach(() => { hljs.highlight.mockImplementation(() => ({ value: highlightedContent })); hljs.highlightAuto.mockImplementation(() => ({ value: highlightedContent })); - jest.spyOn(sourceViewerUtils, 'wrapLines'); + jest.spyOn(window, 'requestIdleCallback').mockImplementation(execImmediately); + jest.spyOn(eventHub, '$emit'); return createComponent(); }); @@ -45,6 +57,8 @@ describe('Source Viewer component', () => { afterEach(() => wrapper.destroy()); describe('highlight.js', () => { + beforeEach(() => createComponent({ language: mappedLanguage })); + it('registers the language definition', async () => { const languageDefinition = await import(`highlight.js/lib/languages/${mappedLanguage}`); @@ -54,72 +68,51 @@ describe('Source Viewer component', () => { ); }); - it('highlights the content', () => { - expect(hljs.highlight).toHaveBeenCalledWith(content, { language: mappedLanguage }); + it('highlights the first chunk', () => { + expect(hljs.highlight).toHaveBeenCalledWith(chunk1.trim(), { language: mappedLanguage }); }); describe('auto-detects if a language cannot be loaded', () => { beforeEach(() => createComponent({ language: 'some_unknown_language' })); it('highlights the content with auto-detection', () => { - expect(hljs.highlightAuto).toHaveBeenCalledWith(content); + expect(hljs.highlightAuto).toHaveBeenCalledWith(chunk1.trim()); }); }); }); describe('rendering', () => { - it('renders a loading icon if no highlighted content is available yet', async () => { - hljs.highlight.mockImplementation(() => ({ value: null })); - await createComponent(); - - expect(findLoadingIcon().exists()).toBe(true); - }); + it('renders the first chunk', async () => { + const firstChunk = findChunks().at(0); - it('calls the wrapLines helper method with highlightedContent and mappedLanguage', () => { - expect(sourceViewerUtils.wrapLines).toHaveBeenCalledWith(highlightedContent, mappedLanguage); - }); - - it('renders Line Numbers', () => { - expect(findLineNumbers().props('lines')).toBe(1); - }); + expect(firstChunk.props('content')).toContain(chunk1); - it('renders the highlighted content', () => { - expect(findHighlightedContent().exists()).toBe(true); + expect(firstChunk.props()).toMatchObject({ + totalLines: 70, + startingFrom: 0, + }); }); - }); - describe('selecting a line', () => { - let firstLine; - let firstLineElement; + it('renders the second chunk', async () => { + const secondChunk = findChunks().at(1); - beforeEach(() => { - firstLine = findFirstLine(); - firstLineElement = firstLine.element; + expect(secondChunk.props('content')).toContain(chunk2.trim()); - jest.spyOn(firstLineElement, 'scrollIntoView'); - jest.spyOn(firstLineElement.classList, 'add'); - jest.spyOn(firstLineElement.classList, 'remove'); - }); - - it('adds the highlight (hll) class', async () => { - wrapper.vm.$router.push('#LC1'); - await nextTick(); - - expect(firstLineElement.classList.add).toHaveBeenCalledWith('hll'); + expect(secondChunk.props()).toMatchObject({ + totalLines: 70, + startingFrom: 70, + }); }); + }); - it('removes the highlight (hll) class from a previously highlighted line', async () => { - wrapper.vm.$router.push('#LC2'); - await nextTick(); - - expect(firstLineElement.classList.remove).toHaveBeenCalledWith('hll'); - }); + it('emits showBlobInteractionZones on the eventHub when chunk appears', () => { + findChunks().at(0).vm.$emit('appear'); + expect(eventHub.$emit).toBeCalledWith('showBlobInteractionZones', path); + }); - it('scrolls the line into view', () => { - expect(firstLineElement.scrollIntoView).toHaveBeenCalledWith({ - behavior: 'smooth', - block: 'center', - }); + describe('LineHighlighter', () => { + it('instantiates the lineHighlighter class', async () => { + expect(LineHighlighter).toHaveBeenCalledWith({ scrollBehavior: 'auto' }); }); }); }); diff --git a/spec/frontend/vue_shared/components/source_viewer/utils_spec.js b/spec/frontend/vue_shared/components/source_viewer/utils_spec.js deleted file mode 100644 index 0631e7efd54..00000000000 --- a/spec/frontend/vue_shared/components/source_viewer/utils_spec.js +++ /dev/null @@ -1,26 +0,0 @@ -import { wrapLines } from '~/vue_shared/components/source_viewer/utils'; - -describe('Wrap lines', () => { - it.each` - content | language | output - ${'line 1'} | ${'javascript'} | ${'<span id="LC1" lang="javascript" class="line">line 1</span>'} - ${'line 1\nline 2'} | ${'html'} | ${`<span id="LC1" lang="html" class="line">line 1</span>\n<span id="LC2" lang="html" class="line">line 2</span>`} - ${'<span class="hljs-code">line 1\nline 2</span>'} | ${'html'} | ${`<span id="LC1" lang="html" class="hljs-code">line 1\n<span id="LC2" lang="html" class="line">line 2</span></span>`} - ${'<span class="hljs-code">```bash'} | ${'bash'} | ${'<span id="LC1" lang="bash" class="hljs-code">```bash'} - ${'<span class="hljs-code">```bash'} | ${'valid-language1'} | ${'<span id="LC1" lang="valid-language1" class="hljs-code">```bash'} - ${'<span class="hljs-code">```bash'} | ${'valid_language2'} | ${'<span id="LC1" lang="valid_language2" class="hljs-code">```bash'} - `('returns lines wrapped in spans containing line numbers', ({ content, language, output }) => { - expect(wrapLines(content, language)).toBe(output); - }); - - it.each` - language - ${'invalidLanguage>'} - ${'"invalidLanguage"'} - ${'<invalidLanguage'} - `('returns lines safely without XSS language is not valid', ({ language }) => { - expect(wrapLines('<span class="hljs-code">```bash', language)).toBe( - '<span id="LC1" lang="" class="hljs-code">```bash', - ); - }); -}); diff --git a/spec/frontend/vue_shared/components/user_avatar/user_avatar_image_new_spec.js b/spec/frontend/vue_shared/components/user_avatar/user_avatar_image_new_spec.js index f624f84eabd..5e05b54cb8c 100644 --- a/spec/frontend/vue_shared/components/user_avatar/user_avatar_image_new_spec.js +++ b/spec/frontend/vue_shared/components/user_avatar/user_avatar_image_new_spec.js @@ -109,19 +109,33 @@ describe('User Avatar Image Component', () => { default: ['Action!'], }; - beforeEach(() => { - wrapper = shallowMount(UserAvatarImage, { - propsData: PROVIDED_PROPS, - slots, + describe('when `tooltipText` is provided and no default slot', () => { + beforeEach(() => { + wrapper = shallowMount(UserAvatarImage, { + propsData: { ...PROVIDED_PROPS }, + }); }); - }); - it('renders the tooltip slot', () => { - expect(wrapper.findComponent(GlTooltip).exists()).toBe(true); + it('renders the tooltip with `tooltipText` as content', () => { + expect(wrapper.findComponent(GlTooltip).text()).toBe(PROVIDED_PROPS.tooltipText); + }); }); - it('renders the tooltip content', () => { - expect(wrapper.findComponent(GlTooltip).text()).toContain(slots.default[0]); + describe('when `tooltipText` and default slot is provided', () => { + beforeEach(() => { + wrapper = shallowMount(UserAvatarImage, { + propsData: { ...PROVIDED_PROPS }, + slots, + }); + }); + + it('does not render `tooltipText` inside the tooltip', () => { + expect(wrapper.findComponent(GlTooltip).text()).not.toBe(PROVIDED_PROPS.tooltipText); + }); + + it('renders the content provided via default slot', () => { + expect(wrapper.findComponent(GlTooltip).text()).toContain(slots.default[0]); + }); }); }); }); diff --git a/spec/frontend/vue_shared/components/user_avatar/user_avatar_image_old_spec.js b/spec/frontend/vue_shared/components/user_avatar/user_avatar_image_old_spec.js index 5051b2b9cae..2c1be6ec47e 100644 --- a/spec/frontend/vue_shared/components/user_avatar/user_avatar_image_old_spec.js +++ b/spec/frontend/vue_shared/components/user_avatar/user_avatar_image_old_spec.js @@ -90,33 +90,38 @@ describe('User Avatar Image Component', () => { }); }); - describe('dynamic tooltip content', () => { - const props = PROVIDED_PROPS; + describe('Dynamic tooltip content', () => { const slots = { default: ['Action!'], }; - beforeEach(() => { - wrapper = shallowMount(UserAvatarImage, { - propsData: { props }, - slots, + describe('when `tooltipText` is provided and no default slot', () => { + beforeEach(() => { + wrapper = shallowMount(UserAvatarImage, { + propsData: { ...PROVIDED_PROPS }, + }); }); - }); - it('renders the tooltip slot', () => { - expect(wrapper.findComponent(GlTooltip).exists()).toBe(true); + it('renders the tooltip with `tooltipText` as content', () => { + expect(wrapper.findComponent(GlTooltip).text()).toBe(PROVIDED_PROPS.tooltipText); + }); }); - it('renders the tooltip content', () => { - expect(wrapper.findComponent(GlTooltip).text()).toContain(slots.default[0]); - }); + describe('when `tooltipText` and default slot is provided', () => { + beforeEach(() => { + wrapper = shallowMount(UserAvatarImage, { + propsData: { ...PROVIDED_PROPS }, + slots, + }); + }); - it('does not render tooltip data attributes on avatar image', () => { - const avatarImg = wrapper.find('img'); + it('does not render `tooltipText` inside the tooltip', () => { + expect(wrapper.findComponent(GlTooltip).text()).not.toBe(PROVIDED_PROPS.tooltipText); + }); - expect(avatarImg.attributes('title')).toBeFalsy(); - expect(avatarImg.attributes('data-placement')).not.toBeDefined(); - expect(avatarImg.attributes('data-container')).not.toBeDefined(); + it('renders the content provided via default slot', () => { + expect(wrapper.findComponent(GlTooltip).text()).toContain(slots.default[0]); + }); }); }); }); diff --git a/spec/frontend/vue_shared/components/user_avatar/user_avatar_list_spec.js b/spec/frontend/vue_shared/components/user_avatar/user_avatar_list_spec.js index 66bb234aef6..20ff0848cff 100644 --- a/spec/frontend/vue_shared/components/user_avatar/user_avatar_list_spec.js +++ b/spec/frontend/vue_shared/components/user_avatar/user_avatar_list_spec.js @@ -153,4 +153,29 @@ describe('UserAvatarList', () => { }); }); }); + + describe('additional styling for the image', () => { + it('should not add CSS class when feature flag `glAvatarForAllUserAvatars` is disabled', () => { + factory({ + propsData: { items: createList(1) }, + }); + + const link = wrapper.findComponent(UserAvatarLink); + expect(link.props('imgCssClasses')).not.toBe('gl-mr-3'); + }); + + it('should add CSS class when feature flag `glAvatarForAllUserAvatars` is enabled', () => { + factory({ + propsData: { items: createList(1) }, + provide: { + glFeatures: { + glAvatarForAllUserAvatars: true, + }, + }, + }); + + const link = wrapper.findComponent(UserAvatarLink); + expect(link.props('imgCssClasses')).toBe('gl-mr-3'); + }); + }); }); diff --git a/spec/frontend/vue_shared/components/user_select_spec.js b/spec/frontend/vue_shared/components/user_select_spec.js index cb476910944..ec9128d5e38 100644 --- a/spec/frontend/vue_shared/components/user_select_spec.js +++ b/spec/frontend/vue_shared/components/user_select_spec.js @@ -16,7 +16,7 @@ import { searchResponseOnMR, projectMembersResponse, participantsQueryResponse, -} from '../../sidebar/mock_data'; +} from 'jest/sidebar/mock_data'; const assignee = { id: 'gid://gitlab/User/4', diff --git a/spec/frontend/vue_shared/components/web_ide_link_spec.js b/spec/frontend/vue_shared/components/web_ide_link_spec.js index e79935f8fa6..040461f6be4 100644 --- a/spec/frontend/vue_shared/components/web_ide_link_spec.js +++ b/spec/frontend/vue_shared/components/web_ide_link_spec.js @@ -261,7 +261,10 @@ describe('Web IDE link component', () => { }); it('should update local storage when selection changes', async () => { - expect(findLocalStorageSync().props('value')).toBe(ACTION_WEB_IDE.key); + expect(findLocalStorageSync().props()).toMatchObject({ + asString: true, + value: ACTION_WEB_IDE.key, + }); findActionsButton().vm.$emit('select', ACTION_GITPOD.key); diff --git a/spec/frontend/vue_shared/issuable/list/components/issuable_list_root_spec.js b/spec/frontend/vue_shared/issuable/list/components/issuable_list_root_spec.js index 64823cd4c6c..058cb30c1d5 100644 --- a/spec/frontend/vue_shared/issuable/list/components/issuable_list_root_spec.js +++ b/spec/frontend/vue_shared/issuable/list/components/issuable_list_root_spec.js @@ -1,4 +1,9 @@ -import { GlAlert, GlKeysetPagination, GlSkeletonLoading, GlPagination } from '@gitlab/ui'; +import { + GlAlert, + GlKeysetPagination, + GlDeprecatedSkeletonLoading as GlSkeletonLoading, + GlPagination, +} from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import VueDraggable from 'vuedraggable'; diff --git a/spec/frontend/vue_shared/security_reports/store/modules/sast/actions_spec.js b/spec/frontend/vue_shared/security_reports/store/modules/sast/actions_spec.js index 6af07273cf6..46bfd7eceb1 100644 --- a/spec/frontend/vue_shared/security_reports/store/modules/sast/actions_spec.js +++ b/spec/frontend/vue_shared/security_reports/store/modules/sast/actions_spec.js @@ -26,8 +26,8 @@ describe('sast report actions', () => { }); describe('setDiffEndpoint', () => { - it(`should commit ${types.SET_DIFF_ENDPOINT} with the correct path`, (done) => { - testAction( + it(`should commit ${types.SET_DIFF_ENDPOINT} with the correct path`, () => { + return testAction( actions.setDiffEndpoint, diffEndpoint, state, @@ -38,20 +38,19 @@ describe('sast report actions', () => { }, ], [], - done, ); }); }); describe('requestDiff', () => { - it(`should commit ${types.REQUEST_DIFF}`, (done) => { - testAction(actions.requestDiff, {}, state, [{ type: types.REQUEST_DIFF }], [], done); + it(`should commit ${types.REQUEST_DIFF}`, () => { + return testAction(actions.requestDiff, {}, state, [{ type: types.REQUEST_DIFF }], []); }); }); describe('receiveDiffSuccess', () => { - it(`should commit ${types.RECEIVE_DIFF_SUCCESS} with the correct response`, (done) => { - testAction( + it(`should commit ${types.RECEIVE_DIFF_SUCCESS} with the correct response`, () => { + return testAction( actions.receiveDiffSuccess, reports, state, @@ -62,14 +61,13 @@ describe('sast report actions', () => { }, ], [], - done, ); }); }); describe('receiveDiffError', () => { - it(`should commit ${types.RECEIVE_DIFF_ERROR} with the correct response`, (done) => { - testAction( + it(`should commit ${types.RECEIVE_DIFF_ERROR} with the correct response`, () => { + return testAction( actions.receiveDiffError, error, state, @@ -80,7 +78,6 @@ describe('sast report actions', () => { }, ], [], - done, ); }); }); @@ -107,9 +104,9 @@ describe('sast report actions', () => { .replyOnce(200, reports.enrichData); }); - it('should dispatch the `receiveDiffSuccess` action', (done) => { + it('should dispatch the `receiveDiffSuccess` action', () => { const { diff, enrichData } = reports; - testAction( + return testAction( actions.fetchDiff, {}, { ...rootState, ...state }, @@ -124,7 +121,6 @@ describe('sast report actions', () => { }, }, ], - done, ); }); }); @@ -135,10 +131,10 @@ describe('sast report actions', () => { mock.onGet(diffEndpoint).replyOnce(200, reports.diff); }); - it('should dispatch the `receiveDiffSuccess` action with empty enrich data', (done) => { + it('should dispatch the `receiveDiffSuccess` action with empty enrich data', () => { const { diff } = reports; const enrichData = []; - testAction( + return testAction( actions.fetchDiff, {}, { ...rootState, ...state }, @@ -153,7 +149,6 @@ describe('sast report actions', () => { }, }, ], - done, ); }); }); @@ -167,14 +162,13 @@ describe('sast report actions', () => { .replyOnce(404); }); - it('should dispatch the `receiveError` action', (done) => { - testAction( + it('should dispatch the `receiveError` action', () => { + return testAction( actions.fetchDiff, {}, { ...rootState, ...state }, [], [{ type: 'requestDiff' }, { type: 'receiveDiffError' }], - done, ); }); }); @@ -188,14 +182,13 @@ describe('sast report actions', () => { .replyOnce(200, reports.enrichData); }); - it('should dispatch the `receiveDiffError` action', (done) => { - testAction( + it('should dispatch the `receiveDiffError` action', () => { + return testAction( actions.fetchDiff, {}, { ...rootState, ...state }, [], [{ type: 'requestDiff' }, { type: 'receiveDiffError' }], - done, ); }); }); diff --git a/spec/frontend/vue_shared/security_reports/store/modules/secret_detection/actions_spec.js b/spec/frontend/vue_shared/security_reports/store/modules/secret_detection/actions_spec.js index d22fee864e7..4f4f653bb72 100644 --- a/spec/frontend/vue_shared/security_reports/store/modules/secret_detection/actions_spec.js +++ b/spec/frontend/vue_shared/security_reports/store/modules/secret_detection/actions_spec.js @@ -26,8 +26,8 @@ describe('secret detection report actions', () => { }); describe('setDiffEndpoint', () => { - it(`should commit ${types.SET_DIFF_ENDPOINT} with the correct path`, (done) => { - testAction( + it(`should commit ${types.SET_DIFF_ENDPOINT} with the correct path`, () => { + return testAction( actions.setDiffEndpoint, diffEndpoint, state, @@ -38,20 +38,19 @@ describe('secret detection report actions', () => { }, ], [], - done, ); }); }); describe('requestDiff', () => { - it(`should commit ${types.REQUEST_DIFF}`, (done) => { - testAction(actions.requestDiff, {}, state, [{ type: types.REQUEST_DIFF }], [], done); + it(`should commit ${types.REQUEST_DIFF}`, () => { + return testAction(actions.requestDiff, {}, state, [{ type: types.REQUEST_DIFF }], []); }); }); describe('receiveDiffSuccess', () => { - it(`should commit ${types.RECEIVE_DIFF_SUCCESS} with the correct response`, (done) => { - testAction( + it(`should commit ${types.RECEIVE_DIFF_SUCCESS} with the correct response`, () => { + return testAction( actions.receiveDiffSuccess, reports, state, @@ -62,14 +61,13 @@ describe('secret detection report actions', () => { }, ], [], - done, ); }); }); describe('receiveDiffError', () => { - it(`should commit ${types.RECEIVE_DIFF_ERROR} with the correct response`, (done) => { - testAction( + it(`should commit ${types.RECEIVE_DIFF_ERROR} with the correct response`, () => { + return testAction( actions.receiveDiffError, error, state, @@ -80,7 +78,6 @@ describe('secret detection report actions', () => { }, ], [], - done, ); }); }); @@ -107,9 +104,10 @@ describe('secret detection report actions', () => { .replyOnce(200, reports.enrichData); }); - it('should dispatch the `receiveDiffSuccess` action', (done) => { + it('should dispatch the `receiveDiffSuccess` action', () => { const { diff, enrichData } = reports; - testAction( + + return testAction( actions.fetchDiff, {}, { ...rootState, ...state }, @@ -124,7 +122,6 @@ describe('secret detection report actions', () => { }, }, ], - done, ); }); }); @@ -135,10 +132,10 @@ describe('secret detection report actions', () => { mock.onGet(diffEndpoint).replyOnce(200, reports.diff); }); - it('should dispatch the `receiveDiffSuccess` action with empty enrich data', (done) => { + it('should dispatch the `receiveDiffSuccess` action with empty enrich data', () => { const { diff } = reports; const enrichData = []; - testAction( + return testAction( actions.fetchDiff, {}, { ...rootState, ...state }, @@ -153,7 +150,6 @@ describe('secret detection report actions', () => { }, }, ], - done, ); }); }); @@ -167,14 +163,13 @@ describe('secret detection report actions', () => { .replyOnce(404); }); - it('should dispatch the `receiveDiffError` action', (done) => { - testAction( + it('should dispatch the `receiveDiffError` action', () => { + return testAction( actions.fetchDiff, {}, { ...rootState, ...state }, [], [{ type: 'requestDiff' }, { type: 'receiveDiffError' }], - done, ); }); }); @@ -188,14 +183,13 @@ describe('secret detection report actions', () => { .replyOnce(200, reports.enrichData); }); - it('should dispatch the `receiveDiffError` action', (done) => { - testAction( + it('should dispatch the `receiveDiffError` action', () => { + return testAction( actions.fetchDiff, {}, { ...rootState, ...state }, [], [{ type: 'requestDiff' }, { type: 'receiveDiffError' }], - done, ); }); }); |