From 926932fc656afbb49f1cd2c26363cb120a4d7f39 Mon Sep 17 00:00:00 2001 From: Wouter Admiraal Date: Tue, 2 Mar 2021 09:41:21 +0100 Subject: [PATCH] Refactor Quality Profiles page - Move all API interactions to the parent component - Merge copy, extend, and rename forms into a single component - Simplify delete form --- .../components/CopyProfileForm.tsx | 123 ------- .../components/DeleteProfileForm.tsx | 115 +++---- .../components/ExtendProfileForm.tsx | 137 -------- .../components/ProfileActions.tsx | 251 ++++++++------ .../components/ProfileModalForm.tsx | 86 +++++ .../components/RenameProfileForm.tsx | 123 ------- .../__tests__/DeleteProfileForm-test.tsx | 36 +- .../__tests__/ExtendProfileForm-test.tsx | 64 ---- .../__tests__/ProfileActions-test.tsx | 300 ++++++++++++++--- .../__tests__/ProfileModalForm-test.tsx | 70 ++++ .../DeleteProfileForm-test.tsx.snap | 100 +++++- .../ExtendProfileForm-test.tsx.snap | 69 ---- .../ProfileActions-test.tsx.snap | 310 +++++++++++++----- .../ProfileModalForm-test.tsx.snap | 190 +++++++++++ .../details/ProfileInheritanceBox.tsx | 3 +- .../ProfileInheritanceBox-test.tsx.snap | 8 +- .../main/js/apps/quality-profiles/types.ts | 7 + .../resources/org/sonar/l10n/core.properties | 1 - 18 files changed, 1158 insertions(+), 835 deletions(-) delete mode 100644 server/sonar-web/src/main/js/apps/quality-profiles/components/CopyProfileForm.tsx delete mode 100644 server/sonar-web/src/main/js/apps/quality-profiles/components/ExtendProfileForm.tsx create mode 100644 server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileModalForm.tsx delete mode 100644 server/sonar-web/src/main/js/apps/quality-profiles/components/RenameProfileForm.tsx delete mode 100644 server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/ExtendProfileForm-test.tsx create mode 100644 server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/ProfileModalForm-test.tsx delete mode 100644 server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/__snapshots__/ExtendProfileForm-test.tsx.snap create mode 100644 server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/__snapshots__/ProfileModalForm-test.tsx.snap diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/CopyProfileForm.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/components/CopyProfileForm.tsx deleted file mode 100644 index b375c9af3f6..00000000000 --- a/server/sonar-web/src/main/js/apps/quality-profiles/components/CopyProfileForm.tsx +++ /dev/null @@ -1,123 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2021 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 * as React from 'react'; -import { ResetButtonLink, SubmitButton } from 'sonar-ui-common/components/controls/buttons'; -import Modal from 'sonar-ui-common/components/controls/Modal'; -import MandatoryFieldMarker from 'sonar-ui-common/components/ui/MandatoryFieldMarker'; -import MandatoryFieldsExplanation from 'sonar-ui-common/components/ui/MandatoryFieldsExplanation'; -import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n'; -import { copyProfile } from '../../../api/quality-profiles'; -import { Profile } from '../types'; - -interface Props { - onClose: () => void; - onCopy: (name: string) => void; - profile: Profile; -} - -interface State { - loading: boolean; - name: string | null; -} - -export default class CopyProfileForm extends React.PureComponent { - mounted = false; - state: State = { loading: false, name: null }; - - componentDidMount() { - this.mounted = true; - } - - componentWillUnmount() { - this.mounted = false; - } - - handleNameChange = (event: React.SyntheticEvent) => { - this.setState({ name: event.currentTarget.value }); - }; - - handleFormSubmit = (event: React.SyntheticEvent) => { - event.preventDefault(); - - const { name } = this.state; - - if (name != null) { - this.setState({ loading: true }); - copyProfile(this.props.profile.key, name).then( - (profile: any) => this.props.onCopy(profile.name), - () => { - if (this.mounted) { - this.setState({ loading: false }); - } - } - ); - } - }; - - render() { - const { profile } = this.props; - const header = translateWithParameters( - 'quality_profiles.copy_x_title', - profile.name, - profile.languageName - ); - const submitDisabled = - this.state.loading || !this.state.name || this.state.name === profile.name; - - return ( - -
-
-

{header}

-
-
- -
- - -
-
-
- {this.state.loading && } - - {translate('copy')} - - - {translate('cancel')} - -
- -
- ); - } -} diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/DeleteProfileForm.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/components/DeleteProfileForm.tsx index 291d325733a..0e5b61310d9 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/components/DeleteProfileForm.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/components/DeleteProfileForm.tsx @@ -22,90 +22,61 @@ import { ResetButtonLink, SubmitButton } from 'sonar-ui-common/components/contro import Modal from 'sonar-ui-common/components/controls/Modal'; import { Alert } from 'sonar-ui-common/components/ui/Alert'; import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n'; -import { deleteProfile } from '../../../api/quality-profiles'; import { Profile } from '../types'; -interface Props { +export interface DeleteProfileFormProps { + loading: boolean; onClose: () => void; onDelete: () => void; profile: Profile; } -interface State { - loading: boolean; -} - -export default class DeleteProfileForm extends React.PureComponent { - mounted = false; - state: State = { loading: false }; - - componentDidMount() { - this.mounted = true; - } - - componentWillUnmount() { - this.mounted = false; - } - - handleFormSubmit = (event: React.SyntheticEvent) => { - event.preventDefault(); - this.setState({ loading: true }); - deleteProfile(this.props.profile).then(this.props.onDelete, () => { - if (this.mounted) { - this.setState({ loading: false }); - } - }); - }; - - render() { - const { profile } = this.props; - const header = translate('quality_profiles.delete_confirm_title'); +export default function DeleteProfileForm(props: DeleteProfileFormProps) { + const { loading, profile } = props; + const header = translate('quality_profiles.delete_confirm_title'); - return ( - -
-
-

{header}

-
-
-
- {profile.childrenCount > 0 ? ( -
- - {translate('quality_profiles.this_profile_has_descendants')} - -

- {translateWithParameters( - 'quality_profiles.are_you_sure_want_delete_profile_x_and_descendants', - profile.name, - profile.languageName - )} -

-
- ) : ( + return ( + + ) => { + e.preventDefault(); + props.onDelete(); + }}> +
+

{header}

+
+
+ {profile.childrenCount > 0 ? ( +
+ + {translate('quality_profiles.this_profile_has_descendants')} +

{translateWithParameters( - 'quality_profiles.are_you_sure_want_delete_profile_x', + 'quality_profiles.are_you_sure_want_delete_profile_x_and_descendants', profile.name, profile.languageName )}

- )} -
-
- {this.state.loading && } - - {translate('delete')} - - - {translate('cancel')} - -
- - - ); - } +
+ ) : ( +

+ {translateWithParameters( + 'quality_profiles.are_you_sure_want_delete_profile_x', + profile.name, + profile.languageName + )} +

+ )} +
+
+ {loading && } + + {translate('delete')} + + {translate('cancel')} +
+ + + ); } diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/ExtendProfileForm.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/components/ExtendProfileForm.tsx deleted file mode 100644 index 2317d7edeb1..00000000000 --- a/server/sonar-web/src/main/js/apps/quality-profiles/components/ExtendProfileForm.tsx +++ /dev/null @@ -1,137 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2021 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 * as React from 'react'; -import { ResetButtonLink, SubmitButton } from 'sonar-ui-common/components/controls/buttons'; -import Modal from 'sonar-ui-common/components/controls/Modal'; -import DeferredSpinner from 'sonar-ui-common/components/ui/DeferredSpinner'; -import MandatoryFieldMarker from 'sonar-ui-common/components/ui/MandatoryFieldMarker'; -import MandatoryFieldsExplanation from 'sonar-ui-common/components/ui/MandatoryFieldsExplanation'; -import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n'; -import { changeProfileParent, createQualityProfile } from '../../../api/quality-profiles'; -import { Profile } from '../types'; - -interface Props { - onClose: () => void; - onExtend: (name: string) => void; - profile: Profile; -} - -interface State { - loading: boolean; - name?: string; -} - -type ValidState = State & Required>; - -export default class ExtendProfileForm extends React.PureComponent { - mounted = false; - state: State = { loading: false }; - - componentDidMount() { - this.mounted = true; - } - - componentWillUnmount() { - this.mounted = false; - } - - canSubmit = (state: State): state is ValidState => { - return Boolean(state.name && state.name.length); - }; - - handleNameChange = (event: React.SyntheticEvent) => { - this.setState({ name: event.currentTarget.value }); - }; - - handleFormSubmit = async (event: React.SyntheticEvent) => { - event.preventDefault(); - if (this.canSubmit(this.state)) { - const { profile: parentProfile } = this.props; - const { name } = this.state; - - const data = new FormData(); - - data.append('language', parentProfile.language); - data.append('name', name); - - this.setState({ loading: true }); - - try { - const { profile: newProfile } = await createQualityProfile(data); - await changeProfileParent(newProfile, parentProfile); - this.props.onExtend(newProfile.name); - } finally { - if (this.mounted) { - this.setState({ loading: false }); - } - } - } - }; - - render() { - const { profile } = this.props; - const header = translateWithParameters( - 'quality_profiles.extend_x_title', - profile.name, - profile.languageName - ); - - return ( - -
-
-

{header}

-
-
- -
- - -
-
-
- - - {translate('extend')} - - - {translate('cancel')} - -
-
-
- ); - } -} diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileActions.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileActions.tsx index 9ce4622be11..c8ac23413f6 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileActions.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileActions.tsx @@ -23,16 +23,22 @@ import ActionsDropdown, { ActionsDropdownItem } from 'sonar-ui-common/components/controls/ActionsDropdown'; import { translate } from 'sonar-ui-common/helpers/l10n'; -import { getQualityProfileBackupUrl, setDefaultProfile } from '../../../api/quality-profiles'; +import { + changeProfileParent, + copyProfile, + createQualityProfile, + deleteProfile, + getQualityProfileBackupUrl, + renameProfile, + setDefaultProfile +} from '../../../api/quality-profiles'; import { Router, withRouter } from '../../../components/hoc/withRouter'; import { getBaseUrl } from '../../../helpers/system'; import { getRulesUrl } from '../../../helpers/urls'; -import { Profile } from '../types'; +import { Profile, ProfileActionModals } from '../types'; import { getProfileComparePath, getProfilePath, PROFILE_PATH } from '../utils'; -import CopyProfileForm from './CopyProfileForm'; import DeleteProfileForm from './DeleteProfileForm'; -import ExtendProfileForm from './ExtendProfileForm'; -import RenameProfileForm from './RenameProfileForm'; +import ProfileModalForm from './ProfileModalForm'; interface Props { className?: string; @@ -43,94 +49,129 @@ interface Props { } interface State { - copyFormOpen: boolean; - extendFormOpen: boolean; - deleteFormOpen: boolean; - renameFormOpen: boolean; + loading: boolean; + openModal?: ProfileActionModals; } export class ProfileActions extends React.PureComponent { + mounted = false; state: State = { - copyFormOpen: false, - extendFormOpen: false, - deleteFormOpen: false, - renameFormOpen: false + loading: false }; - closeCopyForm = () => { - this.setState({ copyFormOpen: false }); - }; + componentDidMount() { + this.mounted = true; + } + + componentWillUnmount() { + this.mounted = false; + } - closeDeleteForm = () => { - this.setState({ deleteFormOpen: false }); + handleCloseModal = () => { + this.setState({ openModal: undefined }); }; - closeExtendForm = () => { - this.setState({ extendFormOpen: false }); + handleCopyClick = () => { + this.setState({ openModal: ProfileActionModals.Copy }); }; - closeRenameForm = () => { - this.setState({ renameFormOpen: false }); + handleExtendClick = () => { + this.setState({ openModal: ProfileActionModals.Extend }); }; - handleCopyClick = () => { - this.setState({ copyFormOpen: true }); + handleRenameClick = () => { + this.setState({ openModal: ProfileActionModals.Rename }); }; handleDeleteClick = () => { - this.setState({ deleteFormOpen: true }); + this.setState({ openModal: ProfileActionModals.Delete }); }; - handleExtendClick = () => { - this.setState({ extendFormOpen: true }); - }; + handleProfileCopy = async (name: string) => { + this.setState({ loading: true }); - handleRenameClick = () => { - this.setState({ renameFormOpen: true }); + try { + await copyProfile(this.props.profile.key, name); + this.profileActionPerformed(name); + } catch { + this.profileActionError(); + } }; - handleProfileCopy = (name: string) => { - this.closeCopyForm(); - this.navigateToNewProfile(name); - }; + handleProfileExtend = async (name: string) => { + const { profile: parentProfile } = this.props; + + const data = { + language: parentProfile.language, + name + }; + + this.setState({ loading: true }); - handleProfileDelete = () => { - this.props.router.replace(PROFILE_PATH); - this.props.updateProfiles(); + try { + const { profile: newProfile } = await createQualityProfile(data); + await changeProfileParent(newProfile, parentProfile); + this.profileActionPerformed(name); + } catch { + this.profileActionError(); + } }; - handleProfileExtend = (name: string) => { - this.closeExtendForm(); - this.navigateToNewProfile(name); + handleProfileRename = async (name: string) => { + this.setState({ loading: true }); + + try { + await renameProfile(this.props.profile.key, name); + this.profileActionPerformed(name); + } catch { + this.profileActionError(); + } }; - handleProfileRename = (name: string) => { - this.closeRenameForm(); - this.props.updateProfiles().then( - () => { - if (!this.props.fromList) { - this.props.router.replace(getProfilePath(name, this.props.profile.language)); - } - }, - () => {} - ); + handleProfileDelete = async () => { + this.setState({ loading: true }); + + try { + await deleteProfile(this.props.profile); + + if (this.mounted) { + this.setState({ loading: false, openModal: undefined }); + this.props.router.replace(PROFILE_PATH); + this.props.updateProfiles(); + } + } catch { + this.profileActionError(); + } }; handleSetDefaultClick = () => { setDefaultProfile(this.props.profile).then(this.props.updateProfiles, () => {}); }; - navigateToNewProfile = (name: string) => { - this.props.updateProfiles().then( - () => { - this.props.router.push(getProfilePath(name, this.props.profile.language)); - }, - () => {} - ); + profileActionPerformed = (name: string) => { + const { profile, router } = this.props; + if (this.mounted) { + this.setState({ loading: false, openModal: undefined }); + this.props.updateProfiles().then( + () => { + router.push(getProfilePath(name, profile.language)); + }, + () => { + /* noop */ + } + ); + } + }; + + profileActionError = () => { + if (this.mounted) { + this.setState({ loading: false }); + } }; render() { const { profile } = this.props; + const { loading, openModal } = this.state; const { actions = {} } = profile; const backupUrl = `${getBaseUrl()}${getQualityProfileBackupUrl(profile)}`; @@ -144,86 +185,110 @@ export class ProfileActions extends React.PureComponent { <> {actions.edit && ( - - - {translate('quality_profiles.activate_more_rules')} - + + {translate('quality_profiles.activate_more_rules')} )} {!profile.isBuiltIn && ( - - {translate('backup_verb')} + + {translate('backup_verb')} )} - - {translate('compare')} + + {translate('compare')} {actions.copy && ( <> - - {translate('copy')} + + {translate('copy')} - - {translate('extend')} + + {translate('extend')} )} {actions.edit && ( - - {translate('rename')} + + {translate('rename')} )} {actions.setAsDefault && ( - - - {translate('set_as_default')} - + + {translate('set_as_default')} )} {actions.delete && } {actions.delete && ( - - {translate('delete')} + + {translate('delete')} )} - {this.state.copyFormOpen && ( - )} - {this.state.extendFormOpen && ( - )} - {this.state.deleteFormOpen && ( - )} - {this.state.renameFormOpen && ( - )} diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileModalForm.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileModalForm.tsx new file mode 100644 index 00000000000..d532c30599d --- /dev/null +++ b/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileModalForm.tsx @@ -0,0 +1,86 @@ +/* + * SonarQube + * Copyright (C) 2009-2021 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 * as React from 'react'; +import { ResetButtonLink, SubmitButton } from 'sonar-ui-common/components/controls/buttons'; +import Modal from 'sonar-ui-common/components/controls/Modal'; +import MandatoryFieldMarker from 'sonar-ui-common/components/ui/MandatoryFieldMarker'; +import MandatoryFieldsExplanation from 'sonar-ui-common/components/ui/MandatoryFieldsExplanation'; +import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n'; +import { Profile } from '../types'; + +export interface ProfileModalFormProps { + btnLabelKey: string; + headerKey: string; + loading: boolean; + onClose: () => void; + onSubmit: (name: string) => void; + profile: Profile; +} + +export default function ProfileModalForm(props: ProfileModalFormProps) { + const { btnLabelKey, headerKey, loading, profile } = props; + const [name, setName] = React.useState(undefined); + + const submitDisabled = loading || !name || name === profile.name; + const header = translateWithParameters(headerKey, profile.name, profile.languageName); + + return ( + +
) => { + e.preventDefault(); + if (name) { + props.onSubmit(name); + } + }}> +
+

{header}

+
+
+ +
+ + ) => { + setName(e.currentTarget.value); + }} + required={true} + size={50} + type="text" + value={name ?? profile.name} + /> +
+
+
+ {loading && } + {translate(btnLabelKey)} + {translate('cancel')} +
+ +
+ ); +} diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/RenameProfileForm.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/components/RenameProfileForm.tsx deleted file mode 100644 index 43ef7a02c94..00000000000 --- a/server/sonar-web/src/main/js/apps/quality-profiles/components/RenameProfileForm.tsx +++ /dev/null @@ -1,123 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2021 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 * as React from 'react'; -import { ResetButtonLink, SubmitButton } from 'sonar-ui-common/components/controls/buttons'; -import Modal from 'sonar-ui-common/components/controls/Modal'; -import MandatoryFieldMarker from 'sonar-ui-common/components/ui/MandatoryFieldMarker'; -import MandatoryFieldsExplanation from 'sonar-ui-common/components/ui/MandatoryFieldsExplanation'; -import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n'; -import { renameProfile } from '../../../api/quality-profiles'; -import { Profile } from '../types'; - -interface Props { - onClose: () => void; - onRename: (name: string) => void; - profile: Profile; -} - -interface State { - loading: boolean; - name: string | null; -} - -export default class RenameProfileForm extends React.PureComponent { - mounted = false; - state: State = { loading: false, name: null }; - - componentDidMount() { - this.mounted = true; - } - - componentWillUnmount() { - this.mounted = false; - } - - handleNameChange = (event: React.SyntheticEvent) => { - this.setState({ name: event.currentTarget.value }); - }; - - handleFormSubmit = (event: React.SyntheticEvent) => { - event.preventDefault(); - - const { name } = this.state; - - if (name != null) { - this.setState({ loading: true }); - renameProfile(this.props.profile.key, name).then( - () => this.props.onRename(name), - () => { - if (this.mounted) { - this.setState({ loading: false }); - } - } - ); - } - }; - - render() { - const { profile } = this.props; - const header = translateWithParameters( - 'quality_profiles.rename_x_title', - profile.name, - profile.languageName - ); - const submitDisabled = - this.state.loading || !this.state.name || this.state.name === profile.name; - - return ( - -
-
-

{header}

-
-
- -
- - -
-
-
- {this.state.loading && } - - {translate('rename')} - - - {translate('cancel')} - -
- -
- ); - } -} diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/DeleteProfileForm-test.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/DeleteProfileForm-test.tsx index 801c736874e..58cbfe7eb0d 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/DeleteProfileForm-test.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/DeleteProfileForm-test.tsx @@ -17,35 +17,35 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ + import { shallow } from 'enzyme'; import * as React from 'react'; -import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils'; -import { deleteProfile } from '../../../../api/quality-profiles'; import { mockEvent, mockQualityProfile } from '../../../../helpers/testMocks'; -import DeleteProfileForm from '../DeleteProfileForm'; - -beforeEach(() => jest.clearAllMocks()); - -jest.mock('../../../../api/quality-profiles', () => ({ - deleteProfile: jest.fn().mockResolvedValue({}) -})); +import DeleteProfileForm, { DeleteProfileFormProps } from '../DeleteProfileForm'; it('should render correctly', () => { - const wrapper = shallowRender(); - expect(wrapper).toMatchSnapshot(); + expect(shallowRender()).toMatchSnapshot('default'); + expect(shallowRender({ loading: true })).toMatchSnapshot('loading'); + expect(shallowRender({ profile: mockQualityProfile({ childrenCount: 2 }) })).toMatchSnapshot( + 'profile has children' + ); }); -it('should handle form submit correctly', async () => { - const wrapper = shallowRender(); - wrapper.instance().handleFormSubmit(mockEvent()); - await waitAndUpdate(wrapper); +it('should correctly submit the form', () => { + const onDelete = jest.fn(); + const wrapper = shallowRender({ onDelete }); - expect(deleteProfile).toHaveBeenCalled(); + const formOnSubmit = wrapper.find('form').props().onSubmit; + if (formOnSubmit) { + formOnSubmit(mockEvent()); + } + expect(onDelete).toBeCalled(); }); -function shallowRender(props: Partial = {}) { - return shallow( +function shallowRender(props: Partial = {}) { + return shallow( ({ - createQualityProfile: jest.fn().mockResolvedValue({ profile: { key: 'new-profile' } }), - changeProfileParent: jest.fn().mockResolvedValue(true) -})); - -it('should render correctly', () => { - const wrapper = shallowRender(); - expect(wrapper).toMatchSnapshot(); -}); - -it('should correctly create a new profile and extend the existing one', async () => { - const profile = mockQualityProfile(); - const name = 'New name'; - const wrapper = shallowRender({ profile }); - - expect(wrapper.find('SubmitButton').props().disabled).toBe(true); - - wrapper.setState({ name }).update(); - wrapper.instance().handleFormSubmit(mockEvent()); - await waitAndUpdate(wrapper); - - const data = new FormData(); - data.append('language', profile.language); - data.append('name', name); - expect(createQualityProfile).toHaveBeenCalledWith(data); - expect(changeProfileParent).toHaveBeenCalledWith({ key: 'new-profile' }, profile); -}); - -function shallowRender(props: Partial = {}) { - return shallow( - - ); -} diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/ProfileActions-test.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/ProfileActions-test.tsx index 4c14e43e208..a6ad0fffb57 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/ProfileActions-test.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/ProfileActions-test.tsx @@ -20,16 +20,36 @@ import { shallow } from 'enzyme'; import * as React from 'react'; import { click, waitAndUpdate } from 'sonar-ui-common/helpers/testUtils'; -import { setDefaultProfile } from '../../../../api/quality-profiles'; +import { + changeProfileParent, + copyProfile, + createQualityProfile, + deleteProfile, + renameProfile, + setDefaultProfile +} from '../../../../api/quality-profiles'; import { mockQualityProfile, mockRouter } from '../../../../helpers/testMocks'; +import { ProfileActionModals } from '../../types'; +import { PROFILE_PATH } from '../../utils'; +import DeleteProfileForm from '../DeleteProfileForm'; import { ProfileActions } from '../ProfileActions'; +import ProfileModalForm from '../ProfileModalForm'; -beforeEach(() => jest.clearAllMocks()); +jest.mock('../../../../api/quality-profiles', () => { + const { mockQualityProfile } = jest.requireActual('../../../../helpers/testMocks'); -jest.mock('../../../../api/quality-profiles', () => ({ - ...jest.requireActual('../../../../api/quality-profiles'), - setDefaultProfile: jest.fn().mockResolvedValue({}) -})); + return { + ...jest.requireActual('../../../../api/quality-profiles'), + copyProfile: jest.fn().mockResolvedValue(null), + changeProfileParent: jest.fn().mockResolvedValue(null), + createQualityProfile: jest + .fn() + .mockResolvedValue({ profile: mockQualityProfile({ key: 'newProfile' }) }), + deleteProfile: jest.fn().mockResolvedValue(null), + setDefaultProfile: jest.fn().mockResolvedValue(null), + renameProfile: jest.fn().mockResolvedValue(null) + }; +}); const PROFILE = mockQualityProfile({ activeRuleCount: 68, @@ -39,15 +59,13 @@ const PROFILE = mockQualityProfile({ rulesUpdatedAt: '2017-06-28T12:58:44+0000' }); -it('renders with no permissions', () => { - expect(shallowRender()).toMatchSnapshot(); -}); - -it('renders with permission to edit only', () => { - expect(shallowRender({ profile: { ...PROFILE, actions: { edit: true } } })).toMatchSnapshot(); -}); +beforeEach(() => jest.clearAllMocks()); -it('renders with all permissions', () => { +it('renders correctly', () => { + expect(shallowRender()).toMatchSnapshot('no permissions'); + expect(shallowRender({ profile: { ...PROFILE, actions: { edit: true } } })).toMatchSnapshot( + 'edit only' + ); expect( shallowRender({ profile: { @@ -61,58 +79,240 @@ it('renders with all permissions', () => { } } }) - ).toMatchSnapshot(); + ).toMatchSnapshot('all permissions'); + + expect(shallowRender().setState({ openModal: ProfileActionModals.Copy })).toMatchSnapshot( + 'copy modal' + ); + expect(shallowRender().setState({ openModal: ProfileActionModals.Extend })).toMatchSnapshot( + 'extend modal' + ); + expect(shallowRender().setState({ openModal: ProfileActionModals.Rename })).toMatchSnapshot( + 'rename modal' + ); + expect(shallowRender().setState({ openModal: ProfileActionModals.Delete })).toMatchSnapshot( + 'delete modal' + ); }); -it('should copy profile', async () => { - const name = 'new-name'; - const updateProfiles = jest.fn(() => Promise.resolve()); - const push = jest.fn(); - const wrapper = shallowRender({ - profile: { ...PROFILE, actions: { copy: true } }, - router: { push, replace: jest.fn() }, - updateProfiles +describe('copy a profile', () => { + it('should correctly copy a profile', async () => { + const name = 'new-name'; + const updateProfiles = jest.fn().mockResolvedValue(null); + const push = jest.fn(); + const wrapper = shallowRender({ + profile: { ...PROFILE, actions: { copy: true } }, + router: mockRouter({ push }), + updateProfiles + }); + + click(wrapper.find('.it__quality-profiles__copy')); + expect(wrapper.find(ProfileModalForm).exists()).toBe(true); + + wrapper + .find(ProfileModalForm) + .props() + .onSubmit(name); + expect(copyProfile).toBeCalledWith(PROFILE.key, name); + await waitAndUpdate(wrapper); + + expect(updateProfiles).toBeCalled(); + expect(push).toBeCalledWith({ + pathname: '/profiles/show', + query: { language: 'js', name } + }); + expect(wrapper.find(ProfileModalForm).exists()).toBe(false); }); - click(wrapper.find('[data-test="quality-profiles__copy"]').parent()); - expect(wrapper.find('CopyProfileForm').exists()).toBe(true); + it('should correctly keep the modal open in case of an error', async () => { + (copyProfile as jest.Mock).mockRejectedValueOnce(null); - wrapper.find('CopyProfileForm').prop('onCopy')(name); - expect(updateProfiles).toBeCalled(); - await waitAndUpdate(wrapper); + const name = 'new-name'; + const updateProfiles = jest.fn(); + const push = jest.fn(); + const wrapper = shallowRender({ + profile: { ...PROFILE, actions: { copy: true } }, + router: mockRouter({ push }), + updateProfiles + }); + wrapper.setState({ openModal: ProfileActionModals.Copy }); + + wrapper.instance().handleProfileCopy(name); + await waitAndUpdate(wrapper); + + expect(updateProfiles).not.toBeCalled(); + await waitAndUpdate(wrapper); - expect(push).toBeCalledWith({ - pathname: '/profiles/show', - query: { language: 'js', name } + expect(push).not.toBeCalled(); + expect(wrapper.state().openModal).toBe(ProfileActionModals.Copy); }); - expect(wrapper.find('CopyProfileForm').exists()).toBe(false); }); -it('should extend profile', async () => { - const name = 'new-name'; - const updateProfiles = jest.fn(() => Promise.resolve()); - const push = jest.fn(); - const wrapper = shallowRender({ - profile: { ...PROFILE, actions: { copy: true } }, - router: { push, replace: jest.fn() }, - updateProfiles +describe('extend a profile', () => { + it('should correctly extend a profile', async () => { + const name = 'new-name'; + const profile = { ...PROFILE, actions: { copy: true } }; + const updateProfiles = jest.fn().mockResolvedValue(null); + const push = jest.fn(); + const wrapper = shallowRender({ + profile, + router: mockRouter({ push }), + updateProfiles + }); + + click(wrapper.find('.it__quality-profiles__extend')); + expect(wrapper.find(ProfileModalForm).exists()).toBe(true); + + wrapper + .find(ProfileModalForm) + .props() + .onSubmit(name); + expect(createQualityProfile).toBeCalledWith({ language: profile.language, name }); + await waitAndUpdate(wrapper); + expect(changeProfileParent).toBeCalledWith( + expect.objectContaining({ + key: 'newProfile' + }), + profile + ); + await waitAndUpdate(wrapper); + + expect(updateProfiles).toBeCalled(); + await waitAndUpdate(wrapper); + + expect(push).toBeCalledWith({ + pathname: '/profiles/show', + query: { language: 'js', name } + }); + expect(wrapper.find(ProfileModalForm).exists()).toBe(false); }); - click(wrapper.find('[data-test="quality-profiles__extend"]').parent()); - expect(wrapper.find('ExtendProfileForm').exists()).toBe(true); + it('should correctly keep the modal open in case of an error', async () => { + (createQualityProfile as jest.Mock).mockRejectedValueOnce(null); - wrapper.find('ExtendProfileForm').prop('onExtend')(name); - expect(updateProfiles).toBeCalled(); - await waitAndUpdate(wrapper); + const name = 'new-name'; + const updateProfiles = jest.fn(); + const push = jest.fn(); + const wrapper = shallowRender({ + profile: { ...PROFILE, actions: { copy: true } }, + router: mockRouter({ push }), + updateProfiles + }); + wrapper.setState({ openModal: ProfileActionModals.Extend }); + + wrapper.instance().handleProfileExtend(name); + await waitAndUpdate(wrapper); + + expect(updateProfiles).not.toBeCalled(); + expect(changeProfileParent).not.toBeCalled(); + expect(push).not.toBeCalled(); + expect(wrapper.state().openModal).toBe(ProfileActionModals.Extend); + }); +}); + +describe('rename a profile', () => { + it('should correctly rename a profile', async () => { + const name = 'new-name'; + const updateProfiles = jest.fn().mockResolvedValue(null); + const push = jest.fn(); + const wrapper = shallowRender({ + profile: { ...PROFILE, actions: { edit: true } }, + router: mockRouter({ push }), + updateProfiles + }); + + click(wrapper.find('.it__quality-profiles__rename')); + expect(wrapper.find(ProfileModalForm).exists()).toBe(true); + + wrapper + .find(ProfileModalForm) + .props() + .onSubmit(name); + expect(renameProfile).toBeCalledWith(PROFILE.key, name); + await waitAndUpdate(wrapper); + + expect(updateProfiles).toBeCalled(); + expect(push).toBeCalledWith({ + pathname: '/profiles/show', + query: { language: 'js', name } + }); + expect(wrapper.find(ProfileModalForm).exists()).toBe(false); + }); + + it('should correctly keep the modal open in case of an error', async () => { + (renameProfile as jest.Mock).mockRejectedValueOnce(null); + + const name = 'new-name'; + const updateProfiles = jest.fn(); + const push = jest.fn(); + const wrapper = shallowRender({ + profile: { ...PROFILE, actions: { copy: true } }, + router: mockRouter({ push }), + updateProfiles + }); + wrapper.setState({ openModal: ProfileActionModals.Rename }); + + wrapper.instance().handleProfileRename(name); + await waitAndUpdate(wrapper); + + expect(updateProfiles).not.toBeCalled(); + await waitAndUpdate(wrapper); + + expect(push).not.toBeCalled(); + expect(wrapper.state().openModal).toBe(ProfileActionModals.Rename); + }); +}); + +describe('delete a profile', () => { + it('should correctly delete a profile', async () => { + const updateProfiles = jest.fn().mockResolvedValue(null); + const replace = jest.fn(); + const profile = { ...PROFILE, actions: { delete: true } }; + const wrapper = shallowRender({ + profile, + router: mockRouter({ replace }), + updateProfiles + }); + + click(wrapper.find('.it__quality-profiles__delete')); + expect(wrapper.find(DeleteProfileForm).exists()).toBe(true); + + wrapper + .find(DeleteProfileForm) + .props() + .onDelete(); + expect(deleteProfile).toBeCalledWith(profile); + await waitAndUpdate(wrapper); + + expect(updateProfiles).toBeCalled(); + expect(replace).toBeCalledWith(PROFILE_PATH); + expect(wrapper.find(ProfileModalForm).exists()).toBe(false); + }); + + it('should correctly keep the modal open in case of an error', async () => { + (deleteProfile as jest.Mock).mockRejectedValueOnce(null); + + const updateProfiles = jest.fn(); + const replace = jest.fn(); + const wrapper = shallowRender({ + profile: { ...PROFILE, actions: { copy: true } }, + router: mockRouter({ replace }), + updateProfiles + }); + wrapper.setState({ openModal: ProfileActionModals.Delete }); + + wrapper.instance().handleProfileDelete(); + await waitAndUpdate(wrapper); + + expect(updateProfiles).not.toBeCalled(); + await waitAndUpdate(wrapper); - expect(push).toBeCalledWith({ - pathname: '/profiles/show', - query: { language: 'js', name } + expect(replace).not.toBeCalled(); + expect(wrapper.state().openModal).toBe(ProfileActionModals.Delete); }); - expect(wrapper.find('ExtendProfileForm').exists()).toBe(false); }); -it('should delete profile properly', async () => { +it('should correctly set a profile as the default', async () => { const updateProfiles = jest.fn(); const wrapper = shallowRender({ updateProfiles }); diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/ProfileModalForm-test.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/ProfileModalForm-test.tsx new file mode 100644 index 00000000000..6029ef64ac1 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/ProfileModalForm-test.tsx @@ -0,0 +1,70 @@ +/* + * SonarQube + * Copyright (C) 2009-2021 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 { shallow } from 'enzyme'; +import * as React from 'react'; +import { change } from 'sonar-ui-common/helpers/testUtils'; +import { mockEvent, mockQualityProfile } from '../../../../helpers/testMocks'; +import ProfileModalForm, { ProfileModalFormProps } from '../ProfileModalForm'; + +it('should render correctly', () => { + expect(shallowRender()).toMatchSnapshot('default'); + expect(shallowRender({ loading: true })).toMatchSnapshot('loading'); + + const wrapper = shallowRender(); + change(wrapper.find('#profile-name'), 'new name'); + expect(wrapper).toMatchSnapshot('can submit'); +}); + +it('should correctly submit the form', () => { + const onSubmit = jest.fn(); + const wrapper = shallowRender({ onSubmit }); + + // Won't submit unless a new name was given. + let formOnSubmit = wrapper.find('form').props().onSubmit; + if (formOnSubmit) { + formOnSubmit(mockEvent()); + } + expect(onSubmit).not.toBeCalled(); + + // Input a new name. + change(wrapper.find('#profile-name'), 'new name'); + + // Now will submit the form. + formOnSubmit = wrapper.find('form').props().onSubmit; + if (formOnSubmit) { + formOnSubmit(mockEvent()); + } + expect(onSubmit).toBeCalledWith('new name'); +}); + +function shallowRender(props: Partial = {}) { + return shallow( + + ); +} diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/__snapshots__/DeleteProfileForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/__snapshots__/DeleteProfileForm-test.tsx.snap index 41855ab772c..d24c01159c2 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/__snapshots__/DeleteProfileForm-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/__snapshots__/DeleteProfileForm-test.tsx.snap @@ -1,12 +1,52 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`should render correctly 1`] = ` +exports[`should render correctly: default 1`] = ` + +
+
+

+ quality_profiles.delete_confirm_title +

+
+
+

+ quality_profiles.are_you_sure_want_delete_profile_x.name.JavaScript +

+
+
+ + delete + + + cancel + +
+
+
+`; + +exports[`should render correctly: loading 1`] = `
-

quality_profiles.are_you_sure_want_delete_profile_x.name.JavaScript

+
+ + + delete + + + cancel + +
+ + +`; + +exports[`should render correctly: profile has children 1`] = ` + +
+
+

+ quality_profiles.delete_confirm_title +

+
+
+
+ + quality_profiles.this_profile_has_descendants + +

+ quality_profiles.are_you_sure_want_delete_profile_x_and_descendants.name.JavaScript +

+
+
delete cancel diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/__snapshots__/ExtendProfileForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/__snapshots__/ExtendProfileForm-test.tsx.snap deleted file mode 100644 index 227de4584d0..00000000000 --- a/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/__snapshots__/ExtendProfileForm-test.tsx.snap +++ /dev/null @@ -1,69 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render correctly 1`] = ` - - -
-

- quality_profiles.extend_x_title.name.JavaScript -

-
-
- -
- - -
-
-
- - - extend - - - cancel - -
- -
-`; diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/__snapshots__/ProfileActions-test.tsx.snap b/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/__snapshots__/ProfileActions-test.tsx.snap index f59b868a871..23a829c4d9a 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/__snapshots__/ProfileActions-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/__snapshots__/ProfileActions-test.tsx.snap @@ -1,9 +1,10 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`renders with all permissions 1`] = ` +exports[`renders correctly: all permissions 1`] = ` - - quality_profiles.activate_more_rules - + quality_profiles.activate_more_rules - - backup_verb - + backup_verb - - compare - + compare - - copy - + copy - - extend - + extend - - rename - + rename - - set_as_default - + set_as_default - - delete - + delete `; -exports[`renders with no permissions 1`] = ` +exports[`renders correctly: copy modal 1`] = ` - - backup_verb - + backup_verb - - compare - + compare + + +`; + +exports[`renders correctly: delete modal 1`] = ` + + + + backup_verb + + + compare + + + `; -exports[`renders with permission to edit only 1`] = ` +exports[`renders correctly: edit only 1`] = ` - - quality_profiles.activate_more_rules - + quality_profiles.activate_more_rules - - backup_verb - + backup_verb - - compare - + compare - - rename - + rename + + + +`; + +exports[`renders correctly: extend modal 1`] = ` + + + + backup_verb + + + compare + + +`; + +exports[`renders correctly: no permissions 1`] = ` + + + + backup_verb + + + compare + + + +`; + +exports[`renders correctly: rename modal 1`] = ` + + + + backup_verb + + + compare + + + `; diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/__snapshots__/ProfileModalForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/__snapshots__/ProfileModalForm-test.tsx.snap new file mode 100644 index 00000000000..96d57215b30 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/__snapshots__/ProfileModalForm-test.tsx.snap @@ -0,0 +1,190 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render correctly: can submit 1`] = ` + +
+
+

+ header-label.name.JavaScript +

+
+
+ +
+ + +
+
+
+ + btn-label + + + cancel + +
+
+
+`; + +exports[`should render correctly: default 1`] = ` + +
+
+

+ header-label.name.JavaScript +

+
+
+ +
+ + +
+
+
+ + btn-label + + + cancel + +
+
+
+`; + +exports[`should render correctly: loading 1`] = ` + +
+
+

+ header-label.name.JavaScript +

+
+
+ +
+ + +
+
+
+ + + btn-label + + + cancel + +
+ +
+`; diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileInheritanceBox.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileInheritanceBox.tsx index 834d936cd60..096152f3372 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileInheritanceBox.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileInheritanceBox.tsx @@ -17,6 +17,7 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import * as classNames from 'classnames'; import * as React from 'react'; import HelpTooltip from 'sonar-ui-common/components/controls/HelpTooltip'; import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n'; @@ -46,7 +47,7 @@ export default function ProfileInheritanceBox(props: Props) { const offset = 25 * depth; return ( - +
{displayLink ? ( diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfileInheritanceBox-test.tsx.snap b/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfileInheritanceBox-test.tsx.snap index c42184e6a85..18416a4f9e3 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfileInheritanceBox-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfileInheritanceBox-test.tsx.snap @@ -2,7 +2,7 @@ exports[`should render correctly 1`] = `