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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'spec/frontend/vue_shared/components')
-rw-r--r--spec/frontend/vue_shared/components/file_row_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/gitlab_version_check_spec.js135
-rw-r--r--spec/frontend/vue_shared/components/group_select/group_select_spec.js202
-rw-r--r--spec/frontend/vue_shared/components/help_popover_spec.js14
-rw-r--r--spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js29
-rw-r--r--spec/frontend/vue_shared/components/markdown_drawer/markdown_drawer_spec.js205
-rw-r--r--spec/frontend/vue_shared/components/markdown_drawer/mock_data.js42
-rw-r--r--spec/frontend/vue_shared/components/markdown_drawer/utils/fetch_spec.js43
-rw-r--r--spec/frontend/vue_shared/components/namespace_select/mock_data.js6
-rw-r--r--spec/frontend/vue_shared/components/namespace_select/namespace_select_deprecated_spec.js236
-rw-r--r--spec/frontend/vue_shared/components/sidebar/issuable_move_dropdown_spec.js24
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js13
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/components/chunk_spec.js43
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/plugins/link_dependencies_spec.js38
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/plugins/mock_data.js30
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/plugins/utils/composer_json_linker_spec.js38
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/plugins/utils/dependency_linker_util_spec.js14
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/plugins/utils/gemfile_linker_spec.js13
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/plugins/utils/gemspec_linker_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/plugins/utils/godeps_json_linker_spec.js27
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/plugins/utils/package_json_linker_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/plugins/utils/podspec_json_linker_spec.js14
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/plugins/wrap_child_nodes_spec.js3
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js54
24 files changed, 764 insertions, 465 deletions
diff --git a/spec/frontend/vue_shared/components/file_row_spec.js b/spec/frontend/vue_shared/components/file_row_spec.js
index f5a545891d5..c3a71d7fda3 100644
--- a/spec/frontend/vue_shared/components/file_row_spec.js
+++ b/spec/frontend/vue_shared/components/file_row_spec.js
@@ -106,7 +106,7 @@ describe('File row component', () => {
level: 2,
});
- expect(wrapper.find('.file-row-name').element.style.marginLeft).toBe('32px');
+ expect(wrapper.find('.file-row-name').element.style.marginLeft).toBe('16px');
});
it('renders header for file', () => {
diff --git a/spec/frontend/vue_shared/components/gitlab_version_check_spec.js b/spec/frontend/vue_shared/components/gitlab_version_check_spec.js
deleted file mode 100644
index 38f28837cc1..00000000000
--- a/spec/frontend/vue_shared/components/gitlab_version_check_spec.js
+++ /dev/null
@@ -1,135 +0,0 @@
-import { GlBadge } from '@gitlab/ui';
-import MockAdapter from 'axios-mock-adapter';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import waitForPromises from 'helpers/wait_for_promises';
-import { mockTracking } from 'helpers/tracking_helper';
-import { helpPagePath } from '~/helpers/help_page_helper';
-import axios from '~/lib/utils/axios_utils';
-import GitlabVersionCheck from '~/vue_shared/components/gitlab_version_check.vue';
-
-describe('GitlabVersionCheck', () => {
- let wrapper;
- let mock;
-
- const UPGRADE_DOCS_URL = helpPagePath('update/index');
-
- const defaultResponse = {
- code: 200,
- res: { severity: 'success' },
- };
-
- const createComponent = (mockResponse) => {
- const response = {
- ...defaultResponse,
- ...mockResponse,
- };
-
- mock = new MockAdapter(axios);
- mock.onGet().replyOnce(response.code, response.res);
-
- wrapper = shallowMountExtended(GitlabVersionCheck);
- };
-
- const dummyGon = {
- relative_url_root: '/',
- };
-
- let originalGon;
-
- afterEach(() => {
- wrapper.destroy();
- mock.restore();
- window.gon = originalGon;
- });
-
- const findGlBadgeClickWrapper = () => wrapper.findByTestId('badge-click-wrapper');
- const findGlBadge = () => wrapper.findComponent(GlBadge);
-
- describe.each`
- root | description
- ${'/'} | ${'not used (uses its own (sub)domain)'}
- ${'/gitlab'} | ${'custom path'}
- ${'/service/gitlab'} | ${'custom path with 2 depth'}
- `('path for version_check.json', ({ root, description }) => {
- describe(`when relative url is ${description}: ${root}`, () => {
- beforeEach(async () => {
- originalGon = window.gon;
- window.gon = { ...dummyGon };
- window.gon.relative_url_root = root;
- createComponent(defaultResponse);
- await waitForPromises(); // Ensure we wrap up the axios call
- });
-
- it('reflects the relative url setting', () => {
- expect(mock.history.get.length).toBe(1);
-
- const pathRegex = new RegExp(`^${root}`);
- expect(mock.history.get[0].url).toMatch(pathRegex);
- });
- });
- });
-
- describe('template', () => {
- describe.each`
- description | mockResponse | renders
- ${'successful but null'} | ${{ code: 200, res: null }} | ${false}
- ${'successful and valid'} | ${{ code: 200, res: { severity: 'success' } }} | ${true}
- ${'an error'} | ${{ code: 500, res: null }} | ${false}
- `('version_check.json response', ({ description, mockResponse, renders }) => {
- describe(`is ${description}`, () => {
- beforeEach(async () => {
- createComponent(mockResponse);
- await waitForPromises(); // Ensure we wrap up the axios call
- });
-
- it(`does${renders ? '' : ' not'} render Badge Click Wrapper and GlBadge`, () => {
- expect(findGlBadgeClickWrapper().exists()).toBe(renders);
- expect(findGlBadge().exists()).toBe(renders);
- });
- });
- });
-
- describe.each`
- mockResponse | expectedUI
- ${{ code: 200, res: { severity: 'success' } }} | ${{ title: 'Up to date', variant: 'success' }}
- ${{ code: 200, res: { severity: 'warning' } }} | ${{ title: 'Update available', variant: 'warning' }}
- ${{ code: 200, res: { severity: 'danger' } }} | ${{ title: 'Update ASAP', variant: 'danger' }}
- `('badge ui', ({ mockResponse, expectedUI }) => {
- describe(`when response is ${mockResponse.res.severity}`, () => {
- let trackingSpy;
-
- beforeEach(async () => {
- createComponent(mockResponse);
- trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
- await waitForPromises(); // Ensure we wrap up the axios call
- });
-
- it(`title is ${expectedUI.title}`, () => {
- expect(findGlBadge().text()).toBe(expectedUI.title);
- });
-
- it(`variant is ${expectedUI.variant}`, () => {
- expect(findGlBadge().attributes('variant')).toBe(expectedUI.variant);
- });
-
- it(`tracks rendered_version_badge with label ${expectedUI.title}`, () => {
- expect(trackingSpy).toHaveBeenCalledWith(undefined, 'rendered_version_badge', {
- label: expectedUI.title,
- });
- });
-
- it(`link is ${UPGRADE_DOCS_URL}`, () => {
- expect(findGlBadge().attributes('href')).toBe(UPGRADE_DOCS_URL);
- });
-
- it(`tracks click_version_badge with label ${expectedUI.title} when badge is clicked`, async () => {
- await findGlBadgeClickWrapper().trigger('click');
-
- expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_version_badge', {
- label: expectedUI.title,
- });
- });
- });
- });
- });
-});
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
new file mode 100644
index 00000000000..f959d2225fa
--- /dev/null
+++ b/spec/frontend/vue_shared/components/group_select/group_select_spec.js
@@ -0,0 +1,202 @@
+import { nextTick } from 'vue';
+import { GlListbox } 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 { createAlert } from '~/flash';
+import GroupSelect from '~/vue_shared/components/group_select/group_select.vue';
+import {
+ TOGGLE_TEXT,
+ FETCH_GROUPS_ERROR,
+ FETCH_GROUP_ERROR,
+ QUERY_TOO_SHORT_MESSAGE,
+} from '~/vue_shared/components/group_select/constants';
+import waitForPromises from 'helpers/wait_for_promises';
+
+jest.mock('~/flash');
+
+describe('GroupSelect', () => {
+ let wrapper;
+ let mock;
+
+ // Mocks
+ const groupMock = {
+ full_name: 'selectedGroup',
+ id: '1',
+ };
+ const groupEndpoint = `/api/undefined/groups/${groupMock.id}`;
+
+ // Props
+ const inputName = 'inputName';
+ const inputId = 'inputId';
+
+ // Finders
+ const findListbox = () => wrapper.findComponent(GlListbox);
+ const findInput = () => wrapper.findByTestId('input');
+
+ // Helpers
+ const createComponent = ({ props = {} } = {}) => {
+ wrapper = shallowMountExtended(GroupSelect, {
+ propsData: {
+ inputName,
+ inputId,
+ ...props,
+ },
+ });
+ };
+ 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();
+ });
+
+ 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 } });
+ await waitForPromises();
+
+ expect(createAlert).toHaveBeenCalledWith({
+ message: FETCH_GROUP_ERROR,
+ error: expect.any(Error),
+ parent: wrapper.vm.$el,
+ });
+ });
+ });
+ });
+
+ it('shows an error when fetching groups fails', async () => {
+ mock.onGet('/api/undefined/groups.json').reply(500);
+ createComponent();
+ openListbox();
+ await waitForPromises();
+
+ expect(createAlert).toHaveBeenCalledWith({
+ message: FETCH_GROUPS_ERROR,
+ error: expect.any(Error),
+ parent: wrapper.vm.$el,
+ });
+ });
+
+ 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({ 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);
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/help_popover_spec.js b/spec/frontend/vue_shared/components/help_popover_spec.js
index 6fd5ae0e946..77c03dc0c3c 100644
--- a/spec/frontend/vue_shared/components/help_popover_spec.js
+++ b/spec/frontend/vue_shared/components/help_popover_spec.js
@@ -96,6 +96,20 @@ describe('HelpPopover', () => {
});
});
+ describe('with alternative icon', () => {
+ beforeEach(() => {
+ createComponent({
+ props: {
+ icon: 'information-o',
+ },
+ });
+ });
+
+ it('uses the given icon', () => {
+ expect(findQuestionButton().props('icon')).toBe('information-o');
+ });
+ });
+
describe('with custom slots', () => {
const titleSlot = '<h1>title</h1>';
const defaultSlot = '<strong>content</strong>';
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 f7e93f45148..625e67c7cc1 100644
--- a/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js
@@ -27,7 +27,7 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
const formFieldAriaLabel = 'Edit your content';
let mock;
- const buildWrapper = ({ propsData = {}, attachTo } = {}) => {
+ const buildWrapper = ({ propsData = {}, attachTo, stubs = {} } = {}) => {
wrapper = mountExtended(MarkdownEditor, {
attachTo,
propsData: {
@@ -45,6 +45,7 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
},
stubs: {
BubbleMenu: stubComponent(BubbleMenu),
+ ...stubs,
},
});
};
@@ -138,9 +139,9 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
expect(wrapper.emitted('input')).toEqual([[newValue]]);
});
- describe('when initOnAutofocus is true', () => {
+ describe('when autofocus is true', () => {
beforeEach(async () => {
- buildWrapper({ attachTo: document.body, propsData: { initOnAutofocus: true } });
+ buildWrapper({ attachTo: document.body, propsData: { autofocus: true } });
await nextTick();
});
@@ -171,7 +172,6 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
renderMarkdown: expect.any(Function),
uploadsPath: window.uploads_path,
markdown: value,
- autofocus: 'end',
}),
);
});
@@ -204,10 +204,12 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
findSegmentedControl().vm.$emit('input', EDITING_MODE_CONTENT_EDITOR);
});
- describe('when initOnAutofocus is true', () => {
+ describe('when autofocus is true', () => {
beforeEach(() => {
- buildWrapper({ propsData: { initOnAutofocus: true } });
- findLocalStorageSync().vm.$emit('input', EDITING_MODE_CONTENT_EDITOR);
+ buildWrapper({
+ propsData: { autofocus: true },
+ stubs: { ContentEditor: stubComponent(ContentEditor) },
+ });
});
it('sets the content editor autofocus property to end', () => {
@@ -247,19 +249,6 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
it('updates localStorage value', () => {
expect(findLocalStorageSync().props().value).toBe(EDITING_MODE_MARKDOWN_FIELD);
});
-
- it('sets the textarea as the activeElement in the document', async () => {
- // The component should be rebuilt to attach it to the document body
- buildWrapper({ attachTo: document.body });
- await findSegmentedControl().vm.$emit('input', EDITING_MODE_CONTENT_EDITOR);
-
- expect(findContentEditor().exists()).toBe(true);
-
- await findSegmentedControl().vm.$emit('input', EDITING_MODE_MARKDOWN_FIELD);
- await findSegmentedControl().vm.$emit('change', EDITING_MODE_MARKDOWN_FIELD);
-
- expect(document.activeElement).toBe(findTextarea().element);
- });
});
describe('when content editor emits loading event', () => {
diff --git a/spec/frontend/vue_shared/components/markdown_drawer/markdown_drawer_spec.js b/spec/frontend/vue_shared/components/markdown_drawer/markdown_drawer_spec.js
new file mode 100644
index 00000000000..8edcb905096
--- /dev/null
+++ b/spec/frontend/vue_shared/components/markdown_drawer/markdown_drawer_spec.js
@@ -0,0 +1,205 @@
+import { GlDrawer, GlAlert, GlSkeletonLoader } from '@gitlab/ui';
+import { nextTick } from 'vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import MarkdownDrawer, { cache } from '~/vue_shared/components/markdown_drawer/markdown_drawer.vue';
+import { getRenderedMarkdown } from '~/vue_shared/components/markdown_drawer/utils/fetch';
+import { contentTop } from '~/lib/utils/common_utils';
+
+jest.mock('~/vue_shared/components/markdown_drawer/utils/fetch', () => ({
+ getRenderedMarkdown: jest.fn().mockReturnValue({
+ title: 'test title test',
+ body: `<div id="content-body">
+ <div class="documentation md gl-mt-3">
+ test body
+ </div>
+ </div>`,
+ }),
+}));
+
+jest.mock('~/lib/utils/common_utils', () => ({
+ contentTop: jest.fn(),
+}));
+
+describe('MarkdownDrawer', () => {
+ let wrapper;
+ const defaultProps = {
+ documentPath: 'user/search/global_search/advanced_search_syntax.json',
+ };
+
+ const createComponent = (props) => {
+ wrapper = shallowMountExtended(MarkdownDrawer, {
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ Object.keys(cache).forEach((key) => delete cache[key]);
+ });
+
+ const findDrawer = () => wrapper.findComponent(GlDrawer);
+ const findAlert = () => wrapper.findComponent(GlAlert);
+ const findSkeleton = () => wrapper.findComponent(GlSkeletonLoader);
+ const findDrawerTitle = () => wrapper.findComponent('[data-testid="title-element"]');
+ const findDrawerBody = () => wrapper.findComponent({ ref: 'content-element' });
+
+ describe('component', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders correctly', () => {
+ expect(findDrawer().exists()).toBe(true);
+ expect(findDrawerTitle().text()).toBe('test title test');
+ expect(findDrawerBody().text()).toBe('test body');
+ });
+ });
+
+ describe.each`
+ hasNavbar | navbarHeight
+ ${false} | ${0}
+ ${true} | ${100}
+ `('computes offsetTop', ({ hasNavbar, navbarHeight }) => {
+ beforeEach(() => {
+ global.document.querySelector = jest.fn(() =>
+ hasNavbar
+ ? {
+ dataset: {
+ page: 'test',
+ },
+ }
+ : undefined,
+ );
+ contentTop.mockReturnValue(navbarHeight);
+ createComponent();
+ });
+
+ afterEach(() => {
+ contentTop.mockClear();
+ });
+
+ it(`computes offsetTop ${hasNavbar ? 'with' : 'without'} .navbar-gitlab`, () => {
+ expect(findDrawer().attributes('headerheight')).toBe(`${navbarHeight}px`);
+ });
+ });
+
+ describe('watcher', () => {
+ let renderGLFMSpy;
+ let fetchMarkdownSpy;
+
+ beforeEach(async () => {
+ renderGLFMSpy = jest.spyOn(MarkdownDrawer.methods, 'renderGLFM');
+ fetchMarkdownSpy = jest.spyOn(MarkdownDrawer.methods, 'fetchMarkdown');
+ global.document.querySelector = jest.fn(() => ({
+ getBoundingClientRect: jest.fn(() => ({ bottom: 100 })),
+ dataset: {
+ page: 'test',
+ },
+ }));
+ createComponent();
+ await nextTick();
+ });
+
+ afterEach(() => {
+ renderGLFMSpy.mockClear();
+ fetchMarkdownSpy.mockClear();
+ });
+
+ it('for documentPath triggers fetch', async () => {
+ expect(fetchMarkdownSpy).toHaveBeenCalledTimes(1);
+
+ await wrapper.setProps({ documentPath: '/test/me' });
+ await nextTick();
+
+ expect(fetchMarkdownSpy).toHaveBeenCalledTimes(2);
+ });
+
+ it('for open triggers renderGLFM', async () => {
+ wrapper.vm.fetchMarkdown();
+ wrapper.vm.openDrawer();
+ await nextTick();
+ expect(renderGLFMSpy).toHaveBeenCalled();
+ });
+ });
+
+ describe('Markdown fetching', () => {
+ let renderGLFMSpy;
+
+ beforeEach(async () => {
+ renderGLFMSpy = jest.spyOn(MarkdownDrawer.methods, 'renderGLFM');
+ createComponent();
+ await nextTick();
+ });
+
+ afterEach(() => {
+ renderGLFMSpy.mockClear();
+ });
+
+ it('fetches the Markdown and caches it', async () => {
+ expect(getRenderedMarkdown).toHaveBeenCalledTimes(1);
+ expect(Object.keys(cache)).toHaveLength(1);
+ });
+
+ it('when the document changes, fetches it and caches it as well', async () => {
+ expect(getRenderedMarkdown).toHaveBeenCalledTimes(1);
+ expect(Object.keys(cache)).toHaveLength(1);
+
+ await wrapper.setProps({ documentPath: '/test/me2' });
+ await nextTick();
+
+ expect(getRenderedMarkdown).toHaveBeenCalledTimes(2);
+ expect(Object.keys(cache)).toHaveLength(2);
+ });
+
+ it('when re-using an already fetched document, gets it from the cache', async () => {
+ await wrapper.setProps({ documentPath: '/test/me2' });
+ await nextTick();
+
+ expect(getRenderedMarkdown).toHaveBeenCalledTimes(2);
+ expect(Object.keys(cache)).toHaveLength(2);
+
+ await wrapper.setProps({ documentPath: defaultProps.documentPath });
+ await nextTick();
+
+ expect(getRenderedMarkdown).toHaveBeenCalledTimes(2);
+ expect(Object.keys(cache)).toHaveLength(2);
+ });
+ });
+
+ describe('Markdown fetching returns error', () => {
+ beforeEach(async () => {
+ getRenderedMarkdown.mockReturnValue({
+ hasFetchError: true,
+ });
+
+ createComponent();
+ await nextTick();
+ });
+ afterEach(() => {
+ getRenderedMarkdown.mockClear();
+ });
+ it('shows alert', () => {
+ expect(findAlert().exists()).toBe(true);
+ });
+ });
+
+ describe('While Markdown is fetching', () => {
+ beforeEach(async () => {
+ getRenderedMarkdown.mockReturnValue(new Promise(() => {}));
+
+ createComponent();
+ });
+
+ afterEach(() => {
+ getRenderedMarkdown.mockClear();
+ });
+
+ it('shows skeleton', async () => {
+ expect(findSkeleton().exists()).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/markdown_drawer/mock_data.js b/spec/frontend/vue_shared/components/markdown_drawer/mock_data.js
new file mode 100644
index 00000000000..53b40407556
--- /dev/null
+++ b/spec/frontend/vue_shared/components/markdown_drawer/mock_data.js
@@ -0,0 +1,42 @@
+export const MOCK_HTML = `<!DOCTYPE html>
+<html>
+<body>
+ <div id="content-body">
+ <h1>test title <strong>test</strong></h1>
+ <div class="documentation md gl-mt-3">
+ <a href="../advanced_search.md">Advanced Search</a>
+ <a href="../advanced_search2.md">Advanced Search2</a>
+ <h2>test header h2</h2>
+ <table class="testClass">
+ <tr>
+ <td>Emil</td>
+ <td>Tobias</td>
+ <td>Linus</td>
+ </tr>
+ <tr>
+ <td>16</td>
+ <td>14</td>
+ <td>10</td>
+ </tr>
+ </table>
+ </div>
+ </div>
+</body>
+</html>`.replace(/\n/g, '');
+
+export const MOCK_DRAWER_DATA = {
+ hasFetchError: false,
+ title: 'test title test',
+ body: ` <div id="content-body"> <div class="documentation md gl-mt-3"> <a href="../advanced_search.md">Advanced Search</a> <a href="../advanced_search2.md">Advanced Search2</a> <h2>test header h2</h2> <table class="testClass"> <tbody><tr> <td>Emil</td> <td>Tobias</td> <td>Linus</td> </tr> <tr> <td>16</td> <td>14</td> <td>10</td> </tr> </tbody></table> </div> </div>`,
+};
+
+export const MOCK_DRAWER_DATA_ERROR = {
+ hasFetchError: true,
+};
+
+export const MOCK_TABLE_DATA_BEFORE = `<head></head><body><h1>test</h1></test><table><tbody><tr><td></td></tr></tbody></table></body>`;
+
+export const MOCK_HTML_DATA_AFTER = {
+ body: '<table><tbody><tr><td></td></tr></tbody></table>',
+ title: 'test',
+};
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
new file mode 100644
index 00000000000..ff07b2cf838
--- /dev/null
+++ b/spec/frontend/vue_shared/components/markdown_drawer/utils/fetch_spec.js
@@ -0,0 +1,43 @@
+import MockAdapter from 'axios-mock-adapter';
+import {
+ getRenderedMarkdown,
+ splitDocument,
+} from '~/vue_shared/components/markdown_drawer/utils/fetch';
+import axios from '~/lib/utils/axios_utils';
+import {
+ MOCK_HTML,
+ MOCK_DRAWER_DATA,
+ MOCK_DRAWER_DATA_ERROR,
+ MOCK_TABLE_DATA_BEFORE,
+ MOCK_HTML_DATA_AFTER,
+} from '../mock_data';
+
+describe('utils/fetch', () => {
+ let mock;
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ describe.each`
+ axiosMock | type | toExpect
+ ${{ code: 200, res: { html: MOCK_HTML } }} | ${'success'} | ${MOCK_DRAWER_DATA}
+ ${{ code: 500, res: null }} | ${'error'} | ${MOCK_DRAWER_DATA_ERROR}
+ `('process markdown data', ({ axiosMock, type, toExpect }) => {
+ describe(`if api fetch responds with ${type}`, () => {
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ mock.onGet().reply(axiosMock.code, axiosMock.res);
+ });
+ it(`should update drawer correctly`, async () => {
+ expect(await getRenderedMarkdown('/any/path')).toStrictEqual(toExpect);
+ });
+ });
+ });
+
+ describe('splitDocument', () => {
+ it(`should update tables correctly`, () => {
+ expect(splitDocument(MOCK_TABLE_DATA_BEFORE)).toStrictEqual(MOCK_HTML_DATA_AFTER);
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/namespace_select/mock_data.js b/spec/frontend/vue_shared/components/namespace_select/mock_data.js
deleted file mode 100644
index cfd521c67cb..00000000000
--- a/spec/frontend/vue_shared/components/namespace_select/mock_data.js
+++ /dev/null
@@ -1,6 +0,0 @@
-export const groupNamespaces = [
- { id: 1, name: 'Group 1', humanName: 'Group 1' },
- { id: 2, name: 'Subgroup 1', humanName: 'Group 1 / Subgroup 1' },
-];
-
-export const userNamespaces = [{ id: 3, name: 'User namespace 1', humanName: 'User namespace 1' }];
diff --git a/spec/frontend/vue_shared/components/namespace_select/namespace_select_deprecated_spec.js b/spec/frontend/vue_shared/components/namespace_select/namespace_select_deprecated_spec.js
deleted file mode 100644
index d930ef63dad..00000000000
--- a/spec/frontend/vue_shared/components/namespace_select/namespace_select_deprecated_spec.js
+++ /dev/null
@@ -1,236 +0,0 @@
-import { nextTick } from 'vue';
-import {
- GlDropdown,
- GlDropdownItem,
- GlDropdownSectionHeader,
- GlSearchBoxByType,
- GlIntersectionObserver,
- GlLoadingIcon,
-} from '@gitlab/ui';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import NamespaceSelect, {
- i18n,
- EMPTY_NAMESPACE_ID,
-} from '~/vue_shared/components/namespace_select/namespace_select_deprecated.vue';
-import { userNamespaces, groupNamespaces } from './mock_data';
-
-const FLAT_NAMESPACES = [...userNamespaces, ...groupNamespaces];
-const EMPTY_NAMESPACE_TITLE = 'Empty namespace TEST';
-const EMPTY_NAMESPACE_ITEM = { id: EMPTY_NAMESPACE_ID, humanName: EMPTY_NAMESPACE_TITLE };
-
-describe('NamespaceSelectDeprecated', () => {
- let wrapper;
-
- const createComponent = (props = {}) =>
- shallowMountExtended(NamespaceSelect, {
- propsData: {
- userNamespaces,
- groupNamespaces,
- ...props,
- },
- stubs: {
- // We have to "full" mount GlDropdown so that slot children will render
- GlDropdown,
- },
- });
-
- const wrappersText = (arr) => arr.wrappers.map((w) => w.text());
- const findDropdown = () => wrapper.findComponent(GlDropdown);
- const findDropdownText = () => findDropdown().props('text');
- const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
- const findGroupDropdownItems = () =>
- wrapper.findByTestId('namespace-list-groups').findAllComponents(GlDropdownItem);
- const findDropdownItemsTexts = () => findDropdownItems().wrappers.map((x) => x.text());
- const findSectionHeaders = () => wrapper.findAllComponents(GlDropdownSectionHeader);
- const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType);
- const search = (term) => findSearchBox().vm.$emit('input', term);
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- describe('default', () => {
- beforeEach(() => {
- wrapper = createComponent();
- });
-
- it('renders the dropdown', () => {
- expect(findDropdown().exists()).toBe(true);
- });
-
- it('renders each dropdown item', () => {
- expect(findDropdownItemsTexts()).toEqual(FLAT_NAMESPACES.map((x) => x.humanName));
- });
-
- it('renders default dropdown text', () => {
- expect(findDropdownText()).toBe(i18n.DEFAULT_TEXT);
- });
-
- it('splits group and user namespaces', () => {
- const headers = findSectionHeaders();
- expect(wrappersText(headers)).toEqual([i18n.USERS, i18n.GROUPS]);
- });
-
- it('does not render wrapper as full width', () => {
- expect(findDropdown().attributes('block')).toBeUndefined();
- });
- });
-
- it('with defaultText, it overrides dropdown text', () => {
- const textOverride = 'Select an option';
-
- wrapper = createComponent({ defaultText: textOverride });
-
- expect(findDropdownText()).toBe(textOverride);
- });
-
- it('with includeHeaders=false, hides group/user headers', () => {
- wrapper = createComponent({ includeHeaders: false });
-
- expect(findSectionHeaders()).toHaveLength(0);
- });
-
- it('with fullWidth=true, sets the dropdown to full width', () => {
- wrapper = createComponent({ fullWidth: true });
-
- expect(findDropdown().attributes('block')).toBe('true');
- });
-
- describe('with search', () => {
- it.each`
- term | includeEmptyNamespace | shouldFilterNamespaces | expectedItems
- ${''} | ${false} | ${true} | ${[...userNamespaces, ...groupNamespaces]}
- ${'sub'} | ${false} | ${true} | ${[groupNamespaces[1]]}
- ${'User'} | ${false} | ${true} | ${[...userNamespaces]}
- ${'User'} | ${true} | ${true} | ${[...userNamespaces]}
- ${'namespace'} | ${true} | ${true} | ${[EMPTY_NAMESPACE_ITEM, ...userNamespaces]}
- ${'sub'} | ${false} | ${false} | ${[...userNamespaces, ...groupNamespaces]}
- `(
- 'with term=$term, includeEmptyNamespace=$includeEmptyNamespace, and shouldFilterNamespaces=$shouldFilterNamespaces should show $expectedItems.length',
- async ({ term, includeEmptyNamespace, shouldFilterNamespaces, expectedItems }) => {
- wrapper = createComponent({
- includeEmptyNamespace,
- emptyNamespaceTitle: EMPTY_NAMESPACE_TITLE,
- shouldFilterNamespaces,
- });
-
- search(term);
-
- await nextTick();
-
- const expected = expectedItems.map((x) => x.humanName);
-
- expect(findDropdownItemsTexts()).toEqual(expected);
- },
- );
- });
-
- describe('when search is typed in', () => {
- it('emits `search` event', async () => {
- wrapper = createComponent();
-
- wrapper.findComponent(GlSearchBoxByType).vm.$emit('input', 'foo');
-
- await nextTick();
-
- expect(wrapper.emitted('search')).toEqual([['foo']]);
- });
- });
-
- describe('with a selected namespace', () => {
- const selectedGroupIndex = 1;
- const selectedItem = groupNamespaces[selectedGroupIndex];
-
- beforeEach(() => {
- wrapper = createComponent();
-
- wrapper.findComponent(GlSearchBoxByType).vm.$emit('input', 'foo');
- findGroupDropdownItems().at(selectedGroupIndex).vm.$emit('click');
- });
-
- it('sets the dropdown text', () => {
- expect(findDropdownText()).toBe(selectedItem.humanName);
- });
-
- it('emits the `select` event when a namespace is selected', () => {
- const args = [selectedItem];
- expect(wrapper.emitted('select')).toEqual([args]);
- });
-
- it('clears search', () => {
- expect(wrapper.findComponent(GlSearchBoxByType).props('value')).toBe('');
- });
- });
-
- describe('with an empty namespace option', () => {
- beforeEach(() => {
- wrapper = createComponent({
- includeEmptyNamespace: true,
- emptyNamespaceTitle: EMPTY_NAMESPACE_TITLE,
- });
- });
-
- it('includes the empty namespace', () => {
- const first = findDropdownItems().at(0);
-
- expect(first.text()).toBe(EMPTY_NAMESPACE_TITLE);
- });
-
- it('emits the `select` event when a namespace is selected', () => {
- findDropdownItems().at(0).vm.$emit('click');
-
- expect(wrapper.emitted('select')).toEqual([[EMPTY_NAMESPACE_ITEM]]);
- });
-
- it.each`
- desc | term | shouldShow
- ${'should hide empty option'} | ${'group'} | ${false}
- ${'should show empty option'} | ${'Empty'} | ${true}
- `('when search for $term, $desc', async ({ term, shouldShow }) => {
- search(term);
-
- await nextTick();
-
- expect(findDropdownItemsTexts().includes(EMPTY_NAMESPACE_TITLE)).toBe(shouldShow);
- });
- });
-
- describe('when `hasNextPageOfGroups` prop is `true`', () => {
- it('renders `GlIntersectionObserver` and emits `load-more-groups` event when bottom is reached', () => {
- wrapper = createComponent({ hasNextPageOfGroups: true });
-
- const intersectionObserver = wrapper.findComponent(GlIntersectionObserver);
-
- intersectionObserver.vm.$emit('appear');
-
- expect(intersectionObserver.exists()).toBe(true);
- expect(wrapper.emitted('load-more-groups')).toEqual([[]]);
- });
-
- describe('when `isLoading` prop is `true`', () => {
- it('renders a loading icon', () => {
- wrapper = createComponent({ hasNextPageOfGroups: true, isLoading: true });
-
- expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
- });
- });
- });
-
- describe('when `isSearchLoading` prop is `true`', () => {
- it('sets `isLoading` prop to `true`', () => {
- wrapper = createComponent({ isSearchLoading: true });
-
- expect(wrapper.findComponent(GlSearchBoxByType).props('isLoading')).toBe(true);
- });
- });
-
- describe('when dropdown is opened', () => {
- it('emits `show` event', () => {
- wrapper = createComponent();
-
- findDropdown().vm.$emit('show');
-
- expect(wrapper.emitted('show')).toEqual([[]]);
- });
- });
-});
diff --git a/spec/frontend/vue_shared/components/sidebar/issuable_move_dropdown_spec.js b/spec/frontend/vue_shared/components/sidebar/issuable_move_dropdown_spec.js
index 9b1316677d7..d531147c0e6 100644
--- a/spec/frontend/vue_shared/components/sidebar/issuable_move_dropdown_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/issuable_move_dropdown_spec.js
@@ -37,6 +37,7 @@ const mockProps = {
dropdownButtonTitle: 'Move issuable',
dropdownHeaderTitle: 'Move issuable',
moveInProgress: false,
+ disabled: false,
};
const mockEvent = {
@@ -44,20 +45,21 @@ const mockEvent = {
preventDefault: jest.fn(),
};
-const createComponent = (propsData = mockProps) =>
- shallowMount(IssuableMoveDropdown, {
- propsData,
- });
-
describe('IssuableMoveDropdown', () => {
let mock;
let wrapper;
- beforeEach(() => {
- mock = new MockAdapter(axios);
- wrapper = createComponent();
+ const createComponent = (propsData = mockProps) => {
+ wrapper = shallowMount(IssuableMoveDropdown, {
+ propsData,
+ });
wrapper.vm.$refs.dropdown.hide = jest.fn();
wrapper.vm.$refs.searchInput.focusInput = jest.fn();
+ };
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ createComponent();
});
afterEach(() => {
@@ -194,6 +196,12 @@ describe('IssuableMoveDropdown', () => {
expect(findDropdownEl().findComponent(GlDropdownForm).exists()).toBe(true);
});
+ it('renders disabled dropdown when `disabled` is true', () => {
+ createComponent({ ...mockProps, disabled: true });
+
+ expect(findDropdownEl().attributes('disabled')).toBe('true');
+ });
+
it('renders header element', () => {
const headerEl = findDropdownEl().find('[data-testid="header"]');
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js
index b58c44645d6..74ddd07d041 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js
@@ -49,7 +49,6 @@ describe('LabelsSelectRoot', () => {
issuableType = IssuableType.Issue,
queryHandler = successfulQueryHandler,
mutationHandler = successfulMutationHandler,
- isRealtimeEnabled = false,
} = {}) => {
const mockApollo = createMockApollo([
[issueLabelsQuery, queryHandler],
@@ -74,9 +73,6 @@ describe('LabelsSelectRoot', () => {
allowLabelEdit: true,
allowLabelCreate: true,
labelsManagePath: 'test',
- glFeatures: {
- realtimeLabels: isRealtimeEnabled,
- },
},
});
};
@@ -204,17 +200,10 @@ describe('LabelsSelectRoot', () => {
});
});
- it('does not emit `updateSelectedLabels` event when the subscription is triggered and FF is disabled', async () => {
+ it('emits `updateSelectedLabels` event when the subscription is triggered', async () => {
createComponent();
await waitForPromises();
- expect(wrapper.emitted('updateSelectedLabels')).toBeUndefined();
- });
-
- it('emits `updateSelectedLabels` event when the subscription is triggered and FF is enabled', async () => {
- createComponent({ isRealtimeEnabled: true });
- await waitForPromises();
-
expect(wrapper.emitted('updateSelectedLabels')).toEqual([
[
{
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 8dc3348acfa..d720574ce6d 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,6 +2,9 @@ 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 { scrollToElement } from '~/lib/utils/common_utils';
+
+jest.mock('~/lib/utils/common_utils');
const DEFAULT_PROPS = {
chunkIndex: 2,
@@ -13,11 +16,17 @@ const DEFAULT_PROPS = {
blamePath: 'blame/file.js',
};
+const hash = '#L142';
+
describe('Chunk component', () => {
let wrapper;
+ let idleCallbackSpy;
const createComponent = (props = {}) => {
- wrapper = shallowMountExtended(Chunk, { propsData: { ...DEFAULT_PROPS, ...props } });
+ wrapper = shallowMountExtended(Chunk, {
+ mocks: { $route: { hash } },
+ propsData: { ...DEFAULT_PROPS, ...props },
+ });
};
const findIntersectionObserver = () => wrapper.findComponent(GlIntersectionObserver);
@@ -26,6 +35,7 @@ describe('Chunk component', () => {
const findContent = () => wrapper.findByTestId('content');
beforeEach(() => {
+ idleCallbackSpy = jest.spyOn(window, 'requestIdleCallback').mockImplementation((fn) => fn());
createComponent();
});
@@ -51,18 +61,30 @@ describe('Chunk component', () => {
});
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()).toMatchObject({
- 'data-line-number': `${DEFAULT_PROPS.startingFrom + 1}`,
- href: `#L${DEFAULT_PROPS.startingFrom + 1}`,
- id: `L${DEFAULT_PROPS.startingFrom + 1}`,
- });
+ expect(findLineNumbers().at(0).attributes('id')).toBe(`L${DEFAULT_PROPS.startingFrom + 1}`);
expect(findContent().text()).toBe(DEFAULT_PROPS.content);
});
@@ -80,5 +102,14 @@ describe('Chunk component', () => {
blamePath: DEFAULT_PROPS.blamePath,
});
});
+
+ it('does not scroll to route hash if last chunk is not loaded', () => {
+ expect(scrollToElement).not.toHaveBeenCalled();
+ });
+
+ it('scrolls to route hash if last chunk is loaded', () => {
+ createComponent({ totalChunks: DEFAULT_PROPS.chunkIndex + 1 });
+ expect(scrollToElement).toHaveBeenCalledWith(hash, { behavior: 'auto' });
+ });
});
});
diff --git a/spec/frontend/vue_shared/components/source_viewer/plugins/link_dependencies_spec.js b/spec/frontend/vue_shared/components/source_viewer/plugins/link_dependencies_spec.js
index 375b1307616..a7b55d7332f 100644
--- a/spec/frontend/vue_shared/components/source_viewer/plugins/link_dependencies_spec.js
+++ b/spec/frontend/vue_shared/components/source_viewer/plugins/link_dependencies_spec.js
@@ -1,10 +1,26 @@
import packageJsonLinker from '~/vue_shared/components/source_viewer/plugins/utils/package_json_linker';
+import godepsJsonLinker from '~/vue_shared/components/source_viewer/plugins/utils/godeps_json_linker';
import gemspecLinker from '~/vue_shared/components/source_viewer/plugins/utils/gemspec_linker';
+import gemfileLinker from '~/vue_shared/components/source_viewer/plugins/utils/gemfile_linker';
+import podspecJsonLinker from '~/vue_shared/components/source_viewer/plugins/utils/podspec_json_linker';
+import composerJsonLinker from '~/vue_shared/components/source_viewer/plugins/utils/composer_json_linker';
import linkDependencies from '~/vue_shared/components/source_viewer/plugins/link_dependencies';
-import { PACKAGE_JSON_FILE_TYPE, PACKAGE_JSON_CONTENT, GEMSPEC_FILE_TYPE } from './mock_data';
+import {
+ PACKAGE_JSON_FILE_TYPE,
+ PACKAGE_JSON_CONTENT,
+ GEMSPEC_FILE_TYPE,
+ GODEPS_JSON_FILE_TYPE,
+ GEMFILE_FILE_TYPE,
+ PODSPEC_JSON_FILE_TYPE,
+ COMPOSER_JSON_FILE_TYPE,
+} from './mock_data';
jest.mock('~/vue_shared/components/source_viewer/plugins/utils/package_json_linker');
jest.mock('~/vue_shared/components/source_viewer/plugins/utils/gemspec_linker');
+jest.mock('~/vue_shared/components/source_viewer/plugins/utils/godeps_json_linker');
+jest.mock('~/vue_shared/components/source_viewer/plugins/utils/gemfile_linker');
+jest.mock('~/vue_shared/components/source_viewer/plugins/utils/podspec_json_linker');
+jest.mock('~/vue_shared/components/source_viewer/plugins/utils/composer_json_linker');
describe('Highlight.js plugin for linking dependencies', () => {
const hljsResultMock = { value: 'test' };
@@ -18,4 +34,24 @@ describe('Highlight.js plugin for linking dependencies', () => {
linkDependencies(hljsResultMock, GEMSPEC_FILE_TYPE);
expect(gemspecLinker).toHaveBeenCalled();
});
+
+ it('calls godepsJsonLinker for godeps_json file types', () => {
+ linkDependencies(hljsResultMock, GODEPS_JSON_FILE_TYPE);
+ expect(godepsJsonLinker).toHaveBeenCalled();
+ });
+
+ it('calls gemfileLinker for gemfile file types', () => {
+ linkDependencies(hljsResultMock, GEMFILE_FILE_TYPE);
+ expect(gemfileLinker).toHaveBeenCalled();
+ });
+
+ it('calls podspecJsonLinker for podspec_json file types', () => {
+ linkDependencies(hljsResultMock, PODSPEC_JSON_FILE_TYPE);
+ expect(podspecJsonLinker).toHaveBeenCalled();
+ });
+
+ it('calls composerJsonLinker for composer_json file types', () => {
+ linkDependencies(hljsResultMock, COMPOSER_JSON_FILE_TYPE);
+ expect(composerJsonLinker).toHaveBeenCalled();
+ });
});
diff --git a/spec/frontend/vue_shared/components/source_viewer/plugins/mock_data.js b/spec/frontend/vue_shared/components/source_viewer/plugins/mock_data.js
index aa874c9c081..5455479ec71 100644
--- a/spec/frontend/vue_shared/components/source_viewer/plugins/mock_data.js
+++ b/spec/frontend/vue_shared/components/source_viewer/plugins/mock_data.js
@@ -1,4 +1,34 @@
export const PACKAGE_JSON_FILE_TYPE = 'package_json';
+
export const PACKAGE_JSON_CONTENT = '{ "dependencies": { "@babel/core": "^7.18.5" } }';
+export const COMPOSER_JSON_EXAMPLES = {
+ packagist: '{ "require": { "composer/installers": "^1.2" } }',
+ drupal: '{ "require": { "drupal/bootstrap": "3.x-dev" } }',
+ withoutLink: '{ "require": { "drupal/erp_common": "dev-master" } }',
+};
+
export const GEMSPEC_FILE_TYPE = 'gemspec';
+
+export const GODEPS_JSON_FILE_TYPE = 'godeps_json';
+
+export const GEMFILE_FILE_TYPE = 'gemfile';
+
+export const PODSPEC_JSON_FILE_TYPE = 'podspec_json';
+
+export const PODSPEC_JSON_CONTENT = `{
+ "dependencies": {
+ "MyCheckCore": [
+ ]
+ },
+ "subspecs": [
+ {
+ "dependencies": {
+ "AFNetworking/Security": [
+ ]
+ }
+ }
+ ]
+ }`;
+
+export const COMPOSER_JSON_FILE_TYPE = 'composer_json';
diff --git a/spec/frontend/vue_shared/components/source_viewer/plugins/utils/composer_json_linker_spec.js b/spec/frontend/vue_shared/components/source_viewer/plugins/utils/composer_json_linker_spec.js
new file mode 100644
index 00000000000..3ecb16ddcd0
--- /dev/null
+++ b/spec/frontend/vue_shared/components/source_viewer/plugins/utils/composer_json_linker_spec.js
@@ -0,0 +1,38 @@
+import composerJsonLinker from '~/vue_shared/components/source_viewer/plugins/utils/composer_json_linker';
+import { COMPOSER_JSON_EXAMPLES } from '../mock_data';
+
+describe('Highlight.js plugin for linking composer.json dependencies', () => {
+ it('mutates the input value by wrapping dependency names and versions in anchors', () => {
+ const inputValue =
+ '<span class="hljs-attr">&quot;drupal/erp_common"&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;dev-master&quot;</span>';
+ const outputValue =
+ '<span class="hljs-attr">&quot;drupal/erp_common"&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;dev-master&quot;</span>';
+ const hljsResultMock = { value: inputValue };
+
+ const output = composerJsonLinker(hljsResultMock, COMPOSER_JSON_EXAMPLES.withoutLink);
+ expect(output).toBe(outputValue);
+ });
+});
+
+const getInputValue = (dependencyString, version) =>
+ `<span class="hljs-attr">&quot;${dependencyString}&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;${version}&quot;</span>`;
+const getOutputValue = (dependencyString, version, expectedHref) =>
+ `<span class="hljs-attr">&quot;<a href="${expectedHref}" target="_blank" rel="nofollow noreferrer noopener">${dependencyString}</a>&quot;</span>: <span class="hljs-attr">&quot;<a href="${expectedHref}" target="_blank" rel="nofollow noreferrer noopener">${version}</a>&quot;</span>`;
+
+describe('Highlight.js plugin for linking Godeps.json dependencies', () => {
+ it.each`
+ type | dependency | version | expectedHref
+ ${'packagist'} | ${'composer/installers'} | ${'^1.2'} | ${'https://packagist.org/packages/composer/installers'}
+ ${'drupal'} | ${'drupal/bootstrap'} | ${'3.x-dev'} | ${'https://www.drupal.org/project/bootstrap'}
+ `(
+ 'mutates the input value by wrapping dependency names in anchors and altering path when needed',
+ ({ type, dependency, version, expectedHref }) => {
+ const inputValue = getInputValue(dependency, version);
+ const outputValue = getOutputValue(dependency, version, expectedHref);
+ const hljsResultMock = { value: inputValue };
+
+ const output = composerJsonLinker(hljsResultMock, COMPOSER_JSON_EXAMPLES[type]);
+ expect(output).toBe(outputValue);
+ },
+ );
+});
diff --git a/spec/frontend/vue_shared/components/source_viewer/plugins/utils/dependency_linker_util_spec.js b/spec/frontend/vue_shared/components/source_viewer/plugins/utils/dependency_linker_util_spec.js
index e4ce07ec668..66e2020da27 100644
--- a/spec/frontend/vue_shared/components/source_viewer/plugins/utils/dependency_linker_util_spec.js
+++ b/spec/frontend/vue_shared/components/source_viewer/plugins/utils/dependency_linker_util_spec.js
@@ -1,13 +1,15 @@
import {
createLink,
generateHLJSOpenTag,
+ getObjectKeysByKeyName,
} from '~/vue_shared/components/source_viewer/plugins/utils/dependency_linker_util';
+import { PODSPEC_JSON_CONTENT } from '../mock_data';
describe('createLink', () => {
it('generates a link with the correct attributes', () => {
const href = 'http://test.com';
const innerText = 'testing';
- const result = `<a href="${href}" rel="nofollow noreferrer noopener">${innerText}</a>`;
+ const result = `<a href="${href}" target="_blank" rel="nofollow noreferrer noopener">${innerText}</a>`;
expect(createLink(href, innerText)).toBe(result);
});
@@ -18,7 +20,7 @@ describe('createLink', () => {
const escapedHref = '&lt;script&gt;XSS&lt;/script&gt;';
const href = `http://test.com/${unescapedXSS}`;
const innerText = `testing${unescapedXSS}`;
- const result = `<a href="http://test.com/${escapedHref}" rel="nofollow noreferrer noopener">testing${escapedPackageName}</a>`;
+ const result = `<a href="http://test.com/${escapedHref}" target="_blank" rel="nofollow noreferrer noopener">testing${escapedPackageName}</a>`;
expect(createLink(href, innerText)).toBe(result);
});
@@ -32,3 +34,11 @@ describe('generateHLJSOpenTag', () => {
expect(generateHLJSOpenTag(type)).toBe(result);
});
});
+
+describe('getObjectKeysByKeyName method', () => {
+ it('gets all object keys within specified key', () => {
+ const acc = [];
+ const keys = getObjectKeysByKeyName(JSON.parse(PODSPEC_JSON_CONTENT), 'dependencies', acc);
+ expect(keys).toEqual(['MyCheckCore', 'AFNetworking/Security']);
+ });
+});
diff --git a/spec/frontend/vue_shared/components/source_viewer/plugins/utils/gemfile_linker_spec.js b/spec/frontend/vue_shared/components/source_viewer/plugins/utils/gemfile_linker_spec.js
new file mode 100644
index 00000000000..4e188c9af7e
--- /dev/null
+++ b/spec/frontend/vue_shared/components/source_viewer/plugins/utils/gemfile_linker_spec.js
@@ -0,0 +1,13 @@
+import gemfileLinker from '~/vue_shared/components/source_viewer/plugins/utils/gemfile_linker';
+
+describe('Highlight.js plugin for linking gemfile dependencies', () => {
+ it('mutates the input value by wrapping dependency names in anchors', () => {
+ const inputValue = 'gem </span><span class="hljs-string">&#39;paranoia&#39;';
+ const outputValue =
+ 'gem </span><span class="hljs-string">&#39;<a href="https://rubygems.org/gems/paranoia" target="_blank" rel="nofollow noreferrer noopener">paranoia</a>&#39;';
+ const hljsResultMock = { value: inputValue };
+
+ const output = gemfileLinker(hljsResultMock);
+ expect(output).toBe(outputValue);
+ });
+});
diff --git a/spec/frontend/vue_shared/components/source_viewer/plugins/utils/gemspec_linker_spec.js b/spec/frontend/vue_shared/components/source_viewer/plugins/utils/gemspec_linker_spec.js
index 3f74bfa117f..4b104b0bf43 100644
--- a/spec/frontend/vue_shared/components/source_viewer/plugins/utils/gemspec_linker_spec.js
+++ b/spec/frontend/vue_shared/components/source_viewer/plugins/utils/gemspec_linker_spec.js
@@ -5,7 +5,7 @@ describe('Highlight.js plugin for linking gemspec dependencies', () => {
const inputValue =
's.add_dependency(<span class="hljs-string">&#x27;rugged&#x27;</span>, <span class="hljs-string">&#x27;~&gt; 0.24.0&#x27;</span>)';
const outputValue =
- 's.add_dependency(<span class="hljs-string linked">&#x27;<a href="https://rubygems.org/gems/rugged" rel="nofollow noreferrer noopener">rugged</a>&#x27;</span>, <span class="hljs-string">&#x27;~&gt; 0.24.0&#x27;</span>)';
+ 's.add_dependency(<span class="hljs-string linked">&#x27;<a href="https://rubygems.org/gems/rugged" target="_blank" rel="nofollow noreferrer noopener">rugged</a>&#x27;</span>, <span class="hljs-string">&#x27;~&gt; 0.24.0&#x27;</span>)';
const hljsResultMock = { value: inputValue };
const output = gemspecLinker(hljsResultMock);
diff --git a/spec/frontend/vue_shared/components/source_viewer/plugins/utils/godeps_json_linker_spec.js b/spec/frontend/vue_shared/components/source_viewer/plugins/utils/godeps_json_linker_spec.js
new file mode 100644
index 00000000000..ea7e3936846
--- /dev/null
+++ b/spec/frontend/vue_shared/components/source_viewer/plugins/utils/godeps_json_linker_spec.js
@@ -0,0 +1,27 @@
+import godepsJsonLinker from '~/vue_shared/components/source_viewer/plugins/utils/godeps_json_linker';
+
+const getInputValue = (dependencyString) =>
+ `<span class="hljs-attr">&quot;ImportPath&quot;</span><span class="hljs-punctuation">:</span><span class=""> </span><span class="hljs-string">&quot;${dependencyString}&quot;</span>`;
+const getOutputValue = (dependencyString, expectedHref) =>
+ `<span class="hljs-attr">&quot;ImportPath&quot;</span><span class="hljs-punctuation">:</span><span class=""> </span><span class="hljs-attr">&quot;<a href="${expectedHref}" target="_blank" rel="nofollow noreferrer noopener">${dependencyString}</a>&quot;</span>`;
+
+describe('Highlight.js plugin for linking Godeps.json dependencies', () => {
+ it.each`
+ dependency | expectedHref
+ ${'gitlab.com/group/project/path'} | ${'https://gitlab.com/group/project/_/tree/master/path'}
+ ${'gitlab.com/group/subgroup/project.git/path'} | ${'https://gitlab.com/group/subgroup/_/tree/master/project.git/path'}
+ ${'github.com/docker/docker/pkg/homedir'} | ${'https://github.com/docker/docker/tree/master/pkg/homedir'}
+ ${'golang.org/x/net/http2'} | ${'https://godoc.org/golang.org/x/net/http2'}
+ ${'gopkg.in/yaml.v1'} | ${'https://gopkg.in/yaml.v1'}
+ `(
+ 'mutates the input value by wrapping dependency names in anchors and altering path when needed',
+ ({ dependency, expectedHref }) => {
+ const inputValue = getInputValue(dependency);
+ const outputValue = getOutputValue(dependency, expectedHref);
+ const hljsResultMock = { value: inputValue };
+
+ const output = godepsJsonLinker(hljsResultMock);
+ expect(output).toBe(outputValue);
+ },
+ );
+});
diff --git a/spec/frontend/vue_shared/components/source_viewer/plugins/utils/package_json_linker_spec.js b/spec/frontend/vue_shared/components/source_viewer/plugins/utils/package_json_linker_spec.js
index e83c129818c..170a44f8ee2 100644
--- a/spec/frontend/vue_shared/components/source_viewer/plugins/utils/package_json_linker_spec.js
+++ b/spec/frontend/vue_shared/components/source_viewer/plugins/utils/package_json_linker_spec.js
@@ -6,7 +6,7 @@ describe('Highlight.js plugin for linking package.json dependencies', () => {
const inputValue =
'<span class="hljs-attr">&quot;@babel/core&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;^7.18.5&quot;</span>';
const outputValue =
- '<span class="hljs-attr">&quot;<a href="https://npmjs.com/package/@babel/core" rel="nofollow noreferrer noopener">@babel/core</a>&quot;</span>: <span class="hljs-attr">&quot;<a href="https://npmjs.com/package/@babel/core" rel="nofollow noreferrer noopener">^7.18.5</a>&quot;</span>';
+ '<span class="hljs-attr">&quot;<a href="https://npmjs.com/package/@babel/core" target="_blank" rel="nofollow noreferrer noopener">@babel/core</a>&quot;</span>: <span class="hljs-attr">&quot;<a href="https://npmjs.com/package/@babel/core" target="_blank" rel="nofollow noreferrer noopener">^7.18.5</a>&quot;</span>';
const hljsResultMock = { value: inputValue };
const output = packageJsonLinker(hljsResultMock, PACKAGE_JSON_CONTENT);
diff --git a/spec/frontend/vue_shared/components/source_viewer/plugins/utils/podspec_json_linker_spec.js b/spec/frontend/vue_shared/components/source_viewer/plugins/utils/podspec_json_linker_spec.js
new file mode 100644
index 00000000000..0ef63de68c6
--- /dev/null
+++ b/spec/frontend/vue_shared/components/source_viewer/plugins/utils/podspec_json_linker_spec.js
@@ -0,0 +1,14 @@
+import podspecJsonLinker from '~/vue_shared/components/source_viewer/plugins/utils/podspec_json_linker';
+import { PODSPEC_JSON_CONTENT } from '../mock_data';
+
+describe('Highlight.js plugin for linking podspec_json dependencies', () => {
+ it('mutates the input value by wrapping dependency names in anchors', () => {
+ const inputValue =
+ '<span class="hljs-attr">&quot;AFNetworking/Security&quot;</span><span class="hljs-punctuation">:</span><span class=""> </span><span class="hljs-punctuation">[';
+ const outputValue =
+ '<span class="hljs-attr">&quot;<a href="https://cocoapods.org/pods/AFNetworking" target="_blank" rel="nofollow noreferrer noopener">AFNetworking/Security</a>&quot;</span><span class="hljs-punctuation">:</span><span class=""> </span><span class="hljs-punctuation">[';
+ const hljsResultMock = { value: inputValue };
+ const output = podspecJsonLinker(hljsResultMock, PODSPEC_JSON_CONTENT);
+ expect(output).toBe(outputValue);
+ });
+});
diff --git a/spec/frontend/vue_shared/components/source_viewer/plugins/wrap_child_nodes_spec.js b/spec/frontend/vue_shared/components/source_viewer/plugins/wrap_child_nodes_spec.js
index bc6df1a2565..8d072c8c8de 100644
--- a/spec/frontend/vue_shared/components/source_viewer/plugins/wrap_child_nodes_spec.js
+++ b/spec/frontend/vue_shared/components/source_viewer/plugins/wrap_child_nodes_spec.js
@@ -8,13 +8,14 @@ describe('Highlight.js plugin for wrapping _emitter nodes', () => {
children: [
{ kind: 'string', children: ['Text 1'] },
{ kind: 'string', children: ['Text 2', { kind: 'comment', children: ['Text 3'] }] },
+ { kind: undefined, sublanguage: true, children: ['Text 3 (sublanguage)'] },
'Text4\nText5',
],
},
},
};
- const outputValue = `<span class="hljs-string">Text 1</span><span class="hljs-string"><span class="hljs-string">Text 2</span><span class="hljs-comment">Text 3</span></span><span class="">Text4</span>\n<span class="">Text5</span>`;
+ const outputValue = `<span class="hljs-string">Text 1</span><span class="hljs-string"><span class="hljs-string">Text 2</span><span class="hljs-comment">Text 3</span></span><span class="">Text 3 (sublanguage)</span><span class="">Text4</span>\n<span class="">Text5</span>`;
wrapChildNodes(hljsResultMock);
expect(hljsResultMock.value).toBe(outputValue);
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 6d319b37b02..33f370efdfa 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
@@ -10,6 +10,7 @@ import {
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';
@@ -121,6 +122,7 @@ describe('Source Viewer component', () => {
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', () => {
@@ -133,45 +135,27 @@ describe('Source Viewer component', () => {
});
describe('rendering', () => {
- it('renders the first chunk', async () => {
- const firstChunk = findChunks().at(0);
-
- expect(firstChunk.props('content')).toContain(chunk1);
-
- expect(firstChunk.props()).toMatchObject({
- totalLines: 70,
- startingFrom: 0,
+ 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('renders the second chunk', async () => {
- const secondChunk = findChunks().at(1);
-
- expect(secondChunk.props('content')).toContain(chunk2.trim());
-
- expect(secondChunk.props()).toMatchObject({
- totalLines: 70,
- startingFrom: 70,
- });
+ it('emits showBlobInteractionZones on the eventHub when chunk appears', () => {
+ findChunks().at(0).vm.$emit('appear');
+ expect(eventHub.$emit).toHaveBeenCalledWith('showBlobInteractionZones', path);
});
-
- it('renders the third chunk', async () => {
- const thirdChunk = findChunks().at(2);
-
- expect(thirdChunk.props('content')).toContain(chunk3Result.trim());
-
- expect(chunk3Result).toEqual(chunk3.replace(/\r?\n/g, '\n'));
-
- expect(thirdChunk.props()).toMatchObject({
- totalLines: 70,
- startingFrom: 140,
- });
- });
- });
-
- it('emits showBlobInteractionZones on the eventHub when chunk appears', () => {
- findChunks().at(0).vm.$emit('appear');
- expect(eventHub.$emit).toHaveBeenCalledWith('showBlobInteractionZones', path);
});
describe('LineHighlighter', () => {