test: unit tests for the MultiEntityPicker
Signed-off-by: Marc Rooding <marc@mrooding.me>
This commit is contained in:
+693
@@ -0,0 +1,693 @@
|
||||
/*
|
||||
* Copyright 2020 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 { CATALOG_FILTER_EXISTS } from '@backstage/catalog-client';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog-react';
|
||||
import { renderInTestApp, TestApiProvider } from '@backstage/test-utils';
|
||||
import { FieldProps } from '@rjsf/core';
|
||||
import { fireEvent, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { MultiEntityPicker } from './MultiEntityPicker';
|
||||
import { MultiEntityPickerProps } from './schema';
|
||||
|
||||
const makeEntity = (kind: string, namespace: string, name: string): Entity => ({
|
||||
apiVersion: 'scaffolder.backstage.io/v1beta3',
|
||||
kind,
|
||||
metadata: { namespace, name },
|
||||
});
|
||||
|
||||
describe('<MultiEntityPicker />', () => {
|
||||
let entities: Entity[];
|
||||
const onChange = jest.fn();
|
||||
const schema = {};
|
||||
const required = false;
|
||||
let uiSchema: MultiEntityPickerProps['uiSchema'];
|
||||
const rawErrors: string[] = [];
|
||||
const formData: string[] = [];
|
||||
|
||||
let props: FieldProps;
|
||||
|
||||
const catalogApi: jest.Mocked<CatalogApi> = {
|
||||
getLocationById: jest.fn(),
|
||||
getEntityByName: jest.fn(),
|
||||
getEntities: jest.fn(async () => ({ items: entities })),
|
||||
addLocation: jest.fn(),
|
||||
getLocationByRef: jest.fn(),
|
||||
removeEntityByUid: jest.fn(),
|
||||
} as any;
|
||||
let Wrapper: React.ComponentType<React.PropsWithChildren<{}>>;
|
||||
|
||||
beforeEach(() => {
|
||||
entities = [
|
||||
makeEntity('Group', 'default', 'team-a'),
|
||||
makeEntity('Group', 'default', 'squad-b'),
|
||||
];
|
||||
|
||||
Wrapper = ({ children }: { children?: React.ReactNode }) => (
|
||||
<TestApiProvider apis={[[catalogApiRef, catalogApi]]}>
|
||||
{children}
|
||||
</TestApiProvider>
|
||||
);
|
||||
});
|
||||
|
||||
// afterEach(() => jest.resetAllMocks());
|
||||
|
||||
describe('without allowedKinds and catalogFilter', () => {
|
||||
beforeEach(() => {
|
||||
uiSchema = { 'ui:options': {} };
|
||||
props = {
|
||||
onChange,
|
||||
schema,
|
||||
required,
|
||||
uiSchema,
|
||||
rawErrors,
|
||||
formData,
|
||||
} as unknown as FieldProps<any>;
|
||||
|
||||
catalogApi.getEntities.mockResolvedValue({ items: entities });
|
||||
});
|
||||
|
||||
it('searches for all entities', async () => {
|
||||
await renderInTestApp(
|
||||
<Wrapper>
|
||||
<MultiEntityPicker {...props} />
|
||||
</Wrapper>,
|
||||
);
|
||||
|
||||
expect(catalogApi.getEntities).toHaveBeenCalledWith(undefined);
|
||||
});
|
||||
|
||||
it('updates even if there is not an exact match', async () => {
|
||||
const { getByRole } = await renderInTestApp(
|
||||
<Wrapper>
|
||||
<MultiEntityPicker {...props} />
|
||||
</Wrapper>,
|
||||
);
|
||||
|
||||
const input = getByRole('textbox');
|
||||
|
||||
fireEvent.change(input, { target: { value: 'squ' } });
|
||||
fireEvent.blur(input);
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith(['squ']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with catalogFilter', () => {
|
||||
beforeEach(() => {
|
||||
uiSchema = {
|
||||
'ui:options': {
|
||||
catalogFilter: [
|
||||
{
|
||||
kind: ['Group'],
|
||||
'metadata.name': 'test-entity',
|
||||
},
|
||||
{
|
||||
kind: ['User'],
|
||||
'metadata.name': 'test-entity',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
props = {
|
||||
onChange,
|
||||
schema,
|
||||
required,
|
||||
uiSchema,
|
||||
rawErrors,
|
||||
formData,
|
||||
} as unknown as FieldProps<any>;
|
||||
|
||||
catalogApi.getEntities.mockResolvedValue({ items: entities });
|
||||
});
|
||||
|
||||
it('searches for a specific group entity', async () => {
|
||||
await renderInTestApp(
|
||||
<Wrapper>
|
||||
<MultiEntityPicker {...props} />
|
||||
</Wrapper>,
|
||||
);
|
||||
|
||||
expect(catalogApi.getEntities).toHaveBeenCalledWith({
|
||||
filter: [
|
||||
{
|
||||
kind: ['Group'],
|
||||
'metadata.name': 'test-entity',
|
||||
},
|
||||
{
|
||||
kind: ['User'],
|
||||
'metadata.name': 'test-entity',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('allow single top level filter', async () => {
|
||||
uiSchema = {
|
||||
'ui:options': {
|
||||
catalogFilter: {
|
||||
kind: ['Group'],
|
||||
'metadata.name': 'test-entity',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
catalogApi.getEntities.mockResolvedValue({ items: entities });
|
||||
|
||||
await renderInTestApp(
|
||||
<Wrapper>
|
||||
<MultiEntityPicker {...props} uiSchema={uiSchema} />
|
||||
</Wrapper>,
|
||||
);
|
||||
|
||||
expect(catalogApi.getEntities).toHaveBeenCalledWith({
|
||||
filter: {
|
||||
kind: ['Group'],
|
||||
'metadata.name': 'test-entity',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('search for entitities containing an specific key', async () => {
|
||||
const uiSchemaWithBoolean = {
|
||||
'ui:options': {
|
||||
catalogFilter: [
|
||||
{
|
||||
kind: ['User'],
|
||||
'metadata.annotation.some/anotation': { exists: true },
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
await renderInTestApp(
|
||||
<Wrapper>
|
||||
<MultiEntityPicker {...props} uiSchema={uiSchemaWithBoolean} />
|
||||
</Wrapper>,
|
||||
);
|
||||
|
||||
expect(catalogApi.getEntities).toHaveBeenCalledWith({
|
||||
filter: [
|
||||
{
|
||||
kind: ['User'],
|
||||
'metadata.annotation.some/anotation': CATALOG_FILTER_EXISTS,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('catalogFilter should take precedence over allowedKinds', () => {
|
||||
beforeEach(() => {
|
||||
uiSchema = {
|
||||
'ui:options': {
|
||||
catalogFilter: [
|
||||
{
|
||||
kind: ['Group'],
|
||||
'metadata.name': 'test-group',
|
||||
},
|
||||
],
|
||||
allowedKinds: ['User'],
|
||||
},
|
||||
};
|
||||
props = {
|
||||
onChange,
|
||||
schema,
|
||||
required,
|
||||
uiSchema,
|
||||
rawErrors,
|
||||
formData,
|
||||
} as unknown as FieldProps<any>;
|
||||
|
||||
catalogApi.getEntities.mockResolvedValue({ items: entities });
|
||||
});
|
||||
|
||||
it('searches for a Group entity', async () => {
|
||||
await renderInTestApp(
|
||||
<Wrapper>
|
||||
<MultiEntityPicker {...props} />
|
||||
</Wrapper>,
|
||||
);
|
||||
|
||||
expect(catalogApi.getEntities).toHaveBeenCalledWith({
|
||||
filter: [
|
||||
{
|
||||
kind: ['Group'],
|
||||
'metadata.name': 'test-group',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('uses full entity ref', () => {
|
||||
beforeEach(() => {
|
||||
uiSchema = {
|
||||
'ui:options': {
|
||||
defaultKind: 'Group',
|
||||
},
|
||||
};
|
||||
props = {
|
||||
onChange,
|
||||
schema,
|
||||
required,
|
||||
uiSchema,
|
||||
rawErrors,
|
||||
formData,
|
||||
} as unknown as FieldProps<any>;
|
||||
|
||||
catalogApi.getEntities.mockResolvedValue({ items: entities });
|
||||
});
|
||||
|
||||
it('returns the full entityRef when entity exists in the list', async () => {
|
||||
const { getByRole } = await renderInTestApp(
|
||||
<Wrapper>
|
||||
<MultiEntityPicker {...props} />
|
||||
</Wrapper>,
|
||||
);
|
||||
|
||||
const input = getByRole('textbox');
|
||||
|
||||
fireEvent.change(input, { target: { value: 'team-a' } });
|
||||
fireEvent.blur(input);
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith(['group:default/team-a']);
|
||||
});
|
||||
|
||||
it('returns the full entityRef when entity does not exist in the list', async () => {
|
||||
const { getByRole } = await renderInTestApp(
|
||||
<Wrapper>
|
||||
<MultiEntityPicker {...props} />
|
||||
</Wrapper>,
|
||||
);
|
||||
|
||||
const input = getByRole('textbox');
|
||||
|
||||
fireEvent.change(input, { target: { value: 'team-b' } });
|
||||
fireEvent.blur(input);
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith(['group:default/team-b']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Required MultiEntityPicker', () => {
|
||||
beforeEach(() => {
|
||||
uiSchema = {
|
||||
'ui:options': {
|
||||
catalogFilter: [
|
||||
{
|
||||
kind: ['Group'],
|
||||
'metadata.name': 'test-entity',
|
||||
},
|
||||
{
|
||||
kind: ['User'],
|
||||
'metadata.name': 'test-entity',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
props = {
|
||||
onChange,
|
||||
schema,
|
||||
required: true,
|
||||
uiSchema,
|
||||
rawErrors,
|
||||
formData,
|
||||
} as unknown as FieldProps<any>;
|
||||
|
||||
catalogApi.getEntities.mockResolvedValue({ items: entities });
|
||||
});
|
||||
|
||||
it('User enters clear input', async () => {
|
||||
await renderInTestApp(
|
||||
<Wrapper>
|
||||
<MultiEntityPicker {...props} />
|
||||
<div data-testid="outside">Outside</div>
|
||||
</Wrapper>,
|
||||
);
|
||||
|
||||
const input = screen.getByRole('textbox');
|
||||
|
||||
fireEvent.change(input, { target: { value: '' } });
|
||||
fireEvent.blur(input);
|
||||
|
||||
expect(input).toHaveValue('');
|
||||
});
|
||||
|
||||
it('User selects item', async () => {
|
||||
await renderInTestApp(
|
||||
<Wrapper>
|
||||
<MultiEntityPicker {...props} />
|
||||
</Wrapper>,
|
||||
);
|
||||
|
||||
const input = screen.getByRole('textbox');
|
||||
|
||||
fireEvent.change(input, { target: { value: 'team-a' } });
|
||||
fireEvent.blur(input);
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith(['team-a']);
|
||||
});
|
||||
|
||||
it('User selects item and enters clear input', async () => {
|
||||
await renderInTestApp(
|
||||
<Wrapper>
|
||||
<MultiEntityPicker {...props} />
|
||||
<div data-testid="outside">Outside</div>
|
||||
</Wrapper>,
|
||||
);
|
||||
|
||||
// Open the Autocomplete dropdown
|
||||
const input = screen.getByRole('textbox');
|
||||
fireEvent.click(input);
|
||||
|
||||
// Select an option from the dropdown
|
||||
fireEvent.change(input, { target: { value: 'team-a' } });
|
||||
|
||||
// Close the dropdown by clicking outside the Autocomplete component
|
||||
const outside = screen.getByTestId('outside');
|
||||
fireEvent.mouseDown(outside);
|
||||
|
||||
// Click back into the Autocomplete component
|
||||
fireEvent.click(input);
|
||||
|
||||
// Verify that the selected option is displayed in the input
|
||||
expect(input).toHaveValue('team-a');
|
||||
|
||||
// Click the Clear button to clear the input
|
||||
const clearButton = screen.getByLabelText('Clear');
|
||||
|
||||
fireEvent.click(clearButton);
|
||||
|
||||
// Verify that the input is empty
|
||||
expect(input).toHaveValue('');
|
||||
|
||||
// Verify that the handleChange function was called with an empty array
|
||||
expect(onChange).toHaveBeenCalledWith([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Optional MultiEntityPicker', () => {
|
||||
beforeEach(() => {
|
||||
uiSchema = {
|
||||
'ui:options': {
|
||||
catalogFilter: [
|
||||
{
|
||||
kind: ['Group'],
|
||||
'metadata.name': 'test-entity',
|
||||
},
|
||||
{
|
||||
kind: ['User'],
|
||||
'metadata.name': 'test-entity',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
props = {
|
||||
onChange,
|
||||
schema,
|
||||
required: false,
|
||||
uiSchema,
|
||||
rawErrors,
|
||||
formData,
|
||||
} as unknown as FieldProps<any>;
|
||||
|
||||
catalogApi.getEntities.mockResolvedValue({ items: entities });
|
||||
});
|
||||
|
||||
it('User enters clear input', async () => {
|
||||
await renderInTestApp(
|
||||
<Wrapper>
|
||||
<MultiEntityPicker {...props} />
|
||||
<div data-testid="outside">Outside</div>
|
||||
</Wrapper>,
|
||||
);
|
||||
|
||||
const input = screen.getByRole('textbox');
|
||||
|
||||
fireEvent.change(input, { target: { value: '' } });
|
||||
fireEvent.blur(input);
|
||||
|
||||
expect(input).toHaveValue('');
|
||||
});
|
||||
|
||||
it('User selects item', async () => {
|
||||
await renderInTestApp(
|
||||
<Wrapper>
|
||||
<MultiEntityPicker {...props} />
|
||||
</Wrapper>,
|
||||
);
|
||||
|
||||
const input = screen.getByRole('textbox');
|
||||
|
||||
fireEvent.change(input, { target: { value: 'team-a' } });
|
||||
fireEvent.blur(input);
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith(['team-a']);
|
||||
});
|
||||
|
||||
it('User selects item and enters clear input', async () => {
|
||||
await renderInTestApp(
|
||||
<Wrapper>
|
||||
<MultiEntityPicker {...props} />
|
||||
<div data-testid="outside">Outside</div>
|
||||
</Wrapper>,
|
||||
);
|
||||
|
||||
// Open the Autocomplete dropdown
|
||||
const input = screen.getByRole('textbox');
|
||||
fireEvent.click(input);
|
||||
|
||||
// Select an option from the dropdown
|
||||
fireEvent.change(input, { target: { value: 'team-a' } });
|
||||
|
||||
// Close the dropdown by clicking outside the Autocomplete component
|
||||
const outside = screen.getByTestId('outside');
|
||||
fireEvent.mouseDown(outside);
|
||||
|
||||
// Click back into the Autocomplete component
|
||||
fireEvent.click(input);
|
||||
|
||||
// Verify that the selected option is displayed in the input
|
||||
expect(input).toHaveValue('team-a');
|
||||
|
||||
// Click the Clear button to clear the input
|
||||
const clearButton = screen.getByLabelText('Clear');
|
||||
fireEvent.click(clearButton);
|
||||
|
||||
// Verify that the input is empty
|
||||
expect(input).toHaveValue('');
|
||||
|
||||
// Verify that the handleChange function was called with an empty array
|
||||
expect(onChange).toHaveBeenCalledWith([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Required Free Solo', () => {
|
||||
beforeEach(() => {
|
||||
uiSchema = {
|
||||
'ui:options': {
|
||||
catalogFilter: [
|
||||
{
|
||||
kind: ['Group'],
|
||||
'metadata.name': 'test-entity',
|
||||
},
|
||||
{
|
||||
kind: ['User'],
|
||||
'metadata.name': 'test-entity',
|
||||
},
|
||||
],
|
||||
},
|
||||
allowArbitraryValues: true,
|
||||
};
|
||||
props = {
|
||||
onChange,
|
||||
schema,
|
||||
required: true,
|
||||
uiSchema,
|
||||
rawErrors,
|
||||
formData,
|
||||
} as unknown as FieldProps<any>;
|
||||
|
||||
catalogApi.getEntities.mockResolvedValue({ items: entities });
|
||||
});
|
||||
|
||||
it('User enters clear input', async () => {
|
||||
await renderInTestApp(
|
||||
<Wrapper>
|
||||
<MultiEntityPicker {...props} />
|
||||
<div data-testid="outside">Outside</div>
|
||||
</Wrapper>,
|
||||
);
|
||||
|
||||
const input = screen.getByRole('textbox');
|
||||
|
||||
fireEvent.change(input, { target: { value: '' } });
|
||||
fireEvent.blur(input);
|
||||
|
||||
expect(input).toHaveValue('');
|
||||
});
|
||||
|
||||
it('User selects item', async () => {
|
||||
await renderInTestApp(
|
||||
<Wrapper>
|
||||
<MultiEntityPicker {...props} />
|
||||
</Wrapper>,
|
||||
);
|
||||
|
||||
const input = screen.getByRole('textbox');
|
||||
|
||||
fireEvent.change(input, { target: { value: 'team-a' } });
|
||||
fireEvent.blur(input);
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith(['team-a']);
|
||||
});
|
||||
|
||||
it('User selects item and enters clear input', async () => {
|
||||
await renderInTestApp(
|
||||
<Wrapper>
|
||||
<MultiEntityPicker {...props} />
|
||||
<div data-testid="outside">Outside</div>
|
||||
</Wrapper>,
|
||||
);
|
||||
|
||||
// Open the Autocomplete dropdown
|
||||
const input = screen.getByRole('textbox');
|
||||
fireEvent.click(input);
|
||||
|
||||
// Select an option from the dropdown
|
||||
fireEvent.change(input, { target: { value: 'team-a' } });
|
||||
|
||||
// Close the dropdown by clicking outside the Autocomplete component
|
||||
const outside = screen.getByTestId('outside');
|
||||
fireEvent.mouseDown(outside);
|
||||
|
||||
// Click back into the Autocomplete component
|
||||
fireEvent.click(input);
|
||||
|
||||
// Verify that the selected option is displayed in the input
|
||||
expect(input).toHaveValue('team-a');
|
||||
|
||||
// Click the Clear button to clear the input
|
||||
const clearButton = screen.getByLabelText('Clear');
|
||||
fireEvent.click(clearButton);
|
||||
|
||||
// Verify that the input is empty
|
||||
expect(input).toHaveValue('');
|
||||
|
||||
// Verify that the handleChange function was called with an empty array
|
||||
expect(onChange).toHaveBeenCalledWith([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Optional Free Solo', () => {
|
||||
beforeEach(() => {
|
||||
uiSchema = {
|
||||
'ui:options': {
|
||||
catalogFilter: [
|
||||
{
|
||||
kind: ['Group'],
|
||||
'metadata.name': 'test-entity',
|
||||
},
|
||||
{
|
||||
kind: ['User'],
|
||||
'metadata.name': 'test-entity',
|
||||
},
|
||||
],
|
||||
},
|
||||
allowArbitraryValues: true,
|
||||
};
|
||||
props = {
|
||||
onChange,
|
||||
schema,
|
||||
required: false,
|
||||
uiSchema,
|
||||
rawErrors,
|
||||
formData,
|
||||
} as unknown as FieldProps<any>;
|
||||
|
||||
catalogApi.getEntities.mockResolvedValue({ items: entities });
|
||||
});
|
||||
|
||||
it('User enters clear input', async () => {
|
||||
await renderInTestApp(
|
||||
<Wrapper>
|
||||
<MultiEntityPicker {...props} />
|
||||
<div data-testid="outside">Outside</div>
|
||||
</Wrapper>,
|
||||
);
|
||||
|
||||
const input = screen.getByRole('textbox');
|
||||
|
||||
fireEvent.change(input, { target: { value: '' } });
|
||||
fireEvent.blur(input);
|
||||
|
||||
expect(input).toHaveValue('');
|
||||
});
|
||||
|
||||
it('User selects item', async () => {
|
||||
await renderInTestApp(
|
||||
<Wrapper>
|
||||
<MultiEntityPicker {...props} />
|
||||
</Wrapper>,
|
||||
);
|
||||
|
||||
const input = screen.getByRole('textbox');
|
||||
|
||||
fireEvent.change(input, { target: { value: 'team-a' } });
|
||||
fireEvent.blur(input);
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith(['team-a']);
|
||||
});
|
||||
|
||||
it('User selects item and enters clear input', async () => {
|
||||
await renderInTestApp(
|
||||
<Wrapper>
|
||||
<MultiEntityPicker {...props} />
|
||||
<div data-testid="outside">Outside</div>
|
||||
</Wrapper>,
|
||||
);
|
||||
|
||||
// Open the Autocomplete dropdown
|
||||
const input = screen.getByRole('textbox');
|
||||
fireEvent.click(input);
|
||||
|
||||
// Select an option from the dropdown
|
||||
fireEvent.change(input, { target: { value: 'team-a' } });
|
||||
|
||||
// Close the dropdown by clicking outside the Autocomplete component
|
||||
const outside = screen.getByTestId('outside');
|
||||
fireEvent.mouseDown(outside);
|
||||
|
||||
// Click back into the Autocomplete component
|
||||
fireEvent.click(input);
|
||||
|
||||
// Verify that the selected option is displayed in the input
|
||||
expect(input).toHaveValue('team-a');
|
||||
|
||||
// Click the Clear button to clear the input
|
||||
const clearButton = screen.getByLabelText('Clear');
|
||||
fireEvent.click(clearButton);
|
||||
|
||||
// Verify that the input is empty
|
||||
expect(input).toHaveValue('');
|
||||
|
||||
// Verify that the handleChange function was called with an empty array
|
||||
expect(onChange).toHaveBeenCalledWith([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -13,8 +13,8 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { makeFieldSchemaFromZod } from '@backstage/plugin-scaffolder';
|
||||
import { z } from 'zod';
|
||||
import { makeFieldSchemaFromZod } from '../utils';
|
||||
|
||||
/**
|
||||
* @public
|
||||
|
||||
Reference in New Issue
Block a user