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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-05-23 03:07:55 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-05-23 03:07:55 +0300
commit82f96a9ae2529898de0e91ccfad1d6457f3c1975 (patch)
tree7e189a0d2cef7df67aaa4f709e3f5bc312878cbd /spec/frontend
parentc50e042a392687730db9b8c2607883485b258ae4 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec/frontend')
-rw-r--r--spec/frontend/__helpers__/mock_window_location_helper.js1
-rw-r--r--spec/frontend/api/user_api_spec.js19
-rw-r--r--spec/frontend/content_editor/components/bubble_menus/bubble_menu_spec.js1
-rw-r--r--spec/frontend/content_editor/components/bubble_menus/reference_bubble_menu_spec.js247
-rw-r--r--spec/frontend/content_editor/components/content_editor_spec.js4
-rw-r--r--spec/frontend/content_editor/extensions/drawio_diagram_spec.js15
-rw-r--r--spec/frontend/content_editor/extensions/reference_spec.js162
-rw-r--r--spec/frontend/content_editor/services/asset_resolver_spec.js68
-rw-r--r--spec/frontend/content_editor/services/create_content_editor_spec.js3
-rw-r--r--spec/frontend/content_editor/test_constants.js9
-rw-r--r--spec/frontend/content_editor/test_utils.js16
-rw-r--r--spec/frontend/fixtures/users.rb18
-rw-r--r--spec/frontend/lib/utils/url_utility_spec.js14
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_form_spec.js4
-rw-r--r--spec/frontend/profile/components/follow_spec.js99
-rw-r--r--spec/frontend/profile/components/followers_tab_spec.js119
-rw-r--r--spec/frontend/profile/components/following_tab_spec.js2
17 files changed, 764 insertions, 37 deletions
diff --git a/spec/frontend/__helpers__/mock_window_location_helper.js b/spec/frontend/__helpers__/mock_window_location_helper.js
index de1e8c99b54..48bc788afaa 100644
--- a/spec/frontend/__helpers__/mock_window_location_helper.js
+++ b/spec/frontend/__helpers__/mock_window_location_helper.js
@@ -12,6 +12,7 @@ const useMockLocation = (fn) => {
Object.defineProperty(window, 'location', {
get: () => currentWindowLocation,
+ assign: jest.fn(),
});
beforeEach(() => {
diff --git a/spec/frontend/api/user_api_spec.js b/spec/frontend/api/user_api_spec.js
index a879c229581..b2ecfeb8394 100644
--- a/spec/frontend/api/user_api_spec.js
+++ b/spec/frontend/api/user_api_spec.js
@@ -1,12 +1,14 @@
import MockAdapter from 'axios-mock-adapter';
import projects from 'test_fixtures/api/users/projects/get.json';
+import followers from 'test_fixtures/api/users/followers/get.json';
import {
followUser,
unfollowUser,
associationsCount,
updateUserStatus,
getUserProjects,
+ getUserFollowers,
} from '~/api/user_api';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
@@ -16,6 +18,7 @@ import {
} from 'jest/admin/users/mock_data';
import { AVAILABILITY_STATUS } from '~/set_status_modal/constants';
import { timeRanges } from '~/vue_shared/constants';
+import { DEFAULT_PER_PAGE } from '~/api';
describe('~/api/user_api', () => {
let axiosMock;
@@ -112,4 +115,20 @@ describe('~/api/user_api', () => {
expect(axiosMock.history.get[0].url).toBe(expectedUrl);
});
});
+
+ describe('getUserFollowers', () => {
+ it('calls correct URL and returns expected response', async () => {
+ const expectedUrl = '/api/v4/users/1/followers';
+ const expectedResponse = { data: followers };
+ const params = { page: 2 };
+
+ axiosMock.onGet(expectedUrl).replyOnce(HTTP_STATUS_OK, expectedResponse);
+
+ await expect(getUserFollowers(1, params)).resolves.toEqual(
+ expect.objectContaining({ data: expectedResponse }),
+ );
+ expect(axiosMock.history.get[0].url).toBe(expectedUrl);
+ expect(axiosMock.history.get[0].params).toEqual({ ...params, per_page: DEFAULT_PER_PAGE });
+ });
+ });
});
diff --git a/spec/frontend/content_editor/components/bubble_menus/bubble_menu_spec.js b/spec/frontend/content_editor/components/bubble_menus/bubble_menu_spec.js
index 97716ce848c..309e5f76b9c 100644
--- a/spec/frontend/content_editor/components/bubble_menus/bubble_menu_spec.js
+++ b/spec/frontend/content_editor/components/bubble_menus/bubble_menu_spec.js
@@ -65,6 +65,7 @@ describe('content_editor/components/bubble_menus/bubble_menu', () => {
onHidden: expect.any(Function),
onShow: expect.any(Function),
appendTo: expect.any(Function),
+ maxWidth: 'auto',
...tippyOptions,
}),
});
diff --git a/spec/frontend/content_editor/components/bubble_menus/reference_bubble_menu_spec.js b/spec/frontend/content_editor/components/bubble_menus/reference_bubble_menu_spec.js
new file mode 100644
index 00000000000..169f77dc054
--- /dev/null
+++ b/spec/frontend/content_editor/components/bubble_menus/reference_bubble_menu_spec.js
@@ -0,0 +1,247 @@
+import { GlLoadingIcon, GlListboxItem, GlCollapsibleListbox } from '@gitlab/ui';
+import { nextTick } from 'vue';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import eventHubFactory from '~/helpers/event_hub_factory';
+import ReferenceBubbleMenu from '~/content_editor/components/bubble_menus/reference_bubble_menu.vue';
+import BubbleMenu from '~/content_editor/components/bubble_menus/bubble_menu.vue';
+import { stubComponent } from 'helpers/stub_component';
+import Reference from '~/content_editor/extensions/reference';
+import { createTestEditor, emitEditorEvent, createDocBuilder } from '../../test_utils';
+
+const mockIssue = {
+ href: 'https://gitlab.com/gitlab-org/gitlab-test/-/issues/24',
+ text: '#24',
+ expandedText: 'Et fuga quos omnis enim dolores amet impedit. (#24)',
+ fullyExpandedText:
+ 'Et fuga quos omnis enim dolores amet impedit. (#24) • Fernanda Adams • Sprint - Eligendi quas non inventore eum quaerat sit.',
+};
+const mockMergeRequest = {
+ href: 'https://gitlab.com/gitlab-org/gitlab-test/-/merge_requests/2',
+ text: '!2',
+ expandedText: 'Qui possimus sit harum ut ipsam autem. (!2)',
+ fullyExpandedText: 'Qui possimus sit harum ut ipsam autem. (!2) • Margrett Wunsch • v0.0',
+};
+const mockEpic = {
+ href: 'https://gitlab.com/groups/gitlab-org/-/epics/5',
+ text: '&5',
+ expandedText: 'Temporibus delectus distinctio quas sed non per... (&5)',
+};
+
+const supportedIssueDisplayFormats = ['Issue ID', 'Issue title', 'Issue summary'];
+
+const supportedMergeRequestDisplayFormats = [
+ 'Merge request ID',
+ 'Merge request title',
+ 'Merge request summary',
+];
+
+const supportedEpicDisplayFormats = ['Epic ID', 'Epic title'];
+
+describe('content_editor/components/bubble_menus/reference_bubble_menu', () => {
+ let wrapper;
+ let tiptapEditor;
+ let contentEditor;
+ let eventHub;
+ let doc;
+ let p;
+ let reference;
+
+ const buildExpectedDoc = (href, originalText, referenceType, text) =>
+ doc(p(reference({ className: 'gfm', href, originalText, referenceType, text })));
+
+ const buildEditor = () => {
+ tiptapEditor = createTestEditor({ extensions: [Reference] });
+ contentEditor = { resolveReference: jest.fn().mockImplementation(() => new Promise(() => {})) };
+ eventHub = eventHubFactory();
+
+ ({
+ builders: { doc, p, reference },
+ } = createDocBuilder({
+ tiptapEditor,
+ names: {
+ reference: { nodeType: Reference.name },
+ },
+ }));
+ };
+
+ const expectedDocs = {
+ issue: [
+ () =>
+ buildExpectedDoc(
+ 'https://gitlab.com/gitlab-org/gitlab-test/-/issues/24',
+ '#24',
+ 'issue',
+ '#24',
+ ),
+ () =>
+ buildExpectedDoc(
+ 'https://gitlab.com/gitlab-org/gitlab-test/-/issues/24',
+ '#24+',
+ 'issue',
+ 'Et fuga quos omnis enim dolores amet impedit. (#24)',
+ ),
+ () =>
+ buildExpectedDoc(
+ 'https://gitlab.com/gitlab-org/gitlab-test/-/issues/24',
+ '#24+s',
+ 'issue',
+ 'Et fuga quos omnis enim dolores amet impedit. (#24) • Fernanda Adams • Sprint - Eligendi quas non inventore eum quaerat sit.',
+ ),
+ ],
+ merge_request: [
+ () =>
+ buildExpectedDoc(
+ 'https://gitlab.com/gitlab-org/gitlab-test/-/merge_requests/2',
+ '!2',
+ 'merge_request',
+ '!2',
+ ),
+ () =>
+ buildExpectedDoc(
+ 'https://gitlab.com/gitlab-org/gitlab-test/-/merge_requests/2',
+ '!2+',
+ 'merge_request',
+ 'Qui possimus sit harum ut ipsam autem. (!2)',
+ ),
+ () =>
+ buildExpectedDoc(
+ 'https://gitlab.com/gitlab-org/gitlab-test/-/merge_requests/2',
+ '!2+s',
+ 'merge_request',
+ 'Qui possimus sit harum ut ipsam autem. (!2) • Margrett Wunsch • v0.0',
+ ),
+ ],
+ epic: [
+ () => buildExpectedDoc('https://gitlab.com/groups/gitlab-org/-/epics/5', '&5', 'epic', '&5'),
+ () =>
+ buildExpectedDoc(
+ 'https://gitlab.com/groups/gitlab-org/-/epics/5',
+ '&5+',
+ 'epic',
+ 'Temporibus delectus distinctio quas sed non per... (&5)',
+ ),
+ ],
+ };
+
+ const buildWrapper = () => {
+ wrapper = mountExtended(ReferenceBubbleMenu, {
+ provide: {
+ tiptapEditor,
+ contentEditor,
+ eventHub,
+ },
+ stubs: {
+ BubbleMenu: stubComponent(BubbleMenu),
+ },
+ });
+ };
+
+ const showMenu = () => {
+ wrapper.findComponent(BubbleMenu).vm.$emit('show');
+ return nextTick();
+ };
+
+ const buildWrapperAndDisplayMenu = async () => {
+ buildWrapper();
+
+ await showMenu();
+ await emitEditorEvent({ event: 'transaction', tiptapEditor });
+ };
+
+ beforeEach(() => {
+ buildEditor();
+
+ tiptapEditor
+ .chain()
+ .setContent(
+ '<a href="https://gitlab.com/gitlab-org/gitlab/issues/1" class="gfm" data-reference-type="issue" data-original="#1">#1</a>',
+ )
+ .setNodeSelection(1)
+ .run();
+ });
+
+ it('shows a loading indicator while the reference is being resolved', async () => {
+ await buildWrapperAndDisplayMenu();
+
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
+ });
+
+ describe.each`
+ referenceType | mockReference | supportedDisplayFormats
+ ${'issue'} | ${mockIssue} | ${supportedIssueDisplayFormats}
+ ${'merge_request'} | ${mockMergeRequest} | ${supportedMergeRequestDisplayFormats}
+ ${'epic'} | ${mockEpic} | ${supportedEpicDisplayFormats}
+ `(
+ 'for reference type $referenceType',
+ ({ referenceType, mockReference, supportedDisplayFormats }) => {
+ beforeEach(async () => {
+ tiptapEditor
+ .chain()
+ .setContent(
+ `<a href="${mockReference.href}" class="gfm" data-reference-type="${referenceType}" data-original="${mockReference.text}">${mockReference.text}</a>`,
+ )
+ .setNodeSelection(1)
+ .run();
+
+ contentEditor.resolveReference.mockImplementation(() => Promise.resolve(mockReference));
+ await emitEditorEvent({ event: 'transaction', tiptapEditor });
+ });
+
+ it('shows a dropdown with supported display formats', async () => {
+ await buildWrapperAndDisplayMenu();
+
+ supportedDisplayFormats.forEach((format) => expect(wrapper.text()).toContain(format));
+ });
+
+ describe.each`
+ option | displayFormat | selectedValue
+ ${0} | ${supportedDisplayFormats[0]} | ${''}
+ ${1} | ${supportedDisplayFormats[1]} | ${'+'}
+ ${2} | ${supportedDisplayFormats[2]} | ${'+s'}
+ `('on selecting option $option', ({ option, displayFormat, selectedValue }) => {
+ if (!displayFormat) return;
+
+ const findDropdownItem = () => wrapper.findAllComponents(GlListboxItem).at(option);
+
+ beforeEach(async () => {
+ await buildWrapperAndDisplayMenu();
+
+ findDropdownItem().trigger('click');
+ });
+
+ it('selects the option', () => {
+ expect(wrapper.findComponent(GlCollapsibleListbox).props()).toMatchObject({
+ selected: selectedValue,
+ toggleText: displayFormat,
+ });
+ });
+
+ it('updates the reference in content editor', () => {
+ expect(tiptapEditor.getJSON()).toEqual(expectedDocs[referenceType][option]().toJSON());
+ });
+ });
+ },
+ );
+
+ describe('copy URL button', () => {
+ it('copies the reference link to clipboard', async () => {
+ jest.spyOn(navigator.clipboard, 'writeText');
+
+ await buildWrapperAndDisplayMenu();
+ await wrapper.findByTestId('copy-reference-url').trigger('click');
+
+ expect(navigator.clipboard.writeText).toHaveBeenCalledWith(
+ 'https://gitlab.com/gitlab-org/gitlab/issues/1',
+ );
+ });
+ });
+
+ describe('remove reference button', () => {
+ it('removes the reference', async () => {
+ await buildWrapperAndDisplayMenu();
+ await wrapper.findByTestId('remove-reference').trigger('click');
+
+ expect(tiptapEditor.getHTML()).toBe('<p></p>');
+ });
+ });
+});
diff --git a/spec/frontend/content_editor/components/content_editor_spec.js b/spec/frontend/content_editor/components/content_editor_spec.js
index 852c8a9591a..44dd328025a 100644
--- a/spec/frontend/content_editor/components/content_editor_spec.js
+++ b/spec/frontend/content_editor/components/content_editor_spec.js
@@ -9,6 +9,7 @@ import EditorStateObserver from '~/content_editor/components/editor_state_observ
import CodeBlockBubbleMenu from '~/content_editor/components/bubble_menus/code_block_bubble_menu.vue';
import LinkBubbleMenu from '~/content_editor/components/bubble_menus/link_bubble_menu.vue';
import MediaBubbleMenu from '~/content_editor/components/bubble_menus/media_bubble_menu.vue';
+import ReferenceBubbleMenu from '~/content_editor/components/bubble_menus/reference_bubble_menu.vue';
import FormattingToolbar from '~/content_editor/components/formatting_toolbar.vue';
import LoadingIndicator from '~/content_editor/components/loading_indicator.vue';
import waitForPromises from 'helpers/wait_for_promises';
@@ -267,7 +268,8 @@ describe('ContentEditor', () => {
${'link'} | ${LinkBubbleMenu}
${'media'} | ${MediaBubbleMenu}
${'codeBlock'} | ${CodeBlockBubbleMenu}
- `('renders formatting bubble menu', ({ component }) => {
+ ${'reference'} | ${ReferenceBubbleMenu}
+ `('renders $name bubble menu', ({ component }) => {
createWrapper();
expect(wrapper.findComponent(component).exists()).toBe(true);
diff --git a/spec/frontend/content_editor/extensions/drawio_diagram_spec.js b/spec/frontend/content_editor/extensions/drawio_diagram_spec.js
index 61dc164c99a..63ed08096b2 100644
--- a/spec/frontend/content_editor/extensions/drawio_diagram_spec.js
+++ b/spec/frontend/content_editor/extensions/drawio_diagram_spec.js
@@ -1,6 +1,5 @@
import DrawioDiagram from '~/content_editor/extensions/drawio_diagram';
import Image from '~/content_editor/extensions/image';
-import createAssetResolver from '~/content_editor/services/asset_resolver';
import { create } from '~/drawio/content_editor_facade';
import { launchDrawioEditor } from '~/drawio/drawio_editor';
import { createTestEditor, createDocBuilder } from '../test_utils';
@@ -19,12 +18,15 @@ describe('content_editor/extensions/drawio_diagram', () => {
let paragraph;
let image;
let drawioDiagram;
+ let assetResolver;
+
const uploadsPath = '/uploads';
- const renderMarkdown = () => {};
beforeEach(() => {
+ assetResolver = new (class {})();
+
tiptapEditor = createTestEditor({
- extensions: [Image, DrawioDiagram.configure({ uploadsPath, renderMarkdown })],
+ extensions: [Image, DrawioDiagram.configure({ uploadsPath, assetResolver })],
});
const { builders } = createDocBuilder({
tiptapEditor,
@@ -72,19 +74,12 @@ describe('content_editor/extensions/drawio_diagram', () => {
describe('createOrEditDiagram command', () => {
let editorFacade;
- let assetResolver;
beforeEach(() => {
editorFacade = {};
- assetResolver = {};
tiptapEditor.commands.createOrEditDiagram();
create.mockReturnValueOnce(editorFacade);
- createAssetResolver.mockReturnValueOnce(assetResolver);
- });
-
- it('creates a new instance of asset resolver', () => {
- expect(createAssetResolver).toHaveBeenCalledWith({ renderMarkdown });
});
it('creates a new instance of the content_editor_facade', () => {
diff --git a/spec/frontend/content_editor/extensions/reference_spec.js b/spec/frontend/content_editor/extensions/reference_spec.js
new file mode 100644
index 00000000000..c25c7c41d75
--- /dev/null
+++ b/spec/frontend/content_editor/extensions/reference_spec.js
@@ -0,0 +1,162 @@
+import Reference from '~/content_editor/extensions/reference';
+import AssetResolver from '~/content_editor/services/asset_resolver';
+import {
+ RESOLVED_ISSUE_HTML,
+ RESOLVED_MERGE_REQUEST_HTML,
+ RESOLVED_EPIC_HTML,
+} from '../test_constants';
+import {
+ createTestEditor,
+ createDocBuilder,
+ triggerNodeInputRule,
+ waitUntilTransaction,
+} from '../test_utils';
+
+describe('content_editor/extensions/reference', () => {
+ let tiptapEditor;
+ let doc;
+ let p;
+ let reference;
+ let renderMarkdown;
+ let assetResolver;
+
+ beforeEach(() => {
+ renderMarkdown = jest.fn().mockImplementation(() => new Promise(() => {}));
+ assetResolver = new AssetResolver({ renderMarkdown });
+
+ tiptapEditor = createTestEditor({
+ extensions: [Reference.configure({ assetResolver })],
+ });
+
+ ({
+ builders: { doc, p, reference },
+ } = createDocBuilder({
+ tiptapEditor,
+ names: {
+ reference: { nodeType: Reference.name },
+ },
+ }));
+ });
+
+ describe('when typing a valid reference input rule', () => {
+ const buildExpectedDoc = (href, originalText, referenceType, text) =>
+ doc(p(reference({ className: null, href, originalText, referenceType, text }), ' '));
+
+ it.each`
+ inputRuleText | mockReferenceHtml | expectedDoc
+ ${'#1 '} | ${RESOLVED_ISSUE_HTML} | ${() => buildExpectedDoc('/gitlab-org/gitlab/-/issues/1', '#1', 'issue', '#1 (closed)')}
+ ${'#1+ '} | ${RESOLVED_ISSUE_HTML} | ${() => buildExpectedDoc('/gitlab-org/gitlab/-/issues/1', '#1+', 'issue', '500 error on MR approvers edit page (#1 - closed)')}
+ ${'#1+s '} | ${RESOLVED_ISSUE_HTML} | ${() => buildExpectedDoc('/gitlab-org/gitlab/-/issues/1', '#1+s', 'issue', '500 error on MR approvers edit page (#1 - closed) • Unassigned')}
+ ${'!1 '} | ${RESOLVED_MERGE_REQUEST_HTML} | ${() => buildExpectedDoc('/gitlab-org/gitlab/-/merge_requests/1', '!1', 'merge_request', '!1 (merged)')}
+ ${'!1+ '} | ${RESOLVED_MERGE_REQUEST_HTML} | ${() => buildExpectedDoc('/gitlab-org/gitlab/-/merge_requests/1', '!1+', 'merge_request', 'Enhance the LDAP group synchronization (!1 - merged)')}
+ ${'!1+s '} | ${RESOLVED_MERGE_REQUEST_HTML} | ${() => buildExpectedDoc('/gitlab-org/gitlab/-/merge_requests/1', '!1+s', 'merge_request', 'Enhance the LDAP group synchronization (!1 - merged) • John Doe')}
+ ${'&1 '} | ${RESOLVED_EPIC_HTML} | ${() => buildExpectedDoc('/groups/gitlab-org/-/epics/1', '&1', 'epic', '&1')}
+ ${'&1+ '} | ${RESOLVED_EPIC_HTML} | ${() => buildExpectedDoc('/groups/gitlab-org/-/epics/1', '&1+', 'epic', 'Approvals in merge request list (&1)')}
+ `(
+ 'replaces the input rule ($inputRuleText) with a reference node',
+ async ({ inputRuleText, mockReferenceHtml, expectedDoc }) => {
+ await waitUntilTransaction({
+ number: 2,
+ tiptapEditor,
+ action() {
+ renderMarkdown.mockResolvedValueOnce(mockReferenceHtml);
+
+ tiptapEditor.commands.insertContent({ type: 'text', text: inputRuleText });
+ triggerNodeInputRule({ tiptapEditor, inputRuleText });
+ },
+ });
+
+ expect(tiptapEditor.getJSON()).toEqual(expectedDoc().toJSON());
+ },
+ );
+
+ it('resolves multiple references in the same paragraph correctly', async () => {
+ await waitUntilTransaction({
+ number: 2,
+ tiptapEditor,
+ action() {
+ renderMarkdown.mockResolvedValueOnce(RESOLVED_ISSUE_HTML);
+
+ tiptapEditor.commands.insertContent({ type: 'text', text: '#1+ ' });
+ triggerNodeInputRule({ tiptapEditor, inputRuleText: '#1+ ' });
+ },
+ });
+
+ await waitUntilTransaction({
+ number: 2,
+ tiptapEditor,
+ action() {
+ renderMarkdown.mockResolvedValueOnce(RESOLVED_MERGE_REQUEST_HTML);
+
+ tiptapEditor.commands.insertContent({ type: 'text', text: 'was resolved with !1+ ' });
+ triggerNodeInputRule({ tiptapEditor, inputRuleText: 'was resolved with !1+ ' });
+ },
+ });
+
+ expect(tiptapEditor.getJSON()).toEqual(
+ doc(
+ p(
+ reference({
+ referenceType: 'issue',
+ originalText: '#1+',
+ text: '500 error on MR approvers edit page (#1 - closed)',
+ href: '/gitlab-org/gitlab/-/issues/1',
+ }),
+ ' was resolved with ',
+ reference({
+ referenceType: 'merge_request',
+ originalText: '!1+',
+ text: 'Enhance the LDAP group synchronization (!1 - merged)',
+ href: '/gitlab-org/gitlab/-/merge_requests/1',
+ }),
+ ' ',
+ ),
+ ).toJSON(),
+ );
+ });
+
+ it('resolves the input rule lazily in the correct position if the user makes a change before the request resolves', async () => {
+ let resolvePromise;
+ const promise = new Promise((resolve) => {
+ resolvePromise = resolve;
+ });
+
+ renderMarkdown.mockImplementation(() => promise);
+
+ tiptapEditor.commands.insertContent({ type: 'text', text: '#1+ ' });
+ triggerNodeInputRule({ tiptapEditor, inputRuleText: '#1+ ' });
+
+ // insert a new paragraph at a random location
+ tiptapEditor.commands.insertContentAt(0, {
+ type: 'paragraph',
+ content: [{ type: 'text', text: 'Hello' }],
+ });
+
+ // update selection
+ tiptapEditor.commands.selectAll();
+
+ await waitUntilTransaction({
+ number: 1,
+ tiptapEditor,
+ action() {
+ resolvePromise(RESOLVED_ISSUE_HTML);
+ },
+ });
+
+ expect(tiptapEditor.state.doc).toEqual(
+ doc(
+ p('Hello'),
+ p(
+ reference({
+ referenceType: 'issue',
+ originalText: '#1+',
+ text: '500 error on MR approvers edit page (#1 - closed)',
+ href: '/gitlab-org/gitlab/-/issues/1',
+ }),
+ ' ',
+ ),
+ ),
+ );
+ });
+ });
+});
diff --git a/spec/frontend/content_editor/services/asset_resolver_spec.js b/spec/frontend/content_editor/services/asset_resolver_spec.js
index 0a99f823be3..292eec6db77 100644
--- a/spec/frontend/content_editor/services/asset_resolver_spec.js
+++ b/spec/frontend/content_editor/services/asset_resolver_spec.js
@@ -1,4 +1,9 @@
-import createAssetResolver from '~/content_editor/services/asset_resolver';
+import AssetResolver from '~/content_editor/services/asset_resolver';
+import {
+ RESOLVED_ISSUE_HTML,
+ RESOLVED_MERGE_REQUEST_HTML,
+ RESOLVED_EPIC_HTML,
+} from '../test_constants';
describe('content_editor/services/asset_resolver', () => {
let renderMarkdown;
@@ -6,7 +11,7 @@ describe('content_editor/services/asset_resolver', () => {
beforeEach(() => {
renderMarkdown = jest.fn();
- assetResolver = createAssetResolver({ renderMarkdown });
+ assetResolver = new AssetResolver({ renderMarkdown });
});
describe('resolveUrl', () => {
@@ -21,6 +26,65 @@ describe('content_editor/services/asset_resolver', () => {
});
});
+ describe('resolveReference', () => {
+ const resolvedEpic = {
+ expandedText: 'Approvals in merge request list (&1)',
+ fullyExpandedText: 'Approvals in merge request list (&1)',
+ href: '/groups/gitlab-org/-/epics/1',
+ text: '&1',
+ };
+
+ const resolvedIssue = {
+ expandedText: '500 error on MR approvers edit page (#1 - closed)',
+ fullyExpandedText: '500 error on MR approvers edit page (#1 - closed) • Unassigned',
+ href: '/gitlab-org/gitlab/-/issues/1',
+ text: '#1 (closed)',
+ };
+
+ const resolvedMergeRequest = {
+ expandedText: 'Enhance the LDAP group synchronization (!1 - merged)',
+ fullyExpandedText: 'Enhance the LDAP group synchronization (!1 - merged) • John Doe',
+ href: '/gitlab-org/gitlab/-/merge_requests/1',
+ text: '!1 (merged)',
+ };
+
+ describe.each`
+ referenceType | referenceId | sentMarkdown | returnedHtml | resolvedReference
+ ${'issue'} | ${'#1'} | ${'#1 #1+ #1+s'} | ${RESOLVED_ISSUE_HTML} | ${resolvedIssue}
+ ${'merge_request'} | ${'!1'} | ${'!1 !1+ !1+s'} | ${RESOLVED_MERGE_REQUEST_HTML} | ${resolvedMergeRequest}
+ ${'epic'} | ${'&1'} | ${'&1 &1+ &1+s'} | ${RESOLVED_EPIC_HTML} | ${resolvedEpic}
+ `(
+ 'for reference type $referenceType',
+ ({ referenceType, referenceId, sentMarkdown, returnedHtml, resolvedReference }) => {
+ it(`resolves ${referenceType} reference to href, text, title and summary`, async () => {
+ renderMarkdown.mockResolvedValue(returnedHtml);
+
+ expect(await assetResolver.resolveReference(referenceId)).toEqual(resolvedReference);
+ });
+
+ it.each`
+ suffix
+ ${''}
+ ${'+'}
+ ${'+s'}
+ `('strips suffix ("$suffix") before resolving', ({ suffix }) => {
+ assetResolver.resolveReference(referenceId + suffix);
+ expect(renderMarkdown).toHaveBeenCalledWith(sentMarkdown);
+ });
+ },
+ );
+
+ it.each`
+ case | sentMarkdown | returnedHtml
+ ${'no html is returned'} | ${''} | ${''}
+ ${'html contains no anchor tags'} | ${'no anchor tags'} | ${'<p>no anchor tags</p>'}
+ `('returns an empty object if $case', async ({ sentMarkdown, returnedHtml }) => {
+ renderMarkdown.mockResolvedValue(returnedHtml);
+
+ expect(await assetResolver.resolveReference(sentMarkdown)).toEqual({});
+ });
+ });
+
describe('renderDiagram', () => {
it('resolves a diagram code to a url containing the diagram image', async () => {
renderMarkdown.mockResolvedValue(
diff --git a/spec/frontend/content_editor/services/create_content_editor_spec.js b/spec/frontend/content_editor/services/create_content_editor_spec.js
index 53cd51b8c5f..b9a9c3ccd17 100644
--- a/spec/frontend/content_editor/services/create_content_editor_spec.js
+++ b/spec/frontend/content_editor/services/create_content_editor_spec.js
@@ -2,6 +2,7 @@ import { PROVIDE_SERIALIZER_OR_RENDERER_ERROR } from '~/content_editor/constants
import { createContentEditor } from '~/content_editor/services/create_content_editor';
import createGlApiMarkdownDeserializer from '~/content_editor/services/gl_api_markdown_deserializer';
import createRemarkMarkdownDeserializer from '~/content_editor/services/remark_markdown_deserializer';
+import AssetResolver from '~/content_editor/services/asset_resolver';
import { createTestContentEditorExtension } from '../test_utils';
jest.mock('~/emoji');
@@ -89,7 +90,7 @@ describe('content_editor/services/create_content_editor', () => {
.options,
).toMatchObject({
uploadsPath,
- renderMarkdown,
+ assetResolver: expect.any(AssetResolver),
});
});
});
diff --git a/spec/frontend/content_editor/test_constants.js b/spec/frontend/content_editor/test_constants.js
index 749f1234de0..cbd4f555e97 100644
--- a/spec/frontend/content_editor/test_constants.js
+++ b/spec/frontend/content_editor/test_constants.js
@@ -35,3 +35,12 @@ export const PROJECT_WIKI_ATTACHMENT_DRAWIO_DIAGRAM_HTML = `<p data-sourcepos="1
export const PROJECT_WIKI_ATTACHMENT_LINK_HTML = `<p data-sourcepos="1:1-1:26" dir="auto">
<a href="/group1/project1/-/wikis/test-file.zip" data-canonical-src="test-file.zip">test-file</a>
</p>`;
+
+export const RESOLVED_ISSUE_HTML =
+ '<p data-sourcepos="1:1-1:11" dir="auto"><a href="/gitlab-org/gitlab/-/issues/1" data-reference-type="issue" data-original="#1" data-link="false" data-link-reference="false" data-project="278964" data-issue="382515" data-project-path="gitlab-org/gitlab" data-iid="1" data-issue-type="issue" data-container="body" data-placement="top" title="500 error on MR approvers edit page" class="gfm gfm-issue">#1 (closed)</a> <a href="/gitlab-org/gitlab/-/issues/1" data-reference-type="issue" data-original="#1+" data-link="false" data-link-reference="false" data-project="278964" data-issue="382515" data-project-path="gitlab-org/gitlab" data-iid="1" data-reference-format="+" data-issue-type="issue" data-container="body" data-placement="top" title="500 error on MR approvers edit page" class="gfm gfm-issue">500 error on MR approvers edit page (#1 - closed)</a> <a href="/gitlab-org/gitlab/-/issues/1" data-reference-type="issue" data-original="#1+s" data-link="false" data-link-reference="false" data-project="278964" data-issue="382515" data-project-path="gitlab-org/gitlab" data-iid="1" data-reference-format="+s" data-issue-type="issue" data-container="body" data-placement="top" title="500 error on MR approvers edit page" class="gfm gfm-issue">500 error on MR approvers edit page (#1 - closed) • Unassigned</a></p>';
+
+export const RESOLVED_MERGE_REQUEST_HTML =
+ '<p data-sourcepos="1:1-1:11" dir="auto"><a href="/gitlab-org/gitlab/-/merge_requests/1" data-reference-type="merge_request" data-original="!1" data-link="false" data-link-reference="false" data-project="278964" data-merge-request="83382" data-project-path="gitlab-org/gitlab" data-iid="1" data-container="body" data-placement="top" title="Enhance the LDAP group synchronization" class="gfm gfm-merge_request">!1 (merged)</a> <a href="/gitlab-org/gitlab/-/merge_requests/1" data-reference-type="merge_request" data-original="!1+" data-link="false" data-link-reference="false" data-project="278964" data-merge-request="83382" data-project-path="gitlab-org/gitlab" data-iid="1" data-reference-format="+" data-container="body" data-placement="top" title="Enhance the LDAP group synchronization" class="gfm gfm-merge_request">Enhance the LDAP group synchronization (!1 - merged)</a> <a href="/gitlab-org/gitlab/-/merge_requests/1" data-reference-type="merge_request" data-original="!1+s" data-link="false" data-link-reference="false" data-project="278964" data-merge-request="83382" data-project-path="gitlab-org/gitlab" data-iid="1" data-reference-format="+s" data-container="body" data-placement="top" title="Enhance the LDAP group synchronization" class="gfm gfm-merge_request">Enhance the LDAP group synchronization (!1 - merged) • John Doe</a></p>';
+
+export const RESOLVED_EPIC_HTML =
+ '<p data-sourcepos="1:1-1:11" dir="auto"><a href="/groups/gitlab-org/-/epics/1" data-reference-type="epic" data-original="&amp;amp;1" data-link="false" data-link-reference="false" data-group="9970" data-epic="1" data-container="body" data-placement="top" title="Approvals in merge request list" class="gfm gfm-epic has-tooltip">&amp;1</a> <a href="/groups/gitlab-org/-/epics/1" data-reference-type="epic" data-original="&amp;amp;1+" data-link="false" data-link-reference="false" data-group="9970" data-epic="1" data-reference-format="+" data-container="body" data-placement="top" title="Approvals in merge request list" class="gfm gfm-epic has-tooltip">Approvals in merge request list (&amp;1)</a> <a href="/groups/gitlab-org/-/epics/1" data-reference-type="epic" data-original="&amp;amp;1+s" data-link="false" data-link-reference="false" data-group="9970" data-epic="1" data-reference-format="+s" data-container="body" data-placement="top" title="Approvals in merge request list" class="gfm gfm-epic has-tooltip">Approvals in merge request list (&amp;1)</a></p>';
diff --git a/spec/frontend/content_editor/test_utils.js b/spec/frontend/content_editor/test_utils.js
index 1f4a367e46c..802ea49631f 100644
--- a/spec/frontend/content_editor/test_utils.js
+++ b/spec/frontend/content_editor/test_utils.js
@@ -212,6 +212,22 @@ export const waitUntilNextDocTransaction = ({ tiptapEditor, action = () => {} })
});
};
+export const waitUntilTransaction = ({ tiptapEditor, number, action }) => {
+ return new Promise((resolve) => {
+ let counter = 0;
+ const handleTransaction = () => {
+ counter += 1;
+ if (counter === number) {
+ tiptapEditor.off('update', handleTransaction);
+ resolve();
+ }
+ };
+
+ tiptapEditor.on('update', handleTransaction);
+ action();
+ });
+};
+
export const expectDocumentAfterTransaction = ({ tiptapEditor, number, expectedDoc, action }) => {
return new Promise((resolve) => {
let counter = 0;
diff --git a/spec/frontend/fixtures/users.rb b/spec/frontend/fixtures/users.rb
index 0e9d7475bf9..fb028a2e055 100644
--- a/spec/frontend/fixtures/users.rb
+++ b/spec/frontend/fixtures/users.rb
@@ -3,12 +3,22 @@
require 'spec_helper'
RSpec.describe 'Users (GraphQL fixtures)', feature_category: :user_profile do
+ include JavaScriptFixturesHelpers
+ include ApiHelpers
+
+ let_it_be(:followers) { create_list(:user, 5) }
+ let_it_be(:user) { create(:user, followers: followers) }
+
+ describe API::Users, '(JavaScript fixtures)', type: :request do
+ it 'api/users/followers/get.json' do
+ get api("/users/#{user.id}/followers", user)
+
+ expect(response).to be_successful
+ end
+ end
+
describe GraphQL::Query, type: :request do
- include ApiHelpers
include GraphqlHelpers
- include JavaScriptFixturesHelpers
-
- let_it_be(:user) { create(:user) }
context 'for user achievements' do
let_it_be(:group) { create(:group, :public) }
diff --git a/spec/frontend/lib/utils/url_utility_spec.js b/spec/frontend/lib/utils/url_utility_spec.js
index 4bf3a779f00..f41fe140ba1 100644
--- a/spec/frontend/lib/utils/url_utility_spec.js
+++ b/spec/frontend/lib/utils/url_utility_spec.js
@@ -406,7 +406,9 @@ describe('URL utility', () => {
Object.defineProperty(window, 'location', {
writable: true,
- value: new URL(TEST_HOST),
+ value: {
+ assign: jest.fn(),
+ },
});
});
@@ -417,11 +419,15 @@ describe('URL utility', () => {
it('navigates to a page', () => {
urlUtils.visitUrl(mockUrl);
- expect(window.location.href).toBe(mockUrl);
+ expect(window.location.assign).toHaveBeenCalledWith(mockUrl);
});
it('navigates to a new page', () => {
- const otherWindow = {};
+ const otherWindow = {
+ location: {
+ assign: jest.fn(),
+ },
+ };
Object.defineProperty(window, 'open', {
writable: true,
@@ -431,7 +437,7 @@ describe('URL utility', () => {
urlUtils.visitUrl(mockUrl, true);
expect(otherWindow.opener).toBe(null);
- expect(otherWindow.location).toBe(mockUrl);
+ expect(otherWindow.location.assign).toHaveBeenCalledWith(mockUrl);
});
});
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_form_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_form_spec.js
index a68087f7f57..e3feb99a9b5 100644
--- a/spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_form_spec.js
+++ b/spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_form_spec.js
@@ -286,8 +286,8 @@ describe('Container Expiration Policy Settings Form', () => {
await submitForm();
- expect(window.location.href.endsWith('settings-path?showSetupSuccessAlert=true')).toBe(
- true,
+ expect(window.location.assign).toHaveBeenCalledWith(
+ 'settings-path?showSetupSuccessAlert=true',
);
});
diff --git a/spec/frontend/profile/components/follow_spec.js b/spec/frontend/profile/components/follow_spec.js
new file mode 100644
index 00000000000..2555e41257f
--- /dev/null
+++ b/spec/frontend/profile/components/follow_spec.js
@@ -0,0 +1,99 @@
+import { GlAvatarLabeled, GlAvatarLink, GlLoadingIcon, GlPagination } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+
+import users from 'test_fixtures/api/users/followers/get.json';
+import Follow from '~/profile/components/follow.vue';
+import { DEFAULT_PER_PAGE } from '~/api';
+
+jest.mock('~/rest_api');
+
+describe('FollowersTab', () => {
+ let wrapper;
+
+ const defaultPropsData = {
+ users,
+ loading: false,
+ page: 1,
+ totalItems: 50,
+ };
+
+ const createComponent = ({ propsData = {} } = {}) => {
+ wrapper = shallowMount(Follow, {
+ propsData: {
+ ...defaultPropsData,
+ ...propsData,
+ },
+ });
+ };
+
+ const findPagination = () => wrapper.findComponent(GlPagination);
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+
+ describe('when `loading` prop is `true`', () => {
+ it('renders loading icon', () => {
+ createComponent({ propsData: { loading: true } });
+
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
+ });
+ });
+
+ describe('when `loading` prop is `false`', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('does not render loading icon', () => {
+ expect(findLoadingIcon().exists()).toBe(false);
+ });
+
+ it('renders users', () => {
+ const avatarLinksHref = wrapper
+ .findAllComponents(GlAvatarLink)
+ .wrappers.map((avatarLinkWrapper) => avatarLinkWrapper.attributes('href'));
+ const expectedAvatarLinksHref = users.map((user) => user.web_url);
+
+ const avatarLabeledProps = wrapper
+ .findAllComponents(GlAvatarLabeled)
+ .wrappers.map((avatarLabeledWrapper) => ({
+ label: avatarLabeledWrapper.props('label'),
+ subLabel: avatarLabeledWrapper.props('subLabel'),
+ size: avatarLabeledWrapper.attributes('size'),
+ entityName: avatarLabeledWrapper.attributes('entity-name'),
+ entityId: avatarLabeledWrapper.attributes('entity-id'),
+ src: avatarLabeledWrapper.attributes('src'),
+ }));
+ const expectedAvatarLabeledProps = users.map((user) => ({
+ src: user.avatar_url,
+ size: '48',
+ entityId: user.id.toString(),
+ entityName: user.name,
+ label: user.name,
+ subLabel: user.username,
+ }));
+
+ expect(avatarLinksHref).toEqual(expectedAvatarLinksHref);
+ expect(avatarLabeledProps).toEqual(expectedAvatarLabeledProps);
+ });
+
+ it('renders `GlPagination` and passes correct props', () => {
+ expect(wrapper.findComponent(GlPagination).props()).toMatchObject({
+ align: 'center',
+ value: defaultPropsData.page,
+ totalItems: defaultPropsData.totalItems,
+ perPage: DEFAULT_PER_PAGE,
+ prevText: Follow.i18n.prev,
+ nextText: Follow.i18n.next,
+ });
+ });
+
+ describe('when `GlPagination` emits `input` event', () => {
+ it('emits `pagination-input` event', () => {
+ const nextPage = defaultPropsData.page + 1;
+
+ findPagination().vm.$emit('input', nextPage);
+
+ expect(wrapper.emitted('pagination-input')).toEqual([[nextPage]]);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/profile/components/followers_tab_spec.js b/spec/frontend/profile/components/followers_tab_spec.js
index 9cc5bdea9be..0370005d0a4 100644
--- a/spec/frontend/profile/components/followers_tab_spec.js
+++ b/spec/frontend/profile/components/followers_tab_spec.js
@@ -1,32 +1,127 @@
import { GlBadge, GlTab } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import followers from 'test_fixtures/api/users/followers/get.json';
import { s__ } from '~/locale';
import FollowersTab from '~/profile/components/followers_tab.vue';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import Follow from '~/profile/components/follow.vue';
+import { getUserFollowers } from '~/rest_api';
+import { createAlert } from '~/alert';
+import waitForPromises from 'helpers/wait_for_promises';
+import { stubComponent } from 'helpers/stub_component';
+
+jest.mock('~/rest_api');
+jest.mock('~/alert');
describe('FollowersTab', () => {
let wrapper;
const createComponent = () => {
- wrapper = shallowMountExtended(FollowersTab, {
+ wrapper = shallowMount(FollowersTab, {
provide: {
- followers: 2,
+ followersCount: 2,
+ userId: 1,
+ },
+ stubs: {
+ GlTab: stubComponent(GlTab, {
+ template: `
+ <li>
+ <slot name="title"></slot>
+ <slot></slot>
+ </li>
+ `,
+ }),
},
});
};
- it('renders `GlTab` and sets title', () => {
- createComponent();
+ const findGlBadge = () => wrapper.findComponent(GlBadge);
+ const findFollow = () => wrapper.findComponent(Follow);
+
+ describe('when API request is loading', () => {
+ beforeEach(() => {
+ getUserFollowers.mockReturnValueOnce(new Promise(() => {}));
+ createComponent();
+ });
+
+ it('renders `Follow` component and sets `loading` prop to `true`', () => {
+ expect(findFollow().props('loading')).toBe(true);
+ });
+ });
+
+ describe('when API request is successful', () => {
+ beforeEach(async () => {
+ getUserFollowers.mockResolvedValueOnce({
+ data: followers,
+ headers: { 'X-TOTAL': '6' },
+ });
+ createComponent();
+
+ await waitForPromises();
+ });
+
+ it('renders `GlTab` and sets title', () => {
+ expect(wrapper.findComponent(GlTab).text()).toContain(s__('UserProfile|Followers'));
+ });
+
+ it('renders `GlBadge`, sets size and content', () => {
+ expect(findGlBadge().props('size')).toBe('sm');
+ expect(findGlBadge().text()).toBe('2');
+ });
+
+ it('renders `Follow` component and passes correct props', () => {
+ expect(findFollow().props()).toMatchObject({
+ users: followers,
+ loading: false,
+ page: 1,
+ totalItems: 6,
+ });
+ });
+
+ describe('when `Follow` component emits `pagination-input` event', () => {
+ it('calls API and updates `users` and `page` props', async () => {
+ const lastFollower = followers.at(-1);
+ const paginationFollowers = [
+ {
+ ...lastFollower,
+ id: lastFollower.id + 1,
+ name: 'page 2 follower',
+ },
+ ];
+
+ getUserFollowers.mockResolvedValueOnce({
+ data: paginationFollowers,
+ headers: { 'X-TOTAL': '6' },
+ });
- expect(wrapper.findComponent(GlTab).element.textContent).toContain(
- s__('UserProfile|Followers'),
- );
+ findFollow().vm.$emit('pagination-input', 2);
+
+ await waitForPromises();
+
+ expect(findFollow().props()).toMatchObject({
+ users: paginationFollowers,
+ loading: false,
+ page: 2,
+ totalItems: 6,
+ });
+ });
+ });
});
- it('renders `GlBadge`, sets size and content', () => {
- createComponent();
+ describe('when API request is not successful', () => {
+ beforeEach(async () => {
+ getUserFollowers.mockRejectedValueOnce(new Error());
+ createComponent();
- expect(wrapper.findComponent(GlBadge).attributes('size')).toBe('sm');
- expect(wrapper.findComponent(GlBadge).element.textContent).toBe('2');
+ await waitForPromises();
+ });
+
+ it('shows error alert', () => {
+ expect(createAlert).toHaveBeenCalledWith({
+ message: FollowersTab.i18n.errorMessage,
+ error: new Error(),
+ captureError: true,
+ });
+ });
});
});
diff --git a/spec/frontend/profile/components/following_tab_spec.js b/spec/frontend/profile/components/following_tab_spec.js
index c9d56360c3e..c0583cf4877 100644
--- a/spec/frontend/profile/components/following_tab_spec.js
+++ b/spec/frontend/profile/components/following_tab_spec.js
@@ -10,7 +10,7 @@ describe('FollowingTab', () => {
const createComponent = () => {
wrapper = shallowMountExtended(FollowingTab, {
provide: {
- followees: 3,
+ followeesCount: 3,
},
});
};