]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-19380 Make alert component less noisy for screen reader
authorMathieu Suen <mathieu.suen@sonarsource.com>
Wed, 7 Jun 2023 08:47:43 +0000 (10:47 +0200)
committersonartech <sonartech@sonarsource.com>
Thu, 8 Jun 2023 20:03:08 +0000 (20:03 +0000)
26 files changed:
server/sonar-web/src/main/js/app/components/GitHubSynchronisationWarning.tsx
server/sonar-web/src/main/js/app/components/SystemAnnouncement.tsx
server/sonar-web/src/main/js/app/components/__tests__/SystemAnnouncement-test.tsx
server/sonar-web/src/main/js/app/components/indexation/IndexationNotification.tsx
server/sonar-web/src/main/js/app/components/indexation/IndexationNotificationRenderer.tsx
server/sonar-web/src/main/js/app/components/indexation/__tests__/__snapshots__/IndexationNotificationRenderer-test.tsx.snap
server/sonar-web/src/main/js/app/components/nav/component/projectInformation/notifications/ProjectNotifications.tsx
server/sonar-web/src/main/js/apps/create/project/__tests__/Azure-it.tsx
server/sonar-web/src/main/js/apps/create/project/__tests__/Bitbucket-it.tsx
server/sonar-web/src/main/js/apps/create/project/__tests__/BitbucketCloud-it.tsx
server/sonar-web/src/main/js/apps/create/project/__tests__/GitLab-it.tsx
server/sonar-web/src/main/js/apps/overview/components/__tests__/App-test.tsx
server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/ProjectQualityGateApp-it.tsx
server/sonar-web/src/main/js/apps/sessions/components/__tests__/Login-it.tsx
server/sonar-web/src/main/js/apps/settings/components/__tests__/NewCodePeriod-it.tsx
server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/AlmBindingDefinitionForm-test.tsx
server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/AlmIntegration-it.tsx
server/sonar-web/src/main/js/apps/settings/components/authentication/GitHubConfigurationValidity.tsx
server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-it.tsx
server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/__tests__/PRDecorationBinding-it.tsx
server/sonar-web/src/main/js/apps/system/components/__tests__/SystemApp-it.tsx
server/sonar-web/src/main/js/components/tutorials/azure-pipelines/__tests__/AzurePipelinesTutorial-it.tsx
server/sonar-web/src/main/js/components/tutorials/components/__tests__/EditTokenModal-test.tsx
server/sonar-web/src/main/js/components/ui/Alert.tsx
server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/Alert-test.tsx.snap
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 5d7cb2544c748343921e77df2c9ebc6fd14e6d03..b9ee787e9ba0e4244571dee05f4f3fe8fa37dc70 100644 (file)
@@ -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} />
     </>
   );
 }
index a35020a1aaf141beedf74afabb6280b3f4ab5729..e954c7769c615c7455f01b41ab01b8faa85127b9 100644 (file)
  * 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>
     );
index 7bf9b99a359d7851347eb2de4c4fab630f53a5df..686e7c50a7c25e4740add24dafaf2580c4713a78 100644 (file)
@@ -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(),
index 4a41b5d0e0ae543b2dc4d828309626415036a90c..d1d96f6f0812b53dd8df80f93217eeb238f7672b 100644 (file)
@@ -97,10 +97,6 @@ export class IndexationNotification extends React.PureComponent<Props, State> {
       },
     } = this.props;
 
-    if (notificationType === undefined) {
-      return null;
-    }
-
     return (
       <IndexationNotificationRenderer
         type={notificationType}
index 07d5839c7f5329af1803ede5ba28609749d1765b..defc97b75270b5cca1739c620a6addd00123f91e 100644 (file)
@@ -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>
   );
index ffa9743fd65f909b82c874f5001ecf39dde81990..edcf985e28b89e9e01ed79ec5f33dc5a7ff0b46e 100644 (file)
@@ -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"
index 8e666804de8ce7fc0b160ecb3f6717324561ceaa..b8d254a6dbc203cbd6ab8a2bee6475c47cb57847 100644 (file)
@@ -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>
 
index a3e4a6b838f4078e071bf2a1862742877fa73169..8ddbbacb83327aaf5bf2c9325e69af0ddde8d8c5 100644 (file)
@@ -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> = {}) {
index 6c66849c475834b1b59e2d8c2b13e84110d941c2..0112268c36fda5cd70f16f2e1500295c206f6c88 100644 (file)
@@ -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> = {}) {
index c301a25e9df9226ba41251230c0186c19ec9bdd8..ee4792f5d1999c68348240aee2349c4cc4c59e20 100644 (file)
@@ -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 () => {
index 0e71dbb86ff4176151cb36a266d136598e879576..aa090fb0c25b24a6d0ed4532af0c00f5c9764a29 100644 (file)
@@ -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> = {}) {
index bb669487c1c9bfd4454398b53bf268567255aab6..22395f7a1138c2a0f22df91f1bd59872fac76898 100644 (file)
@@ -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();
 });
 
index 72f27addb6537944a2044044e5f8b56c1165d3aa..2865c4b984af66ae2bd4adc2af4d53d911fca4cc 100644 (file)
@@ -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();
 });
 
index 7439fe27fc0985075f245fa5b8d750384ceafd41..69a00857dbddee4c415756c59139026f581997d7 100644 (file)
@@ -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();
 });
 
index 2d8d3f4035c707389ede697b56be55d0565ee8cc..640829ede7079b9a35cbd8ff5523548c93b9388d 100644 (file)
@@ -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');
index e46b6a0fb150a7fe7fcbcad084200a191ee53727..bf4bce05fa0029c1f2c7a26643e906ca2e783297 100644 (file)
@@ -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();
index 1cb65f52ee6b7b806692b0a45a9eca692aac8137..5d7308fa44372bae0690435e6abad177b44e2bec 100644 (file)
@@ -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) {
index efaea7d8616b655bca4f8d7e2773828786f738ca..4f3d766774dee35408d00bb21207028aab72be71 100644 (file)
@@ -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) => (
index 01996b13ff156a5cb75b9d34936e440cf774cbaa..7e6e00010bdf24af72a816cd80b40d5c87260f1a 100644 (file)
@@ -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', {
index 6120dab20e11c214c5471b00f85dbf91a86b3e30..76feb1ec980d75982d0c69b10f637d45fca25082 100644 (file)
@@ -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,
   };
 
index 295c2a65f586cee92f526a06203b364d5c7495c5..79d23690ab770acce753401ee0bd8cd0244deb0a 100644 (file)
@@ -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' }),
   };
 
index d101190470d7adb9c019e6331a4aad6d41c9f03a..9781cccea86df74a3d4bd60136c929829af5ec06 100644 (file)
@@ -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.
index 478430b58d73bbc1de841dc226263356606aa89b..6e0ea60e41f1ef220957d7d964e0950b6c54be39 100644 (file)
@@ -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 () => {
index 98ac6328b6c93a219b73f6a7feaa5c1f8c5ed58f..88f91e671fa648948bf0bd8d2fb3674555774d79 100644 (file)
@@ -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>
   );
 }
index 0d404bb9aa13efc50fdb17a608391e38f75d1ad8..ed55cd5b070345631f3e5cd1d9ad48ea45a3f1ca 100644 (file)
@@ -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`;
index 3e125df757d54c5be6f7423b39d1dd780516428e..4bdb1a754098fc6d2f317870e3d882eeccd8618a 100644 (file)
@@ -1391,6 +1391,7 @@ settings.authentication.github.form.provisioning.disabled=Your current edition d
 settings.authentication.github.synchronize_now=Synchronize now
 settings.authentication.github.synchronization_in_progress=Synchronization is in progress.
 settings.authentication.github.synchronization_pending=Synchronization is scheduled.
+settings.authentication.github.synchronization_finish=Synchronization is done.
 settings.authentication.github.synchronization_successful=Last synchronization was done {0} ago.
 settings.authentication.github.synchronization_failed=Last synchronization failed {0} ago.
 settings.authentication.github.synchronization_failed_short=Last synchronization failed. {details}