Merge pull request #8633 from marshmalien/refactor-project-functional

Fix notification list toolbar filter keys and convert Project/* to functional components

Reviewed-by: https://github.com/apps/softwarefactory-project-zuul
This commit is contained in:
softwarefactory-project-zuul[bot] 2020-12-01 13:52:22 +00:00 committed by GitHub
commit 7ca35634a7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 323 additions and 360 deletions

View File

@ -182,9 +182,13 @@ function NotificationList({
key: 'name__icontains',
isDefault: true,
},
{
name: i18n._(t`Description`),
key: 'description__icontains',
},
{
name: i18n._(t`Type`),
key: 'or__type',
key: 'or__notification_type',
options: [
['email', i18n._(t`Email`)],
['grafana', i18n._(t`Grafana`)],
@ -212,6 +216,10 @@ function NotificationList({
name: i18n._(t`Name`),
key: 'name',
},
{
name: i18n._(t`Type`),
key: 'notification_type',
},
]}
toolbarSearchableKeys={searchableKeys}
toolbarRelatedSearchableKeys={relatedSearchableKeys}

View File

@ -160,7 +160,7 @@ function Search({
const searchOptions = columns
.filter(({ key }) => key !== searchKey)
.map(({ key, name }) => (
<SelectOption key={key} value={name}>
<SelectOption key={key} value={name} id={`select-option-${key}`}>
{name}
</SelectOption>
));
@ -177,6 +177,7 @@ function Search({
onSelect={handleDropdownSelect}
selections={searchColumnName}
isOpen={isSearchDropdownOpen}
ouiaId="simple-key-select"
>
{searchOptions}
</Select>
@ -217,9 +218,14 @@ function Search({
})}
isOpen={isFilterDropdownOpen}
placeholderText={`Filter By ${name}`}
ouiaId={`filter-by-${key}`}
>
{options.map(([optionKey, optionLabel]) => (
<SelectOption key={optionKey} value={optionKey}>
<SelectOption
key={optionKey}
value={optionKey}
inputId={`select-option-${optionKey}`}
>
{optionLabel}
</SelectOption>
))}
@ -234,6 +240,7 @@ function Search({
selections={chipsByKey[key].chips[0]?.label}
isOpen={isFilterDropdownOpen}
placeholderText={`Filter By ${name}`}
ouiaId={`filter-by-${key}`}
>
<SelectOption key="true" value="true">
{booleanLabels.true || i18n._(t`Yes`)}

View File

@ -110,7 +110,27 @@ function NotificationTemplatesList({ i18n }) {
},
{
name: i18n._(t`Type`),
key: 'notification_type',
key: 'or__notification_type',
options: [
['email', i18n._(t`Email`)],
['grafana', i18n._(t`Grafana`)],
['hipchat', i18n._(t`Hipchat`)],
['irc', i18n._(t`IRC`)],
['mattermost', i18n._(t`Mattermost`)],
['pagerduty', i18n._(t`Pagerduty`)],
['rocketchat', i18n._(t`Rocket.Chat`)],
['slack', i18n._(t`Slack`)],
['twilio', i18n._(t`Twilio`)],
['webhook', i18n._(t`Webhook`)],
],
},
{
name: i18n._(t`Created By (Username)`),
key: 'created_by__username__icontains',
},
{
name: i18n._(t`Modified By (Username)`),
key: 'modified_by__username__icontains',
},
]}
toolbarSortColumns={[

View File

@ -1,11 +1,22 @@
import React, { Component } from 'react';
import React, { useCallback, useEffect } from 'react';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import { Switch, Route, withRouter, Redirect, Link } from 'react-router-dom';
import {
Switch,
Route,
withRouter,
Redirect,
Link,
useParams,
useLocation,
} from 'react-router-dom';
import { CaretLeftIcon } from '@patternfly/react-icons';
import { Card, PageSection } from '@patternfly/react-core';
import { useConfig } from '../../contexts/Config';
import useRequest from '../../util/useRequest';
import RoutedTabs from '../../components/RoutedTabs';
import ContentError from '../../components/ContentError';
import ContentLoading from '../../components/ContentLoading';
import NotificationList from '../../components/NotificationList';
import { ResourceAccessList } from '../../components/ResourceAccessList';
import { Schedules } from '../../components/Schedule';
@ -14,48 +25,18 @@ import ProjectEdit from './ProjectEdit';
import ProjectJobTemplatesList from './ProjectJobTemplatesList';
import { OrganizationsAPI, ProjectsAPI } from '../../api';
class Project extends Component {
constructor(props) {
super(props);
function Project({ i18n, setBreadcrumb }) {
const { me = {} } = useConfig();
const { id } = useParams();
const location = useLocation();
this.state = {
project: null,
hasContentLoading: true,
contentError: null,
isInitialized: false,
isNotifAdmin: false,
};
this.createSchedule = this.createSchedule.bind(this);
this.loadProject = this.loadProject.bind(this);
this.loadProjectAndRoles = this.loadProjectAndRoles.bind(this);
this.loadSchedules = this.loadSchedules.bind(this);
this.loadScheduleOptions = this.loadScheduleOptions.bind(this);
}
async componentDidMount() {
await this.loadProjectAndRoles();
this.setState({ isInitialized: true });
}
async componentDidUpdate(prevProps) {
const { location, match } = this.props;
const url = `/projects/${match.params.id}/`;
if (
prevProps.location.pathname.startsWith(url) &&
prevProps.location !== location &&
location.pathname === `${url}details`
) {
await this.loadProject();
}
}
async loadProjectAndRoles() {
const { match, setBreadcrumb } = this.props;
const id = parseInt(match.params.id, 10);
this.setState({ contentError: null, hasContentLoading: true });
try {
const {
request: fetchProjectAndRoles,
result: { project, isNotifAdmin },
isLoading: hasContentLoading,
error: contentError,
} = useRequest(
useCallback(async () => {
const [{ data }, notifAdminRes] = await Promise.all([
ProjectsAPI.readDetail(id),
OrganizationsAPI.read({
@ -63,188 +44,155 @@ class Project extends Component {
role_level: 'notification_admin_role',
}),
]);
setBreadcrumb(data);
this.setState({
return {
project: data,
isNotifAdmin: notifAdminRes.data.results.length > 0,
});
} catch (err) {
this.setState({ contentError: err });
} finally {
this.setState({ hasContentLoading: false });
};
}, [id]),
{
project: null,
notifAdminRes: null,
}
}
);
async loadProject() {
const { match, setBreadcrumb } = this.props;
const id = parseInt(match.params.id, 10);
useEffect(() => {
fetchProjectAndRoles();
}, [fetchProjectAndRoles, location.pathname]);
this.setState({ contentError: null, hasContentLoading: true });
try {
const { data } = await ProjectsAPI.readDetail(id);
setBreadcrumb(data);
this.setState({ project: data });
} catch (err) {
this.setState({ contentError: err });
} finally {
this.setState({ hasContentLoading: false });
useEffect(() => {
if (project) {
setBreadcrumb(project);
}
}
}, [project, setBreadcrumb]);
createSchedule(data) {
const { project } = this.state;
function createSchedule(data) {
return ProjectsAPI.createSchedule(project.id, data);
}
loadScheduleOptions() {
const { project } = this.state;
function loadScheduleOptions() {
return ProjectsAPI.readScheduleOptions(project.id);
}
loadSchedules(params) {
const { project } = this.state;
function loadSchedules(params) {
return ProjectsAPI.readSchedules(project.id, params);
}
render() {
const { location, match, me, i18n, setBreadcrumb } = this.props;
const {
project,
contentError,
hasContentLoading,
isInitialized,
isNotifAdmin,
} = this.state;
const canSeeNotificationsTab = me.is_system_auditor || isNotifAdmin;
const canToggleNotifications = isNotifAdmin;
const tabsArray = [
{
name: (
<>
<CaretLeftIcon />
{i18n._(t`Back to Projects`)}
</>
),
link: `/projects`,
id: 99,
},
{ name: i18n._(t`Details`), link: `${match.url}/details` },
{ name: i18n._(t`Access`), link: `${match.url}/access` },
];
if (canSeeNotificationsTab) {
tabsArray.push({
name: i18n._(t`Notifications`),
link: `${match.url}/notifications`,
});
}
const canSeeNotificationsTab = me.is_system_auditor || isNotifAdmin;
const canToggleNotifications = isNotifAdmin;
const tabsArray = [
{
name: (
<>
<CaretLeftIcon />
{i18n._(t`Back to Projects`)}
</>
),
link: `/projects`,
id: 99,
},
{ name: i18n._(t`Details`), link: `/projects/${id}/details` },
{ name: i18n._(t`Access`), link: `/projects/${id}/access` },
];
if (canSeeNotificationsTab) {
tabsArray.push({
name: i18n._(t`Job Templates`),
link: `${match.url}/job_templates`,
name: i18n._(t`Notifications`),
link: `/projects/${id}/notifications`,
});
}
if (project?.scm_type) {
tabsArray.push({
name: i18n._(t`Schedules`),
link: `${match.url}/schedules`,
});
}
tabsArray.push({
name: i18n._(t`Job Templates`),
link: `/projects/${id}/job_templates`,
});
tabsArray.forEach((tab, n) => {
tab.id = n;
if (project?.scm_type) {
tabsArray.push({
name: i18n._(t`Schedules`),
link: `/projects/${id}/schedules`,
});
}
let showCardHeader = true;
if (
!isInitialized ||
location.pathname.endsWith('edit') ||
location.pathname.includes('schedules/')
) {
showCardHeader = false;
}
if (!hasContentLoading && contentError) {
return (
<PageSection>
<Card>
<ContentError error={contentError}>
{contentError.response.status === 404 && (
<span>
{i18n._(t`Project not found.`)}{' '}
<Link to="/projects">{i18n._(t`View all Projects.`)}</Link>
</span>
)}
</ContentError>
</Card>
</PageSection>
);
}
tabsArray.forEach((tab, n) => {
tab.id = n;
});
if (contentError) {
return (
<PageSection>
<Card>
{showCardHeader && <RoutedTabs tabsArray={tabsArray} />}
<ContentError error={contentError}>
{contentError.response.status === 404 && (
<span>
{i18n._(t`Project not found.`)}{' '}
<Link to="/projects">{i18n._(t`View all Projects.`)}</Link>
</span>
)}
</ContentError>
</Card>
</PageSection>
);
}
let showCardHeader = true;
if (['edit', 'schedules/'].some(name => location.pathname.includes(name))) {
showCardHeader = false;
}
return (
<PageSection>
<Card>
{showCardHeader && <RoutedTabs tabsArray={tabsArray} />}
{hasContentLoading && <ContentLoading />}
{!hasContentLoading && project && (
<Switch>
<Redirect from="/projects/:id" to="/projects/:id/details" exact />
{project && (
<Route path="/projects/:id/edit">
<ProjectEdit project={project} />
</Route>
)}
{project && (
<Route path="/projects/:id/details">
<ProjectDetail project={project} />
</Route>
)}
{project && (
<Route path="/projects/:id/access">
<ResourceAccessList resource={project} apiModel={ProjectsAPI} />
</Route>
)}
<Route path="/projects/:id/edit">
<ProjectEdit project={project} />
</Route>
<Route path="/projects/:id/details">
<ProjectDetail project={project} />
</Route>
<Route path="/projects/:id/access">
<ResourceAccessList resource={project} apiModel={ProjectsAPI} />
</Route>
{canSeeNotificationsTab && (
<Route path="/projects/:id/notifications">
<NotificationList
id={Number(match.params.id)}
id={Number(id)}
canToggleNotifications={canToggleNotifications}
apiModel={ProjectsAPI}
/>
</Route>
)}
<Route path="/projects/:id/job_templates">
<ProjectJobTemplatesList id={Number(match.params.id)} />
<ProjectJobTemplatesList />
</Route>
{project?.scm_type && project.scm_type !== '' && (
<Route path="/projects/:id/schedules">
<Schedules
setBreadcrumb={setBreadcrumb}
unifiedJobTemplate={project}
createSchedule={this.createSchedule}
loadSchedules={this.loadSchedules}
loadScheduleOptions={this.loadScheduleOptions}
createSchedule={createSchedule}
loadSchedules={loadSchedules}
loadScheduleOptions={loadScheduleOptions}
/>
</Route>
)}
<Route key="not-found" path="*">
{!hasContentLoading && (
<ContentError isNotFound>
{match.params.id && (
<Link to={`/projects/${match.params.id}/details`}>
{i18n._(t`View Project Details`)}
</Link>
)}
</ContentError>
)}
<ContentError isNotFound>
{id && (
<Link to={`/projects/${id}/details`}>
{i18n._(t`View Project Details`)}
</Link>
)}
</ContentError>
</Route>
,
</Switch>
</Card>
</PageSection>
);
}
)}
</Card>
</PageSection>
);
}
export default withI18n()(withRouter(Project));

View File

@ -1,4 +1,5 @@
import React from 'react';
import { act } from 'react-dom/test-utils';
import { createMemoryHistory } from 'history';
import { OrganizationsAPI, ProjectsAPI } from '../../api';
import {
@ -28,29 +29,34 @@ async function getOrganizations() {
}
describe('<Project />', () => {
test('initially renders succesfully', () => {
let wrapper;
test('initially renders successfully', async () => {
ProjectsAPI.readDetail.mockResolvedValue({ data: mockDetails });
OrganizationsAPI.read.mockImplementation(getOrganizations);
mountWithContexts(<Project setBreadcrumb={() => {}} me={mockMe} />);
await act(async () => {
mountWithContexts(<Project setBreadcrumb={() => {}} me={mockMe} />);
});
});
test('notifications tab shown for admins', async done => {
test('notifications tab shown for admins', async () => {
ProjectsAPI.readDetail.mockResolvedValue({ data: mockDetails });
OrganizationsAPI.read.mockImplementation(getOrganizations);
const wrapper = mountWithContexts(
<Project setBreadcrumb={() => {}} me={mockMe} />
);
await act(async () => {
wrapper = mountWithContexts(
<Project setBreadcrumb={() => {}} me={mockMe} />
);
});
const tabs = await waitForElement(
wrapper,
'.pf-c-tabs__item',
el => el.length === 6
);
expect(tabs.at(3).text()).toEqual('Notifications');
done();
});
test('notifications tab hidden with reduced permissions', async done => {
test('notifications tab hidden with reduced permissions', async () => {
ProjectsAPI.readDetail.mockResolvedValue({ data: mockDetails });
OrganizationsAPI.read.mockResolvedValue({
count: 0,
@ -58,20 +64,20 @@ describe('<Project />', () => {
previous: null,
data: { results: [] },
});
const wrapper = mountWithContexts(
<Project setBreadcrumb={() => {}} me={mockMe} />
);
await act(async () => {
wrapper = mountWithContexts(
<Project setBreadcrumb={() => {}} me={mockMe} />
);
});
const tabs = await waitForElement(
wrapper,
'.pf-c-tabs__item',
el => el.length === 5
);
tabs.forEach(tab => expect(tab.text()).not.toEqual('Notifications'));
done();
});
test('schedules tab shown for scm based projects.', async done => {
test('schedules tab shown for scm based projects.', async () => {
ProjectsAPI.readDetail.mockResolvedValue({ data: mockDetails });
OrganizationsAPI.read.mockResolvedValue({
count: 0,
@ -80,19 +86,21 @@ describe('<Project />', () => {
data: { results: [] },
});
const wrapper = mountWithContexts(
<Project setBreadcrumb={() => {}} me={mockMe} />
);
await act(async () => {
wrapper = mountWithContexts(
<Project setBreadcrumb={() => {}} me={mockMe} />
);
});
const tabs = await waitForElement(
wrapper,
'.pf-c-tabs__item',
el => el.length === 5
);
expect(tabs.at(4).text()).toEqual('Schedules');
done();
});
test('schedules tab hidden for manual projects.', async done => {
test('schedules tab hidden for manual projects.', async () => {
const manualDetails = Object.assign(mockDetails, { scm_type: '' });
ProjectsAPI.readDetail.mockResolvedValue({ data: manualDetails });
OrganizationsAPI.read.mockResolvedValue({
@ -102,40 +110,44 @@ describe('<Project />', () => {
data: { results: [] },
});
const wrapper = mountWithContexts(
<Project setBreadcrumb={() => {}} me={mockMe} />
);
await act(async () => {
wrapper = mountWithContexts(
<Project setBreadcrumb={() => {}} me={mockMe} />
);
});
const tabs = await waitForElement(
wrapper,
'.pf-c-tabs__item',
el => el.length === 4
);
tabs.forEach(tab => expect(tab.text()).not.toEqual('Schedules'));
done();
});
test('should show content error when user attempts to navigate to erroneous route', async () => {
const history = createMemoryHistory({
initialEntries: ['/projects/1/foobar'],
});
const wrapper = mountWithContexts(
<Project setBreadcrumb={() => {}} me={mockMe} />,
{
context: {
router: {
history,
route: {
location: history.location,
match: {
params: { id: 1 },
url: '/projects/1/foobar',
path: '/project/1/foobar',
await act(async () => {
wrapper = mountWithContexts(
<Project setBreadcrumb={() => {}} me={mockMe} />,
{
context: {
router: {
history,
route: {
location: history.location,
match: {
params: { id: 1 },
url: '/projects/1/foobar',
path: '/project/1/foobar',
},
},
},
},
},
}
);
}
);
});
await waitForElement(wrapper, 'ContentError', el => el.length === 1);
});
});

View File

@ -1,90 +1,64 @@
import React, { Component, Fragment } from 'react';
import React, { useState, useCallback } from 'react';
import { Route, withRouter, Switch } from 'react-router-dom';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import { Config } from '../../contexts/Config';
import Breadcrumbs from '../../components/Breadcrumbs/Breadcrumbs';
import ProjectsList from './ProjectList/ProjectList';
import ProjectAdd from './ProjectAdd/ProjectAdd';
import Project from './Project';
class Projects extends Component {
constructor(props) {
super(props);
function Projects({ i18n }) {
const [breadcrumbConfig, setBreadcrumbConfig] = useState({
'/projects': i18n._(t`Projects`),
'/projects/add': i18n._(t`Create New Project`),
});
const { i18n } = props;
this.state = {
breadcrumbConfig: {
const buildBreadcrumbConfig = useCallback(
(project, nested) => {
if (!project) {
return;
}
const projectSchedulesPath = `/projects/${project.id}/schedules`;
setBreadcrumbConfig({
'/projects': i18n._(t`Projects`),
'/projects/add': i18n._(t`Create New Project`),
},
};
}
[`/projects/${project.id}`]: `${project.name}`,
[`/projects/${project.id}/edit`]: i18n._(t`Edit Details`),
[`/projects/${project.id}/details`]: i18n._(t`Details`),
[`/projects/${project.id}/access`]: i18n._(t`Access`),
[`/projects/${project.id}/notifications`]: i18n._(t`Notifications`),
[`/projects/${project.id}/job_templates`]: i18n._(t`Job Templates`),
setBreadcrumbConfig = (project, nested) => {
const { i18n } = this.props;
[`${projectSchedulesPath}`]: i18n._(t`Schedules`),
[`${projectSchedulesPath}/add`]: i18n._(t`Create New Schedule`),
[`${projectSchedulesPath}/${nested?.id}`]: `${nested?.name}`,
[`${projectSchedulesPath}/${nested?.id}/details`]: i18n._(
t`Schedule Details`
),
[`${projectSchedulesPath}/${nested?.id}/edit`]: i18n._(t`Edit Details`),
});
},
[i18n]
);
if (!project) {
return;
}
const projectSchedulesPath = `/projects/${project.id}/schedules`;
const breadcrumbConfig = {
'/projects': i18n._(t`Projects`),
'/projects/add': i18n._(t`Create New Project`),
[`/projects/${project.id}`]: `${project.name}`,
[`/projects/${project.id}/edit`]: i18n._(t`Edit Details`),
[`/projects/${project.id}/details`]: i18n._(t`Details`),
[`/projects/${project.id}/access`]: i18n._(t`Access`),
[`/projects/${project.id}/notifications`]: i18n._(t`Notifications`),
[`/projects/${project.id}/job_templates`]: i18n._(t`Job Templates`),
[`${projectSchedulesPath}`]: i18n._(t`Schedules`),
[`${projectSchedulesPath}/add`]: i18n._(t`Create New Schedule`),
[`${projectSchedulesPath}/${nested?.id}`]: `${nested?.name}`,
[`${projectSchedulesPath}/${nested?.id}/details`]: i18n._(
t`Schedule Details`
),
[`${projectSchedulesPath}/${nested?.id}/edit`]: i18n._(t`Edit Details`),
};
this.setState({ breadcrumbConfig });
};
render() {
const { match, history, location } = this.props;
const { breadcrumbConfig } = this.state;
return (
<Fragment>
<Breadcrumbs breadcrumbConfig={breadcrumbConfig} />
<Switch>
<Route path={`${match.path}/add`}>
<ProjectAdd />
</Route>
<Route path={`${match.path}/:id`}>
<Config>
{({ me }) => (
<Project
history={history}
location={location}
setBreadcrumb={this.setBreadcrumbConfig}
me={me || {}}
/>
)}
</Config>
</Route>
<Route path={`${match.path}`}>
<ProjectsList />
</Route>
</Switch>
</Fragment>
);
}
return (
<>
<Breadcrumbs breadcrumbConfig={breadcrumbConfig} />
<Switch>
<Route path="/projects/add">
<ProjectAdd />
</Route>
<Route path="/projects/:id">
<Project setBreadcrumb={buildBreadcrumbConfig} />
</Route>
<Route path="/projects">
<ProjectsList />
</Route>
</Switch>
</>
);
}
export { Projects as _Projects };

View File

@ -1,70 +1,52 @@
import React, { Fragment } from 'react';
import React, { useCallback } from 'react';
import { number } from 'prop-types';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import useRequest, { useDismissableError } from '../../../util/useRequest';
import AlertModal from '../../../components/AlertModal';
import ErrorDetail from '../../../components/ErrorDetail';
import { ProjectsAPI } from '../../../api';
class ProjectSyncButton extends React.Component {
static propTypes = {
projectId: number.isRequired,
};
constructor(props) {
super(props);
this.state = {
syncError: null,
};
this.handleSync = this.handleSync.bind(this);
this.handleSyncErrorClose = this.handleSyncErrorClose.bind(this);
}
handleSyncErrorClose() {
this.setState({ syncError: null });
}
async handleSync() {
const { i18n, projectId } = this.props;
try {
const { data: syncConfig } = await ProjectsAPI.readSync(projectId);
if (syncConfig.can_update) {
function ProjectSyncButton({ i18n, children, projectId }) {
const { request: handleSync, error: syncError } = useRequest(
useCallback(async () => {
const { data } = await ProjectsAPI.readSync(projectId);
if (data.can_update) {
await ProjectsAPI.sync(projectId);
} else {
this.setState({
syncError: i18n._(
throw new Error(
i18n._(
t`You don't have the necessary permissions to sync this project.`
),
});
)
);
}
} catch (err) {
this.setState({ syncError: err });
}
}
}, [i18n, projectId]),
null
);
render() {
const { syncError } = this.state;
const { i18n, children } = this.props;
return (
<Fragment>
{children(this.handleSync)}
{syncError && (
<AlertModal
isOpen={syncError}
variant="error"
title={i18n._(t`Error!`)}
onClose={this.handleSyncErrorClose}
>
{i18n._(t`Failed to sync job.`)}
<ErrorDetail error={syncError} />
</AlertModal>
)}
</Fragment>
);
}
const { error, dismissError } = useDismissableError(syncError);
return (
<>
{children(handleSync)}
{error && (
<AlertModal
isOpen={error}
variant="error"
title={i18n._(t`Error!`)}
onClose={dismissError}
>
{i18n._(t`Failed to sync project.`)}
<ErrorDetail error={error} />
</AlertModal>
)}
</>
);
}
ProjectSyncButton.propTypes = {
projectId: number.isRequired,
};
export default withI18n()(ProjectSyncButton);

View File

@ -1,4 +1,5 @@
import React from 'react';
import { act } from 'react-dom/test-utils';
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
import { sleep } from '../../../../testUtils/testUtils';
@ -8,6 +9,7 @@ import { ProjectsAPI } from '../../../api';
jest.mock('../../../api');
describe('ProjectSyncButton', () => {
let wrapper;
ProjectsAPI.readSync.mockResolvedValue({
data: {
can_update: true,
@ -18,29 +20,34 @@ describe('ProjectSyncButton', () => {
<button type="submit" onClick={() => handleSync()} />
);
test('renders the expected content', () => {
const wrapper = mountWithContexts(
<ProjectSyncButton projectId={1}>{children}</ProjectSyncButton>
);
test('renders the expected content', async () => {
await act(async () => {
wrapper = mountWithContexts(
<ProjectSyncButton projectId={1}>{children}</ProjectSyncButton>
);
});
expect(wrapper).toHaveLength(1);
});
test('correct api calls are made on sync', async done => {
test('correct api calls are made on sync', async () => {
ProjectsAPI.sync.mockResolvedValue({
data: {
id: 9000,
},
});
const wrapper = mountWithContexts(
<ProjectSyncButton projectId={1}>{children}</ProjectSyncButton>
);
await act(async () => {
wrapper = mountWithContexts(
<ProjectSyncButton projectId={1}>{children}</ProjectSyncButton>
);
});
const button = wrapper.find('button');
button.prop('onClick')();
await act(async () => {
button.prop('onClick')();
});
expect(ProjectsAPI.readSync).toHaveBeenCalledWith(1);
await sleep(0);
expect(ProjectsAPI.sync).toHaveBeenCalledWith(1);
done();
});
test('displays error modal after unsuccessful sync', async done => {
test('displays error modal after unsuccessful sync', async () => {
ProjectsAPI.sync.mockRejectedValue(
new Error({
response: {
@ -53,18 +60,23 @@ describe('ProjectSyncButton', () => {
},
})
);
const wrapper = mountWithContexts(
<ProjectSyncButton projectId={1}>{children}</ProjectSyncButton>
);
await act(async () => {
wrapper = mountWithContexts(
<ProjectSyncButton projectId={1}>{children}</ProjectSyncButton>
);
});
expect(wrapper.find('Modal').length).toBe(0);
wrapper.find('button').prop('onClick')();
await act(async () => {
wrapper.find('button').prop('onClick')();
});
await sleep(0);
wrapper.update();
expect(wrapper.find('Modal').length).toBe(1);
wrapper.find('ModalBoxCloseButton').simulate('click');
await act(async () => {
wrapper.find('ModalBoxCloseButton').simulate('click');
});
await sleep(0);
wrapper.update();
expect(wrapper.find('Modal').length).toBe(0);
done();
});
});