Skip to content

Commit

Permalink
fix: validation should trigger on grouped components changes
Browse files Browse the repository at this point in the history
  • Loading branch information
logaretm committed Sep 10, 2024
1 parent c680f7f commit ff28d3d
Show file tree
Hide file tree
Showing 6 changed files with 129 additions and 12 deletions.
5 changes: 5 additions & 0 deletions .changeset/giant-penguins-suffer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@formwerk/core': patch
---

fix: validation should trigger on grouped components changes
35 changes: 35 additions & 0 deletions packages/core/src/useCheckbox/useCheckboxGroup.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { fireEvent, render, screen } from '@testing-library/vue';
import { axe } from 'vitest-axe';
import { describe } from 'vitest';
import { flush } from '@test-utils/flush';
import { TypedSchema } from '../types';

const createGroup = (props: CheckboxGroupProps): Component => {
return defineComponent({
Expand Down Expand Up @@ -215,6 +216,40 @@ describe('validation', () => {
expect(await axe(screen.getByTestId('fixture'))).toHaveNoViolations();
vi.useFakeTimers();
});

test('should revalidate when value changes', async () => {
const schema: TypedSchema<string[]> = {
parse: value => {
return value?.length >= 2
? Promise.resolve({ output: value, errors: [] })
: Promise.resolve({
output: value,
errors: [{ messages: ['You must select two or more options'], path: '' }],
});
},
};

const CheckboxGroup = createGroup({ label: 'Group', schema });
const Checkbox = createCheckbox(CustomBase);

await render({
components: { CheckboxGroup, Checkbox },
template: `
<CheckboxGroup data-testid="fixture">
<Checkbox label="First" value="1" />
<Checkbox label="Second" value="2" />
<Checkbox label="Third" value="3" />
</CheckboxGroup>
`,
});

await fireEvent.click(screen.getByLabelText('First'));
await flush();
expect(screen.getByLabelText('Group')).toHaveErrorMessage('You must select two or more options');
await fireEvent.click(screen.getByLabelText('Second'));
await flush();
expect(screen.getByLabelText('Group')).not.toHaveErrorMessage();
});
});

test('mixed state', async () => {
Expand Down
5 changes: 2 additions & 3 deletions packages/core/src/useCheckbox/useCheckboxGroup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ export interface CheckboxGroupContext<TCheckbox> {
readonly isTouched: boolean;

setErrors(message: Arrayable<string>): void;
setValue(value: CheckboxGroupValue<TCheckbox>): void;
hasValue(value: TCheckbox): boolean;
toggleValue(value: TCheckbox, force?: boolean): void;
setTouched(touched: boolean): void;
Expand Down Expand Up @@ -93,7 +92,7 @@ export function useCheckboxGroup<TCheckbox>(_props: Reactivify<CheckboxGroupProp
schema: props.schema,
});

const { validityDetails } = useInputValidity({ field });
const { validityDetails, updateValidity } = useInputValidity({ field });
const { fieldValue, setValue, isTouched, setTouched, errorMessage } = field;
const { describedByProps, descriptionProps } = createDescribedByProps({
inputId: groupId,
Expand Down Expand Up @@ -136,6 +135,7 @@ export function useCheckboxGroup<TCheckbox>(_props: Reactivify<CheckboxGroupProp
const nextValue = toggleValueSelection(fieldValue.value ?? [], value, force);

setValue(nextValue);
updateValidity();
}

function hasValue(value: TCheckbox) {
Expand Down Expand Up @@ -164,7 +164,6 @@ export function useCheckboxGroup<TCheckbox>(_props: Reactivify<CheckboxGroupProp
modelValue: fieldValue,
isTouched,
setErrors: field.setErrors,
setValue,
useCheckboxRegistration,
toggleValue,
hasValue,
Expand Down
58 changes: 50 additions & 8 deletions packages/core/src/useNumberField/useNumberField.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { NumberFieldProps, useNumberField } from './useNumberField';
import { type Component } from 'vue';
import { flush } from '@test-utils/flush';
import { SetOptional } from 'type-fest';
import { TypedSchema } from '../types';

const label = 'Amount';
const description = 'Enter a valid amount';
Expand Down Expand Up @@ -113,14 +114,55 @@ test('Applies decimal inputmode if the step contains decimals', async () => {
expect(screen.getByLabelText(label)).toHaveAttribute('inputmode', 'decimal');
});

test('picks up native error messages', async () => {
await render(makeTest({ required: true }));
describe('validation', () => {
test('picks up native error messages', async () => {
await render(makeTest({ required: true }));

await fireEvent.invalid(screen.getByLabelText(label));
await flush();
expect(screen.getByLabelText(label)).toHaveErrorMessage('Constraints not satisfied');
await fireEvent.invalid(screen.getByLabelText(label));
await flush();
expect(screen.getByLabelText(label)).toHaveErrorMessage('Constraints not satisfied');

vi.useRealTimers();
expect(await axe(screen.getByTestId('fixture'))).toHaveNoViolations();
vi.useFakeTimers();
vi.useRealTimers();
expect(await axe(screen.getByTestId('fixture'))).toHaveNoViolations();
vi.useFakeTimers();
});

test('should revalidate when increment/decrement buttons', async () => {
const schema: TypedSchema<number> = {
parse: value => {
return Number(value) > 1
? Promise.resolve({ errors: [] })
: Promise.resolve({ errors: [{ messages: ['Value must be greater than 1'], path: '' }] });
},
};

await render(makeTest({ schema }));
await flush();
expect(screen.getByLabelText(label)).toHaveErrorMessage();
await fireEvent.mouseDown(screen.getByLabelText('Increment'));
expect(screen.getByLabelText(label)).toHaveDisplayValue('1');
expect(screen.getByLabelText(label)).toHaveErrorMessage();
await fireEvent.mouseDown(screen.getByLabelText('Increment'));
await flush();
expect(screen.getByLabelText(label)).not.toHaveErrorMessage();
});

test('should revalidate when increment/decrement with arrows', async () => {
const schema: TypedSchema<number> = {
parse: value => {
return Number(value) > 1
? Promise.resolve({ output: value, errors: [] })
: Promise.resolve({ output: value, errors: [{ messages: ['Value must be greater than 1'], path: '' }] });
},
};

await render(makeTest({ schema }));
await fireEvent.keyDown(screen.getByLabelText(label), { code: 'ArrowUp' });
await flush();
expect(screen.getByLabelText(label)).toHaveErrorMessage('Value must be greater than 1');

await fireEvent.keyDown(screen.getByLabelText(label), { code: 'ArrowUp' });
await flush();
expect(screen.getByLabelText(label)).not.toHaveErrorMessage();
});
});
7 changes: 6 additions & 1 deletion packages/core/src/useNumberField/useNumberField.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,11 @@ export function useNumberField(
schema: props.schema,
});

const { validityDetails } = useInputValidity({ inputEl, field, disableHtmlValidation: props.disableHtmlValidation });
const { validityDetails, updateValidity } = useInputValidity({
inputEl,
field,
disableHtmlValidation: props.disableHtmlValidation,
});
const { fieldValue, setValue, setTouched, errorMessage } = field;
const formattedText = computed<string>(() => {
if (Number.isNaN(fieldValue.value) || isEmpty(fieldValue.value)) {
Expand Down Expand Up @@ -126,6 +130,7 @@ export function useNumberField(
onChange: value => {
setValue(value);
setTouched(true);
updateValidity();
},
});

Expand Down
31 changes: 31 additions & 0 deletions packages/core/src/useRadio/useRadioGroup.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { fireEvent, render, screen } from '@testing-library/vue';
import { axe } from 'vitest-axe';
import { describe } from 'vitest';
import { flush } from '@test-utils/flush';
import { TypedSchema } from '../../dist/core';

const createGroup = (props: RadioGroupProps): Component => {
return defineComponent({
Expand Down Expand Up @@ -396,4 +397,34 @@ describe('validation', () => {
expect(await axe(screen.getByTestId('fixture'))).toHaveNoViolations();
vi.useFakeTimers();
});

test('should revalidate when value changes', async () => {
const schema: TypedSchema<string> = {
parse: value => {
return Number(value) > 2
? Promise.resolve({ output: value, errors: [] })
: Promise.resolve({ output: value, errors: [{ messages: ['Value must be greater than 2'], path: '' }] });
},
};
const RadioGroup = createGroup({ label: 'Group', required: true, schema });
const RadioInput = createRadio(CustomBase);

await render({
components: { RadioGroup, RadioInput },
template: `
<RadioGroup data-testid="fixture">
<RadioInput label="First" value="1" />
<RadioInput label="Second" value="2" />
<RadioInput label="Third" value="3" />
</RadioGroup>
`,
});

await fireEvent.click(screen.getByLabelText('Second'));
await flush();
expect(screen.getByLabelText('Group')).toHaveErrorMessage('Value must be greater than 2');
await fireEvent.keyDown(screen.getByRole('radiogroup'), { code: 'ArrowDown' });
await flush();
expect(screen.getByLabelText('Group')).not.toHaveErrorMessage();
});
});

0 comments on commit ff28d3d

Please sign in to comment.