diff --git a/app/assets/javascripts/repository/components/header_area.vue b/app/assets/javascripts/repository/components/header_area.vue
index b701dfb7d97ea59762e167a91106c7a31639af62..2fe1e5528ec1e293b82e5291d5c4918d0f0041ba 100644
--- a/app/assets/javascripts/repository/components/header_area.vue
+++ b/app/assets/javascripts/repository/components/header_area.vue
@@ -369,7 +369,11 @@ export default {
:show-web-ide-button="showWebIdeButton"
:show-gitpod-button="isGitpodEnabledForInstance"
/>
-
+
-
-
diff --git a/app/assets/javascripts/repository/components/header_area/permalink_dropdown_item.vue b/app/assets/javascripts/repository/components/header_area/permalink_dropdown_item.vue
index b692c37138be216c9f8b616fc7edbc5ba1d501c8..abdd463db5e1ca99a8c53d5235de56b19b6d064d 100644
--- a/app/assets/javascripts/repository/components/header_area/permalink_dropdown_item.vue
+++ b/app/assets/javascripts/repository/components/header_area/permalink_dropdown_item.vue
@@ -21,6 +21,11 @@ export default {
required: true,
},
},
+ data() {
+ return {
+ mousetrap: null,
+ };
+ },
computed: {
permalinkShortcutKey() {
return keysFor(PROJECT_FILES_GO_TO_PERMALINK)[0];
@@ -39,10 +44,11 @@ export default {
},
},
mounted() {
- Mousetrap.bind(keysFor(PROJECT_FILES_GO_TO_PERMALINK), this.triggerCopyPermalink);
+ this.mousetrap = new Mousetrap();
+ this.mousetrap.bind(keysFor(PROJECT_FILES_GO_TO_PERMALINK), this.triggerCopyPermalink);
},
beforeDestroy() {
- Mousetrap.unbind(keysFor(PROJECT_FILES_GO_TO_PERMALINK));
+ this.mousetrap.unbind(keysFor(PROJECT_FILES_GO_TO_PERMALINK));
},
methods: {
triggerCopyPermalink() {
diff --git a/app/assets/javascripts/repository/components/header_area/repository_overflow_menu.vue b/app/assets/javascripts/repository/components/header_area/repository_overflow_menu.vue
index 5d0846384233bc3723c99c1cc65ce669a372b5b6..8707676b6367f2e0803c10d38382fc0561768a31 100644
--- a/app/assets/javascripts/repository/components/header_area/repository_overflow_menu.vue
+++ b/app/assets/javascripts/repository/components/header_area/repository_overflow_menu.vue
@@ -1,6 +1,10 @@
@@ -42,6 +98,7 @@ export default {
:toggle-text="$options.i18n.dropdownLabel"
text-sr-only
>
+
diff --git a/app/assets/javascripts/repository/queries/permalink_path.query.graphql b/app/assets/javascripts/repository/queries/permalink_path.query.graphql
new file mode 100644
index 0000000000000000000000000000000000000000..65bd5253dff058ff33a49f15a50ab7b5dde8af4d
--- /dev/null
+++ b/app/assets/javascripts/repository/queries/permalink_path.query.graphql
@@ -0,0 +1,13 @@
+query getPermalinkPath($fullPath: ID!, $path: String!, $ref: String!) {
+ project(fullPath: $fullPath) {
+ id
+ repository {
+ paginatedTree(path: $path, ref: $ref) {
+ nodes {
+ __typename
+ permalinkPath
+ }
+ }
+ }
+ }
+}
diff --git a/spec/frontend/repository/components/header_area/mock_data.js b/spec/frontend/repository/components/header_area/mock_data.js
index 34aa456100d066406794e44f0b9edaabaeff23b9..3aecb35faf252a7f405c7fc0682229567a505756 100644
--- a/spec/frontend/repository/components/header_area/mock_data.js
+++ b/spec/frontend/repository/components/header_area/mock_data.js
@@ -64,3 +64,47 @@ export const openMRsDetailResult = jest.fn().mockResolvedValue({
},
},
});
+
+export const mockPermalinkResult = jest.fn().mockResolvedValue({
+ data: {
+ project: {
+ id: '1',
+ repository: {
+ paginatedTree: {
+ nodes: [
+ {
+ __typename: 'Tree',
+ permalinkPath:
+ '/gitlab-org/gitlab-shell/-/tree/5059017dea6e834f2f86fc670703ca36cbae98d6/cmd',
+ },
+ ],
+ __typename: 'TreeConnection',
+ },
+ __typename: 'Repository',
+ },
+ __typename: 'Project',
+ },
+ },
+});
+
+export const mockRootPermalinkResult = jest.fn().mockResolvedValue({
+ data: {
+ project: {
+ id: '2',
+ repository: {
+ paginatedTree: {
+ nodes: [
+ {
+ __typename: 'Tree',
+ permalinkPath:
+ '/gitlab-org/gitlab-shell/-/tree/5059017dea6e834f2f86fc670703ca36cbae98d6/',
+ },
+ ],
+ __typename: 'TreeConnection',
+ },
+ __typename: 'Repository',
+ },
+ __typename: 'Project',
+ },
+ },
+});
diff --git a/spec/frontend/repository/components/header_area/permalink_dropdown_item_spec.js b/spec/frontend/repository/components/header_area/permalink_dropdown_item_spec.js
index 2b079eadd1acdbd5ff494bb6c489f0754db6c221..0f1d69f353d8a81e7fea9e341fe83f1155ee5dd1 100644
--- a/spec/frontend/repository/components/header_area/permalink_dropdown_item_spec.js
+++ b/spec/frontend/repository/components/header_area/permalink_dropdown_item_spec.js
@@ -71,9 +71,14 @@ describe('PermalinkDropdownItem', () => {
it('triggers copy permalink when shortcut is used', async () => {
const clickSpy = jest.spyOn(findPermalinkLinkDropdown().element, 'click');
- Mousetrap.trigger('y');
+ const mousetrapInstance = wrapper.vm.mousetrap;
+
+ const triggerSpy = jest.spyOn(mousetrapInstance, 'trigger');
+ mousetrapInstance.trigger('y');
+
await nextTick();
+ expect(triggerSpy).toHaveBeenCalledWith('y');
expect(clickSpy).toHaveBeenCalled();
expect(mockToastShow).toHaveBeenCalledWith('Permalink copied to clipboard.');
});
@@ -81,8 +86,8 @@ describe('PermalinkDropdownItem', () => {
describe('lifecycle hooks', () => {
it('binds and unbinds Mousetrap shortcuts', () => {
- const bindSpy = jest.spyOn(Mousetrap, 'bind');
- const unbindSpy = jest.spyOn(Mousetrap, 'unbind');
+ const bindSpy = jest.spyOn(Mousetrap.prototype, 'bind');
+ const unbindSpy = jest.spyOn(Mousetrap.prototype, 'unbind');
createComponent();
expect(bindSpy).toHaveBeenCalledWith(
diff --git a/spec/frontend/repository/components/header_area/repository_overflow_menu_spec.js b/spec/frontend/repository/components/header_area/repository_overflow_menu_spec.js
index 3f9c2483e3ec9b2456f2bb5857b788d87510855c..278146caecb9e3015fb7ed4b5cdc871370e978ad 100644
--- a/spec/frontend/repository/components/header_area/repository_overflow_menu_spec.js
+++ b/spec/frontend/repository/components/header_area/repository_overflow_menu_spec.js
@@ -1,7 +1,27 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
import { RouterLinkStub } from '@vue/test-utils';
import { GlDisclosureDropdownItem } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import RepositoryOverflowMenu from '~/repository/components/header_area/repository_overflow_menu.vue';
+import PermalinkDropdownItem from '~/repository/components/header_area/permalink_dropdown_item.vue';
+import permalinkPathQuery from '~/repository/queries/permalink_path.query.graphql';
+import { logError } from '~/lib/logger';
+import {
+ mockPermalinkResult,
+ mockRootPermalinkResult,
+} from 'jest/repository/components/header_area/mock_data';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
+
+Vue.use(VueApollo);
+jest.mock('~/lib/logger');
+jest.mock('~/sentry/sentry_browser_wrapper');
+
+const path = 'cmd';
+const projectPath = 'gitlab-org/gitlab-shell';
+const ref = '5059017dea6e834f2f86fc670703ca36cbae98d6';
const defaultMockRoute = {
params: {
@@ -18,18 +38,34 @@ const defaultMockRoute = {
describe('RepositoryOverflowMenu', () => {
let wrapper;
-
+ let permalinkQueryHandler;
const findDropdownItems = () => wrapper.findAllComponents(GlDisclosureDropdownItem);
const findDropdownItemWithText = (text) =>
findDropdownItems().wrappers.find((x) => x.props('item').text === text);
const findCompareItem = () => findDropdownItemWithText('Compare');
- const createComponent = (route = {}, provide = {}) => {
+ const findPermalinkItem = () => wrapper.findComponent(PermalinkDropdownItem);
+
+ const createComponent = ({
+ route = {},
+ provide = {},
+ props = {},
+ mockResolver = mockPermalinkResult,
+ } = {}) => {
+ permalinkQueryHandler = mockResolver;
+ const mockApollo = createMockApollo([[permalinkPathQuery, mockResolver]]);
+
return shallowMountExtended(RepositoryOverflowMenu, {
provide: {
comparePath: null,
...provide,
},
+ propsData: {
+ fullPath: projectPath,
+ path,
+ currentRef: ref,
+ ...props,
+ },
stubs: {
RouterLink: RouterLinkStub,
},
@@ -39,6 +75,7 @@ describe('RepositoryOverflowMenu', () => {
...route,
},
},
+ apolloProvider: mockApollo,
});
};
@@ -50,26 +87,72 @@ describe('RepositoryOverflowMenu', () => {
expect(wrapper.exists()).toBe(true);
});
- describe('Compare item', () => {
- it('does not render Compare button for root ref', () => {
- wrapper = createComponent({ params: { path: '/-/tree/new-branch-3' } });
- expect(findCompareItem()).toBeUndefined();
+ describe('computed properties', () => {
+ it('computes queryVariables correctly', () => {
+ expect(permalinkQueryHandler).toHaveBeenCalledWith({
+ fullPath: 'gitlab-org/gitlab-shell',
+ path: 'cmd',
+ ref: '5059017dea6e834f2f86fc670703ca36cbae98d6',
+ });
});
- it('renders Compare button for non-root ref', () => {
- wrapper = createComponent(
- { params: { path: '/-/tree/new-branch-3' } },
- { comparePath: 'test/project/-/compare?from=master&to=new-branch-3' },
- );
- expect(findCompareItem().exists()).toBe(true);
- expect(findCompareItem().props('item')).toMatchObject({
- href: 'test/project/-/compare?from=master&to=new-branch-3',
+ describe('Compare item', () => {
+ it('does not render Compare button for root ref', () => {
+ wrapper = createComponent({ route: { params: { path: '/-/tree/new-branch-3' } } });
+ expect(findCompareItem()).toBeUndefined();
+ });
+
+ it('renders Compare button for non-root ref', () => {
+ wrapper = createComponent({
+ route: {
+ params: { path: '/-/tree/new-branch-3' },
+ },
+ provide: { comparePath: 'test/project/-/compare?from=master&to=new-branch-3' },
+ });
+ expect(findCompareItem().exists()).toBe(true);
+ expect(findCompareItem().props('item')).toMatchObject({
+ href: 'test/project/-/compare?from=master&to=new-branch-3',
+ });
+ });
+
+ it('does not render compare button when comparePath is not provided', () => {
+ wrapper = createComponent();
+ expect(findCompareItem()).toBeUndefined();
});
});
- it('does not render compare button when comparePath is not provided', () => {
- wrapper = createComponent();
- expect(findCompareItem()).toBeUndefined();
+ describe('Permalink item', () => {
+ it('renders Permalink button for non-root route', async () => {
+ wrapper = createComponent();
+ await waitForPromises();
+ expect(findPermalinkItem().props('permalinkPath')).toBe(
+ '/gitlab-org/gitlab-shell/-/tree/5059017dea6e834f2f86fc670703ca36cbae98d6/cmd',
+ );
+ });
+
+ it('renders Permalink button with projectPath for root route', async () => {
+ wrapper = createComponent({
+ props: { path: undefined },
+ mockResolver: mockRootPermalinkResult,
+ });
+ await waitForPromises();
+ expect(findPermalinkItem().props('permalinkPath')).toBe(
+ '/gitlab-org/gitlab-shell/-/tree/5059017dea6e834f2f86fc670703ca36cbae98d6/',
+ );
+ });
+
+ it('handles errors when fetching permalinkPath', async () => {
+ const mockError = new Error();
+ wrapper = createComponent({ mockResolver: jest.fn().mockRejectedValueOnce(mockError) });
+ await waitForPromises();
+
+ expect(findPermalinkItem().exists()).toBe(false);
+ expect(logError).toHaveBeenCalledWith(
+ 'Failed to fetch permalink. See exception details for more information.',
+ mockError,
+ );
+ expect(Sentry.captureException).toHaveBeenCalledWith(mockError);
+ });
});
});
});
diff --git a/spec/frontend/repository/components/header_area_spec.js b/spec/frontend/repository/components/header_area_spec.js
index fda1fe13dac559ea8f8337c24567b559370cdc56..0834c6285696292198bb3b8ffa773bb30e89be84 100644
--- a/spec/frontend/repository/components/header_area_spec.js
+++ b/spec/frontend/repository/components/header_area_spec.js
@@ -209,19 +209,27 @@ describe('HeaderArea', () => {
});
describe('RepositoryOverflowMenu', () => {
- it('does not render RepositoryOverflowMenu component on default ref', () => {
- expect(findRepositoryOverflowMenu().exists()).toBe(false);
+ it('renders RepositoryOverflowMenu component with correct props when on default branch', () => {
+ wrapper = createComponent({
+ route: { name: 'treePathDecoded' },
+ });
+ expect(findRepositoryOverflowMenu().props()).toStrictEqual({
+ currentRef: 'main',
+ fullPath: 'test/project',
+ path: 'index.js',
+ });
});
- it('renders RepositoryOverflowMenu component with correct props when on ref different than default branch', () => {
+ it('renders RepositoryOverflowMenu component with correct props when on non-default branch', () => {
wrapper = createComponent({
route: { name: 'treePathDecoded' },
provided: { comparePath: 'test/project/compare' },
});
- expect(findRepositoryOverflowMenu().exists()).toBe(true);
- expect(findRepositoryOverflowMenu().props('comparePath')).toBe(
- headerAppInjected.comparePath,
- );
+ expect(findRepositoryOverflowMenu().props()).toStrictEqual({
+ currentRef: 'main',
+ fullPath: 'test/project',
+ path: 'index.js',
+ });
});
});
});