Merge pull request #17336 from morales-martin/master

Fix EntityPicker allowing clear value when required #16993
This commit is contained in:
Ben Lambert
2023-04-18 11:18:18 +02:00
committed by GitHub
3 changed files with 399 additions and 3 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder': minor
---
Fixed bug in EntityPicker component that allowed for empty values when field is required. This bug occurs only after a user fills the EntityPicker field, clears it, and then continues to the next form step.
@@ -19,13 +19,13 @@ 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 } from '@testing-library/react';
import { fireEvent, screen } from '@testing-library/react';
import React from 'react';
import { EntityPicker } from './EntityPicker';
import { EntityPickerProps } from './schema';
const makeEntity = (kind: string, namespace: string, name: string): Entity => ({
apiVersion: 'backstage.io/v1beta1',
apiVersion: 'scaffolder.backstage.io/v1beta3',
kind,
metadata: { namespace, name },
});
@@ -332,4 +332,394 @@ describe('<EntityPicker />', () => {
expect(onChange).toHaveBeenCalledWith('group:default/team-b');
});
});
describe('Required EntityPicker', () => {
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>
<EntityPicker {...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>
<EntityPicker {...props} />
</Wrapper>,
);
const input = screen.getByRole('textbox');
fireEvent.change(input, { target: { value: 'team-a' } });
fireEvent.blur(input);
expect(input).toHaveValue('team-a');
expect(onChange).toHaveBeenCalledWith('team-a');
});
it('User selects item and enters clear input', async () => {
await renderInTestApp(
<Wrapper>
<EntityPicker {...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 undefined
expect(onChange).toHaveBeenCalledWith(undefined);
});
});
describe('Optional EntityPicker', () => {
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>
<EntityPicker {...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>
<EntityPicker {...props} />
</Wrapper>,
);
const input = screen.getByRole('textbox');
fireEvent.change(input, { target: { value: 'team-a' } });
fireEvent.blur(input);
expect(input).toHaveValue('team-a');
expect(onChange).toHaveBeenCalledWith('team-a');
});
it('User selects item and enters clear input', async () => {
await renderInTestApp(
<Wrapper>
<EntityPicker {...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 undefined
expect(onChange).toHaveBeenCalledWith(undefined);
});
});
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>
<EntityPicker {...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>
<EntityPicker {...props} />
</Wrapper>,
);
const input = screen.getByRole('textbox');
fireEvent.change(input, { target: { value: 'team-a' } });
fireEvent.blur(input);
expect(input).toHaveValue('team-a');
expect(onChange).toHaveBeenCalledWith('team-a');
});
it('User selects item and enters clear input', async () => {
await renderInTestApp(
<Wrapper>
<EntityPicker {...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 undefined
expect(onChange).toHaveBeenCalledWith(undefined);
});
});
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>
<EntityPicker {...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>
<EntityPicker {...props} />
</Wrapper>,
);
const input = screen.getByRole('textbox');
fireEvent.change(input, { target: { value: 'team-a' } });
fireEvent.blur(input);
expect(input).toHaveValue('team-a');
expect(onChange).toHaveBeenCalledWith('team-a');
});
it('User selects item and enters clear input', async () => {
await renderInTestApp(
<Wrapper>
<EntityPicker {...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 undefined
expect(onChange).toHaveBeenCalledWith(undefined);
});
});
});
@@ -96,7 +96,8 @@ export const EntityPicker = (props: EntityPickerProps) => {
(_: any, ref: string | Entity | null, reason: AutocompleteChangeReason) => {
// ref can either be a string from free solo entry or
if (typeof ref !== 'string') {
onChange(ref ? stringifyEntityRef(ref as Entity) : '');
// if ref does not exist: pass 'undefined' to trigger validation for required value
onChange(ref ? stringifyEntityRef(ref as Entity) : undefined);
} else {
if (reason === 'blur' || reason === 'create-option') {
// Add in default namespace, etc.