writing tests

Signed-off-by: Peter Macdonald <macdonald.peter90@gmail.com>
This commit is contained in:
Peter Macdonald
2023-05-30 14:58:21 +02:00
committed by blam
parent e7ce0cd7d6
commit 2af2bdfb90
2 changed files with 158 additions and 80 deletions
@@ -15,117 +15,191 @@
*/
import React from 'react';
import { render } from '@testing-library/react';
import { render, waitFor } from '@testing-library/react';
import { CatalogApi } from '@backstage/catalog-client';
import { FieldProps } from '@rjsf/core';
import { EntityPickerProps } from '../EntityPicker/schema';
import { Entity } from '@backstage/catalog-model';
import { OwnershipEntityRefPicker } from './OwnershipEntityRefPicker';
import { TestApiProvider } from '@backstage/test-utils';
import { catalogApiRef } from '@backstage/plugin-catalog-react';
import { Entity } from '@backstage/catalog-model';
import { IdentityApi, identityApiRef } from '@backstage/core-plugin-api';
import userEvent from '@testing-library/user-event';
const makeUserEntity = (
kind: string,
memberOf: string[],
name: string,
): Entity => ({
apiVersion: 'scaffolder.backstage.io/v1beta3',
kind,
metadata: { name },
spec: {
memberOf: memberOf,
},
});
const makeGroupEntity = (
kind: string,
members: string[],
name: string,
): Entity => ({
apiVersion: 'scaffolder.backstage.io/v1beta3',
kind,
metadata: { name },
spec: {
members: members,
},
});
// Create a mock IdentityApi
const mockIdentityApi: IdentityApi = {
getProfileInfo: () =>
Promise.resolve({
displayName: 'Bob',
email: 'bob@example.com',
picture: 'https://example.com/picture.jpg',
}),
getBackstageIdentity: () =>
Promise.resolve({
id: 'Bob',
idToken: 'token',
type: 'user',
userEntityRef: 'user:default/Bob',
ownershipEntityRefs: ['group:default/group1', 'group:default/group2'],
}),
getCredentials: () => Promise.resolve({ token: 'token' }),
signOut: () => Promise.resolve(),
};
describe('<OwnershipEntityRefPicker />', () => {
let entities: Entity[];
const onChange = jest.fn();
const schema = {};
const required = false;
let uiSchema: EntityPickerProps['uiSchema'];
const rawErrors: string[] = [];
const formData = undefined;
let props: FieldProps;
const catalogApi: jest.Mocked<CatalogApi> = {
getLocationById: jest.fn(),
getEntityByName: jest.fn(),
addLocation: jest.fn(),
getLocationByRef: jest.fn(),
removeEntityByUid: jest.fn(),
getEntities: jest.fn(async () => ({ items: entities })),
} as any;
beforeEach(() => {
entities = [
makeUserEntity('User', ['group1', 'group2'], 'Bob'),
makeGroupEntity('Group', ['Alice', 'Dave'], 'group3'),
{
apiVersion: 'backstage.io/v1alpha1',
kind: 'Group',
metadata: { name: 'group1' },
spec: { members: ['Bob'] },
},
{
apiVersion: 'backstage.io/v1alpha1',
kind: 'Group',
metadata: { name: 'group2' },
spec: { members: ['Bob'] },
},
{
apiVersion: 'backstage.io/v1alpha1',
kind: 'Group',
metadata: { name: 'group3' },
spec: { members: ['Alice'] },
},
];
onChange.mockClear();
catalogApi.getEntities.mockClear();
});
afterEach(() => jest.resetAllMocks());
afterEach(() => {
jest.resetAllMocks();
});
it('should only return the groups a user is part of', async () => {
catalogApi.getEntityByRef.mockResolvedValueOnce(entities[0]);
it('should only return the groups a user is part of and not the groups a user is not part of', async () => {
const userGroups = entities.filter(
entity =>
entity.spec &&
Array.isArray(entity.spec.members) &&
entity.spec.members.includes('Bob'),
);
uiSchema = { 'ui:options': { catalogApi } };
props = {
catalogApi.getEntities.mockResolvedValue({ items: userGroups });
const props = {
onChange,
schema,
required,
uiSchema,
rawErrors,
formData,
} as unknown as FieldProps<any>;
render(<OwnershipEntityRefPicker {...props} />);
render(
<TestApiProvider
apis={[
[identityApiRef, mockIdentityApi],
[catalogApiRef, catalogApi],
]}
>
<OwnershipEntityRefPicker {...props} />
</TestApiProvider>,
);
expect(onChange).toHaveBeenCalledWith(['group1', 'group2']);
await waitFor(() =>
expect(catalogApi.getEntities).toHaveBeenCalledTimes(1),
);
expect(catalogApi.getEntities).toHaveBeenCalledWith({
filter: {
kind: ['Group'],
'relations.hasMember': ['group:default/group1', 'group:default/group2'],
},
});
// Check that getEntities was set up to return the correct data
await expect(catalogApi.getEntities.mock.results[0].value).resolves.toEqual(
{
items: [
{
apiVersion: 'backstage.io/v1alpha1',
kind: 'Group',
metadata: { name: 'group1' },
spec: { members: ['Bob'] },
},
{
apiVersion: 'backstage.io/v1alpha1',
kind: 'Group',
metadata: { name: 'group2' },
spec: { members: ['Bob'] },
},
],
},
);
await expect(
catalogApi.getEntities.mock.results[0].value,
).resolves.not.toEqual(
expect.objectContaining({
items: expect.arrayContaining([
expect.objectContaining({
metadata: { name: 'group3' },
}),
]),
}),
);
});
it('should not return groups a user is not part of', async () => {
// Mock 'getEntityByRef' to return the entity that represents Bob
catalogApi.getEntityByRef.mockResolvedValueOnce(entities[0]);
it('should display the groups a user is part of and not display the groups a user is not part of', async () => {
const userGroups = entities.filter(
entity =>
entity.spec &&
Array.isArray(entity.spec.members) &&
entity.spec.members.includes('Bob'),
);
catalogApi.getEntities.mockResolvedValue({ items: userGroups });
uiSchema = { 'ui:options': { catalogApi } };
props = {
const props = {
onChange,
schema,
required,
uiSchema,
rawErrors,
formData,
} as unknown as FieldProps<any>;
render(<OwnershipEntityRefPicker {...props} />);
const { queryByText, getByRole } = render(
<TestApiProvider
apis={[
[identityApiRef, mockIdentityApi],
[catalogApiRef, catalogApi],
]}
>
<OwnershipEntityRefPicker {...props} />
</TestApiProvider>,
);
// The onChange should not have been called with 'group3', since Bob is not part of 'group3'
expect(onChange).not.toHaveBeenCalledWith(['group3']);
});
await waitFor(() =>
expect(catalogApi.getEntities).toHaveBeenCalledTimes(1),
);
it('should render without imploding', () => {
uiSchema = { 'ui:options': { catalogApi } };
props = {
onChange,
schema,
required,
uiSchema,
rawErrors,
formData,
} as unknown as FieldProps<any>;
// Simulate user input
const inputField = getByRole('combobox');
userEvent.click(inputField);
userEvent.type(inputField, 'group');
const { container } = render(<OwnershipEntityRefPicker {...props} />);
expect(container).not.toBeNull();
// Wait for the dropdown elements to appear
await waitFor(() => {
const group1Element = queryByText('group1');
const group2Element = queryByText('group2');
expect(group1Element).toBeInTheDocument();
expect(group2Element).toBeInTheDocument();
});
// Assert that 'group3' is not rendered in the component
expect(queryByText('group3')).not.toBeInTheDocument();
});
});
@@ -30,12 +30,16 @@ export { OwnershipEntityRefPickerSchema };
export const OwnershipEntityRefPicker = (
props: OwnershipEntityRefPickerProps,
) => {
const { uiSchema, required, rawErrors, formData } = props;
const {
schema: { title, description },
required,
rawErrors,
} = props;
const identityApi = useApi(identityApiRef);
const catalogApi = useApi(catalogApiRef);
const [groups, setGroups] = useState<string[]>([]);
const [selectedGroup, setSelectedGroup] = useState('');
const [selectedGroup, setSelectedGroup] = useState<string | null>(null);
useEffect(() => {
const fetchUserGroups = async () => {
@@ -63,20 +67,20 @@ export const OwnershipEntityRefPicker = (
<FormControl
margin="normal"
required={required}
error={rawErrors?.length > 0 && !formData}
error={rawErrors?.length > 0}
>
<Autocomplete
id="OwnershipEntityRefPicker-dropdown"
options={groups || []}
value={selectedGroup || null}
value={selectedGroup}
onChange={(_, value) => setSelectedGroup(value || '')}
getOptionLabel={group => group}
renderInput={params => (
<TextField
{...params}
label={uiSchema['ui:options']?.title}
label={title}
margin="dense"
helperText={uiSchema['ui:options']?.description}
helperText={description}
FormHelperTextProps={{ margin: 'dense', style: { marginLeft: 0 } }}
variant="outlined"
required={required}