瀏覽代碼

[NO JIRA] Improve background tasks and account RTL ITs

tags/9.8.0.63668
Mathieu Suen 1 年之前
父節點
當前提交
2d5f569fbd

+ 1
- 2
server/sonar-web/src/main/js/api/mocks/ComputeEngineServiceMock.ts 查看文件

@@ -19,6 +19,7 @@
*/
import { differenceInMilliseconds, isAfter, isBefore } from 'date-fns';
import { cloneDeep, groupBy, sortBy } from 'lodash';
import { PAGE_SIZE } from '../../apps/background-tasks/constants';
import { parseDate } from '../../helpers/dates';
import { mockTask } from '../../helpers/mocks/tasks';
import { ActivityRequestParameters, Task, TaskStatuses, TaskTypes } from '../../types/tasks';
@@ -35,8 +36,6 @@ import {
const RANDOM_RADIX = 36;
const RANDOM_PREFIX = 2;

const PAGE_SIZE = 100;

const TASK_TYPES = [
TaskTypes.Report,
TaskTypes.IssueSync,

+ 4
- 12
server/sonar-web/src/main/js/api/mocks/NotificationsMock.ts 查看文件

@@ -22,22 +22,14 @@ import { cloneDeep } from 'lodash';
import {
AddRemoveNotificationParameters,
Notification,
NotificationGlobalType,
NotificationProjectType,
NotificationsResponse,
} from '../../types/notifications';
import { addNotification, getNotifications, removeNotification } from '../notifications';

/* Constants */
const channels = ['EmailNotificationChannel'];
const globalTypes = ['CeReportTaskFailure', 'ChangesOnMyIssue', 'NewAlerts', 'SQ-MyNewIssues'];
const perProjectTypes = [
'CeReportTaskFailure',
'ChangesOnMyIssue',
'NewAlerts',
'NewFalsePositiveIssue',
'NewIssues',
'SQ-MyNewIssues',
];

const defaultNotifications: Notification[] = [
{ channel: 'EmailNotificationChannel', type: 'ChangesOnMyIssue' },
];
@@ -56,9 +48,9 @@ export default class NotificationsMock {
handleGetNotifications: () => Promise<NotificationsResponse> = () => {
return Promise.resolve({
channels: [...channels],
globalTypes: [...globalTypes],
globalTypes: Object.values(NotificationGlobalType),
notifications: cloneDeep(this.notifications),
perProjectTypes: [...perProjectTypes],
perProjectTypes: Object.values(NotificationProjectType),
});
};


+ 6
- 2
server/sonar-web/src/main/js/app/components/nav/component/projectInformation/notifications/__tests__/ProjectNotifications-test.tsx 查看文件

@@ -20,6 +20,10 @@
import { shallow } from 'enzyme';
import * as React from 'react';
import { mockComponent } from '../../../../../../../helpers/mocks/component';
import {
NotificationGlobalType,
NotificationProjectType,
} from '../../../../../../../types/notifications';
import { ProjectNotifications } from '../ProjectNotifications';

jest.mock('react', () => {
@@ -65,7 +69,7 @@ function shallowRender(props = {}) {
addNotification={jest.fn()}
channels={['channel1', 'channel2']}
component={mockComponent({ key: 'foo' })}
globalTypes={['type-global', 'type-common']}
globalTypes={[NotificationGlobalType.CeReportTaskFailure, NotificationGlobalType.NewAlerts]}
loading={false}
notifications={[
{
@@ -87,7 +91,7 @@ function shallowRender(props = {}) {
projectName: 'Qux',
},
]}
perProjectTypes={['type-common']}
perProjectTypes={[NotificationProjectType.CeReportTaskFailure]}
removeNotification={jest.fn()}
{...props}
/>

+ 1
- 1
server/sonar-web/src/main/js/app/components/nav/component/projectInformation/notifications/__tests__/__snapshots__/ProjectNotifications-test.tsx.snap 查看文件

@@ -66,7 +66,7 @@ exports[`should render correctly 1`] = `
project={true}
types={
Array [
"type-common",
"CeReportTaskFailure",
]
}
/>

+ 45
- 55
server/sonar-web/src/main/js/apps/account/__tests__/Account-it.tsx 查看文件

@@ -17,10 +17,11 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { screen, waitFor, within } from '@testing-library/react';
import { screen, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { UserEvent } from '@testing-library/user-event/dist/types/setup';
import selectEvent from 'react-select-event';
import { byLabelText, byRole, byText } from 'testing-library-selector';
import { getMyProjects, getScannableProjects } from '../../../api/components';
import NotificationsMock from '../../../api/mocks/NotificationsMock';
import UserTokensMock from '../../../api/mocks/UserTokensMock';
@@ -28,6 +29,7 @@ import { mockUserToken } from '../../../helpers/mocks/token';
import { setKeyboardShortcutEnabled } from '../../../helpers/preferences';
import { mockCurrentUser, mockLoggedInUser } from '../../../helpers/testMocks';
import { renderAppRoutes } from '../../../helpers/testReactTestingUtils';
import { NotificationGlobalType, NotificationProjectType } from '../../../types/notifications';
import { Permissions } from '../../../types/permissions';
import { TokenType } from '../../../types/token';
import { CurrentUser } from '../../../types/users';
@@ -463,6 +465,27 @@ describe('security page', () => {
});

describe('notifications page', () => {
const projectUI = {
title: byRole('button', { name: 'my_profile.per_project_notifications.add' }),
addButton: byRole('button', { name: 'my_profile.per_project_notifications.add' }),
addModalButton: byRole('button', { name: 'add_verb' }),
searchInput: byLabelText('search_verb', { selector: 'input' }),
sonarQubeProject: byRole('heading', { name: 'SonarQube' }),
checkbox: (type: NotificationProjectType) =>
byRole('checkbox', {
name: `notification.dispatcher.descrption_x.notification.dispatcher.${type}.project`,
}),
};

const globalUI = {
title: byRole('heading', { name: 'my_profile.overall_notifications.title' }),
noNotificationForProject: byText('my_account.no_project_notifications'),
checkbox: (type: NotificationGlobalType) =>
byRole('checkbox', {
name: `notification.dispatcher.descrption_x.notification.dispatcher.${type}`,
}),
};

let notificationsMock: NotificationsMock;
beforeAll(() => {
notificationsMock = new NotificationsMock();
@@ -475,43 +498,28 @@ describe('notifications page', () => {
const notificationsPagePath = 'account/notifications';

it('should display global notifications status and allow edits', async () => {
const user = userEvent.setup();
const user = userEvent.setup({ delay: null });

renderAccountApp(mockLoggedInUser(), notificationsPagePath);

expect(
await screen.findByRole('heading', { name: 'my_profile.overall_notifications.title' })
).toBeInTheDocument();

expect(screen.getAllByRole('row')).toHaveLength(5); // 4 + header
expect(await globalUI.title.find()).toBeInTheDocument();

/*
* Verify Checkbox statuses
*/
expect(getCheckboxByRowName('notification.dispatcher.ChangesOnMyIssue')).toBeChecked();

// first row is the header: skip it!
const otherRows = screen
.getAllByRole('row', {
name: (n: string) => n !== 'notification.dispatcher.ChangesOnMyIssue',
})
.slice(1);

otherRows.forEach((row) => {
expect(within(row).getByRole('checkbox')).not.toBeChecked();
});

// Make sure the second block is empty
expect(screen.getByText('my_account.no_project_notifications')).toBeInTheDocument();
expect(globalUI.checkbox(NotificationGlobalType.ChangesOnMyIssue).get()).toBeChecked();
expect(globalUI.checkbox(NotificationGlobalType.CeReportTaskFailure).get()).not.toBeChecked();
expect(globalUI.checkbox(NotificationGlobalType.NewAlerts).get()).not.toBeChecked();
expect(globalUI.checkbox(NotificationGlobalType.MyNewIssues).get()).not.toBeChecked();

/*
* Update notifications
*/
await user.click(getCheckboxByRowName('notification.dispatcher.ChangesOnMyIssue'));
expect(getCheckboxByRowName('notification.dispatcher.ChangesOnMyIssue')).not.toBeChecked();
await user.click(globalUI.checkbox(NotificationGlobalType.ChangesOnMyIssue).get());
expect(globalUI.checkbox(NotificationGlobalType.ChangesOnMyIssue).get()).not.toBeChecked();

await user.click(getCheckboxByRowName('notification.dispatcher.NewAlerts'));
expect(getCheckboxByRowName('notification.dispatcher.NewAlerts')).toBeChecked();
await user.click(globalUI.checkbox(NotificationGlobalType.NewAlerts).get());
expect(globalUI.checkbox(NotificationGlobalType.NewAlerts).get()).toBeChecked();
});

it('should allow adding notifications for a project', async () => {
@@ -519,35 +527,23 @@ describe('notifications page', () => {

renderAccountApp(mockLoggedInUser(), notificationsPagePath);

await user.click(
await screen.findByRole('button', { name: 'my_profile.per_project_notifications.add' })
);

expect(await screen.findByLabelText('search_verb', { selector: 'input' })).toBeInTheDocument();

expect(screen.getByRole('button', { name: 'add_verb' })).toBeDisabled();

await user.keyboard('sonar');
await user.click(await projectUI.addButton.find());
expect(projectUI.addModalButton.get()).toBeDisabled();
await user.type(projectUI.searchInput.get(), 'sonar');
// navigate within the two results, choose the first:
await user.keyboard('[ArrowDown][ArrowDown][ArrowUp][Enter]');
await user.click(projectUI.addModalButton.get());

const addButton = screen.getByRole('button', { name: 'add_verb' });
expect(addButton).toBeEnabled();

await user.click(addButton);

expect(screen.getByRole('heading', { name: 'SonarQube' })).toBeInTheDocument();
expect(projectUI.sonarQubeProject.get()).toBeInTheDocument();
expect(
getCheckboxByRowName('notification.dispatcher.NewFalsePositiveIssue.project')
projectUI.checkbox(NotificationProjectType.NewFalsePositiveIssue).get()
).toBeInTheDocument();

await user.click(getCheckboxByRowName('notification.dispatcher.NewAlerts.project'));
expect(getCheckboxByRowName('notification.dispatcher.NewAlerts.project')).toBeChecked();

expect(screen.getAllByRole('checkbox', { checked: true })).toHaveLength(2);
await user.click(projectUI.checkbox(NotificationProjectType.NewAlerts).get());
expect(projectUI.checkbox(NotificationProjectType.NewAlerts).get()).toBeChecked();

await user.click(getCheckboxByRowName('notification.dispatcher.NewAlerts.project'));
expect(getCheckboxByRowName('notification.dispatcher.NewAlerts.project')).not.toBeChecked();
await user.click(projectUI.checkbox(NotificationProjectType.NewAlerts).get());
expect(projectUI.checkbox(NotificationProjectType.NewAlerts).get()).not.toBeChecked();
});

it('should allow searching for projects', async () => {
@@ -571,9 +567,7 @@ describe('notifications page', () => {
await user.click(screen.getByRole('searchbox'));
await user.keyboard('bla');

await waitFor(() => {
expect(screen.queryByRole('heading', { name: 'SonarQube' })).not.toBeInTheDocument();
});
expect(screen.queryByRole('heading', { name: 'SonarQube' })).not.toBeInTheDocument();

await user.keyboard('[Backspace>3/]');

@@ -667,10 +661,6 @@ function getProjectBlock(projectName: string) {
return result;
}

function getCheckboxByRowName(name: string) {
return within(screen.getByRole('row', { name })).getByRole('checkbox');
}

function renderAccountApp(currentUser: CurrentUser, navigateTo?: string) {
renderAppRoutes('account', routes, { currentUser, navigateTo });
}

+ 2
- 2
server/sonar-web/src/main/js/apps/account/notifications/GlobalNotifications.tsx 查看文件

@@ -19,7 +19,7 @@
*/
import * as React from 'react';
import { translate } from '../../../helpers/l10n';
import { Notification } from '../../../types/notifications';
import { Notification, NotificationGlobalType } from '../../../types/notifications';
import NotificationsList from './NotificationsList';

interface Props {
@@ -27,7 +27,7 @@ interface Props {
channels: string[];
notifications: Notification[];
removeNotification: (n: Notification) => void;
types: string[];
types: NotificationGlobalType[];
}

export default function GlobalNotifications(props: Props) {

+ 11
- 3
server/sonar-web/src/main/js/apps/account/notifications/NotificationsList.tsx 查看文件

@@ -19,8 +19,12 @@
*/
import * as React from 'react';
import Checkbox from '../../../components/controls/Checkbox';
import { hasMessage, translate } from '../../../helpers/l10n';
import { Notification } from '../../../types/notifications';
import { hasMessage, translate, translateWithParameters } from '../../../helpers/l10n';
import {
Notification,
NotificationGlobalType,
NotificationProjectType,
} from '../../../types/notifications';

interface Props {
onAdd: (n: Notification) => void;
@@ -28,7 +32,7 @@ interface Props {
channels: string[];
checkboxId: (type: string, channel: string) => string;
project?: boolean;
types: string[];
types: (NotificationGlobalType | NotificationProjectType)[];
notifications: Notification[];
}

@@ -67,6 +71,10 @@ export default class NotificationsList extends React.PureComponent<Props> {
{channels.map((channel) => (
<td className="text-center" key={channel}>
<Checkbox
label={translateWithParameters(
'notification.dispatcher.descrption_x',
this.getDispatcherLabel(type)
)}
checked={this.isEnabled(type, channel)}
id={checkboxId(type, channel)}
onCheck={(checked) => this.handleCheck(type, channel, checked)}

+ 6
- 2
server/sonar-web/src/main/js/apps/account/notifications/ProjectNotifications.tsx 查看文件

@@ -20,7 +20,11 @@
import * as React from 'react';
import BoxedGroupAccordion from '../../../components/controls/BoxedGroupAccordion';
import { translate } from '../../../helpers/l10n';
import { Notification, NotificationProject } from '../../../types/notifications';
import {
Notification,
NotificationProject,
NotificationProjectType,
} from '../../../types/notifications';
import NotificationsList from './NotificationsList';

interface Props {
@@ -30,7 +34,7 @@ interface Props {
notifications: Notification[];
project: NotificationProject;
removeNotification: (n: Notification) => void;
types: string[];
types: NotificationProjectType[];
}

export default function ProjectNotifications(props: Props) {

+ 6
- 2
server/sonar-web/src/main/js/apps/account/notifications/Projects.tsx 查看文件

@@ -22,7 +22,11 @@ import * as React from 'react';
import { Button } from '../../../components/controls/buttons';
import SearchBox from '../../../components/controls/SearchBox';
import { translate } from '../../../helpers/l10n';
import { Notification, NotificationProject } from '../../../types/notifications';
import {
Notification,
NotificationProject,
NotificationProjectType,
} from '../../../types/notifications';
import ProjectModal from './ProjectModal';
import ProjectNotifications from './ProjectNotifications';

@@ -31,7 +35,7 @@ export interface Props {
channels: string[];
notifications: Notification[];
removeNotification: (n: Notification) => void;
types: string[];
types: NotificationProjectType[];
}

const THRESHOLD_COLLAPSED = 3;

+ 9
- 3
server/sonar-web/src/main/js/apps/background-tasks/__tests__/BackgroundTasks-it.tsx 查看文件

@@ -28,6 +28,12 @@ import routes from '../routes';

jest.mock('../../../api/ce');

jest.mock('../constants', () => {
const contants = jest.requireActual('../constants');

return { ...contants, PAGE_SIZE: 9 };
});

let computeEngineServiceMock: ComputeEngineServiceMock;

beforeAll(() => {
@@ -139,7 +145,7 @@ describe('The Global background task page', () => {
const user = userEvent.setup();

computeEngineServiceMock.clearTasks();
computeEngineServiceMock.createTasks(101);
computeEngineServiceMock.createTasks(10);

renderGlobalBackgroundTasksApp();

@@ -147,12 +153,12 @@ describe('The Global background task page', () => {
await screen.findByRole('heading', { name: 'background_tasks.page' })
).toBeInTheDocument();

expect(screen.getAllByRole('row')).toHaveLength(101); // including header
expect(screen.getAllByRole('row')).toHaveLength(10); // including header

user.click(screen.getByRole('button', { name: 'show_more' }));

await waitFor(() => {
expect(screen.getAllByRole('row')).toHaveLength(102); // including header
expect(screen.getAllByRole('row')).toHaveLength(11); // including header
});
});


+ 1
- 3
server/sonar-web/src/main/js/apps/background-tasks/components/BackgroundTasksApp.tsx 查看文件

@@ -37,7 +37,7 @@ import { parseAsDate } from '../../../helpers/query';
import { Task, TaskStatuses } from '../../../types/tasks';
import { Component, Paging, RawQuery } from '../../../types/types';
import '../background-tasks.css';
import { CURRENTS, DEBOUNCE_DELAY, DEFAULT_FILTERS } from '../constants';
import { CURRENTS, DEBOUNCE_DELAY, DEFAULT_FILTERS, PAGE_SIZE } from '../constants';
import { mapFiltersToParameters, Query, updateTask } from '../utils';
import Header from './Header';
import Search from './Search';
@@ -60,8 +60,6 @@ interface State {
types?: string[];
}

const PAGE_SIZE = 100;

export class BackgroundTasksApp extends React.PureComponent<Props, State> {
loadTasksDebounced: () => void;
mounted = false;

+ 2
- 0
server/sonar-web/src/main/js/apps/background-tasks/constants.ts 查看文件

@@ -49,3 +49,5 @@ export const DEFAULT_FILTERS: Query = {
export const DATE_FORMAT = 'YYYY-MM-DD';

export const DEBOUNCE_DELAY = 250;

export const PAGE_SIZE = 100;

+ 9
- 5
server/sonar-web/src/main/js/components/hoc/withNotifications.tsx 查看文件

@@ -20,24 +20,28 @@
import { uniqWith } from 'lodash';
import * as React from 'react';
import { addNotification, getNotifications, removeNotification } from '../../api/notifications';
import { Notification } from '../../types/notifications';
import {
Notification,
NotificationGlobalType,
NotificationProjectType,
} from '../../types/notifications';
import { getWrappedDisplayName } from './utils';

interface State {
channels: string[];
globalTypes: string[];
globalTypes: NotificationGlobalType[];
loading: boolean;
notifications: Notification[];
perProjectTypes: string[];
perProjectTypes: NotificationProjectType[];
}

export interface WithNotificationsProps {
addNotification: (added: Notification) => void;
channels: string[];
globalTypes: string[];
globalTypes: NotificationGlobalType[];
loading: boolean;
notifications: Notification[];
perProjectTypes: string[];
perProjectTypes: NotificationProjectType[];
removeNotification: (removed: Notification) => void;
}


+ 19
- 2
server/sonar-web/src/main/js/types/notifications.ts 查看文件

@@ -17,6 +17,23 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/

export enum NotificationGlobalType {
CeReportTaskFailure = 'CeReportTaskFailure',
ChangesOnMyIssue = 'ChangesOnMyIssue',
NewAlerts = 'NewAlerts',
MyNewIssues = 'SQ-MyNewIssues',
}

export enum NotificationProjectType {
CeReportTaskFailure = 'CeReportTaskFailure',
ChangesOnMyIssue = 'ChangesOnMyIssue',
NewAlerts = 'NewAlerts',
NewFalsePositiveIssue = 'NewFalsePositiveIssue',
NewIssues = 'NewIssues',
MyNewIssues = 'SQ-MyNewIssues',
}

export interface Notification {
channel: string;
project?: string;
@@ -31,9 +48,9 @@ export interface NotificationProject {

export interface NotificationsResponse {
channels: string[];
globalTypes: string[];
globalTypes: NotificationGlobalType[];
notifications: Notification[];
perProjectTypes: string[];
perProjectTypes: NotificationProjectType[];
}

export interface AddRemoveNotificationParameters {

+ 1
- 1
sonar-core/src/main/resources/org/sonar/l10n/core.properties 查看文件

@@ -2043,7 +2043,7 @@ notification.dispatcher.NewFalsePositiveIssue=Issues resolved as false positive
notification.dispatcher.SQ-MyNewIssues=My new issues
notification.dispatcher.CeReportTaskFailure=Background tasks in failure on my administered projects
notification.dispatcher.CeReportTaskFailure.project=Background tasks in failure
notification.dispatcher.descrption_x=Check to receive notification for {0}

#------------------------------------------------------------------------------
#

Loading…
取消
儲存