Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-04-20 13:00:54 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2022-04-20 13:00:54 +0300
commit3cccd102ba543e02725d247893729e5c73b38295 (patch)
treef36a04ec38517f5deaaacb5acc7d949688d1e187 /spec/frontend/vue_shared/components
parent205943281328046ef7b4528031b90fbda70c75ac (diff)
Add latest changes from gitlab-org/gitlab@14-10-stable-eev14.10.0-rc42
Diffstat (limited to 'spec/frontend/vue_shared/components')
-rw-r--r--spec/frontend/vue_shared/components/__snapshots__/awards_list_spec.js.snap127
-rw-r--r--spec/frontend/vue_shared/components/__snapshots__/identicon_spec.js.snap21
-rw-r--r--spec/frontend/vue_shared/components/awards_list_spec.js3
-rw-r--r--spec/frontend/vue_shared/components/blob_viewers/simple_viewer_spec.js17
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js32
-rw-r--r--spec/frontend/vue_shared/components/form/input_copy_toggle_visibility_spec.js10
-rw-r--r--spec/frontend/vue_shared/components/help_popover_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/identicon_spec.js50
-rw-r--r--spec/frontend/vue_shared/components/line_numbers_spec.js37
-rw-r--r--spec/frontend/vue_shared/components/local_storage_sync_spec.js277
-rw-r--r--spec/frontend/vue_shared/components/markdown/apply_suggestion_spec.js23
-rw-r--r--spec/frontend/vue_shared/components/markdown/field_spec.js17
-rw-r--r--spec/frontend/vue_shared/components/markdown/header_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/metric_images/__snapshots__/metric_images_table_spec.js.snap73
-rw-r--r--spec/frontend/vue_shared/components/metric_images/metric_images_tab_spec.js174
-rw-r--r--spec/frontend/vue_shared/components/metric_images/metric_images_table_spec.js230
-rw-r--r--spec/frontend/vue_shared/components/metric_images/mock_data.js5
-rw-r--r--spec/frontend/vue_shared/components/metric_images/store/actions_spec.js158
-rw-r--r--spec/frontend/vue_shared/components/metric_images/store/mutations_spec.js147
-rw-r--r--spec/frontend/vue_shared/components/notes/placeholder_note_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/project_avatar/default_spec.js50
-rw-r--r--spec/frontend/vue_shared/components/project_selector/project_list_item_spec.js9
-rw-r--r--spec/frontend/vue_shared/components/registry/persisted_dropdown_selection_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/runner_aws_deployments/__snapshots__/runner_aws_deployments_modal_spec.js.snap8
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/actions_spec.js74
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js35
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_widget/mock_data.js28
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/components/chunk_line_spec.js69
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/components/chunk_spec.js82
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js107
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/utils_spec.js26
-rw-r--r--spec/frontend/vue_shared/components/user_avatar/user_avatar_image_new_spec.js32
-rw-r--r--spec/frontend/vue_shared/components/user_avatar/user_avatar_image_old_spec.js39
-rw-r--r--spec/frontend/vue_shared/components/user_avatar/user_avatar_list_spec.js25
-rw-r--r--spec/frontend/vue_shared/components/user_select_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/web_ide_link_spec.js5
36 files changed, 1386 insertions, 616 deletions
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);