From 2737452e1bad4c73b5e9cd9928c410be902583ef Mon Sep 17 00:00:00 2001 From: Johny Ho Date: Mon, 17 Aug 2026 23:56:31 -0400 Subject: [PATCH 1/6] Add React Spectrum S2 wrapper Uses @react-spectrum/s2 subpath imports with value/defaultValue API for Select (not deprecated selectedKey). Strips HTML size from text-like fields to avoid S2 variant conflict. Includes ColorField and multi-select support. Co-Authored-By: Claude Opus 4.6 (1M context) --- wrappers/js/react-spectrum/s2/index.jsx | 300 +++++++ .../ts/react-spectrum/s2/Checkbox.test.jsx | 107 +++ .../s2/CollectionCheckboxes.test.jsx | 102 +++ .../s2/CollectionRadioButtons.test.jsx | 102 +++ .../ts/react-spectrum/s2/ColorField.test.jsx | 44 + .../ts/react-spectrum/s2/DateField.test.jsx | 44 + .../s2/DateTimeLocalField.test.jsx | 52 ++ .../ts/react-spectrum/s2/EmailField.test.jsx | 44 + .../ts/react-spectrum/s2/FileField.test.jsx | 42 + .../ts/react-spectrum/s2/MonthField.test.jsx | 44 + .../ts/react-spectrum/s2/NumberField.test.jsx | 51 ++ .../react-spectrum/s2/PasswordField.test.jsx | 44 + .../ts/react-spectrum/s2/RangeField.test.jsx | 50 ++ .../ts/react-spectrum/s2/SearchField.test.jsx | 46 + wrappers/ts/react-spectrum/s2/Select.test.jsx | 89 ++ .../ts/react-spectrum/s2/TelField.test.jsx | 44 + .../ts/react-spectrum/s2/TextArea.test.jsx | 47 + .../ts/react-spectrum/s2/TextField.test.jsx | 47 + .../ts/react-spectrum/s2/TimeField.test.jsx | 42 + .../ts/react-spectrum/s2/UrlField.test.jsx | 44 + wrappers/ts/react-spectrum/s2/index.tsx | 820 ++++++++++++++++++ wrappers/ts/react-spectrum/s2/package.json | 12 + .../ts/react-spectrum/s2/tsconfig.build.json | 14 + wrappers/ts/react-spectrum/s2/tsconfig.json | 10 + 24 files changed, 2241 insertions(+) create mode 100644 wrappers/js/react-spectrum/s2/index.jsx create mode 100644 wrappers/ts/react-spectrum/s2/Checkbox.test.jsx create mode 100644 wrappers/ts/react-spectrum/s2/CollectionCheckboxes.test.jsx create mode 100644 wrappers/ts/react-spectrum/s2/CollectionRadioButtons.test.jsx create mode 100644 wrappers/ts/react-spectrum/s2/ColorField.test.jsx create mode 100644 wrappers/ts/react-spectrum/s2/DateField.test.jsx create mode 100644 wrappers/ts/react-spectrum/s2/DateTimeLocalField.test.jsx create mode 100644 wrappers/ts/react-spectrum/s2/EmailField.test.jsx create mode 100644 wrappers/ts/react-spectrum/s2/FileField.test.jsx create mode 100644 wrappers/ts/react-spectrum/s2/MonthField.test.jsx create mode 100644 wrappers/ts/react-spectrum/s2/NumberField.test.jsx create mode 100644 wrappers/ts/react-spectrum/s2/PasswordField.test.jsx create mode 100644 wrappers/ts/react-spectrum/s2/RangeField.test.jsx create mode 100644 wrappers/ts/react-spectrum/s2/SearchField.test.jsx create mode 100644 wrappers/ts/react-spectrum/s2/Select.test.jsx create mode 100644 wrappers/ts/react-spectrum/s2/TelField.test.jsx create mode 100644 wrappers/ts/react-spectrum/s2/TextArea.test.jsx create mode 100644 wrappers/ts/react-spectrum/s2/TextField.test.jsx create mode 100644 wrappers/ts/react-spectrum/s2/TimeField.test.jsx create mode 100644 wrappers/ts/react-spectrum/s2/UrlField.test.jsx create mode 100644 wrappers/ts/react-spectrum/s2/index.tsx create mode 100644 wrappers/ts/react-spectrum/s2/package.json create mode 100644 wrappers/ts/react-spectrum/s2/tsconfig.build.json create mode 100644 wrappers/ts/react-spectrum/s2/tsconfig.json diff --git a/wrappers/js/react-spectrum/s2/index.jsx b/wrappers/js/react-spectrum/s2/index.jsx new file mode 100644 index 0000000..e177f49 --- /dev/null +++ b/wrappers/js/react-spectrum/s2/index.jsx @@ -0,0 +1,300 @@ +/** + * A set of [candy_wrappers](https://github.com/thoughtbot/candy_wrapper) around + * React Spectrum S2 input components. It works with the output from + * [FormProps](https://github.com/thoughtbot/form_props). + * + * You modify these components to fit your design needs. + */ +import React, { createContext, useContext, useMemo } from 'react'; +import { parseDate, parseDateTime, parseTime, } from '@internationalized/date'; +import { TextField as SpectrumTextField } from '@react-spectrum/s2/TextField'; +import { TextArea as SpectrumTextArea } from '@react-spectrum/s2/TextArea'; +import { NumberField as SpectrumNumberField } from '@react-spectrum/s2/NumberField'; +import { DateField as SpectrumDateField } from '@react-spectrum/s2/DateField'; +import { TimeField as SpectrumTimeField } from '@react-spectrum/s2/TimeField'; +import { DatePicker as SpectrumDatePicker } from '@react-spectrum/s2/DatePicker'; +import { ColorField as SpectrumColorField } from '@react-spectrum/s2/ColorField'; +import { SearchField as SpectrumSearchField } from '@react-spectrum/s2/SearchField'; +import { Checkbox as SpectrumCheckbox, } from '@react-spectrum/s2/Checkbox'; +import { CheckboxGroup as SpectrumCheckboxGroup, } from '@react-spectrum/s2/CheckboxGroup'; +import { RadioGroup as SpectrumRadioGroup, Radio as SpectrumRadio, } from '@react-spectrum/s2/RadioGroup'; +import { Picker as SpectrumPicker, PickerItem, PickerSection, } from '@react-spectrum/s2/Picker'; +import { Slider as SpectrumSlider } from '@react-spectrum/s2/Slider'; +import { Button as SpectrumButton } from '@react-spectrum/s2/Button'; +import { Form as SpectrumForm } from '@react-spectrum/s2/Form'; +import { FileTrigger as SpectrumFileTrigger } from '@react-spectrum/s2/FileTrigger'; +export const ValidationContext = createContext({}); +export const useErrorMessage = (errorKey) => { + const errors = useContext(ValidationContext); + return useMemo(() => { + if (!errorKey) { + return null; + } + const validationError = errors[errorKey]; + const hasErrors = errorKey && validationError; + if (!hasErrors) { + return null; + } + const errorMessages = Array.isArray(validationError) + ? validationError + : [validationError]; + return errorMessages.join(' '); + }, [errors, errorKey]); +}; +export const Extras = (hiddenInputAttributes) => { + const hiddenProps = Object.values(hiddenInputAttributes); + const hiddenInputs = hiddenProps.map((props) => ()); + return <>{hiddenInputs}; +}; +export const Form = ({ extras, validationErrors = {}, children, ...props }) => { + const formProps = props; + return ( + + + {children} + + ); +}; +export const Checkbox = ({ type: _type, includeHidden, uncheckedValue, errorKey, label, checked, defaultChecked, ...rest }) => { + const errorMessage = useErrorMessage(errorKey); + // Transform: Rails checked/defaultChecked → Spectrum isSelected/defaultSelected + const isSelected = checked ?? undefined; + const defaultSelected = defaultChecked ?? undefined; + return (<> + {includeHidden && ()} + + {label} + + ); +}; +export const CollectionCheckboxes = ({ includeHidden, collection, label, errorKey, }) => { + const errorMessage = useErrorMessage(errorKey); + if (collection.length === 0) { + return null; + } + const defaultItems = collection.filter((option) => !!option.defaultChecked); + const items = collection.filter((option) => !!option.checked); + const valueProps = {}; + if (defaultItems.length > 0) { + valueProps.defaultValue = defaultItems.map((option) => option.value); + } + else if (items.length > 0) { + valueProps.value = items.map((option) => option.value); + } + const { name } = collection[0]; + return (<> + {includeHidden && ()} + + {collection.map((option) => ( + {option.label} + ))} + + ); +}; +export const CollectionRadioButtons = ({ includeHidden, collection, label, errorKey, }) => { + const errorMessage = useErrorMessage(errorKey); + if (collection.length === 0) { + return null; + } + const defaultItem = collection.find((option) => !!option.defaultChecked); + const item = collection.find((option) => !!option.checked); + const valueProps = {}; + if (defaultItem) { + valueProps.defaultValue = defaultItem.value; + } + else if (item) { + valueProps.value = item.value; + } + const { name } = collection[0]; + return (<> + {includeHidden && ()} + + {collection.map((option) => ( + {option.label} + ))} + + ); +}; +export const TextField = ({ type: _type, label, errorKey, size: _size, ...rest }) => { + const errorMessage = useErrorMessage(errorKey); + return (); +}; +export const EmailField = ({ type: _type, label, errorKey, size: _size, ...rest }) => { + const errorMessage = useErrorMessage(errorKey); + return (); +}; +export const ColorField = ({ type: _type, label, errorKey, ...rest }) => { + const errorMessage = useErrorMessage(errorKey); + return (); +}; +export const DateField = ({ type: _type, label, errorKey, value, defaultValue, min, max, ...rest }) => { + const errorMessage = useErrorMessage(errorKey); + // Transform: string values → CalendarDate objects + const valueProps = {}; + if (value) { + valueProps.value = parseDate(value); + } + else if (defaultValue) { + valueProps.defaultValue = parseDate(defaultValue); + } + // Transform: min/max → minValue/maxValue + const minMaxProps = {}; + if (min) { + minMaxProps.minValue = parseDate(min); + } + if (max) { + minMaxProps.maxValue = parseDate(max); + } + return (); +}; +export const DateTimeLocalField = ({ type: _type, label, errorKey, value, defaultValue, min, max, ...rest }) => { + const errorMessage = useErrorMessage(errorKey); + // Transform: string values → CalendarDateTime objects + const valueProps = {}; + if (value) { + valueProps.value = parseDateTime(value); + } + else if (defaultValue) { + valueProps.defaultValue = parseDateTime(defaultValue); + } + // Transform: min/max → minValue/maxValue + const minMaxProps = {}; + if (min) { + minMaxProps.minValue = parseDateTime(min); + } + if (max) { + minMaxProps.maxValue = parseDateTime(max); + } + return (); +}; +export const TimeField = ({ type: _type, label, errorKey, value, defaultValue, ...rest }) => { + const errorMessage = useErrorMessage(errorKey); + // Transform: string values → Time objects + const valueProps = {}; + if (value) { + valueProps.value = parseTime(value); + } + else if (defaultValue) { + valueProps.defaultValue = parseTime(defaultValue); + } + return (); +}; +export const SearchField = ({ type: _type, label, errorKey, size: _size, autosave: _autosave, results: _results, onsearch: _onsearch, incremental: _incremental, ...rest }) => { + const errorMessage = useErrorMessage(errorKey); + return (); +}; +export const TelField = ({ type: _type, label, errorKey, size: _size, ...rest }) => { + const errorMessage = useErrorMessage(errorKey); + return (); +}; +export const UrlField = ({ type: _type, label, errorKey, size: _size, ...rest }) => { + const errorMessage = useErrorMessage(errorKey); + return (); +}; +export const MonthField = ({ type: _type, label, errorKey, min: _min, max: _max, ...rest }) => { + const errorMessage = useErrorMessage(errorKey); + return (); +}; +export const NumberField = ({ type: _type, label, errorKey, value, defaultValue, min, max, ...rest }) => { + const errorMessage = useErrorMessage(errorKey); + // Transform: string/number values → number + const valueProps = {}; + if (value !== undefined && value !== '') { + valueProps.value = Number(value); + } + else if (defaultValue !== undefined && defaultValue !== '') { + valueProps.defaultValue = Number(defaultValue); + } + return (); +}; +export const RangeField = ({ type: _type, label, errorKey, value, defaultValue, min, max, ...rest }) => { + const errorMessage = useErrorMessage(errorKey); + const numericValue = value ? Number(value) : undefined; + const numericDefault = defaultValue ? Number(defaultValue) : undefined; + return (<> + + + {errorMessage && {errorMessage}} + ); +}; +export const PasswordField = ({ type: _type, label, errorKey, size: _size, ...rest }) => { + const errorMessage = useErrorMessage(errorKey); + return (); +}; +export const Select = ({ includeHidden, name, id, options, label, errorKey, multiple, type: _type, ...rest }) => { + const errorMessage = useErrorMessage(errorKey); + const addHidden = includeHidden && multiple; + const selectedValue = 'value' in rest ? rest.value : undefined; + const selectedDefault = 'defaultValue' in rest ? rest.defaultValue : undefined; + const hasGroups = options.some((item) => 'options' in item); + const selectionProps = {}; + if (multiple) { + selectionProps.selectionMode = 'multiple'; + if (selectedValue) { + selectionProps.value = Array.isArray(selectedValue) + ? selectedValue + : [selectedValue]; + } + else if (selectedDefault) { + selectionProps.defaultValue = Array.isArray(selectedDefault) + ? selectedDefault + : [selectedDefault]; + } + } + else { + if (selectedValue) { + selectionProps.value = Array.isArray(selectedValue) + ? selectedValue[0] + : selectedValue; + } + else if (selectedDefault) { + selectionProps.defaultValue = Array.isArray(selectedDefault) + ? selectedDefault[0] + : selectedDefault; + } + } + return (<> + {addHidden && ()} + + {hasGroups + ? options.map((item) => { + if ('options' in item) { + return ( + {item.options.map((opt) => ( + {opt.label} + ))} + ); + } + return ( + {item.label} + ); + }) + : options.map((item) => { + if ('options' in item) { + return null; + } + return ( + {item.label} + ); + })} + + ); +}; +export const TextArea = ({ type: _type, label, errorKey, ...rest }) => { + const errorMessage = useErrorMessage(errorKey); + return (); +}; +export const FileField = ({ type: _type, label, errorKey }) => { + const errorMessage = useErrorMessage(errorKey); + return (<> + + {label} + + {errorMessage && {errorMessage}} + ); +}; +export const SubmitButton = ({ type: _type, text, ...rest }) => { + return ( + {text} + ); +}; diff --git a/wrappers/ts/react-spectrum/s2/Checkbox.test.jsx b/wrappers/ts/react-spectrum/s2/Checkbox.test.jsx new file mode 100644 index 0000000..9104369 --- /dev/null +++ b/wrappers/ts/react-spectrum/s2/Checkbox.test.jsx @@ -0,0 +1,107 @@ +import React from 'react' +import { render } from '@testing-library/react' +import { Checkbox, ValidationContext } from '.' + +const buildPayload = () => { + return { + type: 'checkbox', + value: '1', + uncheckedValue: '0', + defaultChecked: true, + name: 'post[admin]', + id: 'post_admin', + } +} + +describe('Checkbox', () => { + it('renders', () => { + const payload = buildPayload() + + const { getByRole } = render() + + const element = getByRole('checkbox', { name: 'Is admin' }) + expect(element).not.toBeNull() + }) + + it('renders defaultSelected', () => { + const payload = buildPayload() + payload.defaultChecked = true + + const { getByRole } = render( + + ) + + const input = getByRole('checkbox', { name: 'Is admin' }) + expect(input.checked).toEqual(true) + }) + + it('renders defaultSelected false', () => { + const payload = buildPayload() + payload.defaultChecked = false + + const { getByRole } = render( + + ) + + const input = getByRole('checkbox', { name: 'Is admin' }) + expect(input.checked).toEqual(false) + }) + + it('renders selected', () => { + const payload = buildPayload() + delete payload.defaultChecked + payload.checked = true + + const { getByRole } = render( + + ) + + const input = getByRole('checkbox', { name: 'Is admin' }) + expect(input.checked).toEqual(true) + }) + + it('renders unselected', () => { + const payload = buildPayload() + delete payload.defaultChecked + payload.checked = false + + const { getByRole } = render( + + ) + + const input = getByRole('checkbox', { name: 'Is admin' }) + expect(input.checked).toEqual(false) + }) + + it('adds a hidden input if includeHidden is true', () => { + const payload = buildPayload() + payload.includeHidden = true + payload.uncheckedValue = '10' + + const { container } = render( + + ) + + const hiddenInput = container.querySelector('input[type=hidden]') + expect(hiddenInput).not.toBe(null) + expect(hiddenInput.name).toEqual('post[admin]') + expect(hiddenInput.value).toEqual('10') + }) + + it('renders with errors', async () => { + const payload = buildPayload() + + const validationErrors = { + is_admin: 'Admin invalid', + } + + const { container } = render( + + + + ) + + const checkbox = container.querySelector('[aria-invalid="true"]') + expect(checkbox).not.toBeNull() + }) +}) diff --git a/wrappers/ts/react-spectrum/s2/CollectionCheckboxes.test.jsx b/wrappers/ts/react-spectrum/s2/CollectionCheckboxes.test.jsx new file mode 100644 index 0000000..fb18c8f --- /dev/null +++ b/wrappers/ts/react-spectrum/s2/CollectionCheckboxes.test.jsx @@ -0,0 +1,102 @@ +import React from 'react' +import { render } from '@testing-library/react' +import { CollectionCheckboxes, ValidationContext } from '.' + +const buildCheckboxPayload = (value, label) => { + return { + type: 'checkbox', + value, + defaultChecked: true, + label, + name: 'post[author_ids][]', + id: `post_author_ids_${value}`, + } +} + +const buildPayload = () => { + return { + collection: [ + buildCheckboxPayload(1, 'one'), + buildCheckboxPayload(2, 'two'), + ], + includeHidden: true, + label: 'authors', + required: false, + } +} + +describe('CollectionCheckboxes', () => { + it('renders', () => { + const payload = buildPayload() + + const { getByRole } = render( + + ) + + const group = getByRole('group', { name: 'Authors' }) + expect(group).not.toBeNull() + + const checkbox1 = getByRole('checkbox', { name: 'one' }) + expect(checkbox1).not.toBeNull() + + const checkbox2 = getByRole('checkbox', { name: 'two' }) + expect(checkbox2).not.toBeNull() + }) + + it('renders nothing when the collection is blank', () => { + const payload = buildPayload() + payload.collection = [] + + const { container } = render( + + ) + + expect(container.innerHTML).toEqual('') + }) + + it('renders hidden', () => { + const payload = buildPayload() + payload.includeHidden = true + + const { container } = render( + + ) + + const hiddenInput = container.querySelector('input[type=hidden]') + expect(hiddenInput.name).toEqual('post[author_ids][]') + expect(hiddenInput.value).toEqual('') + }) + + it('renders with error highlighting', async () => { + const payload = buildPayload() + + const validationErrors = { + author_ids: 'id invalid', + } + + const { getByText } = render( + + + + ) + + const errorField = getByText('id invalid') + expect(errorField).not.toBeNull() + }) +}) diff --git a/wrappers/ts/react-spectrum/s2/CollectionRadioButtons.test.jsx b/wrappers/ts/react-spectrum/s2/CollectionRadioButtons.test.jsx new file mode 100644 index 0000000..b6f0e3b --- /dev/null +++ b/wrappers/ts/react-spectrum/s2/CollectionRadioButtons.test.jsx @@ -0,0 +1,102 @@ +import React from 'react' +import { render } from '@testing-library/react' +import { CollectionRadioButtons, ValidationContext } from '.' + +const buildRadioButtonPayload = (value, label, rest) => { + return { + type: 'radio', + value, + label, + name: 'post[subscribe]', + id: `post_subscribe_${value}`, + ...rest, + } +} + +const buildPayload = () => { + return { + collection: [ + buildRadioButtonPayload(1, 'one', { defaultChecked: true }), + buildRadioButtonPayload(2, 'two'), + ], + includeHidden: true, + label: 'Subscribe', + required: false, + } +} + +describe('CollectionRadioButtons', () => { + it('renders', () => { + const payload = buildPayload() + + const { getByRole } = render( + + ) + + const radioGroup = getByRole('radiogroup', { name: 'Subscribe' }) + expect(radioGroup).not.toBeNull() + + const radio1 = getByRole('radio', { name: 'one' }) + expect(radio1).not.toBeNull() + + const radio2 = getByRole('radio', { name: 'two' }) + expect(radio2).not.toBeNull() + }) + + it('renders nothing when the collection is blank', () => { + const payload = buildPayload() + payload.collection = [] + + const { container } = render( + + ) + + expect(container.innerHTML).toEqual('') + }) + + it('renders hidden', () => { + const payload = buildPayload() + payload.includeHidden = true + + const { container } = render( + + ) + + const hiddenInput = container.querySelector('input[type=hidden]') + expect(hiddenInput.name).toEqual('post[subscribe]') + expect(hiddenInput.value).toEqual('') + }) + + it('renders with error highlighting', async () => { + const payload = buildPayload() + + const validationErrors = { + subscribe: 'id invalid', + } + + const { getByText } = render( + + + + ) + + const errorField = getByText('id invalid') + expect(errorField).not.toBeNull() + }) +}) diff --git a/wrappers/ts/react-spectrum/s2/ColorField.test.jsx b/wrappers/ts/react-spectrum/s2/ColorField.test.jsx new file mode 100644 index 0000000..979abb3 --- /dev/null +++ b/wrappers/ts/react-spectrum/s2/ColorField.test.jsx @@ -0,0 +1,44 @@ +import React from 'react' +import { render } from '@testing-library/react' +import { ColorField, ValidationContext } from '.' + +const buildPayload = () => { + return { + type: 'color', + name: 'post[color]', + id: 'post_color', + required: false, + defaultValue: '#000000', + } +} + +describe('ColorField', () => { + it('renders', () => { + const payload = buildPayload() + + const { getByRole } = render( + + ) + + const input = getByRole('textbox', { name: 'color' }) + expect(input).not.toBeNull() + expect(input.value).toEqual('#000000') + }) + + it('renders with field errors', async () => { + const payload = buildPayload() + + const validationErrors = { + color: 'color invalid', + } + + const { getByText } = render( + + + + ) + + const errorField = getByText('color invalid') + expect(errorField).not.toBeNull() + }) +}) diff --git a/wrappers/ts/react-spectrum/s2/DateField.test.jsx b/wrappers/ts/react-spectrum/s2/DateField.test.jsx new file mode 100644 index 0000000..de5551e --- /dev/null +++ b/wrappers/ts/react-spectrum/s2/DateField.test.jsx @@ -0,0 +1,44 @@ +import React from 'react' +import { render } from '@testing-library/react' +import { DateField, ValidationContext } from '.' + +const buildPayload = () => { + return { + type: 'date', + defaultValue: '2004-06-15', + max: '2010-08-15', + min: '2000-06-15', + name: 'post[birth_date]', + id: 'post_birth_date', + } +} + +describe('DateField', () => { + it('renders', () => { + const payload = buildPayload() + + const { getByRole } = render( + + ) + + const group = getByRole('group', { name: 'Birth Date' }) + expect(group).not.toBeNull() + }) + + it('renders with field errors', async () => { + const payload = buildPayload() + + const validationErrors = { + birth_date: 'birth invalid', + } + + const { getByText } = render( + + + + ) + + const errorField = getByText('birth invalid') + expect(errorField).not.toBeNull() + }) +}) diff --git a/wrappers/ts/react-spectrum/s2/DateTimeLocalField.test.jsx b/wrappers/ts/react-spectrum/s2/DateTimeLocalField.test.jsx new file mode 100644 index 0000000..b0e5e6f --- /dev/null +++ b/wrappers/ts/react-spectrum/s2/DateTimeLocalField.test.jsx @@ -0,0 +1,52 @@ +import React from 'react' +import { render } from '@testing-library/react' +import { DateTimeLocalField, ValidationContext } from '.' + +const buildPayload = () => { + return { + type: 'datetime-local', + defaultValue: '2004-06-15T01:02:03', + max: '2010-08-15T00:00:00', + min: '2000-06-15T00:00:00', + name: 'post[birth_date]', + id: 'post_birth_date', + } +} + +describe('DateTimeLocalField', () => { + it('renders', () => { + const payload = buildPayload() + + const { getByText } = render( + + ) + + const label = getByText('Birth Date') + expect(label).not.toBeNull() + }) + + it('renders with field errors', async () => { + const payload = buildPayload() + + const validationErrors = { + birth_date: 'birth invalid', + } + + const { getByText } = render( + + + + ) + + const errorField = getByText('birth invalid') + expect(errorField).not.toBeNull() + }) +}) diff --git a/wrappers/ts/react-spectrum/s2/EmailField.test.jsx b/wrappers/ts/react-spectrum/s2/EmailField.test.jsx new file mode 100644 index 0000000..150c019 --- /dev/null +++ b/wrappers/ts/react-spectrum/s2/EmailField.test.jsx @@ -0,0 +1,44 @@ +import React from 'react' +import { render } from '@testing-library/react' +import { EmailField, ValidationContext } from '.' + +const buildPayload = () => { + return { + type: 'email', + name: 'post[email]', + id: 'post_email', + required: false, + defaultValue: 'john@smith.com', + } +} + +describe('EmailField', () => { + it('renders', () => { + const payload = buildPayload() + + const { getByRole } = render( + + ) + + const input = getByRole('textbox', { name: 'Email' }) + expect(input).not.toBeNull() + expect(input.value).toEqual('john@smith.com') + }) + + it('renders with field errors', async () => { + const payload = buildPayload() + + const validationErrors = { + email: 'email invalid', + } + + const { getByText } = render( + + + + ) + + const errorField = getByText('email invalid') + expect(errorField).not.toBeNull() + }) +}) diff --git a/wrappers/ts/react-spectrum/s2/FileField.test.jsx b/wrappers/ts/react-spectrum/s2/FileField.test.jsx new file mode 100644 index 0000000..a23a756 --- /dev/null +++ b/wrappers/ts/react-spectrum/s2/FileField.test.jsx @@ -0,0 +1,42 @@ +import React from 'react' +import { render } from '@testing-library/react' +import { FileField, ValidationContext } from '.' + +const buildPayload = () => { + return { + type: 'file', + name: 'post[attachment]', + id: 'post_attachment', + required: false, + } +} + +describe('FileField', () => { + it('renders', () => { + const payload = buildPayload() + + const { getByRole } = render( + + ) + + const button = getByRole('button', { name: 'attachment' }) + expect(button).not.toBeNull() + }) + + it('renders with field errors', async () => { + const payload = buildPayload() + + const validationErrors = { + attachment: 'attachment invalid', + } + + const { getByText } = render( + + + + ) + + const errorField = getByText('attachment invalid') + expect(errorField).not.toBeNull() + }) +}) diff --git a/wrappers/ts/react-spectrum/s2/MonthField.test.jsx b/wrappers/ts/react-spectrum/s2/MonthField.test.jsx new file mode 100644 index 0000000..7da26b7 --- /dev/null +++ b/wrappers/ts/react-spectrum/s2/MonthField.test.jsx @@ -0,0 +1,44 @@ +import React from 'react' +import { render } from '@testing-library/react' +import { MonthField, ValidationContext } from '.' + +const buildPayload = () => { + return { + type: 'month', + defaultValue: '2004-06', + name: 'post[written_on]', + id: 'post_written_on', + } +} + +describe('MonthField', () => { + it('renders', () => { + const payload = buildPayload() + + const { container, getByText } = render( + + ) + + getByText('Month') + const input = container.querySelector('input[type="month"]') + expect(input).not.toBeNull() + expect(input.value).toEqual('2004-06') + }) + + it('renders with field errors', async () => { + const payload = buildPayload() + + const validationErrors = { + month: 'Month invalid', + } + + const { getByText } = render( + + + + ) + + const errorField = getByText('Month invalid') + expect(errorField).not.toBeNull() + }) +}) diff --git a/wrappers/ts/react-spectrum/s2/NumberField.test.jsx b/wrappers/ts/react-spectrum/s2/NumberField.test.jsx new file mode 100644 index 0000000..5191f95 --- /dev/null +++ b/wrappers/ts/react-spectrum/s2/NumberField.test.jsx @@ -0,0 +1,51 @@ +import React from 'react' +import { render } from '@testing-library/react' +import { NumberField, ValidationContext } from '.' + +const buildPayload = () => { + return { + type: 'number', + defaultValue: '3', + name: 'post[favs]', + min: 1, + max: 9, + id: 'post_favs', + step: 2, + } +} + +describe('NumberField', () => { + it('renders', () => { + const payload = buildPayload() + + const { getByRole, container } = render( + + ) + + const input = getByRole('textbox', { name: 'Favs' }) + expect(input).not.toBeNull() + + const hiddenInput = container.querySelector( + 'input[type=hidden][name="post[favs]"]' + ) + expect(hiddenInput).not.toBeNull() + expect(hiddenInput.value).toEqual('3') + }) + + it('renders with field errors', async () => { + const payload = buildPayload() + + const validationErrors = { + favs: 'favs invalid', + } + + const { getByText } = render( + + + + ) + + const errorField = getByText('favs invalid') + expect(errorField).not.toBeNull() + }) +}) diff --git a/wrappers/ts/react-spectrum/s2/PasswordField.test.jsx b/wrappers/ts/react-spectrum/s2/PasswordField.test.jsx new file mode 100644 index 0000000..311550d --- /dev/null +++ b/wrappers/ts/react-spectrum/s2/PasswordField.test.jsx @@ -0,0 +1,44 @@ +import React from 'react' +import { render } from '@testing-library/react' +import { PasswordField, ValidationContext } from '.' + +const buildPayload = () => { + return { + type: 'password', + name: 'post[password]', + id: 'post_password', + required: false, + defaultValue: 'Password123', + } +} + +describe('PasswordField', () => { + it('renders', () => { + const payload = buildPayload() + + const { container } = render( + + ) + + const input = container.querySelector('input[type="password"]') + expect(input).not.toBeNull() + expect(input.value).toEqual('Password123') + }) + + it('renders with field errors', async () => { + const payload = buildPayload() + + const validationErrors = { + password: 'Does not match', + } + + const { getByText } = render( + + + + ) + + const errorField = getByText('Does not match') + expect(errorField).not.toBeNull() + }) +}) diff --git a/wrappers/ts/react-spectrum/s2/RangeField.test.jsx b/wrappers/ts/react-spectrum/s2/RangeField.test.jsx new file mode 100644 index 0000000..2cb7cab --- /dev/null +++ b/wrappers/ts/react-spectrum/s2/RangeField.test.jsx @@ -0,0 +1,50 @@ +import React from 'react' +import { render } from '@testing-library/react' +import { RangeField, ValidationContext } from '.' + +const buildPayload = () => { + return { + type: 'range', + defaultValue: '2', + name: 'post[volume]', + min: 1, + max: 9, + id: 'post_volume', + step: 2, + } +} + +describe('RangeField', () => { + it('renders', () => { + const payload = buildPayload() + + const { getByRole, container } = render( + + ) + + const slider = getByRole('slider', { name: 'Volume' }) + expect(slider).not.toBeNull() + + const hiddenInput = container.querySelector('input[type="hidden"]') + expect(hiddenInput).not.toBeNull() + expect(hiddenInput.name).toEqual('post[volume]') + expect(hiddenInput.value).toEqual('2') + }) + + it('renders with field errors', async () => { + const payload = buildPayload() + + const validationErrors = { + volume: 'volume invalid', + } + + const { getByText } = render( + + + + ) + + const errorField = getByText('volume invalid') + expect(errorField).not.toBeNull() + }) +}) diff --git a/wrappers/ts/react-spectrum/s2/SearchField.test.jsx b/wrappers/ts/react-spectrum/s2/SearchField.test.jsx new file mode 100644 index 0000000..8090bff --- /dev/null +++ b/wrappers/ts/react-spectrum/s2/SearchField.test.jsx @@ -0,0 +1,46 @@ +import React from 'react' +import { render } from '@testing-library/react' +import { SearchField, ValidationContext } from '.' + +const buildPayload = () => { + return { + type: 'search', + defaultValue: 'Hello World', + name: 'post[search]', + id: 'post_search', + } +} + +describe('SearchField', () => { + it('renders', () => { + const payload = buildPayload() + + const { getByRole } = render( + + ) + + const input = getByRole('searchbox', { name: 'Search Posts' }) + expect(input).not.toBeNull() + expect(input.value).toEqual('Hello World') + }) + + it('renders with field errors', async () => { + const payload = buildPayload() + + const validationErrors = { + search: 'title invalid', + } + + const { getByText, getByRole } = render( + + + + ) + + const errorField = getByText('title invalid') + expect(errorField).not.toBeNull() + + const input = getByRole('searchbox', { name: 'Search Posts' }) + expect(input.value).toEqual('Hello World') + }) +}) diff --git a/wrappers/ts/react-spectrum/s2/Select.test.jsx b/wrappers/ts/react-spectrum/s2/Select.test.jsx new file mode 100644 index 0000000..55c2d54 --- /dev/null +++ b/wrappers/ts/react-spectrum/s2/Select.test.jsx @@ -0,0 +1,89 @@ +import React from 'react' +import { render } from '@testing-library/react' +import { Select, ValidationContext } from '.' + +const buildPayload = () => { + return { + type: 'select', + name: 'post[category]', + id: 'post_category', + defaultValue: '', + options: [ + { value: 'abe', label: 'abe' }, + { value: '', label: '' }, + { value: 'hest', label: 'hest' }, + ], + } +} + +describe('Select', () => { + describe('rendering', () => { + it('renders a Picker with the correct label', () => { + const payload = buildPayload() + + const { getByRole } = render( + + ) + + const hiddenInput = container.querySelector('input[type=hidden]') + expect(hiddenInput).not.toBe(null) + expect(hiddenInput.name).toEqual('post[category]') + }) + + it('excludes a hidden input on multiple selects if includeHidden is false', () => { + const payload = buildPayload() + payload.multiple = false + payload.includeHidden = false + + const { container } = render( + + ) + + const button = getByRole('button', { name: /category/ }) + expect(button).not.toBeNull() + }) + }) +}) diff --git a/wrappers/ts/react-spectrum/s2/TelField.test.jsx b/wrappers/ts/react-spectrum/s2/TelField.test.jsx new file mode 100644 index 0000000..2bec655 --- /dev/null +++ b/wrappers/ts/react-spectrum/s2/TelField.test.jsx @@ -0,0 +1,44 @@ +import React from 'react' +import { render } from '@testing-library/react' +import { TelField, ValidationContext } from '.' + +const buildPayload = () => { + return { + type: 'tel', + name: 'post[phone]', + id: 'post_phone', + required: false, + defaultValue: '555-1234', + } +} + +describe('TelField', () => { + it('renders', () => { + const payload = buildPayload() + + const { getByRole } = render( + + ) + + const input = getByRole('textbox', { name: 'phone' }) + expect(input).not.toBeNull() + expect(input.value).toEqual('555-1234') + }) + + it('renders with field errors', async () => { + const payload = buildPayload() + + const validationErrors = { + phone_number: 'phone number invalid', + } + + const { getByText } = render( + + + + ) + + const errorField = getByText('phone number invalid') + expect(errorField).not.toBeNull() + }) +}) diff --git a/wrappers/ts/react-spectrum/s2/TextArea.test.jsx b/wrappers/ts/react-spectrum/s2/TextArea.test.jsx new file mode 100644 index 0000000..2c5997a --- /dev/null +++ b/wrappers/ts/react-spectrum/s2/TextArea.test.jsx @@ -0,0 +1,47 @@ +import React from 'react' +import { render } from '@testing-library/react' +import { TextArea, ValidationContext } from '.' + +const buildPayload = () => { + return { + type: 'text', + name: 'post[category]', + id: 'post_category', + required: false, + defaultValue: 'books', + } +} + +describe('TextArea', () => { + it('renders', () => { + const payload = buildPayload() + + const { getByRole } = render( +