diff options
Diffstat (limited to 'spec/frontend/pages')
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); + }); +}); |