From a28a071f492ae38746e715efcb11236f7296925c Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Mon, 19 Aug 2024 02:10:43 +0300 Subject: [PATCH] test: checkboxes tests --- .../core/src/useCheckbox/useCheckbox.spec.ts | 129 +++++++++- packages/core/src/useCheckbox/useCheckbox.ts | 33 ++- .../src/useCheckbox/useCheckboxGroup.spec.ts | 242 ++++++++++++++++++ .../core/src/useCheckbox/useCheckboxGroup.ts | 4 +- 4 files changed, 385 insertions(+), 23 deletions(-) create mode 100644 packages/core/src/useCheckbox/useCheckboxGroup.spec.ts diff --git a/packages/core/src/useCheckbox/useCheckbox.spec.ts b/packages/core/src/useCheckbox/useCheckbox.spec.ts index c1d1eef0..f53b79fc 100644 --- a/packages/core/src/useCheckbox/useCheckbox.spec.ts +++ b/packages/core/src/useCheckbox/useCheckbox.spec.ts @@ -10,6 +10,7 @@ const InputBase: string = ` {{ errorMessage }} + {{ fieldValue }} `; @@ -18,19 +19,19 @@ const CustomBase: string = `
{{ label }}
{{ errorMessage }} + {{ fieldValue }} `; -const createCheckbox = (template = InputBase): Component => { +const createCheckbox = (props: CheckboxProps, template = InputBase): Component => { return defineComponent({ template, inheritAttrs: false, - setup(props: CheckboxProps, { attrs }) { - const box = useCheckbox({ ...props, ...attrs }); + setup() { + const box = useCheckbox(props); return { ...props, - ...attrs, ...box, }; }, @@ -39,13 +40,13 @@ const createCheckbox = (template = InputBase): Component => { describe('has no a11y violations', () => { test('with input as base element', async () => { - const Checkbox = createCheckbox(); + const Checkbox = createCheckbox({ label: 'First' }); await render({ components: { Checkbox }, template: `
- +
`, }); @@ -56,13 +57,13 @@ describe('has no a11y violations', () => { }); test('with custom elements as base', async () => { - const Checkbox = createCheckbox(CustomBase); + const Checkbox = createCheckbox({ label: 'First' }, CustomBase); await render({ components: { Checkbox }, template: `
- +
`, }); @@ -73,15 +74,123 @@ describe('has no a11y violations', () => { }); }); +describe('value toggling on click', () => { + test('with input as base element', async () => { + const Checkbox = createCheckbox({ label: 'First' }); + + await render({ + components: { Checkbox }, + template: ` + + `, + }); + + expect(screen.getByTestId('value')).toHaveTextContent(''); + await fireEvent.click(screen.getByLabelText('First')); + expect(screen.getByTestId('value')).toHaveTextContent('true'); + await fireEvent.click(screen.getByLabelText('First')); + expect(screen.getByTestId('value')).toHaveTextContent('false'); + }); + + test('with custom elements as base', async () => { + const Checkbox = createCheckbox({ label: 'First' }, CustomBase); + + await render({ + components: { Checkbox }, + template: ` + + `, + }); + + expect(screen.getByTestId('value')).toHaveTextContent(''); + await fireEvent.click(screen.getByLabelText('First')); + expect(screen.getByTestId('value')).toHaveTextContent('true'); + await fireEvent.click(screen.getByLabelText('First')); + expect(screen.getByTestId('value')).toHaveTextContent('false'); + }); +}); + +describe('value toggling on space key', () => { + test('with input as base element', async () => { + const Checkbox = createCheckbox({ label: 'First' }); + + await render({ + components: { Checkbox }, + template: ` + + `, + }); + + expect(screen.getByTestId('value')).toHaveTextContent(''); + await fireEvent.keyDown(screen.getByLabelText('First'), { key: 'Space' }); + expect(screen.getByTestId('value')).toHaveTextContent('true'); + await fireEvent.keyDown(screen.getByLabelText('First'), { key: 'Space' }); + expect(screen.getByTestId('value')).toHaveTextContent('false'); + }); + + test('with custom elements as base', async () => { + const Checkbox = createCheckbox({ label: 'First' }, CustomBase); + + await render({ + components: { Checkbox }, + template: ` + + `, + }); + + expect(screen.getByTestId('value')).toHaveTextContent(''); + await fireEvent.click(screen.getByLabelText('First')); + expect(screen.getByTestId('value')).toHaveTextContent('true'); + await fireEvent.click(screen.getByLabelText('First')); + expect(screen.getByTestId('value')).toHaveTextContent('false'); + }); +}); + +describe('value toggling with custom true and false values', () => { + test('with input as base element', async () => { + const Checkbox = createCheckbox({ label: 'First', trueValue: '1', falseValue: '2' }); + + await render({ + components: { Checkbox }, + template: ` + + `, + }); + + expect(screen.getByTestId('value')).toHaveTextContent(''); + await fireEvent.click(screen.getByLabelText('First')); + expect(screen.getByTestId('value')).toHaveTextContent('1'); + await fireEvent.click(screen.getByLabelText('First')); + expect(screen.getByTestId('value')).toHaveTextContent('2'); + }); + + test('with custom elements as base', async () => { + const Checkbox = createCheckbox({ label: 'First', trueValue: '1', falseValue: '2' }, CustomBase); + + await render({ + components: { Checkbox }, + template: ` + + `, + }); + + expect(screen.getByTestId('value')).toHaveTextContent(''); + await fireEvent.click(screen.getByLabelText('First')); + expect(screen.getByTestId('value')).toHaveTextContent('1'); + await fireEvent.click(screen.getByLabelText('First')); + expect(screen.getByTestId('value')).toHaveTextContent('2'); + }); +}); + describe('validation', () => { test('picks up native error messages', async () => { - const Checkbox = createCheckbox(); + const Checkbox = createCheckbox({ label: 'First', required: true }); await render({ components: { Checkbox }, template: `
- +
`, }); diff --git a/packages/core/src/useCheckbox/useCheckbox.ts b/packages/core/src/useCheckbox/useCheckbox.ts index b1b76fee..49a3e2b2 100644 --- a/packages/core/src/useCheckbox/useCheckbox.ts +++ b/packages/core/src/useCheckbox/useCheckbox.ts @@ -1,4 +1,4 @@ -import { Ref, computed, inject, nextTick, ref, toValue } from 'vue'; +import { computed, inject, nextTick, Ref, ref, toValue } from 'vue'; import { createAccessibleErrorMessageProps, isEqual, @@ -9,22 +9,24 @@ import { } from '../utils/common'; import { AriaLabelableProps, - Reactivify, InputBaseAttributes, + NormalizedProps, + Reactivify, RovingTabIndex, TypedSchema, - NormalizedProps, } from '../types'; import { useLabel } from '../a11y/useLabel'; import { CheckboxGroupContext, CheckboxGroupKey } from './useCheckboxGroup'; import { useFormField } from '../useFormField'; import { FieldTypePrefixes } from '../constants'; -import { useInputValidity } from '@core/validation'; +import { useInputValidity } from '../validation'; export interface CheckboxProps { name?: string; label?: string; modelValue?: TValue; + + value?: TValue; trueValue?: TValue; falseValue?: TValue; @@ -57,7 +59,7 @@ export function useCheckbox( ) { const props = normalizeProps(_props, ['schema']); const inputId = useUniqId(FieldTypePrefixes.Checkbox); - const getTrueValue = () => (toValue(props.trueValue) as TValue) ?? (true as TValue); + const getTrueValue = createTrueValueGetter(props); const getFalseValue = () => (toValue(props.falseValue) as TValue) ?? (false as TValue); const group: CheckboxGroupContext | null = inject(CheckboxGroupKey, null); const inputRef = elementRef || ref(); @@ -108,7 +110,7 @@ export function useCheckbox( return; } - if (e.code === 'Space') { + if (e.key === 'Space') { e.preventDefault(); toggleValue(); setTouched(true); @@ -196,7 +198,13 @@ export function useCheckbox( return; } - group?.toggleValue(getTrueValue(), force); + if (group) { + group?.toggleValue(getTrueValue(), force); + + return; + } + + setValue(force ? getTrueValue() : getFalseValue()); } function toggleValue(force?: boolean) { @@ -228,20 +236,19 @@ function useCheckboxField( props: NormalizedProps, 'schema'>, 'schema'>, ) { const group: CheckboxGroupContext | null = inject(CheckboxGroupKey, null); - const getTrueValue = () => (toValue(props.trueValue) as TValue) ?? (true as TValue); if (group) { + const getTrueValue = createTrueValueGetter(props); + return createGroupField(group, getTrueValue); } - const field = useFormField({ + return useFormField({ path: props.name, initialValue: toValue(props.modelValue) as TValue, disabled: props.disabled, schema: props.schema, }); - - return field; } function createGroupField(group: CheckboxGroupContext, getTrueValue: () => TValue) { @@ -254,3 +261,7 @@ function createGroupField(group: CheckboxGroupContext, setValue, }; } + +function createTrueValueGetter(props: NormalizedProps, 'schema'>, 'schema'>) { + return () => (toValue(props.trueValue) as TValue) ?? (toValue(props.value) as TValue) ?? (true as TValue); +} diff --git a/packages/core/src/useCheckbox/useCheckboxGroup.spec.ts b/packages/core/src/useCheckbox/useCheckboxGroup.spec.ts new file mode 100644 index 00000000..b25a8598 --- /dev/null +++ b/packages/core/src/useCheckbox/useCheckboxGroup.spec.ts @@ -0,0 +1,242 @@ +import { CheckboxGroupProps, useCheckboxGroup } from './useCheckboxGroup'; +import { type Component, defineComponent } from 'vue'; +import { CheckboxProps, useCheckbox } from './useCheckbox'; +import { fireEvent, render, screen } from '@testing-library/vue'; +import { axe } from 'vitest-axe'; +import { describe } from 'vitest'; +import { flush } from '@test-utils/flush'; + +const createGroup = (props: CheckboxGroupProps): Component => { + return defineComponent({ + setup() { + const group = useCheckboxGroup(props); + + return { + ...props, + ...group, + }; + }, + template: ` +
+ {{ label }} + +
{{ errorMessage }}
+
{{ description }}
+
{{ fieldValue }}
+
{{ groupState }}
+
+ `, + }); +}; + +const InputBase: string = ` +
+ + +
+`; + +const CustomBase: string = ` +
+
+
{{ label }}
+
+`; + +const createCheckbox = (template = InputBase): Component => { + return defineComponent({ + template, + inheritAttrs: false, + setup(props: CheckboxProps, { attrs }) { + const checkbox = useCheckbox({ ...props, ...attrs }); + + return { + ...props, + ...attrs, + ...checkbox, + }; + }, + }); +}; + +describe('has no a11y violations', () => { + test('with input as base element', async () => { + const CheckboxGroup = createGroup({ label: 'Group' }); + const Checkbox = createCheckbox(); + + await render({ + components: { CheckboxGroup, Checkbox }, + template: ` + + + + + `, + }); + + vi.useRealTimers(); + expect(await axe(screen.getByTestId('fixture'))).toHaveNoViolations(); + vi.useFakeTimers(); + }); + + test('with custom elements as base', async () => { + const CheckboxGroup = createGroup({ label: 'Group' }); + const Checkbox = createCheckbox(CustomBase); + + await render({ + components: { CheckboxGroup, Checkbox }, + template: ` + + + + + `, + }); + + vi.useRealTimers(); + expect(await axe(screen.getByTestId('fixture'))).toHaveNoViolations(); + vi.useFakeTimers(); + }); +}); + +describe('click toggles the values', () => { + test('with input as base element', async () => { + const CheckboxGroup = createGroup({ label: 'Group' }); + const Checkbox = createCheckbox(); + + await render({ + components: { CheckboxGroup, Checkbox }, + template: ` + + + + + `, + }); + + await fireEvent.click(screen.getByLabelText('First')); + expect(screen.getByTestId('value')).toHaveTextContent('[ "1" ]'); + await fireEvent.click(screen.getByLabelText('Second')); + expect(screen.getByTestId('value')).toHaveTextContent('[ "1", "2" ]'); + await fireEvent.click(screen.getByLabelText('Second')); + expect(screen.getByTestId('value')).toHaveTextContent('[ "1" ]'); + }); + + test('with custom elements as base', async () => { + const CheckboxGroup = createGroup({ label: 'Group' }); + const Checkbox = createCheckbox(CustomBase); + + await render({ + components: { CheckboxGroup, Checkbox }, + template: ` + + + + + `, + }); + + await fireEvent.click(screen.getByLabelText('First')); + expect(screen.getByTestId('value')).toHaveTextContent('[ "1" ]'); + await fireEvent.click(screen.getByLabelText('Second')); + expect(screen.getByTestId('value')).toHaveTextContent('[ "1", "2" ]'); + await fireEvent.click(screen.getByLabelText('Second')); + expect(screen.getByTestId('value')).toHaveTextContent('[ "1" ]'); + }); +}); + +describe('Space key toggles the values', () => { + test('with input as base element', async () => { + const CheckboxGroup = createGroup({ label: 'Group' }); + const Checkbox = createCheckbox(); + + await render({ + components: { CheckboxGroup, Checkbox }, + template: ` + + + + + `, + }); + + await fireEvent.keyDown(screen.getByLabelText('First'), { key: 'Space' }); + expect(screen.getByTestId('value')).toHaveTextContent('[ "1" ]'); + await fireEvent.keyDown(screen.getByLabelText('Second'), { key: 'Space' }); + expect(screen.getByTestId('value')).toHaveTextContent('[ "1", "2" ]'); + await fireEvent.keyDown(screen.getByLabelText('Second'), { key: 'Space' }); + expect(screen.getByTestId('value')).toHaveTextContent('[ "1" ]'); + }); + + test('with custom elements as base', async () => { + const CheckboxGroup = createGroup({ label: 'Group' }); + const Checkbox = createCheckbox(CustomBase); + + await render({ + components: { CheckboxGroup, Checkbox }, + template: ` + + + + + `, + }); + + await fireEvent.keyDown(screen.getByLabelText('First'), { key: 'Space' }); + expect(screen.getByTestId('value')).toHaveTextContent('[ "1" ]'); + await fireEvent.keyDown(screen.getByLabelText('Second'), { key: 'Space' }); + expect(screen.getByTestId('value')).toHaveTextContent('[ "1", "2" ]'); + await fireEvent.keyDown(screen.getByLabelText('Second'), { key: 'Space' }); + expect(screen.getByTestId('value')).toHaveTextContent('[ "1" ]'); + }); +}); + +describe('validation', () => { + test('picks up native error messages', async () => { + const CheckboxGroup = createGroup({ label: 'Group', required: true }); + const Checkbox = createCheckbox(); + + await render({ + components: { CheckboxGroup, Checkbox }, + template: ` + + + + + + `, + }); + + await fireEvent.invalid(screen.getByLabelText('First')); + await flush(); + expect(screen.getByLabelText('Group')).toHaveErrorMessage('Constraints not satisfied'); + + vi.useRealTimers(); + expect(await axe(screen.getByTestId('fixture'))).toHaveNoViolations(); + vi.useFakeTimers(); + }); +}); + +test('mixed state', async () => { + const CheckboxGroup = createGroup({ label: 'Group' }); + const Checkbox = createCheckbox(); + + await render({ + components: { CheckboxGroup, Checkbox }, + template: ` + + + + + + `, + }); + + expect(screen.getByTestId('state')).toHaveTextContent('unchecked'); + await fireEvent.click(screen.getByLabelText('First')); + expect(screen.getByTestId('state')).toHaveTextContent('mixed'); + await fireEvent.click(screen.getByLabelText('Second')); + expect(screen.getByTestId('state')).toHaveTextContent('mixed'); + await fireEvent.click(screen.getByLabelText('Third')); + expect(screen.getByTestId('state')).toHaveTextContent('checked'); +}); diff --git a/packages/core/src/useCheckbox/useCheckboxGroup.ts b/packages/core/src/useCheckbox/useCheckboxGroup.ts index b051275d..b4389d05 100644 --- a/packages/core/src/useCheckbox/useCheckboxGroup.ts +++ b/packages/core/src/useCheckbox/useCheckboxGroup.ts @@ -104,7 +104,7 @@ export function useCheckboxGroup(_props: Reactivify(() => { + const groupProps = computed(() => { return { ...labelledByProps.value, ...describedByProps.value, @@ -187,7 +187,7 @@ export function useCheckboxGroup(_props: Reactivify