/* * SonarQube * Copyright (C) 2009-2024 SonarSource SA * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import { waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { UserEvent } from '@testing-library/user-event/dist/types/setup/setup'; import AlmSettingsServiceMock from '../../../../api/mocks/AlmSettingsServiceMock'; import DopTranslationServiceMock from '../../../../api/mocks/DopTranslationServiceMock'; import NewCodeDefinitionServiceMock from '../../../../api/mocks/NewCodeDefinitionServiceMock'; import { ProjectsServiceMock } from '../../../../api/mocks/ProjectsServiceMock'; import { getNewCodeDefinition } from '../../../../api/newCodeDefinition'; import { mockProject } from '../../../../helpers/mocks/projects'; import { mockAppState, mockCurrentUser } from '../../../../helpers/testMocks'; import { renderAppRoutes } from '../../../../helpers/testReactTestingUtils'; import { byRole, byText } from '../../../../helpers/testSelector'; import { NewCodeDefinitionType } from '../../../../types/new-code-definition'; import { Permissions } from '../../../../types/permissions'; import routes from '../../../projects/routes'; jest.mock('../../../../api/measures'); jest.mock('../../../../api/favorites'); jest.mock('../../../../api/alm-settings'); jest.mock('../../../../api/dop-translation'); jest.mock('../../../../api/newCodeDefinition'); jest.mock('../../../../api/project-management', () => ({ createProject: jest.fn().mockReturnValue(Promise.resolve({ project: mockProject() })), })); jest.mock('../../../../api/components', () => ({ ...jest.requireActual('../../../../api/components'), searchProjects: jest.fn(), getScannableProjects: jest.fn(), doesComponentExists: jest .fn() .mockImplementation(({ component }) => Promise.resolve(component === 'exists')), })); jest.mock('../../../../api/settings', () => ({ getValue: jest.fn().mockResolvedValue({ value: 'main' }), })); const ui = { manualCreateProjectOption: byText('onboarding.create_project.select_method.manual'), manualProjectHeader: byText('onboarding.create_project.manual.title'), displayNameField: byRole('textbox', { name: /onboarding.create_project.display_name/, }), projectNextButton: byRole('button', { name: 'next' }), newCodeDefinitionSection: byRole('region', { name: 'onboarding.create_project.new_code_definition.title', }), newCodeDefinitionHeader: byText('onboarding.create_x_project.new_code_definition.title1'), inheritGlobalNcdRadio: byRole('radio', { name: 'new_code_definition.global_setting' }), projectCreateButton: byRole('button', { name: 'onboarding.create_project.new_code_definition.create_x_projects1', }), cancelButton: byRole('button', { name: 'cancel' }), closeButton: byRole('button', { name: 'clear' }), createProjectsButton: byRole('button', { name: 'projects.add' }), createLocalProject: byRole('menuitem', { name: 'my_account.add_project.manual' }), overrideNcdRadio: byRole('radio', { name: 'new_code_definition.specific_setting' }), ncdOptionPreviousVersionRadio: byRole('radio', { name: /new_code_definition.previous_version/, }), ncdOptionRefBranchRadio: byRole('radio', { name: /new_code_definition.reference_branch/, }), ncdOptionDaysRadio: byRole('radio', { name: /new_code_definition.number_days/, }), ncdOptionDaysInput: byRole('spinbutton', { name: /new_code_definition.number_days.specify_days/, }), ncdOptionDaysInputError: byText('new_code_definition.number_days.invalid.1.90'), projectDashboardText: byText('/dashboard?id=foo'), projectsPageTitle: byRole('heading', { name: 'projects.page' }), }; async function fillFormAndNext(displayName: string, user: UserEvent) { expect(ui.manualProjectHeader.get()).toBeInTheDocument(); await user.click(ui.displayNameField.get()); await user.keyboard(displayName); expect(ui.projectNextButton.get()).toBeEnabled(); await user.click(ui.projectNextButton.get()); } let almSettingsHandler: AlmSettingsServiceMock; let dopTranslationHandler: DopTranslationServiceMock; let newCodePeriodHandler: NewCodeDefinitionServiceMock; let projectHandler: ProjectsServiceMock; const original = window.location; beforeAll(() => { Object.defineProperty(window, 'location', { configurable: true, value: { replace: jest.fn() }, }); almSettingsHandler = new AlmSettingsServiceMock(); dopTranslationHandler = new DopTranslationServiceMock(); newCodePeriodHandler = new NewCodeDefinitionServiceMock(); projectHandler = new ProjectsServiceMock(); }); beforeEach(() => { jest.clearAllMocks(); almSettingsHandler.reset(); dopTranslationHandler.reset(); newCodePeriodHandler.reset(); projectHandler.reset(); }); afterAll(() => { Object.defineProperty(window, 'location', { configurable: true, value: original }); }); it('should fill form and move to NCD selection', async () => { const user = userEvent.setup(); renderCreateProject(); await fillFormAndNext('test', user); expect(ui.newCodeDefinitionHeader.get()).toBeInTheDocument(); }); it('should select the global NCD when it is compliant', async () => { jest .mocked(getNewCodeDefinition) .mockResolvedValue({ type: NewCodeDefinitionType.NumberOfDays, value: '30' }); const user = userEvent.setup(); renderCreateProject(); await fillFormAndNext('test', user); expect(ui.newCodeDefinitionHeader.get()).toBeInTheDocument(); expect(ui.inheritGlobalNcdRadio.get()).toBeInTheDocument(); expect(ui.inheritGlobalNcdRadio.get()).toBeEnabled(); expect(ui.projectCreateButton.get()).toBeDisabled(); await user.click(ui.inheritGlobalNcdRadio.get()); expect(ui.projectCreateButton.get()).toBeEnabled(); }); it('number of days ignores non-numeric inputs', async () => { jest .mocked(getNewCodeDefinition) .mockResolvedValue({ type: NewCodeDefinitionType.NumberOfDays, value: '60' }); const user = userEvent.setup(); renderCreateProject(); await fillFormAndNext('test', user); expect(ui.projectCreateButton.get()).toBeDisabled(); expect(ui.overrideNcdRadio.get()).not.toHaveClass('disabled'); expect(ui.ncdOptionDaysRadio.get()).toHaveClass('disabled'); await user.click(ui.overrideNcdRadio.get()); expect(ui.ncdOptionDaysRadio.get()).not.toHaveClass('disabled'); await user.click(ui.ncdOptionDaysRadio.get()); expect(ui.ncdOptionDaysInput.get()).toBeInTheDocument(); expect(ui.ncdOptionDaysInput.get()).toHaveValue(60); expect(ui.projectCreateButton.get()).toBeEnabled(); await user.click(ui.ncdOptionDaysInput.get()); await user.keyboard('abc'); // it ignores the input and preserves its value expect(ui.ncdOptionDaysInput.get()).toHaveValue(60); }); it('the project onboarding page should be displayed when the project is created', async () => { newCodePeriodHandler.setNewCodePeriod({ type: NewCodeDefinitionType.NumberOfDays }); const user = userEvent.setup(); renderCreateProject(); await fillFormAndNext('testing', user); await user.click(ui.overrideNcdRadio.get()); expect(ui.projectCreateButton.get()).toBeEnabled(); await user.click(ui.projectCreateButton.get()); expect(await ui.projectDashboardText.find()).toBeInTheDocument(); }); it('validate the private key field', async () => { const user = userEvent.setup(); renderCreateProject(); expect(ui.manualProjectHeader.get()).toBeInTheDocument(); await user.click(ui.displayNameField.get()); await user.keyboard('exists'); await waitFor(() => { expect(ui.projectNextButton.get()).toBeDisabled(); }); await user.click(ui.projectNextButton.get()); }); it('should navigate back to the Projects page when clicking cancel or close', async () => { newCodePeriodHandler.setNewCodePeriod({ type: NewCodeDefinitionType.NumberOfDays }); const user = userEvent.setup(); renderCreateProject(); await user.click(ui.cancelButton.get()); expect(await ui.projectsPageTitle.find()).toBeInTheDocument(); await user.click(ui.createProjectsButton.get()); await user.click(await ui.createLocalProject.find()); await user.click(ui.closeButton.get()); expect(await ui.projectsPageTitle.find()).toBeInTheDocument(); await user.click(ui.createProjectsButton.get()); await user.click(await ui.createLocalProject.find()); expect(await ui.manualProjectHeader.find()).toBeInTheDocument(); await fillFormAndNext('testing', user); expect(ui.newCodeDefinitionHeader.get()).toBeInTheDocument(); await user.click(await ui.newCodeDefinitionSection.byRole('button', { name: 'clear' }).find()); expect(await ui.projectsPageTitle.find()).toBeInTheDocument(); }); function renderCreateProject() { renderAppRoutes('projects/create?mode=manual', routes, { currentUser: mockCurrentUser({ permissions: { global: [Permissions.ProjectCreation] }, }), appState: mockAppState({ canAdmin: true }), }); }