getDuplications,
getSources,
getTree,
+ setApplicationTags,
+ setProjectTags,
} from '../components';
import {
ComponentTree,
jest.mocked(changeKey).mockImplementation(this.handleChangeKey);
jest.mocked(getComponentLeaves).mockImplementation(this.handleGetComponentLeaves);
jest.mocked(getBreadcrumbs).mockImplementation(this.handleGetBreadcrumbs);
+ jest.mocked(setProjectTags).mockImplementation(this.handleSetProjectTags);
+ jest.mocked(setApplicationTags).mockImplementation(this.handleSetApplicationTags);
}
findComponentTree = (key: string, from?: ComponentTree) => {
return this.reply([...(base.ancestors as ComponentRaw[]), base.component as ComponentRaw]);
};
- reply<T>(response: T): Promise<T> {
- return Promise.resolve(cloneDeep(response));
+ handleSetProjectTags: typeof setProjectTags = ({ project, tags }) => {
+ const base = this.findComponentTree(project);
+ if (base !== undefined) {
+ base.component.tags = tags.split(',');
+ }
+ return this.reply();
+ };
+
+ handleSetApplicationTags: typeof setApplicationTags = ({ application, tags }) => {
+ const base = this.findComponentTree(application);
+ if (base !== undefined) {
+ base.component.tags = tags.split(',');
+ }
+ return this.reply();
+ };
+
+ reply<T>(): Promise<void>;
+ reply<T>(response: T): Promise<T>;
+ reply<T>(response?: T): Promise<T | void> {
+ return Promise.resolve(response ? cloneDeep(response) : undefined);
}
}
return this.#measures;
};
+ setComponents = (components: ComponentTree) => {
+ this.#components = components;
+ };
+
findComponentTree = (key: string, from?: ComponentTree): ComponentTree => {
const recurse = (node: ComponentTree): ComponentTree | undefined => {
if (node.component.key === key) {
} from '../../types/notifications';
import { addNotification, getNotifications, removeNotification } from '../notifications';
+jest.mock('../notifications');
+
/* Constants */
const channels = ['EmailNotificationChannel'];
const defaultNotifications: Notification[] = [
--- /dev/null
+/*
+ * 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 { getProjectBadgesToken, renewProjectBadgesToken } from '../project-badges';
+
+jest.mock('../project-badges');
+jest.mock('../project-badges');
+
+const defaultToken = 'sqb_2b5052cef8eac91a921ac71be9227a27f6b6b38b';
+
+export class ProjectBadgesServiceMock {
+ token: string;
+
+ constructor() {
+ this.token = defaultToken;
+
+ jest.mocked(getProjectBadgesToken).mockImplementation(this.handleGetProjectBadgesToken);
+ jest.mocked(renewProjectBadgesToken).mockImplementation(this.handleRenewProjectBadgesToken);
+ }
+
+ handleGetProjectBadgesToken = () => {
+ return Promise.resolve(this.token);
+ };
+
+ handleRenewProjectBadgesToken = () => {
+ const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
+ this.token =
+ 'sqb-' +
+ new Array(40)
+ .fill(null)
+ .map(() => chars.charAt(Math.floor(Math.random() * chars.length)))
+ .join('');
+
+ return Promise.resolve(this.token);
+ };
+
+ reset = () => {
+ this.token = defaultToken;
+ };
+}
import { ProjectLink } from '../../types/types';
import { createLink, deleteLink, getProjectLinks } from '../projectLinks';
+jest.mock('../projectLinks');
+
export default class ProjectLinksServiceMock {
projectLinks: ProjectLink[] = [];
idCounter: number = 0;
import routes from '../routes';
jest.mock('../../../api/user-tokens');
-jest.mock('../../../api/notifications');
jest.mock('../../../helpers/preferences', () => ({
getKeyboardShortcutEnabled: jest.fn().mockResolvedValue(true),
measures?: Measure[];
}
-export class ProjectInformationApp extends React.PureComponent<Props, State> {
+class ProjectInformationApp extends React.PureComponent<Props, State> {
mounted = false;
state: State = {};
* 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 } from '@testing-library/react';
+import BranchesServiceMock from '../../../api/mocks/BranchesServiceMock';
+import CodingRulesServiceMock from '../../../api/mocks/CodingRulesServiceMock';
import ComponentsServiceMock from '../../../api/mocks/ComponentsServiceMock';
+import { MeasuresServiceMock } from '../../../api/mocks/MeasuresServiceMock';
+import NotificationsMock from '../../../api/mocks/NotificationsMock';
+import { ProjectBadgesServiceMock } from '../../../api/mocks/ProjectBadgesServiceMock';
+import ProjectLinksServiceMock from '../../../api/mocks/ProjectLinksServiceMock';
+import { mockComponent } from '../../../helpers/mocks/component';
+import { mockCurrentUser, mockLoggedInUser, mockMeasure } from '../../../helpers/testMocks';
import { renderAppWithComponentContext } from '../../../helpers/testReactTestingUtils';
import { byRole } from '../../../helpers/testSelector';
+import { ComponentQualifier, Visibility } from '../../../types/component';
+import { MetricKey } from '../../../types/metrics';
+import { Component } from '../../../types/types';
+import { CurrentUser } from '../../../types/users';
import routes from '../routes';
+jest.mock('../../../api/rules');
+jest.mock('../../../api/issues');
+jest.mock('../../../api/quality-profiles');
+jest.mock('../../../api/users');
+jest.mock('../../../api/web-api', () => ({
+ fetchWebApi: () => Promise.resolve([]),
+}));
+
const componentsMock = new ComponentsServiceMock();
+const measuresHandler = new MeasuresServiceMock();
+const linksHandler = new ProjectLinksServiceMock();
+const rulesHandler = new CodingRulesServiceMock();
+const badgesHandler = new ProjectBadgesServiceMock();
+const notificationsHandler = new NotificationsMock();
+const branchesHandler = new BranchesServiceMock();
const ui = {
projectPageTitle: byRole('heading', { name: 'project.info.title' }),
applicationPageTitle: byRole('heading', { name: 'application.info.title' }),
qualityGateList: byRole('list', { name: 'project.info.quality_gate' }),
- qualityProfilesList: byRole('list', { name: 'project.info.qualit_profiles' }),
+ qualityProfilesList: byRole('list', { name: 'overview.quality_profiles' }),
+ externalLinksList: byRole('list', { name: 'overview.external_links' }),
link: byRole('link'),
tags: byRole('generic', { name: /tags:/ }),
size: byRole('link', { name: /project.info.see_more_info_on_x_locs/ }),
newKeyInput: byRole('textbox'),
updateInputButton: byRole('button', { name: 'update_verb' }),
resetInputButton: byRole('button', { name: 'reset_verb' }),
+ projectHomeCheckbox: byRole('checkbox', { name: 'project.info.make_home.label' }),
+ applicationHomeCheckbox: byRole('checkbox', { name: 'application.info.make_home.label' }),
};
afterEach(() => {
componentsMock.reset();
+ measuresHandler.reset();
+ linksHandler.reset();
+ rulesHandler.reset();
+ badgesHandler.reset();
+ notificationsHandler.reset();
+ branchesHandler.reset();
+});
+
+it('should show fields for project', async () => {
+ measuresHandler.registerComponentMeasures({
+ 'my-project': { [MetricKey.ncloc]: mockMeasure({ metric: MetricKey.ncloc, value: '1000' }) },
+ });
+ linksHandler.projectLinks = [{ id: '1', name: 'test', type: '', url: 'http://test.com' }];
+ renderProjectInformationApp(
+ {
+ visibility: Visibility.Private,
+ description: 'Test description',
+ tags: ['bar'],
+ },
+ mockLoggedInUser()
+ );
+ expect(await ui.projectPageTitle.find()).toBeInTheDocument();
+ expect(ui.qualityGateList.get()).toBeInTheDocument();
+ expect(ui.link.getAll(ui.qualityGateList.get())).toHaveLength(1);
+ expect(ui.link.getAll(ui.qualityProfilesList.get())).toHaveLength(1);
+ expect(ui.link.getAll(ui.externalLinksList.get())).toHaveLength(1);
+ expect(screen.getByText('Test description')).toBeInTheDocument();
+ expect(screen.getByText('my-project')).toBeInTheDocument();
+ expect(screen.getByText('visibility.private')).toBeInTheDocument();
+ expect(ui.tags.get()).toHaveTextContent('bar');
+ expect(ui.size.get()).toHaveTextContent('1short_number_suffix.k');
+ expect(ui.projectHomeCheckbox.get()).toBeInTheDocument();
+ expect(ui.applicationHomeCheckbox.query()).not.toBeInTheDocument();
+});
+
+it('should show application fields', async () => {
+ measuresHandler.registerComponentMeasures({
+ 'my-project': {
+ [MetricKey.ncloc]: mockMeasure({ metric: MetricKey.ncloc, value: '1000' }),
+ [MetricKey.projects]: mockMeasure({ metric: MetricKey.projects, value: '2' }),
+ },
+ });
+ renderProjectInformationApp(
+ {
+ qualifier: ComponentQualifier.Application,
+ visibility: Visibility.Private,
+ description: 'Test description',
+ tags: ['bar'],
+ },
+ mockLoggedInUser()
+ );
+ expect(await ui.applicationPageTitle.find()).toBeInTheDocument();
+ expect(ui.qualityGateList.query()).not.toBeInTheDocument();
+ expect(ui.qualityProfilesList.query()).not.toBeInTheDocument();
+ expect(ui.externalLinksList.query()).not.toBeInTheDocument();
+ expect(screen.getByText('Test description')).toBeInTheDocument();
+ expect(screen.getByText('my-project')).toBeInTheDocument();
+ expect(screen.getByText('visibility.private')).toBeInTheDocument();
+ expect(ui.tags.get()).toHaveTextContent('bar');
+ expect(ui.size.get()).toHaveTextContent('1short_number_suffix.k');
+ expect(screen.getByRole('link', { name: '2' })).toBeInTheDocument();
+ expect(ui.applicationHomeCheckbox.get()).toBeInTheDocument();
+ expect(ui.projectHomeCheckbox.query()).not.toBeInTheDocument();
+});
+
+it('should hide some fields for application', async () => {
+ renderProjectInformationApp({
+ qualifier: ComponentQualifier.Application,
+ });
+ expect(await ui.applicationPageTitle.find()).toBeInTheDocument();
+ expect(screen.getByText('application.info.empty_description')).toBeInTheDocument();
+ expect(screen.queryByText(/visibility/)).not.toBeInTheDocument();
+ expect(ui.tags.get()).toHaveTextContent('no_tags');
+ expect(ui.applicationHomeCheckbox.query()).not.toBeInTheDocument();
+});
+
+it('should not show field that is not configured', async () => {
+ renderProjectInformationApp({
+ qualityGate: undefined,
+ qualityProfiles: [],
+ });
+ expect(await ui.projectPageTitle.find()).toBeInTheDocument();
+ expect(ui.qualityGateList.query()).not.toBeInTheDocument();
+ expect(ui.qualityProfilesList.query()).not.toBeInTheDocument();
+ expect(screen.queryByText(/visibility/)).not.toBeInTheDocument();
+ expect(ui.tags.get()).toHaveTextContent('no_tags');
+ expect(screen.getByText('project.info.empty_description')).toBeInTheDocument();
+ expect(ui.projectHomeCheckbox.query()).not.toBeInTheDocument();
});
-it('can update project key', async () => {
- renderProjectInformationApp();
+it('should hide visibility if public', async () => {
+ renderProjectInformationApp({
+ visibility: Visibility.Public,
+ qualityGate: undefined,
+ qualityProfiles: [],
+ });
expect(await ui.projectPageTitle.find()).toBeInTheDocument();
+ expect(ui.qualityGateList.query()).not.toBeInTheDocument();
+ expect(ui.qualityProfilesList.query()).not.toBeInTheDocument();
+ expect(screen.queryByText(/visibility/)).not.toBeInTheDocument();
+ expect(ui.tags.get()).toHaveTextContent('no_tags');
+ expect(screen.getByText('project.info.empty_description')).toBeInTheDocument();
+ expect(ui.projectHomeCheckbox.query()).not.toBeInTheDocument();
});
-function renderProjectInformationApp() {
+function renderProjectInformationApp(
+ overrides: Partial<Component> = {},
+ currentUser: CurrentUser = mockCurrentUser()
+) {
+ const component = mockComponent(overrides);
+ componentsMock.registerComponent(component, [componentsMock.components[0].component]);
+ measuresHandler.setComponents({ component, ancestors: [], children: [] });
return renderAppWithComponentContext(
'project/information',
routes,
- {},
- { component: componentsMock.components[0].component }
+ { currentUser },
+ { component }
);
}
<MetaTags component={component} onComponentChange={props.onComponentChange} />
</ProjectInformationSection>
- <ProjectInformationSection>
+ <ProjectInformationSection last={!isLoggedIn(currentUser) && (isApp || !links?.length)}>
<MetaSize component={component} measures={measures} />
</ProjectInformationSection>
const { children, className, last = false } = props;
return (
<>
- <div className={classNames('sw-py-6', className)}>{children}</div>
+ <div className={classNames('sw-py-4', className)}>{children}</div>
{!last && <BasicSeparator />}
</>
);
return (
<>
<h3>{translate('project.info.description')}</h3>
- {description ? (
- <p className="it__project-description">{description}</p>
- ) : (
- <TextMuted text={translate(isApp ? 'application' : 'project', 'info.empty_description')} />
- )}
+ <TextMuted
+ className="it__project-description"
+ text={description ?? translate(isApp ? 'application' : 'project', 'info.empty_description')}
+ />
</>
);
}
const { updateCurrentUserHomepage } = useContext(CurrentUserContext);
const currentPage: HomePage = {
component: componentKey,
- type: 'PROJECT',
+ type: isApp ? 'APPLICATION' : 'PROJECT',
branch: undefined,
};
return (
<>
- <h3>{translate('overview.external_links')}</h3>
- <ul className="project-info-list">
+ <h3 id="external-links">{translate('overview.external_links')}</h3>
+ <ul className="project-info-list" aria-labelledby="external-links">
{orderedLinks.map((link) => (
<MetaLink miui key={link.id} link={link} />
))}
<Tags
allowUpdate={canUpdateTags()}
ariaTagsListLabel={translate('tags')}
- className="js-issue-edit-tags"
+ className="project-info-tags"
emptyText={translate('no_tags')}
overlay={<MetaTagsSelector selectedTags={tags} setProjectTags={handleSetProjectTags} />}
popupPlacement={PopupPlacement.Bottom}
--- /dev/null
+/*
+ * 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 { act } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import * as React from 'react';
+import { setHomePage } from '../../../../../api/users';
+import { CurrentUserContext } from '../../../../../app/components/current-user/CurrentUserContext';
+import { mockLoggedInUser } from '../../../../../helpers/testMocks';
+import { renderComponent } from '../../../../../helpers/testReactTestingUtils';
+import { byRole } from '../../../../../helpers/testSelector';
+import { LoggedInUser, isLoggedIn } from '../../../../../types/users';
+import MetaHome from '../MetaHome';
+
+jest.mock('../../../../../api/users', () => ({
+ setHomePage: jest.fn().mockImplementation(() => Promise.resolve()),
+}));
+
+beforeEach(() => {
+ jest.clearAllMocks();
+});
+
+const ui = {
+ checkbox: byRole('checkbox', { name: /info.make_home.label/ }),
+};
+
+it('should update homepage for project', async () => {
+ const user = userEvent.setup();
+ renderMetaHome(
+ mockLoggedInUser({
+ homepage: { component: 'test-project', type: 'PROJECT', branch: undefined },
+ })
+ );
+
+ expect(await ui.checkbox.find()).toBeInTheDocument();
+ expect(ui.checkbox.get()).not.toBeChecked();
+ await act(() => user.click(ui.checkbox.get()));
+ expect(ui.checkbox.get()).toBeChecked();
+ expect(jest.mocked(setHomePage)).toHaveBeenCalledWith({
+ component: 'my-project',
+ type: 'PROJECT',
+ branch: undefined,
+ });
+ await act(() => user.click(ui.checkbox.get()));
+ expect(jest.mocked(setHomePage)).toHaveBeenCalledWith({ type: 'PROJECTS' });
+ expect(await ui.checkbox.find()).not.toBeChecked();
+});
+
+it('should update homepage for application', async () => {
+ const user = userEvent.setup();
+ renderMetaHome(
+ mockLoggedInUser({
+ homepage: { component: 'test-project', type: 'PROJECT', branch: undefined },
+ }),
+ 'test',
+ true
+ );
+
+ expect(await ui.checkbox.find()).toBeInTheDocument();
+ expect(ui.checkbox.get()).not.toBeChecked();
+ await act(() => user.click(ui.checkbox.get()));
+ expect(ui.checkbox.get()).toBeChecked();
+ expect(jest.mocked(setHomePage)).toHaveBeenCalledWith({
+ component: 'test',
+ type: 'APPLICATION',
+ branch: undefined,
+ });
+});
+
+function TestComponent({
+ user,
+ componentKey,
+ isApp,
+}: {
+ user: LoggedInUser;
+ componentKey: string;
+ isApp?: boolean;
+}) {
+ const [currentUser, setCurrentUser] = React.useState(user);
+ return (
+ <CurrentUserContext.Provider
+ value={{
+ currentUser,
+ updateCurrentUserHomepage: (homepage) => {
+ setCurrentUser({ ...currentUser, homepage });
+ },
+ updateDismissedNotices: () => {},
+ }}
+ >
+ <CurrentUserContext.Consumer>
+ {({ currentUser }) =>
+ isLoggedIn(currentUser) ? (
+ <MetaHome componentKey={componentKey} currentUser={currentUser} isApp={isApp} />
+ ) : null
+ }
+ </CurrentUserContext.Consumer>
+ </CurrentUserContext.Provider>
+ );
+}
+
+function renderMetaHome(
+ currentUser: LoggedInUser,
+ componentKey: string = 'my-project',
+ isApp?: boolean
+) {
+ return renderComponent(
+ <TestComponent user={currentUser} componentKey={componentKey} isApp={isApp} />
+ );
+}
expect(setProjectTags).toHaveBeenCalled();
expect(setApplicationTags).not.toHaveBeenCalled();
+ await user.click(document.body);
+ expect(screen.queryByRole('checkbox')).not.toBeInTheDocument();
});
it('should set tags for an app', async () => {
import { byRole, byText } from '../../../helpers/testSelector';
import ProjectLinksApp from '../ProjectLinksApp';
-jest.mock('../../../api/projectLinks');
-
const componentsMock = new ProjectLinksServiceMock();
afterEach(() => {
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { LinkExternalIcon } from '@primer/octicons-react';
-import { HomeIcon } from 'design-system';
-import * as React from 'react';
+import {
+ FileIcon,
+ HomeIcon,
+ LinkExternalIcon,
+ IconProps as MIUIIconProps,
+ PulseIcon,
+ SyncIcon,
+} from '@primer/octicons-react';
+import React, { FC } from 'react';
import BugTrackerIcon from './BugTrackerIcon';
import ContinuousIntegrationIcon from './ContinuousIntegrationIcon';
import DetachIcon from './DetachIcon';
type,
...iconProps
}: IconProps & ProjectLinkIconProps) {
- const getIcon = (): any => {
+ const getIcon = (): FC<IconProps | MIUIIconProps> => {
switch (type) {
case 'issue':
- return BugTrackerIcon;
+ return miui ? PulseIcon : BugTrackerIcon;
case 'homepage':
return miui ? HomeIcon : HouseIcon;
case 'ci':
- return ContinuousIntegrationIcon;
+ return miui ? SyncIcon : ContinuousIntegrationIcon;
case 'scm':
- return SCMIcon;
+ return miui ? FileIcon : SCMIcon;
default:
return miui ? LinkExternalIcon : DetachIcon;
}