feat: adding a new TemplateListPage and CategoryPicker with tests

Signed-off-by: blam <ben@blam.sh>
This commit is contained in:
blam
2022-02-04 15:43:20 +01:00
parent 06ce6d19d5
commit 8afeb82cc3
4 changed files with 348 additions and 0 deletions
@@ -0,0 +1,130 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { useEntityTypeFilter } from '@backstage/plugin-catalog-react';
import { CategoryPicker } from './CategoryPicker';
import { renderInTestApp, TestApiProvider } from '@backstage/test-utils';
import { alertApiRef } from '@backstage/core-plugin-api';
import { fireEvent } from '@testing-library/react';
jest.mock('@backstage/plugin-catalog-react', () => ({
useEntityTypeFilter: jest.fn(),
}));
describe('CategoryPicker', () => {
const mockAlertApi = { post: jest.fn() };
beforeEach(() => {
mockAlertApi.post.mockClear();
});
it('should post the error to errorApi if an errors is returned', async () => {
(useEntityTypeFilter as jest.Mock).mockReturnValue({
error: new Error('something broked'),
});
await renderInTestApp(
<TestApiProvider apis={[[alertApiRef, mockAlertApi]]}>
<CategoryPicker />
</TestApiProvider>,
);
expect(mockAlertApi.post).toHaveBeenCalledWith({
message: expect.stringContaining('something broked'),
severity: 'error',
});
});
it('should render loading if the hook is loading', async () => {
(useEntityTypeFilter as jest.Mock).mockReturnValue({
loading: true,
});
const { findByTestId } = await renderInTestApp(
<TestApiProvider apis={[[alertApiRef, mockAlertApi]]}>
<CategoryPicker />
</TestApiProvider>,
);
expect(await findByTestId('progress')).toBeInTheDocument();
});
it('should not render if there is no available types', async () => {
(useEntityTypeFilter as jest.Mock).mockReturnValue({
availableTypes: null,
});
const { queryByText } = await renderInTestApp(
<TestApiProvider apis={[[alertApiRef, mockAlertApi]]}>
<CategoryPicker />
</TestApiProvider>,
);
expect(queryByText('Categories')).not.toBeInTheDocument();
});
it('renders the autocomplete with the availableTypes', async () => {
const mockAvailableTypes = ['foo', 'bar'];
(useEntityTypeFilter as jest.Mock).mockReturnValue({
availableTypes: mockAvailableTypes,
});
const { getByRole } = await renderInTestApp(
<TestApiProvider apis={[[alertApiRef, mockAlertApi]]}>
<CategoryPicker />
</TestApiProvider>,
);
const openButton = getByRole('button', { name: 'Open' });
openButton.click();
expect(getByRole('checkbox', { name: 'Foo' })).toBeInTheDocument();
expect(getByRole('checkbox', { name: 'Bar' })).toBeInTheDocument();
});
it('should call setSelectedTypes when one of the options are called', async () => {
const mockAvailableTypes = ['foo', 'bar'];
const mockSetSelectedTypes = jest.fn();
(useEntityTypeFilter as jest.Mock).mockReturnValue({
availableTypes: mockAvailableTypes,
setSelectedTypes: mockSetSelectedTypes,
});
const { getByRole } = await renderInTestApp(
<TestApiProvider apis={[[alertApiRef, mockAlertApi]]}>
<CategoryPicker />
</TestApiProvider>,
);
const openButton = getByRole('button', { name: 'Open' });
await fireEvent(openButton, new MouseEvent('click', { bubbles: true }));
const fooCheckbox = getByRole('checkbox', { name: 'Foo' });
await fireEvent(fooCheckbox, new MouseEvent('click', { bubbles: true }));
expect(mockSetSelectedTypes).toHaveBeenCalledWith(['foo']);
await fireEvent(openButton, new MouseEvent('click', { bubbles: true }));
const barCheckbox = getByRole('checkbox', { name: 'Bar' });
await fireEvent(barCheckbox, new MouseEvent('click', { bubbles: true }));
expect(mockSetSelectedTypes).toHaveBeenCalledWith(['foo', 'bar']);
});
});
@@ -0,0 +1,81 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import capitalize from 'lodash/capitalize';
import { Progress } from '@backstage/core-components';
import {
Box,
Checkbox,
FormControlLabel,
TextField,
Typography,
} from '@material-ui/core';
import CheckBoxIcon from '@material-ui/icons/CheckBox';
import CheckBoxOutlineBlankIcon from '@material-ui/icons/CheckBoxOutlineBlank';
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
import { Autocomplete } from '@material-ui/lab';
import { useEntityTypeFilter } from '@backstage/plugin-catalog-react';
import { alertApiRef, useApi } from '@backstage/core-plugin-api';
const icon = <CheckBoxOutlineBlankIcon fontSize="small" />;
const checkedIcon = <CheckBoxIcon fontSize="small" />;
export const CategoryPicker = () => {
const alertApi = useApi(alertApiRef);
const { error, loading, availableTypes, selectedTypes, setSelectedTypes } =
useEntityTypeFilter();
if (loading) return <Progress />;
if (error) {
alertApi.post({
message: `Failed to load entity types with error: ${error}`,
severity: 'error',
});
return null;
}
if (!availableTypes) return null;
return (
<Box pb={1} pt={1}>
<Typography variant="button">Categories</Typography>
<Autocomplete
multiple
aria-label="Categories"
options={availableTypes}
value={selectedTypes}
onChange={(_: object, value: string[]) => setSelectedTypes(value)}
renderOption={(option, { selected }) => (
<FormControlLabel
control={
<Checkbox
icon={icon}
checkedIcon={checkedIcon}
checked={selected}
/>
}
label={capitalize(option)}
/>
)}
size="small"
popupIcon={<ExpandMoreIcon />}
renderInput={params => <TextField {...params} variant="outlined" />}
/>
</Box>
);
};
@@ -0,0 +1,121 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common';
import { Entity } from '@backstage/catalog-model';
import { makeStyles } from '@material-ui/core';
import {
Content,
ContentHeader,
CreateButton,
Header,
Lifecycle,
Page,
SupportButton,
} from '@backstage/core-components';
import {
EntityKindPicker,
EntityListProvider,
EntitySearchBar,
EntityTagPicker,
UserListPicker,
} from '@backstage/plugin-catalog-react';
import { CategoryPicker } from './CategoryPicker';
export type TemplateListGroup = {
title?: string;
titleComponent?: React.ReactNode;
filter: (entity: Entity) => boolean;
};
export type TemplateListPageProps = {
TemplateCardComponent?: React.ComponentType<{
template: TemplateEntityV1beta3;
}>;
groups?: TemplateListGroup[];
};
const useStyles = makeStyles(theme => ({
contentWrapper: {
display: 'grid',
gridTemplateAreas: "'filters' 'grid'",
gridTemplateColumns: '250px 1fr',
gridColumnGap: theme.spacing(2),
},
}));
export const TemplateListPage = (props: TemplateListPageProps) => {
const styles = useStyles();
return (
<EntityListProvider>
<Page themeId="home">
<Header
pageTitleOverride="Create a New Component"
title={
<>
Create a New Component <Lifecycle shorthand />
</>
}
subtitle="Create new software components using standard templates"
/>
<Content>
<ContentHeader title="Available Templates">
{/* {allowed && (
<CreateButton
title="Register Existing Component"
to={registerComponentLink && registerComponentLink()}
/>
)} */}
<SupportButton>
Create new software components using standard templates. Different
templates create different kinds of components (services,
websites, documentation, ...).
</SupportButton>
</ContentHeader>
<div className={styles.contentWrapper}>
<div>
<EntitySearchBar />
<EntityKindPicker initialFilter="template" hidden />
<UserListPicker
initialFilter="all"
availableFilters={['all', 'starred']}
/>
<CategoryPicker />
<EntityTagPicker />
</div>
<div>
{/* {groups &&
groups.map((group, index) => (
<TemplateList
key={index}
TemplateCardComponent={TemplateCardComponent}
group={group}
/>
))}
<TemplateList
key="other"
TemplateCardComponent={TemplateCardComponent}
group={otherTemplatesGroup}
/>*/}
</div>
</div>
</Content>
</Page>
</EntityListProvider>
);
};
@@ -0,0 +1,16 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { TemplateListPage } from './TemplateListPage';