Browse Source

SONAR-19380 Make alert component less noisy for screen reader

tags/10.1.0.73491
Mathieu Suen 1 year ago
parent
commit
6f888d6312
26 changed files with 222 additions and 280 deletions
  1. 52
    32
      server/sonar-web/src/main/js/app/components/GitHubSynchronisationWarning.tsx
  2. 7
    7
      server/sonar-web/src/main/js/app/components/SystemAnnouncement.tsx
  3. 1
    1
      server/sonar-web/src/main/js/app/components/__tests__/SystemAnnouncement-test.tsx
  4. 0
    4
      server/sonar-web/src/main/js/app/components/indexation/IndexationNotification.tsx
  5. 15
    11
      server/sonar-web/src/main/js/app/components/indexation/IndexationNotificationRenderer.tsx
  6. 8
    0
      server/sonar-web/src/main/js/app/components/indexation/__tests__/__snapshots__/IndexationNotificationRenderer-test.tsx.snap
  7. 1
    1
      server/sonar-web/src/main/js/app/components/nav/component/projectInformation/notifications/ProjectNotifications.tsx
  8. 1
    1
      server/sonar-web/src/main/js/apps/create/project/__tests__/Azure-it.tsx
  9. 1
    1
      server/sonar-web/src/main/js/apps/create/project/__tests__/Bitbucket-it.tsx
  10. 3
    3
      server/sonar-web/src/main/js/apps/create/project/__tests__/BitbucketCloud-it.tsx
  11. 4
    6
      server/sonar-web/src/main/js/apps/create/project/__tests__/GitLab-it.tsx
  12. 5
    7
      server/sonar-web/src/main/js/apps/overview/components/__tests__/App-test.tsx
  13. 3
    3
      server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/ProjectQualityGateApp-it.tsx
  14. 0
    1
      server/sonar-web/src/main/js/apps/sessions/components/__tests__/Login-it.tsx
  15. 1
    3
      server/sonar-web/src/main/js/apps/settings/components/__tests__/NewCodePeriod-it.tsx
  16. 3
    3
      server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/AlmBindingDefinitionForm-test.tsx
  17. 5
    6
      server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/AlmIntegration-it.tsx
  18. 8
    1
      server/sonar-web/src/main/js/apps/settings/components/authentication/GitHubConfigurationValidity.tsx
  19. 1
    1
      server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-it.tsx
  20. 13
    14
      server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/__tests__/PRDecorationBinding-it.tsx
  21. 7
    8
      server/sonar-web/src/main/js/apps/system/components/__tests__/SystemApp-it.tsx
  22. 8
    8
      server/sonar-web/src/main/js/components/tutorials/azure-pipelines/__tests__/AzurePipelinesTutorial-it.tsx
  23. 2
    6
      server/sonar-web/src/main/js/components/tutorials/components/__tests__/EditTokenModal-test.tsx
  24. 48
    53
      server/sonar-web/src/main/js/components/ui/Alert.tsx
  25. 24
    99
      server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/Alert-test.tsx.snap
  26. 1
    0
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 52
- 32
server/sonar-web/src/main/js/app/components/GitHubSynchronisationWarning.tsx View 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} />
</>
);
}

+ 7
- 7
server/sonar-web/src/main/js/app/components/SystemAnnouncement.tsx View File

@@ -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>
);

+ 1
- 1
server/sonar-web/src/main/js/app/components/__tests__/SystemAnnouncement-test.tsx View 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(),

+ 0
- 4
server/sonar-web/src/main/js/app/components/indexation/IndexationNotification.tsx View File

@@ -97,10 +97,6 @@ export class IndexationNotification extends React.PureComponent<Props, State> {
},
} = this.props;

if (notificationType === undefined) {
return null;
}

return (
<IndexationNotificationRenderer
type={notificationType}

+ 15
- 11
server/sonar-web/src/main/js/app/components/indexation/IndexationNotificationRenderer.tsx View 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>
);

+ 8
- 0
server/sonar-web/src/main/js/app/components/indexation/__tests__/__snapshots__/IndexationNotificationRenderer-test.tsx.snap View 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"

+ 1
- 1
server/sonar-web/src/main/js/app/components/nav/component/projectInformation/notifications/ProjectNotifications.tsx View 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>


+ 1
- 1
server/sonar-web/src/main/js/apps/create/project/__tests__/Azure-it.tsx View 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> = {}) {

+ 1
- 1
server/sonar-web/src/main/js/apps/create/project/__tests__/Bitbucket-it.tsx View 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> = {}) {

+ 3
- 3
server/sonar-web/src/main/js/apps/create/project/__tests__/BitbucketCloud-it.tsx View 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 () => {

+ 4
- 6
server/sonar-web/src/main/js/apps/create/project/__tests__/GitLab-it.tsx View 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> = {}) {

+ 5
- 7
server/sonar-web/src/main/js/apps/overview/components/__tests__/App-test.tsx View 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();
});


+ 3
- 3
server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/ProjectQualityGateApp-it.tsx View 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();
});


+ 0
- 1
server/sonar-web/src/main/js/apps/sessions/components/__tests__/Login-it.tsx View 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();
});


+ 1
- 3
server/sonar-web/src/main/js/apps/settings/components/__tests__/NewCodePeriod-it.tsx View 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');

+ 3
- 3
server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/AlmBindingDefinitionForm-test.tsx View 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();

+ 5
- 6
server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/AlmIntegration-it.tsx View 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) {

+ 8
- 1
server/sonar-web/src/main/js/apps/settings/components/authentication/GitHubConfigurationValidity.tsx View 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) => (

+ 1
- 1
server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-it.tsx View 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', {

+ 13
- 14
server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/__tests__/PRDecorationBinding-it.tsx View 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,
};


+ 7
- 8
server/sonar-web/src/main/js/apps/system/components/__tests__/SystemApp-it.tsx View 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' }),
};


+ 8
- 8
server/sonar-web/src/main/js/components/tutorials/azure-pipelines/__tests__/AzurePipelinesTutorial-it.tsx View 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.

+ 2
- 6
server/sonar-web/src/main/js/components/tutorials/components/__tests__/EditTokenModal-test.tsx View 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 () => {

+ 48
- 53
server/sonar-web/src/main/js/components/ui/Alert.tsx View 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>
);
}

+ 24
- 99
server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/Alert-test.tsx.snap View 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`;

+ 1
- 0
sonar-core/src/main/resources/org/sonar/l10n/core.properties View 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}

Loading…
Cancel
Save