diff options
author | Mathieu Suen <mathieu.suen@sonarsource.com> | 2023-06-07 10:47:43 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2023-06-08 20:03:08 +0000 |
commit | 6f888d63127e272d76683f05af7949222c424f9e (patch) | |
tree | 5c0a789195396c29bdf85e847bf31040800878fc /server | |
parent | 514e75c10add5b1bbe95387dd6b03db5f6a8aa93 (diff) | |
download | sonarqube-6f888d63127e272d76683f05af7949222c424f9e.tar.gz sonarqube-6f888d63127e272d76683f05af7949222c424f9e.zip |
SONAR-19380 Make alert component less noisy for screen reader
Diffstat (limited to 'server')
25 files changed, 221 insertions, 280 deletions
diff --git a/server/sonar-web/src/main/js/app/components/GitHubSynchronisationWarning.tsx b/server/sonar-web/src/main/js/app/components/GitHubSynchronisationWarning.tsx index 5d7cb2544c7..b9ee787e9ba 100644 --- a/server/sonar-web/src/main/js/app/components/GitHubSynchronisationWarning.tsx +++ b/server/sonar-web/src/main/js/app/components/GitHubSynchronisationWarning.tsx @@ -31,7 +31,7 @@ import './SystemAnnouncement.css'; interface LastSyncProps { short?: boolean; - info: Required<GithubStatusEnabled>['lastSync']; + info: GithubStatusEnabled['lastSync']; } interface GitHubSynchronisationWarningProps { @@ -39,6 +39,9 @@ interface GitHubSynchronisationWarningProps { } function LastSyncAlert({ info, short }: LastSyncProps) { + if (info === undefined) { + return null; + } const { finishedAt, errorMessage, status, summary } = info; const formattedDate = finishedAt ? formatDistance(new Date(finishedAt), new Date()) : ''; @@ -73,25 +76,33 @@ function LastSyncAlert({ info, short }: LastSyncProps) { ); } - return status === TaskStatuses.Success ? ( - <Alert variant="success"> - {translateWithParameters( - 'settings.authentication.github.synchronization_successful', - formattedDate + return ( + <Alert + variant={status === TaskStatuses.Success ? 'success' : 'error'} + role="alert" + aria-live="assertive" + > + {status === TaskStatuses.Success ? ( + <> + {translateWithParameters( + 'settings.authentication.github.synchronization_successful', + formattedDate + )} + <br /> + {summary ?? ''} + </> + ) : ( + <React.Fragment key={`synch-alert-${finishedAt}`}> + <div> + {translateWithParameters( + 'settings.authentication.github.synchronization_failed', + formattedDate + )} + </div> + <br /> + {errorMessage ?? ''} + </React.Fragment> )} - <br /> - {summary ?? ''} - </Alert> - ) : ( - <Alert variant="error"> - <div> - {translateWithParameters( - 'settings.authentication.github.synchronization_failed', - formattedDate - )} - </div> - <br /> - {errorMessage ?? ''} </Alert> ); } @@ -105,19 +116,28 @@ function GitHubSynchronisationWarning({ short }: GitHubSynchronisationWarningPro return ( <> - {!short && data?.nextSync && ( - <> - <Alert variant="loading" className="spacer-bottom"> - {translate( - data.nextSync.status === TaskStatuses.Pending - ? 'settings.authentication.github.synchronization_pending' - : 'settings.authentication.github.synchronization_in_progress' - )} - </Alert> - <br /> - </> - )} - {data?.lastSync && <LastSyncAlert short={short} info={data.lastSync} />} + <Alert + variant="loading" + className="spacer-bottom" + aria-atomic={true} + role="alert" + aria-live="assertive" + aria-label={ + data.nextSync === undefined + ? translate('settings.authentication.github.synchronization_finish') + : '' + } + > + {!short && + data?.nextSync && + translate( + data.nextSync.status === TaskStatuses.Pending + ? 'settings.authentication.github.synchronization_pending' + : 'settings.authentication.github.synchronization_in_progress' + )} + </Alert> + + <LastSyncAlert short={short} info={data.lastSync} /> </> ); } diff --git a/server/sonar-web/src/main/js/app/components/SystemAnnouncement.tsx b/server/sonar-web/src/main/js/app/components/SystemAnnouncement.tsx index a35020a1aaf..e954c7769c6 100644 --- a/server/sonar-web/src/main/js/app/components/SystemAnnouncement.tsx +++ b/server/sonar-web/src/main/js/app/components/SystemAnnouncement.tsx @@ -17,16 +17,17 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { isEmpty, keyBy, throttle } from 'lodash'; +import classNames from 'classnames'; +import { keyBy, throttle } from 'lodash'; import * as React from 'react'; import { getValues } from '../../api/settings'; import { Alert } from '../../components/ui/Alert'; import { Feature } from '../../types/features'; import { GlobalSettingKeys, SettingValue } from '../../types/settings'; +import './SystemAnnouncement.css'; import withAvailableFeatures, { WithAvailableFeaturesProps, } from './available-features/withAvailableFeatures'; -import './SystemAnnouncement.css'; const THROTTLE_TIME_MS = 10000; @@ -75,19 +76,18 @@ export class SystemAnnouncement extends React.PureComponent<WithAvailableFeature render() { const { displayMessage, message } = this.state; - if (!displayMessage || isEmpty(message)) { - return null; - } return ( - <div className="system-announcement-wrapper"> + <div className={classNames({ 'system-announcement-wrapper': displayMessage && message })}> <Alert className="system-announcement-banner" title={message} display="banner" variant="warning" + aria-live="assertive" + role="alert" > - {message} + {displayMessage && message} </Alert> </div> ); diff --git a/server/sonar-web/src/main/js/app/components/__tests__/SystemAnnouncement-test.tsx b/server/sonar-web/src/main/js/app/components/__tests__/SystemAnnouncement-test.tsx index 7bf9b99a359..686e7c50a7c 100644 --- a/server/sonar-web/src/main/js/app/components/__tests__/SystemAnnouncement-test.tsx +++ b/server/sonar-web/src/main/js/app/components/__tests__/SystemAnnouncement-test.tsx @@ -22,8 +22,8 @@ import * as React from 'react'; import { getValues } from '../../../api/settings'; import { renderComponent } from '../../../helpers/testReactTestingUtils'; import { Feature } from '../../../types/features'; -import { AvailableFeaturesContext } from '../available-features/AvailableFeaturesContext'; import SystemAnnouncement from '../SystemAnnouncement'; +import { AvailableFeaturesContext } from '../available-features/AvailableFeaturesContext'; jest.mock('../../../api/settings', () => ({ getValues: jest.fn(), diff --git a/server/sonar-web/src/main/js/app/components/indexation/IndexationNotification.tsx b/server/sonar-web/src/main/js/app/components/indexation/IndexationNotification.tsx index 4a41b5d0e0a..d1d96f6f081 100644 --- a/server/sonar-web/src/main/js/app/components/indexation/IndexationNotification.tsx +++ b/server/sonar-web/src/main/js/app/components/indexation/IndexationNotification.tsx @@ -97,10 +97,6 @@ export class IndexationNotification extends React.PureComponent<Props, State> { }, } = this.props; - if (notificationType === undefined) { - return null; - } - return ( <IndexationNotificationRenderer type={notificationType} diff --git a/server/sonar-web/src/main/js/app/components/indexation/IndexationNotificationRenderer.tsx b/server/sonar-web/src/main/js/app/components/indexation/IndexationNotificationRenderer.tsx index 07d5839c7f5..defc97b7527 100644 --- a/server/sonar-web/src/main/js/app/components/indexation/IndexationNotificationRenderer.tsx +++ b/server/sonar-web/src/main/js/app/components/indexation/IndexationNotificationRenderer.tsx @@ -19,6 +19,7 @@ */ /* eslint-disable react/no-unused-prop-types */ +import classNames from 'classnames'; import * as React from 'react'; import { FormattedMessage } from 'react-intl'; import Link from '../../../components/common/Link'; @@ -29,7 +30,7 @@ import { IndexationNotificationType } from '../../../types/indexation'; import { TaskStatuses, TaskTypes } from '../../../types/tasks'; export interface IndexationNotificationRendererProps { - type: IndexationNotificationType; + type?: IndexationNotificationType; percentCompleted: number; isSystemAdmin: boolean; } @@ -45,20 +46,23 @@ export default function IndexationNotificationRenderer(props: IndexationNotifica const { type } = props; return ( - <div className="indexation-notification-wrapper"> + <div className={classNames({ 'indexation-notification-wrapper': type })}> <Alert className="indexation-notification-banner" display="banner" - variant={NOTIFICATION_VARIANTS[type]} + variant={type ? NOTIFICATION_VARIANTS[type] : 'success'} + aria-live="assertive" > - <div className="display-flex-center"> - {type === IndexationNotificationType.Completed && renderCompletedBanner(props)} - {type === IndexationNotificationType.CompletedWithFailure && - renderCompletedWithFailureBanner(props)} - {type === IndexationNotificationType.InProgress && renderInProgressBanner(props)} - {type === IndexationNotificationType.InProgressWithFailure && - renderInProgressWithFailureBanner(props)} - </div> + {type !== undefined && ( + <div className="display-flex-center"> + {type === IndexationNotificationType.Completed && renderCompletedBanner(props)} + {type === IndexationNotificationType.CompletedWithFailure && + renderCompletedWithFailureBanner(props)} + {type === IndexationNotificationType.InProgress && renderInProgressBanner(props)} + {type === IndexationNotificationType.InProgressWithFailure && + renderInProgressWithFailureBanner(props)} + </div> + )} </Alert> </div> ); diff --git a/server/sonar-web/src/main/js/app/components/indexation/__tests__/__snapshots__/IndexationNotificationRenderer-test.tsx.snap b/server/sonar-web/src/main/js/app/components/indexation/__tests__/__snapshots__/IndexationNotificationRenderer-test.tsx.snap index ffa9743fd65..edcf985e28b 100644 --- a/server/sonar-web/src/main/js/app/components/indexation/__tests__/__snapshots__/IndexationNotificationRenderer-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/indexation/__tests__/__snapshots__/IndexationNotificationRenderer-test.tsx.snap @@ -5,6 +5,7 @@ exports[`should render correctly for type="Completed" & isSystemAdmin=false 1`] className="indexation-notification-wrapper" > <Alert + aria-live="assertive" className="indexation-notification-banner" display="banner" variant="success" @@ -27,6 +28,7 @@ exports[`should render correctly for type="Completed" & isSystemAdmin=true 1`] = className="indexation-notification-wrapper" > <Alert + aria-live="assertive" className="indexation-notification-banner" display="banner" variant="success" @@ -49,6 +51,7 @@ exports[`should render correctly for type="CompletedWithFailure" & isSystemAdmin className="indexation-notification-wrapper" > <Alert + aria-live="assertive" className="indexation-notification-banner" display="banner" variant="error" @@ -79,6 +82,7 @@ exports[`should render correctly for type="CompletedWithFailure" & isSystemAdmin className="indexation-notification-wrapper" > <Alert + aria-live="assertive" className="indexation-notification-banner" display="banner" variant="error" @@ -118,6 +122,7 @@ exports[`should render correctly for type="InProgress" & isSystemAdmin=false 1`] className="indexation-notification-wrapper" > <Alert + aria-live="assertive" className="indexation-notification-banner" display="banner" variant="warning" @@ -148,6 +153,7 @@ exports[`should render correctly for type="InProgress" & isSystemAdmin=true 1`] className="indexation-notification-wrapper" > <Alert + aria-live="assertive" className="indexation-notification-banner" display="banner" variant="warning" @@ -200,6 +206,7 @@ exports[`should render correctly for type="InProgressWithFailure" & isSystemAdmi className="indexation-notification-wrapper" > <Alert + aria-live="assertive" className="indexation-notification-banner" display="banner" variant="error" @@ -238,6 +245,7 @@ exports[`should render correctly for type="InProgressWithFailure" & isSystemAdmi className="indexation-notification-wrapper" > <Alert + aria-live="assertive" className="indexation-notification-banner" display="banner" variant="error" diff --git a/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/notifications/ProjectNotifications.tsx b/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/notifications/ProjectNotifications.tsx index 8e666804de8..b8d254a6dbc 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/notifications/ProjectNotifications.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/notifications/ProjectNotifications.tsx @@ -69,7 +69,7 @@ export function ProjectNotifications(props: WithNotificationsProps & Props) { {translate('project.info.notifications')} </h3> - <Alert className="spacer-top" variant="info" aria-live="off"> + <Alert className="spacer-top" variant="info"> {translate('notification.dispatcher.information')} </Alert> diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/Azure-it.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/Azure-it.tsx index a3e4a6b838f..8ddbbacb833 100644 --- a/server/sonar-web/src/main/js/apps/create/project/__tests__/Azure-it.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/Azure-it.tsx @@ -129,7 +129,7 @@ it('should show search filter when PAT is already set', async () => { // Should search with empty results almIntegrationHandler.setSearchAzureRepositories([]); await user.keyboard('f'); - expect(screen.getByRole('alert')).toHaveTextContent('onboarding.create_project.azure.no_results'); + expect(screen.getByText('onboarding.create_project.azure.no_results')).toBeInTheDocument(); }); function renderCreateProject(props: Partial<CreateProjectPageProps> = {}) { diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/Bitbucket-it.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/Bitbucket-it.tsx index 6c66849c475..0112268c36f 100644 --- a/server/sonar-web/src/main/js/apps/create/project/__tests__/Bitbucket-it.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/Bitbucket-it.tsx @@ -158,7 +158,7 @@ it('should show no result message when there are no projects', async () => { await selectEvent.select(ui.instanceSelector.get(), [/conf-bitbucketserver-2/]); }); - expect(screen.getByRole('alert')).toHaveTextContent('onboarding.create_project.no_bbs_projects'); + expect(screen.getByText('onboarding.create_project.no_bbs_projects')).toBeInTheDocument(); }); function renderCreateProject(props: Partial<CreateProjectPageProps> = {}) { diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/BitbucketCloud-it.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/BitbucketCloud-it.tsx index c301a25e9df..ee4792f5d19 100644 --- a/server/sonar-web/src/main/js/apps/create/project/__tests__/BitbucketCloud-it.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/BitbucketCloud-it.tsx @@ -189,9 +189,9 @@ it('should show no result message when there are no projects', async () => { await selectEvent.select(ui.instanceSelector.get(), [/conf-bitbucketcloud-2/]); }); - expect(screen.getByRole('alert')).toHaveTextContent( - 'onboarding.create_project.bitbucketcloud.no_projects' - ); + expect( + screen.getByText('onboarding.create_project.bitbucketcloud.no_projects') + ).toBeInTheDocument(); }); it('should have load more', async () => { diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/GitLab-it.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/GitLab-it.tsx index 0e71dbb86ff..aa090fb0c25 100644 --- a/server/sonar-web/src/main/js/apps/create/project/__tests__/GitLab-it.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/GitLab-it.tsx @@ -179,9 +179,7 @@ it('should show no result message when there are no projects', async () => { await selectEvent.select(ui.instanceSelector.get(), [/conf-final-2/]); }); - expect(screen.getByRole('alert')).toHaveTextContent( - 'onboarding.create_project.gitlab.no_projects' - ); + expect(screen.getByText('onboarding.create_project.gitlab.no_projects')).toBeInTheDocument(); }); it('should display a warning if the instance default new code definition is not CaYC compliant', async () => { @@ -197,9 +195,9 @@ it('should display a warning if the instance default new code definition is not expect(screen.getByText('Gitlab project 1')).toBeInTheDocument(); expect(screen.getByText('Gitlab project 2')).toBeInTheDocument(); - expect(screen.getByRole('alert')).toHaveTextContent( - 'onboarding.create_project.new_code_option.warning.title' - ); + expect( + screen.getByText('onboarding.create_project.new_code_option.warning.title') + ).toBeInTheDocument(); }); function renderCreateProject(props: Partial<CreateProjectPageProps> = {}) { diff --git a/server/sonar-web/src/main/js/apps/overview/components/__tests__/App-test.tsx b/server/sonar-web/src/main/js/apps/overview/components/__tests__/App-test.tsx index bb669487c1c..22395f7a113 100644 --- a/server/sonar-web/src/main/js/apps/overview/components/__tests__/App-test.tsx +++ b/server/sonar-web/src/main/js/apps/overview/components/__tests__/App-test.tsx @@ -30,16 +30,14 @@ import { App } from '../App'; it('should render Empty Overview for Application with no analysis', async () => { renderApp({ component: mockComponent({ qualifier: ComponentQualifier.Application }) }); - expect( - await screen.findByRole('alert', { name: 'provisioning.no_analysis.application' }) - ).toBeInTheDocument(); + expect(await screen.findByText('provisioning.no_analysis.application')).toBeInTheDocument(); }); it('should render Empty Overview on main branch with no analysis', async () => { renderApp({}, mockCurrentUser()); expect( - await screen.findByRole('alert', { name: 'provisioning.no_analysis_on_main_branch.master' }) + await screen.findByText('provisioning.no_analysis_on_main_branch.master') ).toBeInTheDocument(); }); @@ -47,9 +45,9 @@ it('should render Empty Overview on main branch with multiple branches with bad renderApp({ branchLikes: [mockBranch(), mockBranch()] }); expect( - await screen.findByRole('alert', { - name: 'provisioning.no_analysis_on_main_branch.bad_configuration.master.branches.main_branch', - }) + await screen.findByText( + 'provisioning.no_analysis_on_main_branch.bad_configuration.master.branches.main_branch' + ) ).toBeInTheDocument(); }); diff --git a/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/ProjectQualityGateApp-it.tsx b/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/ProjectQualityGateApp-it.tsx index 72f27addb65..2865c4b984a 100644 --- a/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/ProjectQualityGateApp-it.tsx +++ b/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/ProjectQualityGateApp-it.tsx @@ -24,8 +24,8 @@ import { QualityGatesServiceMock } from '../../../api/mocks/QualityGatesServiceM import handleRequiredAuthorization from '../../../app/utils/handleRequiredAuthorization'; import { mockComponent } from '../../../helpers/mocks/component'; import { - renderAppWithComponentContext, RenderContext, + renderAppWithComponentContext, } from '../../../helpers/testReactTestingUtils'; import { Component } from '../../../types/types'; import routes from '../routes'; @@ -52,7 +52,7 @@ const ui = { saveButton: byRole('button', { name: 'save' }), statusMessage: byRole('status'), noConditionsNewCodeWarning: byText('project_quality_gate.no_condition_on_new_code'), - alertMessage: byRole('alert'), + alertMessage: byText('unknown'), }; beforeAll(() => { @@ -106,7 +106,7 @@ it('renders nothing and shows alert when any API fails', async () => { handler.setThrowOnGetGateForProject(true); renderProjectQualityGateApp(); - expect(await ui.alertMessage.find()).toHaveTextContent('unknown'); + expect(await ui.alertMessage.find()).toBeInTheDocument(); expect(ui.qualityGateHeading.query()).not.toBeInTheDocument(); }); diff --git a/server/sonar-web/src/main/js/apps/sessions/components/__tests__/Login-it.tsx b/server/sonar-web/src/main/js/apps/sessions/components/__tests__/Login-it.tsx index 7439fe27fc0..69a00857dbd 100644 --- a/server/sonar-web/src/main/js/apps/sessions/components/__tests__/Login-it.tsx +++ b/server/sonar-web/src/main/js/apps/sessions/components/__tests__/Login-it.tsx @@ -139,7 +139,6 @@ it("should show a warning if there's an authorization error", async () => { const heading = await screen.findByRole('heading', { name: 'login.login_to_sonarqube' }); expect(heading).toBeInTheDocument(); - expect(screen.getByRole('alert')).toBeInTheDocument(); expect(screen.getByText('login.unauthorized_access_alert')).toBeInTheDocument(); }); diff --git a/server/sonar-web/src/main/js/apps/settings/components/__tests__/NewCodePeriod-it.tsx b/server/sonar-web/src/main/js/apps/settings/components/__tests__/NewCodePeriod-it.tsx index 2d8d3f4035c..640829ede70 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/__tests__/NewCodePeriod-it.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/__tests__/NewCodePeriod-it.tsx @@ -101,9 +101,7 @@ it('renders and behaves properly when the current value is not compliant', async expect(ui.daysInput.get()).toHaveValue('91'); // Should warn about non compliant value - expect(screen.getByRole('alert')).toHaveTextContent( - 'baseline.number_days.compliance_warning.title' - ); + expect(screen.getByText('baseline.number_days.compliance_warning.title')).toBeInTheDocument(); await user.clear(ui.daysInput.get()); await user.type(ui.daysInput.get(), '92'); diff --git a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/AlmBindingDefinitionForm-test.tsx b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/AlmBindingDefinitionForm-test.tsx index e46b6a0fb15..bf4bce05fa0 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/AlmBindingDefinitionForm-test.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/AlmBindingDefinitionForm-test.tsx @@ -19,7 +19,7 @@ */ import userEvent from '@testing-library/user-event'; import React from 'react'; -import { byRole } from 'testing-library-selector'; +import { byRole, byText } from 'testing-library-selector'; import AlmSettingsServiceMock from '../../../../../api/mocks/AlmSettingsServiceMock'; import { renderComponent } from '../../../../../helpers/testReactTestingUtils'; import { AlmKeys } from '../../../../../types/alm-settings'; @@ -46,7 +46,7 @@ const ui = { byRole('textbox', { name: `settings.almintegration.form.${id}` }), saveConfigurationButton: byRole('button', { name: 'settings.almintegration.form.save' }), cancelButton: byRole('button', { name: 'cancel' }), - validationError: byRole('alert'), + validationError: (text: string) => byText(text), }; const onCancel = jest.fn(); @@ -61,7 +61,7 @@ it('enforceValidation enabled', async () => { await userEvent.type(ui.configurationInput('personal_access_token').get(), 'Access Token'); await userEvent.click(ui.saveConfigurationButton.get()); - expect(ui.validationError.get()).toHaveTextContent('Validation Error'); + expect(ui.validationError('Validation Error').get()).toBeInTheDocument(); await userEvent.click(ui.cancelButton.get()); expect(onCancel).toHaveBeenCalled(); diff --git a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/AlmIntegration-it.tsx b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/AlmIntegration-it.tsx index 1cb65f52ee6..5d7308fa443 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/AlmIntegration-it.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/AlmIntegration-it.tsx @@ -176,8 +176,7 @@ function getPageObjects() { confirmDelete: byRole('button', { name: 'delete' }), checkConfigurationButton: (key: string) => byRole('button', { name: `settings.almintegration.check_configuration_x.${key}` }), - validationErrorMessage: byRole('alert'), - validationSuccessMessage: byRole('status'), + validationMessage: (text: string) => byText(text), }; async function createConfiguration( @@ -221,15 +220,15 @@ function getPageObjects() { // Existing configuration is edited expect(screen.queryByRole('heading', { name: currentName })).not.toBeInTheDocument(); expect(screen.getByRole('heading', { name: newName })).toBeInTheDocument(); - expect(ui.validationErrorMessage.get()).toHaveTextContent('Something is wrong'); + expect(ui.validationMessage('Something is wrong').get()).toBeInTheDocument(); } async function checkConfiguration(name: string) { almSettings.setDefinitionErrorMessage(''); await userEvent.click(ui.checkConfigurationButton(name).get()); - expect(ui.validationSuccessMessage.getAll()[0]).toHaveTextContent( - 'alert.tooltip.successsettings.almintegration.configuration_valid' - ); + expect( + ui.validationMessage('settings.almintegration.configuration_valid').getAll()[0] + ).toBeInTheDocument(); } async function deleteConfiguration(name: string) { diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/GitHubConfigurationValidity.tsx b/server/sonar-web/src/main/js/apps/settings/components/authentication/GitHubConfigurationValidity.tsx index efaea7d8616..4f3d766774d 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/authentication/GitHubConfigurationValidity.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/authentication/GitHubConfigurationValidity.tsx @@ -103,7 +103,14 @@ function GitHubConfigurationValidity({ isAutoProvisioning }: Props) { return ( <> - <Alert title={messages[0]} variant={alertVariant}> + <Alert + title={messages[0]} + variant={alertVariant} + aria-live="polite" + role="status" + aria-atomic={true} + aria-busy={isFetching} + > <div className="sw-flex sw-justify-between sw-items-center"> <div> {messages.map((msg) => ( diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-it.tsx b/server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-it.tsx index 01996b13ff1..7e6e00010bd 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-it.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-it.tsx @@ -170,7 +170,7 @@ const ui = { configurationValiditySuccess: byRole('status', { name: /github.configuration.validation.valid/, }), - configurationValidityError: byRole('alert', { + configurationValidityError: byRole('status', { name: /github.configuration.validation.invalid/, }), checkConfigButton: byRole('button', { diff --git a/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/__tests__/PRDecorationBinding-it.tsx b/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/__tests__/PRDecorationBinding-it.tsx index 6120dab20e1..76feb1ec980 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/__tests__/PRDecorationBinding-it.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/__tests__/PRDecorationBinding-it.tsx @@ -20,7 +20,7 @@ import userEvent from '@testing-library/user-event'; import React from 'react'; import selectEvent from 'react-select-event'; -import { byRole } from 'testing-library-selector'; +import { byRole, byText } from 'testing-library-selector'; import AlmSettingsServiceMock from '../../../../../api/mocks/AlmSettingsServiceMock'; import CurrentUserContextProvider from '../../../../../app/components/current-user/CurrentUserContextProvider'; import { mockComponent } from '../../../../../helpers/mocks/component'; @@ -110,11 +110,11 @@ it.each([ } // Save form and check for errors await user.click(ui.saveButton.get()); - expect(ui.validationErrorMsg.get()).toHaveTextContent('cute error'); + expect(ui.validationMsg('cute error').get()).toBeInTheDocument(); // Check validation with errors await user.click(ui.validateButton.get()); - expect(ui.validationErrorMsg.get()).toHaveTextContent('cute error'); + expect(ui.validationMsg('cute error').get()).toBeInTheDocument(); // Save form and check for errors almSettings.setProjectBindingConfigurationErrors(undefined); @@ -123,22 +123,22 @@ it.each([ 'Anything' ); await user.click(ui.saveButton.get()); - expect(await ui.validationSuccessMsg.find()).toHaveTextContent( - 'settings.pr_decoration.binding.check_configuration.success' - ); + expect( + await ui.validationMsg('settings.pr_decoration.binding.check_configuration.success').find() + ).toBeInTheDocument(); await user.click(ui.validateButton.get()); - expect(ui.validationSuccessMsg.get()).toHaveTextContent( - 'settings.pr_decoration.binding.check_configuration.success' - ); + expect( + ui.validationMsg('settings.pr_decoration.binding.check_configuration.success').get() + ).toBeInTheDocument(); // Rerender and verify that validation is done for binding rerender( <MockedPRDecorationBinding component={mockComponent()} currentUser={mockCurrentUser()} /> ); - expect(await ui.validationSuccessMsg.find()).toHaveTextContent( - 'settings.pr_decoration.binding.check_configuration.success' - ); + expect( + await ui.validationMsg('settings.pr_decoration.binding.check_configuration.success').find() + ).toBeInTheDocument(); expect(ui.saveButton.query()).not.toBeInTheDocument(); // Reset binding @@ -172,8 +172,7 @@ function getPageObjects() { validateButton: byRole('button', { name: 'settings.pr_decoration.binding.check_configuration', }), - validationErrorMsg: byRole('alert'), - validationSuccessMsg: byRole('status'), + validationMsg: (text: string) => byText(text), setInput, }; diff --git a/server/sonar-web/src/main/js/apps/system/components/__tests__/SystemApp-it.tsx b/server/sonar-web/src/main/js/apps/system/components/__tests__/SystemApp-it.tsx index 295c2a65f58..79d23690ab7 100644 --- a/server/sonar-web/src/main/js/apps/system/components/__tests__/SystemApp-it.tsx +++ b/server/sonar-web/src/main/js/apps/system/components/__tests__/SystemApp-it.tsx @@ -20,7 +20,7 @@ import { screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { first } from 'lodash'; -import { byRole } from 'testing-library-selector'; +import { byRole, byText } from 'testing-library-selector'; import SystemServiceMock from '../../../../api/mocks/SystemServiceMock'; import { renderAppRoutes } from '../../../../helpers/testReactTestingUtils'; import routes from '../../routes'; @@ -59,12 +59,10 @@ describe('System Info Standalone', () => { await user.click(ui.changeLogLevelButton.get()); expect(ui.logLevelWarning.queryAll()).toHaveLength(0); await user.click(ui.logLevelsRadioButton(LogsLevels.DEBUG).get()); - expect(ui.logLevelWarning.get()).toHaveTextContent( - 'alert.tooltip.warningsystem.log_level.warning' - ); + expect(ui.logLevelWarning.get()).toBeInTheDocument(); await user.click(ui.saveButton.get()); - expect(ui.logLevelWarning.queryAll()).toHaveLength(2); + expect(ui.logLevelWarningShort.queryAll()).toHaveLength(2); }); it('can download logs & system info', async () => { @@ -100,7 +98,7 @@ describe('System Info Cluster', () => { ); // Renders health checks - expect(ui.healthCauseWarning.getAll()).toHaveLength(3); + expect(ui.healthCauseWarning.get()).toBeInTheDocument(); // Renders App node expect(first(ui.sectionButton('server1.example.com').getAll())).toBeInTheDocument(); @@ -125,8 +123,9 @@ function getPageObjects() { sectionButton: (name: string) => byRole('button', { name }), changeLogLevelButton: byRole('button', { name: 'system.logs_level.change' }), logLevelsRadioButton: (name: LogsLevels) => byRole('radio', { name }), - logLevelWarning: byRole('alert'), - healthCauseWarning: byRole('alert'), + logLevelWarning: byText('system.log_level.warning'), + logLevelWarningShort: byText('system.log_level.warning.short'), + healthCauseWarning: byText('Friendly warning'), saveButton: byRole('button', { name: 'save' }), }; diff --git a/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/__tests__/AzurePipelinesTutorial-it.tsx b/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/__tests__/AzurePipelinesTutorial-it.tsx index d101190470d..9781cccea86 100644 --- a/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/__tests__/AzurePipelinesTutorial-it.tsx +++ b/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/__tests__/AzurePipelinesTutorial-it.tsx @@ -24,7 +24,7 @@ import * as React from 'react'; import UserTokensMock from '../../../../api/mocks/UserTokensMock'; import { mockComponent } from '../../../../helpers/mocks/component'; import { mockLanguage, mockLoggedInUser } from '../../../../helpers/testMocks'; -import { renderApp, RenderContext } from '../../../../helpers/testReactTestingUtils'; +import { RenderContext, renderApp } from '../../../../helpers/testReactTestingUtils'; import { Permissions } from '../../../../types/permissions'; import { TokenType } from '../../../../types/token'; import { getCopyToClipboardValue } from '../../test-utils'; @@ -69,13 +69,13 @@ it('should render correctly and allow navigating between the different steps', a const modal = screen.getByRole('dialog'); await clickButton(user, 'onboarding.token.generate', modal); const lastToken = tokenMock.getLastToken(); - if (lastToken === undefined) { - throw new Error("Couldn't find the latest generated token."); - } - expect(lastToken.type).toBe(TokenType.Global); - expect(within(modal).getByRole('alert')).toHaveTextContent( - `users.tokens.new_token_created.${lastToken.token}` - ); + + expect(lastToken).toBeDefined(); + + expect(lastToken!.type).toBe(TokenType.Global); + expect( + within(modal).getByText(`users.tokens.new_token_created.${lastToken!.token}`) + ).toBeInTheDocument(); await clickButton(user, 'continue', modal); // Continue. diff --git a/server/sonar-web/src/main/js/components/tutorials/components/__tests__/EditTokenModal-test.tsx b/server/sonar-web/src/main/js/components/tutorials/components/__tests__/EditTokenModal-test.tsx index 478430b58d7..6e0ea60e41f 100644 --- a/server/sonar-web/src/main/js/components/tutorials/components/__tests__/EditTokenModal-test.tsx +++ b/server/sonar-web/src/main/js/components/tutorials/components/__tests__/EditTokenModal-test.tsx @@ -81,9 +81,7 @@ it('should behave correctly', async () => { expect(lastToken.type).toBe(TokenType.Project); expect(lastToken.expirationDate).toBe(computeTokenExpirationDate(365)); - expect(screen.getByRole('alert')).toHaveTextContent( - `users.tokens.new_token_created.${lastToken.token}` - ); + expect(screen.getByText(`users.tokens.new_token_created.${lastToken.token}`)).toBeInTheDocument(); expect(screen.getByRole('button', { name: 'copy_to_clipboard' })).toBeInTheDocument(); // Revoke token. @@ -104,9 +102,7 @@ it('should behave correctly', async () => { } expect(lastToken.type).toBe(TokenType.Project); expect(lastToken.expirationDate).toBe(computeTokenExpirationDate(365)); - expect(screen.getByRole('alert')).toHaveTextContent( - `users.tokens.new_token_created.${lastToken.token}` - ); + expect(screen.getByText(`users.tokens.new_token_created.${lastToken.token}`)).toBeInTheDocument(); }); it('should allow setting a preferred token type', async () => { diff --git a/server/sonar-web/src/main/js/components/ui/Alert.tsx b/server/sonar-web/src/main/js/components/ui/Alert.tsx index 98ac6328b6c..88f91e671fa 100644 --- a/server/sonar-web/src/main/js/components/ui/Alert.tsx +++ b/server/sonar-web/src/main/js/components/ui/Alert.tsx @@ -23,7 +23,6 @@ import classNames from 'classnames'; import * as React from 'react'; import { colors, sizes } from '../../app/theme'; import { translate } from '../../helpers/l10n'; -import { Dict } from '../../types/types'; import AlertErrorIcon from '../icons/AlertErrorIcon'; import AlertSuccessIcon from '../icons/AlertSuccessIcon'; import AlertWarnIcon from '../icons/AlertWarnIcon'; @@ -36,36 +35,12 @@ export type AlertVariant = 'error' | 'warning' | 'success' | 'info' | 'loading'; export interface AlertProps { display?: AlertDisplay; variant: AlertVariant; -} - -interface AlertVariantInformation { - icon: JSX.Element; - color: string; - borderColor: string; - backGroundColor: string; - role: string; + live?: boolean; } const DOUBLE = 2; const QUADRUPLE = 4; -const StyledAlertIcon = styled.div<{ isBanner: boolean; variantInfo: AlertVariantInformation }>` - flex: 0 0 auto; - display: flex; - justify-content: center; - align-items: center; - width: calc(${({ isBanner }) => (isBanner ? DOUBLE : QUADRUPLE)} * ${sizes.gridSize}); - border-right: ${({ isBanner }) => (!isBanner ? '1px solid' : 'none')}; - border-color: ${({ variantInfo }) => variantInfo.borderColor}; -`; - -const StyledAlertContent = styled.div` - flex: 1 1 auto; - overflow: auto; - text-align: left; - padding: ${sizes.gridSize} calc(2 * ${sizes.gridSize}); -`; - const alertInnerIsBannerMixin = () => css` min-width: ${sizes.minPageWidth}; max-width: ${sizes.maxPageWidth}; @@ -76,19 +51,19 @@ const alertInnerIsBannerMixin = () => css` box-sizing: border-box; `; -const StyledAlertInner = styled.div<{ isBanner: boolean }>` - display: flex; - align-items: stretch; - ${({ isBanner }) => (isBanner ? alertInnerIsBannerMixin : null)} -`; - -const StyledAlert = styled.div<{ isInline: boolean; variantInfo: AlertVariantInformation }>` +const StyledAlert = styled.div<{ + isInline: boolean; + color: string; + backGroundColor: string; + borderColor: string; + isBanner: boolean; +}>` border: 1px solid; border-radius: 2px; margin-bottom: ${sizes.gridSize}; - border-color: ${({ variantInfo }) => variantInfo.borderColor}; - background-color: ${({ variantInfo }) => variantInfo.backGroundColor}; - color: ${({ variantInfo }) => variantInfo.color}; + border-color: ${({ borderColor }) => borderColor}; + background-color: ${({ backGroundColor }) => backGroundColor}; + color: ${({ color }) => color}; display: ${({ isInline }) => (isInline ? 'inline-block' : 'block')}; :empty { @@ -104,10 +79,33 @@ const StyledAlert = styled.div<{ isInline: boolean; variantInfo: AlertVariantInf .button-link:hover { border-color: ${colors.darkBlue}; } + + & .alert-inner { + display: flex; + align-items: stretch; + ${({ isBanner }) => (isBanner ? alertInnerIsBannerMixin : null)} + } + + & .alert-icon { + flex: 0 0 auto; + display: flex; + justify-content: center; + align-items: center; + width: calc(${({ isBanner }) => (isBanner ? DOUBLE : QUADRUPLE)} * ${sizes.gridSize}); + border-right: ${({ isBanner }) => (!isBanner ? '1px solid' : 'none')}; + border-color: ${({ borderColor }) => borderColor}; + } + + & .alert-content { + flex: 1 1 auto; + overflow: auto; + text-align: left; + padding: ${sizes.gridSize} calc(2 * ${sizes.gridSize}); + } `; -function getAlertVariantInfo(variant: AlertVariant): AlertVariantInformation { - const variantList: Dict<AlertVariantInformation> = { +function getAlertVariantInfo(variant: AlertVariant) { + const variantList = { error: { icon: ( <AlertErrorIcon label={translate('alert.tooltip.error')} fill={colors.alertIconError} /> @@ -115,7 +113,6 @@ function getAlertVariantInfo(variant: AlertVariant): AlertVariantInformation { color: colors.alertTextError, borderColor: colors.alertBorderError, backGroundColor: colors.alertBackgroundError, - role: 'alert', }, warning: { icon: ( @@ -124,7 +121,6 @@ function getAlertVariantInfo(variant: AlertVariant): AlertVariantInformation { color: colors.alertTextWarning, borderColor: colors.alertBorderWarning, backGroundColor: colors.alertBackgroundWarning, - role: 'alert', }, success: { icon: ( @@ -136,29 +132,26 @@ function getAlertVariantInfo(variant: AlertVariant): AlertVariantInformation { color: colors.alertTextSuccess, borderColor: colors.alertBorderSuccess, backGroundColor: colors.alertBackgroundSuccess, - role: 'status', }, info: { icon: <InfoIcon label={translate('alert.tooltip.info')} fill={colors.alertIconInfo} />, color: colors.alertTextInfo, borderColor: colors.alertBorderInfo, backGroundColor: colors.alertBackgroundInfo, - role: 'status', }, loading: { icon: <DeferredSpinner timeout={0} />, color: colors.alertTextInfo, borderColor: colors.alertBorderInfo, backGroundColor: colors.alertBackgroundInfo, - role: 'status', }, - }; + } as const; return variantList[variant]; } export function Alert(props: AlertProps & React.HTMLAttributes<HTMLDivElement>) { - const { className, display, variant, ...domProps } = props; + const { className, display, variant, children, live, ...domProps } = props; const isInline = display === 'inline'; const isBanner = display === 'banner'; const variantInfo = getAlertVariantInfo(variant); @@ -166,17 +159,19 @@ export function Alert(props: AlertProps & React.HTMLAttributes<HTMLDivElement>) return ( <StyledAlert className={classNames('alert', className)} + isBanner={isBanner} isInline={isInline} - role={variantInfo.role} - variantInfo={variantInfo} + color={variantInfo.color} + borderColor={variantInfo.borderColor} + backGroundColor={variantInfo.backGroundColor} {...domProps} > - <StyledAlertInner isBanner={isBanner}> - <StyledAlertIcon isBanner={isBanner} variantInfo={variantInfo}> - {variantInfo.icon} - </StyledAlertIcon> - <StyledAlertContent className="alert-content">{props.children}</StyledAlertContent> - </StyledAlertInner> + {children && ( + <div className="alert-inner"> + <div className="alert-icon">{variantInfo.icon}</div> + <div className="alert-content">{children}</div> + </div> + )} </StyledAlert> ); } diff --git a/server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/Alert-test.tsx.snap b/server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/Alert-test.tsx.snap index 0d404bb9aa1..ed55cd5b070 100644 --- a/server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/Alert-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/Alert-test.tsx.snap @@ -25,7 +25,7 @@ exports[`should render banner alert with correct css 1`] = ` border-color: #236a97; } -.emotion-1 { +.emotion-0 .alert-inner { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -43,7 +43,7 @@ exports[`should render banner alert with correct css 1`] = ` box-sizing: border-box; } -.emotion-2 { +.emotion-0 .alert-icon { -webkit-flex: 0 0 auto; -ms-flex: 0 0 auto; flex: 0 0 auto; @@ -64,7 +64,7 @@ exports[`should render banner alert with correct css 1`] = ` border-color: #f4b1b0; } -.emotion-3 { +.emotion-0 .alert-content { -webkit-flex: 1 1 auto; -ms-flex: 1 1 auto; flex: 1 1 auto; @@ -75,14 +75,14 @@ exports[`should render banner alert with correct css 1`] = ` <div class="alert alert-test emotion-0" + color="#862422" id="error-message" - role="alert" > <div - class="emotion-1" + class="alert-inner" > <div - class="emotion-2" + class="alert-icon" > <svg height="16" @@ -103,7 +103,7 @@ exports[`should render banner alert with correct css 1`] = ` </svg> </div> <div - class="alert-content emotion-3" + class="alert-content" > This is an error! </div> @@ -113,115 +113,40 @@ exports[`should render banner alert with correct css 1`] = ` exports[`should render properly 1`] = ` <Styled(div) + backGroundColor="#f2dede" + borderColor="#f4b1b0" className="alert alert-test" + color="#862422" id="error-message" + isBanner={false} isInline={false} - role="alert" - variantInfo={ - { - "backGroundColor": "#f2dede", - "borderColor": "#f4b1b0", - "color": "#862422", - "icon": <AlertErrorIcon - fill="#a4030f" - label="alert.tooltip.error" - />, - "role": "alert", - } - } > - <Styled(div) - isBanner={false} + <div + className="alert-inner" > - <Styled(div) - isBanner={false} - variantInfo={ - { - "backGroundColor": "#f2dede", - "borderColor": "#f4b1b0", - "color": "#862422", - "icon": <AlertErrorIcon - fill="#a4030f" - label="alert.tooltip.error" - />, - "role": "alert", - } - } + <div + className="alert-icon" > <AlertErrorIcon fill="#a4030f" label="alert.tooltip.error" /> - </Styled(div)> - <Styled(div) + </div> + <div className="alert-content" > This is an error! - </Styled(div)> - </Styled(div)> + </div> + </div> </Styled(div)> `; -exports[`verification of all variants of alert 1`] = ` -{ - "backGroundColor": "#f2dede", - "borderColor": "#f4b1b0", - "color": "#862422", - "icon": <AlertErrorIcon - fill="#a4030f" - label="alert.tooltip.error" - />, - "role": "alert", -} -`; +exports[`verification of all variants of alert 1`] = `undefined`; -exports[`verification of all variants of alert 2`] = ` -{ - "backGroundColor": "#fcf8e3", - "borderColor": "#faebcc", - "color": "#6f4f17", - "icon": <AlertWarnIcon - fill="#db781a" - label="alert.tooltip.warning" - />, - "role": "alert", -} -`; +exports[`verification of all variants of alert 2`] = `undefined`; -exports[`verification of all variants of alert 3`] = ` -{ - "backGroundColor": "#dff0d8", - "borderColor": "#d6e9c6", - "color": "#215821", - "icon": <AlertSuccessIcon - fill="#6d9867" - label="alert.tooltip.success" - />, - "role": "status", -} -`; +exports[`verification of all variants of alert 3`] = `undefined`; -exports[`verification of all variants of alert 4`] = ` -{ - "backGroundColor": "#d9edf7", - "borderColor": "#b1dff3", - "color": "#0e516f", - "icon": <InfoIcon - fill="#0271b9" - label="alert.tooltip.info" - />, - "role": "status", -} -`; +exports[`verification of all variants of alert 4`] = `undefined`; -exports[`verification of all variants of alert 5`] = ` -{ - "backGroundColor": "#d9edf7", - "borderColor": "#b1dff3", - "color": "#0e516f", - "icon": <DeferredSpinner - timeout={0} - />, - "role": "status", -} -`; +exports[`verification of all variants of alert 5`] = `undefined`; |