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>2021-10-20 11:43:02 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2021-10-20 11:43:02 +0300
commitd9ab72d6080f594d0b3cae15f14b3ef2c6c638cb (patch)
tree2341ef426af70ad1e289c38036737e04b0aa5007 /spec/frontend/repository
parentd6e514dd13db8947884cd58fe2a9c2a063400a9b (diff)
Add latest changes from gitlab-org/gitlab@14-4-stable-eev14.4.0-rc42
Diffstat (limited to 'spec/frontend/repository')
-rw-r--r--spec/frontend/repository/commits_service_spec.js84
-rw-r--r--spec/frontend/repository/components/blob_content_viewer_spec.js109
-rw-r--r--spec/frontend/repository/components/blob_edit_spec.js6
-rw-r--r--spec/frontend/repository/components/blob_viewers/video_viewer_spec.js22
-rw-r--r--spec/frontend/repository/components/breadcrumbs_spec.js38
-rw-r--r--spec/frontend/repository/components/fork_suggestion_spec.js44
-rw-r--r--spec/frontend/repository/components/new_directory_modal_spec.js203
-rw-r--r--spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap75
-rw-r--r--spec/frontend/repository/components/table/index_spec.js33
-rw-r--r--spec/frontend/repository/components/table/row_spec.js34
-rw-r--r--spec/frontend/repository/components/tree_content_spec.js22
-rw-r--r--spec/frontend/repository/router_spec.js28
12 files changed, 653 insertions, 45 deletions
diff --git a/spec/frontend/repository/commits_service_spec.js b/spec/frontend/repository/commits_service_spec.js
new file mode 100644
index 00000000000..d924974aede
--- /dev/null
+++ b/spec/frontend/repository/commits_service_spec.js
@@ -0,0 +1,84 @@
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+import { loadCommits, isRequested, resetRequestedCommits } from '~/repository/commits_service';
+import httpStatus from '~/lib/utils/http_status';
+import createFlash from '~/flash';
+import { I18N_COMMIT_DATA_FETCH_ERROR } from '~/repository/constants';
+
+jest.mock('~/flash');
+
+describe('commits service', () => {
+ let mock;
+ const url = `${gon.relative_url_root || ''}/my-project/-/refs/main/logs_tree/`;
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+
+ mock.onGet(url).reply(httpStatus.OK, [], {});
+
+ jest.spyOn(axios, 'get');
+ });
+
+ afterEach(() => {
+ mock.restore();
+ resetRequestedCommits();
+ });
+
+ const requestCommits = (offset, project = 'my-project', path = '', ref = 'main') =>
+ loadCommits(project, path, ref, offset);
+
+ it('calls axios get', async () => {
+ const offset = 10;
+ const project = 'my-project';
+ const path = 'my-path';
+ const ref = 'my-ref';
+ const testUrl = `${gon.relative_url_root || ''}/${project}/-/refs/${ref}/logs_tree/${path}`;
+
+ await requestCommits(offset, project, path, ref);
+
+ expect(axios.get).toHaveBeenCalledWith(testUrl, { params: { format: 'json', offset } });
+ });
+
+ it('encodes the path correctly', async () => {
+ await requestCommits(1, 'some-project', 'with $peci@l ch@rs/');
+
+ const encodedUrl = '/some-project/-/refs/main/logs_tree/with%20%24peci%40l%20ch%40rs%2F';
+ expect(axios.get).toHaveBeenCalledWith(encodedUrl, expect.anything());
+ });
+
+ it('calls axios get once per batch', async () => {
+ await Promise.all([requestCommits(0), requestCommits(1), requestCommits(23)]);
+
+ expect(axios.get.mock.calls.length).toEqual(1);
+ });
+
+ it('calls axios get twice if an offset is larger than 25', async () => {
+ await requestCommits(100);
+
+ expect(axios.get.mock.calls[0][1]).toEqual({ params: { format: 'json', offset: 75 } });
+ expect(axios.get.mock.calls[1][1]).toEqual({ params: { format: 'json', offset: 100 } });
+ });
+
+ it('updates the list of requested offsets', async () => {
+ await requestCommits(200);
+
+ expect(isRequested(200)).toBe(true);
+ });
+
+ it('resets the list of requested offsets', async () => {
+ await requestCommits(300);
+
+ resetRequestedCommits();
+ expect(isRequested(300)).toBe(false);
+ });
+
+ it('calls `createFlash` when the request fails', async () => {
+ const invalidPath = '/#@ some/path';
+ const invalidUrl = `${url}${invalidPath}`;
+ mock.onGet(invalidUrl).replyOnce(httpStatus.INTERNAL_SERVER_ERROR, [], {});
+
+ await requestCommits(1, 'my-project', invalidPath);
+
+ expect(createFlash).toHaveBeenCalledWith({ message: I18N_COMMIT_DATA_FETCH_ERROR });
+ });
+});
diff --git a/spec/frontend/repository/components/blob_content_viewer_spec.js b/spec/frontend/repository/components/blob_content_viewer_spec.js
index 8331adcdfc2..59db537282b 100644
--- a/spec/frontend/repository/components/blob_content_viewer_spec.js
+++ b/spec/frontend/repository/components/blob_content_viewer_spec.js
@@ -11,13 +11,18 @@ import BlobHeader from '~/blob/components/blob_header.vue';
import BlobButtonGroup from '~/repository/components/blob_button_group.vue';
import BlobContentViewer from '~/repository/components/blob_content_viewer.vue';
import BlobEdit from '~/repository/components/blob_edit.vue';
+import ForkSuggestion from '~/repository/components/fork_suggestion.vue';
import { loadViewer, viewerProps } from '~/repository/components/blob_viewers';
import DownloadViewer from '~/repository/components/blob_viewers/download_viewer.vue';
import EmptyViewer from '~/repository/components/blob_viewers/empty_viewer.vue';
import TextViewer from '~/repository/components/blob_viewers/text_viewer.vue';
import blobInfoQuery from '~/repository/queries/blob_info.query.graphql';
+import { redirectTo } from '~/lib/utils/url_utility';
+import { isLoggedIn } from '~/lib/utils/common_utils';
jest.mock('~/repository/components/blob_viewers');
+jest.mock('~/lib/utils/url_utility');
+jest.mock('~/lib/utils/common_utils');
let wrapper;
let mockResolver;
@@ -34,12 +39,14 @@ const simpleMockData = {
webPath: 'some_file.js',
editBlobPath: 'some_file.js/edit',
ideEditPath: 'some_file.js/ide/edit',
+ forkAndEditPath: 'some_file.js/fork/edit',
+ ideForkAndEditPath: 'some_file.js/fork/ide',
+ canModifyBlob: true,
storedExternally: false,
rawPath: 'some_file.js',
externalStorageUrl: 'some_file.js',
replacePath: 'some_file.js/replace',
deletePath: 'some_file.js/delete',
- forkPath: 'some_file.js/fork',
simpleViewer: {
fileType: 'text',
tooLarge: false,
@@ -62,6 +69,8 @@ const projectMockData = {
userPermissions: {
pushCode: true,
downloadCode: true,
+ createMergeRequestIn: true,
+ forkProject: true,
},
repository: {
empty: false,
@@ -82,6 +91,8 @@ const createComponentWithApollo = (mockData = {}, inject = {}) => {
emptyRepo = defaultEmptyRepo,
canPushCode = defaultPushCode,
canDownloadCode = defaultDownloadCode,
+ createMergeRequestIn = projectMockData.userPermissions.createMergeRequestIn,
+ forkProject = projectMockData.userPermissions.forkProject,
pathLocks = [],
} = mockData;
@@ -89,7 +100,12 @@ const createComponentWithApollo = (mockData = {}, inject = {}) => {
data: {
project: {
id: '1234',
- userPermissions: { pushCode: canPushCode, downloadCode: canDownloadCode },
+ userPermissions: {
+ pushCode: canPushCode,
+ downloadCode: canDownloadCode,
+ createMergeRequestIn,
+ forkProject,
+ },
pathLocks: {
nodes: pathLocks,
},
@@ -158,9 +174,16 @@ describe('Blob content viewer component', () => {
const findBlobEdit = () => wrapper.findComponent(BlobEdit);
const findBlobContent = () => wrapper.findComponent(BlobContent);
const findBlobButtonGroup = () => wrapper.findComponent(BlobButtonGroup);
+ const findForkSuggestion = () => wrapper.findComponent(ForkSuggestion);
+
+ beforeEach(() => {
+ gon.features = { refactorTextViewer: true };
+ isLoggedIn.mockReturnValue(true);
+ });
afterEach(() => {
wrapper.destroy();
+ mockAxios.reset();
});
it('renders a GlLoadingIcon component', () => {
@@ -183,7 +206,6 @@ describe('Blob content viewer component', () => {
it('renders a BlobContent component', () => {
expect(findBlobContent().props('loading')).toEqual(false);
- expect(findBlobContent().props('content')).toEqual('raw content');
expect(findBlobContent().props('isRawContent')).toBe(true);
expect(findBlobContent().props('activeViewer')).toEqual({
fileType: 'text',
@@ -192,6 +214,16 @@ describe('Blob content viewer component', () => {
renderError: null,
});
});
+
+ describe('legacy viewers', () => {
+ it('loads a legacy viewer when a viewer component is not available', async () => {
+ createComponentWithApollo({ blobs: { ...simpleMockData, fileType: 'unknown' } });
+ await waitForPromises();
+
+ expect(mockAxios.history.get).toHaveLength(1);
+ expect(mockAxios.history.get[0].url).toEqual('some_file.js?format=json&viewer=simple');
+ });
+ });
});
describe('rich viewer', () => {
@@ -210,7 +242,6 @@ describe('Blob content viewer component', () => {
it('renders a BlobContent component', () => {
expect(findBlobContent().props('loading')).toEqual(false);
- expect(findBlobContent().props('content')).toEqual('raw content');
expect(findBlobContent().props('isRawContent')).toBe(true);
expect(findBlobContent().props('activeViewer')).toEqual({
fileType: 'markup',
@@ -241,18 +272,12 @@ describe('Blob content viewer component', () => {
});
describe('legacy viewers', () => {
- it('does not load a legacy viewer when a rich viewer is not available', async () => {
- createComponentWithApollo({ blobs: simpleMockData });
- await waitForPromises();
-
- expect(mockAxios.history.get).toHaveLength(0);
- });
-
- it('loads a legacy viewer when a rich viewer is available', async () => {
- createComponentWithApollo({ blobs: richMockData });
+ it('loads a legacy viewer when a viewer component is not available', async () => {
+ createComponentWithApollo({ blobs: { ...richMockData, fileType: 'unknown' } });
await waitForPromises();
expect(mockAxios.history.get).toHaveLength(1);
+ expect(mockAxios.history.get[0].url).toEqual('some_file.js?format=json&viewer=rich');
});
});
@@ -462,7 +487,7 @@ describe('Blob content viewer component', () => {
});
it('does not render if not logged in', async () => {
- window.gon.current_user_id = null;
+ isLoggedIn.mockReturnValueOnce(false);
fullFactory({
mockData: { blobInfo: simpleMockData },
@@ -506,4 +531,60 @@ describe('Blob content viewer component', () => {
);
});
});
+
+ describe('edit blob', () => {
+ beforeEach(() => {
+ fullFactory({
+ mockData: { blobInfo: simpleMockData },
+ stubs: {
+ BlobContent: true,
+ BlobReplace: true,
+ },
+ });
+ });
+
+ it('simple edit redirects to the simple editor', () => {
+ findBlobEdit().vm.$emit('edit', 'simple');
+ expect(redirectTo).toHaveBeenCalledWith(simpleMockData.editBlobPath);
+ });
+
+ it('IDE edit redirects to the IDE editor', () => {
+ findBlobEdit().vm.$emit('edit', 'ide');
+ expect(redirectTo).toHaveBeenCalledWith(simpleMockData.ideEditPath);
+ });
+
+ it.each`
+ loggedIn | canModifyBlob | createMergeRequestIn | forkProject | showForkSuggestion
+ ${true} | ${false} | ${true} | ${true} | ${true}
+ ${false} | ${false} | ${true} | ${true} | ${false}
+ ${true} | ${true} | ${false} | ${true} | ${false}
+ ${true} | ${true} | ${true} | ${false} | ${false}
+ `(
+ 'shows/hides a fork suggestion according to a set of conditions',
+ async ({
+ loggedIn,
+ canModifyBlob,
+ createMergeRequestIn,
+ forkProject,
+ showForkSuggestion,
+ }) => {
+ isLoggedIn.mockReturnValueOnce(loggedIn);
+ fullFactory({
+ mockData: {
+ blobInfo: { ...simpleMockData, canModifyBlob },
+ project: { userPermissions: { createMergeRequestIn, forkProject } },
+ },
+ stubs: {
+ BlobContent: true,
+ BlobButtonGroup: true,
+ },
+ });
+
+ findBlobEdit().vm.$emit('edit', 'simple');
+ await nextTick();
+
+ expect(findForkSuggestion().exists()).toBe(showForkSuggestion);
+ },
+ );
+ });
});
diff --git a/spec/frontend/repository/components/blob_edit_spec.js b/spec/frontend/repository/components/blob_edit_spec.js
index 11739674bc9..e2de7bc2957 100644
--- a/spec/frontend/repository/components/blob_edit_spec.js
+++ b/spec/frontend/repository/components/blob_edit_spec.js
@@ -7,6 +7,7 @@ const DEFAULT_PROPS = {
editPath: 'some_file.js/edit',
webIdePath: 'some_file.js/ide/edit',
showEditButton: true,
+ needsToFork: false,
};
describe('BlobEdit component', () => {
@@ -56,7 +57,6 @@ describe('BlobEdit component', () => {
it('renders the Edit button', () => {
createComponent();
- expect(findEditButton().attributes('href')).toBe(DEFAULT_PROPS.editPath);
expect(findEditButton().text()).toBe('Edit');
expect(findEditButton()).not.toBeDisabled();
});
@@ -64,7 +64,6 @@ describe('BlobEdit component', () => {
it('renders the Web IDE button', () => {
createComponent();
- expect(findWebIdeButton().attributes('href')).toBe(DEFAULT_PROPS.webIdePath);
expect(findWebIdeButton().text()).toBe('Web IDE');
expect(findWebIdeButton()).not.toBeDisabled();
});
@@ -72,13 +71,14 @@ describe('BlobEdit component', () => {
it('renders WebIdeLink component', () => {
createComponent(true);
- const { editPath: editUrl, webIdePath: webIdeUrl } = DEFAULT_PROPS;
+ const { editPath: editUrl, webIdePath: webIdeUrl, needsToFork } = DEFAULT_PROPS;
expect(findWebIdeLink().props()).toMatchObject({
editUrl,
webIdeUrl,
isBlob: true,
showEditButton: true,
+ needsToFork,
});
});
diff --git a/spec/frontend/repository/components/blob_viewers/video_viewer_spec.js b/spec/frontend/repository/components/blob_viewers/video_viewer_spec.js
new file mode 100644
index 00000000000..34448c03b31
--- /dev/null
+++ b/spec/frontend/repository/components/blob_viewers/video_viewer_spec.js
@@ -0,0 +1,22 @@
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import VideoViewer from '~/repository/components/blob_viewers/video_viewer.vue';
+
+describe('Video Viewer', () => {
+ let wrapper;
+
+ const propsData = { url: 'some/video.mp4' };
+
+ const createComponent = () => {
+ wrapper = shallowMountExtended(VideoViewer, { propsData });
+ };
+
+ const findVideo = () => wrapper.findByTestId('video');
+
+ it('renders a Video element', () => {
+ createComponent();
+
+ expect(findVideo().exists()).toBe(true);
+ expect(findVideo().attributes('src')).toBe(propsData.url);
+ expect(findVideo().attributes('controls')).not.toBeUndefined();
+ });
+});
diff --git a/spec/frontend/repository/components/breadcrumbs_spec.js b/spec/frontend/repository/components/breadcrumbs_spec.js
index 0733cffe4f4..eb957c635ac 100644
--- a/spec/frontend/repository/components/breadcrumbs_spec.js
+++ b/spec/frontend/repository/components/breadcrumbs_spec.js
@@ -2,6 +2,7 @@ import { GlDropdown } from '@gitlab/ui';
import { shallowMount, RouterLinkStub } from '@vue/test-utils';
import Breadcrumbs from '~/repository/components/breadcrumbs.vue';
import UploadBlobModal from '~/repository/components/upload_blob_modal.vue';
+import NewDirectoryModal from '~/repository/components/new_directory_modal.vue';
const defaultMockRoute = {
name: 'blobPath',
@@ -10,7 +11,7 @@ const defaultMockRoute = {
describe('Repository breadcrumbs component', () => {
let wrapper;
- const factory = (currentPath, extraProps = {}, mockRoute = {}) => {
+ const factory = (currentPath, extraProps = {}, mockRoute = {}, newDirModal = true) => {
const $apollo = {
queries: {
userPermissions: {
@@ -34,10 +35,12 @@ describe('Repository breadcrumbs component', () => {
},
$apollo,
},
+ provide: { glFeatures: { newDirModal } },
});
};
const findUploadBlobModal = () => wrapper.find(UploadBlobModal);
+ const findNewDirectoryModal = () => wrapper.find(NewDirectoryModal);
afterEach(() => {
wrapper.destroy();
@@ -121,4 +124,37 @@ describe('Repository breadcrumbs component', () => {
expect(findUploadBlobModal().exists()).toBe(true);
});
});
+
+ describe('renders the new directory modal', () => {
+ describe('with the feature flag enabled', () => {
+ beforeEach(() => {
+ window.gon.features = {
+ newDirModal: true,
+ };
+ factory('/', { canEditTree: true });
+ });
+
+ it('does not render the modal while loading', () => {
+ expect(findNewDirectoryModal().exists()).toBe(false);
+ });
+
+ it('renders the modal once loaded', async () => {
+ wrapper.setData({ $apollo: { queries: { userPermissions: { loading: false } } } });
+
+ await wrapper.vm.$nextTick();
+
+ expect(findNewDirectoryModal().exists()).toBe(true);
+ });
+ });
+
+ describe('with the feature flag disabled', () => {
+ it('does not render the modal', () => {
+ window.gon.features = {
+ newDirModal: false,
+ };
+ factory('/', { canEditTree: true }, {}, {}, false);
+ expect(findNewDirectoryModal().exists()).toBe(false);
+ });
+ });
+ });
});
diff --git a/spec/frontend/repository/components/fork_suggestion_spec.js b/spec/frontend/repository/components/fork_suggestion_spec.js
new file mode 100644
index 00000000000..36a48a3fdb8
--- /dev/null
+++ b/spec/frontend/repository/components/fork_suggestion_spec.js
@@ -0,0 +1,44 @@
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import ForkSuggestion from '~/repository/components/fork_suggestion.vue';
+
+const DEFAULT_PROPS = { forkPath: 'some_file.js/fork' };
+
+describe('ForkSuggestion component', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = shallowMountExtended(ForkSuggestion, {
+ propsData: { ...DEFAULT_PROPS },
+ });
+ };
+
+ beforeEach(() => createComponent());
+
+ afterEach(() => wrapper.destroy());
+
+ const { i18n } = ForkSuggestion;
+ const findMessage = () => wrapper.findByTestId('message');
+ const findForkButton = () => wrapper.findByTestId('fork');
+ const findCancelButton = () => wrapper.findByTestId('cancel');
+
+ it('renders a message', () => {
+ expect(findMessage().text()).toBe(i18n.message);
+ });
+
+ it('renders a Fork button', () => {
+ const forkButton = findForkButton();
+
+ expect(forkButton.text()).toBe(i18n.fork);
+ expect(forkButton.attributes('href')).toBe(DEFAULT_PROPS.forkPath);
+ });
+
+ it('renders a Cancel button', () => {
+ expect(findCancelButton().text()).toBe(i18n.cancel);
+ });
+
+ it('emits a cancel event when Cancel button is clicked', () => {
+ findCancelButton().vm.$emit('click');
+
+ expect(wrapper.emitted('cancel')).toEqual([[]]);
+ });
+});
diff --git a/spec/frontend/repository/components/new_directory_modal_spec.js b/spec/frontend/repository/components/new_directory_modal_spec.js
new file mode 100644
index 00000000000..fe7f024e3ea
--- /dev/null
+++ b/spec/frontend/repository/components/new_directory_modal_spec.js
@@ -0,0 +1,203 @@
+import { GlModal, GlFormTextarea, GlToggle } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
+import axios from 'axios';
+import MockAdapter from 'axios-mock-adapter';
+import waitForPromises from 'helpers/wait_for_promises';
+import createFlash from '~/flash';
+import httpStatusCodes from '~/lib/utils/http_status';
+import { visitUrl } from '~/lib/utils/url_utility';
+import NewDirectoryModal from '~/repository/components/new_directory_modal.vue';
+
+jest.mock('~/flash');
+jest.mock('~/lib/utils/url_utility', () => ({
+ visitUrl: jest.fn(),
+}));
+
+const initialProps = {
+ modalTitle: 'Create New Directory',
+ modalId: 'modal-new-directory',
+ commitMessage: 'Add new directory',
+ targetBranch: 'some-target-branch',
+ originalBranch: 'master',
+ canPushCode: true,
+ path: 'create_dir',
+};
+
+const defaultFormValue = {
+ dirName: 'foo',
+ originalBranch: initialProps.originalBranch,
+ branchName: initialProps.targetBranch,
+ commitMessage: initialProps.commitMessage,
+ createNewMr: true,
+};
+
+describe('NewDirectoryModal', () => {
+ let wrapper;
+ let mock;
+
+ const createComponent = (props = {}) => {
+ wrapper = shallowMount(NewDirectoryModal, {
+ propsData: {
+ ...initialProps,
+ ...props,
+ },
+ attrs: {
+ static: true,
+ visible: true,
+ },
+ });
+ };
+
+ const findModal = () => wrapper.findComponent(GlModal);
+ const findDirName = () => wrapper.find('[name="dir_name"]');
+ const findBranchName = () => wrapper.find('[name="branch_name"]');
+ const findCommitMessage = () => wrapper.findComponent(GlFormTextarea);
+ const findMrToggle = () => wrapper.findComponent(GlToggle);
+
+ const fillForm = async (inputValue = {}) => {
+ const {
+ dirName = defaultFormValue.dirName,
+ branchName = defaultFormValue.branchName,
+ commitMessage = defaultFormValue.commitMessage,
+ createNewMr = true,
+ } = inputValue;
+
+ await findDirName().vm.$emit('input', dirName);
+ await findBranchName().vm.$emit('input', branchName);
+ await findCommitMessage().vm.$emit('input', commitMessage);
+ await findMrToggle().vm.$emit('change', createNewMr);
+ await nextTick;
+ };
+
+ const submitForm = async () => {
+ const mockEvent = { preventDefault: jest.fn() };
+ findModal().vm.$emit('primary', mockEvent);
+ await waitForPromises();
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders modal component', () => {
+ createComponent();
+
+ const { modalTitle: title } = initialProps;
+
+ expect(findModal().props()).toMatchObject({
+ title,
+ size: 'md',
+ actionPrimary: {
+ text: NewDirectoryModal.i18n.PRIMARY_OPTIONS_TEXT,
+ },
+ actionCancel: {
+ text: 'Cancel',
+ },
+ });
+ });
+
+ describe('form', () => {
+ it.each`
+ component | defaultValue | canPushCode | targetBranch | originalBranch | exist
+ ${findDirName} | ${undefined} | ${true} | ${initialProps.targetBranch} | ${initialProps.originalBranch} | ${true}
+ ${findBranchName} | ${initialProps.targetBranch} | ${true} | ${initialProps.targetBranch} | ${initialProps.originalBranch} | ${true}
+ ${findBranchName} | ${undefined} | ${false} | ${initialProps.targetBranch} | ${initialProps.originalBranch} | ${false}
+ ${findCommitMessage} | ${initialProps.commitMessage} | ${true} | ${initialProps.targetBranch} | ${initialProps.originalBranch} | ${true}
+ ${findMrToggle} | ${'true'} | ${true} | ${'new-target-branch'} | ${'master'} | ${true}
+ ${findMrToggle} | ${'true'} | ${true} | ${'master'} | ${'master'} | ${true}
+ `(
+ 'has the correct form fields ',
+ ({ component, defaultValue, canPushCode, targetBranch, originalBranch, exist }) => {
+ createComponent({
+ canPushCode,
+ targetBranch,
+ originalBranch,
+ });
+ const formField = component();
+
+ if (!exist) {
+ expect(formField.exists()).toBe(false);
+ return;
+ }
+
+ expect(formField.exists()).toBe(true);
+ expect(formField.attributes('value')).toBe(defaultValue);
+ },
+ );
+ });
+
+ describe('form submission', () => {
+ beforeEach(async () => {
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ describe('valid form', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('passes the formData', async () => {
+ const {
+ dirName,
+ branchName,
+ commitMessage,
+ originalBranch,
+ createNewMr,
+ } = defaultFormValue;
+ mock.onPost(initialProps.path).reply(httpStatusCodes.OK, {});
+ await fillForm();
+ await submitForm();
+
+ expect(mock.history.post[0].data.get('dir_name')).toEqual(dirName);
+ expect(mock.history.post[0].data.get('branch_name')).toEqual(branchName);
+ expect(mock.history.post[0].data.get('commit_message')).toEqual(commitMessage);
+ expect(mock.history.post[0].data.get('original_branch')).toEqual(originalBranch);
+ expect(mock.history.post[0].data.get('create_merge_request')).toEqual(String(createNewMr));
+ });
+
+ it('does not submit "create_merge_request" formData if createNewMr is not checked', async () => {
+ mock.onPost(initialProps.path).reply(httpStatusCodes.OK, {});
+ await fillForm({ createNewMr: false });
+ await submitForm();
+ expect(mock.history.post[0].data.get('create_merge_request')).toBeNull();
+ });
+
+ it('redirects to the new directory', async () => {
+ const response = { filePath: 'new-dir-path' };
+ mock.onPost(initialProps.path).reply(httpStatusCodes.OK, response);
+
+ await fillForm({ dirName: 'foo', branchName: 'master', commitMessage: 'foo' });
+ await submitForm();
+
+ expect(visitUrl).toHaveBeenCalledWith(response.filePath);
+ });
+ });
+
+ describe('invalid form', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('disables submit button', async () => {
+ await fillForm({ dirName: '', branchName: '', commitMessage: '' });
+ expect(findModal().props('actionPrimary').attributes[0].disabled).toBe(true);
+ });
+
+ it('creates a flash error', async () => {
+ mock.onPost(initialProps.path).timeout();
+
+ await fillForm({ dirName: 'foo', branchName: 'master', commitMessage: 'foo' });
+ await submitForm();
+
+ expect(createFlash).toHaveBeenCalledWith({
+ message: NewDirectoryModal.i18n.ERROR_MESSAGE,
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap b/spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap
index 6f461f4c69b..26064e9b248 100644
--- a/spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap
+++ b/spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap
@@ -31,25 +31,36 @@ exports[`Repository table row component renders a symlink table row 1`] = `
<!---->
- <!---->
+ <gl-icon-stub
+ class="ml-1"
+ name="lock"
+ size="12"
+ title="Locked by Root"
+ />
</td>
<td
class="d-none d-sm-table-cell tree-commit cursor-default"
>
- <gl-skeleton-loading-stub
- class="h-auto"
- lines="1"
+ <gl-link-stub
+ class="str-truncated-100 tree-commit-link"
/>
+
+ <gl-intersection-observer-stub>
+ <!---->
+ </gl-intersection-observer-stub>
</td>
<td
class="tree-time-ago text-right cursor-default"
>
- <gl-skeleton-loading-stub
- class="ml-auto h-auto w-50"
- lines="1"
+ <timeago-tooltip-stub
+ cssclass=""
+ time="2019-01-01"
+ tooltipplacement="top"
/>
+
+ <!---->
</td>
</tr>
`;
@@ -85,25 +96,36 @@ exports[`Repository table row component renders table row 1`] = `
<!---->
- <!---->
+ <gl-icon-stub
+ class="ml-1"
+ name="lock"
+ size="12"
+ title="Locked by Root"
+ />
</td>
<td
class="d-none d-sm-table-cell tree-commit cursor-default"
>
- <gl-skeleton-loading-stub
- class="h-auto"
- lines="1"
+ <gl-link-stub
+ class="str-truncated-100 tree-commit-link"
/>
+
+ <gl-intersection-observer-stub>
+ <!---->
+ </gl-intersection-observer-stub>
</td>
<td
class="tree-time-ago text-right cursor-default"
>
- <gl-skeleton-loading-stub
- class="ml-auto h-auto w-50"
- lines="1"
+ <timeago-tooltip-stub
+ cssclass=""
+ time="2019-01-01"
+ tooltipplacement="top"
/>
+
+ <!---->
</td>
</tr>
`;
@@ -139,25 +161,36 @@ exports[`Repository table row component renders table row for path with special
<!---->
- <!---->
+ <gl-icon-stub
+ class="ml-1"
+ name="lock"
+ size="12"
+ title="Locked by Root"
+ />
</td>
<td
class="d-none d-sm-table-cell tree-commit cursor-default"
>
- <gl-skeleton-loading-stub
- class="h-auto"
- lines="1"
+ <gl-link-stub
+ class="str-truncated-100 tree-commit-link"
/>
+
+ <gl-intersection-observer-stub>
+ <!---->
+ </gl-intersection-observer-stub>
</td>
<td
class="tree-time-ago text-right cursor-default"
>
- <gl-skeleton-loading-stub
- class="ml-auto h-auto w-50"
- lines="1"
+ <timeago-tooltip-stub
+ cssclass=""
+ time="2019-01-01"
+ tooltipplacement="top"
/>
+
+ <!---->
</td>
</tr>
`;
diff --git a/spec/frontend/repository/components/table/index_spec.js b/spec/frontend/repository/components/table/index_spec.js
index e9e51abaf0f..c8dddefc4f2 100644
--- a/spec/frontend/repository/components/table/index_spec.js
+++ b/spec/frontend/repository/components/table/index_spec.js
@@ -34,17 +34,45 @@ const MOCK_BLOBS = [
},
];
-function factory({ path, isLoading = false, hasMore = true, entries = {} }) {
+const MOCK_COMMITS = [
+ {
+ fileName: 'blob.md',
+ type: 'blob',
+ commit: {
+ message: 'Updated blob.md',
+ },
+ },
+ {
+ fileName: 'blob2.md',
+ type: 'blob',
+ commit: {
+ message: 'Updated blob2.md',
+ },
+ },
+ {
+ fileName: 'blob3.md',
+ type: 'blob',
+ commit: {
+ message: 'Updated blob3.md',
+ },
+ },
+];
+
+function factory({ path, isLoading = false, hasMore = true, entries = {}, commits = [] }) {
vm = shallowMount(Table, {
propsData: {
path,
isLoading,
entries,
hasMore,
+ commits,
},
mocks: {
$apollo,
},
+ provide: {
+ glFeatures: { lazyLoadCommits: true },
+ },
});
}
@@ -82,12 +110,15 @@ describe('Repository table component', () => {
entries: {
blobs: MOCK_BLOBS,
},
+ commits: MOCK_COMMITS,
});
const rows = vm.findAll(TableRow);
expect(rows.length).toEqual(3);
expect(rows.at(2).attributes().mode).toEqual('120000');
+ expect(rows.at(2).props().rowNumber).toBe(2);
+ expect(rows.at(2).props().commitInfo).toEqual(MOCK_COMMITS[2]);
});
describe('Show more button', () => {
diff --git a/spec/frontend/repository/components/table/row_spec.js b/spec/frontend/repository/components/table/row_spec.js
index da28c9873d9..76e9f7da011 100644
--- a/spec/frontend/repository/components/table/row_spec.js
+++ b/spec/frontend/repository/components/table/row_spec.js
@@ -1,10 +1,12 @@
-import { GlBadge, GlLink, GlIcon } from '@gitlab/ui';
+import { GlBadge, GlLink, GlIcon, GlIntersectionObserver } from '@gitlab/ui';
import { shallowMount, RouterLinkStub } from '@vue/test-utils';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import TableRow from '~/repository/components/table/row.vue';
import FileIcon from '~/vue_shared/components/file_icon.vue';
import { FILE_SYMLINK_MODE } from '~/vue_shared/constants';
+const COMMIT_MOCK = { lockLabel: 'Locked by Root', committedDate: '2019-01-01' };
+
let vm;
let $router;
@@ -20,12 +22,14 @@ function factory(propsData = {}) {
projectPath: 'gitlab-org/gitlab-ce',
url: `https://test.com`,
totalEntries: 10,
+ commitInfo: COMMIT_MOCK,
+ rowNumber: 123,
},
directives: {
GlHoverLoad: createMockDirective(),
},
provide: {
- glFeatures: { refactorBlobViewer: true },
+ glFeatures: { refactorBlobViewer: true, lazyLoadCommits: true },
},
mocks: {
$router,
@@ -40,6 +44,7 @@ function factory(propsData = {}) {
describe('Repository table row component', () => {
const findRouterLink = () => vm.find(RouterLinkStub);
+ const findIntersectionObserver = () => vm.findComponent(GlIntersectionObserver);
afterEach(() => {
vm.destroy();
@@ -226,8 +231,6 @@ describe('Repository table row component', () => {
currentPath: '/',
});
- vm.setData({ commit: { lockLabel: 'Locked by Root', committedDate: '2019-01-01' } });
-
return vm.vm.$nextTick().then(() => {
expect(vm.find(GlIcon).exists()).toBe(true);
expect(vm.find(GlIcon).props('name')).toBe('lock');
@@ -246,4 +249,27 @@ describe('Repository table row component', () => {
expect(vm.find(FileIcon).props('loading')).toBe(true);
});
+
+ describe('row visibility', () => {
+ beforeEach(() => {
+ factory({
+ id: '1',
+ sha: '1',
+ path: 'test',
+ type: 'tree',
+ currentPath: '/',
+ });
+ });
+ it('emits a `row-appear` event', () => {
+ findIntersectionObserver().vm.$emit('appear');
+ expect(vm.emitted('row-appear')).toEqual([
+ [
+ {
+ hasCommit: true,
+ rowNumber: 123,
+ },
+ ],
+ ]);
+ });
+ });
});
diff --git a/spec/frontend/repository/components/tree_content_spec.js b/spec/frontend/repository/components/tree_content_spec.js
index e36287eff29..49397c77215 100644
--- a/spec/frontend/repository/components/tree_content_spec.js
+++ b/spec/frontend/repository/components/tree_content_spec.js
@@ -3,6 +3,13 @@ import paginatedTreeQuery from 'shared_queries/repository/paginated_tree.query.g
import FilePreview from '~/repository/components/preview/index.vue';
import FileTable from '~/repository/components/table/index.vue';
import TreeContent from '~/repository/components/tree_content.vue';
+import { loadCommits, isRequested, resetRequestedCommits } from '~/repository/commits_service';
+
+jest.mock('~/repository/commits_service', () => ({
+ loadCommits: jest.fn(() => Promise.resolve()),
+ isRequested: jest.fn(),
+ resetRequestedCommits: jest.fn(),
+}));
let vm;
let $apollo;
@@ -23,6 +30,7 @@ function factory(path, data = () => ({})) {
glFeatures: {
increasePageSizeExponentially: true,
paginatedTreeGraphqlQuery: true,
+ lazyLoadCommits: true,
},
},
});
@@ -45,7 +53,7 @@ describe('Repository table component', () => {
expect(vm.find(FilePreview).exists()).toBe(true);
});
- it('trigger fetchFiles when mounted', async () => {
+ it('trigger fetchFiles and resetRequestedCommits when mounted', async () => {
factory('/');
jest.spyOn(vm.vm, 'fetchFiles').mockImplementation(() => {});
@@ -53,6 +61,7 @@ describe('Repository table component', () => {
await vm.vm.$nextTick();
expect(vm.vm.fetchFiles).toHaveBeenCalled();
+ expect(resetRequestedCommits).toHaveBeenCalled();
});
describe('normalizeData', () => {
@@ -180,4 +189,15 @@ describe('Repository table component', () => {
});
});
});
+
+ it('loads commit data when row-appear event is emitted', () => {
+ const path = 'some/path';
+ const rowNumber = 1;
+
+ factory(path);
+ findFileTable().vm.$emit('row-appear', { hasCommit: false, rowNumber });
+
+ expect(isRequested).toHaveBeenCalledWith(rowNumber);
+ expect(loadCommits).toHaveBeenCalledWith('', path, '', rowNumber);
+ });
});
diff --git a/spec/frontend/repository/router_spec.js b/spec/frontend/repository/router_spec.js
index bb82fa706fd..3f822db601f 100644
--- a/spec/frontend/repository/router_spec.js
+++ b/spec/frontend/repository/router_spec.js
@@ -24,4 +24,32 @@ describe('Repository router spec', () => {
expect(componentsForRoute).toContain(component);
}
});
+
+ describe('Storing Web IDE path globally', () => {
+ const proj = 'foo-bar-group/foo-bar-proj';
+ let originalGl;
+
+ beforeEach(() => {
+ originalGl = window.gl;
+ });
+
+ afterEach(() => {
+ window.gl = originalGl;
+ });
+
+ it.each`
+ path | branch | expectedPath
+ ${'/'} | ${'main'} | ${`/-/ide/project/${proj}/edit/main/-/`}
+ ${'/tree/main'} | ${'main'} | ${`/-/ide/project/${proj}/edit/main/-/`}
+ ${'/tree/feat(test)'} | ${'feat(test)'} | ${`/-/ide/project/${proj}/edit/feat(test)/-/`}
+ ${'/-/tree/main'} | ${'main'} | ${`/-/ide/project/${proj}/edit/main/-/`}
+ ${'/-/tree/main/app/assets'} | ${'main'} | ${`/-/ide/project/${proj}/edit/main/-/app/assets/`}
+ ${'/-/blob/main/file.md'} | ${'main'} | ${`/-/ide/project/${proj}/edit/main/-/file.md`}
+ `('generates the correct Web IDE url for $path', ({ path, branch, expectedPath } = {}) => {
+ const router = createRouter(proj, branch);
+
+ router.push(path);
+ expect(window.gl.webIDEPath).toBe(expectedPath);
+ });
+ });
});