@@ -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, |
@@ -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), | |||
}); | |||
}; | |||
@@ -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} | |||
/> |
@@ -66,7 +66,7 @@ exports[`should render correctly 1`] = ` | |||
project={true} | |||
types={ | |||
Array [ | |||
"type-common", | |||
"CeReportTaskFailure", | |||
] | |||
} | |||
/> |
@@ -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 }); | |||
} |
@@ -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) { |
@@ -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)} |
@@ -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) { |
@@ -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; |
@@ -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 | |||
}); | |||
}); | |||
@@ -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; |
@@ -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; |
@@ -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; | |||
} | |||
@@ -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 { |
@@ -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} | |||
#------------------------------------------------------------------------------ | |||
# |