diff options
Diffstat (limited to 'spec/frontend/pages')
12 files changed, 626 insertions, 97 deletions
diff --git a/spec/frontend/pages/admin/users/components/__snapshots__/delete_user_modal_spec.js.snap b/spec/frontend/pages/admin/users/components/__snapshots__/delete_user_modal_spec.js.snap index 9f02e5b9432..4c644a0d05f 100644 --- a/spec/frontend/pages/admin/users/components/__snapshots__/delete_user_modal_spec.js.snap +++ b/spec/frontend/pages/admin/users/components/__snapshots__/delete_user_modal_spec.js.snap @@ -8,6 +8,10 @@ exports[`User Operation confirmation modal renders modal with form included 1`] /> </p> + <oncall-schedules-list-stub + schedules="schedule1,schedule2" + /> + <p> <gl-sprintf-stub message="To confirm, type %{username}" diff --git a/spec/frontend/pages/admin/users/components/delete_user_modal_spec.js b/spec/frontend/pages/admin/users/components/delete_user_modal_spec.js index 318b6d16008..93d9ee43179 100644 --- a/spec/frontend/pages/admin/users/components/delete_user_modal_spec.js +++ b/spec/frontend/pages/admin/users/components/delete_user_modal_spec.js @@ -1,6 +1,7 @@ import { GlButton, GlFormInput } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import DeleteUserModal from '~/pages/admin/users/components/delete_user_modal.vue'; +import OncallSchedulesList from '~/vue_shared/components/oncall_schedules_list.vue'; import ModalStub from './stubs/modal_stub'; const TEST_DELETE_USER_URL = 'delete-url'; @@ -17,13 +18,14 @@ describe('User Operation confirmation modal', () => { .filter((w) => w.attributes('variant') === variant && w.attributes('category') === category) .at(0); const findForm = () => wrapper.find('form'); - const findUsernameInput = () => wrapper.find(GlFormInput); + const findUsernameInput = () => wrapper.findComponent(GlFormInput); const findPrimaryButton = () => findButton('danger', 'primary'); const findSecondaryButton = () => findButton('danger', 'secondary'); const findAuthenticityToken = () => new FormData(findForm().element).get('authenticity_token'); const getUsername = () => findUsernameInput().attributes('value'); const getMethodParam = () => new FormData(findForm().element).get('_method'); const getFormAction = () => findForm().attributes('action'); + const findOnCallSchedulesList = () => wrapper.findComponent(OncallSchedulesList); const setUsername = (username) => { findUsernameInput().vm.$emit('input', username); @@ -31,6 +33,7 @@ describe('User Operation confirmation modal', () => { const username = 'username'; const badUsername = 'bad_username'; + const oncallSchedules = '["schedule1", "schedule2"]'; const createComponent = (props = {}) => { wrapper = shallowMount(DeleteUserModal, { @@ -43,6 +46,7 @@ describe('User Operation confirmation modal', () => { deleteUserUrl: TEST_DELETE_USER_URL, blockUserUrl: TEST_BLOCK_USER_URL, csrfToken: TEST_CSRF, + oncallSchedules, ...props, }, stubs: { @@ -145,4 +149,19 @@ describe('User Operation confirmation modal', () => { }); }); }); + + describe('Related oncall-schedules list', () => { + it('does NOT render the list when user has no related schedules', () => { + createComponent({ oncallSchedules: '[]' }); + expect(findOnCallSchedulesList().exists()).toBe(false); + }); + + it('renders the list when user has related schedules', () => { + createComponent(); + + const schedules = findOnCallSchedulesList(); + expect(schedules.exists()).toBe(true); + expect(schedules.props('schedules')).toEqual(JSON.parse(oncallSchedules)); + }); + }); }); 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 2992c7f0624..6d853120232 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,5 +1,5 @@ -import { GlForm, GlFormInputGroup, GlFormInput } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; +import { GlFormInputGroup, GlFormInput, GlForm } from '@gitlab/ui'; +import { mount, shallowMount } from '@vue/test-utils'; import axios from 'axios'; import AxiosMockAdapter from 'axios-mock-adapter'; import { kebabCase } from 'lodash'; @@ -43,8 +43,8 @@ describe('ForkForm component', () => { axiosMock.onGet(DEFAULT_PROPS.endpoint).replyOnce(statusCode, data); }; - const createComponent = (props = {}, data = {}) => { - wrapper = shallowMount(ForkForm, { + const createComponentFactory = (mountFn) => (props = {}, data = {}) => { + wrapper = mountFn(ForkForm, { provide: { newGroupPath: 'some/groups/path', visibilityHelpPath: 'some/visibility/help/path', @@ -65,6 +65,9 @@ describe('ForkForm component', () => { }); }; + const createComponent = createComponentFactory(shallowMount); + const createFullComponent = createComponentFactory(mount); + beforeEach(() => { axiosMock = new AxiosMockAdapter(axios); window.gon = { @@ -99,44 +102,6 @@ describe('ForkForm component', () => { expect(cancelButton.attributes('href')).toBe(projectFullPath); }); - it('make POST request with project param', async () => { - jest.spyOn(axios, 'post'); - - const namespaceId = 20; - - mockGetRequest(); - createComponent( - {}, - { - selectedNamespace: { - id: namespaceId, - }, - }, - ); - - wrapper.find(GlForm).vm.$emit('submit', { preventDefault: () => {} }); - - const { - projectId, - projectDescription, - projectName, - projectPath, - projectVisibility, - } = DEFAULT_PROPS; - - const url = `/api/${GON_API_VERSION}/projects/${projectId}/fork`; - const project = { - description: projectDescription, - id: projectId, - name: projectName, - namespace_id: namespaceId, - path: projectPath, - visibility: projectVisibility, - }; - - expect(axios.post).toHaveBeenCalledWith(url, project); - }); - it('has input with csrf token', () => { mockGetRequest(); createComponent(); @@ -258,9 +223,7 @@ describe('ForkForm component', () => { projectVisibility: project, }, { - selectedNamespace: { - visibility: namespace, - }, + form: { fields: { namespace: { value: { visibility: namespace } } } }, }, ); @@ -274,34 +237,101 @@ describe('ForkForm component', () => { describe('onSubmit', () => { beforeEach(() => { jest.spyOn(urlUtility, 'redirectTo').mockImplementation(); + + mockGetRequest(); + createFullComponent( + {}, + { + namespaces: MOCK_NAMESPACES_RESPONSE, + form: { + state: true, + }, + }, + ); }); - it('redirect to POST web_url response', async () => { - const webUrl = `new/fork-project`; + const selectedMockNamespaceIndex = 1; + const namespaceId = MOCK_NAMESPACES_RESPONSE[selectedMockNamespaceIndex].id; - jest.spyOn(axios, 'post').mockResolvedValue({ data: { web_url: webUrl } }); + const fillForm = async () => { + const namespaceOptions = findForkUrlInput().findAll('option'); - mockGetRequest(); - createComponent(); + await namespaceOptions.at(selectedMockNamespaceIndex + 1).setSelected(); + }; - await wrapper.vm.onSubmit(); + const submitForm = async () => { + await fillForm(); + const form = wrapper.find(GlForm); - expect(urlUtility.redirectTo).toHaveBeenCalledWith(webUrl); + await form.trigger('submit'); + await wrapper.vm.$nextTick(); + }; + + describe('with invalid form', () => { + it('does not make POST request', async () => { + jest.spyOn(axios, 'post'); + + expect(axios.post).not.toHaveBeenCalled(); + }); + + it('does not redirect the current page', async () => { + await submitForm(); + + expect(urlUtility.redirectTo).not.toHaveBeenCalled(); + }); }); - it('display flash when POST is unsuccessful', async () => { - const dummyError = 'Fork project failed'; + describe('with valid form', () => { + beforeEach(() => { + fillForm(); + }); - jest.spyOn(axios, 'post').mockRejectedValue(dummyError); + it('make POST request with project param', async () => { + jest.spyOn(axios, 'post'); + + await submitForm(); + + const { + projectId, + projectDescription, + projectName, + projectPath, + projectVisibility, + } = DEFAULT_PROPS; + + const url = `/api/${GON_API_VERSION}/projects/${projectId}/fork`; + const project = { + description: projectDescription, + id: projectId, + name: projectName, + namespace_id: namespaceId, + path: projectPath, + visibility: projectVisibility, + }; - mockGetRequest(); - createComponent(); + expect(axios.post).toHaveBeenCalledWith(url, project); + }); + + it('redirect to POST web_url response', async () => { + const webUrl = `new/fork-project`; + jest.spyOn(axios, 'post').mockResolvedValue({ data: { web_url: webUrl } }); + + await submitForm(); + + expect(urlUtility.redirectTo).toHaveBeenCalledWith(webUrl); + }); + + it('display flash when POST is unsuccessful', async () => { + const dummyError = 'Fork project failed'; + + jest.spyOn(axios, 'post').mockRejectedValue(dummyError); - await wrapper.vm.onSubmit(); + await submitForm(); - expect(urlUtility.redirectTo).not.toHaveBeenCalled(); - expect(createFlash).toHaveBeenCalledWith({ - message: dummyError, + expect(urlUtility.redirectTo).not.toHaveBeenCalled(); + expect(createFlash).toHaveBeenCalledWith({ + message: dummyError, + }); }); }); }); 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 8b54a06ac7c..350669433f0 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 @@ -44,9 +44,7 @@ exports[`Learn GitLab Design A renders correctly 1`] = ` class="progress-bar" role="progressbar" style="width: 22.22222222222222%;" - > - <!----> - </div> + /> </div> </div> @@ -68,7 +66,7 @@ exports[`Learn GitLab Design A renders correctly 1`] = ` class="learn-gitlab-section-card-header" > <img - src="/assets/learn_gitlab/section_workspace.svg" + src="workspace.svg" /> <h2 @@ -134,9 +132,16 @@ exports[`Learn GitLab Design A renders correctly 1`] = ` <span> <a class="gl-link" + data-track-action="click_link" + data-track-label="Set up CI/CD" + data-track-property="Growth::Conversion::Experiment::LearnGitLabA" href="http://example.com/" + rel="noopener noreferrer" + target="_blank" > - Set up CI/CD + + Set up CI/CD + </a> </span> @@ -148,9 +153,16 @@ exports[`Learn GitLab Design A renders correctly 1`] = ` <span> <a class="gl-link" + data-track-action="click_link" + data-track-label="Start a free Ultimate trial" + data-track-property="Growth::Conversion::Experiment::LearnGitLabA" href="http://example.com/" + rel="noopener noreferrer" + target="_blank" > - Start a free Ultimate trial + + Start a free Ultimate trial + </a> </span> @@ -162,9 +174,16 @@ exports[`Learn GitLab Design A renders correctly 1`] = ` <span> <a class="gl-link" + data-track-action="click_link" + data-track-label="Add code owners" + data-track-property="Growth::Conversion::Experiment::LearnGitLabA" href="http://example.com/" + rel="noopener noreferrer" + target="_blank" > - Add code owners + + Add code owners + </a> </span> @@ -183,9 +202,16 @@ exports[`Learn GitLab Design A renders correctly 1`] = ` <span> <a class="gl-link" + data-track-action="click_link" + data-track-label="Add merge request approval" + data-track-property="Growth::Conversion::Experiment::LearnGitLabA" href="http://example.com/" + rel="noopener noreferrer" + target="_blank" > - Add merge request approval + + Add merge request approval + </a> </span> @@ -218,7 +244,7 @@ exports[`Learn GitLab Design A renders correctly 1`] = ` class="learn-gitlab-section-card-header" > <img - src="/assets/learn_gitlab/section_plan.svg" + src="plan.svg" /> <h2 @@ -240,9 +266,16 @@ exports[`Learn GitLab Design A renders correctly 1`] = ` <span> <a class="gl-link" + data-track-action="click_link" + data-track-label="Create an issue" + data-track-property="Growth::Conversion::Experiment::LearnGitLabA" href="http://example.com/" + rel="noopener noreferrer" + target="_blank" > - Create an issue + + Create an issue + </a> </span> @@ -254,9 +287,16 @@ exports[`Learn GitLab Design A renders correctly 1`] = ` <span> <a class="gl-link" + data-track-action="click_link" + data-track-label="Submit a merge request" + data-track-property="Growth::Conversion::Experiment::LearnGitLabA" href="http://example.com/" + rel="noopener noreferrer" + target="_blank" > - Submit a merge request + + Submit a merge request + </a> </span> @@ -282,7 +322,7 @@ exports[`Learn GitLab Design A renders correctly 1`] = ` class="learn-gitlab-section-card-header" > <img - src="/assets/learn_gitlab/section_deploy.svg" + src="deploy.svg" /> <h2 @@ -304,9 +344,16 @@ exports[`Learn GitLab Design A renders correctly 1`] = ` <span> <a class="gl-link" + data-track-action="click_link" + data-track-label="Run a Security scan using CI/CD" + data-track-property="Growth::Conversion::Experiment::LearnGitLabA" href="http://example.com/" + rel="noopener noreferrer" + target="_blank" > - Run a Security scan using CI/CD + + Run a Security scan using CI/CD + </a> </span> 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 07c7f2df09e..c9d8ab4566c 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 @@ -44,9 +44,7 @@ exports[`Learn GitLab Design B renders correctly 1`] = ` class="progress-bar" role="progressbar" style="width: 22.22222222222222%;" - > - <!----> - </div> + /> </div> </div> @@ -110,6 +108,9 @@ exports[`Learn GitLab Design B renders correctly 1`] = ` <a class="gl-link" + data-track-action="click_link" + data-track-label="Invite your colleagues" + data-track-property="Growth::Activation::Experiment::LearnGitLabB" href="http://example.com/" rel="noopener noreferrer" target="_blank" @@ -168,6 +169,9 @@ exports[`Learn GitLab Design B renders correctly 1`] = ` <a class="gl-link" + data-track-action="click_link" + data-track-label="Create or import a repository" + data-track-property="Growth::Activation::Experiment::LearnGitLabB" href="http://example.com/" rel="noopener noreferrer" target="_blank" @@ -218,6 +222,9 @@ exports[`Learn GitLab Design B renders correctly 1`] = ` <a class="gl-link" + data-track-action="click_link" + data-track-label="Set-up CI/CD" + data-track-property="Growth::Activation::Experiment::LearnGitLabB" href="http://example.com/" rel="noopener noreferrer" target="_blank" @@ -268,6 +275,9 @@ exports[`Learn GitLab Design B renders correctly 1`] = ` <a class="gl-link" + data-track-action="click_link" + data-track-label="Try GitLab Ultimate for free" + data-track-property="Growth::Activation::Experiment::LearnGitLabB" href="http://example.com/" rel="noopener noreferrer" target="_blank" @@ -323,6 +333,9 @@ exports[`Learn GitLab Design B renders correctly 1`] = ` <a class="gl-link" + data-track-action="click_link" + data-track-label="Add code owners" + data-track-property="Growth::Activation::Experiment::LearnGitLabB" href="http://example.com/" rel="noopener noreferrer" target="_blank" @@ -378,6 +391,9 @@ exports[`Learn GitLab Design B renders correctly 1`] = ` <a class="gl-link" + data-track-action="click_link" + data-track-label="Enable require merge approvals" + data-track-property="Growth::Activation::Experiment::LearnGitLabB" href="http://example.com/" rel="noopener noreferrer" target="_blank" @@ -444,6 +460,9 @@ exports[`Learn GitLab Design B renders correctly 1`] = ` <a class="gl-link" + data-track-action="click_link" + data-track-label="Create an issue" + data-track-property="Growth::Activation::Experiment::LearnGitLabB" href="http://example.com/" rel="noopener noreferrer" target="_blank" @@ -494,6 +513,9 @@ exports[`Learn GitLab Design B renders correctly 1`] = ` <a class="gl-link" + data-track-action="click_link" + data-track-label="Submit a merge request (MR)" + data-track-property="Growth::Activation::Experiment::LearnGitLabB" href="http://example.com/" rel="noopener noreferrer" target="_blank" @@ -560,6 +582,9 @@ exports[`Learn GitLab Design B renders correctly 1`] = ` <a class="gl-link" + data-track-action="click_link" + data-track-label="Run a Security scan using CI/CD" + data-track-property="Growth::Activation::Experiment::LearnGitLabB" href="http://example.com/" rel="noopener noreferrer" target="_blank" diff --git a/spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_section_card_spec.js.snap b/spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_section_card_spec.js.snap index ad8db0822cc..9e00ace761c 100644 --- a/spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_section_card_spec.js.snap +++ b/spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_section_card_spec.js.snap @@ -11,7 +11,7 @@ exports[`Learn GitLab Section Card renders correctly 1`] = ` class="learn-gitlab-section-card-header" > <img - src="/assets/learn_gitlab/section_workspace.svg" + src="workspace.svg" /> <h2 diff --git a/spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_a_spec.js b/spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_a_spec.js index 64ace341038..ac997c1f237 100644 --- a/spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_a_spec.js +++ b/spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_a_spec.js @@ -1,13 +1,13 @@ import { GlProgressBar } from '@gitlab/ui'; import { mount } from '@vue/test-utils'; import LearnGitlabA from '~/pages/projects/learn_gitlab/components/learn_gitlab_a.vue'; -import { testActions } from './mock_data'; +import { testActions, testSections } from './mock_data'; describe('Learn GitLab Design A', () => { let wrapper; const createWrapper = () => { - wrapper = mount(LearnGitlabA, { propsData: { actions: testActions } }); + wrapper = mount(LearnGitlabA, { propsData: { actions: testActions, sections: testSections } }); }; beforeEach(() => { diff --git a/spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_section_card_spec.js b/spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_section_card_spec.js index de6aca08235..3a511a009a9 100644 --- a/spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_section_card_spec.js +++ b/spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_section_card_spec.js @@ -3,6 +3,7 @@ import LearnGitlabSectionCard from '~/pages/projects/learn_gitlab/components/lea import { testActions } from './mock_data'; const defaultSection = 'workspace'; +const testImage = 'workspace.svg'; describe('Learn GitLab Section Card', () => { let wrapper; @@ -14,7 +15,7 @@ describe('Learn GitLab Section Card', () => { const createWrapper = () => { wrapper = shallowMount(LearnGitlabSectionCard, { - propsData: { section: defaultSection, actions: testActions }, + propsData: { section: defaultSection, actions: testActions, svg: testImage }, }); }; diff --git a/spec/frontend/pages/projects/learn_gitlab/components/mock_data.js b/spec/frontend/pages/projects/learn_gitlab/components/mock_data.js index d6ee2b00c8e..8d6ac737db8 100644 --- a/spec/frontend/pages/projects/learn_gitlab/components/mock_data.js +++ b/spec/frontend/pages/projects/learn_gitlab/components/mock_data.js @@ -45,3 +45,15 @@ export const testActions = { svg: 'http://example.com/images/illustration.svg', }, }; + +export const testSections = { + workspace: { + svg: 'workspace.svg', + }, + deploy: { + svg: 'deploy.svg', + }, + plan: { + svg: 'plan.svg', + }, +}; diff --git a/spec/frontend/pages/projects/new/components/app_spec.js b/spec/frontend/pages/projects/new/components/app_spec.js new file mode 100644 index 00000000000..b604e636243 --- /dev/null +++ b/spec/frontend/pages/projects/new/components/app_spec.js @@ -0,0 +1,77 @@ +import { shallowMount } from '@vue/test-utils'; +import { assignGitlabExperiment } from 'helpers/experimentation_helper'; +import App from '~/pages/projects/new/components/app.vue'; +import NewNamespacePage from '~/vue_shared/new_namespace/new_namespace_page.vue'; + +describe('Experimental new project creation app', () => { + let wrapper; + + const findNewNamespacePage = () => wrapper.findComponent(NewNamespacePage); + + const createComponent = (propsData) => { + wrapper = shallowMount(App, { propsData }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + describe('new_repo experiment', () => { + it('passes new_repo experiment', () => { + createComponent(); + + expect(findNewNamespacePage().props().experiment).toBe('new_repo'); + }); + + describe('when in the candidate variant', () => { + assignGitlabExperiment('new_repo', 'candidate'); + + it('has "repository" in the panel title', () => { + createComponent(); + + expect(findNewNamespacePage().props().panels[0].title).toBe( + 'Create blank project/repository', + ); + }); + }); + + describe('when in the control variant', () => { + assignGitlabExperiment('new_repo', 'control'); + + it('has "project" in the panel title', () => { + createComponent(); + + expect(findNewNamespacePage().props().panels[0].title).toBe('Create blank project'); + }); + }); + }); + + it('passes custom new project guideline text to underlying component', () => { + const DEMO_GUIDELINES = 'Demo guidelines'; + const guidelineSelector = '#new-project-guideline'; + createComponent({ + newProjectGuidelines: DEMO_GUIDELINES, + }); + + expect(wrapper.find(guidelineSelector).text()).toBe(DEMO_GUIDELINES); + }); + + it.each` + isCiCdAvailable | outcome + ${false} | ${'do not show CI/CD panel'} + ${true} | ${'show CI/CD panel'} + `('$outcome when isCiCdAvailable is $isCiCdAvailable', ({ isCiCdAvailable }) => { + createComponent({ + isCiCdAvailable, + }); + + expect( + Boolean( + wrapper + .findComponent(NewNamespacePage) + .props() + .panels.find((p) => p.name === 'cicd_for_external_repo'), + ), + ).toBe(isCiCdAvailable); + }); +}); diff --git a/spec/frontend/pages/projects/new/components/new_project_push_tip_popover_spec.js b/spec/frontend/pages/projects/new/components/new_project_push_tip_popover_spec.js new file mode 100644 index 00000000000..d4cf8c78600 --- /dev/null +++ b/spec/frontend/pages/projects/new/components/new_project_push_tip_popover_spec.js @@ -0,0 +1,75 @@ +import { GlPopover, GlFormInputGroup } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import NewProjectPushTipPopover from '~/pages/projects/new/components/new_project_push_tip_popover.vue'; +import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; + +describe('New project push tip popover', () => { + let wrapper; + const targetId = 'target'; + const pushToCreateProjectCommand = 'command'; + const workingWithProjectsHelpPath = 'path'; + + const findPopover = () => wrapper.findComponent(GlPopover); + const findClipboardButton = () => wrapper.findComponent(ClipboardButton); + const findFormInput = () => wrapper.findComponent(GlFormInputGroup); + const findHelpLink = () => wrapper.find('a'); + const findTarget = () => document.getElementById(targetId); + + const buildWrapper = () => { + wrapper = shallowMount(NewProjectPushTipPopover, { + propsData: { + target: findTarget(), + }, + stubs: { + GlFormInputGroup, + }, + provide: { + pushToCreateProjectCommand, + workingWithProjectsHelpPath, + }, + }); + }; + + beforeEach(() => { + setFixtures(`<a id="${targetId}"></a>`); + buildWrapper(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('renders popover that targets the specified target', () => { + expect(findPopover().props()).toMatchObject({ + target: findTarget(), + triggers: 'click blur', + placement: 'top', + title: 'Push to create a project', + }); + }); + + it('renders a readonly form input with the push to create command', () => { + expect(findFormInput().props()).toMatchObject({ + value: pushToCreateProjectCommand, + selectOnClick: true, + }); + expect(findFormInput().attributes()).toMatchObject({ + 'aria-label': 'Push project from command line', + readonly: 'readonly', + }); + }); + + it('allows copying the push command using the clipboard button', () => { + expect(findClipboardButton().props()).toMatchObject({ + text: pushToCreateProjectCommand, + tooltipPlacement: 'right', + title: 'Copy command', + }); + }); + + it('displays a link to open the push command help page reference', () => { + expect(findHelpLink().attributes().href).toBe( + `${workingWithProjectsHelpPath}#push-to-create-a-new-project`, + ); + }); +}); 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 8ab0b87d2ee..1cac8ef8ee2 100644 --- a/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js +++ b/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js @@ -1,9 +1,16 @@ +import { GlLoadingIcon, GlModal } from '@gitlab/ui'; import { mount } from '@vue/test-utils'; +import axios from 'axios'; +import MockAdapter from 'axios-mock-adapter'; 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 MarkdownField from '~/vue_shared/components/markdown/field.vue'; describe('WikiForm', () => { let wrapper; + let mock; const findForm = () => wrapper.find('form'); const findTitle = () => wrapper.find('#wiki_title'); @@ -11,10 +18,28 @@ describe('WikiForm', () => { const findContent = () => wrapper.find('#wiki_content'); const findMessage = () => wrapper.find('#wiki_message'); const findSubmitButton = () => wrapper.findByTestId('wiki-submit-button'); - const findCancelButton = () => wrapper.findByTestId('wiki-cancel-button'); - const findTitleHelpLink = () => wrapper.findByTestId('wiki-title-help-link'); + const findCancelButton = () => wrapper.findByRole('link', { name: 'Cancel' }); + const findUseNewEditorButton = () => wrapper.findByRole('button', { name: 'Use new editor' }); + const findSwitchToOldEditorButton = () => + wrapper.findByRole('button', { name: 'Switch to old editor' }); + const findTitleHelpLink = () => wrapper.findByRole('link', { name: 'More Information.' }); const findMarkdownHelpLink = () => wrapper.findByTestId('wiki-markdown-help-link'); + const setFormat = (value) => { + const format = findFormat(); + format.find(`option[value=${value}]`).setSelected(); + format.element.dispatchEvent(new Event('change')); + }; + + const triggerFormSubmit = () => findForm().element.dispatchEvent(new Event('submit')); + + const dispatchBeforeUnload = () => { + const e = new Event('beforeunload'); + jest.spyOn(e, 'preventDefault'); + window.dispatchEvent(e); + return e; + }; + const pageInfoNew = { persisted: false, uploadsPath: '/project/path/-/wikis/attachments', @@ -35,7 +60,10 @@ describe('WikiForm', () => { path: '/project/path/-/wikis/home', }; - function createWrapper(persisted = false, pageInfo = {}) { + function createWrapper( + persisted = false, + { pageInfo, glFeatures } = { glFeatures: { wikiContentEditor: false } }, + ) { wrapper = extendedWrapper( mount( WikiForm, @@ -51,16 +79,20 @@ describe('WikiForm', () => { ...(persisted ? pageInfoPersisted : pageInfoNew), ...pageInfo, }, + glFeatures, }, }, { attachToDocument: true }, ), ); - - jest.spyOn(wrapper.vm, 'onBeforeUnload'); } + beforeEach(() => { + mock = new MockAdapter(axios); + }); + afterEach(() => { + mock.restore(); wrapper.destroy(); wrapper = null; }); @@ -101,7 +133,7 @@ describe('WikiForm', () => { `('updates the link help message when format=$value is selected', async ({ value, text }) => { createWrapper(); - findFormat().find(`option[value=${value}]`).setSelected(); + setFormat(value); await wrapper.vm.$nextTick(); @@ -113,9 +145,9 @@ describe('WikiForm', () => { await wrapper.vm.$nextTick(); - window.dispatchEvent(new Event('beforeunload')); - - expect(wrapper.vm.onBeforeUnload).not.toHaveBeenCalled(); + const e = dispatchBeforeUnload(); + expect(typeof e.returnValue).not.toBe('string'); + expect(e.preventDefault).not.toHaveBeenCalled(); }); it.each` @@ -156,19 +188,18 @@ describe('WikiForm', () => { }); it('sets before unload warning', () => { - window.dispatchEvent(new Event('beforeunload')); + const e = dispatchBeforeUnload(); - expect(wrapper.vm.onBeforeUnload).toHaveBeenCalled(); + expect(e.preventDefault).toHaveBeenCalledTimes(1); }); it('when form submitted, unsets before unload warning', async () => { - findForm().element.dispatchEvent(new Event('submit')); + triggerFormSubmit(); await wrapper.vm.$nextTick(); - window.dispatchEvent(new Event('beforeunload')); - - expect(wrapper.vm.onBeforeUnload).not.toHaveBeenCalled(); + const e = dispatchBeforeUnload(); + expect(e.preventDefault).not.toHaveBeenCalled(); }); }); @@ -219,4 +250,212 @@ describe('WikiForm', () => { }, ); }); + + describe('when feature flag wikiContentEditor is enabled', () => { + beforeEach(() => { + createWrapper(true, { glFeatures: { wikiContentEditor: true } }); + }); + + it.each` + format | buttonExists + ${'markdown'} | ${true} + ${'rdoc'} | ${false} + `( + 'switch to new editor button exists: $buttonExists if format is $format', + async ({ format, buttonExists }) => { + setFormat(format); + + await wrapper.vm.$nextTick(); + + expect(findUseNewEditorButton().exists()).toBe(buttonExists); + }, + ); + + const assertOldEditorIsVisible = () => { + expect(wrapper.findComponent(ContentEditor).exists()).toBe(false); + expect(wrapper.findComponent(MarkdownField).exists()).toBe(true); + expect(findSubmitButton().props('disabled')).toBe(false); + + expect(wrapper.text()).not.toContain( + "Switching will discard any changes you've made in the new editor.", + ); + expect(wrapper.text()).not.toContain( + "This editor is in beta and may not display the page's contents properly.", + ); + }; + + it('shows old editor by default', assertOldEditorIsVisible); + + describe('switch format to rdoc', () => { + beforeEach(async () => { + setFormat('rdoc'); + + await wrapper.vm.$nextTick(); + }); + + it('continues to show the old editor', assertOldEditorIsVisible); + + describe('switch format back to markdown', () => { + beforeEach(async () => { + setFormat('rdoc'); + + await wrapper.vm.$nextTick(); + }); + + it( + 'still shows the old editor and does not automatically switch to the content editor ', + assertOldEditorIsVisible, + ); + }); + }); + + describe('clicking "use new editor": editor fails to load', () => { + beforeEach(async () => { + mock.onPost(/preview-markdown/).reply(400); + + await findUseNewEditorButton().trigger('click'); + + // try waiting for content editor to load (but it will never actually load) + await waitForPromises(); + }); + + it('editor is shown in a perpetual loading state', () => { + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); + expect(wrapper.findComponent(ContentEditor).exists()).toBe(false); + }); + + it('disables the submit button', () => { + expect(findSubmitButton().props('disabled')).toBe(true); + }); + + describe('clicking "switch to old editor"', () => { + beforeEach(() => { + return findSwitchToOldEditorButton().trigger('click'); + }); + + it('switches to old editor directly without showing a modal', () => { + expect(wrapper.findComponent(ContentEditor).exists()).toBe(false); + expect(wrapper.findComponent(MarkdownField).exists()).toBe(true); + }); + }); + }); + + describe('clicking "use new editor": editor loads successfully', () => { + beforeEach(() => { + mock.onPost(/preview-markdown/).reply(200, { body: '<p>hello <strong>world</strong></p>' }); + + findUseNewEditorButton().trigger('click'); + }); + + it('shows a loading indicator for the rich text editor', () => { + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); + }); + + 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.", + ); + }); + + it('shows the rich text editor when loading finishes', async () => { + // wait for content editor to load + await waitForPromises(); + + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false); + expect(wrapper.findComponent(ContentEditor).exists()).toBe(true); + }); + + it('disables the format dropdown', () => { + expect(findFormat().element.getAttribute('disabled')).toBeDefined(); + }); + + describe('when wiki content is updated', () => { + beforeEach(async () => { + // wait for content editor to load + await waitForPromises(); + + wrapper.vm.contentEditor.tiptapEditor.commands.setContent( + '<p>hello __world__ from content editor</p>', + true, + ); + + return wrapper.vm.$nextTick(); + }); + + it('sets before unload warning', () => { + const e = dispatchBeforeUnload(); + expect(e.preventDefault).toHaveBeenCalledTimes(1); + }); + + it('unsets before unload warning on form submit', async () => { + triggerFormSubmit(); + + await wrapper.vm.$nextTick(); + + const e = dispatchBeforeUnload(); + expect(e.preventDefault).not.toHaveBeenCalled(); + }); + }); + + it('updates content from content editor on form submit', async () => { + // old value + expect(findContent().element.value).toBe('My page content'); + + // wait for content editor to load + await waitForPromises(); + + triggerFormSubmit(); + + await wrapper.vm.$nextTick(); + + expect(findContent().element.value).toBe('hello **world**'); + }); + + describe('clicking "switch to old editor"', () => { + let modal; + + beforeEach(async () => { + modal = wrapper.findComponent(GlModal); + jest.spyOn(modal.vm, 'show'); + + findSwitchToOldEditorButton().trigger('click'); + }); + + it('shows a modal confirming the change', () => { + expect(modal.vm.show).toHaveBeenCalled(); + }); + + describe('confirming "switch to old editor" in the modal', () => { + beforeEach(async () => { + wrapper.vm.contentEditor.tiptapEditor.commands.setContent( + '<p>hello __world__ from content editor</p>', + true, + ); + + wrapper.findComponent(GlModal).vm.$emit('primary'); + + await wrapper.vm.$nextTick(); + }); + + it('switches to old editor', () => { + expect(wrapper.findComponent(ContentEditor).exists()).toBe(false); + expect(wrapper.findComponent(MarkdownField).exists()).toBe(true); + }); + + it('does not show a warning about content editor', () => { + expect(wrapper.text()).not.toContain( + "This editor is in beta and may not display the page's contents properly.", + ); + }); + + 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'); + }); + }); + }); + }); + }); }); |