@@ -30,7 +30,7 @@ export interface ComponentNavProjectBindingErrorNotifProps { | |||
component: Component; | |||
} | |||
export function ComponentNavProjectBindingErrorNotif( | |||
export default function ComponentNavProjectBindingErrorNotif( | |||
props: ComponentNavProjectBindingErrorNotifProps | |||
) { | |||
const { component } = props; | |||
@@ -56,5 +56,3 @@ export function ComponentNavProjectBindingErrorNotif( | |||
</Alert> | |||
); | |||
} | |||
export default ComponentNavProjectBindingErrorNotif; |
@@ -17,23 +17,38 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { shallow } from 'enzyme'; | |||
import { screen } from '@testing-library/react'; | |||
import * as React from 'react'; | |||
import { mockComponent } from '../../../../../helpers/mocks/component'; | |||
import { | |||
ComponentNavProjectBindingErrorNotif, | |||
import { renderComponent } from '../../../../../helpers/testReactTestingUtils'; | |||
import ComponentNavProjectBindingErrorNotif, { | |||
ComponentNavProjectBindingErrorNotifProps, | |||
} from '../ComponentNavProjectBindingErrorNotif'; | |||
it('should render correctly', () => { | |||
expect(shallowRender()).toMatchSnapshot('non-project admin'); | |||
it('should not show a link if use is not allowed', () => { | |||
renderComponentNavProjectBindingErrorNotif({ | |||
component: mockComponent({ configuration: { showSettings: false } }), | |||
}); | |||
expect( | |||
shallowRender({ component: mockComponent({ configuration: { showSettings: true } }) }) | |||
).toMatchSnapshot('project admin'); | |||
screen.queryByRole('link', { | |||
name: 'component_navigation.pr_deco.action.check_project_settings', | |||
}) | |||
).not.toBeInTheDocument(); | |||
}); | |||
function shallowRender(props: Partial<ComponentNavProjectBindingErrorNotifProps> = {}) { | |||
return shallow<ComponentNavProjectBindingErrorNotifProps>( | |||
it('should show a link if use is allowed', () => { | |||
renderComponentNavProjectBindingErrorNotif({ | |||
component: mockComponent({ configuration: { showSettings: true } }), | |||
}); | |||
expect( | |||
screen.getByRole('link', { name: 'component_navigation.pr_deco.action.check_project_settings' }) | |||
).toBeInTheDocument(); | |||
}); | |||
function renderComponentNavProjectBindingErrorNotif( | |||
props: Partial<ComponentNavProjectBindingErrorNotifProps> = {} | |||
) { | |||
return renderComponent( | |||
<ComponentNavProjectBindingErrorNotif component={mockComponent()} {...props} /> | |||
); | |||
} |
@@ -1,44 +0,0 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render correctly: non-project admin 1`] = ` | |||
<Alert | |||
display="banner" | |||
variant="warning" | |||
> | |||
<FormattedMessage | |||
defaultMessage="component_navigation.pr_deco.error_detected_X" | |||
id="component_navigation.pr_deco.error_detected_X" | |||
values={ | |||
{ | |||
"action": "component_navigation.pr_deco.action.contact_project_admin", | |||
} | |||
} | |||
/> | |||
</Alert> | |||
`; | |||
exports[`should render correctly: project admin 1`] = ` | |||
<Alert | |||
display="banner" | |||
variant="warning" | |||
> | |||
<FormattedMessage | |||
defaultMessage="component_navigation.pr_deco.error_detected_X" | |||
id="component_navigation.pr_deco.error_detected_X" | |||
values={ | |||
{ | |||
"action": <ForwardRef(Link) | |||
to={ | |||
{ | |||
"pathname": "/project/settings", | |||
"search": "?id=my-project&category=pull_request_decoration_binding", | |||
} | |||
} | |||
> | |||
component_navigation.pr_deco.action.check_project_settings | |||
</ForwardRef(Link)>, | |||
} | |||
} | |||
/> | |||
</Alert> | |||
`; |
@@ -110,7 +110,7 @@ export class BadgeParams extends React.PureComponent<Props> { | |||
</label> | |||
<Select | |||
className="input-medium it__metric-badge-select" | |||
name="badge-metric" | |||
inputId="badge-metric" | |||
isSearchable={false} | |||
onChange={this.handleMetricChange} | |||
options={metricOptions} | |||
@@ -139,7 +139,7 @@ export class BadgeParams extends React.PureComponent<Props> { | |||
</label> | |||
<Select | |||
className="input-medium" | |||
name="badge-format" | |||
inputId="badge-format" | |||
isSearchable={false} | |||
onChange={this.handleFormatChange} | |||
options={formatOptions} |
@@ -1,49 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2023 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import { click } from '../../../../../../../helpers/testUtils'; | |||
import BadgeButton from '../BadgeButton'; | |||
import { BadgeType } from '../utils'; | |||
it('should display correctly', () => { | |||
expect(getWrapper()).toMatchSnapshot(); | |||
expect(getWrapper({ selected: true })).toMatchSnapshot(); | |||
expect(getWrapper({ type: BadgeType.measure })).toMatchSnapshot(); | |||
}); | |||
it('should return the badge type on click', () => { | |||
const onClick = jest.fn(); | |||
const wrapper = getWrapper({ onClick }); | |||
click(wrapper.find('Button')); | |||
expect(onClick).toHaveBeenCalledWith(BadgeType.qualityGate); | |||
}); | |||
function getWrapper(props = {}) { | |||
return shallow( | |||
<BadgeButton | |||
onClick={jest.fn()} | |||
selected={false} | |||
type={BadgeType.qualityGate} | |||
url="http://foo.bar" | |||
{...props} | |||
/> | |||
); | |||
} |
@@ -1,72 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2023 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import { Metric } from '../../../../../../../types/types'; | |||
import { BadgeParams } from '../BadgeParams'; | |||
import { BadgeType } from '../utils'; | |||
jest.mock('../../../../../../../api/web-api', () => ({ | |||
fetchWebApi: () => | |||
Promise.resolve([ | |||
{ | |||
path: 'api/project_badges', | |||
actions: [ | |||
{ | |||
key: 'measure', | |||
params: [{ key: 'metric', possibleValues: ['alert_status', 'coverage'] }], | |||
}, | |||
], | |||
}, | |||
]), | |||
})); | |||
const METRICS = { | |||
alert_status: { key: 'alert_status', name: 'Quality Gate' } as Metric, | |||
coverage: { key: 'coverage', name: 'Coverage' } as Metric, | |||
}; | |||
it('should display measure badge params', () => { | |||
const updateOptions = jest.fn(); | |||
const wrapper = getWrapper({ updateOptions, type: BadgeType.measure }); | |||
expect(wrapper).toMatchSnapshot(); | |||
(wrapper.instance() as BadgeParams).handleMetricChange({ value: 'code_smell' }); | |||
expect(updateOptions).toHaveBeenCalledWith({ metric: 'code_smell' }); | |||
}); | |||
it('should display quality gate badge params', () => { | |||
const updateOptions = jest.fn(); | |||
const wrapper = getWrapper({ updateOptions, type: BadgeType.qualityGate }); | |||
expect(wrapper).toMatchSnapshot(); | |||
(wrapper.instance() as BadgeParams).handleFormatChange({ value: 'md' }); | |||
expect(updateOptions).toHaveBeenCalledWith({ format: 'md' }); | |||
}); | |||
function getWrapper(props = {}) { | |||
return shallow( | |||
<BadgeParams | |||
metrics={METRICS} | |||
options={{ metric: 'alert_status' }} | |||
type={BadgeType.measure} | |||
updateOptions={jest.fn()} | |||
{...props} | |||
/> | |||
); | |||
} |
@@ -17,17 +17,19 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { shallow } from 'enzyme'; | |||
import { screen } from '@testing-library/react'; | |||
import userEvent from '@testing-library/user-event'; | |||
import * as React from 'react'; | |||
import selectEvent from 'react-select-event'; | |||
import { getProjectBadgesToken } from '../../../../../../../api/project-badges'; | |||
import CodeSnippet from '../../../../../../../components/common/CodeSnippet'; | |||
import { mockBranch } from '../../../../../../../helpers/mocks/branch-like'; | |||
import { mockComponent } from '../../../../../../../helpers/mocks/component'; | |||
import { waitAndUpdate } from '../../../../../../../helpers/testUtils'; | |||
import { renderComponent } from '../../../../../../../helpers/testReactTestingUtils'; | |||
import { Location } from '../../../../../../../helpers/urls'; | |||
import { ComponentQualifier } from '../../../../../../../types/component'; | |||
import BadgeButton from '../BadgeButton'; | |||
import { MetricKey } from '../../../../../../../types/metrics'; | |||
import ProjectBadges from '../ProjectBadges'; | |||
import { BadgeType } from '../utils'; | |||
jest.mock('../../../../../../../helpers/urls', () => ({ | |||
getHostUrl: () => 'host', | |||
@@ -40,51 +42,90 @@ jest.mock('../../../../../../../api/project-badges', () => ({ | |||
renewProjectBadgesToken: jest.fn().mockResolvedValue({}), | |||
})); | |||
jest.mock('react', () => { | |||
return { | |||
...jest.requireActual('react'), | |||
createRef: jest.fn().mockReturnValue({ current: document.createElement('h3') }), | |||
}; | |||
}); | |||
it('should display correctly', async () => { | |||
const wrapper = shallowRender(); | |||
await waitAndUpdate(wrapper); | |||
expect(wrapper).toMatchSnapshot(); | |||
}); | |||
jest.mock('../../../../../../../api/web-api', () => ({ | |||
fetchWebApi: () => | |||
Promise.resolve([ | |||
{ | |||
path: 'api/project_badges', | |||
actions: [ | |||
{ | |||
key: 'measure', | |||
// eslint-disable-next-line local-rules/use-metrickey-enum | |||
params: [{ key: 'metric', possibleValues: ['alert_status', 'coverage'] }], | |||
}, | |||
], | |||
}, | |||
]), | |||
})); | |||
it('should renew token', async () => { | |||
(getProjectBadgesToken as jest.Mock).mockResolvedValueOnce('foo').mockResolvedValueOnce('bar'); | |||
const wrapper = shallowRender({ | |||
const user = userEvent.setup(); | |||
jest.mocked(getProjectBadgesToken).mockResolvedValueOnce('foo').mockResolvedValueOnce('bar'); | |||
renderProjectBadges({ | |||
component: mockComponent({ configuration: { showSettings: true } }), | |||
}); | |||
await waitAndUpdate(wrapper); | |||
wrapper.find('.it__project-info-renew-badge').simulate('click'); | |||
// it shoud be loading | |||
expect(wrapper.find('.it__project-info-renew-badge').props().disabled).toBe(true); | |||
expect( | |||
await screen.findByText(`overview.badges.get_badge.${ComponentQualifier.Project}`) | |||
).toHaveFocus(); | |||
expect(screen.getByAltText(`overview.badges.${BadgeType.qualityGate}.alt`)).toHaveAttribute( | |||
'src', | |||
'host/api/project_badges/quality_gate?branch=branch-6.7&project=my-project&token=foo' | |||
); | |||
expect(screen.getByAltText(`overview.badges.${BadgeType.measure}.alt`)).toHaveAttribute( | |||
'src', | |||
'host/api/project_badges/measure?branch=branch-6.7&project=my-project&metric=alert_status&token=foo' | |||
); | |||
await user.click(screen.getByText('overview.badges.renew')); | |||
await waitAndUpdate(wrapper); | |||
const buttons = wrapper.find(BadgeButton); | |||
expect(buttons.at(0).props().url).toMatch('token=bar'); | |||
expect(buttons.at(1).props().url).toMatch('token=bar'); | |||
expect(wrapper.find(CodeSnippet).props().snippet).toMatch('token=bar'); | |||
expect( | |||
await screen.findByAltText(`overview.badges.${BadgeType.qualityGate}.alt`) | |||
).toHaveAttribute( | |||
'src', | |||
'host/api/project_badges/quality_gate?branch=branch-6.7&project=my-project&token=bar' | |||
); | |||
// let's check that the loading has correclty ends. | |||
expect(wrapper.find('.it__project-info-renew-badge').props().disabled).toBe(false); | |||
expect(screen.getByAltText(`overview.badges.${BadgeType.measure}.alt`)).toHaveAttribute( | |||
'src', | |||
'host/api/project_badges/measure?branch=branch-6.7&project=my-project&metric=alert_status&token=bar' | |||
); | |||
}); | |||
it('should set focus on the heading when rendered', async () => { | |||
const fakeElement = document.createElement('h3'); | |||
const focus = jest.fn(); | |||
(React.createRef as jest.Mock).mockReturnValueOnce({ current: { ...fakeElement, focus } }); | |||
const wrapper = shallowRender(); | |||
await waitAndUpdate(wrapper); | |||
expect(focus).toHaveBeenCalled(); | |||
it('should update params', async () => { | |||
renderProjectBadges({ | |||
component: mockComponent({ configuration: { showSettings: true } }), | |||
}); | |||
expect( | |||
await screen.findByText( | |||
'[![alert_status](host/api/project_badges/measure?branch=branch-6.7&project=my-project&metric=alert_status&token=foo)](/dashboard)' | |||
) | |||
).toBeInTheDocument(); | |||
await selectEvent.select(screen.getByLabelText('format:'), [ | |||
'overview.badges.options.formats.url', | |||
]); | |||
expect( | |||
screen.getByText( | |||
'host/api/project_badges/measure?branch=branch-6.7&project=my-project&metric=alert_status&token=foo' | |||
) | |||
).toBeInTheDocument(); | |||
await selectEvent.select(screen.getByLabelText('overview.badges.metric:'), MetricKey.coverage); | |||
expect( | |||
screen.getByText( | |||
`host/api/project_badges/measure?branch=branch-6.7&project=my-project&metric=${MetricKey.coverage}&token=foo` | |||
) | |||
).toBeInTheDocument(); | |||
}); | |||
function shallowRender(props: Partial<ProjectBadges['props']> = {}) { | |||
return shallow<ProjectBadges>( | |||
function renderProjectBadges(props: Partial<ProjectBadges['props']> = {}) { | |||
return renderComponent( | |||
<ProjectBadges | |||
branchLike={mockBranch()} | |||
component={mockComponent({ key: 'foo', qualifier: ComponentQualifier.Project })} |
@@ -1,39 +0,0 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should display correctly 1`] = ` | |||
<Button | |||
className="badge-button" | |||
onClick={[Function]} | |||
> | |||
<img | |||
alt="overview.badges.quality_gate.alt" | |||
src="http://foo.bar" | |||
width="128px" | |||
/> | |||
</Button> | |||
`; | |||
exports[`should display correctly 2`] = ` | |||
<Button | |||
className="badge-button selected" | |||
onClick={[Function]} | |||
> | |||
<img | |||
alt="overview.badges.quality_gate.alt" | |||
src="http://foo.bar" | |||
width="128px" | |||
/> | |||
</Button> | |||
`; | |||
exports[`should display correctly 3`] = ` | |||
<Button | |||
className="badge-button" | |||
onClick={[Function]} | |||
> | |||
<img | |||
alt="overview.badges.measure.alt" | |||
src="http://foo.bar" | |||
/> | |||
</Button> | |||
`; |
@@ -1,87 +0,0 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should display measure badge params 1`] = ` | |||
<div> | |||
<label | |||
className="spacer-right" | |||
htmlFor="badge-metric" | |||
> | |||
overview.badges.metric | |||
: | |||
</label> | |||
<Select | |||
className="input-medium it__metric-badge-select" | |||
isSearchable={false} | |||
name="badge-metric" | |||
onChange={[Function]} | |||
options={[]} | |||
/> | |||
<label | |||
className="spacer-right spacer-top" | |||
htmlFor="badge-format" | |||
> | |||
format | |||
: | |||
</label> | |||
<Select | |||
className="input-medium" | |||
defaultValue={ | |||
{ | |||
"label": "overview.badges.options.formats.md", | |||
"value": "md", | |||
} | |||
} | |||
isSearchable={false} | |||
name="badge-format" | |||
onChange={[Function]} | |||
options={ | |||
[ | |||
{ | |||
"label": "overview.badges.options.formats.md", | |||
"value": "md", | |||
}, | |||
{ | |||
"label": "overview.badges.options.formats.url", | |||
"value": "url", | |||
}, | |||
] | |||
} | |||
/> | |||
</div> | |||
`; | |||
exports[`should display quality gate badge params 1`] = ` | |||
<div> | |||
<label | |||
className="spacer-right" | |||
htmlFor="badge-format" | |||
> | |||
format | |||
: | |||
</label> | |||
<Select | |||
className="input-medium" | |||
defaultValue={ | |||
{ | |||
"label": "overview.badges.options.formats.md", | |||
"value": "md", | |||
} | |||
} | |||
isSearchable={false} | |||
name="badge-format" | |||
onChange={[Function]} | |||
options={ | |||
[ | |||
{ | |||
"label": "overview.badges.options.formats.md", | |||
"value": "md", | |||
}, | |||
{ | |||
"label": "overview.badges.options.formats.url", | |||
"value": "url", | |||
}, | |||
] | |||
} | |||
/> | |||
</div> | |||
`; |
@@ -1,62 +0,0 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should display correctly 1`] = ` | |||
<div | |||
className="display-flex-column" | |||
> | |||
<h3 | |||
tabIndex={-1} | |||
> | |||
overview.badges.get_badge.TRK | |||
</h3> | |||
<p | |||
className="big-spacer-bottom" | |||
> | |||
overview.badges.description.TRK | |||
</p> | |||
<BadgeButton | |||
onClick={[Function]} | |||
selected={true} | |||
type="measure" | |||
url="host/api/project_badges/measure?branch=branch-6.7&project=foo&metric=alert_status&token=foo" | |||
/> | |||
<p | |||
className="huge-spacer-bottom spacer-top" | |||
> | |||
overview.badges.measure.description.TRK | |||
</p> | |||
<BadgeButton | |||
onClick={[Function]} | |||
selected={false} | |||
type="quality_gate" | |||
url="host/api/project_badges/quality_gate?branch=branch-6.7&project=foo&token=foo" | |||
/> | |||
<p | |||
className="huge-spacer-bottom spacer-top" | |||
> | |||
overview.badges.quality_gate.description.TRK | |||
</p> | |||
<withMetricsContext(BadgeParams) | |||
className="big-spacer-bottom display-flex-column" | |||
options={ | |||
{ | |||
"metric": "alert_status", | |||
} | |||
} | |||
type="measure" | |||
updateOptions={[Function]} | |||
/> | |||
<CodeSnippet | |||
isOneLine={true} | |||
snippet="[![alert_status](host/api/project_badges/measure?branch=branch-6.7&project=foo&metric=alert_status&token=foo)](/dashboard)" | |||
/> | |||
<Alert | |||
variant="warning" | |||
> | |||
<p> | |||
overview.badges.leak_warning | |||
</p> | |||
</Alert> | |||
</div> | |||
`; |
@@ -71,16 +71,14 @@ export default class MetaLinks extends React.PureComponent<Props, State> { | |||
const orderedLinks = orderLinks(links); | |||
return ( | |||
<> | |||
<div className="big-padded bordered-bottom"> | |||
<h3>{translate('overview.external_links')}</h3> | |||
<ul className="project-info-list"> | |||
{orderedLinks.map((link) => ( | |||
<MetaLink key={link.id} link={link} /> | |||
))} | |||
</ul> | |||
</div> | |||
</> | |||
<div className="big-padded bordered-bottom"> | |||
<h3>{translate('overview.external_links')}</h3> | |||
<ul className="project-info-list"> | |||
{orderedLinks.map((link) => ( | |||
<MetaLink key={link.id} link={link} /> | |||
))} | |||
</ul> | |||
</div> | |||
); | |||
} | |||
} |
@@ -34,20 +34,11 @@ interface Props { | |||
} | |||
export default class MetaTags extends React.PureComponent<Props> { | |||
card?: HTMLDivElement | null; | |||
tagsList?: HTMLElement | null; | |||
tagsSelector?: HTMLDivElement | null; | |||
canUpdateTags = () => { | |||
const { configuration } = this.props.component; | |||
return configuration && configuration.showSettings; | |||
}; | |||
getPopupPos = (eltPos: ClientRect, containerPos: ClientRect) => ({ | |||
top: eltPos.height, | |||
right: containerPos.width - eltPos.width, | |||
}); | |||
setTags = (values: string[]) => { | |||
const { component } = this.props; | |||
@@ -56,12 +47,12 @@ export default class MetaTags extends React.PureComponent<Props> { | |||
application: component.key, | |||
tags: values.join(','), | |||
}); | |||
} else { | |||
return setProjectTags({ | |||
project: component.key, | |||
tags: values.join(','), | |||
}); | |||
} | |||
return setProjectTags({ | |||
project: component.key, | |||
tags: values.join(','), | |||
}); | |||
}; | |||
handleSetProjectTags = (values: string[]) => { | |||
@@ -74,33 +65,29 @@ export default class MetaTags extends React.PureComponent<Props> { | |||
render() { | |||
const tags = this.props.component.tags || []; | |||
if (this.canUpdateTags()) { | |||
return ( | |||
<div className="big-spacer-top project-info-tags" ref={(card) => (this.card = card)}> | |||
<Dropdown | |||
closeOnClick={false} | |||
closeOnClickOutside={true} | |||
overlay={ | |||
<MetaTagsSelector selectedTags={tags} setProjectTags={this.handleSetProjectTags} /> | |||
} | |||
overlayPlacement={PopupPlacement.BottomLeft} | |||
> | |||
<ButtonLink innerRef={(tagsList) => (this.tagsList = tagsList)} stopPropagation={true}> | |||
<TagsList allowUpdate={true} tags={tags.length ? tags : [translate('no_tags')]} /> | |||
</ButtonLink> | |||
</Dropdown> | |||
</div> | |||
); | |||
} else { | |||
return ( | |||
<div className="big-spacer-top project-info-tags"> | |||
<TagsList | |||
allowUpdate={false} | |||
className="note" | |||
tags={tags.length ? tags : [translate('no_tags')]} | |||
/> | |||
</div> | |||
); | |||
} | |||
return this.canUpdateTags() ? ( | |||
<div className="big-spacer-top project-info-tags"> | |||
<Dropdown | |||
closeOnClick={false} | |||
closeOnClickOutside={true} | |||
overlay={ | |||
<MetaTagsSelector selectedTags={tags} setProjectTags={this.handleSetProjectTags} /> | |||
} | |||
overlayPlacement={PopupPlacement.BottomLeft} | |||
> | |||
<ButtonLink stopPropagation={true}> | |||
<TagsList allowUpdate={true} tags={tags.length ? tags : [translate('no_tags')]} /> | |||
</ButtonLink> | |||
</Dropdown> | |||
</div> | |||
) : ( | |||
<div className="big-spacer-top project-info-tags"> | |||
<TagsList | |||
allowUpdate={false} | |||
className="note" | |||
tags={tags.length ? tags : [translate('no_tags')]} | |||
/> | |||
</div> | |||
); | |||
} | |||
} |
@@ -1,72 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2023 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import { ClearButton } from '../../../../../../../components/controls/buttons'; | |||
import { click } from '../../../../../../../helpers/testUtils'; | |||
import MetaLink from '../MetaLink'; | |||
const DANGEROUS_LINK = { | |||
id: '1', | |||
name: 'Dangerous', | |||
url: 'javascript:alert("hi")', | |||
type: 'dangerous', | |||
}; | |||
it('should match snapshot', () => { | |||
expect(shallowRender()).toMatchSnapshot('default'); | |||
expect(shallowRender({ iconOnly: true })).toMatchSnapshot('icon only'); | |||
const wrapper = shallowRender({ link: DANGEROUS_LINK }); | |||
expect(wrapper).toMatchSnapshot('dangerous link, collapsed'); | |||
wrapper.setState({ expanded: true }); | |||
expect(wrapper).toMatchSnapshot('dangerous link, expanded'); | |||
}); | |||
it('should expand and collapse dangerous links', () => { | |||
const wrapper = shallowRender({ link: DANGEROUS_LINK }); | |||
expect(wrapper.state().expanded).toBe(false); | |||
// expand | |||
click(wrapper.find('a')); | |||
expect(wrapper.state().expanded).toBe(true); | |||
// collapse | |||
click(wrapper.find('a')); | |||
expect(wrapper.state().expanded).toBe(false); | |||
// collapse with button | |||
wrapper.setState({ expanded: true }); | |||
click(wrapper.find(ClearButton)); | |||
expect(wrapper.state().expanded).toBe(false); | |||
}); | |||
function shallowRender(props: Partial<MetaLink['props']> = {}) { | |||
return shallow<MetaLink>( | |||
<MetaLink | |||
link={{ | |||
id: '1', | |||
name: 'Foo', | |||
url: 'http://example.com', | |||
type: 'foo', | |||
}} | |||
{...props} | |||
/> | |||
); | |||
} |
@@ -17,11 +17,13 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { shallow } from 'enzyme'; | |||
import { screen } from '@testing-library/react'; | |||
import * as React from 'react'; | |||
import { searchRules } from '../../../../../../../api/rules'; | |||
import { mockLanguage, mockQualityProfile } from '../../../../../../../helpers/testMocks'; | |||
import { waitAndUpdate } from '../../../../../../../helpers/testUtils'; | |||
import { renderComponent } from '../../../../../../../helpers/testReactTestingUtils'; | |||
import { SearchRulesResponse } from '../../../../../../../types/coding-rules'; | |||
import { Dict } from '../../../../../../../types/types'; | |||
import { MetaQualityProfiles } from '../MetaQualityProfiles'; | |||
jest.mock('../../../../../../../api/rules', () => { | |||
@@ -33,28 +35,39 @@ jest.mock('../../../../../../../api/rules', () => { | |||
}); | |||
it('should render correctly', async () => { | |||
const wrapper = shallowRender(); | |||
expect(wrapper).toMatchSnapshot(); | |||
const totals: Dict<number> = { | |||
js: 0, | |||
ts: 10, | |||
css: 0, | |||
}; | |||
jest.mocked(searchRules).mockImplementation(({ qprofile }: { qprofile: string }) => { | |||
return Promise.resolve({ total: totals[qprofile] } as SearchRulesResponse); | |||
}); | |||
renderMetaQualityprofiles(); | |||
await waitAndUpdate(wrapper); | |||
expect(wrapper).toMatchSnapshot(); | |||
expect(wrapper.find('.project-info-deprecated-rules').exists()).toBe(true); | |||
expect(wrapper.find('.project-info-deleted-profile').exists()).toBe(true); | |||
expect(searchRules).toHaveBeenCalled(); | |||
expect(await screen.findByText('overview.deleted_profile.javascript')).toBeInTheDocument(); | |||
expect(screen.getByText('overview.deprecated_profile.10')).toBeInTheDocument(); | |||
}); | |||
function shallowRender(props: Partial<MetaQualityProfiles['props']> = {}) { | |||
return shallow( | |||
function renderMetaQualityprofiles(overrides: Partial<MetaQualityProfiles['props']> = {}) { | |||
return renderComponent( | |||
<MetaQualityProfiles | |||
languages={{ css: mockLanguage() }} | |||
profiles={[ | |||
{ ...mockQualityProfile({ key: 'js' }), deleted: true }, | |||
{ ...mockQualityProfile({ key: 'js', name: 'javascript' }), deleted: true }, | |||
{ ...mockQualityProfile({ key: 'ts', name: 'typescript' }), deleted: false }, | |||
{ | |||
...mockQualityProfile({ key: 'css', language: 'css', languageName: 'CSS' }), | |||
...mockQualityProfile({ | |||
key: 'css', | |||
name: 'style', | |||
language: 'css', | |||
languageName: 'CSS', | |||
}), | |||
deleted: false, | |||
}, | |||
]} | |||
{...props} | |||
{...overrides} | |||
/> | |||
); | |||
} |
@@ -1,46 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2023 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import { mockComponent } from '../../../../../../../helpers/mocks/component'; | |||
import { mockMeasure } from '../../../../../../../helpers/testMocks'; | |||
import { ComponentQualifier } from '../../../../../../../types/component'; | |||
import { MetricKey } from '../../../../../../../types/metrics'; | |||
import MetaSize, { MetaSizeProps } from '../MetaSize'; | |||
it('should render correctly', () => { | |||
expect(shallowRender()).toMatchSnapshot('project'); | |||
expect( | |||
shallowRender({ component: mockComponent({ qualifier: ComponentQualifier.Application }) }) | |||
).toMatchSnapshot('application'); | |||
}); | |||
function shallowRender(props: Partial<MetaSizeProps> = {}) { | |||
return shallow<MetaSizeProps>( | |||
<MetaSize | |||
component={mockComponent()} | |||
measures={[ | |||
mockMeasure({ metric: MetricKey.ncloc }), | |||
mockMeasure({ metric: MetricKey.projects }), | |||
]} | |||
{...props} | |||
/> | |||
); | |||
} |
@@ -17,27 +17,41 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { shallow } from 'enzyme'; | |||
import { screen } from '@testing-library/react'; | |||
import userEvent from '@testing-library/user-event'; | |||
import * as React from 'react'; | |||
import { setApplicationTags, setProjectTags } from '../../../../../../../api/components'; | |||
import { | |||
searchProjectTags, | |||
setApplicationTags, | |||
setProjectTags, | |||
} from '../../../../../../../api/components'; | |||
import { mockComponent } from '../../../../../../../helpers/mocks/component'; | |||
import { renderComponent } from '../../../../../../../helpers/testReactTestingUtils'; | |||
import { ComponentQualifier } from '../../../../../../../types/component'; | |||
import MetaTags from '../MetaTags'; | |||
jest.mock('../../../../../../../api/components', () => ({ | |||
setApplicationTags: jest.fn().mockResolvedValue(true), | |||
setProjectTags: jest.fn().mockResolvedValue(true), | |||
searchProjectTags: jest.fn(), | |||
})); | |||
beforeEach(() => { | |||
jest.clearAllMocks(); | |||
}); | |||
it('should render without tags and admin rights', () => { | |||
expect(shallowRender()).toMatchSnapshot(); | |||
it('should render without tags and admin rights', async () => { | |||
renderMetaTags(); | |||
expect(await screen.findByText('no_tags')).toBeInTheDocument(); | |||
expect(screen.queryByRole('button')).not.toBeInTheDocument(); | |||
}); | |||
it('should render with tags and admin rights', () => { | |||
it('should allow to edit tags for a project', async () => { | |||
const user = userEvent.setup(); | |||
jest.mocked(searchProjectTags).mockResolvedValue({ tags: ['best', 'useless'] }); | |||
const onComponentChange = jest.fn(); | |||
const component = mockComponent({ | |||
key: 'my-second-project', | |||
tags: ['foo', 'bar'], | |||
@@ -47,37 +61,58 @@ it('should render with tags and admin rights', () => { | |||
name: 'MySecondProject', | |||
}); | |||
expect(shallowRender({ component })).toMatchSnapshot(); | |||
}); | |||
renderMetaTags({ component, onComponentChange }); | |||
expect(await screen.findByText('foo, bar')).toBeInTheDocument(); | |||
expect(screen.getByRole('button')).toBeInTheDocument(); | |||
await user.click(screen.getByRole('button', { name: 'tags_list_x.foo, bar' })); | |||
it('should set tags for a project', () => { | |||
const wrapper = shallowRender(); | |||
expect(await screen.findByText('best')).toBeInTheDocument(); | |||
wrapper.instance().handleSetProjectTags(['tag1', 'tag2']); | |||
await user.click(screen.getByText('best')); | |||
expect(onComponentChange).toHaveBeenCalledWith({ tags: ['foo', 'bar', 'best'] }); | |||
onComponentChange.mockClear(); | |||
/* | |||
* Since we're not actually updating the tags, we're back to having the foo, bar only | |||
*/ | |||
await user.click(screen.getByText('bar')); | |||
expect(onComponentChange).toHaveBeenCalledWith({ tags: ['foo'] }); | |||
expect(setProjectTags).toHaveBeenCalled(); | |||
expect(setApplicationTags).not.toHaveBeenCalled(); | |||
}); | |||
it('should set tags for an app', () => { | |||
const wrapper = shallowRender({ | |||
component: mockComponent({ qualifier: ComponentQualifier.Application }), | |||
it('should set tags for an app', async () => { | |||
const user = userEvent.setup(); | |||
renderMetaTags({ | |||
component: mockComponent({ | |||
configuration: { | |||
showSettings: true, | |||
}, | |||
qualifier: ComponentQualifier.Application, | |||
}), | |||
}); | |||
wrapper.instance().handleSetProjectTags(['tag1', 'tag2']); | |||
await user.click(screen.getByRole('button', { name: 'tags_list_x.no_tags' })); | |||
await user.click(screen.getByText('best')); | |||
expect(setProjectTags).not.toHaveBeenCalled(); | |||
expect(setApplicationTags).toHaveBeenCalled(); | |||
}); | |||
function shallowRender(overrides: Partial<MetaTags['props']> = {}) { | |||
function renderMetaTags(overrides: Partial<MetaTags['props']> = {}) { | |||
const component = mockComponent({ | |||
configuration: { | |||
showSettings: false, | |||
}, | |||
}); | |||
return shallow<MetaTags>( | |||
return renderComponent( | |||
<MetaTags component={component} onComponentChange={jest.fn()} {...overrides} /> | |||
); | |||
} |
@@ -1,57 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2023 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
/* eslint-disable import/first */ | |||
import { mount, shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import { searchProjectTags } from '../../../../../../../api/components'; | |||
import MetaTagsSelector from '../MetaTagsSelector'; | |||
jest.mock('../../../../../../../api/components', () => ({ | |||
searchProjectTags: jest.fn(), | |||
})); | |||
jest.mock('lodash', () => { | |||
const lodash = jest.requireActual('lodash'); | |||
lodash.debounce = jest.fn((fn) => fn); | |||
return lodash; | |||
}); | |||
it('searches tags on mount', () => { | |||
(searchProjectTags as jest.Mock).mockImplementation(() => | |||
Promise.resolve({ tags: ['foo', 'bar'] }) | |||
); | |||
mount(<MetaTagsSelector selectedTags={[]} setProjectTags={jest.fn()} />); | |||
expect(searchProjectTags).toHaveBeenCalledWith({ ps: 9, q: '' }); | |||
}); | |||
it('selects and deselects tags', () => { | |||
const setProjectTags = jest.fn(); | |||
const wrapper = shallow( | |||
<MetaTagsSelector selectedTags={['foo', 'bar']} setProjectTags={setProjectTags} /> | |||
); | |||
const tagSelect: any = wrapper.find('TagsSelector'); | |||
tagSelect.prop('onSelect')('baz'); | |||
expect(setProjectTags).toHaveBeenLastCalledWith(['foo', 'bar', 'baz']); | |||
// note that the `selectedTags` is a prop and so it wasn't changed | |||
tagSelect.prop('onUnselect')('bar'); | |||
expect(setProjectTags).toHaveBeenLastCalledWith(['foo']); | |||
}); |
@@ -1,87 +0,0 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should match snapshot: dangerous link, collapsed 1`] = ` | |||
<li> | |||
<a | |||
className="link-no-underline" | |||
onClick={[Function]} | |||
rel="nofollow noreferrer noopener" | |||
target="_blank" | |||
title="Dangerous" | |||
> | |||
<ProjectLinkIcon | |||
className="little-spacer-right" | |||
type="dangerous" | |||
/> | |||
Dangerous | |||
</a> | |||
</li> | |||
`; | |||
exports[`should match snapshot: dangerous link, expanded 1`] = ` | |||
<li> | |||
<a | |||
className="link-no-underline" | |||
onClick={[Function]} | |||
rel="nofollow noreferrer noopener" | |||
target="_blank" | |||
title="Dangerous" | |||
> | |||
<ProjectLinkIcon | |||
className="little-spacer-right" | |||
type="dangerous" | |||
/> | |||
Dangerous | |||
</a> | |||
<div | |||
className="little-spacer-top display-flex-center" | |||
> | |||
<input | |||
className="overview-key width-80" | |||
onClick={[Function]} | |||
readOnly={true} | |||
type="text" | |||
value="javascript:alert("hi")" | |||
/> | |||
<ClearButton | |||
className="little-spacer-left" | |||
onClick={[Function]} | |||
/> | |||
</div> | |||
</li> | |||
`; | |||
exports[`should match snapshot: default 1`] = ` | |||
<li> | |||
<a | |||
className="link-no-underline" | |||
href="http://example.com" | |||
rel="nofollow noreferrer noopener" | |||
target="_blank" | |||
title="Foo" | |||
> | |||
<ProjectLinkIcon | |||
className="little-spacer-right" | |||
type="foo" | |||
/> | |||
Foo | |||
</a> | |||
</li> | |||
`; | |||
exports[`should match snapshot: icon only 1`] = ` | |||
<li> | |||
<a | |||
className="link-no-underline" | |||
href="http://example.com" | |||
rel="nofollow noreferrer noopener" | |||
target="_blank" | |||
title="Foo" | |||
> | |||
<ProjectLinkIcon | |||
className="little-spacer-right" | |||
type="foo" | |||
/> | |||
</a> | |||
</li> | |||
`; |
@@ -1,130 +0,0 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render correctly 1`] = ` | |||
<Fragment> | |||
<h3> | |||
overview.quality_profiles | |||
</h3> | |||
<ul | |||
className="project-info-list" | |||
> | |||
<Tooltip | |||
key="js" | |||
overlay="overview.deleted_profile.name" | |||
> | |||
<li | |||
className="project-info-deleted-profile" | |||
> | |||
<div | |||
className="text-ellipsis" | |||
> | |||
<span | |||
className="spacer-right" | |||
> | |||
( | |||
js | |||
) | |||
</span> | |||
name | |||
</div> | |||
</li> | |||
</Tooltip> | |||
<li | |||
key="css" | |||
> | |||
<div | |||
className="text-ellipsis" | |||
> | |||
<span | |||
className="spacer-right" | |||
> | |||
( | |||
CSS | |||
) | |||
</span> | |||
<ForwardRef(Link) | |||
to={ | |||
{ | |||
"pathname": "/profiles/show", | |||
"search": "?name=name&language=css", | |||
} | |||
} | |||
> | |||
<span | |||
aria-label="overview.link_to_x_profile_y.CSS.name" | |||
> | |||
name | |||
</span> | |||
</ForwardRef(Link)> | |||
</div> | |||
</li> | |||
</ul> | |||
</Fragment> | |||
`; | |||
exports[`should render correctly 2`] = ` | |||
<Fragment> | |||
<h3> | |||
overview.quality_profiles | |||
</h3> | |||
<ul | |||
className="project-info-list" | |||
> | |||
<Tooltip | |||
key="js" | |||
overlay="overview.deleted_profile.name" | |||
> | |||
<li | |||
className="project-info-deleted-profile" | |||
> | |||
<div | |||
className="text-ellipsis" | |||
> | |||
<span | |||
className="spacer-right" | |||
> | |||
( | |||
js | |||
) | |||
</span> | |||
name | |||
</div> | |||
</li> | |||
</Tooltip> | |||
<Tooltip | |||
key="css" | |||
overlay="overview.deprecated_profile.10" | |||
> | |||
<li | |||
className="project-info-deprecated-rules" | |||
> | |||
<div | |||
className="text-ellipsis" | |||
> | |||
<span | |||
className="spacer-right" | |||
> | |||
( | |||
CSS | |||
) | |||
</span> | |||
<ForwardRef(Link) | |||
to={ | |||
{ | |||
"pathname": "/profiles/show", | |||
"search": "?name=name&language=css", | |||
} | |||
} | |||
> | |||
<span | |||
aria-label="overview.link_to_x_profile_y.CSS.name" | |||
> | |||
name | |||
</span> | |||
</ForwardRef(Link)> | |||
</div> | |||
</li> | |||
</Tooltip> | |||
</ul> | |||
</Fragment> | |||
`; |
@@ -1,102 +0,0 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render correctly: application 1`] = ` | |||
<Fragment> | |||
<div | |||
className="display-flex-row display-inline-flex-baseline" | |||
> | |||
<h3> | |||
metric.ncloc.name | |||
</h3> | |||
<span | |||
className="spacer-left small" | |||
> | |||
( | |||
project.info.main_branch | |||
) | |||
</span> | |||
</div> | |||
<div | |||
className="display-flex-center" | |||
> | |||
<DrilldownLink | |||
className="huge" | |||
component="my-project" | |||
metric="ncloc" | |||
> | |||
<span | |||
aria-label="project.info.see_more_info_on_x_locs.1.0" | |||
> | |||
1 | |||
</span> | |||
</DrilldownLink> | |||
<span | |||
className="spacer-left" | |||
> | |||
<SizeRating | |||
value={1} | |||
/> | |||
</span> | |||
<span | |||
className="huge-spacer-left display-inline-flex-center" | |||
> | |||
<DrilldownLink | |||
component="my-project" | |||
metric="projects" | |||
> | |||
<span | |||
className="big" | |||
> | |||
1 | |||
</span> | |||
</DrilldownLink> | |||
<span | |||
className="little-spacer-left text-muted" | |||
> | |||
metric.projects.name | |||
</span> | |||
</span> | |||
</div> | |||
</Fragment> | |||
`; | |||
exports[`should render correctly: project 1`] = ` | |||
<Fragment> | |||
<div | |||
className="display-flex-row display-inline-flex-baseline" | |||
> | |||
<h3> | |||
metric.ncloc.name | |||
</h3> | |||
<span | |||
className="spacer-left small" | |||
> | |||
( | |||
project.info.main_branch | |||
) | |||
</span> | |||
</div> | |||
<div | |||
className="display-flex-center" | |||
> | |||
<DrilldownLink | |||
className="huge" | |||
component="my-project" | |||
metric="ncloc" | |||
> | |||
<span | |||
aria-label="project.info.see_more_info_on_x_locs.1.0" | |||
> | |||
1 | |||
</span> | |||
</DrilldownLink> | |||
<span | |||
className="spacer-left" | |||
> | |||
<SizeRating | |||
value={1} | |||
/> | |||
</span> | |||
</div> | |||
</Fragment> | |||
`; |
@@ -1,55 +0,0 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render with tags and admin rights 1`] = ` | |||
<div | |||
className="big-spacer-top project-info-tags" | |||
> | |||
<Dropdown | |||
closeOnClick={false} | |||
closeOnClickOutside={true} | |||
overlay={ | |||
<MetaTagsSelector | |||
selectedTags={ | |||
[ | |||
"foo", | |||
"bar", | |||
] | |||
} | |||
setProjectTags={[Function]} | |||
/> | |||
} | |||
overlayPlacement="bottom-left" | |||
> | |||
<ButtonLink | |||
innerRef={[Function]} | |||
stopPropagation={true} | |||
> | |||
<TagsList | |||
allowUpdate={true} | |||
tags={ | |||
[ | |||
"foo", | |||
"bar", | |||
] | |||
} | |||
/> | |||
</ButtonLink> | |||
</Dropdown> | |||
</div> | |||
`; | |||
exports[`should render without tags and admin rights 1`] = ` | |||
<div | |||
className="big-spacer-top project-info-tags" | |||
> | |||
<TagsList | |||
allowUpdate={false} | |||
className="note" | |||
tags={ | |||
[ | |||
"no_tags", | |||
] | |||
} | |||
/> | |||
</div> | |||
`; |
@@ -17,83 +17,83 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { shallow } from 'enzyme'; | |||
import { screen } from '@testing-library/react'; | |||
import userEvent from '@testing-library/user-event'; | |||
import * as React from 'react'; | |||
import { getNotifications } from '../../../../../../../api/notifications'; | |||
import { mockComponent } from '../../../../../../../helpers/mocks/component'; | |||
import { mockNotification } from '../../../../../../../helpers/testMocks'; | |||
import { renderComponent } from '../../../../../../../helpers/testReactTestingUtils'; | |||
import { | |||
NotificationGlobalType, | |||
NotificationProjectType, | |||
} from '../../../../../../../types/notifications'; | |||
import { ProjectNotifications } from '../ProjectNotifications'; | |||
import ProjectNotifications from '../ProjectNotifications'; | |||
jest.mock('react', () => { | |||
return { | |||
...jest.requireActual('react'), | |||
useEffect: jest.fn().mockImplementation((f) => f()), | |||
useRef: jest.fn().mockReturnValue({ current: document.createElement('h3') }), | |||
}; | |||
}); | |||
jest.mock('../../../../../../../api/notifications', () => ({ | |||
addNotification: jest.fn().mockResolvedValue(undefined), | |||
removeNotification: jest.fn().mockResolvedValue(undefined), | |||
getNotifications: jest.fn(), | |||
})); | |||
it('should render correctly', () => { | |||
expect(shallowRender()).toMatchSnapshot(); | |||
beforeAll(() => { | |||
jest.mocked(getNotifications).mockResolvedValue({ | |||
channels: ['channel1'], | |||
globalTypes: [NotificationGlobalType.MyNewIssues], | |||
notifications: [ | |||
mockNotification({}), | |||
mockNotification({ type: NotificationProjectType.NewAlerts }), | |||
], | |||
perProjectTypes: [NotificationProjectType.NewAlerts, NotificationProjectType.NewIssues], | |||
}); | |||
}); | |||
it('should add and remove a notification for the project', () => { | |||
const addNotification = jest.fn(); | |||
const removeNotification = jest.fn(); | |||
const wrapper = shallowRender({ addNotification, removeNotification }); | |||
const notification = { | |||
channel: 'EmailNotificationChannel', | |||
type: 'SQ-MyNewIssues', | |||
}; | |||
it('should render correctly', async () => { | |||
const user = userEvent.setup(); | |||
renderProjectNotifications(); | |||
wrapper.find('NotificationsList').prop<Function>('onAdd')(notification); | |||
expect(addNotification).toHaveBeenCalledWith({ ...notification, project: 'foo' }); | |||
expect(await screen.findByText('notification.channel.channel1')).toBeInTheDocument(); | |||
expect( | |||
screen.getByLabelText( | |||
'notification.dispatcher.descrption_x.notification.dispatcher.NewAlerts.project' | |||
) | |||
).toBeChecked(); | |||
wrapper.find('NotificationsList').prop<Function>('onRemove')(notification); | |||
expect(removeNotification).toHaveBeenCalledWith({ ...notification, project: 'foo' }); | |||
}); | |||
expect( | |||
screen.getByLabelText( | |||
'notification.dispatcher.descrption_x.notification.dispatcher.NewIssues.project' | |||
) | |||
).not.toBeChecked(); | |||
// Toggle New Alerts | |||
await user.click( | |||
screen.getByLabelText( | |||
'notification.dispatcher.descrption_x.notification.dispatcher.NewAlerts.project' | |||
) | |||
); | |||
expect( | |||
screen.getByLabelText( | |||
'notification.dispatcher.descrption_x.notification.dispatcher.NewAlerts.project' | |||
) | |||
).not.toBeChecked(); | |||
it('should set focus on the heading when rendered', () => { | |||
const fakeElement = document.createElement('h3'); | |||
const focus = jest.fn(); | |||
(React.useRef as jest.Mock).mockReturnValueOnce({ current: { ...fakeElement, focus } }); | |||
// Toggle New Issues | |||
await user.click( | |||
screen.getByLabelText( | |||
'notification.dispatcher.descrption_x.notification.dispatcher.NewIssues.project' | |||
) | |||
); | |||
shallowRender(); | |||
expect(focus).toHaveBeenCalled(); | |||
expect( | |||
screen.getByLabelText( | |||
'notification.dispatcher.descrption_x.notification.dispatcher.NewIssues.project' | |||
) | |||
).toBeChecked(); | |||
}); | |||
function shallowRender(props = {}) { | |||
return shallow( | |||
<ProjectNotifications | |||
addNotification={jest.fn()} | |||
channels={['channel1', 'channel2']} | |||
component={mockComponent({ key: 'foo' })} | |||
globalTypes={[NotificationGlobalType.CeReportTaskFailure, NotificationGlobalType.NewAlerts]} | |||
loading={false} | |||
notifications={[ | |||
{ | |||
channel: 'channel1', | |||
type: 'type-global', | |||
project: 'foo', | |||
projectName: 'Foo', | |||
}, | |||
{ | |||
channel: 'channel1', | |||
type: 'type-common', | |||
project: 'bar', | |||
projectName: 'Bar', | |||
}, | |||
{ | |||
channel: 'channel2', | |||
type: 'type-common', | |||
project: 'qux', | |||
projectName: 'Qux', | |||
}, | |||
]} | |||
perProjectTypes={[NotificationProjectType.CeReportTaskFailure]} | |||
removeNotification={jest.fn()} | |||
{...props} | |||
/> | |||
function renderProjectNotifications() { | |||
return renderComponent( | |||
<ProjectNotifications component={mockComponent({ key: 'foo', name: 'Foo' })} /> | |||
); | |||
} |
@@ -1,76 +0,0 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render correctly 1`] = ` | |||
<Fragment> | |||
<h3 | |||
tabIndex={-1} | |||
> | |||
project.info.notifications | |||
</h3> | |||
<Alert | |||
aria-live="off" | |||
className="spacer-top" | |||
variant="info" | |||
> | |||
notification.dispatcher.information | |||
</Alert> | |||
<DeferredSpinner | |||
loading={false} | |||
> | |||
<table | |||
className="data zebra notifications-table" | |||
> | |||
<thead> | |||
<tr> | |||
<th | |||
aria-label="project" | |||
/> | |||
<th | |||
className="text-center" | |||
key="channel1" | |||
> | |||
<h4> | |||
notification.channel.channel1 | |||
</h4> | |||
</th> | |||
<th | |||
className="text-center" | |||
key="channel2" | |||
> | |||
<h4> | |||
notification.channel.channel2 | |||
</h4> | |||
</th> | |||
</tr> | |||
</thead> | |||
<NotificationsList | |||
channels={ | |||
[ | |||
"channel1", | |||
"channel2", | |||
] | |||
} | |||
checkboxId={[Function]} | |||
notifications={ | |||
[ | |||
{ | |||
"channel": "channel1", | |||
"project": "foo", | |||
"projectName": "Foo", | |||
"type": "type-global", | |||
}, | |||
] | |||
} | |||
onAdd={[Function]} | |||
onRemove={[Function]} | |||
project={true} | |||
types={ | |||
[ | |||
"CeReportTaskFailure", | |||
] | |||
} | |||
/> | |||
</table> | |||
</DeferredSpinner> | |||
</Fragment> | |||
`; |
@@ -27,6 +27,7 @@ import { RuleRepository } from '../types/coding-rules'; | |||
import { EditionKey } from '../types/editions'; | |||
import { IssueScope, IssueSeverity, IssueStatus, IssueType, RawIssue } from '../types/issues'; | |||
import { Language } from '../types/languages'; | |||
import { Notification } from '../types/notifications'; | |||
import { DumpStatus, DumpTask } from '../types/project-dump'; | |||
import { TaskStatuses } from '../types/tasks'; | |||
import { | |||
@@ -414,6 +415,16 @@ export function mockMeasureEnhanced(overrides: Partial<MeasureEnhanced> = {}): M | |||
}; | |||
} | |||
export function mockNotification(overrides: Partial<Notification> = {}): Notification { | |||
return { | |||
channel: 'channel1', | |||
type: 'type-global', | |||
project: 'foo', | |||
projectName: 'Foo', | |||
...overrides, | |||
}; | |||
} | |||
export function mockPeriod(overrides: Partial<Period> = {}): Period { | |||
return { | |||
date: '2019-04-23T02:12:32+0100', |