summaryrefslogtreecommitdiff
path: root/spec/frontend/pages
diff options
context:
space:
mode:
Diffstat (limited to 'spec/frontend/pages')
-rw-r--r--spec/frontend/pages/milestones/shared/components/promote_milestone_modal_spec.js4
-rw-r--r--spec/frontend/pages/projects/forks/new/components/app_spec.js1
-rw-r--r--spec/frontend/pages/projects/forks/new/components/fork_form_spec.js182
-rw-r--r--spec/frontend/pages/projects/forks/new/components/fork_groups_list_spec.js2
-rw-r--r--spec/frontend/pages/projects/graphs/__snapshots__/code_coverage_spec.js.snap2
-rw-r--r--spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_a_spec.js.snap2
-rw-r--r--spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_b_spec.js.snap2
-rw-r--r--spec/frontend/pages/projects/pipeline_schedules/shared/components/interval_pattern_input_spec.js27
-rw-r--r--spec/frontend/pages/shared/nav/sidebar_tracking_spec.js160
-rw-r--r--spec/frontend/pages/shared/wikis/components/wiki_form_spec.js115
-rw-r--r--spec/frontend/pages/users/activity_calendar_spec.js16
11 files changed, 463 insertions, 50 deletions
diff --git a/spec/frontend/pages/milestones/shared/components/promote_milestone_modal_spec.js b/spec/frontend/pages/milestones/shared/components/promote_milestone_modal_spec.js
index d22e0474e06..4280a78c202 100644
--- a/spec/frontend/pages/milestones/shared/components/promote_milestone_modal_spec.js
+++ b/spec/frontend/pages/milestones/shared/components/promote_milestone_modal_spec.js
@@ -3,7 +3,7 @@ import { shallowMount } from '@vue/test-utils';
import { setHTMLFixture } from 'helpers/fixtures';
import { TEST_HOST } from 'helpers/test_constants';
import waitForPromises from 'helpers/wait_for_promises';
-import * as flash from '~/flash';
+import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import * as urlUtils from '~/lib/utils/url_utility';
import PromoteMilestoneModal from '~/pages/milestones/shared/components/promote_milestone_modal.vue';
@@ -103,7 +103,7 @@ describe('Promote milestone modal', () => {
wrapper.findComponent(GlModal).vm.$emit('primary');
await waitForPromises();
- expect(flash.deprecatedCreateFlash).toHaveBeenCalledWith(dummyError);
+ expect(createFlash).toHaveBeenCalledWith({ message: dummyError });
});
});
});
diff --git a/spec/frontend/pages/projects/forks/new/components/app_spec.js b/spec/frontend/pages/projects/forks/new/components/app_spec.js
index e1820606704..a7b4b9c42bd 100644
--- a/spec/frontend/pages/projects/forks/new/components/app_spec.js
+++ b/spec/frontend/pages/projects/forks/new/components/app_spec.js
@@ -13,6 +13,7 @@ describe('App component', () => {
projectPath: 'project-name',
projectDescription: 'some project description',
projectVisibility: 'private',
+ restrictedVisibilityLevels: [],
};
const createComponent = (props = {}) => {
diff --git a/spec/frontend/pages/projects/forks/new/components/fork_form_spec.js b/spec/frontend/pages/projects/forks/new/components/fork_form_spec.js
index 6d853120232..c80ccfa8256 100644
--- a/spec/frontend/pages/projects/forks/new/components/fork_form_spec.js
+++ b/spec/frontend/pages/projects/forks/new/components/fork_form_spec.js
@@ -1,4 +1,5 @@
-import { GlFormInputGroup, GlFormInput, GlForm } from '@gitlab/ui';
+import { GlFormInputGroup, GlFormInput, GlForm, GlFormRadioGroup, GlFormRadio } from '@gitlab/ui';
+import { getByRole, getAllByRole } from '@testing-library/dom';
import { mount, shallowMount } from '@vue/test-utils';
import axios from 'axios';
import AxiosMockAdapter from 'axios-mock-adapter';
@@ -15,6 +16,13 @@ describe('ForkForm component', () => {
let wrapper;
let axiosMock;
+ const PROJECT_VISIBILITY_TYPE = {
+ private:
+ 'Private Project access must be granted explicitly to each user. If this project is part of a group, access will be granted to members of the group.',
+ internal: 'Internal The project can be accessed by any logged in user.',
+ public: 'Public The project can be accessed without any authentication.',
+ };
+
const GON_GITLAB_URL = 'https://gitlab.com';
const GON_API_VERSION = 'v7';
@@ -37,6 +45,7 @@ describe('ForkForm component', () => {
projectPath: 'project-name',
projectDescription: 'some project description',
projectVisibility: 'private',
+ restrictedVisibilityLevels: [],
};
const mockGetRequest = (data = {}, statusCode = httpStatus.OK) => {
@@ -61,6 +70,8 @@ describe('ForkForm component', () => {
stubs: {
GlFormInputGroup,
GlFormInput,
+ GlFormRadioGroup,
+ GlFormRadio,
},
});
};
@@ -81,6 +92,7 @@ describe('ForkForm component', () => {
axiosMock.restore();
});
+ const findFormSelectOptions = () => wrapper.find('select[name="namespace"]').findAll('option');
const findPrivateRadio = () => wrapper.find('[data-testid="radio-private"]');
const findInternalRadio = () => wrapper.find('[data-testid="radio-internal"]');
const findPublicRadio = () => wrapper.find('[data-testid="radio-public"]');
@@ -203,24 +215,145 @@ describe('ForkForm component', () => {
});
describe('visibility level', () => {
+ it('displays the correct description', () => {
+ mockGetRequest();
+ createComponent();
+
+ const formRadios = wrapper.findAll(GlFormRadio);
+
+ Object.keys(PROJECT_VISIBILITY_TYPE).forEach((visibilityType, index) => {
+ expect(formRadios.at(index).text()).toBe(PROJECT_VISIBILITY_TYPE[visibilityType]);
+ });
+ });
+
+ it('displays all 3 visibility levels', () => {
+ mockGetRequest();
+ createComponent();
+
+ expect(wrapper.findAll(GlFormRadio)).toHaveLength(3);
+ });
+
+ describe('when the namespace is changed', () => {
+ const namespaces = [
+ {
+ visibility: 'private',
+ },
+ {
+ visibility: 'internal',
+ },
+ {
+ visibility: 'public',
+ },
+ ];
+
+ beforeEach(() => {
+ mockGetRequest();
+ });
+
+ it('resets the visibility to default "private"', async () => {
+ createFullComponent({ projectVisibility: 'public' }, { namespaces });
+
+ expect(wrapper.vm.form.fields.visibility.value).toBe('public');
+ await findFormSelectOptions().at(1).setSelected();
+
+ await wrapper.vm.$nextTick();
+
+ expect(getByRole(wrapper.element, 'radio', { name: /private/i }).checked).toBe(true);
+ });
+
+ it('sets the visibility to be null when restrictedVisibilityLevels is set', async () => {
+ createFullComponent({ restrictedVisibilityLevels: [10] }, { namespaces });
+
+ await findFormSelectOptions().at(1).setSelected();
+
+ await wrapper.vm.$nextTick();
+
+ const container = getByRole(wrapper.element, 'radiogroup', { name: /visibility/i });
+ const visibilityRadios = getAllByRole(container, 'radio');
+ expect(visibilityRadios.filter((e) => e.checked)).toHaveLength(0);
+ });
+ });
+
+ it.each`
+ project | restrictedVisibilityLevels
+ ${'private'} | ${[]}
+ ${'internal'} | ${[]}
+ ${'public'} | ${[]}
+ ${'private'} | ${[0]}
+ ${'private'} | ${[10]}
+ ${'private'} | ${[20]}
+ ${'private'} | ${[0, 10]}
+ ${'private'} | ${[0, 20]}
+ ${'private'} | ${[10, 20]}
+ ${'private'} | ${[0, 10, 20]}
+ ${'internal'} | ${[0]}
+ ${'internal'} | ${[10]}
+ ${'internal'} | ${[20]}
+ ${'internal'} | ${[0, 10]}
+ ${'internal'} | ${[0, 20]}
+ ${'internal'} | ${[10, 20]}
+ ${'internal'} | ${[0, 10, 20]}
+ ${'public'} | ${[0]}
+ ${'public'} | ${[10]}
+ ${'public'} | ${[0, 10]}
+ ${'public'} | ${[0, 20]}
+ ${'public'} | ${[10, 20]}
+ ${'public'} | ${[0, 10, 20]}
+ `('checks the correct radio button', async ({ project, restrictedVisibilityLevels }) => {
+ mockGetRequest();
+ createFullComponent({
+ projectVisibility: project,
+ restrictedVisibilityLevels,
+ });
+
+ if (restrictedVisibilityLevels.length === 0) {
+ expect(wrapper.find('[name="visibility"]:checked').attributes('value')).toBe(project);
+ } else {
+ expect(wrapper.find('[name="visibility"]:checked').exists()).toBe(false);
+ }
+ });
+
it.each`
- project | namespace | privateIsDisabled | internalIsDisabled | publicIsDisabled
- ${'private'} | ${'private'} | ${undefined} | ${'true'} | ${'true'}
- ${'private'} | ${'internal'} | ${undefined} | ${'true'} | ${'true'}
- ${'private'} | ${'public'} | ${undefined} | ${'true'} | ${'true'}
- ${'internal'} | ${'private'} | ${undefined} | ${'true'} | ${'true'}
- ${'internal'} | ${'internal'} | ${undefined} | ${undefined} | ${'true'}
- ${'internal'} | ${'public'} | ${undefined} | ${undefined} | ${'true'}
- ${'public'} | ${'private'} | ${undefined} | ${'true'} | ${'true'}
- ${'public'} | ${'internal'} | ${undefined} | ${undefined} | ${'true'}
- ${'public'} | ${'public'} | ${undefined} | ${undefined} | ${undefined}
+ project | namespace | privateIsDisabled | internalIsDisabled | publicIsDisabled | restrictedVisibilityLevels
+ ${'private'} | ${'private'} | ${undefined} | ${'true'} | ${'true'} | ${[]}
+ ${'private'} | ${'internal'} | ${undefined} | ${'true'} | ${'true'} | ${[]}
+ ${'private'} | ${'public'} | ${undefined} | ${'true'} | ${'true'} | ${[]}
+ ${'internal'} | ${'private'} | ${undefined} | ${'true'} | ${'true'} | ${[]}
+ ${'internal'} | ${'internal'} | ${undefined} | ${undefined} | ${'true'} | ${[]}
+ ${'internal'} | ${'public'} | ${undefined} | ${undefined} | ${'true'} | ${[]}
+ ${'public'} | ${'private'} | ${undefined} | ${'true'} | ${'true'} | ${[]}
+ ${'public'} | ${'internal'} | ${undefined} | ${undefined} | ${'true'} | ${[]}
+ ${'public'} | ${'public'} | ${undefined} | ${undefined} | ${undefined} | ${[]}
+ ${'private'} | ${'private'} | ${undefined} | ${'true'} | ${'true'} | ${[0]}
+ ${'internal'} | ${'internal'} | ${'true'} | ${undefined} | ${'true'} | ${[0]}
+ ${'public'} | ${'public'} | ${'true'} | ${undefined} | ${undefined} | ${[0]}
+ ${'private'} | ${'private'} | ${undefined} | ${'true'} | ${'true'} | ${[10]}
+ ${'internal'} | ${'internal'} | ${undefined} | ${'true'} | ${'true'} | ${[10]}
+ ${'public'} | ${'public'} | ${undefined} | ${'true'} | ${undefined} | ${[10]}
+ ${'private'} | ${'private'} | ${undefined} | ${'true'} | ${'true'} | ${[20]}
+ ${'internal'} | ${'internal'} | ${undefined} | ${undefined} | ${'true'} | ${[20]}
+ ${'public'} | ${'public'} | ${undefined} | ${undefined} | ${'true'} | ${[20]}
+ ${'private'} | ${'private'} | ${undefined} | ${'true'} | ${'true'} | ${[10, 20]}
+ ${'internal'} | ${'internal'} | ${undefined} | ${'true'} | ${'true'} | ${[10, 20]}
+ ${'public'} | ${'public'} | ${undefined} | ${'true'} | ${'true'} | ${[10, 20]}
+ ${'private'} | ${'private'} | ${undefined} | ${'true'} | ${'true'} | ${[0, 10, 20]}
+ ${'internal'} | ${'internal'} | ${undefined} | ${'true'} | ${'true'} | ${[0, 10, 20]}
+ ${'public'} | ${'public'} | ${undefined} | ${'true'} | ${'true'} | ${[0, 10, 20]}
`(
'sets appropriate radio button disabled state',
- async ({ project, namespace, privateIsDisabled, internalIsDisabled, publicIsDisabled }) => {
+ async ({
+ project,
+ namespace,
+ privateIsDisabled,
+ internalIsDisabled,
+ publicIsDisabled,
+ restrictedVisibilityLevels,
+ }) => {
mockGetRequest();
createComponent(
{
projectVisibility: project,
+ restrictedVisibilityLevels,
},
{
form: { fields: { namespace: { value: { visibility: namespace } } } },
@@ -235,7 +368,7 @@ describe('ForkForm component', () => {
});
describe('onSubmit', () => {
- beforeEach(() => {
+ const setupComponent = (fields = {}) => {
jest.spyOn(urlUtility, 'redirectTo').mockImplementation();
mockGetRequest();
@@ -245,9 +378,14 @@ describe('ForkForm component', () => {
namespaces: MOCK_NAMESPACES_RESPONSE,
form: {
state: true,
+ ...fields,
},
},
);
+ };
+
+ beforeEach(() => {
+ setupComponent();
});
const selectedMockNamespaceIndex = 1;
@@ -279,6 +417,22 @@ describe('ForkForm component', () => {
expect(urlUtility.redirectTo).not.toHaveBeenCalled();
});
+
+ it('does not make POST request if no visbility is checked', async () => {
+ jest.spyOn(axios, 'post');
+
+ setupComponent({
+ fields: {
+ visibility: {
+ value: null,
+ },
+ },
+ });
+
+ await submitForm();
+
+ expect(axios.post).not.toHaveBeenCalled();
+ });
});
describe('with valid form', () => {
@@ -330,7 +484,7 @@ describe('ForkForm component', () => {
expect(urlUtility.redirectTo).not.toHaveBeenCalled();
expect(createFlash).toHaveBeenCalledWith({
- message: dummyError,
+ message: 'An error occurred while forking the project. Please try again.',
});
});
});
diff --git a/spec/frontend/pages/projects/forks/new/components/fork_groups_list_spec.js b/spec/frontend/pages/projects/forks/new/components/fork_groups_list_spec.js
index e7ac837a4c8..9f8dbf3d542 100644
--- a/spec/frontend/pages/projects/forks/new/components/fork_groups_list_spec.js
+++ b/spec/frontend/pages/projects/forks/new/components/fork_groups_list_spec.js
@@ -3,7 +3,7 @@ import { shallowMount } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
+import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import ForkGroupsList from '~/pages/projects/forks/new/components/fork_groups_list.vue';
import ForkGroupsListItem from '~/pages/projects/forks/new/components/fork_groups_list_item.vue';
diff --git a/spec/frontend/pages/projects/graphs/__snapshots__/code_coverage_spec.js.snap b/spec/frontend/pages/projects/graphs/__snapshots__/code_coverage_spec.js.snap
index c4c48ea7517..4ba9120d196 100644
--- a/spec/frontend/pages/projects/graphs/__snapshots__/code_coverage_spec.js.snap
+++ b/spec/frontend/pages/projects/graphs/__snapshots__/code_coverage_spec.js.snap
@@ -66,7 +66,7 @@ exports[`Code Coverage when fetching data is successful matches the snapshot 1`]
<gl-area-chart-stub
annotations=""
data="[object Object]"
- formattooltiptext="function () { [native code] }"
+ formattooltiptext="[Function]"
height="200"
includelegendavgmax="true"
legendaveragetext="Avg"
diff --git a/spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_a_spec.js.snap b/spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_a_spec.js.snap
index 350669433f0..59b42de2485 100644
--- a/spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_a_spec.js.snap
+++ b/spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_a_spec.js.snap
@@ -92,6 +92,7 @@ exports[`Learn GitLab Design A renders correctly 1`] = `
aria-hidden="true"
class="gl-icon s16"
data-testid="completed-icon"
+ role="img"
>
<use
href="#check-circle-filled"
@@ -114,6 +115,7 @@ exports[`Learn GitLab Design A renders correctly 1`] = `
aria-hidden="true"
class="gl-icon s16"
data-testid="completed-icon"
+ role="img"
>
<use
href="#check-circle-filled"
diff --git a/spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_b_spec.js.snap b/spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_b_spec.js.snap
index c9d8ab4566c..091edc7505c 100644
--- a/spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_b_spec.js.snap
+++ b/spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_b_spec.js.snap
@@ -81,6 +81,7 @@ exports[`Learn GitLab Design B renders correctly 1`] = `
aria-hidden="true"
class="gl-text-green-500 gl-icon s16"
data-testid="completed-icon"
+ role="img"
>
<use
href="#check-circle-filled"
@@ -142,6 +143,7 @@ exports[`Learn GitLab Design B renders correctly 1`] = `
aria-hidden="true"
class="gl-text-green-500 gl-icon s16"
data-testid="completed-icon"
+ role="img"
>
<use
href="#check-circle-filled"
diff --git a/spec/frontend/pages/projects/pipeline_schedules/shared/components/interval_pattern_input_spec.js b/spec/frontend/pages/projects/pipeline_schedules/shared/components/interval_pattern_input_spec.js
index 27cd0fe34bf..de0d70a07d7 100644
--- a/spec/frontend/pages/projects/pipeline_schedules/shared/components/interval_pattern_input_spec.js
+++ b/spec/frontend/pages/projects/pipeline_schedules/shared/components/interval_pattern_input_spec.js
@@ -1,3 +1,4 @@
+import { GlIcon } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { trimText } from 'helpers/text_helper';
import IntervalPatternInput from '~/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue';
@@ -27,6 +28,7 @@ describe('Interval Pattern Input Component', () => {
const findAllLabels = () => wrapper.findAll('label');
const findSelectedRadio = () =>
wrapper.findAll('input[type="radio"]').wrappers.find((x) => x.element.checked);
+ const findIcon = () => wrapper.findComponent(GlIcon);
const findSelectedRadioKey = () => findSelectedRadio()?.attributes('data-testid');
const selectEveryDayRadio = () => findEveryDayRadio().trigger('click');
const selectEveryWeekRadio = () => findEveryWeekRadio().trigger('click');
@@ -40,6 +42,11 @@ describe('Interval Pattern Input Component', () => {
wrapper = mount(IntervalPatternInput, {
propsData: { ...props },
+ provide: {
+ glFeatures: {
+ ciDailyLimitForPipelineSchedules: true,
+ },
+ },
data() {
return {
randomHour: data?.hour || mockHour,
@@ -202,4 +209,24 @@ describe('Interval Pattern Input Component', () => {
expect(findSelectedRadioKey()).toBe(customKey);
});
});
+
+ describe('Custom cron syntax quota info', () => {
+ it('the info message includes 5 minutes', () => {
+ createWrapper({ dailyLimit: '288' });
+
+ expect(findIcon().attributes('title')).toContain('5 minutes');
+ });
+
+ it('the info message includes 60 minutes', () => {
+ createWrapper({ dailyLimit: '24' });
+
+ expect(findIcon().attributes('title')).toContain('60 minutes');
+ });
+
+ it('the info message icon is not shown when there is no daily limit', () => {
+ createWrapper();
+
+ expect(findIcon().exists()).toBe(false);
+ });
+ });
});
diff --git a/spec/frontend/pages/shared/nav/sidebar_tracking_spec.js b/spec/frontend/pages/shared/nav/sidebar_tracking_spec.js
new file mode 100644
index 00000000000..2c8eb8e459f
--- /dev/null
+++ b/spec/frontend/pages/shared/nav/sidebar_tracking_spec.js
@@ -0,0 +1,160 @@
+import { setHTMLFixture } from 'helpers/fixtures';
+import { initSidebarTracking } from '~/pages/shared/nav/sidebar_tracking';
+
+describe('~/pages/shared/nav/sidebar_tracking.js', () => {
+ beforeEach(() => {
+ setHTMLFixture(`
+ <aside class="nav-sidebar">
+ <div class="nav-sidebar-inner-scroll">
+ <ul class="sidebar-top-level-items">
+ <li data-track-label="project_information_menu" class="home">
+ <a aria-label="Project information" class="shortcuts-project-information has-sub-items" href="">
+ <span class="nav-icon-container">
+ <svg class="s16" data-testid="project-icon">
+ <use xlink:href="/assets/icons-1b2dadc4c3d49797908ba67b8f10da5d63dd15d859bde28d66fb60bbb97a4dd5.svg#project"></use>
+ </svg>
+ </span>
+ <span class="nav-item-name">Project information</span>
+ </a>
+ <ul class="sidebar-sub-level-items">
+ <li class="fly-out-top-item">
+ <a aria-label="Project information" href="#">
+ <strong class="fly-out-top-item-name">Project information</strong>
+ </a>
+ </li>
+ <li class="divider fly-out-top-item"></li>
+ <li data-track-label="activity" class="">
+ <a aria-label="Activity" class="shortcuts-project-activity" href=#">
+ <span>Activity</span>
+ </a>
+ </li>
+ <li data-track-label="labels" class="">
+ <a aria-label="Labels" href="#">
+ <span>Labels</span>
+ </a>
+ </li>
+ <li data-track-label="members" class="">
+ <a aria-label="Members" href="#">
+ <span>Members</span>
+ </a>
+ </li>
+ </ul>
+ </li>
+ </ul>
+ </div>
+ </aside>
+ `);
+
+ initSidebarTracking();
+ });
+
+ describe('sidebar is not collapsed', () => {
+ describe('menu is not expanded', () => {
+ it('sets the proper data tracking attributes when clicking on menu', () => {
+ const menu = document.querySelector('li[data-track-label="project_information_menu"]');
+ const menuLink = menu.querySelector('a');
+
+ menu.classList.add('is-over', 'is-showing-fly-out');
+ menuLink.click();
+
+ expect(menu.dataset).toMatchObject({
+ trackAction: 'click_menu',
+ trackExtra: JSON.stringify({
+ sidebar_display: 'Expanded',
+ menu_display: 'Fly out',
+ }),
+ });
+ });
+
+ it('sets the proper data tracking attributes when clicking on submenu', () => {
+ const menu = document.querySelector('li[data-track-label="activity"]');
+ const menuLink = menu.querySelector('a');
+ const submenuList = document.querySelector('ul.sidebar-sub-level-items');
+
+ submenuList.classList.add('fly-out-list');
+ menuLink.click();
+
+ expect(menu.dataset).toMatchObject({
+ trackAction: 'click_menu_item',
+ trackExtra: JSON.stringify({
+ sidebar_display: 'Expanded',
+ menu_display: 'Fly out',
+ }),
+ });
+ });
+ });
+
+ describe('menu is expanded', () => {
+ it('sets the proper data tracking attributes when clicking on menu', () => {
+ const menu = document.querySelector('li[data-track-label="project_information_menu"]');
+ const menuLink = menu.querySelector('a');
+
+ menu.classList.add('active');
+ menuLink.click();
+
+ expect(menu.dataset).toMatchObject({
+ trackAction: 'click_menu',
+ trackExtra: JSON.stringify({
+ sidebar_display: 'Expanded',
+ menu_display: 'Expanded',
+ }),
+ });
+ });
+
+ it('sets the proper data tracking attributes when clicking on submenu', () => {
+ const menu = document.querySelector('li[data-track-label="activity"]');
+ const menuLink = menu.querySelector('a');
+
+ menu.classList.add('active');
+ menuLink.click();
+
+ expect(menu.dataset).toMatchObject({
+ trackAction: 'click_menu_item',
+ trackExtra: JSON.stringify({
+ sidebar_display: 'Expanded',
+ menu_display: 'Expanded',
+ }),
+ });
+ });
+ });
+ });
+
+ describe('sidebar is collapsed', () => {
+ beforeEach(() => {
+ document.querySelector('aside.nav-sidebar').classList.add('js-sidebar-collapsed');
+ });
+
+ it('sets the proper data tracking attributes when clicking on menu', () => {
+ const menu = document.querySelector('li[data-track-label="project_information_menu"]');
+ const menuLink = menu.querySelector('a');
+
+ menu.classList.add('is-over', 'is-showing-fly-out');
+ menuLink.click();
+
+ expect(menu.dataset).toMatchObject({
+ trackAction: 'click_menu',
+ trackExtra: JSON.stringify({
+ sidebar_display: 'Collapsed',
+ menu_display: 'Fly out',
+ }),
+ });
+ });
+
+ it('sets the proper data tracking attributes when clicking on submenu', () => {
+ const menu = document.querySelector('li[data-track-label="activity"]');
+ const menuLink = menu.querySelector('a');
+ const submenuList = document.querySelector('ul.sidebar-sub-level-items');
+
+ submenuList.classList.add('fly-out-list');
+ menuLink.click();
+
+ expect(menu.dataset).toMatchObject({
+ trackAction: 'click_menu_item',
+ trackExtra: JSON.stringify({
+ sidebar_display: 'Collapsed',
+ menu_display: 'Fly out',
+ }),
+ });
+ });
+ });
+});
diff --git a/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js b/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js
index 1cac8ef8ee2..f36d6262b5f 100644
--- a/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js
+++ b/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js
@@ -2,15 +2,23 @@ import { GlLoadingIcon, GlModal } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
+import { mockTracking } from 'helpers/tracking_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import ContentEditor from '~/content_editor/components/content_editor.vue';
import WikiForm from '~/pages/shared/wikis/components/wiki_form.vue';
+import {
+ WIKI_CONTENT_EDITOR_TRACKING_LABEL,
+ CONTENT_EDITOR_LOADED_ACTION,
+ SAVED_USING_CONTENT_EDITOR_ACTION,
+} from '~/pages/shared/wikis/constants';
+
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
describe('WikiForm', () => {
let wrapper;
let mock;
+ let trackingSpy;
const findForm = () => wrapper.find('form');
const findTitle = () => wrapper.find('#wiki_title');
@@ -19,9 +27,11 @@ describe('WikiForm', () => {
const findMessage = () => wrapper.find('#wiki_message');
const findSubmitButton = () => wrapper.findByTestId('wiki-submit-button');
const findCancelButton = () => wrapper.findByRole('link', { name: 'Cancel' });
- const findUseNewEditorButton = () => wrapper.findByRole('button', { name: 'Use new editor' });
+ const findUseNewEditorButton = () => wrapper.findByRole('button', { name: 'Use the new editor' });
+ const findDismissContentEditorAlertButton = () =>
+ wrapper.findByRole('button', { name: 'Try this later' });
const findSwitchToOldEditorButton = () =>
- wrapper.findByRole('button', { name: 'Switch to old editor' });
+ wrapper.findByRole('button', { name: 'Switch me back to the classic editor.' });
const findTitleHelpLink = () => wrapper.findByRole('link', { name: 'More Information.' });
const findMarkdownHelpLink = () => wrapper.findByTestId('wiki-markdown-help-link');
@@ -55,15 +65,12 @@ describe('WikiForm', () => {
persisted: true,
title: 'My page',
- content: 'My page content',
+ content: ' My page content ',
format: 'markdown',
path: '/project/path/-/wikis/home',
};
- function createWrapper(
- persisted = false,
- { pageInfo, glFeatures } = { glFeatures: { wikiContentEditor: false } },
- ) {
+ function createWrapper(persisted = false, { pageInfo } = {}) {
wrapper = extendedWrapper(
mount(
WikiForm,
@@ -79,7 +86,6 @@ describe('WikiForm', () => {
...(persisted ? pageInfoPersisted : pageInfoNew),
...pageInfo,
},
- glFeatures,
},
},
{ attachToDocument: true },
@@ -88,6 +94,7 @@ describe('WikiForm', () => {
}
beforeEach(() => {
+ trackingSpy = mockTracking(undefined, null, jest.spyOn);
mock = new MockAdapter(axios);
});
@@ -124,6 +131,12 @@ describe('WikiForm', () => {
expect(findMessage().element.value).toBe('Update My page');
});
+ it('does not trim page content by default', () => {
+ createWrapper(true);
+
+ expect(findContent().element.value).toBe(' My page content ');
+ });
+
it.each`
value | text
${'markdown'} | ${'[Link Title](page-slug)'}
@@ -178,10 +191,10 @@ describe('WikiForm', () => {
describe('when wiki content is updated', () => {
beforeEach(() => {
- createWrapper();
+ createWrapper(true);
const input = findContent();
- input.setValue('Lorem ipsum dolar sit!');
+ input.setValue(' Lorem ipsum dolar sit! ');
input.element.dispatchEvent(new Event('input'));
return wrapper.vm.$nextTick();
@@ -193,13 +206,25 @@ describe('WikiForm', () => {
expect(e.preventDefault).toHaveBeenCalledTimes(1);
});
- it('when form submitted, unsets before unload warning', async () => {
- triggerFormSubmit();
+ describe('form submit', () => {
+ beforeEach(async () => {
+ triggerFormSubmit();
- await wrapper.vm.$nextTick();
+ await wrapper.vm.$nextTick();
+ });
- const e = dispatchBeforeUnload();
- expect(e.preventDefault).not.toHaveBeenCalled();
+ it('when form submitted, unsets before unload warning', async () => {
+ const e = dispatchBeforeUnload();
+ expect(e.preventDefault).not.toHaveBeenCalled();
+ });
+
+ it('does not trigger tracking event', async () => {
+ expect(trackingSpy).not.toHaveBeenCalled();
+ });
+
+ it('does not trim page content', () => {
+ expect(findContent().element.value).toBe(' Lorem ipsum dolar sit! ');
+ });
});
});
@@ -251,9 +276,9 @@ describe('WikiForm', () => {
);
});
- describe('when feature flag wikiContentEditor is enabled', () => {
+ describe('wiki content editor', () => {
beforeEach(() => {
- createWrapper(true, { glFeatures: { wikiContentEditor: true } });
+ createWrapper(true);
});
it.each`
@@ -261,7 +286,7 @@ describe('WikiForm', () => {
${'markdown'} | ${true}
${'rdoc'} | ${false}
`(
- 'switch to new editor button exists: $buttonExists if format is $format',
+ 'gl-alert containing "use new editor" button exists: $buttonExists if format is $format',
async ({ format, buttonExists }) => {
setFormat(format);
@@ -271,6 +296,12 @@ describe('WikiForm', () => {
},
);
+ it('gl-alert containing "use new editor" button is dismissed on clicking dismiss button', async () => {
+ await findDismissContentEditorAlertButton().trigger('click');
+
+ expect(findUseNewEditorButton().exists()).toBe(false);
+ });
+
const assertOldEditorIsVisible = () => {
expect(wrapper.findComponent(ContentEditor).exists()).toBe(false);
expect(wrapper.findComponent(MarkdownField).exists()).toBe(true);
@@ -284,7 +315,7 @@ describe('WikiForm', () => {
);
};
- it('shows old editor by default', assertOldEditorIsVisible);
+ it('shows classic editor by default', assertOldEditorIsVisible);
describe('switch format to rdoc', () => {
beforeEach(async () => {
@@ -293,7 +324,7 @@ describe('WikiForm', () => {
await wrapper.vm.$nextTick();
});
- it('continues to show the old editor', assertOldEditorIsVisible);
+ it('continues to show the classic editor', assertOldEditorIsVisible);
describe('switch format back to markdown', () => {
beforeEach(async () => {
@@ -303,7 +334,7 @@ describe('WikiForm', () => {
});
it(
- 'still shows the old editor and does not automatically switch to the content editor ',
+ 'still shows the classic editor and does not automatically switch to the content editor ',
assertOldEditorIsVisible,
);
});
@@ -328,12 +359,12 @@ describe('WikiForm', () => {
expect(findSubmitButton().props('disabled')).toBe(true);
});
- describe('clicking "switch to old editor"', () => {
+ describe('clicking "switch to classic editor"', () => {
beforeEach(() => {
return findSwitchToOldEditorButton().trigger('click');
});
- it('switches to old editor directly without showing a modal', () => {
+ it('switches to classic editor directly without showing a modal', () => {
expect(wrapper.findComponent(ContentEditor).exists()).toBe(false);
expect(wrapper.findComponent(MarkdownField).exists()).toBe(true);
});
@@ -351,11 +382,12 @@ describe('WikiForm', () => {
expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
});
+ it('shows a tip to send feedback', () => {
+ expect(wrapper.text()).toContain('Tell us your experiences with the new Markdown editor');
+ });
+
it('shows warnings that the rich text editor is in beta and may not work properly', () => {
expect(wrapper.text()).toContain(
- "Switching will discard any changes you've made in the new editor.",
- );
- expect(wrapper.text()).toContain(
"This editor is in beta and may not display the page's contents properly.",
);
});
@@ -368,6 +400,15 @@ describe('WikiForm', () => {
expect(wrapper.findComponent(ContentEditor).exists()).toBe(true);
});
+ it('sends tracking event when editor loads', async () => {
+ // wait for content editor to load
+ await waitForPromises();
+
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, CONTENT_EDITOR_LOADED_ACTION, {
+ label: WIKI_CONTENT_EDITOR_TRACKING_LABEL,
+ });
+ });
+
it('disables the format dropdown', () => {
expect(findFormat().element.getAttribute('disabled')).toBeDefined();
});
@@ -400,9 +441,19 @@ describe('WikiForm', () => {
});
});
+ it('triggers tracking event on form submit', async () => {
+ triggerFormSubmit();
+
+ await wrapper.vm.$nextTick();
+
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, SAVED_USING_CONTENT_EDITOR_ACTION, {
+ label: WIKI_CONTENT_EDITOR_TRACKING_LABEL,
+ });
+ });
+
it('updates content from content editor on form submit', async () => {
// old value
- expect(findContent().element.value).toBe('My page content');
+ expect(findContent().element.value).toBe(' My page content ');
// wait for content editor to load
await waitForPromises();
@@ -414,7 +465,7 @@ describe('WikiForm', () => {
expect(findContent().element.value).toBe('hello **world**');
});
- describe('clicking "switch to old editor"', () => {
+ describe('clicking "switch to classic editor"', () => {
let modal;
beforeEach(async () => {
@@ -428,7 +479,7 @@ describe('WikiForm', () => {
expect(modal.vm.show).toHaveBeenCalled();
});
- describe('confirming "switch to old editor" in the modal', () => {
+ describe('confirming "switch to classic editor" in the modal', () => {
beforeEach(async () => {
wrapper.vm.contentEditor.tiptapEditor.commands.setContent(
'<p>hello __world__ from content editor</p>',
@@ -440,7 +491,7 @@ describe('WikiForm', () => {
await wrapper.vm.$nextTick();
});
- it('switches to old editor', () => {
+ it('switches to classic editor', () => {
expect(wrapper.findComponent(ContentEditor).exists()).toBe(false);
expect(wrapper.findComponent(MarkdownField).exists()).toBe(true);
});
@@ -451,8 +502,8 @@ describe('WikiForm', () => {
);
});
- it('the old editor retains its old value and does not use the content from the content editor', () => {
- expect(findContent().element.value).toBe('My page content');
+ it('the classic editor retains its old value and does not use the content from the content editor', () => {
+ expect(findContent().element.value).toBe(' My page content ');
});
});
});
diff --git a/spec/frontend/pages/users/activity_calendar_spec.js b/spec/frontend/pages/users/activity_calendar_spec.js
new file mode 100644
index 00000000000..b33e92e14b2
--- /dev/null
+++ b/spec/frontend/pages/users/activity_calendar_spec.js
@@ -0,0 +1,16 @@
+import { getLevelFromContributions } from '~/pages/users/activity_calendar';
+
+describe('getLevelFromContributions', () => {
+ it.each([
+ [0, 0],
+ [1, 1],
+ [9, 1],
+ [10, 2],
+ [19, 2],
+ [20, 3],
+ [30, 4],
+ [99, 4],
+ ])('.getLevelFromContributions(%i, %i)', (count, expected) => {
+ expect(getLevelFromContributions(count)).toBe(expected);
+ });
+});