summaryrefslogtreecommitdiff
path: root/spec/frontend/vue_shared/components
diff options
context:
space:
mode:
Diffstat (limited to 'spec/frontend/vue_shared/components')
-rw-r--r--spec/frontend/vue_shared/components/__snapshots__/memory_graph_spec.js.snap1
-rw-r--r--spec/frontend/vue_shared/components/confidentiality_badge_spec.js10
-rw-r--r--spec/frontend/vue_shared/components/content_viewer/viewers/markdown_viewer_spec.js17
-rw-r--r--spec/frontend/vue_shared/components/date_time_picker/date_time_picker_input_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/dismissible_container_spec.js3
-rw-r--r--spec/frontend/vue_shared/components/dropdown_keyboard_navigation_spec.js19
-rw-r--r--spec/frontend/vue_shared/components/entity_select/entity_select_spec.js268
-rw-r--r--spec/frontend/vue_shared/components/entity_select/group_select_spec.js135
-rw-r--r--spec/frontend/vue_shared/components/entity_select/project_select_spec.js248
-rw-r--r--spec/frontend/vue_shared/components/entity_select/utils_spec.js (renamed from spec/frontend/vue_shared/components/group_select/utils_spec.js)4
-rw-r--r--spec/frontend/vue_shared/components/file_icon_spec.js5
-rw-r--r--spec/frontend/vue_shared/components/file_row_spec.js9
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/store/modules/filters/mutations_spec.js12
-rw-r--r--spec/frontend/vue_shared/components/group_select/group_select_spec.js322
-rw-r--r--spec/frontend/vue_shared/components/header_ci_component_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/incubation/incubation_alert_spec.js34
-rw-r--r--spec/frontend/vue_shared/components/incubation/pagination_spec.js76
-rw-r--r--spec/frontend/vue_shared/components/markdown/field_spec.js43
-rw-r--r--spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js41
-rw-r--r--spec/frontend/vue_shared/components/markdown_drawer/utils/fetch_spec.js7
-rw-r--r--spec/frontend/vue_shared/components/new_resource_dropdown/mock_data.js54
-rw-r--r--spec/frontend/vue_shared/components/new_resource_dropdown/new_resource_dropdown_spec.js262
-rw-r--r--spec/frontend/vue_shared/components/notes/system_note_spec.js3
-rw-r--r--spec/frontend/vue_shared/components/runner_aws_deployments/runner_aws_deployments_modal_spec.js72
-rw-r--r--spec/frontend/vue_shared/components/runner_aws_deployments/runner_aws_deployments_spec.js41
-rw-r--r--spec/frontend/vue_shared/components/runner_instructions/instructions/runner_aws_instructions_spec.js29
-rw-r--r--spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/security_reports/__snapshots__/security_summary_spec.js.snap8
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/components/__snapshots__/chunk_spec.js.snap24
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/components/chunk_deprecated_spec.js123
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/components/chunk_spec.js94
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/highlight_util_spec.js70
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/mock_data.js24
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/source_viewer_deprecated_spec.js177
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js156
-rw-r--r--spec/frontend/vue_shared/components/url_sync_spec.js80
-rw-r--r--spec/frontend/vue_shared/components/user_avatar/user_avatar_list_spec.js31
-rw-r--r--spec/frontend/vue_shared/components/user_select_spec.js14
-rw-r--r--spec/frontend/vue_shared/components/web_ide_link_spec.js13
39 files changed, 1750 insertions, 791 deletions
diff --git a/spec/frontend/vue_shared/components/__snapshots__/memory_graph_spec.js.snap b/spec/frontend/vue_shared/components/__snapshots__/memory_graph_spec.js.snap
index b7b43264330..ad08120fada 100644
--- a/spec/frontend/vue_shared/components/__snapshots__/memory_graph_spec.js.snap
+++ b/spec/frontend/vue_shared/components/__snapshots__/memory_graph_spec.js.snap
@@ -9,6 +9,7 @@ exports[`MemoryGraph Render chart should draw container with chart 1`] = `
data="Nov 12 2019 19:17:33,2.87,Nov 12 2019 19:18:33,2.78,Nov 12 2019 19:19:33,2.78,Nov 12 2019 19:20:33,3.01"
gradient=""
height="25"
+ smooth="0"
tooltiplabel="MB"
/>
</div>
diff --git a/spec/frontend/vue_shared/components/confidentiality_badge_spec.js b/spec/frontend/vue_shared/components/confidentiality_badge_spec.js
index e1860d3399b..3f7ec156c19 100644
--- a/spec/frontend/vue_shared/components/confidentiality_badge_spec.js
+++ b/spec/frontend/vue_shared/components/confidentiality_badge_spec.js
@@ -1,13 +1,13 @@
import { GlBadge } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import { WorkspaceType, IssuableType } from '~/issues/constants';
+import { WorkspaceType, TYPE_ISSUE, TYPE_EPIC } from '~/issues/constants';
import ConfidentialityBadge from '~/vue_shared/components/confidentiality_badge.vue';
const createComponent = ({
workspaceType = WorkspaceType.project,
- issuableType = IssuableType.Issue,
+ issuableType = TYPE_ISSUE,
} = {}) =>
shallowMount(ConfidentialityBadge, {
propsData: {
@@ -28,9 +28,9 @@ describe('ConfidentialityBadge', () => {
});
it.each`
- workspaceType | issuableType | expectedTooltip
- ${WorkspaceType.project} | ${IssuableType.Issue} | ${'Only project members with at least the Reporter role, the author, and assignees can view or be notified about this issue.'}
- ${WorkspaceType.group} | ${IssuableType.Epic} | ${'Only group members with at least the Reporter role can view or be notified about this epic.'}
+ workspaceType | issuableType | expectedTooltip
+ ${WorkspaceType.project} | ${TYPE_ISSUE} | ${'Only project members with at least the Reporter role, the author, and assignees can view or be notified about this issue.'}
+ ${WorkspaceType.group} | ${TYPE_EPIC} | ${'Only group members with at least the Reporter role can view or be notified about this epic.'}
`(
'should render gl-badge with correct tooltip when workspaceType is $workspaceType and issuableType is $issuableType',
({ workspaceType, issuableType, expectedTooltip }) => {
diff --git a/spec/frontend/vue_shared/components/content_viewer/viewers/markdown_viewer_spec.js b/spec/frontend/vue_shared/components/content_viewer/viewers/markdown_viewer_spec.js
index 0d329b6a065..b0c0fc79676 100644
--- a/spec/frontend/vue_shared/components/content_viewer/viewers/markdown_viewer_spec.js
+++ b/spec/frontend/vue_shared/components/content_viewer/viewers/markdown_viewer_spec.js
@@ -3,6 +3,7 @@ import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import MarkdownViewer from '~/vue_shared/components/content_viewer/viewers/markdown_viewer.vue';
jest.mock('~/behaviors/markdown/render_gfm');
@@ -35,9 +36,11 @@ describe('MarkdownViewer', () => {
describe('success', () => {
beforeEach(() => {
- mock.onPost(`${gon.relative_url_root}/testproject/preview_markdown`).replyOnce(200, {
- body: '<b>testing</b> {{gl_md_img_1}}',
- });
+ mock
+ .onPost(`${gon.relative_url_root}/testproject/preview_markdown`)
+ .replyOnce(HTTP_STATUS_OK, {
+ body: '<b>testing</b> {{gl_md_img_1}}',
+ });
});
it('renders a skeleton loader while the markdown is loading', () => {
@@ -100,9 +103,11 @@ describe('MarkdownViewer', () => {
describe('error', () => {
beforeEach(() => {
- mock.onPost(`${gon.relative_url_root}/testproject/preview_markdown`).replyOnce(500, {
- body: 'Internal Server Error',
- });
+ mock
+ .onPost(`${gon.relative_url_root}/testproject/preview_markdown`)
+ .replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR, {
+ body: 'Internal Server Error',
+ });
});
it('renders an error message if loading the markdown preview fails', () => {
createComponent();
diff --git a/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_input_spec.js b/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_input_spec.js
index 2c5bb86d8a5..c1495e8264a 100644
--- a/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_input_spec.js
+++ b/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_input_spec.js
@@ -56,11 +56,11 @@ describe('DateTimePickerInput', () => {
it('input event is emitted when focus is lost', () => {
createComponent();
- jest.spyOn(wrapper.vm, '$emit');
+
const input = wrapper.find('input');
input.setValue(inputValue);
input.trigger('blur');
- expect(wrapper.vm.$emit).toHaveBeenCalledWith('input', inputValue);
+ expect(wrapper.emitted('input')[0][0]).toEqual(inputValue);
});
});
diff --git a/spec/frontend/vue_shared/components/dismissible_container_spec.js b/spec/frontend/vue_shared/components/dismissible_container_spec.js
index f7030f38709..7d8581e11e9 100644
--- a/spec/frontend/vue_shared/components/dismissible_container_spec.js
+++ b/spec/frontend/vue_shared/components/dismissible_container_spec.js
@@ -1,6 +1,7 @@
import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import dismissibleContainer from '~/vue_shared/components/dismissible_container.vue';
describe('DismissibleContainer', () => {
@@ -28,7 +29,7 @@ describe('DismissibleContainer', () => {
});
it('successfully dismisses', () => {
- mockAxios.onPost(propsData.path).replyOnce(200);
+ mockAxios.onPost(propsData.path).replyOnce(HTTP_STATUS_OK);
const button = findBtn();
button.trigger('click');
diff --git a/spec/frontend/vue_shared/components/dropdown_keyboard_navigation_spec.js b/spec/frontend/vue_shared/components/dropdown_keyboard_navigation_spec.js
index c34041f9305..119d6448507 100644
--- a/spec/frontend/vue_shared/components/dropdown_keyboard_navigation_spec.js
+++ b/spec/frontend/vue_shared/components/dropdown_keyboard_navigation_spec.js
@@ -61,27 +61,8 @@ describe('DropdownKeyboardNavigation', () => {
});
describe('keydown events', () => {
- let incrementSpy;
-
beforeEach(() => {
createComponent();
- incrementSpy = jest.spyOn(wrapper.vm, 'increment');
- });
-
- afterEach(() => {
- incrementSpy.mockRestore();
- });
-
- it('onKeydown-Down calls increment(1)', () => {
- helpers.arrowDown();
-
- expect(incrementSpy).toHaveBeenCalledWith(1);
- });
-
- it('onKeydown-Up calls increment(-1)', () => {
- helpers.arrowUp();
-
- expect(incrementSpy).toHaveBeenCalledWith(-1);
});
it('onKeydown-Tab $emits @tab event', () => {
diff --git a/spec/frontend/vue_shared/components/entity_select/entity_select_spec.js b/spec/frontend/vue_shared/components/entity_select/entity_select_spec.js
new file mode 100644
index 00000000000..6b98f6c5e89
--- /dev/null
+++ b/spec/frontend/vue_shared/components/entity_select/entity_select_spec.js
@@ -0,0 +1,268 @@
+import { nextTick } from 'vue';
+import { GlCollapsibleListbox, GlFormGroup } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import EntitySelect from '~/vue_shared/components/entity_select/entity_select.vue';
+import { QUERY_TOO_SHORT_MESSAGE } from '~/vue_shared/components/entity_select/constants';
+import waitForPromises from 'helpers/wait_for_promises';
+
+describe('EntitySelect', () => {
+ let wrapper;
+ let fetchItemsMock;
+ let fetchInitialSelectionTextMock;
+
+ // Mocks
+ const itemMock = {
+ text: 'selectedGroup',
+ value: '1',
+ };
+
+ // Stubs
+ const GlAlert = {
+ template: '<div><slot /></div>',
+ };
+
+ // Props
+ const label = 'label';
+ const inputName = 'inputName';
+ const inputId = 'inputId';
+ const headerText = 'headerText';
+ const defaultToggleText = 'defaultToggleText';
+
+ // Finders
+ const findListbox = () => wrapper.findComponent(GlCollapsibleListbox);
+ const findInput = () => wrapper.findByTestId('input');
+
+ // Helpers
+ const createComponent = ({ props = {}, slots = {}, stubs = {} } = {}) => {
+ wrapper = shallowMountExtended(EntitySelect, {
+ propsData: {
+ label,
+ inputName,
+ inputId,
+ headerText,
+ defaultToggleText,
+ fetchItems: fetchItemsMock,
+ ...props,
+ },
+ stubs: {
+ GlAlert,
+ EntitySelect,
+ ...stubs,
+ },
+ slots,
+ });
+ };
+ const openListbox = () => findListbox().vm.$emit('shown');
+ const search = (searchString) => findListbox().vm.$emit('search', searchString);
+ const selectGroup = async () => {
+ openListbox();
+ await nextTick();
+ findListbox().vm.$emit('select', itemMock.value);
+ return nextTick();
+ };
+
+ beforeEach(() => {
+ fetchItemsMock = jest.fn().mockImplementation(() => ({ items: [itemMock], totalPages: 1 }));
+ });
+
+ describe('on mount', () => {
+ it('calls the fetch function when the listbox is opened', async () => {
+ createComponent();
+ openListbox();
+ await nextTick();
+
+ expect(fetchItemsMock).toHaveBeenCalledTimes(1);
+ });
+
+ it("fetches the initially selected value's name", async () => {
+ fetchInitialSelectionTextMock = jest.fn().mockImplementation(() => itemMock.text);
+ createComponent({
+ props: {
+ fetchInitialSelectionText: fetchInitialSelectionTextMock,
+ initialSelection: itemMock.value,
+ },
+ });
+ await nextTick();
+
+ expect(fetchInitialSelectionTextMock).toHaveBeenCalledTimes(1);
+ expect(findListbox().props('toggleText')).toBe(itemMock.text);
+ });
+ });
+
+ it("renders the error slot's content", () => {
+ const selector = 'data-test-id="error-element"';
+ createComponent({
+ slots: {
+ error: `<div ${selector} />`,
+ },
+ });
+
+ expect(wrapper.find(`[${selector}]`).exists()).toBe(true);
+ });
+
+ it('renders the label slot if provided', () => {
+ const testid = 'label-slot';
+ createComponent({
+ slots: {
+ label: `<div data-testid="${testid}" />`,
+ },
+ stubs: {
+ GlFormGroup,
+ },
+ });
+
+ expect(wrapper.findByTestId(testid).exists()).toBe(true);
+ });
+
+ describe('selection', () => {
+ it('uses the default toggle text while no group is selected', () => {
+ createComponent();
+
+ expect(findListbox().props('toggleText')).toBe(defaultToggleText);
+ });
+
+ describe('once a group is selected', () => {
+ it(`uses the selected group's name as the toggle text`, async () => {
+ createComponent();
+ await selectGroup();
+
+ expect(findListbox().props('toggleText')).toBe(itemMock.text);
+ });
+
+ it(`uses the selected group's ID as the listbox' and input value`, async () => {
+ createComponent();
+ await selectGroup();
+
+ expect(findListbox().attributes('selected')).toBe(itemMock.value);
+ expect(findInput().attributes('value')).toBe(itemMock.value);
+ });
+
+ it(`on reset, falls back to the default toggle text`, async () => {
+ createComponent();
+ await selectGroup();
+
+ findListbox().vm.$emit('reset');
+ await nextTick();
+
+ expect(findListbox().props('toggleText')).toBe(defaultToggleText);
+ });
+ });
+ });
+
+ describe('search', () => {
+ it('sets `searching` to `true` when first opening the dropdown', async () => {
+ createComponent();
+
+ expect(findListbox().props('searching')).toBe(false);
+
+ openListbox();
+ await nextTick();
+
+ expect(findListbox().props('searching')).toBe(true);
+ });
+
+ it('sets `searching` to `true` while searching', async () => {
+ createComponent();
+
+ expect(findListbox().props('searching')).toBe(false);
+
+ search('foo');
+ await nextTick();
+
+ expect(findListbox().props('searching')).toBe(true);
+ });
+
+ it('fetches groups matching the search string', async () => {
+ const searchString = 'searchString';
+ createComponent();
+ openListbox();
+
+ expect(fetchItemsMock).toHaveBeenCalledTimes(1);
+
+ fetchItemsMock.mockImplementation(() => ({ items: [], totalPages: 1 }));
+ search(searchString);
+ await nextTick();
+
+ expect(fetchItemsMock).toHaveBeenCalledTimes(2);
+ });
+
+ it('shows a notice if the search query is too short', async () => {
+ const searchString = 'a';
+ createComponent();
+ openListbox();
+ search(searchString);
+ await nextTick();
+
+ expect(fetchItemsMock).toHaveBeenCalledTimes(1);
+ expect(findListbox().props('noResultsText')).toBe(QUERY_TOO_SHORT_MESSAGE);
+ });
+ });
+
+ describe('pagination', () => {
+ const searchString = 'searchString';
+
+ beforeEach(async () => {
+ let requestCount = 0;
+ fetchItemsMock.mockImplementation((searchQuery, page) => {
+ requestCount += 1;
+ return {
+ items: [
+ {
+ text: `Group [page: ${page} - search: ${searchQuery}]`,
+ value: `id:${requestCount}`,
+ },
+ ],
+ totalPages: 3,
+ };
+ });
+ createComponent();
+ openListbox();
+ findListbox().vm.$emit('bottom-reached');
+ return nextTick();
+ });
+
+ it('fetches the next page when bottom is reached', () => {
+ expect(fetchItemsMock).toHaveBeenCalledTimes(2);
+ expect(fetchItemsMock).toHaveBeenLastCalledWith('', 2);
+ });
+
+ it('fetches the first page when the search query changes', async () => {
+ search(searchString);
+ await nextTick();
+
+ expect(fetchItemsMock).toHaveBeenCalledTimes(3);
+ expect(fetchItemsMock).toHaveBeenLastCalledWith(searchString, 1);
+ });
+
+ it('retains the search query when infinite scrolling', async () => {
+ search(searchString);
+ await nextTick();
+ findListbox().vm.$emit('bottom-reached');
+ await nextTick();
+
+ expect(fetchItemsMock).toHaveBeenCalledTimes(4);
+ expect(fetchItemsMock).toHaveBeenLastCalledWith(searchString, 2);
+ });
+
+ it('pauses infinite scroll after fetching the last page', async () => {
+ expect(findListbox().props('infiniteScroll')).toBe(true);
+
+ findListbox().vm.$emit('bottom-reached');
+ await waitForPromises();
+
+ expect(findListbox().props('infiniteScroll')).toBe(false);
+ });
+
+ it('resumes infinite scroll when search query changes', async () => {
+ findListbox().vm.$emit('bottom-reached');
+ await waitForPromises();
+
+ expect(findListbox().props('infiniteScroll')).toBe(false);
+
+ search(searchString);
+ await waitForPromises();
+
+ expect(findListbox().props('infiniteScroll')).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/entity_select/group_select_spec.js b/spec/frontend/vue_shared/components/entity_select/group_select_spec.js
new file mode 100644
index 00000000000..83560e367ea
--- /dev/null
+++ b/spec/frontend/vue_shared/components/entity_select/group_select_spec.js
@@ -0,0 +1,135 @@
+import { GlCollapsibleListbox } from '@gitlab/ui';
+import MockAdapter from 'axios-mock-adapter';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
+import GroupSelect from '~/vue_shared/components/entity_select/group_select.vue';
+import EntitySelect from '~/vue_shared/components/entity_select/entity_select.vue';
+import {
+ GROUP_TOGGLE_TEXT,
+ GROUP_HEADER_TEXT,
+ FETCH_GROUPS_ERROR,
+ FETCH_GROUP_ERROR,
+} from '~/vue_shared/components/entity_select/constants';
+import waitForPromises from 'helpers/wait_for_promises';
+
+describe('GroupSelect', () => {
+ let wrapper;
+ let mock;
+
+ // Mocks
+ const groupMock = {
+ full_name: 'selectedGroup',
+ id: '1',
+ };
+ const groupEndpoint = `/api/undefined/groups/${groupMock.id}`;
+
+ // Stubs
+ const GlAlert = {
+ template: '<div><slot /></div>',
+ };
+
+ // Props
+ const label = 'label';
+ const inputName = 'inputName';
+ const inputId = 'inputId';
+
+ // Finders
+ const findListbox = () => wrapper.findComponent(GlCollapsibleListbox);
+ const findEntitySelect = () => wrapper.findComponent(EntitySelect);
+ const findAlert = () => wrapper.findComponent(GlAlert);
+
+ // Helpers
+ const createComponent = ({ props = {} } = {}) => {
+ wrapper = shallowMountExtended(GroupSelect, {
+ propsData: {
+ label,
+ inputName,
+ inputId,
+ ...props,
+ },
+ stubs: {
+ GlAlert,
+ EntitySelect,
+ },
+ });
+ };
+ const openListbox = () => findListbox().vm.$emit('shown');
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ describe('entity_select props', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it.each`
+ prop | expectedValue
+ ${'label'} | ${label}
+ ${'inputName'} | ${inputName}
+ ${'inputId'} | ${inputId}
+ ${'defaultToggleText'} | ${GROUP_TOGGLE_TEXT}
+ ${'headerText'} | ${GROUP_HEADER_TEXT}
+ `('passes the $prop prop to entity-select', ({ prop, expectedValue }) => {
+ expect(findEntitySelect().props(prop)).toBe(expectedValue);
+ });
+ });
+
+ describe('on mount', () => {
+ it('fetches groups when the listbox is opened', async () => {
+ createComponent();
+ await waitForPromises();
+
+ expect(mock.history.get).toHaveLength(0);
+
+ openListbox();
+ await waitForPromises();
+
+ expect(mock.history.get).toHaveLength(1);
+ });
+
+ describe('with an initial selection', () => {
+ it("fetches the initially selected value's name", async () => {
+ mock.onGet(groupEndpoint).reply(HTTP_STATUS_OK, groupMock);
+ createComponent({ props: { initialSelection: groupMock.id } });
+ await waitForPromises();
+
+ expect(mock.history.get).toHaveLength(1);
+ expect(findListbox().props('toggleText')).toBe(groupMock.full_name);
+ });
+
+ it('show an error if fetching the individual group fails', async () => {
+ mock
+ .onGet('/api/undefined/groups.json')
+ .reply(HTTP_STATUS_OK, [{ full_name: 'notTheSelectedGroup', id: '2' }]);
+ mock.onGet(groupEndpoint).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
+ createComponent({ props: { initialSelection: groupMock.id } });
+
+ expect(findAlert().exists()).toBe(false);
+
+ await waitForPromises();
+
+ expect(findAlert().exists()).toBe(true);
+ expect(findAlert().text()).toBe(FETCH_GROUP_ERROR);
+ });
+ });
+ });
+
+ it('shows an error when fetching groups fails', async () => {
+ mock.onGet('/api/undefined/groups.json').reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
+ createComponent();
+ openListbox();
+ expect(findAlert().exists()).toBe(false);
+
+ await waitForPromises();
+
+ expect(findAlert().exists()).toBe(true);
+ expect(findAlert().text()).toBe(FETCH_GROUPS_ERROR);
+ });
+});
diff --git a/spec/frontend/vue_shared/components/entity_select/project_select_spec.js b/spec/frontend/vue_shared/components/entity_select/project_select_spec.js
new file mode 100644
index 00000000000..57dce032d30
--- /dev/null
+++ b/spec/frontend/vue_shared/components/entity_select/project_select_spec.js
@@ -0,0 +1,248 @@
+import { GlCollapsibleListbox } from '@gitlab/ui';
+import MockAdapter from 'axios-mock-adapter';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK, HTTP_STATUS_INTERNAL_SERVER_ERROR } from '~/lib/utils/http_status';
+import ProjectSelect from '~/vue_shared/components/entity_select/project_select.vue';
+import EntitySelect from '~/vue_shared/components/entity_select/entity_select.vue';
+import {
+ PROJECT_TOGGLE_TEXT,
+ PROJECT_HEADER_TEXT,
+ FETCH_PROJECTS_ERROR,
+ FETCH_PROJECT_ERROR,
+} from '~/vue_shared/components/entity_select/constants';
+import waitForPromises from 'helpers/wait_for_promises';
+
+describe('ProjectSelect', () => {
+ let wrapper;
+ let mock;
+
+ // Stubs
+ const GlAlert = {
+ template: '<div><slot /></div>',
+ };
+
+ // Props
+ const label = 'label';
+ const inputName = 'inputName';
+ const inputId = 'inputId';
+ const groupId = '22';
+ const userId = '1';
+
+ // Mocks
+ const apiVersion = 'v4';
+ const projectMock = {
+ name_with_namespace: 'selectedProject',
+ id: '1',
+ };
+ const projectsEndpoint = `/api/${apiVersion}/projects.json`;
+ const groupProjectEndpoint = `/api/${apiVersion}/groups/${groupId}/projects.json`;
+ const userProjectEndpoint = `/api/${apiVersion}/users/${userId}/projects`;
+ const projectEndpoint = `/api/${apiVersion}/projects/${projectMock.id}`;
+
+ // Finders
+ const findListbox = () => wrapper.findComponent(GlCollapsibleListbox);
+ const findEntitySelect = () => wrapper.findComponent(EntitySelect);
+ const findAlert = () => wrapper.findComponent(GlAlert);
+
+ // Helpers
+ const createComponent = ({ props = {} } = {}) => {
+ wrapper = mountExtended(ProjectSelect, {
+ propsData: {
+ label,
+ inputName,
+ inputId,
+ groupId,
+ ...props,
+ },
+ stubs: {
+ GlAlert,
+ EntitySelect,
+ },
+ });
+ };
+ const openListbox = () => findListbox().vm.$emit('shown');
+
+ beforeAll(() => {
+ gon.api_version = apiVersion;
+ });
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ it('renders HTML label when hasHtmlLabel is true', () => {
+ const testid = 'html-label';
+ createComponent({
+ props: {
+ label: `<div data-testid="${testid}" />`,
+ hasHtmlLabel: true,
+ },
+ });
+
+ expect(wrapper.findByTestId(testid).exists()).toBe(true);
+ });
+
+ describe('entity_select props', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it.each`
+ prop | expectedValue
+ ${'label'} | ${label}
+ ${'inputName'} | ${inputName}
+ ${'inputId'} | ${inputId}
+ ${'defaultToggleText'} | ${PROJECT_TOGGLE_TEXT}
+ ${'headerText'} | ${PROJECT_HEADER_TEXT}
+ ${'clearable'} | ${true}
+ `('passes the $prop prop to entity-select', ({ prop, expectedValue }) => {
+ expect(findEntitySelect().props(prop)).toBe(expectedValue);
+ });
+ });
+
+ describe('on mount', () => {
+ it('fetches projects when the listbox is opened', async () => {
+ createComponent();
+ await waitForPromises();
+
+ expect(mock.history.get).toHaveLength(0);
+
+ openListbox();
+ await waitForPromises();
+
+ expect(mock.history.get).toHaveLength(1);
+ expect(mock.history.get[0].url).toBe(groupProjectEndpoint);
+ expect(mock.history.get[0].params).toEqual({
+ include_subgroups: false,
+ order_by: 'similarity',
+ per_page: 20,
+ search: '',
+ simple: true,
+ with_shared: true,
+ });
+ });
+
+ it('includes projects from subgroups if includeSubgroups is true', async () => {
+ createComponent({
+ props: {
+ includeSubgroups: true,
+ },
+ });
+ openListbox();
+ await waitForPromises();
+
+ expect(mock.history.get[0].params.include_subgroups).toBe(true);
+ });
+
+ it('fetches projects globally if no group ID is provided', async () => {
+ createComponent({
+ props: {
+ groupId: null,
+ },
+ });
+ openListbox();
+ await waitForPromises();
+
+ expect(mock.history.get[0].url).toBe(projectsEndpoint);
+ expect(mock.history.get[0].params).toEqual({
+ membership: false,
+ order_by: 'similarity',
+ per_page: 20,
+ search: '',
+ simple: true,
+ });
+ });
+
+ it('restricts search to owned projects if membership is true', async () => {
+ createComponent({
+ props: {
+ groupId: null,
+ membership: true,
+ },
+ });
+ openListbox();
+ await waitForPromises();
+
+ expect(mock.history.get[0].params.membership).toBe(true);
+ });
+
+ it("fetches the user's projects if a user ID is provided", async () => {
+ createComponent({
+ props: {
+ groupId: null,
+ userId,
+ },
+ });
+ openListbox();
+ await waitForPromises();
+
+ expect(mock.history.get[0].url).toBe(userProjectEndpoint);
+ expect(mock.history.get[0].params).toEqual({
+ per_page: 20,
+ search: '',
+ with_shared: true,
+ include_subgroups: false,
+ });
+ });
+
+ it.each([null, groupId])(
+ 'fetches with the provided sort key when groupId is %s',
+ async (groupIdProp) => {
+ const orderBy = 'last_activity_at';
+ createComponent({
+ props: {
+ groupId: groupIdProp,
+ orderBy,
+ },
+ });
+ openListbox();
+ await waitForPromises();
+
+ expect(mock.history.get[0].params.order_by).toBe(orderBy);
+ },
+ );
+
+ describe('with an initial selection', () => {
+ it("fetches the initially selected value's name", async () => {
+ mock.onGet(projectEndpoint).reply(HTTP_STATUS_OK, projectMock);
+ createComponent({ props: { initialSelection: projectMock.id } });
+ await waitForPromises();
+
+ expect(mock.history.get).toHaveLength(1);
+ expect(findListbox().props('toggleText')).toBe(projectMock.name_with_namespace);
+ });
+
+ it('show an error if fetching the individual project fails', async () => {
+ mock
+ .onGet(groupProjectEndpoint)
+ .reply(HTTP_STATUS_OK, [{ full_name: 'notTheSelectedProject', id: '2' }]);
+ mock.onGet(projectEndpoint).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
+ createComponent({ props: { initialSelection: projectMock.id } });
+
+ expect(findAlert().exists()).toBe(false);
+
+ await waitForPromises();
+
+ expect(findAlert().exists()).toBe(true);
+ expect(findAlert().text()).toBe(FETCH_PROJECT_ERROR);
+ });
+ });
+ });
+
+ it('shows an error when fetching projects fails', async () => {
+ mock.onGet(groupProjectEndpoint).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
+ createComponent();
+ openListbox();
+ expect(findAlert().exists()).toBe(false);
+
+ await waitForPromises();
+
+ expect(findAlert().exists()).toBe(true);
+ expect(findAlert().text()).toBe(FETCH_PROJECTS_ERROR);
+ });
+});
diff --git a/spec/frontend/vue_shared/components/group_select/utils_spec.js b/spec/frontend/vue_shared/components/entity_select/utils_spec.js
index 5188e1aabf1..9aa1baf204e 100644
--- a/spec/frontend/vue_shared/components/group_select/utils_spec.js
+++ b/spec/frontend/vue_shared/components/entity_select/utils_spec.js
@@ -1,6 +1,6 @@
-import { groupsPath } from '~/vue_shared/components/group_select/utils';
+import { groupsPath } from '~/vue_shared/components/entity_select/utils';
-describe('group_select utils', () => {
+describe('entity_select utils', () => {
describe('groupsPath', () => {
it.each`
groupsFilter | parentGroupID | expectedPath
diff --git a/spec/frontend/vue_shared/components/file_icon_spec.js b/spec/frontend/vue_shared/components/file_icon_spec.js
index 3f4bfc86b67..0fcc0678c13 100644
--- a/spec/frontend/vue_shared/components/file_icon_spec.js
+++ b/spec/frontend/vue_shared/components/file_icon_spec.js
@@ -8,10 +8,7 @@ describe('File Icon component', () => {
const findSvgIcon = () => wrapper.find('svg');
const findGlIcon = () => wrapper.findComponent(GlIcon);
const getIconName = () =>
- findSvgIcon()
- .find('use')
- .element.getAttribute('xlink:href')
- .replace(`${gon.sprite_file_icons}#`, '');
+ findSvgIcon().find('use').element.getAttribute('href').replace(`${gon.sprite_file_icons}#`, '');
const createComponent = (props = {}) => {
wrapper = shallowMount(FileIcon, {
diff --git a/spec/frontend/vue_shared/components/file_row_spec.js b/spec/frontend/vue_shared/components/file_row_spec.js
index c3a71d7fda3..b70d4565f56 100644
--- a/spec/frontend/vue_shared/components/file_row_spec.js
+++ b/spec/frontend/vue_shared/components/file_row_spec.js
@@ -100,15 +100,6 @@ describe('File row component', () => {
});
});
- it('indents row based on level', () => {
- createComponent({
- file: file('t4'),
- level: 2,
- });
-
- expect(wrapper.find('.file-row-name').element.style.marginLeft).toBe('16px');
- });
-
it('renders header for file', () => {
createComponent({
file: {
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/store/modules/filters/mutations_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/store/modules/filters/mutations_spec.js
index 66c6267027b..305f56255a5 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/store/modules/filters/mutations_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/store/modules/filters/mutations_spec.js
@@ -1,6 +1,7 @@
import { get } from 'lodash';
import { mockBranches } from 'jest/vue_shared/components/filtered_search_bar/mock_data';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR } from '~/lib/utils/http_status';
import * as types from '~/vue_shared/components/filtered_search_bar/store/modules/filters/mutation_types';
import mutations from '~/vue_shared/components/filtered_search_bar/store/modules/filters/mutations';
import initialState from '~/vue_shared/components/filtered_search_bar/store/modules/filters/state';
@@ -16,7 +17,6 @@ const labels = filterLabels.map(convertObjectPropsToCamelCase);
const filterValue = { value: 'foo' };
describe('Filters mutations', () => {
- const errorCode = 500;
beforeEach(() => {
state = initialState();
});
@@ -79,35 +79,35 @@ describe('Filters mutations', () => {
${types.RECEIVE_BRANCHES_SUCCESS} | ${'branches'} | ${'errorCode'} | ${null}
${types.RECEIVE_BRANCHES_ERROR} | ${'branches'} | ${'isLoading'} | ${false}
${types.RECEIVE_BRANCHES_ERROR} | ${'branches'} | ${'data'} | ${[]}
- ${types.RECEIVE_BRANCHES_ERROR} | ${'branches'} | ${'errorCode'} | ${errorCode}
+ ${types.RECEIVE_BRANCHES_ERROR} | ${'branches'} | ${'errorCode'} | ${HTTP_STATUS_INTERNAL_SERVER_ERROR}
${types.REQUEST_MILESTONES} | ${'milestones'} | ${'isLoading'} | ${true}
${types.RECEIVE_MILESTONES_SUCCESS} | ${'milestones'} | ${'isLoading'} | ${false}
${types.RECEIVE_MILESTONES_SUCCESS} | ${'milestones'} | ${'data'} | ${milestones}
${types.RECEIVE_MILESTONES_SUCCESS} | ${'milestones'} | ${'errorCode'} | ${null}
${types.RECEIVE_MILESTONES_ERROR} | ${'milestones'} | ${'isLoading'} | ${false}
${types.RECEIVE_MILESTONES_ERROR} | ${'milestones'} | ${'data'} | ${[]}
- ${types.RECEIVE_MILESTONES_ERROR} | ${'milestones'} | ${'errorCode'} | ${errorCode}
+ ${types.RECEIVE_MILESTONES_ERROR} | ${'milestones'} | ${'errorCode'} | ${HTTP_STATUS_INTERNAL_SERVER_ERROR}
${types.REQUEST_AUTHORS} | ${'authors'} | ${'isLoading'} | ${true}
${types.RECEIVE_AUTHORS_SUCCESS} | ${'authors'} | ${'isLoading'} | ${false}
${types.RECEIVE_AUTHORS_SUCCESS} | ${'authors'} | ${'data'} | ${users}
${types.RECEIVE_AUTHORS_SUCCESS} | ${'authors'} | ${'errorCode'} | ${null}
${types.RECEIVE_AUTHORS_ERROR} | ${'authors'} | ${'isLoading'} | ${false}
${types.RECEIVE_AUTHORS_ERROR} | ${'authors'} | ${'data'} | ${[]}
- ${types.RECEIVE_AUTHORS_ERROR} | ${'authors'} | ${'errorCode'} | ${errorCode}
+ ${types.RECEIVE_AUTHORS_ERROR} | ${'authors'} | ${'errorCode'} | ${HTTP_STATUS_INTERNAL_SERVER_ERROR}
${types.REQUEST_LABELS} | ${'labels'} | ${'isLoading'} | ${true}
${types.RECEIVE_LABELS_SUCCESS} | ${'labels'} | ${'isLoading'} | ${false}
${types.RECEIVE_LABELS_SUCCESS} | ${'labels'} | ${'data'} | ${labels}
${types.RECEIVE_LABELS_SUCCESS} | ${'labels'} | ${'errorCode'} | ${null}
${types.RECEIVE_LABELS_ERROR} | ${'labels'} | ${'isLoading'} | ${false}
${types.RECEIVE_LABELS_ERROR} | ${'labels'} | ${'data'} | ${[]}
- ${types.RECEIVE_LABELS_ERROR} | ${'labels'} | ${'errorCode'} | ${errorCode}
+ ${types.RECEIVE_LABELS_ERROR} | ${'labels'} | ${'errorCode'} | ${HTTP_STATUS_INTERNAL_SERVER_ERROR}
${types.REQUEST_ASSIGNEES} | ${'assignees'} | ${'isLoading'} | ${true}
${types.RECEIVE_ASSIGNEES_SUCCESS} | ${'assignees'} | ${'isLoading'} | ${false}
${types.RECEIVE_ASSIGNEES_SUCCESS} | ${'assignees'} | ${'data'} | ${users}
${types.RECEIVE_ASSIGNEES_SUCCESS} | ${'assignees'} | ${'errorCode'} | ${null}
${types.RECEIVE_ASSIGNEES_ERROR} | ${'assignees'} | ${'isLoading'} | ${false}
${types.RECEIVE_ASSIGNEES_ERROR} | ${'assignees'} | ${'data'} | ${[]}
- ${types.RECEIVE_ASSIGNEES_ERROR} | ${'assignees'} | ${'errorCode'} | ${errorCode}
+ ${types.RECEIVE_ASSIGNEES_ERROR} | ${'assignees'} | ${'errorCode'} | ${HTTP_STATUS_INTERNAL_SERVER_ERROR}
`('$mutation will set $stateKey with a given value', ({ mutation, rootKey, stateKey, value }) => {
mutations[mutation](state, value);
diff --git a/spec/frontend/vue_shared/components/group_select/group_select_spec.js b/spec/frontend/vue_shared/components/group_select/group_select_spec.js
deleted file mode 100644
index 87dd7795b98..00000000000
--- a/spec/frontend/vue_shared/components/group_select/group_select_spec.js
+++ /dev/null
@@ -1,322 +0,0 @@
-import { nextTick } from 'vue';
-import { GlFormGroup, GlCollapsibleListbox } from '@gitlab/ui';
-import MockAdapter from 'axios-mock-adapter';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import axios from '~/lib/utils/axios_utils';
-import GroupSelect from '~/vue_shared/components/group_select/group_select.vue';
-import {
- TOGGLE_TEXT,
- RESET_LABEL,
- FETCH_GROUPS_ERROR,
- FETCH_GROUP_ERROR,
- QUERY_TOO_SHORT_MESSAGE,
-} from '~/vue_shared/components/group_select/constants';
-import waitForPromises from 'helpers/wait_for_promises';
-
-describe('GroupSelect', () => {
- let wrapper;
- let mock;
-
- // Mocks
- const groupMock = {
- full_name: 'selectedGroup',
- id: '1',
- };
- const groupEndpoint = `/api/undefined/groups/${groupMock.id}`;
-
- // Stubs
- const GlAlert = {
- template: '<div><slot /></div>',
- };
-
- // Props
- const label = 'label';
- const inputName = 'inputName';
- const inputId = 'inputId';
-
- // Finders
- const findFormGroup = () => wrapper.findComponent(GlFormGroup);
- const findListbox = () => wrapper.findComponent(GlCollapsibleListbox);
- const findInput = () => wrapper.findByTestId('input');
- const findAlert = () => wrapper.findComponent(GlAlert);
-
- // Helpers
- const createComponent = ({ props = {} } = {}) => {
- wrapper = shallowMountExtended(GroupSelect, {
- propsData: {
- label,
- inputName,
- inputId,
- ...props,
- },
- stubs: {
- GlAlert,
- },
- });
- };
- const openListbox = () => findListbox().vm.$emit('shown');
- const search = (searchString) => findListbox().vm.$emit('search', searchString);
- const createComponentWithGroups = () => {
- mock.onGet('/api/undefined/groups.json').reply(200, [groupMock]);
- createComponent();
- openListbox();
- return waitForPromises();
- };
- const selectGroup = () => {
- findListbox().vm.$emit('select', groupMock.id);
- return nextTick();
- };
-
- beforeEach(() => {
- mock = new MockAdapter(axios);
- });
-
- afterEach(() => {
- mock.restore();
- });
-
- it('passes the label to GlFormGroup', () => {
- createComponent();
-
- expect(findFormGroup().attributes('label')).toBe(label);
- });
-
- describe('on mount', () => {
- it('fetches groups when the listbox is opened', async () => {
- createComponent();
- await waitForPromises();
-
- expect(mock.history.get).toHaveLength(0);
-
- openListbox();
- await waitForPromises();
-
- expect(mock.history.get).toHaveLength(1);
- });
-
- describe('with an initial selection', () => {
- it('if the selected group is not part of the fetched list, fetches it individually', async () => {
- mock.onGet(groupEndpoint).reply(200, groupMock);
- createComponent({ props: { initialSelection: groupMock.id } });
- await waitForPromises();
-
- expect(mock.history.get).toHaveLength(1);
- expect(findListbox().props('toggleText')).toBe(groupMock.full_name);
- });
-
- it('show an error if fetching the individual group fails', async () => {
- mock
- .onGet('/api/undefined/groups.json')
- .reply(200, [{ full_name: 'notTheSelectedGroup', id: '2' }]);
- mock.onGet(groupEndpoint).reply(500);
- createComponent({ props: { initialSelection: groupMock.id } });
-
- expect(findAlert().exists()).toBe(false);
-
- await waitForPromises();
-
- expect(findAlert().exists()).toBe(true);
- expect(findAlert().text()).toBe(FETCH_GROUP_ERROR);
- });
- });
- });
-
- it('shows an error when fetching groups fails', async () => {
- mock.onGet('/api/undefined/groups.json').reply(500);
- createComponent();
- openListbox();
- expect(findAlert().exists()).toBe(false);
-
- await waitForPromises();
-
- expect(findAlert().exists()).toBe(true);
- expect(findAlert().text()).toBe(FETCH_GROUPS_ERROR);
- });
-
- describe('selection', () => {
- it('uses the default toggle text while no group is selected', async () => {
- await createComponentWithGroups();
-
- expect(findListbox().props('toggleText')).toBe(TOGGLE_TEXT);
- });
-
- describe('once a group is selected', () => {
- it(`uses the selected group's name as the toggle text`, async () => {
- await createComponentWithGroups();
- await selectGroup();
-
- expect(findListbox().props('toggleText')).toBe(groupMock.full_name);
- });
-
- it(`uses the selected group's ID as the listbox' and input value`, async () => {
- await createComponentWithGroups();
- await selectGroup();
-
- expect(findListbox().attributes('selected')).toBe(groupMock.id);
- expect(findInput().attributes('value')).toBe(groupMock.id);
- });
-
- it(`on reset, falls back to the default toggle text`, async () => {
- await createComponentWithGroups();
- await selectGroup();
-
- findListbox().vm.$emit('reset');
- await nextTick();
-
- expect(findListbox().props('toggleText')).toBe(TOGGLE_TEXT);
- });
- });
- });
-
- describe('search', () => {
- it('sets `searching` to `true` when first opening the dropdown', async () => {
- createComponent();
-
- expect(findListbox().props('searching')).toBe(false);
-
- openListbox();
- await nextTick();
-
- expect(findListbox().props('searching')).toBe(true);
- });
-
- it('sets `searching` to `true` while searching', async () => {
- await createComponentWithGroups();
-
- expect(findListbox().props('searching')).toBe(false);
-
- search('foo');
- await nextTick();
-
- expect(findListbox().props('searching')).toBe(true);
- });
-
- it('fetches groups matching the search string', async () => {
- const searchString = 'searchString';
- await createComponentWithGroups();
-
- expect(mock.history.get).toHaveLength(1);
-
- search(searchString);
- await waitForPromises();
-
- expect(mock.history.get).toHaveLength(2);
- expect(mock.history.get[1].params).toStrictEqual({
- page: 1,
- per_page: 20,
- search: searchString,
- });
- });
-
- it('shows a notice if the search query is too short', async () => {
- const searchString = 'a';
- await createComponentWithGroups();
- search(searchString);
- await waitForPromises();
-
- expect(mock.history.get).toHaveLength(1);
- expect(findListbox().props('noResultsText')).toBe(QUERY_TOO_SHORT_MESSAGE);
- });
- });
-
- describe('pagination', () => {
- const searchString = 'searchString';
-
- beforeEach(async () => {
- let requestCount = 0;
- mock.onGet('/api/undefined/groups.json').reply(({ params }) => {
- requestCount += 1;
- return [
- 200,
- [
- {
- full_name: `Group [page: ${params.page} - search: ${params.search}]`,
- id: requestCount,
- },
- ],
- {
- page: params.page,
- 'x-total-pages': 3,
- },
- ];
- });
- createComponent();
- openListbox();
- findListbox().vm.$emit('bottom-reached');
- return waitForPromises();
- });
-
- it('fetches the next page when bottom is reached', async () => {
- expect(mock.history.get).toHaveLength(2);
- expect(mock.history.get[1].params).toStrictEqual({
- page: 2,
- per_page: 20,
- search: '',
- });
- });
-
- it('fetches the first page when the search query changes', async () => {
- search(searchString);
- await waitForPromises();
-
- expect(mock.history.get).toHaveLength(3);
- expect(mock.history.get[2].params).toStrictEqual({
- page: 1,
- per_page: 20,
- search: searchString,
- });
- });
-
- it('retains the search query when infinite scrolling', async () => {
- search(searchString);
- await waitForPromises();
- findListbox().vm.$emit('bottom-reached');
- await waitForPromises();
-
- expect(mock.history.get).toHaveLength(4);
- expect(mock.history.get[3].params).toStrictEqual({
- page: 2,
- per_page: 20,
- search: searchString,
- });
- });
-
- it('pauses infinite scroll after fetching the last page', async () => {
- expect(findListbox().props('infiniteScroll')).toBe(true);
-
- findListbox().vm.$emit('bottom-reached');
- await waitForPromises();
-
- expect(findListbox().props('infiniteScroll')).toBe(false);
- });
-
- it('resumes infinite scroll when search query changes', async () => {
- findListbox().vm.$emit('bottom-reached');
- await waitForPromises();
-
- expect(findListbox().props('infiniteScroll')).toBe(false);
-
- search(searchString);
- await waitForPromises();
-
- expect(findListbox().props('infiniteScroll')).toBe(true);
- });
- });
-
- it.each`
- description | clearable | expectedLabel
- ${'passes'} | ${true} | ${RESET_LABEL}
- ${'does not pass'} | ${false} | ${''}
- `(
- '$description the reset button label to the listbox when clearable is $clearable',
- ({ clearable, expectedLabel }) => {
- createComponent({
- props: {
- clearable,
- },
- });
-
- expect(findListbox().props('resetButtonLabel')).toBe(expectedLabel);
- },
- );
-});
diff --git a/spec/frontend/vue_shared/components/header_ci_component_spec.js b/spec/frontend/vue_shared/components/header_ci_component_spec.js
index 94e1ece8c6b..458f2cc5374 100644
--- a/spec/frontend/vue_shared/components/header_ci_component_spec.js
+++ b/spec/frontend/vue_shared/components/header_ci_component_spec.js
@@ -1,7 +1,7 @@
import { GlButton, GlAvatarLink, GlTooltip } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
-import CiIconBadge from '~/vue_shared/components/ci_badge_link.vue';
+import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
import HeaderCi from '~/vue_shared/components/header_ci_component.vue';
import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
@@ -28,7 +28,7 @@ describe('Header CI Component', () => {
hasSidebarButton: true,
};
- const findIconBadge = () => wrapper.findComponent(CiIconBadge);
+ const findCiBadgeLink = () => wrapper.findComponent(CiBadgeLink);
const findTimeAgo = () => wrapper.findComponent(TimeagoTooltip);
const findUserLink = () => wrapper.findComponent(GlAvatarLink);
const findSidebarToggleBtn = () => wrapper.findComponent(GlButton);
@@ -59,7 +59,7 @@ describe('Header CI Component', () => {
});
it('should render status badge', () => {
- expect(findIconBadge().exists()).toBe(true);
+ expect(findCiBadgeLink().exists()).toBe(true);
});
it('should render timeago date', () => {
diff --git a/spec/frontend/vue_shared/components/incubation/incubation_alert_spec.js b/spec/frontend/vue_shared/components/incubation/incubation_alert_spec.js
new file mode 100644
index 00000000000..1783538beb3
--- /dev/null
+++ b/spec/frontend/vue_shared/components/incubation/incubation_alert_spec.js
@@ -0,0 +1,34 @@
+import { mount } from '@vue/test-utils';
+import { GlAlert, GlButton } from '@gitlab/ui';
+import IncubationAlert from '~/vue_shared/components/incubation/incubation_alert.vue';
+
+describe('IncubationAlert', () => {
+ let wrapper;
+
+ const findAlert = () => wrapper.findComponent(GlAlert);
+
+ const findButton = () => wrapper.findComponent(GlButton);
+
+ beforeEach(() => {
+ wrapper = mount(IncubationAlert, {
+ propsData: {
+ featureName: 'some feature',
+ linkToFeedbackIssue: 'some_link',
+ },
+ });
+ });
+
+ it('displays the feature name in the title', () => {
+ expect(wrapper.html()).toContain('some feature is in incubating phase');
+ });
+
+ it('displays link to issue', () => {
+ expect(findButton().attributes().href).toBe('some_link');
+ });
+
+ it('is removed if dismissed', async () => {
+ await wrapper.find('[aria-label="Dismiss"]').trigger('click');
+
+ expect(findAlert().exists()).toBe(false);
+ });
+});
diff --git a/spec/frontend/vue_shared/components/incubation/pagination_spec.js b/spec/frontend/vue_shared/components/incubation/pagination_spec.js
new file mode 100644
index 00000000000..a621e60c627
--- /dev/null
+++ b/spec/frontend/vue_shared/components/incubation/pagination_spec.js
@@ -0,0 +1,76 @@
+import { GlKeysetPagination } from '@gitlab/ui';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import Pagination from '~/vue_shared/components/incubation/pagination.vue';
+
+describe('~/vue_shared/incubation/components/pagination.vue', () => {
+ let wrapper;
+
+ const pageInfo = {
+ startCursor: 'eyJpZCI6IjE2In0',
+ endCursor: 'eyJpZCI6IjIifQ',
+ hasNextPage: true,
+ hasPreviousPage: true,
+ };
+
+ const findPagination = () => wrapper.findComponent(GlKeysetPagination);
+
+ const createWrapper = (pageInfoProp) => {
+ wrapper = mountExtended(Pagination, {
+ propsData: pageInfoProp,
+ });
+ };
+
+ describe('when neither next nor previous page exists', () => {
+ beforeEach(() => {
+ const emptyPageInfo = { ...pageInfo, hasPreviousPage: false, hasNextPage: false };
+
+ createWrapper(emptyPageInfo);
+ });
+
+ it('should not render pagination component', () => {
+ expect(wrapper.html()).toBe('');
+ });
+ });
+
+ describe('when Pagination is rendered for environment details page', () => {
+ beforeEach(() => {
+ createWrapper(pageInfo);
+ });
+
+ it('should pass correct props to keyset pagination', () => {
+ expect(findPagination().exists()).toBe(true);
+ expect(findPagination().props()).toEqual(expect.objectContaining(pageInfo));
+ });
+
+ describe.each([
+ {
+ testPageInfo: pageInfo,
+ expectedAfter: `cursor=${pageInfo.endCursor}`,
+ expectedBefore: `cursor=${pageInfo.startCursor}`,
+ },
+ {
+ testPageInfo: { ...pageInfo, hasNextPage: true, hasPreviousPage: false },
+ expectedAfter: `cursor=${pageInfo.endCursor}`,
+ expectedBefore: '',
+ },
+ {
+ testPageInfo: { ...pageInfo, hasNextPage: false, hasPreviousPage: true },
+ expectedAfter: '',
+ expectedBefore: `cursor=${pageInfo.startCursor}`,
+ },
+ ])(
+ 'button links generation for $testPageInfo',
+ ({ testPageInfo, expectedAfter, expectedBefore }) => {
+ beforeEach(() => {
+ createWrapper(testPageInfo);
+ });
+
+ it(`should have button links defined as ${expectedAfter || 'empty'} and
+ ${expectedBefore || 'empty'}`, () => {
+ expect(findPagination().props().prevButtonLink).toContain(expectedBefore);
+ expect(findPagination().props().nextButtonLink).toContain(expectedAfter);
+ });
+ },
+ );
+ });
+});
diff --git a/spec/frontend/vue_shared/components/markdown/field_spec.js b/spec/frontend/vue_shared/components/markdown/field_spec.js
index 3b8e78bbadd..68ce07f86b9 100644
--- a/spec/frontend/vue_shared/components/markdown/field_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/field_spec.js
@@ -1,11 +1,13 @@
+import $ from 'jquery';
import { nextTick } from 'vue';
import AxiosMockAdapter from 'axios-mock-adapter';
import { TEST_HOST, FIXTURES_PATH } from 'spec/test_constants';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
import MarkdownFieldHeader from '~/vue_shared/components/markdown/header.vue';
import MarkdownToolbar from '~/vue_shared/components/markdown/toolbar.vue';
-import { mountExtended } from 'helpers/vue_test_utils_helper';
+import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { renderGFM } from '~/behaviors/markdown/render_gfm';
jest.mock('~/behaviors/markdown/render_gfm');
@@ -74,6 +76,22 @@ describe('Markdown field component', () => {
);
}
+ function createWrapper({ autocompleteDataSources = {} } = {}) {
+ subject = shallowMountExtended(MarkdownField, {
+ propsData: {
+ markdownDocsPath,
+ markdownPreviewPath,
+ isSubmitting: false,
+ textareaValue,
+ lines: [],
+ enablePreview: true,
+ restrictedToolBarItems,
+ showContentEditorSwitcher: false,
+ autocompleteDataSources,
+ },
+ });
+ }
+
const getPreviewLink = () => subject.findByTestId('preview-tab');
const getWriteLink = () => subject.findByTestId('write-tab');
const getMarkdownButton = () => subject.find('.js-md');
@@ -84,6 +102,7 @@ describe('Markdown field component', () => {
const findDropzone = () => subject.find('.div-dropzone');
const findMarkdownHeader = () => subject.findComponent(MarkdownFieldHeader);
const findMarkdownToolbar = () => subject.findComponent(MarkdownToolbar);
+ const findGlForm = () => $(subject.vm.$refs['gl-form']).data('glForm');
describe('mounted', () => {
const previewHTML = `
@@ -100,6 +119,18 @@ describe('Markdown field component', () => {
findDropzone().element.addEventListener('click', dropzoneSpy);
});
+ describe('GlForm', () => {
+ beforeEach(() => {
+ createWrapper({ autocompleteDataSources: { commands: '/foobar/-/autocomplete_sources' } });
+ });
+
+ it('initializes GlForm with autocomplete data sources', () => {
+ expect(findGlForm().autoComplete.dataSources).toMatchObject({
+ commands: '/foobar/-/autocomplete_sources',
+ });
+ });
+ });
+
it('renders textarea inside backdrop', () => {
expect(subject.find('.zen-backdrop textarea').element).not.toBeNull();
});
@@ -107,7 +138,7 @@ describe('Markdown field component', () => {
it('renders referenced commands on markdown preview', async () => {
axiosMock
.onPost(markdownPreviewPath)
- .reply(200, { references: { users: [], commands: 'test command' } });
+ .reply(HTTP_STATUS_OK, { references: { users: [], commands: 'test command' } });
previewLink = getPreviewLink();
previewLink.vm.$emit('click', { target: {} });
@@ -121,7 +152,7 @@ describe('Markdown field component', () => {
describe('markdown preview', () => {
beforeEach(() => {
- axiosMock.onPost(markdownPreviewPath).reply(200, { body: previewHTML });
+ axiosMock.onPost(markdownPreviewPath).reply(HTTP_STATUS_OK, { body: previewHTML });
});
it('sets preview link as active', async () => {
@@ -267,7 +298,7 @@ describe('Markdown field component', () => {
const users = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11].map((i) => `user_${i}`);
it('shows warning on mention of all users', async () => {
- axiosMock.onPost(markdownPreviewPath).reply(200, { references: { users } });
+ axiosMock.onPost(markdownPreviewPath).reply(HTTP_STATUS_OK, { references: { users } });
subject.setProps({ textareaValue: 'hello @all' });
@@ -279,7 +310,7 @@ describe('Markdown field component', () => {
});
it('removes warning when all mention is removed', async () => {
- axiosMock.onPost(markdownPreviewPath).reply(200, { references: { users } });
+ axiosMock.onPost(markdownPreviewPath).reply(HTTP_STATUS_OK, { references: { users } });
subject.setProps({ textareaValue: 'hello @all' });
@@ -298,7 +329,7 @@ describe('Markdown field component', () => {
});
it('removes warning when all mention is removed while endpoint is loading', async () => {
- axiosMock.onPost(markdownPreviewPath).reply(200, { references: { users } });
+ axiosMock.onPost(markdownPreviewPath).reply(HTTP_STATUS_OK, { references: { users } });
jest.spyOn(axios, 'post');
subject.setProps({ textareaValue: 'hello @all' });
diff --git a/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js b/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js
index e3df2cde1c1..26b536984ff 100644
--- a/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js
@@ -24,6 +24,7 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
const formFieldName = 'form[markdown_field]';
const formFieldPlaceholder = 'Write some markdown';
const formFieldAriaLabel = 'Edit your content';
+ const autocompleteDataSources = { commands: '/foobar/-/autcomplete_sources' };
let mock;
const buildWrapper = ({ propsData = {}, attachTo, stubs = {} } = {}) => {
@@ -35,11 +36,14 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
markdownDocsPath,
quickActionsDocsPath,
enableAutocomplete,
+ autocompleteDataSources,
enablePreview,
- formFieldId,
- formFieldName,
- formFieldPlaceholder,
- formFieldAriaLabel,
+ formFieldProps: {
+ id: formFieldId,
+ name: formFieldName,
+ placeholder: formFieldPlaceholder,
+ 'aria-label': formFieldAriaLabel,
+ },
...propsData,
},
stubs: {
@@ -66,18 +70,17 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
it('displays markdown field by default', () => {
buildWrapper({ propsData: { supportsQuickActions: true } });
- expect(findMarkdownField().props()).toEqual(
- expect.objectContaining({
- markdownPreviewPath: renderMarkdownPath,
- quickActionsDocsPath,
- canAttachFile: true,
- enableAutocomplete,
- textareaValue: value,
- markdownDocsPath,
- uploadsPath: window.uploads_path,
- enablePreview,
- }),
- );
+ expect(findMarkdownField().props()).toMatchObject({
+ autocompleteDataSources,
+ markdownPreviewPath: renderMarkdownPath,
+ quickActionsDocsPath,
+ canAttachFile: true,
+ enableAutocomplete,
+ textareaValue: value,
+ markdownDocsPath,
+ uploadsPath: window.uploads_path,
+ enablePreview,
+ });
});
it('renders markdown field textarea', () => {
@@ -95,6 +98,12 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
expect(findTextarea().element.value).toBe(value);
});
+ it('fails to render if textarea id and name is not passed', () => {
+ expect(() => {
+ buildWrapper({ propsData: { formFieldProps: {} } });
+ }).toThrow('Invalid prop: custom validator check failed for prop "formFieldProps"');
+ });
+
it(`emits ${EDITING_MODE_CONTENT_EDITOR} event when enableContentEditor emitted from markdown editor`, async () => {
buildWrapper();
diff --git a/spec/frontend/vue_shared/components/markdown_drawer/utils/fetch_spec.js b/spec/frontend/vue_shared/components/markdown_drawer/utils/fetch_spec.js
index adcf57b76a4..c1e61f6e43d 100644
--- a/spec/frontend/vue_shared/components/markdown_drawer/utils/fetch_spec.js
+++ b/spec/frontend/vue_shared/components/markdown_drawer/utils/fetch_spec.js
@@ -4,6 +4,7 @@ import {
splitDocument,
} from '~/vue_shared/components/markdown_drawer/utils/fetch';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import {
MOCK_HTML,
MOCK_DRAWER_DATA,
@@ -20,9 +21,9 @@ describe('utils/fetch', () => {
});
describe.each`
- axiosMock | type | toExpect
- ${{ code: 200, res: MOCK_HTML }} | ${'success'} | ${MOCK_DRAWER_DATA}
- ${{ code: 500, res: null }} | ${'error'} | ${MOCK_DRAWER_DATA_ERROR}
+ axiosMock | type | toExpect
+ ${{ code: HTTP_STATUS_OK, res: MOCK_HTML }} | ${'success'} | ${MOCK_DRAWER_DATA}
+ ${{ code: HTTP_STATUS_INTERNAL_SERVER_ERROR, res: null }} | ${'error'} | ${MOCK_DRAWER_DATA_ERROR}
`('process markdown data', ({ axiosMock, type, toExpect }) => {
describe(`if api fetch responds with ${type}`, () => {
beforeEach(() => {
diff --git a/spec/frontend/vue_shared/components/new_resource_dropdown/mock_data.js b/spec/frontend/vue_shared/components/new_resource_dropdown/mock_data.js
new file mode 100644
index 00000000000..19b1453e8ac
--- /dev/null
+++ b/spec/frontend/vue_shared/components/new_resource_dropdown/mock_data.js
@@ -0,0 +1,54 @@
+export const emptySearchProjectsQueryResponse = {
+ data: {
+ projects: {
+ nodes: [],
+ },
+ },
+};
+
+export const emptySearchProjectsWithinGroupQueryResponse = {
+ data: {
+ group: {
+ id: '1',
+ projects: emptySearchProjectsQueryResponse.data.projects,
+ },
+ },
+};
+
+export const project1 = {
+ id: 'gid://gitlab/Group/26',
+ name: 'Super Mario Project',
+ nameWithNamespace: 'Mushroom Kingdom / Super Mario Project',
+ webUrl: 'https://127.0.0.1:3000/mushroom-kingdom/super-mario-project',
+};
+
+export const project2 = {
+ id: 'gid://gitlab/Group/59',
+ name: 'Mario Kart Project',
+ nameWithNamespace: 'Mushroom Kingdom / Mario Kart Project',
+ webUrl: 'https://127.0.0.1:3000/mushroom-kingdom/mario-kart-project',
+};
+
+export const project3 = {
+ id: 'gid://gitlab/Group/103',
+ name: 'Mario Party Project',
+ nameWithNamespace: 'Mushroom Kingdom / Mario Party Project',
+ webUrl: 'https://127.0.0.1:3000/mushroom-kingdom/mario-party-project',
+};
+
+export const searchProjectsQueryResponse = {
+ data: {
+ projects: {
+ nodes: [project1, project2, project3],
+ },
+ },
+};
+
+export const searchProjectsWithinGroupQueryResponse = {
+ data: {
+ group: {
+ id: '1',
+ projects: searchProjectsQueryResponse.data.projects,
+ },
+ },
+};
diff --git a/spec/frontend/vue_shared/components/new_resource_dropdown/new_resource_dropdown_spec.js b/spec/frontend/vue_shared/components/new_resource_dropdown/new_resource_dropdown_spec.js
new file mode 100644
index 00000000000..31320b1d2a6
--- /dev/null
+++ b/spec/frontend/vue_shared/components/new_resource_dropdown/new_resource_dropdown_spec.js
@@ -0,0 +1,262 @@
+import { GlDropdown, GlDropdownItem, GlSearchBoxByType } from '@gitlab/ui';
+import { mount, shallowMount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import NewResourceDropdown from '~/vue_shared/components/new_resource_dropdown/new_resource_dropdown.vue';
+import searchUserProjectsWithIssuesEnabledQuery from '~/vue_shared/components/new_resource_dropdown/graphql/search_user_projects_with_issues_enabled.query.graphql';
+import { RESOURCE_TYPES } from '~/vue_shared/components/new_resource_dropdown/constants';
+import searchProjectsWithinGroupQuery from '~/issues/list/queries/search_projects.query.graphql';
+import { DASH_SCOPE, joinPaths } from '~/lib/utils/url_utility';
+import { DEBOUNCE_DELAY } from '~/vue_shared/components/filtered_search_bar/constants';
+import { useLocalStorageSpy } from 'helpers/local_storage_helper';
+import {
+ emptySearchProjectsQueryResponse,
+ emptySearchProjectsWithinGroupQueryResponse,
+ project1,
+ project2,
+ project3,
+ searchProjectsQueryResponse,
+ searchProjectsWithinGroupQueryResponse,
+} from './mock_data';
+
+jest.mock('~/flash');
+
+describe('NewResourceDropdown component', () => {
+ useLocalStorageSpy();
+
+ let wrapper;
+
+ Vue.use(VueApollo);
+
+ // Props
+ const withinGroupProps = {
+ query: searchProjectsWithinGroupQuery,
+ queryVariables: { fullPath: 'mushroom-kingdom' },
+ extractProjects: (data) => data.group.projects.nodes,
+ };
+
+ const mountComponent = ({
+ search = '',
+ query = searchUserProjectsWithIssuesEnabledQuery,
+ queryResponse = searchProjectsQueryResponse,
+ mountFn = shallowMount,
+ propsData = {},
+ } = {}) => {
+ const requestHandlers = [[query, jest.fn().mockResolvedValue(queryResponse)]];
+ const apolloProvider = createMockApollo(requestHandlers);
+
+ return mountFn(NewResourceDropdown, {
+ apolloProvider,
+ propsData,
+ data() {
+ return { search };
+ },
+ });
+ };
+
+ const findDropdown = () => wrapper.findComponent(GlDropdown);
+ const findInput = () => wrapper.findComponent(GlSearchBoxByType);
+ const showDropdown = async () => {
+ findDropdown().vm.$emit('shown');
+ await waitForPromises();
+ jest.advanceTimersByTime(DEBOUNCE_DELAY);
+ await waitForPromises();
+ };
+
+ afterEach(() => {
+ localStorage.clear();
+ });
+
+ it('renders a split dropdown', () => {
+ wrapper = mountComponent();
+
+ expect(findDropdown().props('split')).toBe(true);
+ });
+
+ it('renders a label for the dropdown toggle button', () => {
+ wrapper = mountComponent();
+
+ expect(findDropdown().attributes('toggle-text')).toBe(
+ NewResourceDropdown.i18n.toggleButtonLabel,
+ );
+ });
+
+ it('focuses on input when dropdown is shown', async () => {
+ wrapper = mountComponent({ mountFn: mount });
+
+ const inputSpy = jest.spyOn(findInput().vm, 'focusInput');
+
+ await showDropdown();
+
+ expect(inputSpy).toHaveBeenCalledTimes(1);
+ });
+
+ describe.each`
+ description | propsData | query | queryResponse | emptyResponse
+ ${'by default'} | ${undefined} | ${searchUserProjectsWithIssuesEnabledQuery} | ${searchProjectsQueryResponse} | ${emptySearchProjectsQueryResponse}
+ ${'within a group'} | ${withinGroupProps} | ${searchProjectsWithinGroupQuery} | ${searchProjectsWithinGroupQueryResponse} | ${emptySearchProjectsWithinGroupQueryResponse}
+ `('$description', ({ propsData, query, queryResponse, emptyResponse }) => {
+ it('renders projects options', async () => {
+ wrapper = mountComponent({ mountFn: mount, query, queryResponse, propsData });
+ await showDropdown();
+
+ const listItems = wrapper.findAll('li');
+
+ expect(listItems.at(0).text()).toBe(project1.nameWithNamespace);
+ expect(listItems.at(1).text()).toBe(project2.nameWithNamespace);
+ expect(listItems.at(2).text()).toBe(project3.nameWithNamespace);
+ });
+
+ it('renders `No matches found` when there are no matches', async () => {
+ wrapper = mountComponent({
+ search: 'no matches',
+ query,
+ queryResponse: emptyResponse,
+ mountFn: mount,
+ propsData,
+ });
+
+ await showDropdown();
+
+ expect(wrapper.find('li').text()).toBe(NewResourceDropdown.i18n.noMatchesFound);
+ });
+
+ describe.each`
+ resourceType | expectedDefaultLabel | expectedPath | expectedLabel
+ ${'issue'} | ${'Select project to create issue'} | ${'issues/new'} | ${'New issue in'}
+ ${'merge-request'} | ${'Select project to create merge request'} | ${'merge_requests/new'} | ${'New merge request in'}
+ ${'milestone'} | ${'Select project to create milestone'} | ${'milestones/new'} | ${'New milestone in'}
+ `(
+ 'with resource type $resourceType',
+ ({ resourceType, expectedDefaultLabel, expectedPath, expectedLabel }) => {
+ describe('when no project is selected', () => {
+ beforeEach(() => {
+ wrapper = mountComponent({
+ query,
+ queryResponse,
+ propsData: { ...propsData, resourceType },
+ });
+ });
+
+ it('dropdown button is not a link', () => {
+ expect(findDropdown().attributes('split-href')).toBeUndefined();
+ });
+
+ it('displays default text on the dropdown button', () => {
+ expect(findDropdown().props('text')).toBe(expectedDefaultLabel);
+ });
+ });
+
+ describe('when a project is selected', () => {
+ beforeEach(async () => {
+ wrapper = mountComponent({
+ mountFn: mount,
+ query,
+ queryResponse,
+ propsData: { ...propsData, resourceType },
+ });
+ await showDropdown();
+
+ wrapper.findComponent(GlDropdownItem).vm.$emit('click', project1);
+ });
+
+ it('dropdown button is a link', () => {
+ const href = joinPaths(project1.webUrl, DASH_SCOPE, expectedPath);
+
+ expect(findDropdown().attributes('split-href')).toBe(href);
+ });
+
+ it('displays project name on the dropdown button', () => {
+ expect(findDropdown().props('text')).toBe(`${expectedLabel} ${project1.name}`);
+ });
+ });
+ },
+ );
+ });
+
+ describe('without localStorage', () => {
+ beforeEach(() => {
+ wrapper = mountComponent({ mountFn: mount });
+ });
+
+ it('does not attempt to save the selected project to the localStorage', async () => {
+ await showDropdown();
+ wrapper.findComponent(GlDropdownItem).vm.$emit('click', project1);
+
+ expect(localStorage.setItem).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('with localStorage', () => {
+ it('retrieves the selected project from the localStorage', async () => {
+ localStorage.setItem(
+ 'group--new-issue-recent-project',
+ JSON.stringify({
+ webUrl: project1.webUrl,
+ name: project1.name,
+ }),
+ );
+ wrapper = mountComponent({ mountFn: mount, propsData: { withLocalStorage: true } });
+ await nextTick();
+ const dropdown = findDropdown();
+
+ expect(dropdown.attributes('split-href')).toBe(
+ joinPaths(project1.webUrl, DASH_SCOPE, 'issues/new'),
+ );
+ expect(dropdown.props('text')).toBe(`New issue in ${project1.name}`);
+ });
+
+ it('retrieves legacy cache from the localStorage', async () => {
+ localStorage.setItem(
+ 'group--new-issue-recent-project',
+ JSON.stringify({
+ url: `${project1.webUrl}/issues/new`,
+ name: project1.name,
+ }),
+ );
+ wrapper = mountComponent({ mountFn: mount, propsData: { withLocalStorage: true } });
+ await nextTick();
+ const dropdown = findDropdown();
+
+ expect(dropdown.attributes('split-href')).toBe(
+ joinPaths(project1.webUrl, DASH_SCOPE, 'issues/new'),
+ );
+ expect(dropdown.props('text')).toBe(`New issue in ${project1.name}`);
+ });
+
+ describe.each(RESOURCE_TYPES)('with resource type %s', (resourceType) => {
+ it('computes the local storage key without a group', async () => {
+ wrapper = mountComponent({
+ mountFn: mount,
+ propsData: { resourceType, withLocalStorage: true },
+ });
+ await showDropdown();
+ wrapper.findComponent(GlDropdownItem).vm.$emit('click', project1);
+ await nextTick();
+
+ expect(localStorage.setItem).toHaveBeenLastCalledWith(
+ `group--new-${resourceType}-recent-project`,
+ expect.any(String),
+ );
+ });
+
+ it('computes the local storage key with a group', async () => {
+ const groupId = '22';
+ wrapper = mountComponent({
+ mountFn: mount,
+ propsData: { groupId, resourceType, withLocalStorage: true },
+ });
+ await showDropdown();
+ wrapper.findComponent(GlDropdownItem).vm.$emit('click', project1);
+ await nextTick();
+
+ expect(localStorage.setItem).toHaveBeenLastCalledWith(
+ `group-${groupId}-new-${resourceType}-recent-project`,
+ expect.any(String),
+ );
+ });
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/notes/system_note_spec.js b/spec/frontend/vue_shared/components/notes/system_note_spec.js
index 559f9bcb1a8..bcfd7a8ec70 100644
--- a/spec/frontend/vue_shared/components/notes/system_note_spec.js
+++ b/spec/frontend/vue_shared/components/notes/system_note_spec.js
@@ -4,6 +4,7 @@ import waitForPromises from 'helpers/wait_for_promises';
import createStore from '~/notes/stores';
import IssueSystemNote from '~/vue_shared/components/notes/system_note.vue';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { renderGFM } from '~/behaviors/markdown/render_gfm';
jest.mock('~/behaviors/markdown/render_gfm');
@@ -85,7 +86,7 @@ describe('system note component', () => {
it('renders outdated code lines', async () => {
mock
.onGet('/outdated_line_change_path')
- .reply(200, [
+ .reply(HTTP_STATUS_OK, [
{ rich_text: 'console.log', type: 'new', line_code: '123', old_line: null, new_line: 1 },
]);
diff --git a/spec/frontend/vue_shared/components/runner_aws_deployments/runner_aws_deployments_modal_spec.js b/spec/frontend/vue_shared/components/runner_aws_deployments/runner_aws_deployments_modal_spec.js
deleted file mode 100644
index c8ca75787f1..00000000000
--- a/spec/frontend/vue_shared/components/runner_aws_deployments/runner_aws_deployments_modal_spec.js
+++ /dev/null
@@ -1,72 +0,0 @@
-import { GlModal } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import { s__ } from '~/locale';
-import RunnerAwsDeploymentsModal from '~/vue_shared/components/runner_aws_deployments/runner_aws_deployments_modal.vue';
-import RunnerAwsInstructions from '~/vue_shared/components/runner_instructions/instructions/runner_aws_instructions.vue';
-
-jest.mock('~/lib/utils/url_utility', () => ({
- ...jest.requireActual('~/lib/utils/url_utility'),
- visitUrl: jest.fn(),
-}));
-
-const mockModalId = 'runner-aws-deployments-modal';
-
-describe('RunnerAwsDeploymentsModal', () => {
- let wrapper;
-
- const findModal = () => wrapper.findComponent(GlModal);
- const findRunnerAwsInstructions = () => wrapper.findComponent(RunnerAwsInstructions);
-
- const createComponent = (options) => {
- wrapper = shallowMount(RunnerAwsDeploymentsModal, {
- propsData: {
- modalId: mockModalId,
- },
- ...options,
- });
- };
-
- beforeEach(() => {
- createComponent();
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('renders modal', () => {
- expect(findModal().props()).toMatchObject({
- size: 'sm',
- modalId: mockModalId,
- title: s__('Runners|Deploy GitLab Runner in AWS'),
- });
- expect(findModal().attributes()).toMatchObject({
- 'hide-footer': '',
- });
- });
-
- it('renders modal contents', () => {
- expect(findRunnerAwsInstructions().exists()).toBe(true);
- });
-
- it('when contents trigger closing, modal closes', () => {
- const mockClose = jest.fn();
-
- createComponent({
- stubs: {
- GlModal: {
- template: '<div><slot/></div>',
- methods: {
- close: mockClose,
- },
- },
- },
- });
-
- expect(mockClose).toHaveBeenCalledTimes(0);
-
- findRunnerAwsInstructions().vm.$emit('close');
-
- expect(mockClose).toHaveBeenCalledTimes(1);
- });
-});
diff --git a/spec/frontend/vue_shared/components/runner_aws_deployments/runner_aws_deployments_spec.js b/spec/frontend/vue_shared/components/runner_aws_deployments/runner_aws_deployments_spec.js
deleted file mode 100644
index 639668761ea..00000000000
--- a/spec/frontend/vue_shared/components/runner_aws_deployments/runner_aws_deployments_spec.js
+++ /dev/null
@@ -1,41 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import { nextTick } from 'vue';
-import { extendedWrapper } from 'helpers/vue_test_utils_helper';
-import RunnerAwsDeployments from '~/vue_shared/components/runner_aws_deployments/runner_aws_deployments.vue';
-import RunnerAwsDeploymentsModal from '~/vue_shared/components/runner_aws_deployments/runner_aws_deployments_modal.vue';
-
-describe('RunnerAwsDeployments component', () => {
- let wrapper;
-
- const findModalButton = () => wrapper.findByTestId('show-modal-button');
- const findModal = () => wrapper.findComponent(RunnerAwsDeploymentsModal);
-
- const createComponent = () => {
- wrapper = extendedWrapper(shallowMount(RunnerAwsDeployments));
- };
-
- beforeEach(() => {
- createComponent();
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('should show the "Deploy GitLab Runner in AWS" button', () => {
- expect(findModalButton().exists()).toBe(true);
- expect(findModalButton().text()).toBe('Deploy GitLab Runner in AWS');
- });
-
- it('should not render the modal once mounted', () => {
- expect(findModal().exists()).toBe(false);
- });
-
- it('should render the modal once clicked', async () => {
- findModalButton().vm.$emit('click');
-
- await nextTick();
-
- expect(findModal().exists()).toBe(true);
- });
-});
diff --git a/spec/frontend/vue_shared/components/runner_instructions/instructions/runner_aws_instructions_spec.js b/spec/frontend/vue_shared/components/runner_instructions/instructions/runner_aws_instructions_spec.js
index 4d566dbec0c..6d8f895a185 100644
--- a/spec/frontend/vue_shared/components/runner_instructions/instructions/runner_aws_instructions_spec.js
+++ b/spec/frontend/vue_shared/components/runner_instructions/instructions/runner_aws_instructions_spec.js
@@ -16,14 +16,18 @@ import {
AWS_TEMPLATES_BASE_URL,
AWS_EASY_BUTTONS,
} from '~/vue_shared/components/runner_instructions/constants';
-import RunnerAwsInstructions from '~/vue_shared/components/runner_instructions/instructions/runner_aws_instructions.vue';
import { __ } from '~/locale';
+import RunnerAwsInstructions from '~/vue_shared/components/runner_instructions/instructions/runner_aws_instructions.vue';
+import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
+
jest.mock('~/lib/utils/url_utility', () => ({
...jest.requireActual('~/lib/utils/url_utility'),
visitUrl: jest.fn(),
}));
+const mockRegistrationToken = 'MY_TOKEN';
+
describe('RunnerAwsInstructions', () => {
let wrapper;
@@ -31,6 +35,7 @@ describe('RunnerAwsInstructions', () => {
const findEasyButtons = () => wrapper.findAllComponents(GlFormRadio);
const findEasyButtonAt = (i) => findEasyButtons().at(i);
const findLink = () => wrapper.findComponent(GlLink);
+ const findModalCopyButton = () => wrapper.findComponent(ModalCopyButton);
const findOkButton = () =>
wrapper
.findAllComponents(GlButton)
@@ -38,8 +43,12 @@ describe('RunnerAwsInstructions', () => {
.at(0);
const findCloseButton = () => wrapper.findByText(__('Close'));
- const createComponent = () => {
+ const createComponent = ({ props = {} } = {}) => {
wrapper = shallowMountExtended(RunnerAwsInstructions, {
+ propsData: {
+ registrationToken: mockRegistrationToken,
+ ...props,
+ },
stubs: {
GlSprintf,
},
@@ -109,6 +118,22 @@ describe('RunnerAwsInstructions', () => {
expect(findLink().attributes('href')).toBe(AWS_README_URL);
});
+ it('shows registration token and copy button', () => {
+ const token = wrapper.findByText(mockRegistrationToken);
+
+ expect(token.exists()).toBe(true);
+ expect(token.element.tagName).toBe('PRE');
+
+ expect(findModalCopyButton().props('text')).toBe(mockRegistrationToken);
+ });
+
+ it('does not show registration token and copy button when token is not present', () => {
+ createComponent({ props: { registrationToken: null } });
+
+ expect(wrapper.find('pre').exists()).toBe(false);
+ expect(findModalCopyButton().exists()).toBe(false);
+ });
+
it('triggers the modal to close', () => {
findCloseButton().vm.$emit('click');
diff --git a/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js b/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js
index 19f2dd137ff..8f593b6aa1b 100644
--- a/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js
+++ b/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js
@@ -11,6 +11,7 @@ import RunnerInstructionsModal from '~/vue_shared/components/runner_instructions
import RunnerCliInstructions from '~/vue_shared/components/runner_instructions/instructions/runner_cli_instructions.vue';
import RunnerDockerInstructions from '~/vue_shared/components/runner_instructions/instructions/runner_docker_instructions.vue';
import RunnerKubernetesInstructions from '~/vue_shared/components/runner_instructions/instructions/runner_kubernetes_instructions.vue';
+import RunnerAwsInstructions from '~/vue_shared/components/runner_instructions/instructions/runner_aws_instructions.vue';
import { mockRunnerPlatforms } from './mock_data';
@@ -156,6 +157,7 @@ describe('RunnerInstructionsModal component', () => {
platform | component
${'docker'} | ${RunnerDockerInstructions}
${'kubernetes'} | ${RunnerKubernetesInstructions}
+ ${'aws'} | ${RunnerAwsInstructions}
`('with platform "$platform"', ({ platform, component }) => {
beforeEach(async () => {
createComponent({ props: { defaultPlatformName: platform } });
diff --git a/spec/frontend/vue_shared/components/security_reports/__snapshots__/security_summary_spec.js.snap b/spec/frontend/vue_shared/components/security_reports/__snapshots__/security_summary_spec.js.snap
index 1e08394dd56..66d27b5d605 100644
--- a/spec/frontend/vue_shared/components/security_reports/__snapshots__/security_summary_spec.js.snap
+++ b/spec/frontend/vue_shared/components/security_reports/__snapshots__/security_summary_spec.js.snap
@@ -22,7 +22,7 @@ exports[`SecuritySummary component given the message {"countMessage": "%{critica
<span>
<strong
- class="text-danger-600 gl-px-2"
+ class="gl-text-red-600 gl-px-2"
>
1 High
@@ -55,7 +55,7 @@ exports[`SecuritySummary component given the message {"countMessage": "%{critica
>
<span>
<strong
- class="text-danger-800 gl-pl-4"
+ class="gl-text-red-800 gl-pl-4"
>
1 Critical
@@ -98,7 +98,7 @@ exports[`SecuritySummary component given the message {"countMessage": "%{critica
>
<span>
<strong
- class="text-danger-800 gl-pl-4"
+ class="gl-text-red-800 gl-pl-4"
>
1 Critical
@@ -108,7 +108,7 @@ exports[`SecuritySummary component given the message {"countMessage": "%{critica
<span>
<strong
- class="text-danger-600 gl-px-2"
+ class="gl-text-red-600 gl-px-2"
>
2 High
diff --git a/spec/frontend/vue_shared/components/source_viewer/components/__snapshots__/chunk_spec.js.snap b/spec/frontend/vue_shared/components/source_viewer/components/__snapshots__/chunk_spec.js.snap
new file mode 100644
index 00000000000..26c9a6f8d5a
--- /dev/null
+++ b/spec/frontend/vue_shared/components/source_viewer/components/__snapshots__/chunk_spec.js.snap
@@ -0,0 +1,24 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Chunk component rendering isHighlighted is true renders line numbers 1`] = `
+<div
+ class="gl-p-0! gl-z-index-3 diff-line-num gl-border-r gl-display-flex line-links line-numbers"
+ data-testid="line-numbers"
+>
+ <a
+ class="gl-user-select-none gl-shadow-none! file-line-blame"
+ href="some/blame/path.js#L71"
+ />
+
+ <a
+ class="gl-user-select-none gl-shadow-none! file-line-num"
+ data-line-number="71"
+ href="#L71"
+ id="L71"
+ >
+
+ 71
+
+ </a>
+</div>
+`;
diff --git a/spec/frontend/vue_shared/components/source_viewer/components/chunk_deprecated_spec.js b/spec/frontend/vue_shared/components/source_viewer/components/chunk_deprecated_spec.js
new file mode 100644
index 00000000000..da9067a8ddc
--- /dev/null
+++ b/spec/frontend/vue_shared/components/source_viewer/components/chunk_deprecated_spec.js
@@ -0,0 +1,123 @@
+import { nextTick } from 'vue';
+import { GlIntersectionObserver } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import Chunk from '~/vue_shared/components/source_viewer/components/chunk_deprecated.vue';
+import ChunkLine from '~/vue_shared/components/source_viewer/components/chunk_line.vue';
+import LineHighlighter from '~/blob/line_highlighter';
+
+const lineHighlighter = new LineHighlighter();
+jest.mock('~/blob/line_highlighter', () =>
+ jest.fn().mockReturnValue({
+ highlightHash: jest.fn(),
+ }),
+);
+
+const DEFAULT_PROPS = {
+ chunkIndex: 2,
+ isHighlighted: false,
+ content: '// Line 1 content \n // Line 2 content',
+ startingFrom: 140,
+ totalLines: 50,
+ language: 'javascript',
+ blamePath: 'blame/file.js',
+};
+
+const hash = '#L142';
+
+describe('Chunk component', () => {
+ let wrapper;
+ let idleCallbackSpy;
+
+ const createComponent = (props = {}) => {
+ wrapper = shallowMountExtended(Chunk, {
+ mocks: { $route: { hash } },
+ 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(() => {
+ idleCallbackSpy = jest.spyOn(window, 'requestIdleCallback').mockImplementation((fn) => fn());
+ 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 register window.requestIdleCallback if isFirstChunk prop is true, renders lines immediately', () => {
+ jest.clearAllMocks();
+ createComponent({ isFirstChunk: true });
+
+ expect(window.requestIdleCallback).not.toHaveBeenCalled();
+ expect(findContent().exists()).toBe(true);
+ });
+
+ it('does not render a Chunk Line component if isHighlighted is false', () => {
+ expect(findChunkLines().length).toBe(0);
+ });
+
+ it('does not render simplified line numbers and content if browser is not in idle state', () => {
+ idleCallbackSpy.mockRestore();
+ createComponent();
+
+ expect(findLineNumbers()).toHaveLength(0);
+ expect(findContent().exists()).toBe(false);
+ });
+
+ it('renders simplified line numbers and content if isHighlighted is false', () => {
+ expect(findLineNumbers().length).toBe(DEFAULT_PROPS.totalLines);
+
+ expect(findLineNumbers().at(0).attributes('id')).toBe(`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,
+ blamePath: DEFAULT_PROPS.blamePath,
+ });
+ });
+
+ it('does not scroll to route hash if last chunk is not loaded', () => {
+ expect(LineHighlighter).not.toHaveBeenCalled();
+ });
+
+ it('scrolls to route hash if last chunk is loaded', async () => {
+ createComponent({ totalChunks: DEFAULT_PROPS.chunkIndex + 1 });
+ await nextTick();
+ expect(LineHighlighter).toHaveBeenCalledWith({ scrollBehavior: 'auto' });
+ expect(lineHighlighter.highlightHash).toHaveBeenCalledWith(hash);
+ });
+ });
+});
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
index 657bd59dac6..95ef11d776a 100644
--- a/spec/frontend/vue_shared/components/source_viewer/components/chunk_spec.js
+++ b/spec/frontend/vue_shared/components/source_viewer/components/chunk_spec.js
@@ -2,27 +2,7 @@ import { nextTick } from 'vue';
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';
-import LineHighlighter from '~/blob/line_highlighter';
-
-const lineHighlighter = new LineHighlighter();
-jest.mock('~/blob/line_highlighter', () =>
- jest.fn().mockReturnValue({
- highlightHash: jest.fn(),
- }),
-);
-
-const DEFAULT_PROPS = {
- chunkIndex: 2,
- isHighlighted: false,
- content: '// Line 1 content \n // Line 2 content',
- startingFrom: 140,
- totalLines: 50,
- language: 'javascript',
- blamePath: 'blame/file.js',
-};
-
-const hash = '#L142';
+import { CHUNK_1, CHUNK_2 } from '../mock_data';
describe('Chunk component', () => {
let wrapper;
@@ -30,14 +10,13 @@ describe('Chunk component', () => {
const createComponent = (props = {}) => {
wrapper = shallowMountExtended(Chunk, {
- mocks: { $route: { hash } },
- propsData: { ...DEFAULT_PROPS, ...props },
+ propsData: { ...CHUNK_1, ...props },
+ provide: { glFeatures: { fileLineBlame: true } },
});
};
const findIntersectionObserver = () => wrapper.findComponent(GlIntersectionObserver);
- const findChunkLines = () => wrapper.findAllComponents(ChunkLine);
- const findLineNumbers = () => wrapper.findAllByTestId('line-number');
+ const findLineNumbers = () => wrapper.findAllByTestId('line-numbers');
const findContent = () => wrapper.findByTestId('content');
beforeEach(() => {
@@ -52,72 +31,57 @@ describe('Chunk component', () => {
expect(findIntersectionObserver().exists()).toBe(true);
});
- it('emits an appear event when intersection-observer appears', () => {
+ it('renders highlighted content if appear event is emitted', async () => {
+ createComponent({ chunkIndex: 1, isHighlighted: false });
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');
+ await nextTick();
- expect(wrapper.emitted('appear')).toEqual(undefined);
+ expect(findContent().exists()).toBe(true);
});
});
describe('rendering', () => {
- it('does not register window.requestIdleCallback if isFirstChunk prop is true, renders lines immediately', () => {
+ it('does not register window.requestIdleCallback for the first chunk, renders content immediately', () => {
jest.clearAllMocks();
- createComponent({ isFirstChunk: true });
expect(window.requestIdleCallback).not.toHaveBeenCalled();
- expect(findContent().exists()).toBe(true);
- });
-
- it('does not render a Chunk Line component if isHighlighted is false', () => {
- expect(findChunkLines().length).toBe(0);
+ expect(findContent().text()).toBe(CHUNK_1.highlightedContent);
});
- it('does not render simplified line numbers and content if browser is not in idle state', () => {
+ it('does not render content if browser is not in idle state', () => {
idleCallbackSpy.mockRestore();
- createComponent();
+ createComponent({ chunkIndex: 1, ...CHUNK_2 });
expect(findLineNumbers()).toHaveLength(0);
expect(findContent().exists()).toBe(false);
});
- it('renders simplified line numbers and content if isHighlighted is false', () => {
- expect(findLineNumbers().length).toBe(DEFAULT_PROPS.totalLines);
+ describe('isHighlighted is false', () => {
+ beforeEach(() => createComponent(CHUNK_2));
- expect(findLineNumbers().at(0).attributes('id')).toBe(`L${DEFAULT_PROPS.startingFrom + 1}`);
+ it('does not render line numbers', () => {
+ expect(findLineNumbers()).toHaveLength(0);
+ });
- expect(findContent().text()).toBe(DEFAULT_PROPS.content);
+ it('renders raw content', () => {
+ expect(findContent().text()).toBe(CHUNK_2.rawContent);
+ });
});
- it('renders Chunk Line components if isHighlighted is true', () => {
- const splitContent = DEFAULT_PROPS.content.split('\n');
- createComponent({ isHighlighted: true });
+ describe('isHighlighted is true', () => {
+ beforeEach(() => createComponent({ ...CHUNK_2, isHighlighted: true }));
- expect(findChunkLines().length).toBe(splitContent.length);
+ it('renders line numbers', () => {
+ expect(findLineNumbers()).toHaveLength(CHUNK_2.totalLines);
- expect(findChunkLines().at(0).props()).toMatchObject({
- number: DEFAULT_PROPS.startingFrom + 1,
- content: splitContent[0],
- language: DEFAULT_PROPS.language,
- blamePath: DEFAULT_PROPS.blamePath,
+ // Opted for a snapshot test here since the output is simple and verifies native HTML elements
+ expect(findLineNumbers().at(0).element).toMatchSnapshot();
});
- });
- it('does not scroll to route hash if last chunk is not loaded', () => {
- expect(LineHighlighter).not.toHaveBeenCalled();
- });
-
- it('scrolls to route hash if last chunk is loaded', async () => {
- createComponent({ totalChunks: DEFAULT_PROPS.chunkIndex + 1 });
- await nextTick();
- expect(LineHighlighter).toHaveBeenCalledWith({ scrollBehavior: 'auto' });
- expect(lineHighlighter.highlightHash).toHaveBeenCalledWith(hash);
+ it('renders highlighted content', () => {
+ expect(findContent().text()).toBe(CHUNK_2.highlightedContent);
+ });
});
});
});
diff --git a/spec/frontend/vue_shared/components/source_viewer/highlight_util_spec.js b/spec/frontend/vue_shared/components/source_viewer/highlight_util_spec.js
index 4a995e2fde1..d2dd4afe09e 100644
--- a/spec/frontend/vue_shared/components/source_viewer/highlight_util_spec.js
+++ b/spec/frontend/vue_shared/components/source_viewer/highlight_util_spec.js
@@ -1,15 +1,10 @@
-import hljs from 'highlight.js/lib/core';
-import languageLoader from '~/content_editor/services/highlight_js_language_loader';
+import hljs from 'highlight.js';
import { registerPlugins } from '~/vue_shared/components/source_viewer/plugins/index';
import { highlight } from '~/vue_shared/components/source_viewer/workers/highlight_utils';
+import { LINES_PER_CHUNK, NEWLINE } from '~/vue_shared/components/source_viewer/constants';
-jest.mock('highlight.js/lib/core', () => ({
- highlight: jest.fn().mockReturnValue({}),
- registerLanguage: jest.fn(),
-}));
-
-jest.mock('~/content_editor/services/highlight_js_language_loader', () => ({
- javascript: jest.fn().mockReturnValue({ default: jest.fn() }),
+jest.mock('highlight.js', () => ({
+ highlight: jest.fn().mockReturnValue({ value: 'highlighted content' }),
}));
jest.mock('~/vue_shared/components/source_viewer/plugins/index', () => ({
@@ -17,28 +12,61 @@ jest.mock('~/vue_shared/components/source_viewer/plugins/index', () => ({
}));
const fileType = 'text';
-const content = 'function test() { return true };';
+const rawContent = 'function test() { return true }; \n // newline';
+const highlightedContent = 'highlighted content';
const language = 'javascript';
describe('Highlight utility', () => {
- beforeEach(() => highlight(fileType, content, language));
-
- it('loads the language', () => {
- expect(languageLoader.javascript).toHaveBeenCalled();
- });
+ beforeEach(() => highlight(fileType, rawContent, language));
it('registers the plugins', () => {
expect(registerPlugins).toHaveBeenCalled();
});
- it('registers the language', () => {
- expect(hljs.registerLanguage).toHaveBeenCalledWith(
- language,
- languageLoader[language]().default,
+ it('highlights the content', () => {
+ expect(hljs.highlight).toHaveBeenCalledWith(rawContent, { language });
+ });
+
+ it('splits the content into chunks', () => {
+ const contentArray = Array.from({ length: 140 }, () => 'newline'); // simulate 140 lines of code
+
+ const chunks = [
+ {
+ language,
+ highlightedContent,
+ rawContent: contentArray.slice(0, 70).join(NEWLINE), // first 70 lines
+ startingFrom: 0,
+ totalLines: LINES_PER_CHUNK,
+ },
+ {
+ language,
+ highlightedContent: '',
+ rawContent: contentArray.slice(70, 140).join(NEWLINE), // last 70 lines
+ startingFrom: 70,
+ totalLines: LINES_PER_CHUNK,
+ },
+ ];
+
+ expect(highlight(fileType, contentArray.join(NEWLINE), language)).toEqual(
+ expect.arrayContaining(chunks),
);
});
+});
- it('highlights the content', () => {
- expect(hljs.highlight).toHaveBeenCalledWith(content, { language });
+describe('unsupported languages', () => {
+ const unsupportedLanguage = 'some_unsupported_language';
+
+ beforeEach(() => highlight(fileType, rawContent, unsupportedLanguage));
+
+ it('does not register plugins', () => {
+ expect(registerPlugins).not.toHaveBeenCalled();
+ });
+
+ it('does not attempt to highlight the content', () => {
+ expect(hljs.highlight).not.toHaveBeenCalled();
+ });
+
+ it('does not return a result', () => {
+ expect(highlight(fileType, rawContent, unsupportedLanguage)).toBe(undefined);
});
});
diff --git a/spec/frontend/vue_shared/components/source_viewer/mock_data.js b/spec/frontend/vue_shared/components/source_viewer/mock_data.js
new file mode 100644
index 00000000000..f35e9607d5c
--- /dev/null
+++ b/spec/frontend/vue_shared/components/source_viewer/mock_data.js
@@ -0,0 +1,24 @@
+const path = 'some/path.js';
+const blamePath = 'some/blame/path.js';
+
+export const LANGUAGE_MOCK = 'docker';
+
+export const BLOB_DATA_MOCK = { language: LANGUAGE_MOCK, path, blamePath };
+
+export const CHUNK_1 = {
+ isHighlighted: true,
+ rawContent: 'chunk 1 raw',
+ highlightedContent: 'chunk 1 highlighted',
+ totalLines: 70,
+ startingFrom: 0,
+ blamePath,
+};
+
+export const CHUNK_2 = {
+ isHighlighted: false,
+ rawContent: 'chunk 2 raw',
+ highlightedContent: 'chunk 2 highlighted',
+ totalLines: 40,
+ startingFrom: 70,
+ blamePath,
+};
diff --git a/spec/frontend/vue_shared/components/source_viewer/source_viewer_deprecated_spec.js b/spec/frontend/vue_shared/components/source_viewer/source_viewer_deprecated_spec.js
new file mode 100644
index 00000000000..0beec8e9d3e
--- /dev/null
+++ b/spec/frontend/vue_shared/components/source_viewer/source_viewer_deprecated_spec.js
@@ -0,0 +1,177 @@
+import hljs from 'highlight.js/lib/core';
+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_deprecated.vue';
+import { registerPlugins } from '~/vue_shared/components/source_viewer/plugins/index';
+import Chunk from '~/vue_shared/components/source_viewer/components/chunk_deprecated.vue';
+import {
+ EVENT_ACTION,
+ EVENT_LABEL_VIEWER,
+ EVENT_LABEL_FALLBACK,
+ ROUGE_TO_HLJS_LANGUAGE_MAP,
+ LINES_PER_CHUNK,
+} from '~/vue_shared/components/source_viewer/constants';
+import waitForPromises from 'helpers/wait_for_promises';
+import LineHighlighter from '~/blob/line_highlighter';
+import eventHub from '~/notes/event_hub';
+import Tracking from '~/tracking';
+
+jest.mock('~/blob/line_highlighter');
+jest.mock('highlight.js/lib/core');
+jest.mock('~/vue_shared/components/source_viewer/plugins/index');
+Vue.use(VueRouter);
+const router = new VueRouter();
+
+const generateContent = (content, totalLines = 1, delimiter = '\n') => {
+ let generatedContent = '';
+ for (let i = 0; i < totalLines; i += 1) {
+ generatedContent += `Line: ${i + 1} = ${content}${delimiter}`;
+ }
+ return generatedContent;
+};
+
+const execImmediately = (callback) => callback();
+
+describe('Source Viewer component', () => {
+ let wrapper;
+ const language = 'docker';
+ const mappedLanguage = ROUGE_TO_HLJS_LANGUAGE_MAP[language];
+ const chunk1 = generateContent('// Some source code 1', 70);
+ const chunk2 = generateContent('// Some source code 2', 70);
+ const chunk3 = generateContent('// Some source code 3', 70, '\r\n');
+ const chunk3Result = generateContent('// Some source code 3', 70, '\n');
+ const content = chunk1 + chunk2 + chunk3;
+ const path = 'some/path.js';
+ const blamePath = 'some/blame/path.js';
+ const fileType = 'javascript';
+ const DEFAULT_BLOB_DATA = { language, rawTextBlob: content, path, blamePath, fileType };
+ const highlightedContent = `<span data-testid='test-highlighted' id='LC1'>${content}</span><span id='LC2'></span>`;
+
+ const createComponent = async (blob = {}) => {
+ wrapper = shallowMountExtended(SourceViewer, {
+ router,
+ propsData: { blob: { ...DEFAULT_BLOB_DATA, ...blob } },
+ });
+ await waitForPromises();
+ };
+
+ const findChunks = () => wrapper.findAllComponents(Chunk);
+
+ beforeEach(() => {
+ hljs.highlight.mockImplementation(() => ({ value: highlightedContent }));
+ hljs.highlightAuto.mockImplementation(() => ({ value: highlightedContent }));
+ jest.spyOn(window, 'requestIdleCallback').mockImplementation(execImmediately);
+ jest.spyOn(eventHub, '$emit');
+ jest.spyOn(Tracking, 'event');
+
+ return createComponent();
+ });
+
+ afterEach(() => wrapper.destroy());
+
+ describe('event tracking', () => {
+ it('fires a tracking event when the component is created', () => {
+ const eventData = { label: EVENT_LABEL_VIEWER, property: language };
+ expect(Tracking.event).toHaveBeenCalledWith(undefined, EVENT_ACTION, eventData);
+ });
+
+ it('does not emit an error event when the language is supported', () => {
+ expect(wrapper.emitted('error')).toBeUndefined();
+ });
+
+ it('fires a tracking event and emits an error when the language is not supported', () => {
+ const unsupportedLanguage = 'apex';
+ const eventData = { label: EVENT_LABEL_FALLBACK, property: unsupportedLanguage };
+ createComponent({ language: unsupportedLanguage });
+
+ expect(Tracking.event).toHaveBeenCalledWith(undefined, EVENT_ACTION, eventData);
+ expect(wrapper.emitted('error')).toHaveLength(1);
+ });
+ });
+
+ describe('legacy fallbacks', () => {
+ it('tracks a fallback event and emits an error when viewing python files', () => {
+ const fallbackLanguage = 'python';
+ const eventData = { label: EVENT_LABEL_FALLBACK, property: fallbackLanguage };
+ createComponent({ language: fallbackLanguage });
+
+ expect(Tracking.event).toHaveBeenCalledWith(undefined, EVENT_ACTION, eventData);
+ expect(wrapper.emitted('error')).toHaveLength(1);
+ });
+ });
+
+ describe('highlight.js', () => {
+ beforeEach(() => createComponent({ language: mappedLanguage }));
+
+ it('registers our plugins for Highlight.js', () => {
+ expect(registerPlugins).toHaveBeenCalledWith(hljs, fileType, content);
+ });
+
+ it('registers the language definition', async () => {
+ const languageDefinition = await import(`highlight.js/lib/languages/${mappedLanguage}`);
+
+ expect(hljs.registerLanguage).toHaveBeenCalledWith(
+ mappedLanguage,
+ languageDefinition.default,
+ );
+ });
+
+ it('registers json language definition if fileType is package_json', async () => {
+ await createComponent({ language: 'json', fileType: 'package_json' });
+ const languageDefinition = await import(`highlight.js/lib/languages/json`);
+
+ expect(hljs.registerLanguage).toHaveBeenCalledWith('json', languageDefinition.default);
+ });
+
+ it('correctly maps languages starting with uppercase', async () => {
+ await createComponent({ language: 'Ruby' });
+ const languageDefinition = await import(`highlight.js/lib/languages/ruby`);
+
+ expect(hljs.registerLanguage).toHaveBeenCalledWith('ruby', languageDefinition.default);
+ });
+
+ it('highlights the first chunk', () => {
+ expect(hljs.highlight).toHaveBeenCalledWith(chunk1.trim(), { language: mappedLanguage });
+ expect(findChunks().at(0).props('isFirstChunk')).toBe(true);
+ });
+
+ 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(chunk1.trim());
+ });
+ });
+ });
+
+ describe('rendering', () => {
+ it.each`
+ chunkIndex | chunkContent | totalChunks
+ ${0} | ${chunk1} | ${0}
+ ${1} | ${chunk2} | ${3}
+ ${2} | ${chunk3Result} | ${3}
+ `('renders chunk $chunkIndex', ({ chunkIndex, chunkContent, totalChunks }) => {
+ const chunk = findChunks().at(chunkIndex);
+
+ expect(chunk.props('content')).toContain(chunkContent.trim());
+
+ expect(chunk.props()).toMatchObject({
+ totalLines: LINES_PER_CHUNK,
+ startingFrom: LINES_PER_CHUNK * chunkIndex,
+ totalChunks,
+ });
+ });
+
+ it('emits showBlobInteractionZones on the eventHub when chunk appears', () => {
+ findChunks().at(0).vm.$emit('appear');
+ expect(eventHub.$emit).toHaveBeenCalledWith('showBlobInteractionZones', path);
+ });
+ });
+
+ describe('LineHighlighter', () => {
+ it('instantiates the lineHighlighter class', async () => {
+ expect(LineHighlighter).toHaveBeenCalledWith({ scrollBehavior: 'auto' });
+ });
+ });
+});
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 5461d38599d..1c75442b4a8 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,70 +1,27 @@
-import hljs from 'highlight.js/lib/core';
-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 { registerPlugins } from '~/vue_shared/components/source_viewer/plugins/index';
import Chunk from '~/vue_shared/components/source_viewer/components/chunk.vue';
-import {
- EVENT_ACTION,
- EVENT_LABEL_VIEWER,
- EVENT_LABEL_FALLBACK,
- ROUGE_TO_HLJS_LANGUAGE_MAP,
- LINES_PER_CHUNK,
-} from '~/vue_shared/components/source_viewer/constants';
-import waitForPromises from 'helpers/wait_for_promises';
-import LineHighlighter from '~/blob/line_highlighter';
-import eventHub from '~/notes/event_hub';
+import { EVENT_ACTION, EVENT_LABEL_VIEWER } from '~/vue_shared/components/source_viewer/constants';
import Tracking from '~/tracking';
+import addBlobLinksTracking from '~/blob/blob_links_tracking';
+import { BLOB_DATA_MOCK, CHUNK_1, CHUNK_2, LANGUAGE_MOCK } from './mock_data';
-jest.mock('~/blob/line_highlighter');
-jest.mock('highlight.js/lib/core');
-jest.mock('~/vue_shared/components/source_viewer/plugins/index');
-Vue.use(VueRouter);
-const router = new VueRouter();
-
-const generateContent = (content, totalLines = 1, delimiter = '\n') => {
- let generatedContent = '';
- for (let i = 0; i < totalLines; i += 1) {
- generatedContent += `Line: ${i + 1} = ${content}${delimiter}`;
- }
- return generatedContent;
-};
-
-const execImmediately = (callback) => callback();
+jest.mock('~/blob/blob_links_tracking');
describe('Source Viewer component', () => {
let wrapper;
- const language = 'docker';
- const mappedLanguage = ROUGE_TO_HLJS_LANGUAGE_MAP[language];
- const chunk1 = generateContent('// Some source code 1', 70);
- const chunk2 = generateContent('// Some source code 2', 70);
- const chunk3 = generateContent('// Some source code 3', 70, '\r\n');
- const chunk3Result = generateContent('// Some source code 3', 70, '\n');
- const content = chunk1 + chunk2 + chunk3;
- const path = 'some/path.js';
- const blamePath = 'some/blame/path.js';
- const fileType = 'javascript';
- const DEFAULT_BLOB_DATA = { language, rawTextBlob: content, path, blamePath, fileType };
- const highlightedContent = `<span data-testid='test-highlighted' id='LC1'>${content}</span><span id='LC2'></span>`;
+ const CHUNKS_MOCK = [CHUNK_1, CHUNK_2];
- const createComponent = async (blob = {}) => {
+ const createComponent = () => {
wrapper = shallowMountExtended(SourceViewer, {
- router,
- propsData: { blob: { ...DEFAULT_BLOB_DATA, ...blob } },
+ propsData: { blob: BLOB_DATA_MOCK, chunks: CHUNKS_MOCK },
});
- await waitForPromises();
};
const findChunks = () => wrapper.findAllComponents(Chunk);
beforeEach(() => {
- hljs.highlight.mockImplementation(() => ({ value: highlightedContent }));
- hljs.highlightAuto.mockImplementation(() => ({ value: highlightedContent }));
- jest.spyOn(window, 'requestIdleCallback').mockImplementation(execImmediately);
- jest.spyOn(eventHub, '$emit');
jest.spyOn(Tracking, 'event');
-
return createComponent();
});
@@ -72,106 +29,19 @@ describe('Source Viewer component', () => {
describe('event tracking', () => {
it('fires a tracking event when the component is created', () => {
- const eventData = { label: EVENT_LABEL_VIEWER, property: language };
+ const eventData = { label: EVENT_LABEL_VIEWER, property: LANGUAGE_MOCK };
expect(Tracking.event).toHaveBeenCalledWith(undefined, EVENT_ACTION, eventData);
});
- it('does not emit an error event when the language is supported', () => {
- expect(wrapper.emitted('error')).toBeUndefined();
- });
-
- it('fires a tracking event and emits an error when the language is not supported', () => {
- const unsupportedLanguage = 'apex';
- const eventData = { label: EVENT_LABEL_FALLBACK, property: unsupportedLanguage };
- createComponent({ language: unsupportedLanguage });
-
- expect(Tracking.event).toHaveBeenCalledWith(undefined, EVENT_ACTION, eventData);
- expect(wrapper.emitted('error')).toHaveLength(1);
- });
- });
-
- describe('legacy fallbacks', () => {
- it('tracks a fallback event and emits an error when viewing python files', () => {
- const fallbackLanguage = 'python';
- const eventData = { label: EVENT_LABEL_FALLBACK, property: fallbackLanguage };
- createComponent({ language: fallbackLanguage });
-
- expect(Tracking.event).toHaveBeenCalledWith(undefined, EVENT_ACTION, eventData);
- expect(wrapper.emitted('error')).toHaveLength(1);
- });
- });
-
- describe('highlight.js', () => {
- beforeEach(() => createComponent({ language: mappedLanguage }));
-
- it('registers our plugins for Highlight.js', () => {
- expect(registerPlugins).toHaveBeenCalledWith(hljs, fileType, content);
- });
-
- it('registers the language definition', async () => {
- const languageDefinition = await import(`highlight.js/lib/languages/${mappedLanguage}`);
-
- expect(hljs.registerLanguage).toHaveBeenCalledWith(
- mappedLanguage,
- languageDefinition.default,
- );
- });
-
- it('registers json language definition if fileType is package_json', async () => {
- await createComponent({ language: 'json', fileType: 'package_json' });
- const languageDefinition = await import(`highlight.js/lib/languages/json`);
-
- expect(hljs.registerLanguage).toHaveBeenCalledWith('json', languageDefinition.default);
- });
-
- it('correctly maps languages starting with uppercase', async () => {
- await createComponent({ language: 'Ruby' });
- const languageDefinition = await import(`highlight.js/lib/languages/ruby`);
-
- expect(hljs.registerLanguage).toHaveBeenCalledWith('ruby', languageDefinition.default);
- });
-
- it('highlights the first chunk', () => {
- expect(hljs.highlight).toHaveBeenCalledWith(chunk1.trim(), { language: mappedLanguage });
- expect(findChunks().at(0).props('isFirstChunk')).toBe(true);
- });
-
- 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(chunk1.trim());
- });
+ it('adds blob links tracking', () => {
+ expect(addBlobLinksTracking).toHaveBeenCalled();
});
});
describe('rendering', () => {
- it.each`
- chunkIndex | chunkContent | totalChunks
- ${0} | ${chunk1} | ${0}
- ${1} | ${chunk2} | ${3}
- ${2} | ${chunk3Result} | ${3}
- `('renders chunk $chunkIndex', ({ chunkIndex, chunkContent, totalChunks }) => {
- const chunk = findChunks().at(chunkIndex);
-
- expect(chunk.props('content')).toContain(chunkContent.trim());
-
- expect(chunk.props()).toMatchObject({
- totalLines: LINES_PER_CHUNK,
- startingFrom: LINES_PER_CHUNK * chunkIndex,
- totalChunks,
- });
- });
-
- it('emits showBlobInteractionZones on the eventHub when chunk appears', () => {
- findChunks().at(0).vm.$emit('appear');
- expect(eventHub.$emit).toHaveBeenCalledWith('showBlobInteractionZones', path);
- });
- });
-
- describe('LineHighlighter', () => {
- it('instantiates the lineHighlighter class', async () => {
- expect(LineHighlighter).toHaveBeenCalledWith({ scrollBehavior: 'auto' });
+ it('renders a Chunk component for each chunk', () => {
+ expect(findChunks().at(0).props()).toMatchObject(CHUNK_1);
+ expect(findChunks().at(1).props()).toMatchObject(CHUNK_2);
});
});
});
diff --git a/spec/frontend/vue_shared/components/url_sync_spec.js b/spec/frontend/vue_shared/components/url_sync_spec.js
index acda1a64a75..30a7439579f 100644
--- a/spec/frontend/vue_shared/components/url_sync_spec.js
+++ b/spec/frontend/vue_shared/components/url_sync_spec.js
@@ -1,7 +1,10 @@
import { shallowMount } from '@vue/test-utils';
-import { historyPushState } from '~/lib/utils/common_utils';
+import { historyPushState, historyReplaceState } from '~/lib/utils/common_utils';
import { mergeUrlParams, setUrlParams } from '~/lib/utils/url_utility';
-import UrlSyncComponent, { URL_SET_PARAMS_STRATEGY } from '~/vue_shared/components/url_sync.vue';
+import UrlSyncComponent, {
+ URL_SET_PARAMS_STRATEGY,
+ HISTORY_REPLACE_UPDATE_METHOD,
+} from '~/vue_shared/components/url_sync.vue';
jest.mock('~/lib/utils/url_utility', () => ({
mergeUrlParams: jest.fn((query, url) => `urlParams: ${JSON.stringify(query)} ${url}`),
@@ -10,6 +13,7 @@ jest.mock('~/lib/utils/url_utility', () => ({
jest.mock('~/lib/utils/common_utils', () => ({
historyPushState: jest.fn(),
+ historyReplaceState: jest.fn(),
}));
describe('url sync component', () => {
@@ -18,14 +22,12 @@ describe('url sync component', () => {
const findButton = () => wrapper.find('button');
- const createComponent = ({
- query = mockQuery,
- scopedSlots,
- slots,
- urlParamsUpdateStrategy,
- } = {}) => {
+ const createComponent = ({ props = {}, scopedSlots, slots } = {}) => {
wrapper = shallowMount(UrlSyncComponent, {
- propsData: { query, ...(urlParamsUpdateStrategy && { urlParamsUpdateStrategy }) },
+ propsData: {
+ query: mockQuery,
+ ...props,
+ },
scopedSlots,
slots,
});
@@ -35,32 +37,27 @@ describe('url sync component', () => {
wrapper.destroy();
});
- const expectUrlSyncFactory = (
+ const expectUrlSyncWithMergeUrlParams = (
query,
times,
- urlParamsUpdateStrategy,
- urlOptions,
- urlReturnValue,
+ mergeUrlParamsReturnValue,
+ historyMethod = historyPushState,
) => {
- expect(urlParamsUpdateStrategy).toHaveBeenCalledTimes(times);
- expect(urlParamsUpdateStrategy).toHaveBeenCalledWith(query, window.location.href, urlOptions);
-
- expect(historyPushState).toHaveBeenCalledTimes(times);
- expect(historyPushState).toHaveBeenCalledWith(urlReturnValue);
- };
+ expect(mergeUrlParams).toHaveBeenCalledTimes(times);
+ expect(mergeUrlParams).toHaveBeenCalledWith(query, window.location.href, {
+ spreadArrays: true,
+ });
- const expectUrlSyncWithMergeUrlParams = (query, times, mergeUrlParamsReturnValue) => {
- expectUrlSyncFactory(
- query,
- times,
- mergeUrlParams,
- { spreadArrays: true },
- mergeUrlParamsReturnValue,
- );
+ expect(historyMethod).toHaveBeenCalledTimes(times);
+ expect(historyMethod).toHaveBeenCalledWith(mergeUrlParamsReturnValue);
};
const expectUrlSyncWithSetUrlParams = (query, times, setUrlParamsReturnValue) => {
- expectUrlSyncFactory(query, times, setUrlParams, true, setUrlParamsReturnValue);
+ expect(setUrlParams).toHaveBeenCalledTimes(times);
+ expect(setUrlParams).toHaveBeenCalledWith(query, window.location.href, true, true, true);
+
+ expect(historyPushState).toHaveBeenCalledTimes(times);
+ expect(historyPushState).toHaveBeenCalledWith(setUrlParamsReturnValue);
};
describe('with query as a props', () => {
@@ -86,13 +83,32 @@ describe('url sync component', () => {
describe('with url-params-update-strategy equals to URL_SET_PARAMS_STRATEGY', () => {
it('uses setUrlParams to generate URL', () => {
createComponent({
- urlParamsUpdateStrategy: URL_SET_PARAMS_STRATEGY,
+ props: {
+ urlParamsUpdateStrategy: URL_SET_PARAMS_STRATEGY,
+ },
});
expectUrlSyncWithSetUrlParams(mockQuery, 1, setUrlParams.mock.results[0].value);
});
});
+ describe('with history-update-method equals to HISTORY_REPLACE_UPDATE_METHOD', () => {
+ it('uses historyReplaceState to update the URL', () => {
+ createComponent({
+ props: {
+ historyUpdateMethod: HISTORY_REPLACE_UPDATE_METHOD,
+ },
+ });
+
+ expectUrlSyncWithMergeUrlParams(
+ mockQuery,
+ 1,
+ mergeUrlParams.mock.results[0].value,
+ historyReplaceState,
+ );
+ });
+ });
+
describe('with scoped slot', () => {
const scopedSlots = {
default: `
@@ -101,13 +117,13 @@ describe('url sync component', () => {
};
it('renders the scoped slot', () => {
- createComponent({ query: null, scopedSlots });
+ createComponent({ props: { query: null }, scopedSlots });
expect(findButton().exists()).toBe(true);
});
it('syncs the url with the scoped slots function', () => {
- createComponent({ query: null, scopedSlots });
+ createComponent({ props: { query: null }, scopedSlots });
findButton().trigger('click');
@@ -121,7 +137,7 @@ describe('url sync component', () => {
};
it('renders the default slot', () => {
- createComponent({ query: null, slots });
+ createComponent({ props: { query: null }, slots });
expect(findButton().exists()).toBe(true);
});
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 1ad6d043399..63371b1492b 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
@@ -4,6 +4,7 @@ import { nextTick } from 'vue';
import { TEST_HOST } from 'spec/test_constants';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import UserAvatarList from '~/vue_shared/components/user_avatar/user_avatar_list.vue';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
const TEST_IMAGE_SIZE = 7;
const TEST_BREAKPOINT = 5;
@@ -16,10 +17,13 @@ const createUser = (id) => ({
web_url: `${TEST_HOST}/${id}`,
avatar_url: `${TEST_HOST}/${id}/avatar`,
});
+
const createList = (n) =>
Array(n)
.fill(1)
.map((x, id) => createUser(id));
+const createListCamelCase = (n) =>
+ createList(n).map((user) => convertObjectPropsToCamelCase(user, { deep: true }));
describe('UserAvatarList', () => {
let props;
@@ -75,14 +79,14 @@ describe('UserAvatarList', () => {
props.breakpoint = 0;
});
- it('renders avatars', () => {
+ const linkProps = () =>
+ wrapper.findAllComponents(UserAvatarLink).wrappers.map((x) => x.props());
+
+ it('renders avatars when user has snake_case attributes', () => {
const items = createList(20);
factory({ propsData: { items } });
- const links = wrapper.findAllComponents(UserAvatarLink);
- const linkProps = links.wrappers.map((x) => x.props());
-
- expect(linkProps).toEqual(
+ expect(linkProps()).toEqual(
items.map((x) =>
expect.objectContaining({
linkHref: x.web_url,
@@ -94,6 +98,23 @@ describe('UserAvatarList', () => {
),
);
});
+
+ it('renders avatars when user has camelCase attributes', () => {
+ const items = createListCamelCase(20);
+ factory({ propsData: { items } });
+
+ expect(linkProps()).toEqual(
+ items.map((x) =>
+ expect.objectContaining({
+ linkHref: x.webUrl,
+ imgSrc: x.avatarUrl,
+ imgAlt: x.name,
+ tooltipText: x.name,
+ imgSize: TEST_IMAGE_SIZE,
+ }),
+ ),
+ );
+ });
});
describe('with breakpoint and length equal to breakpoint', () => {
diff --git a/spec/frontend/vue_shared/components/user_select_spec.js b/spec/frontend/vue_shared/components/user_select_spec.js
index 874796f653a..b0e9584a15b 100644
--- a/spec/frontend/vue_shared/components/user_select_spec.js
+++ b/spec/frontend/vue_shared/components/user_select_spec.js
@@ -285,6 +285,20 @@ describe('User select dropdown', () => {
expect(wrapper.emitted('input')).toEqual([[[]]]);
});
+ it('hides the dropdown after clicking on `Unassigned`', async () => {
+ createComponent({
+ props: {
+ value: [assignee],
+ },
+ });
+ wrapper.vm.$refs.dropdown.hide = jest.fn();
+ await waitForPromises();
+
+ findUnassignLink().trigger('click');
+
+ expect(wrapper.vm.$refs.dropdown.hide).toHaveBeenCalledTimes(1);
+ });
+
it('emits an empty array after unselecting the only selected assignee', async () => {
createComponent({
props: {
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 2a0d2089fe3..18afe049149 100644
--- a/spec/frontend/vue_shared/components/web_ide_link_spec.js
+++ b/spec/frontend/vue_shared/components/web_ide_link_spec.js
@@ -1,4 +1,4 @@
-import { GlButton, GlModal, GlPopover } from '@gitlab/ui';
+import { GlButton, GlLink, GlModal, GlPopover } from '@gitlab/ui';
import { nextTick } from 'vue';
import ActionsButton from '~/vue_shared/components/actions_button.vue';
@@ -147,6 +147,11 @@ describe('Web IDE link component', () => {
const findForkConfirmModal = () => wrapper.findComponent(ConfirmForkModal);
const findUserCalloutDismisser = () => wrapper.findComponent(UserCalloutDismisser);
const findNewWebIdeCalloutPopover = () => wrapper.findComponent(GlPopover);
+ const findTryItOutLink = () =>
+ wrapper
+ .findAllComponents(GlLink)
+ .filter((link) => link.text().includes('Try it out'))
+ .at(0);
it.each([
{
@@ -516,6 +521,12 @@ describe('Web IDE link component', () => {
expect(dismiss).toHaveBeenCalled();
});
+ it('dismisses the callout when try it now link is clicked', () => {
+ findTryItOutLink().vm.$emit('click');
+
+ expect(dismiss).toHaveBeenCalled();
+ });
+
it('dismisses the callout when action button is clicked', () => {
findActionsButton().vm.$emit('actionClicked');