diff --git a/.changeset/chilly-clocks-train.md b/.changeset/chilly-clocks-train.md new file mode 100644 index 00000000..80ff5aed --- /dev/null +++ b/.changeset/chilly-clocks-train.md @@ -0,0 +1,5 @@ +--- +'@reactive-forms/core': patch +--- + +Created useStringField hook diff --git a/.changeset/healthy-comics-refuse.md b/.changeset/healthy-comics-refuse.md new file mode 100644 index 00000000..b99cfadb --- /dev/null +++ b/.changeset/healthy-comics-refuse.md @@ -0,0 +1,5 @@ +--- +'@reactive-forms/core': minor +--- + +Created useDecimalField hook diff --git a/.changeset/red-badgers-doubt.md b/.changeset/red-badgers-doubt.md new file mode 100644 index 00000000..c1a17c52 --- /dev/null +++ b/.changeset/red-badgers-doubt.md @@ -0,0 +1,5 @@ +--- +'@reactive-forms/core': minor +--- + +Created useIntegerField hook diff --git a/.changeset/red-flies-study.md b/.changeset/red-flies-study.md new file mode 100644 index 00000000..a070de20 --- /dev/null +++ b/.changeset/red-flies-study.md @@ -0,0 +1,5 @@ +--- +'@reactive-forms/core': patch +--- + +Created useDateField hook diff --git a/.changeset/stale-cars-heal.md b/.changeset/stale-cars-heal.md new file mode 100644 index 00000000..e89f101e --- /dev/null +++ b/.changeset/stale-cars-heal.md @@ -0,0 +1,5 @@ +--- +'@reactive-forms/core': minor +--- + +Created useBooleanField hook diff --git a/.gitignore b/.gitignore index 7c73a981..e6e1e2ff 100644 --- a/.gitignore +++ b/.gitignore @@ -21,4 +21,7 @@ yarn-error.log* scripts/*.mjs coverage -**/.turbo \ No newline at end of file +**/.turbo + +# vscode +launch.json diff --git a/.vscode/launch.json b/.vscode/launch.json index b1f33dea..b6f2cad4 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -5,8 +5,8 @@ "name": "Debug Core Tests", "type": "node", "request": "launch", - "runtimeArgs": ["--inspect-brk", "${workspaceFolder}/node_modules/aqu/dist/aqu.js", "test", "--runInBand"], - "cwd": "${workspaceFolder}/packages/core", + "runtimeArgs": ["--inspect-brk", ".\\node_modules\\jest\\bin\\jest.js", "--watch", "useDecimalField"], + "cwd": "${workspaceFolder}/packages/x", "console": "integratedTerminal", "internalConsoleOptions": "neverOpen", "port": 9229 diff --git a/.vscode/launch.template.json b/.vscode/launch.template.json new file mode 100644 index 00000000..e3c68b49 --- /dev/null +++ b/.vscode/launch.template.json @@ -0,0 +1,15 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Debug Core Tests", + "type": "node", + "request": "launch", + "runtimeArgs": ["--inspect-brk", ".\\node_modules\\jest\\bin\\jest.js", "--watch"], + "cwd": "${workspaceFolder}/packages/core", + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen", + "port": 9229 + } + ] +} diff --git a/packages/core/aqu.config.json b/packages/core/aqu.config.json deleted file mode 100644 index cd4658b7..00000000 --- a/packages/core/aqu.config.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "buildOptions": { - "target": ["es2019", "chrome58", "firefox57", "safari11", "edge18", "node12"], - "preserveSymlinks": false - }, - "dtsBundleGeneratorOptions": { - "libraries": { - "importedLibraries": ["stocked", "yup", "react", "pxth"], - "allowedTypesLibraries": [] - } - } -} diff --git a/packages/core/jest.config.js b/packages/core/jest.config.js index 0bbb8647..e6227517 100644 --- a/packages/core/jest.config.js +++ b/packages/core/jest.config.js @@ -6,7 +6,7 @@ const config = { preset: 'ts-jest', moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'cjs', 'mjs', 'json', 'node'], collectCoverageFrom: ['src/**/*.{ts,tsx,js,jsx,cjs,mjs}'], - testMatch: ['/**/*.(spec|test).{ts,tsx,js,jsx,cjs,mjs}'], + testMatch: ['/tests/**/*.(spec|test).{ts,tsx,js,jsx,cjs,mjs}'], testEnvironmentOptions: { url: 'http://localhost', }, diff --git a/packages/core/package.json b/packages/core/package.json index f67d1987..cabc42e9 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -15,8 +15,8 @@ "directory": "prepared-package" }, "scripts": { - "build": "aqu build && rimraf ./prepared-package && clean-publish", - "dev": "aqu watch --no-cleanup", + "build": "tsc && rimraf ./prepared-package && clean-publish", + "dev": "tsc --watch", "lint": "eslint .", "lint:fix": "npm run lint --fix", "test": "jest --passWithNoTests", @@ -24,6 +24,7 @@ "test:watch": "jest --passWithNoTests --watch" }, "dependencies": { + "dayjs": "^1.11.9", "lodash": "4.17.21", "lodash-es": "4.17.15", "pxth": "0.7.0", @@ -33,17 +34,16 @@ "devDependencies": { "@babel/core": "7.19.6", "@reactive-tools/eslint-config": "workspace:*", - "@testing-library/react": "13.4.0", - "@types/jest": "26.0.24", + "@testing-library/react": "14.0.0", + "@types/jest": "29.5.4", "@types/lodash": "4.14.161", "@types/node": "^18.11.18", "@types/react": "18.0.23", - "aqu": "0.4.3", - "jest": "29.2.2", + "jest": "29.7.0", "react": "18.2.0", "react-dom": "18.2.0", "rimraf": "3.0.2", - "ts-jest": "29.0.3", + "ts-jest": "29.1.1", "tslib": "2.3.1", "typescript": "4.8.4", "yup": "0.32.9" diff --git a/packages/core/src/components/BooleanField/BooleanFieldI18n.tsx b/packages/core/src/components/BooleanField/BooleanFieldI18n.tsx new file mode 100644 index 00000000..da548842 --- /dev/null +++ b/packages/core/src/components/BooleanField/BooleanFieldI18n.tsx @@ -0,0 +1,22 @@ +import React, { createContext, PropsWithChildren } from 'react'; +import merge from 'lodash/merge'; + +export type BooleanFieldI18n = { + required: string; +}; + +export const defaultBooleanFieldI18n: BooleanFieldI18n = { + required: 'Field is required', +}; + +export const BooleanFieldI18nContext = createContext(defaultBooleanFieldI18n); + +export type BooleanFieldI18nContextProviderProps = PropsWithChildren<{ i18n?: Partial }>; + +export const BooleanFieldI18nContextProvider = ({ i18n, children }: BooleanFieldI18nContextProviderProps) => { + return ( + + {children} + + ); +}; diff --git a/packages/core/src/components/BooleanField/index.ts b/packages/core/src/components/BooleanField/index.ts new file mode 100644 index 00000000..82adc36e --- /dev/null +++ b/packages/core/src/components/BooleanField/index.ts @@ -0,0 +1,7 @@ +export { type BooleanFieldBag, type BooleanFieldConfig, useBooleanField } from './useBooleanField'; +export { + type BooleanFieldI18n, + BooleanFieldI18nContextProvider, + type BooleanFieldI18nContextProviderProps, + defaultBooleanFieldI18n, +} from './BooleanFieldI18n'; diff --git a/packages/core/src/components/BooleanField/useBooleanField.ts b/packages/core/src/components/BooleanField/useBooleanField.ts new file mode 100644 index 00000000..ff28d37c --- /dev/null +++ b/packages/core/src/components/BooleanField/useBooleanField.ts @@ -0,0 +1,44 @@ +import { useContext } from 'react'; + +import { BooleanFieldI18nContext } from './BooleanFieldI18n'; +import { useFieldValidator } from '../../helpers/useFieldValidator'; +import { FieldContext } from '../../typings/FieldContext'; +import { FieldConfig, useField } from '../Field/useField'; + +export type BooleanFieldConfig = FieldConfig & { + required?: boolean; +}; + +export type BooleanFieldBag = FieldContext & { + onBlur: () => void; +}; + +export const useBooleanField = ({ required, ...config }: BooleanFieldConfig) => { + const fieldBag = useField(config); + + const { + control: { setTouched }, + } = fieldBag; + + const i18n = useContext(BooleanFieldI18nContext); + + const onBlur = () => { + setTouched({ $touched: true }); + }; + + useFieldValidator({ + name: config.name, + validator: (value) => { + if (required && !value) { + return i18n.required; + } + + return undefined; + }, + }); + + return { + ...fieldBag, + onBlur, + }; +}; diff --git a/packages/core/src/components/ConverterField/index.ts b/packages/core/src/components/ConverterField/index.ts new file mode 100644 index 00000000..e92556ce --- /dev/null +++ b/packages/core/src/components/ConverterField/index.ts @@ -0,0 +1,6 @@ +export { + useConverterField, + ConversionError, + type ConverterFieldBag, + type ConverterFieldConfig, +} from './useConverterField'; diff --git a/packages/x/src/useConverterField.ts b/packages/core/src/components/ConverterField/useConverterField.ts similarity index 87% rename from packages/x/src/useConverterField.ts rename to packages/core/src/components/ConverterField/useConverterField.ts index c0fdc236..0d479a7c 100644 --- a/packages/x/src/useConverterField.ts +++ b/packages/core/src/components/ConverterField/useConverterField.ts @@ -1,17 +1,24 @@ import { useCallback, useEffect, useRef, useState } from 'react'; -import { FieldConfig, FieldContext, FieldError, FieldTouched, useField, useFieldValidator } from '@reactive-forms/core'; import isObject from 'lodash/isObject'; +import { useFieldValidator } from '../../helpers'; +import { FieldContext } from '../../typings/FieldContext'; +import { FieldError } from '../../typings/FieldError'; +import { FieldTouched } from '../../typings/FieldTouched'; +import { FieldConfig, useField } from '../Field/useField'; + export class ConversionError extends Error { public constructor(errorMessage: string) { super(errorMessage); } } -export type ConverterFieldConfig = { +export type ValueConverter = { parse: (value: string) => T; format: (value: T) => string; -} & FieldConfig; +}; + +export type ConverterFieldConfig = ValueConverter & FieldConfig; export type ConverterFieldBag = { text: string; diff --git a/packages/core/src/components/DateField/DateFieldI18n.tsx b/packages/core/src/components/DateField/DateFieldI18n.tsx new file mode 100644 index 00000000..246b0db8 --- /dev/null +++ b/packages/core/src/components/DateField/DateFieldI18n.tsx @@ -0,0 +1,30 @@ +import React, { createContext, PropsWithChildren } from 'react'; +import merge from 'lodash/merge'; + +import { formatDate } from '../../utils/formatDate'; + +export type DateFieldI18n = { + required: string; + invalidInput: string; + minDate: (min: Date, pickTime: boolean) => string; + maxDate: (max: Date, pickTime: boolean) => string; +}; + +export const defaultDateFieldI18n: DateFieldI18n = { + required: 'Field is required', + invalidInput: 'Must be date', + minDate: (min, pickTime) => `Date must not be earlier than ${formatDate(min, pickTime)}`, + maxDate: (max, pickTime) => `Date must not be later than ${formatDate(max, pickTime)}`, +}; + +export const DateFieldI18nContext = createContext(defaultDateFieldI18n); + +export type DateFieldI18nContextProviderProps = PropsWithChildren<{ i18n?: Partial }>; + +export const DateFieldI18nContextProvider = ({ i18n, children }: DateFieldI18nContextProviderProps) => { + return ( + + {children} + + ); +}; diff --git a/packages/core/src/components/DateField/index.ts b/packages/core/src/components/DateField/index.ts new file mode 100644 index 00000000..7af4cfae --- /dev/null +++ b/packages/core/src/components/DateField/index.ts @@ -0,0 +1,7 @@ +export { type DateFieldBag, type DateFieldConfig, useDateField } from './useDateField'; +export { + type DateFieldI18n, + DateFieldI18nContextProvider, + type DateFieldI18nContextProviderProps, + defaultDateFieldI18n, +} from './DateFieldI18n'; diff --git a/packages/core/src/components/DateField/useDateField.ts b/packages/core/src/components/DateField/useDateField.ts new file mode 100644 index 00000000..02408dd3 --- /dev/null +++ b/packages/core/src/components/DateField/useDateField.ts @@ -0,0 +1,112 @@ +import { useCallback, useContext } from 'react'; +import dayjs from 'dayjs'; +import customParseFormat from 'dayjs/plugin/customParseFormat'; + +import { DateFieldI18nContext } from './DateFieldI18n'; +import { useFieldValidator } from '../../helpers'; +import { formatDate } from '../../utils'; +import { ConversionError, ConverterFieldBag, useConverterField } from '../ConverterField'; +import { ValueConverter } from '../ConverterField/useConverterField'; +import { FieldConfig } from '../Field'; + +dayjs.extend(customParseFormat); + +const defaultDateFormats = ['DD.MM.YYYY', 'YYYY-MM-DD', 'YYYY/MM/DD', 'YYYY.MM.DD', 'DD-MM-YYYY', 'DD/MM/YYYY']; +const defaultDateTimeFormats = [ + 'DD.MM.YYYY HH:mm', + 'YYYY-MM-DD HH:mm', + 'YYYY/MM/DD HH:mm', + 'YYYY.MM.DD HH:mm', + 'DD-MM-YYYY HH:mm', + 'DD/MM/YYYY HH:mm', +]; + +export type DateFieldConfig = FieldConfig & { + required?: boolean; + minDate?: Date; + maxDate?: Date; + pickTime?: boolean; +} & Partial>; + +export type DateFieldBag = ConverterFieldBag; + +export const useDateField = ({ + name, + validator, + schema, + required, + minDate, + maxDate, + pickTime = false, + format: customFormatDate, + parse: customParseDate, +}: DateFieldConfig): DateFieldBag => { + const i18n = useContext(DateFieldI18nContext); + + const parse = useCallback( + (text: string) => { + text = text.trim(); + + if (customParseDate) { + return customParseDate(text); + } + + if (text.length === 0) { + return null; + } + + const date = dayjs(text, [...defaultDateFormats, ...(pickTime ? defaultDateTimeFormats : [])], true); + + if (!date.isValid()) { + throw new ConversionError(i18n.invalidInput); + } + + return date.toDate(); + }, + [customParseDate, i18n.invalidInput, pickTime], + ); + + const format = useCallback( + (value: Date | null | undefined) => { + if (customFormatDate) { + return customFormatDate(value); + } + + return formatDate(value, pickTime); + }, + [customFormatDate, pickTime], + ); + + const dateBag = useConverterField({ + parse, + format, + name, + validator, + schema, + }); + + useFieldValidator({ + name, + validator: (value) => { + if (required && !(value instanceof Date)) { + return i18n.required; + } + + if (!(value instanceof Date)) { + return undefined; + } + + if (minDate instanceof Date && dayjs(minDate).diff(dayjs(value)) > 0) { + return i18n.minDate(minDate, pickTime); + } + + if (maxDate instanceof Date && dayjs(value).diff(dayjs(maxDate)) > 0) { + return i18n.maxDate(maxDate, pickTime); + } + + return undefined; + }, + }); + + return dateBag; +}; diff --git a/packages/core/src/components/DecimalField/DecimalFieldI18n.tsx b/packages/core/src/components/DecimalField/DecimalFieldI18n.tsx new file mode 100644 index 00000000..20d9cc66 --- /dev/null +++ b/packages/core/src/components/DecimalField/DecimalFieldI18n.tsx @@ -0,0 +1,30 @@ +import React, { createContext, PropsWithChildren } from 'react'; +import merge from 'lodash/merge'; + +import { formatDecimal } from '../../utils'; + +export type DecimalFieldI18n = { + required: string; + invalidInput: string; + minValue: (value: number, precision: number) => string; + maxValue: (value: number, precision: number) => string; +}; + +export const defaultDecimalFieldI18n: DecimalFieldI18n = { + required: 'Field is required', + invalidInput: 'Must be decimal', + minValue: (min: number, precision: number) => `Value should not be less than ${formatDecimal(min, precision)}`, + maxValue: (max: number, precision: number) => `Value should not be greater than ${formatDecimal(max, precision)}`, +}; + +export const DecimalFieldI18nContext = createContext(defaultDecimalFieldI18n); + +export type DecimalFieldI18nContextProviderProps = PropsWithChildren<{ i18n?: Partial }>; + +export const DecimalFieldI18nContextProvider = ({ i18n, children }: DecimalFieldI18nContextProviderProps) => { + return ( + + {children} + + ); +}; diff --git a/packages/core/src/components/DecimalField/index.ts b/packages/core/src/components/DecimalField/index.ts new file mode 100644 index 00000000..10f61b04 --- /dev/null +++ b/packages/core/src/components/DecimalField/index.ts @@ -0,0 +1,7 @@ +export { type DecimalFieldBag, type DecimalFieldConfig, useDecimalField } from './useDecimalField'; +export { + type DecimalFieldI18n, + DecimalFieldI18nContextProvider, + type DecimalFieldI18nContextProviderProps, + defaultDecimalFieldI18n, +} from './DecimalFieldI18n'; diff --git a/packages/core/src/components/DecimalField/useDecimalField.ts b/packages/core/src/components/DecimalField/useDecimalField.ts new file mode 100644 index 00000000..a45d0028 --- /dev/null +++ b/packages/core/src/components/DecimalField/useDecimalField.ts @@ -0,0 +1,110 @@ +import { useCallback, useContext } from 'react'; + +import { DecimalFieldI18nContext } from './DecimalFieldI18n'; +import { useFieldValidator } from '../../helpers'; +import { formatDecimal } from '../../utils/formatDecimal'; +import { + ConversionError, + ConverterFieldBag, + useConverterField, + ValueConverter, +} from '../ConverterField/useConverterField'; +import { FieldConfig } from '../Field/useField'; + +const DECIMAL_REGEX = /^\d*\.?\d*$/; +export const defaultPrecision = 2; + +export type DecimalFieldConfig = FieldConfig & { + required?: boolean; + min?: number; + max?: number; + + precision?: number; +} & Partial>; + +export type DecimalFieldBag = ConverterFieldBag; + +export const useDecimalField = ({ + name, + validator, + schema, + required, + min, + max, + format: customFormat, + parse: customParse, + precision = defaultPrecision, +}: DecimalFieldConfig): DecimalFieldBag => { + const i18n = useContext(DecimalFieldI18nContext); + + const parse = useCallback( + (text: string) => { + text = text.trim(); + + if (customParse) { + return customParse(text); + } + + if (text.length === 0) { + return null; + } + + if (!DECIMAL_REGEX.test(text)) { + throw new ConversionError(i18n.invalidInput); + } + + const value = Number.parseFloat(text); + + if (Number.isNaN(value)) { + throw new ConversionError(i18n.invalidInput); + } + + return value; + }, + [customParse, i18n.invalidInput], + ); + + const format = useCallback( + (value: number | null | undefined) => { + if (customFormat) { + return customFormat(value); + } + + return formatDecimal(value, precision); + }, + [customFormat, precision], + ); + + const decimalBag = useConverterField({ + parse, + format, + name, + validator, + schema, + }); + + useFieldValidator({ + name, + validator: (value) => { + if (required && typeof value !== 'number') { + return i18n.required; + } + + if (typeof value !== 'number') { + return undefined; + } + + if (typeof min === 'number' && value < min) { + return i18n.minValue(min, precision); + } + + if (typeof max === 'number' && value > max) { + return i18n.maxValue(max, precision); + } + + return undefined; + }, + }); + + return decimalBag; +}; diff --git a/packages/core/src/components/Field.tsx b/packages/core/src/components/Field/Field.tsx similarity index 72% rename from packages/core/src/components/Field.tsx rename to packages/core/src/components/Field/Field.tsx index 77715c2b..8f89ad57 100644 --- a/packages/core/src/components/Field.tsx +++ b/packages/core/src/components/Field/Field.tsx @@ -1,7 +1,7 @@ import React from 'react'; -import { FieldConfig, useField } from '../hooks/useField'; -import { FieldContext } from '../typings/FieldContext'; +import { FieldConfig, useField } from './useField'; +import { FieldContext } from '../../typings/FieldContext'; export type FieldProps = FieldConfig & { children: (ctx: FieldContext) => React.ReactNode; diff --git a/packages/core/src/components/Field/index.ts b/packages/core/src/components/Field/index.ts new file mode 100644 index 00000000..5519f716 --- /dev/null +++ b/packages/core/src/components/Field/index.ts @@ -0,0 +1,2 @@ +export { Field, type FieldProps } from './Field'; +export { type FieldConfig, useField } from './useField'; diff --git a/packages/core/src/hooks/useField.ts b/packages/core/src/components/Field/useField.ts similarity index 61% rename from packages/core/src/hooks/useField.ts rename to packages/core/src/components/Field/useField.ts index 765d9396..d09e8355 100644 --- a/packages/core/src/hooks/useField.ts +++ b/packages/core/src/components/Field/useField.ts @@ -1,10 +1,10 @@ import { Pxth } from 'pxth'; -import { useFieldError } from './useFieldError'; -import { useFieldTouched } from './useFieldTouched'; -import { FieldValidationProps, useFieldValidator } from './useFieldValidator'; -import { useFieldValue } from './useFieldValue'; -import { FieldContext } from '../typings/FieldContext'; +import { useFieldError } from '../../helpers/useFieldError'; +import { useFieldTouched } from '../../helpers/useFieldTouched'; +import { FieldValidationProps, useFieldValidator } from '../../helpers/useFieldValidator'; +import { useFieldValue } from '../../helpers/useFieldValue'; +import { FieldContext } from '../../typings/FieldContext'; export type FieldConfig = { name: Pxth; diff --git a/packages/core/src/components/FormContext.ts b/packages/core/src/components/Form/FormContext.ts similarity index 81% rename from packages/core/src/components/FormContext.ts rename to packages/core/src/components/Form/FormContext.ts index 83678533..7166ada4 100644 --- a/packages/core/src/components/FormContext.ts +++ b/packages/core/src/components/Form/FormContext.ts @@ -1,6 +1,6 @@ import { createContext } from 'react'; -import { FormShared } from '../hooks/useForm'; +import { FormShared } from './useForm'; export type FormContextType = FormShared; diff --git a/packages/core/src/components/FormPlugins.tsx b/packages/core/src/components/Form/FormPlugins.tsx similarity index 88% rename from packages/core/src/components/FormPlugins.tsx rename to packages/core/src/components/Form/FormPlugins.tsx index 9d4e8460..fe20c814 100644 --- a/packages/core/src/components/FormPlugins.tsx +++ b/packages/core/src/components/Form/FormPlugins.tsx @@ -1,8 +1,8 @@ import React, { createContext, PropsWithChildren, useRef } from 'react'; import invariant from 'tiny-invariant'; -import { Plugin } from '../typings/Plugin'; -import { PluginArray } from '../typings/PluginArray'; +import { Plugin } from '../../typings/Plugin'; +import { PluginArray } from '../../typings/PluginArray'; export const FormPluginsContext = createContext([]); diff --git a/packages/core/src/components/FormProxyProvider.tsx b/packages/core/src/components/Form/FormProxyProvider.tsx similarity index 91% rename from packages/core/src/components/FormProxyProvider.tsx rename to packages/core/src/components/Form/FormProxyProvider.tsx index 0c8331f4..6b59787a 100644 --- a/packages/core/src/components/FormProxyProvider.tsx +++ b/packages/core/src/components/Form/FormProxyProvider.tsx @@ -3,7 +3,7 @@ import { Pxth } from 'pxth'; import { StockProxy } from 'stocked'; import { FormContext } from './FormContext'; -import { useProxyInterception } from '../hooks/useProxyInterception'; +import { useProxyInterception } from '../../helpers'; export type FormProxyProviderProps = { proxy: StockProxy; diff --git a/packages/core/src/components/ReactiveForm.tsx b/packages/core/src/components/Form/ReactiveForm.tsx similarity index 75% rename from packages/core/src/components/ReactiveForm.tsx rename to packages/core/src/components/Form/ReactiveForm.tsx index 222121f4..9de9021c 100644 --- a/packages/core/src/components/ReactiveForm.tsx +++ b/packages/core/src/components/Form/ReactiveForm.tsx @@ -1,7 +1,7 @@ import React, { PropsWithChildren } from 'react'; import { ReactiveFormProvider } from './ReactiveFormProvider'; -import { FormConfig, useForm } from '../hooks/useForm'; +import { FormConfig, useForm } from './useForm'; export type ReactiveFormProps = PropsWithChildren>; @@ -16,7 +16,10 @@ export type ReactiveFormProps = PropsWithChildren * ``` */ -export const ReactiveForm = ({ children, ...config }: ReactiveFormProps) => { +export const ReactiveForm = >({ + children, + ...config +}: ReactiveFormProps) => { const formBag = useForm(config); return {children}; diff --git a/packages/core/src/components/ReactiveFormConsumer.tsx b/packages/core/src/components/Form/ReactiveFormConsumer.tsx similarity index 88% rename from packages/core/src/components/ReactiveFormConsumer.tsx rename to packages/core/src/components/Form/ReactiveFormConsumer.tsx index 7d3f6757..1efed0ee 100644 --- a/packages/core/src/components/ReactiveFormConsumer.tsx +++ b/packages/core/src/components/Form/ReactiveFormConsumer.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { FormContextType } from './FormContext'; -import { useFormContext } from '../hooks/useFormContext'; +import { useFormContext } from '../../helpers'; export type ReactiveFormConsumerProps = { children: (shared: FormContextType) => React.ReactNode; diff --git a/packages/core/src/components/ReactiveFormProvider.tsx b/packages/core/src/components/Form/ReactiveFormProvider.tsx similarity index 90% rename from packages/core/src/components/ReactiveFormProvider.tsx rename to packages/core/src/components/Form/ReactiveFormProvider.tsx index 5ee499c4..4a58eb05 100644 --- a/packages/core/src/components/ReactiveFormProvider.tsx +++ b/packages/core/src/components/Form/ReactiveFormProvider.tsx @@ -1,7 +1,7 @@ import React, { PropsWithChildren } from 'react'; import { FormContext } from './FormContext'; -import { FormShared } from '../hooks/useForm'; +import { FormShared } from './useForm'; export type ReactiveFormProviderProps = PropsWithChildren<{ formBag: FormShared; diff --git a/packages/core/src/components/Form/index.ts b/packages/core/src/components/Form/index.ts new file mode 100644 index 00000000..da84c5f1 --- /dev/null +++ b/packages/core/src/components/Form/index.ts @@ -0,0 +1,6 @@ +export { useForm, type FormConfig, type FormShared } from './useForm'; +export { FormPlugins, FormPluginsContext, type FormPluginsProps } from './FormPlugins'; +export { FormProxyContext, FormProxyProvider, type FormProxyProviderProps } from './FormProxyProvider'; +export { ReactiveForm, type ReactiveFormProps } from './ReactiveForm'; +export { ReactiveFormConsumer, type ReactiveFormConsumerProps } from './ReactiveFormConsumer'; +export { ReactiveFormProvider, type ReactiveFormProviderProps } from './ReactiveFormProvider'; diff --git a/packages/core/src/hooks/useForm.ts b/packages/core/src/components/Form/useForm.ts similarity index 83% rename from packages/core/src/hooks/useForm.ts rename to packages/core/src/components/Form/useForm.ts index 114ae142..443eda83 100644 --- a/packages/core/src/hooks/useForm.ts +++ b/packages/core/src/components/Form/useForm.ts @@ -9,22 +9,26 @@ import invariant from 'tiny-invariant'; import type { BaseSchema } from 'yup'; import { useFormControl } from './useFormControl'; -import { usePluginBagDecorators, usePluginConfigDecorators } from './usePlugins'; -import { useValidationRegistry, ValidationRegistryControl } from './useValidationRegistry'; -import { FieldError } from '../typings/FieldError'; -import { FieldPostProcessor } from '../typings/FieldPostProcessor'; -import { FieldTouched } from '../typings/FieldTouched'; -import { FieldValidator } from '../typings/FieldValidator'; -import { FormHelpers } from '../typings/FormHelpers'; -import { FormMeta } from '../typings/FormMeta'; -import { SubmitAction } from '../typings/SubmitAction'; -import { deepRemoveEmpty } from '../utils/deepRemoveEmpty'; -import { excludeOverlaps } from '../utils/excludeOverlaps'; -import { overrideMerge } from '../utils/overrideMerge'; -import { runYupSchema } from '../utils/runYupSchema'; -import { setNestedValues } from '../utils/setNestedValues'; -import { useRefCallback } from '../utils/useRefCallback'; -import { validatorResultToError } from '../utils/validatorResultToError'; +import { + usePluginBagDecorators, + usePluginConfigDecorators, + useValidationRegistry, + ValidationRegistryControl, +} from '../../helpers'; +import { FieldError } from '../../typings/FieldError'; +import { FieldPostProcessor } from '../../typings/FieldPostProcessor'; +import { FieldTouched } from '../../typings/FieldTouched'; +import { FieldValidator } from '../../typings/FieldValidator'; +import { FormHelpers } from '../../typings/FormHelpers'; +import { FormMeta } from '../../typings/FormMeta'; +import { SubmitAction } from '../../typings/SubmitAction'; +import { deepRemoveEmpty } from '../../utils/deepRemoveEmpty'; +import { excludeOverlaps } from '../../utils/excludeOverlaps'; +import { overrideMerge } from '../../utils/overrideMerge'; +import { runYupSchema } from '../../utils/runYupSchema'; +import { setNestedValues } from '../../utils/setNestedValues'; +import { useRefCallback } from '../../utils/useRefCallback'; +import { validatorResultToError } from '../../utils/validatorResultToError'; export type InitialFormStateConfig = { initialValues: Values; @@ -77,7 +81,9 @@ const deepCustomizer = (src1: unknown, src2: unknown) => { const formMetaPaths = createPxth([]); -export const useForm = (initialConfig: FormConfig): FormShared => { +export const useForm = >( + initialConfig: FormConfig, +): FormShared => { const config = usePluginConfigDecorators(initialConfig); const { schema, disablePureFieldsValidation } = config; @@ -126,12 +132,19 @@ export const useForm = (initialConfig: FormConfig }, []); const normalizeErrors = useCallback( - (errors: FieldError, source: object, compare: object): FieldError => { + ( + errors: FieldError, + source: Record, + compare: Record, + ): FieldError => { if (!disablePureFieldsValidation) { return errors; } - return merge(setNestedValues(errors as object, undefined), excludeOverlaps(source, compare, errors)); + return merge( + setNestedValues(errors as Record, undefined), + excludeOverlaps(source, compare, errors), + ); }, [disablePureFieldsValidation], ); @@ -147,8 +160,8 @@ export const useForm = (initialConfig: FormConfig return normalizeErrors( valueErrors, - deepGet(allValues, name) as object, - deepGet(initialValuesRef.current, name) as object, + deepGet(allValues, name) as Record, + deepGet(initialValuesRef.current, name) as Record, ); } @@ -282,22 +295,23 @@ export const useForm = (initialConfig: FormConfig ); const updateFormValidness = useCallback( - ({ values }: BatchUpdate) => setFormMeta(formMetaPaths.isValid, deepRemoveEmpty(values) === undefined), + ({ values }: BatchUpdate>) => + setFormMeta(formMetaPaths.isValid, deepRemoveEmpty(values) === undefined), [setFormMeta], ); const validateUpdatedFields = useCallback( - async ({ values, origin }: BatchUpdate) => { + async ({ values, origin }: BatchUpdate>) => { const { attachPath, errors } = await validateBranch(origin, values); const onlyNecessaryErrors = deepGet(errors, attachPath) as FieldError; const normalizedErrors = normalizeErrors( onlyNecessaryErrors, values, - deepGet(initialValuesRef.current, attachPath) as object, + deepGet(initialValuesRef.current, attachPath) as Record, ); - setFieldError(attachPath, (old) => overrideMerge(old ?? {}, normalizedErrors as object)); + setFieldError(attachPath, (old) => overrideMerge(old ?? {}, normalizedErrors as Record)); }, [normalizeErrors, setFieldError, validateBranch], ); diff --git a/packages/core/src/hooks/useFormControl.ts b/packages/core/src/components/Form/useFormControl.ts similarity index 81% rename from packages/core/src/hooks/useFormControl.ts rename to packages/core/src/components/Form/useFormControl.ts index 0fc255ff..7ae839a1 100644 --- a/packages/core/src/hooks/useFormControl.ts +++ b/packages/core/src/components/Form/useFormControl.ts @@ -1,9 +1,9 @@ import { Stock, useStock } from 'stocked'; -import { ControlHandlers, useControlHandlers } from './useControlHandlers'; -import { FieldError } from '../typings/FieldError'; -import { FieldTouched } from '../typings/FieldTouched'; -import { FormMeta } from '../typings/FormMeta'; +import { ControlHandlers, useControlHandlers } from '../../helpers/useControlHandlers'; +import { FieldError } from '../../typings/FieldError'; +import { FieldTouched } from '../../typings/FieldTouched'; +import { FormMeta } from '../../typings/FormMeta'; export type FormControl = { formMeta: Stock; diff --git a/packages/core/src/components/IntegerField/IntegerFieldI18n.tsx b/packages/core/src/components/IntegerField/IntegerFieldI18n.tsx new file mode 100644 index 00000000..0a8fe0ee --- /dev/null +++ b/packages/core/src/components/IntegerField/IntegerFieldI18n.tsx @@ -0,0 +1,30 @@ +import React, { createContext, PropsWithChildren } from 'react'; +import merge from 'lodash/merge'; + +export type IntegerFieldI18n = { + required: string; + invalidInput: string; + minValue: (value: number) => string; + maxValue: (value: number) => string; +}; + +export const defaultIntegerFieldI18n: IntegerFieldI18n = { + required: 'Field is required', + invalidInput: 'Must be integer', + minValue: (min: number) => `Value should not be less than ${min.toFixed(0)}`, + maxValue: (max: number) => `Value should not be greater than ${max.toFixed(0)}`, +}; + +export const IntegerFieldI18nContext = createContext(defaultIntegerFieldI18n); + +export type IntegerFieldI18nContextProviderProps = PropsWithChildren<{ + i18n?: Partial; +}>; + +export const IntegerFieldI18nContextProvider = ({ children, i18n }: IntegerFieldI18nContextProviderProps) => { + return ( + + {children} + + ); +}; diff --git a/packages/core/src/components/IntegerField/index.ts b/packages/core/src/components/IntegerField/index.ts new file mode 100644 index 00000000..fa200f31 --- /dev/null +++ b/packages/core/src/components/IntegerField/index.ts @@ -0,0 +1,7 @@ +export { type IntegerFieldBag, type IntegerFieldConfig, useIntegerField } from './useIntegerField'; +export { + type IntegerFieldI18n, + IntegerFieldI18nContextProvider, + type IntegerFieldI18nContextProviderProps, + defaultIntegerFieldI18n, +} from './IntegerFieldI18n'; diff --git a/packages/core/src/components/IntegerField/useIntegerField.ts b/packages/core/src/components/IntegerField/useIntegerField.ts new file mode 100644 index 00000000..75fce837 --- /dev/null +++ b/packages/core/src/components/IntegerField/useIntegerField.ts @@ -0,0 +1,95 @@ +import { useCallback, useContext } from 'react'; + +import { IntegerFieldI18nContext } from './IntegerFieldI18n'; +import { useFieldValidator } from '../../helpers/useFieldValidator'; +import { formatInteger } from '../../utils/formatInteger'; +import { + ConversionError, + ConverterFieldBag, + useConverterField, + ValueConverter, +} from '../ConverterField/useConverterField'; +import { FieldConfig } from '../Field/useField'; + +const INTEGER_REGEX = /^-?\d+$/; + +export type IntegerFieldConfig = FieldConfig & { + required?: boolean; + min?: number; + max?: number; +} & Partial>; + +export type IntegerFieldBag = ConverterFieldBag; + +export const useIntegerField = ({ + name, + validator, + schema, + required, + min, + max, + parse: customParse, + format = formatInteger, +}: IntegerFieldConfig): IntegerFieldBag => { + const i18n = useContext(IntegerFieldI18nContext); + + const parse = useCallback( + (text: string) => { + text = text.trim(); + + if (customParse) { + return customParse(text); + } + + if (text.length === 0) { + return null; + } + + if (!INTEGER_REGEX.test(text)) { + throw new ConversionError(i18n.invalidInput); + } + + const value = Number.parseInt(text); + + if (Number.isNaN(value)) { + throw new ConversionError(i18n.invalidInput); + } + + return value; + }, + [customParse, i18n.invalidInput], + ); + + const integerBag = useConverterField({ + parse, + format, + name, + validator, + schema, + }); + + useFieldValidator({ + name, + validator: (value) => { + if (required && typeof value !== 'number') { + return i18n.required; + } + + if (typeof value !== 'number') { + return undefined; + } + + if (typeof min === 'number' && value < min) { + return i18n.minValue(min); + } + + if (typeof max === 'number' && value > max) { + return i18n.maxValue(max); + } + + return undefined; + }, + }); + + return integerBag; +}; diff --git a/packages/core/src/components/ObjectField/index.ts b/packages/core/src/components/ObjectField/index.ts new file mode 100644 index 00000000..a197e28c --- /dev/null +++ b/packages/core/src/components/ObjectField/index.ts @@ -0,0 +1 @@ +export { type ObjectFieldConfig, type ObjectFieldProps, useObjectField } from './useObjectField'; diff --git a/packages/core/src/hooks/useObjectField.ts b/packages/core/src/components/ObjectField/useObjectField.ts similarity index 90% rename from packages/core/src/hooks/useObjectField.ts rename to packages/core/src/components/ObjectField/useObjectField.ts index 4fe9f628..9ee73395 100644 --- a/packages/core/src/hooks/useObjectField.ts +++ b/packages/core/src/components/ObjectField/useObjectField.ts @@ -1,9 +1,9 @@ import { useCallback } from 'react'; import { deepSet, Pxth } from 'pxth'; -import { FieldConfig, useField } from './useField'; -import { FieldError } from '../typings/FieldError'; -import { FieldTouched } from '../typings/FieldTouched'; +import { FieldError } from '../../typings/FieldError'; +import { FieldTouched } from '../../typings/FieldTouched'; +import { FieldConfig, useField } from '../Field/useField'; export type ObjectFieldConfig = {} & FieldConfig; diff --git a/packages/core/src/components/StringField/StringFieldI18n.tsx b/packages/core/src/components/StringField/StringFieldI18n.tsx new file mode 100644 index 00000000..a62f3f46 --- /dev/null +++ b/packages/core/src/components/StringField/StringFieldI18n.tsx @@ -0,0 +1,26 @@ +import React, { createContext, PropsWithChildren } from 'react'; +import merge from 'lodash/merge'; + +export type StringFieldI18n = { + required: string; + minLength: (length: number) => string; + maxLength: (length: number) => string; +}; + +export const defaultStringFieldI18n: StringFieldI18n = { + required: 'Field is required', + minLength: (minLength: number) => `String should not include less than ${minLength} character(s)`, + maxLength: (maxLength: number) => `String should not include more than ${maxLength} character(s)`, +}; + +export const StringFieldI18nContext = createContext(defaultStringFieldI18n); + +export type StringFieldI18nContextProviderProps = PropsWithChildren<{ i18n?: Partial }>; + +export const StringFieldI18nContextProvider = ({ i18n, children }: StringFieldI18nContextProviderProps) => { + return ( + + {children} + + ); +}; diff --git a/packages/core/src/components/StringField/index.ts b/packages/core/src/components/StringField/index.ts new file mode 100644 index 00000000..95cdd02f --- /dev/null +++ b/packages/core/src/components/StringField/index.ts @@ -0,0 +1,7 @@ +export { type StringFieldBag, type StringFieldConfig, useStringField } from './useStringField'; +export { + type StringFieldI18n, + StringFieldI18nContextProvider, + type StringFieldI18nContextProviderProps, + defaultStringFieldI18n, +} from './StringFieldI18n'; diff --git a/packages/core/src/components/StringField/useStringField.ts b/packages/core/src/components/StringField/useStringField.ts new file mode 100644 index 00000000..de396aa9 --- /dev/null +++ b/packages/core/src/components/StringField/useStringField.ts @@ -0,0 +1,58 @@ +import { useCallback, useContext } from 'react'; + +import { StringFieldI18nContext } from './StringFieldI18n'; +import { useFieldValidator } from '../../helpers/useFieldValidator'; +import { FieldContext } from '../../typings/FieldContext'; +import { FieldConfig, useField } from '../Field/useField'; + +export type StringFieldConfig = FieldConfig & { + required?: boolean; + minLength?: number; + maxLength?: number; +}; + +export type StringFieldBag = FieldContext & { + onBlur: () => void; +}; + +export const useStringField = ({ name, validator, schema, required, maxLength, minLength }: StringFieldConfig) => { + const fieldBag = useField({ name, validator, schema }); + + const { + control: { setTouched }, + } = fieldBag; + + const i18n = useContext(StringFieldI18nContext); + + useFieldValidator({ + name, + validator: (value: string | undefined | null) => { + const isValueEmpty = !value || value.trim().length === 0; + + if (required && isValueEmpty) { + return i18n.required; + } + + const valueLength = value?.length ?? 0; + + if (typeof minLength === 'number' && valueLength < minLength) { + return i18n.minLength(minLength); + } + + if (typeof maxLength === 'number' && valueLength > maxLength) { + return i18n.maxLength(maxLength); + } + + return undefined; + }, + }); + + const onBlur = useCallback(() => { + setTouched({ $touched: true }); + }, [setTouched]); + + return { + onBlur, + ...fieldBag, + }; +}; diff --git a/packages/core/src/helpers/index.ts b/packages/core/src/helpers/index.ts new file mode 100644 index 00000000..c30a51a1 --- /dev/null +++ b/packages/core/src/helpers/index.ts @@ -0,0 +1,20 @@ +export { type ArrayControl, type ArrayControlConfig, useArrayControl } from './useArrayControl'; +export { type ArrayFieldConfig, type ArrayFieldProps, useArrayField } from './useArrayField'; +export { type ControlHandlers, type ControlHandlersConfig, useControlHandlers } from './useControlHandlers'; +export { useFieldError } from './useFieldError'; +export { useFieldTouched } from './useFieldTouched'; +export { type FieldValidationProps, type UseFieldValidatorConfig, useFieldValidator } from './useFieldValidator'; +export { useFieldValue } from './useFieldValue'; +export { type FieldValueArrayConfig, useFieldValueArray } from './useFieldValueArray'; +export { useFormContext } from './useFormContext'; +export { type FormControl, type FormControlConfig, useFormControl } from '../components/Form/useFormControl'; +export { useFormMeta } from './useFormMeta'; +export { usePluginAssertion } from './usePluginAssertion'; +export { usePluginBagDecorators, usePluginConfigDecorators } from './usePlugins'; +export { useProxyInterception } from './useProxyInterception'; +export { useSubmitAction } from './useSubmitAction'; +export { + type ValidationRegistry, + type ValidationRegistryControl, + useValidationRegistry, +} from './useValidationRegistry'; diff --git a/packages/core/src/hooks/useArrayControl.ts b/packages/core/src/helpers/useArrayControl.ts similarity index 100% rename from packages/core/src/hooks/useArrayControl.ts rename to packages/core/src/helpers/useArrayControl.ts diff --git a/packages/core/src/hooks/useArrayField.ts b/packages/core/src/helpers/useArrayField.ts similarity index 91% rename from packages/core/src/hooks/useArrayField.ts rename to packages/core/src/helpers/useArrayField.ts index d38f6bbe..e34d5470 100644 --- a/packages/core/src/hooks/useArrayField.ts +++ b/packages/core/src/helpers/useArrayField.ts @@ -1,7 +1,7 @@ import { Pxth } from 'pxth'; import { ArrayControl, useArrayControl } from './useArrayControl'; -import { FieldConfig, useField } from './useField'; +import { FieldConfig, useField } from '../components/Field/useField'; import { FieldError } from '../typings/FieldError'; import { FieldTouched } from '../typings/FieldTouched'; diff --git a/packages/core/src/hooks/useControlHandlers.ts b/packages/core/src/helpers/useControlHandlers.ts similarity index 100% rename from packages/core/src/hooks/useControlHandlers.ts rename to packages/core/src/helpers/useControlHandlers.ts diff --git a/packages/core/src/hooks/useFieldError.ts b/packages/core/src/helpers/useFieldError.ts similarity index 100% rename from packages/core/src/hooks/useFieldError.ts rename to packages/core/src/helpers/useFieldError.ts diff --git a/packages/core/src/hooks/useFieldTouched.ts b/packages/core/src/helpers/useFieldTouched.ts similarity index 100% rename from packages/core/src/hooks/useFieldTouched.ts rename to packages/core/src/helpers/useFieldTouched.ts diff --git a/packages/core/src/hooks/useFieldValidator.ts b/packages/core/src/helpers/useFieldValidator.ts similarity index 100% rename from packages/core/src/hooks/useFieldValidator.ts rename to packages/core/src/helpers/useFieldValidator.ts diff --git a/packages/core/src/hooks/useFieldValue.ts b/packages/core/src/helpers/useFieldValue.ts similarity index 100% rename from packages/core/src/hooks/useFieldValue.ts rename to packages/core/src/helpers/useFieldValue.ts diff --git a/packages/core/src/hooks/useFieldValueArray.ts b/packages/core/src/helpers/useFieldValueArray.ts similarity index 78% rename from packages/core/src/hooks/useFieldValueArray.ts rename to packages/core/src/helpers/useFieldValueArray.ts index a2850ea6..6b3974c6 100644 --- a/packages/core/src/hooks/useFieldValueArray.ts +++ b/packages/core/src/helpers/useFieldValueArray.ts @@ -13,12 +13,13 @@ export const useFieldValueArray = (paths: FieldValueArrayConfi getFieldValue, } = useFormContext(); - const [object, setObject] = useState(() => - Object.entries(paths).reduce((acc, [to, from]) => { - acc[to] = getFieldValue(from as Pxth); + const [object, setObject] = useState( + () => + Object.entries(paths).reduce((acc, [to, from]) => { + acc[to] = getFieldValue(from as Pxth); - return acc; - }, {} as T), + return acc; + }, {} as Record) as T, ); useEffect(() => { diff --git a/packages/core/src/hooks/useFormContext.ts b/packages/core/src/helpers/useFormContext.ts similarity index 96% rename from packages/core/src/hooks/useFormContext.ts rename to packages/core/src/helpers/useFormContext.ts index 68cd194a..e6b683da 100644 --- a/packages/core/src/hooks/useFormContext.ts +++ b/packages/core/src/helpers/useFormContext.ts @@ -1,7 +1,7 @@ import { useContext } from 'react'; import invariant from 'tiny-invariant'; -import { FormContext, FormContextType } from '../components/FormContext'; +import { FormContext, FormContextType } from '../components/Form/FormContext'; export const useFormContext = (): FormContextType => { const context = useContext(FormContext); diff --git a/packages/core/src/hooks/useFormMeta.ts b/packages/core/src/helpers/useFormMeta.ts similarity index 100% rename from packages/core/src/hooks/useFormMeta.ts rename to packages/core/src/helpers/useFormMeta.ts diff --git a/packages/core/src/hooks/usePluginAssertion.ts b/packages/core/src/helpers/usePluginAssertion.ts similarity index 82% rename from packages/core/src/hooks/usePluginAssertion.ts rename to packages/core/src/helpers/usePluginAssertion.ts index 972a858e..8a25d381 100644 --- a/packages/core/src/hooks/usePluginAssertion.ts +++ b/packages/core/src/helpers/usePluginAssertion.ts @@ -1,7 +1,7 @@ import { useContext } from 'react'; import invariant from 'tiny-invariant'; -import { FormPluginsContext } from '../components/FormPlugins'; +import { FormPluginsContext } from '../components/Form/FormPlugins'; import { Plugin } from '../typings/Plugin'; export const usePluginAssertion = (plugin: Plugin, message: string) => { diff --git a/packages/core/src/hooks/usePlugins.ts b/packages/core/src/helpers/usePlugins.ts similarity index 81% rename from packages/core/src/hooks/usePlugins.ts rename to packages/core/src/helpers/usePlugins.ts index 1bbc9636..c5d8e748 100644 --- a/packages/core/src/hooks/usePlugins.ts +++ b/packages/core/src/helpers/usePlugins.ts @@ -1,7 +1,7 @@ import { useContext } from 'react'; -import { FormConfig, FormShared } from './useForm'; -import { FormPluginsContext } from '../components/FormPlugins'; +import { FormPluginsContext } from '../components/Form/FormPlugins'; +import { FormConfig, FormShared } from '../components/Form/useForm'; export const usePluginBagDecorators = (bag: FormShared, config: FormConfig): FormShared => { const plugins = useContext(FormPluginsContext); diff --git a/packages/core/src/hooks/useProxyInterception.ts b/packages/core/src/helpers/useProxyInterception.ts similarity index 98% rename from packages/core/src/hooks/useProxyInterception.ts rename to packages/core/src/helpers/useProxyInterception.ts index 218714a8..66b562ce 100644 --- a/packages/core/src/hooks/useProxyInterception.ts +++ b/packages/core/src/helpers/useProxyInterception.ts @@ -3,8 +3,8 @@ import { deepGet, deepSet, Pxth, relativePxth } from 'pxth'; import { intercept, StockProxy, useStockContext } from 'stocked'; import { useControlHandlers } from './useControlHandlers'; -import { FormShared } from './useForm'; import { useFormContext } from './useFormContext'; +import { FormShared } from '../components/Form/useForm'; import { FieldError } from '../typings/FieldError'; import { FieldValidator } from '../typings/FieldValidator'; diff --git a/packages/core/src/hooks/useSubmitAction.ts b/packages/core/src/helpers/useSubmitAction.ts similarity index 100% rename from packages/core/src/hooks/useSubmitAction.ts rename to packages/core/src/helpers/useSubmitAction.ts diff --git a/packages/core/src/hooks/useValidationRegistry.ts b/packages/core/src/helpers/useValidationRegistry.ts similarity index 100% rename from packages/core/src/hooks/useValidationRegistry.ts rename to packages/core/src/helpers/useValidationRegistry.ts diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 826e59ed..c0f08266 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,38 +1,14 @@ -// main component -export { ReactiveForm as default } from './components/ReactiveForm'; - -// components +export * from './components/BooleanField'; +export * from './components/StringField'; +export * from './components/IntegerField'; +export * from './components/DecimalField'; +export * from './components/ConverterField'; +export * from './components/DateField'; export * from './components/Field'; -export * from './components/ReactiveFormProvider'; -export * from './components/ReactiveFormConsumer'; -export * from './components/FormProxyProvider'; -export * from './components/FormPlugins'; +export * from './components/Form'; +export * from './components/ObjectField'; -// hooks -export * from './hooks/useField'; -export * from './hooks/useArrayControl'; -export * from './hooks/useArrayField'; -export * from './hooks/useFieldValidator'; -export * from './hooks/useForm'; -export * from './hooks/useFormContext'; -export * from './hooks/useFormMeta'; -export * from './hooks/useFieldError'; -export * from './hooks/useFieldTouched'; -export * from './hooks/useFieldValue'; -export * from './hooks/useObjectField'; -export * from './hooks/useSubmitAction'; -export * from './hooks/useValidationRegistry'; -export * from './hooks/usePluginAssertion'; -export * from './hooks/useFieldValueArray'; +export * from './helpers'; +export * from './typings'; -// typings -export * from './typings/FieldContext'; -export * from './typings/FieldValidator'; -export * from './typings/FieldError'; -export * from './typings/FieldMeta'; -export * from './typings/FormMeta'; -export * from './typings/FieldTouched'; -export * from './typings/SubmitAction'; -export * from './typings/FormHelpers'; -export * from './typings/Plugin'; -export * from './typings/PluginArray'; +export { ReactiveForm as default } from './components/Form/ReactiveForm'; diff --git a/packages/core/src/typings/FormHelpers.ts b/packages/core/src/typings/FormHelpers.ts index c1a4b386..f7df836b 100644 --- a/packages/core/src/typings/FormHelpers.ts +++ b/packages/core/src/typings/FormHelpers.ts @@ -2,8 +2,8 @@ import { Pxth } from 'pxth'; import { FieldError } from './FieldError'; import { FieldPostProcessor } from './FieldPostProcessor'; -import { InitialFormState } from '../hooks/useForm'; -import { FormControl } from '../hooks/useFormControl'; +import { InitialFormState } from '../components/Form/useForm'; +import { FormControl } from '../components/Form/useFormControl'; export type FormHelpers = FormControl & { validateForm: (values: Values) => Promise>; diff --git a/packages/core/src/typings/Plugin.ts b/packages/core/src/typings/Plugin.ts index ff294add..e92d4b81 100644 --- a/packages/core/src/typings/Plugin.ts +++ b/packages/core/src/typings/Plugin.ts @@ -1,4 +1,4 @@ -import { FormConfig, FormShared } from '../hooks/useForm'; +import { FormConfig, FormShared } from '../components/Form/useForm'; export type Plugin = { token: Symbol; diff --git a/packages/core/src/typings/index.ts b/packages/core/src/typings/index.ts new file mode 100644 index 00000000..c466949f --- /dev/null +++ b/packages/core/src/typings/index.ts @@ -0,0 +1,12 @@ +export * from './FieldContext'; +export * from './FieldError'; +export * from './FieldMeta'; +export * from './FieldPostProcessor'; +export * from './FieldTouched'; +export * from './FieldValidator'; +export * from './FormHelpers'; +export * from './FormMeta'; +export * from './NestedObject'; +export * from './Plugin'; +export * from './PluginArray'; +export * from './SubmitAction'; diff --git a/packages/core/src/utils/deepRemoveEmpty.ts b/packages/core/src/utils/deepRemoveEmpty.ts index 52ed2066..15e5146b 100644 --- a/packages/core/src/utils/deepRemoveEmpty.ts +++ b/packages/core/src/utils/deepRemoveEmpty.ts @@ -14,15 +14,17 @@ export const deepRemoveEmpty = (obj: object): object | undefined => { ); return Object.values(newArr).every(isNil) ? undefined : newArr; } else if (obj !== null && typeof obj === 'object') { + const casted = obj as Record; const newObj = Object.keys(obj).reduce((acc, key) => { - const value = typeof obj[key] === 'object' ? deepRemoveEmpty(obj[key]) : obj[key]; + const value = + typeof casted[key] === 'object' ? deepRemoveEmpty(casted[key] as Record) : casted[key]; if (!isNil(value)) { acc[key] = value; } return acc; - }, {}); + }, {} as Record); return isEmpty(newObj) ? undefined : newObj; } return undefined; diff --git a/packages/core/src/utils/excludeOverlaps.ts b/packages/core/src/utils/excludeOverlaps.ts index 8266a91d..7c02cd5b 100644 --- a/packages/core/src/utils/excludeOverlaps.ts +++ b/packages/core/src/utils/excludeOverlaps.ts @@ -3,7 +3,11 @@ import { RootPathToken } from 'pxth'; import { getDifferenceMap } from './getDifferenceMap'; -export const excludeOverlaps = (source: object, compare: object, exclusionObject: T): Partial => { +export const excludeOverlaps = ( + source: Record, + compare: Record, + exclusionObject: T, +): Partial => { const diffMap = getDifferenceMap(source, compare); if (diffMap[RootPathToken]) return {}; diff --git a/packages/core/src/utils/flattenObject.ts b/packages/core/src/utils/flattenObject.ts index f46076e6..9e7186f8 100644 --- a/packages/core/src/utils/flattenObject.ts +++ b/packages/core/src/utils/flattenObject.ts @@ -2,8 +2,8 @@ import { RootPath, RootPathToken } from 'pxth'; import { joinPaths } from './joinPaths'; -export const flattenObject = (obj: object): Record => { - const queue: Array<[string | RootPath, object]> = [[RootPathToken, obj]]; +export const flattenObject = (obj: Record): Record => { + const queue: Array<[string | RootPath, Record]> = [[RootPathToken, obj]]; const result: Record = {}; @@ -16,7 +16,7 @@ export const flattenObject = (obj: object): Record => { if (typeof item !== 'object' || item === null || Object.keys(item).length === 0) { result[pathToItem] = item; } else { - queue.push([pathToItem, item]); + queue.push([pathToItem, item as Record]); } } } diff --git a/packages/core/src/utils/formatDate.ts b/packages/core/src/utils/formatDate.ts new file mode 100644 index 00000000..10e57ef8 --- /dev/null +++ b/packages/core/src/utils/formatDate.ts @@ -0,0 +1,9 @@ +import dayjs from 'dayjs'; + +export const formatDate = (value: Date | null | undefined, pickTime: boolean) => { + if (!(value instanceof Date)) { + return ''; + } + + return dayjs(value).format(`YYYY.MM.DD${pickTime ? ' HH:mm' : ''}`); +}; diff --git a/packages/core/src/utils/formatDecimal.ts b/packages/core/src/utils/formatDecimal.ts new file mode 100644 index 00000000..8da992d3 --- /dev/null +++ b/packages/core/src/utils/formatDecimal.ts @@ -0,0 +1,7 @@ +export const formatDecimal = (value: number | null | undefined, precision: number) => { + if (typeof value !== 'number' || !Number.isFinite(value)) { + return ''; + } + + return value.toFixed(precision).toString(); +}; diff --git a/packages/core/src/utils/formatInteger.ts b/packages/core/src/utils/formatInteger.ts new file mode 100644 index 00000000..49a9a970 --- /dev/null +++ b/packages/core/src/utils/formatInteger.ts @@ -0,0 +1,7 @@ +export const formatInteger = (value: number | null | undefined) => { + if (typeof value !== 'number' || !Number.isFinite(value)) { + return ''; + } + + return value.toFixed(0); +}; diff --git a/packages/core/src/utils/getDifferenceMap.ts b/packages/core/src/utils/getDifferenceMap.ts index 3ef1ce78..c56602ea 100644 --- a/packages/core/src/utils/getDifferenceMap.ts +++ b/packages/core/src/utils/getDifferenceMap.ts @@ -24,7 +24,7 @@ const isInnerPath = (parent: string | RootPath, child: string) => { return child.indexOf(parent + '.') === 0; }; -export const getDifferenceMap = (obj1: object, obj2: object): DifferenceMap => { +export const getDifferenceMap = (obj1: Record, obj2: Record): DifferenceMap => { const flattenedObj1 = flattenObject(obj1); const flattenedObj2 = flattenObject(obj2); diff --git a/packages/core/src/utils/index.ts b/packages/core/src/utils/index.ts new file mode 100644 index 00000000..1ac34df0 --- /dev/null +++ b/packages/core/src/utils/index.ts @@ -0,0 +1,16 @@ +export * from './deepRemoveEmpty'; +export * from './excludeOverlaps'; +export * from './flattenObject'; +export * from './formatDate'; +export * from './formatDecimal'; +export * from './formatInteger'; +export * from './FunctionArray'; +export * from './getDifferenceMap'; +export * from './isYupError'; +export * from './joinPaths'; +export * from './overrideMerge'; +export * from './runYupSchema'; +export * from './setNestedValues'; +export * from './useRefCallback'; +export * from './validatorResultToError'; +export * from './yupToFormErrors'; diff --git a/packages/core/src/utils/overrideMerge.ts b/packages/core/src/utils/overrideMerge.ts index f1b527be..a6464a62 100644 --- a/packages/core/src/utils/overrideMerge.ts +++ b/packages/core/src/utils/overrideMerge.ts @@ -1,7 +1,7 @@ import cloneDeep from 'lodash/cloneDeep'; import isObject from 'lodash/isObject'; -export const overrideMerge = (object: object, source: object) => { +export const overrideMerge = (object: Record, source: Record) => { object = cloneDeep(object); const queue = [[object, source]]; @@ -19,7 +19,7 @@ export const overrideMerge = (object: object, source: object) => { const compareSourceValue = currentSource[key]; if (isObject(compareValue) && isObject(compareSourceValue)) { - queue.push([compareValue, compareSourceValue]); + queue.push([compareValue as Record, compareSourceValue as Record]); } else { currentObject[key] = cloneDeep(currentSource[key]); } diff --git a/packages/core/src/utils/setNestedValues.ts b/packages/core/src/utils/setNestedValues.ts index 91e4736d..c412c67e 100644 --- a/packages/core/src/utils/setNestedValues.ts +++ b/packages/core/src/utils/setNestedValues.ts @@ -2,13 +2,13 @@ import isObject from 'lodash/isObject'; import { NestedObject } from '../typings/NestedObject'; -export const setNestedValues = ( +export const setNestedValues = , Value>( exampleObject: Example, value: Value, visited: WeakMap = new WeakMap(), output: NestedObject = {} as NestedObject, ): NestedObject => - Object.keys(exampleObject).reduce>((acc, key) => { + Object.keys(exampleObject).reduce((acc, key) => { const part = exampleObject[key]; if (isObject(part)) { @@ -16,11 +16,12 @@ export const setNestedValues = ( visited.set(part, true); acc[key] = Array.isArray(part) ? [] : {}; acc[key] = Object.assign(acc[key], value); - setNestedValues(part, value, visited, acc[key]); + setNestedValues(part as Record, value, visited, acc[key]); } } else { acc[key] = value; } return acc; - }, output); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + }, output as Record) as NestedObject; diff --git a/packages/core/tests/components/FormPlugins.test.tsx b/packages/core/tests/components/FormPlugins.test.tsx index efaf2e44..a3a14f98 100644 --- a/packages/core/tests/components/FormPlugins.test.tsx +++ b/packages/core/tests/components/FormPlugins.test.tsx @@ -12,13 +12,13 @@ import { useFormContext, } from '../../src'; -const renderPlugins = (config: FormConfig, plugins: PluginArray) => { +const renderPlugins = >(config: FormConfig, plugins: PluginArray) => { return renderHook(() => useForm(config), { wrapper: ({ children }) => {children}, }); }; -const renderForm = (config: FormConfig, plugins: PluginArray) => { +const renderForm = >(config: FormConfig, plugins: PluginArray) => { const { result: { current: bag }, } = renderPlugins(config, plugins); diff --git a/packages/core/tests/hooks/useArrayControl.test.tsx b/packages/core/tests/hooks/useArrayControl.test.tsx index 4bd884d7..29cbb943 100644 --- a/packages/core/tests/hooks/useArrayControl.test.tsx +++ b/packages/core/tests/hooks/useArrayControl.test.tsx @@ -4,7 +4,7 @@ import { createPxth, Pxth } from 'pxth'; import { ArrayControl, FormConfig, FormShared, ReactiveFormProvider, useArrayControl, useForm } from '../../src'; -const renderArrayControl = ( +const renderArrayControl = , V>( name: Pxth, config: FormConfig, ): [RenderHookResult, undefined>, FormShared] => { diff --git a/packages/core/tests/hooks/useArrayField.test.tsx b/packages/core/tests/hooks/useArrayField.test.tsx index f01e4e34..9b3d5b0a 100644 --- a/packages/core/tests/hooks/useArrayField.test.tsx +++ b/packages/core/tests/hooks/useArrayField.test.tsx @@ -4,7 +4,7 @@ import { createPxth, Pxth } from 'pxth'; import { ArrayFieldProps, FormConfig, FormShared, ReactiveFormProvider, useArrayField, useForm } from '../../src'; -const renderArrayField = ( +const renderArrayField = >( name: Pxth, config: FormConfig, ): RenderHookResult, undefined> => { diff --git a/packages/core/tests/hooks/useBooleanField.test.tsx b/packages/core/tests/hooks/useBooleanField.test.tsx new file mode 100644 index 00000000..ecc6d95b --- /dev/null +++ b/packages/core/tests/hooks/useBooleanField.test.tsx @@ -0,0 +1,140 @@ +import React from 'react'; +import { act, renderHook, waitFor } from '@testing-library/react'; + +import { + BooleanFieldConfig, + BooleanFieldI18n, + BooleanFieldI18nContextProvider, + defaultBooleanFieldI18n, + ReactiveFormProvider, + useBooleanField, + useForm, +} from '../../src'; + +type Config = Omit & { + initialValue?: boolean; + i18n?: Partial; +}; + +const renderUseBooleanField = (config: Config = {}) => { + const { initialValue = false, i18n, ...initialProps } = config; + + const formBag = renderHook(() => + useForm({ + initialValues: { + test: initialValue, + }, + }), + ); + + const stringFieldBag = renderHook( + (props: Omit) => + useBooleanField({ + name: formBag.result.current.paths.test, + ...props, + }), + { + wrapper: ({ children }) => ( + + {children} + + ), + initialProps, + }, + ); + + return [stringFieldBag, formBag] as const; +}; + +describe('Boolean field', () => { + it('Should set touched=true on blur', async () => { + const [{ result }] = renderUseBooleanField(); + + expect(result.current.meta.touched?.$touched).toBeFalsy(); + + await act(() => { + result.current.onBlur(); + }); + + await waitFor(() => { + expect(result.current.meta.touched?.$touched).toBeTruthy(); + }); + }); + + it('Should set default error if field is required and empty', async () => { + const [{ result }] = renderUseBooleanField({ required: true }); + + act(() => { + result.current.control.setValue(null); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBe(defaultBooleanFieldI18n.required); + }); + + act(() => { + result.current.control.setValue(undefined); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBe(defaultBooleanFieldI18n.required); + }); + + act(() => { + result.current.control.setValue(false); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBe(defaultBooleanFieldI18n.required); + }); + + act(() => { + result.current.control.setValue(true); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBeUndefined(); + }); + }); + + it('Should set custom error if field is required and empty', async () => { + const [{ result }] = renderUseBooleanField({ + required: true, + i18n: { + required: 'custom', + }, + }); + + act(() => { + result.current.control.setValue(null); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBe('custom'); + }); + + act(() => { + result.current.control.setValue(undefined); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBe('custom'); + }); + + act(() => { + result.current.control.setValue(false); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBe('custom'); + }); + + act(() => { + result.current.control.setValue(true); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBeUndefined(); + }); + }); +}); diff --git a/packages/core/tests/hooks/useControlHandlers.test.tsx b/packages/core/tests/hooks/useControlHandlers.test.tsx index c47e7e70..5ceb5055 100644 --- a/packages/core/tests/hooks/useControlHandlers.test.tsx +++ b/packages/core/tests/hooks/useControlHandlers.test.tsx @@ -2,8 +2,7 @@ import React, { PropsWithChildren } from 'react'; import { act, renderHook } from '@testing-library/react'; import { createPxth } from 'pxth'; -import { FormShared, ReactiveFormProvider, useForm } from '../../src'; -import { useControlHandlers } from '../../src/hooks/useControlHandlers'; +import { FormShared, ReactiveFormProvider, useControlHandlers, useForm } from '../../src'; const renderControlHandlers = () => { const { diff --git a/packages/x/tests/useConverterField.test.tsx b/packages/core/tests/hooks/useConverterField.test.tsx similarity index 97% rename from packages/x/tests/useConverterField.test.tsx rename to packages/core/tests/hooks/useConverterField.test.tsx index 66854721..109e41c4 100644 --- a/packages/x/tests/useConverterField.test.tsx +++ b/packages/core/tests/hooks/useConverterField.test.tsx @@ -1,8 +1,7 @@ import React from 'react'; -import { ReactiveFormProvider, useForm } from '@reactive-forms/core'; import { act, renderHook, waitFor } from '@testing-library/react'; -import { ConversionError, useConverterField } from '../src/useConverterField'; +import { ConversionError, ReactiveFormProvider, useConverterField, useForm } from '../../src'; const defaultParse = (text: string) => { const parsingResult = Number.parseInt(text); diff --git a/packages/core/tests/hooks/useDateField.test.tsx b/packages/core/tests/hooks/useDateField.test.tsx new file mode 100644 index 00000000..bcdaee72 --- /dev/null +++ b/packages/core/tests/hooks/useDateField.test.tsx @@ -0,0 +1,300 @@ +import React from 'react'; +import { act, renderHook, waitFor } from '@testing-library/react'; + +import { + DateFieldConfig, + DateFieldI18n, + DateFieldI18nContextProvider, + defaultDateFieldI18n, + ReactiveFormProvider, + useDateField, + useForm, +} from '../../src'; +import { formatDate } from '../../src/utils/formatDate'; + +type Config = Omit & { + initialValue?: Date | null; + i18n?: Partial; +}; + +const renderUseDateField = (config: Config = {}) => { + const { initialValue = null, i18n, ...initialProps } = config; + + const formBag = renderHook(() => + useForm({ + initialValues: { + test: initialValue, + }, + }), + ); + + const dateFieldBag = renderHook( + (props: Omit) => + useDateField({ + name: formBag.result.current.paths.test, + ...props, + }), + { + wrapper: ({ children }) => ( + + {children} + + ), + initialProps, + }, + ); + + return [dateFieldBag, formBag] as const; +}; + +describe('Date field', () => { + it('Should format initial value correctly', () => { + const initialValue = new Date(); + + const [{ result }] = renderUseDateField({ initialValue }); + + expect(result.current.text).toBe(formatDate(initialValue, false)); + expect(result.current.value?.getTime()).toBe(initialValue.getTime()); + }); + + it('Should set default conversion error correctly', async () => { + const [{ result }] = renderUseDateField(); + + await act(() => { + result.current.onTextChange('2000-20-20'); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBe(defaultDateFieldI18n.invalidInput); + }); + + await act(() => { + result.current.onTextChange('aaaa'); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBe(defaultDateFieldI18n.invalidInput); + }); + + await act(() => { + result.current.onTextChange('1000'); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBe(defaultDateFieldI18n.invalidInput); + }); + + await act(() => { + result.current.onTextChange('2003-07-08'); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBeUndefined(); + }); + + await act(() => { + result.current.onTextChange(''); + }); + + await waitFor(() => { + expect(result.current.value).toBe(null); + expect(result.current.meta.error?.$error).toBeUndefined(); + }); + + await act(() => { + result.current.onTextChange(' '); + }); + + await waitFor(() => { + expect(result.current.value).toBe(null); + expect(result.current.text).toBe(' '); + expect(result.current.meta.error?.$error).toBeUndefined(); + }); + + await act(() => { + result.current.onTextChange('1999/12/12'); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBeUndefined(); + }); + }); + + it('Should set default error if field is required and empty', async () => { + const [{ result }] = renderUseDateField({ required: true }); + + act(() => { + result.current.control.setValue(null); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBe(defaultDateFieldI18n.required); + }); + }); + + it('Should set default error if date is earlier than minDate', async () => { + const minDate = new Date(2000, 0, 5); + const [{ result }] = renderUseDateField({ minDate }); + + await act(() => { + result.current.control.setValue(new Date(2000, 0, 4)); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBe(defaultDateFieldI18n.minDate(minDate, false)); + }); + + await act(() => { + result.current.control.setValue(new Date(2000, 0, 6)); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBeUndefined(); + }); + }); + + it('Should set default error if date is later than maxDate', async () => { + const maxDate = new Date(2000, 0, 5); + const [{ result }] = renderUseDateField({ maxDate }); + + act(() => { + result.current.control.setValue(new Date(2000, 0, 6)); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBe(defaultDateFieldI18n.maxDate(maxDate, false)); + }); + + await act(() => { + result.current.control.setValue(new Date(2000, 0, 4)); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBeUndefined(); + }); + }); + + it('Should set custom conversion error correctly', async () => { + const [{ result }] = renderUseDateField({ + i18n: { + invalidInput: 'custom', + }, + }); + + await act(() => { + result.current.onTextChange('2000-20-20'); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBe('custom'); + }); + + await act(() => { + result.current.onTextChange('aaaa'); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBe('custom'); + }); + + await act(() => { + result.current.onTextChange('1000'); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBe('custom'); + }); + }); + + it('Should set custom error if field is required and empty', async () => { + const [{ result }] = renderUseDateField({ + required: true, + i18n: { + required: 'custom', + }, + }); + + act(() => { + result.current.control.setValue(null); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBe('custom'); + }); + }); + + it('Should set custom error if date is earlier than min date', async () => { + const [{ result }] = renderUseDateField({ + minDate: new Date(42), + i18n: { + minDate: () => 'custom', + }, + }); + + act(() => { + result.current.control.setValue(new Date(41)); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBe('custom'); + }); + }); + + it('Should set custom error if date is later than max date', async () => { + const [{ result }] = renderUseDateField({ + maxDate: new Date(42), + i18n: { + maxDate: () => 'custom', + }, + }); + + act(() => { + result.current.control.setValue(new Date(43)); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBe('custom'); + }); + }); + + it('Should be able to format date differently', () => { + const format = jest.fn(() => 'custom'); + const initialValue = new Date(); + const [{ result }] = renderUseDateField({ format, initialValue }); + + expect(result.current.text).toBe('custom'); + expect(format).toBeCalledWith(initialValue); + }); + + it('Should call custom parseDate function', async () => { + const parse = jest.fn(); + + const [{ result }] = renderUseDateField({ parse }); + + await act(() => { + result.current.onTextChange('2023-09-12'); + }); + + await waitFor(() => { + expect(parse).toBeCalledWith('2023-09-12'); + }); + }); + + it('Should format date with time', () => { + const [{ result }] = renderUseDateField({ pickTime: true, initialValue: new Date(2023, 8, 12, 17, 29) }); + + expect(result.current.text).toBe('2023.09.12 17:29'); + }); + + it('Should parse date with time', async () => { + const [{ result }] = renderUseDateField({ pickTime: true }); + + await act(() => { + result.current.onTextChange('2023-09-12 17:31'); + }); + + await waitFor(() => { + expect(result.current.value?.getTime()).toBe(new Date(2023, 8, 12, 17, 31).getTime()); + }); + }); +}); diff --git a/packages/core/tests/hooks/useDecimalField.test.tsx b/packages/core/tests/hooks/useDecimalField.test.tsx new file mode 100644 index 00000000..3f5fc3a2 --- /dev/null +++ b/packages/core/tests/hooks/useDecimalField.test.tsx @@ -0,0 +1,308 @@ +import React from 'react'; +import { act, renderHook, waitFor } from '@testing-library/react'; + +import { + DecimalFieldConfig, + DecimalFieldI18n, + DecimalFieldI18nContextProvider, + defaultDecimalFieldI18n, + ReactiveFormProvider, + useDecimalField, + useForm, +} from '../../src'; +import { formatDecimal } from '../../src/utils/formatDecimal'; + +type Config = Omit & { + initialValue?: number | null; + i18n?: Partial; +}; + +const renderUseDecimalField = (config: Config = {}) => { + const { initialValue = 0, i18n, ...initialProps } = config; + + const formBag = renderHook(() => + useForm({ + initialValues: { + test: initialValue, + }, + }), + ); + + const decimalFieldBag = renderHook( + (props: Omit) => + useDecimalField({ + name: formBag.result.current.paths.test, + ...props, + }), + { + wrapper: ({ children }) => ( + + {children} + + ), + initialProps, + }, + ); + + return [decimalFieldBag, formBag] as const; +}; + +describe('Decimal field', () => { + it('Should format initial value correctly', () => { + const [{ result }] = renderUseDecimalField(); + + expect(result.current.text).toBe(formatDecimal(0, 2)); + expect(result.current.value).toBe(0); + }); + + it('Should set default conversion error correctly', async () => { + const [{ result }] = renderUseDecimalField(); + + await act(() => { + result.current.onTextChange('0a'); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBe(defaultDecimalFieldI18n.invalidInput); + }); + + await act(() => { + result.current.onTextChange('a0'); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBe(defaultDecimalFieldI18n.invalidInput); + }); + + await act(() => { + result.current.onTextChange('hello'); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBe(defaultDecimalFieldI18n.invalidInput); + }); + + await act(() => { + result.current.onTextChange('0'); + }); + + await waitFor(() => { + expect(result.current.value).toBe(0); + expect(result.current.meta.error?.$error).toBeUndefined(); + }); + + await act(() => { + result.current.onTextChange(''); + }); + + await waitFor(() => { + expect(result.current.value).toBe(null); + expect(result.current.meta.error?.$error).toBeUndefined(); + }); + + await act(() => { + result.current.onTextChange(' '); + }); + + await waitFor(() => { + expect(result.current.value).toBe(null); + expect(result.current.text).toBe(' '); + expect(result.current.meta.error?.$error).toBeUndefined(); + }); + + await act(() => { + result.current.onTextChange('.'); + }); + + await waitFor(() => { + expect(result.current.value).toBe(null); + expect(result.current.meta.error?.$error).toBe(defaultDecimalFieldI18n.invalidInput); + }); + + await act(() => { + result.current.onTextChange('.0'); + }); + + await waitFor(() => { + expect(result.current.value).toBe(0); + expect(result.current.meta.error?.$error).toBeUndefined(); + }); + + await act(() => { + result.current.onTextChange('0.'); + }); + + await waitFor(() => { + expect(result.current.value).toBe(0); + expect(result.current.meta.error?.$error).toBeUndefined(); + }); + + await act(() => { + result.current.onTextChange('0.0'); + }); + + await waitFor(() => { + expect(result.current.value).toBe(0); + expect(result.current.meta.error?.$error).toBeUndefined(); + }); + }); + + it('Should set default error if field is required and empty', async () => { + const [{ result }] = renderUseDecimalField({ required: true }); + + act(() => { + result.current.control.setValue(null); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBe(defaultDecimalFieldI18n.required); + }); + }); + + it('Should set default error if field value is less than min', async () => { + const [{ result }] = renderUseDecimalField({ min: 0.5 }); + + act(() => { + result.current.control.setValue(0.25); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBe(defaultDecimalFieldI18n.minValue(0.5, 2)); + }); + + act(() => { + result.current.control.setValue(0.5); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBeUndefined(); + }); + }); + + it('Should set default error if field value is more than max', async () => { + const [{ result }] = renderUseDecimalField({ max: 0.5 }); + + act(() => { + result.current.control.setValue(0.75); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBe(defaultDecimalFieldI18n.maxValue(0.5, 2)); + }); + + act(() => { + result.current.control.setValue(0.5); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBeUndefined(); + }); + }); + + it('Should set custom conversion error correctly', async () => { + const [{ result }] = renderUseDecimalField({ + i18n: { + invalidInput: 'custom', + }, + }); + + await act(() => { + result.current.onTextChange('0a'); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBe('custom'); + }); + + await act(() => { + result.current.onTextChange('a0'); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBe('custom'); + }); + + await act(() => { + result.current.onTextChange('hello'); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBe('custom'); + }); + }); + + it('Should set custom error if field is required and empty', async () => { + const [{ result }] = renderUseDecimalField({ + required: true, + i18n: { + required: 'custom', + }, + }); + + act(() => { + result.current.control.setValue(null); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBe('custom'); + }); + }); + + it('Should set custom error if field value is less than min', async () => { + const [{ result }] = renderUseDecimalField({ + min: 0.5, + i18n: { + minValue: () => 'custom', + }, + }); + + act(() => { + result.current.control.setValue(0.25); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBe('custom'); + }); + }); + + it('Should set custom error if field value is more than max', async () => { + const [{ result }] = renderUseDecimalField({ + max: 0.5, + i18n: { + maxValue: () => 'custom', + }, + }); + + act(() => { + result.current.control.setValue(0.75); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBe('custom'); + }); + }); + + it('Should call custom format function', () => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const format = jest.fn((_value: number | null | undefined) => 'custom'); + const initialValue = 3.14; + const [{ result }] = renderUseDecimalField({ format, initialValue }); + + expect(result.current.text).toBe('custom'); + expect(format.mock.calls[0][0]).toBe(initialValue); + }); + + it('Should call custom parse function', async () => { + const parse = jest.fn(); + + const [{ result }] = renderUseDecimalField({ parse }); + + await act(() => { + result.current.onTextChange('0.0'); + }); + + await waitFor(() => { + expect(parse).toBeCalledWith('0.0'); + }); + }); +}); diff --git a/packages/core/tests/hooks/useField.test.tsx b/packages/core/tests/hooks/useField.test.tsx index 0890439f..f7b900ba 100644 --- a/packages/core/tests/hooks/useField.test.tsx +++ b/packages/core/tests/hooks/useField.test.tsx @@ -4,7 +4,7 @@ import { createPxth, Pxth } from 'pxth'; import { FieldContext, FormConfig, FormShared, ReactiveFormProvider, useField, useForm } from '../../src'; -const renderField = ( +const renderField = >( name: Pxth, config: FormConfig, ): RenderHookResult, undefined> => { diff --git a/packages/core/tests/hooks/useFieldError.test.tsx b/packages/core/tests/hooks/useFieldError.test.tsx index d35277ab..84f98725 100644 --- a/packages/core/tests/hooks/useFieldError.test.tsx +++ b/packages/core/tests/hooks/useFieldError.test.tsx @@ -4,7 +4,7 @@ import { createPxth, Pxth } from 'pxth'; import { FieldError, FormConfig, FormShared, ReactiveFormProvider, useFieldError, useForm } from '../../src'; -const renderFieldError = ( +const renderFieldError = >( name: Pxth, config: FormConfig, ): RenderHookResult<[FieldError | undefined, Dispatch>], undefined> => { diff --git a/packages/core/tests/hooks/useFieldTouched.test.tsx b/packages/core/tests/hooks/useFieldTouched.test.tsx index fc372ed3..9f9cc1f0 100644 --- a/packages/core/tests/hooks/useFieldTouched.test.tsx +++ b/packages/core/tests/hooks/useFieldTouched.test.tsx @@ -4,7 +4,7 @@ import { createPxth, Pxth } from 'pxth'; import { FieldTouched, FormConfig, FormShared, ReactiveFormProvider, useFieldTouched, useForm } from '../../src'; -const renderFieldTouched = ( +const renderFieldTouched = >( name: Pxth, config: FormConfig, ): RenderHookResult<[FieldTouched | undefined, Dispatch>], undefined> => { diff --git a/packages/core/tests/hooks/useFieldValidator.test.tsx b/packages/core/tests/hooks/useFieldValidator.test.tsx index 22587bf2..6e956d8a 100644 --- a/packages/core/tests/hooks/useFieldValidator.test.tsx +++ b/packages/core/tests/hooks/useFieldValidator.test.tsx @@ -12,7 +12,7 @@ import { useForm, } from '../../src'; -const renderUseFieldValidator = ( +const renderUseFieldValidator = >( config: UseFieldValidatorConfig, formConfig: FormConfig, ) => { diff --git a/packages/core/tests/hooks/useFieldValue.test.tsx b/packages/core/tests/hooks/useFieldValue.test.tsx index 548f678f..019c6001 100644 --- a/packages/core/tests/hooks/useFieldValue.test.tsx +++ b/packages/core/tests/hooks/useFieldValue.test.tsx @@ -4,7 +4,7 @@ import { createPxth, Pxth } from 'pxth'; import { FormConfig, FormShared, ReactiveFormProvider, useFieldValue, useForm } from '../../src'; -const renderFieldValue = ( +const renderFieldValue = >( name: Pxth, config: FormConfig, ): RenderHookResult<[V, Dispatch], undefined> => { diff --git a/packages/core/tests/hooks/useFieldValueArray.test.tsx b/packages/core/tests/hooks/useFieldValueArray.test.tsx index c3504b81..468c1020 100644 --- a/packages/core/tests/hooks/useFieldValueArray.test.tsx +++ b/packages/core/tests/hooks/useFieldValueArray.test.tsx @@ -11,7 +11,7 @@ import { useForm, } from '../../src'; -const renderFieldValueArray = ( +const renderFieldValueArray = , T extends Record>( paths: FieldValueArrayConfig, config: FormConfig, ): [RenderHookResult, FormShared] => { diff --git a/packages/core/tests/hooks/useFormControl.proxy.test.tsx b/packages/core/tests/hooks/useFormControl.proxy.test.tsx index 303362ad..0d4297c2 100644 --- a/packages/core/tests/hooks/useFormControl.proxy.test.tsx +++ b/packages/core/tests/hooks/useFormControl.proxy.test.tsx @@ -3,10 +3,16 @@ import { act, renderHook, RenderHookResult } from '@testing-library/react'; import { createPxth } from 'pxth'; import { MappingProxy, StockProxy } from 'stocked'; -import { FormProxyProvider, FormShared, ReactiveFormProvider, useForm, useFormContext } from '../../src'; -import { FormControlConfig } from '../../src/hooks/useFormControl'; - -const renderFormContextWithProxy = ( +import { + FormControlConfig, + FormProxyProvider, + FormShared, + ReactiveFormProvider, + useForm, + useFormContext, +} from '../../src'; + +const renderFormContextWithProxy = >( config: FormControlConfig, proxy: StockProxy, ): [RenderHookResult, undefined>, FormShared] => { diff --git a/packages/core/tests/hooks/useIntegerField.test.tsx b/packages/core/tests/hooks/useIntegerField.test.tsx new file mode 100644 index 00000000..2ec7e758 --- /dev/null +++ b/packages/core/tests/hooks/useIntegerField.test.tsx @@ -0,0 +1,251 @@ +import React from 'react'; +import { act, renderHook, waitFor } from '@testing-library/react'; + +import { + defaultIntegerFieldI18n, + IntegerFieldConfig, + IntegerFieldI18n, + IntegerFieldI18nContextProvider, + ReactiveFormProvider, + useForm, + useIntegerField, +} from '../../src'; + +type Config = Omit & { + initialValue?: number | null; + i18n?: Partial; +}; + +const renderUseIntegerField = (config: Config = {}) => { + const { initialValue = 0, i18n, ...initialProps } = config; + + const formBag = renderHook(() => + useForm({ + initialValues: { + test: initialValue, + }, + }), + ); + + const result = renderHook( + (props: Omit) => + useIntegerField({ + name: formBag.result.current.paths.test, + ...props, + }), + { + wrapper: ({ children }) => ( + + {children} + + ), + initialProps, + }, + ); + + return [result, formBag] as const; +}; + +describe('Integer field', () => { + it('Should format initial value correctly', () => { + const [{ result }] = renderUseIntegerField(); + + expect(result.current.text).toBe('0'); + expect(result.current.value).toBe(0); + }); + + it('Should set default error in case of conversion error and clear it afterwards', async () => { + const [{ result }] = renderUseIntegerField(); + + act(() => { + result.current.onTextChange('0a'); + }); + + await waitFor(() => { + expect(result.current.value).toBe(0); + expect(result.current.meta.error?.$error).toBe(defaultIntegerFieldI18n.invalidInput); + }); + + act(() => { + result.current.onTextChange('a0'); + }); + + await waitFor(() => { + expect(result.current.value).toBe(0); + expect(result.current.meta.error?.$error).toBe(defaultIntegerFieldI18n.invalidInput); + }); + + act(() => { + result.current.onTextChange('1'); + }); + + await waitFor(() => { + expect(result.current.value).toBe(1); + expect(result.current.meta.error?.$error).toBeUndefined(); + }); + }); + + it('Should set default error if field is required and empty', async () => { + const [{ result }] = renderUseIntegerField({ required: true }); + + act(() => { + result.current.control.setValue(null); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBe(defaultIntegerFieldI18n.required); + }); + }); + + it('Should set default error if field value is less than min', async () => { + const [{ result }] = renderUseIntegerField({ min: 0 }); + + act(() => { + result.current.control.setValue(-1); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBe(defaultIntegerFieldI18n.minValue(0)); + }); + + act(() => { + result.current.control.setValue(0); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBeUndefined(); + }); + }); + + it('Should set default error if field value is more than max', async () => { + const [{ result }] = renderUseIntegerField({ max: 0 }); + + act(() => { + result.current.control.setValue(1); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBe(defaultIntegerFieldI18n.maxValue(0)); + }); + + act(() => { + result.current.control.setValue(0); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBeUndefined(); + }); + }); + + it('Should set custom error in case of conversion error and clear it afterwards', async () => { + const [{ result }] = renderUseIntegerField({ + i18n: { + invalidInput: 'custom', + }, + }); + + act(() => { + result.current.onTextChange('0a'); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBe('custom'); + }); + + act(() => { + result.current.onTextChange('a0'); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBe('custom'); + }); + + act(() => { + result.current.onTextChange('0'); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBeUndefined(); + }); + }); + + it('Should set custom error if field is required and empty', async () => { + const [{ result }] = renderUseIntegerField({ + required: true, + i18n: { + required: 'custom', + }, + }); + + act(() => { + result.current.control.setValue(null); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBe('custom'); + }); + }); + + it('Should set custom error if field value is less than min', async () => { + const minValue = jest.fn(() => 'custom'); + + const [{ result }] = renderUseIntegerField({ + min: 0, + i18n: { + minValue, + }, + }); + + act(() => { + result.current.control.setValue(-1); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBe('custom'); + expect(minValue).toBeCalledWith(0); + }); + }); + + it('Should set custom error if field value is more than max', async () => { + const maxValue = jest.fn(() => 'custom'); + + const [{ result }] = renderUseIntegerField({ + max: 0, + i18n: { + maxValue, + }, + }); + + act(() => { + result.current.control.setValue(1); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBe('custom'); + expect(maxValue).toBeCalledWith(0); + }); + }); + + it('Should be able to format integer differently', () => { + const format = jest.fn(() => 'custom'); + const initialValue = 42; + const [{ result }] = renderUseIntegerField({ format, initialValue }); + + expect(result.current.text).toBe('custom'); + expect(format).toBeCalledWith(initialValue); + }); + + it('Should call custom parse function', async () => { + const parse = jest.fn(); + + const [{ result }] = renderUseIntegerField({ parse }); + + await act(() => { + result.current.onTextChange('0'); + }); + + await waitFor(() => { + expect(parse).toBeCalledWith('0'); + }); + }); +}); diff --git a/packages/core/tests/hooks/useProxyInterception.test.tsx b/packages/core/tests/hooks/useProxyInterception.test.tsx index daa021a8..62521282 100644 --- a/packages/core/tests/hooks/useProxyInterception.test.tsx +++ b/packages/core/tests/hooks/useProxyInterception.test.tsx @@ -3,8 +3,7 @@ import { renderHook } from '@testing-library/react'; import { createPxth, getPxthSegments, Pxth } from 'pxth'; import { MappingProxy } from 'stocked'; -import { FieldError, FieldValidator, ReactiveFormProvider, useForm } from '../../src'; -import { useProxyInterception } from '../../src/hooks/useProxyInterception'; +import { FieldError, FieldValidator, ReactiveFormProvider, useForm, useProxyInterception } from '../../src'; type ProxyValue = { id: number; diff --git a/packages/core/tests/hooks/useStringField.test.tsx b/packages/core/tests/hooks/useStringField.test.tsx new file mode 100644 index 00000000..01523f34 --- /dev/null +++ b/packages/core/tests/hooks/useStringField.test.tsx @@ -0,0 +1,298 @@ +import React from 'react'; +import { act, renderHook, waitFor } from '@testing-library/react'; + +import { + defaultStringFieldI18n, + ReactiveFormProvider, + StringFieldConfig, + StringFieldI18n, + StringFieldI18nContextProvider, + useForm, + useStringField, +} from '../../src'; + +type Config = Omit & { + initialValue?: string; + i18n?: Partial; +}; + +const renderUseStringField = (config: Config = {}) => { + const { initialValue = '', i18n, ...initialProps } = config; + + const formBag = renderHook(() => + useForm({ + initialValues: { + test: initialValue, + }, + }), + ); + + const stringFieldBag = renderHook( + (props: Omit) => + useStringField({ + name: formBag.result.current.paths.test, + ...props, + }), + { + wrapper: ({ children }) => ( + + {children} + + ), + initialProps, + }, + ); + + return [stringFieldBag, formBag] as const; +}; + +describe('String field', () => { + it('Should set touched=true on blur', async () => { + const [{ result }] = renderUseStringField(); + + await act(() => { + result.current.onBlur(); + }); + + await waitFor(() => { + expect(result.current.meta.touched?.$touched).toBeTruthy(); + }); + }); + + it('Should set default error if field is required and empty', async () => { + const [{ result }] = renderUseStringField({ required: true }); + + act(() => { + result.current.control.setValue(null); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBe(defaultStringFieldI18n.required); + }); + + act(() => { + result.current.control.setValue(undefined); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBe(defaultStringFieldI18n.required); + }); + + act(() => { + result.current.control.setValue(''); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBe(defaultStringFieldI18n.required); + }); + + act(() => { + result.current.control.setValue(' '); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBe(defaultStringFieldI18n.required); + }); + + act(() => { + result.current.control.setValue('a'); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBeUndefined(); + }); + }); + + it('Should set default error if value is longer than maxLength', async () => { + const [{ result }] = renderUseStringField({ maxLength: 3 }); + + act(() => { + result.current.control.setValue('aaaa'); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBe(defaultStringFieldI18n.maxLength(3)); + }); + + act(() => { + result.current.control.setValue('aaa'); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBeUndefined(); + }); + }); + + it('Should set default error if value is shorter than minLength', async () => { + const [{ result }] = renderUseStringField({ minLength: 3 }); + + act(() => { + result.current.control.setValue('aa'); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBe(defaultStringFieldI18n.minLength(3)); + }); + + act(() => { + result.current.control.setValue('aaa'); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBeUndefined(); + }); + }); + + it('Should set custom error if field is required and empty', async () => { + const [{ result }] = renderUseStringField({ + required: true, + i18n: { + required: 'custom', + }, + }); + + act(() => { + result.current.control.setValue(null); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBe('custom'); + }); + + act(() => { + result.current.control.setValue(undefined); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBe('custom'); + }); + + act(() => { + result.current.control.setValue(''); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBe('custom'); + }); + + act(() => { + result.current.control.setValue(' '); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBe('custom'); + }); + + act(() => { + result.current.control.setValue('a'); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBeUndefined(); + }); + }); + + it('Should set custom error if value is longer than maxLength', async () => { + const [{ result }] = renderUseStringField({ + maxLength: 3, + i18n: { + maxLength: () => 'custom', + }, + }); + + act(() => { + result.current.control.setValue('aaa'); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBeUndefined(); + }); + + act(() => { + result.current.control.setValue('aaaa'); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBe('custom'); + }); + }); + + it('Should set custom error if value is longer than maxLength (with callback)', async () => { + const callback = jest.fn(() => 'custom'); + const [{ result }] = renderUseStringField({ + maxLength: 3, + i18n: { + maxLength: callback, + }, + }); + + act(() => { + result.current.control.setValue('aaa'); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBeUndefined(); + }); + + act(() => { + result.current.control.setValue('aaaa'); + }); + + await waitFor(() => { + expect(callback).toBeCalledWith(3); + expect(result.current.meta.error?.$error).toBe('custom'); + }); + }); + + it('Should set custom error if value is shorter than minLength', async () => { + const [{ result }] = renderUseStringField({ + minLength: 3, + i18n: { + minLength: () => 'custom', + }, + }); + + act(() => { + result.current.control.setValue('aaa'); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBeUndefined(); + }); + + act(() => { + result.current.control.setValue('aa'); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBe('custom'); + }); + }); + + it('Should set custom error if value is shorter than minLength', async () => { + const callback = jest.fn(() => 'custom'); + const [{ result }] = renderUseStringField({ + minLength: 3, + i18n: { + minLength: callback, + }, + }); + + act(() => { + result.current.control.setValue('aaa'); + }); + + await waitFor(() => { + expect(result.current.meta.error?.$error).toBeUndefined(); + }); + + act(() => { + result.current.control.setValue('aa'); + }); + + await waitFor(() => { + expect(callback).toBeCalledWith(3); + expect(result.current.meta.error?.$error).toBe('custom'); + }); + }); +}); diff --git a/packages/core/tests/hooks/useSubmitAction.test.tsx b/packages/core/tests/hooks/useSubmitAction.test.tsx index 46b15278..8743eef0 100644 --- a/packages/core/tests/hooks/useSubmitAction.test.tsx +++ b/packages/core/tests/hooks/useSubmitAction.test.tsx @@ -1,8 +1,7 @@ import React, { PropsWithChildren } from 'react'; import { act, renderHook } from '@testing-library/react'; -import ReactiveForm from '../../src'; -import { useSubmitAction } from '../../src/hooks/useSubmitAction'; +import ReactiveForm, { useSubmitAction } from '../../src'; describe('useSubmitAction', () => { it('should return default submit', async () => { diff --git a/packages/core/tests/hooks/useValidationRegistry.test.tsx b/packages/core/tests/hooks/useValidationRegistry.test.tsx index f0e4374f..da57d565 100644 --- a/packages/core/tests/hooks/useValidationRegistry.test.tsx +++ b/packages/core/tests/hooks/useValidationRegistry.test.tsx @@ -1,8 +1,7 @@ import { renderHook } from '@testing-library/react'; import { createPxth, getPxthSegments } from 'pxth'; -import { FieldError, FieldInnerError } from '../../src'; -import { useValidationRegistry } from '../../src/hooks/useValidationRegistry'; +import { FieldError, FieldInnerError, useValidationRegistry } from '../../src'; const renderUseValidationRegistry = () => { return renderHook(() => useValidationRegistry()); diff --git a/packages/core/tests/utils/deepRemoveEmpty.test.ts b/packages/core/tests/utils/deepRemoveEmpty.test.ts index 5bd8e825..00395b20 100644 --- a/packages/core/tests/utils/deepRemoveEmpty.test.ts +++ b/packages/core/tests/utils/deepRemoveEmpty.test.ts @@ -17,7 +17,7 @@ describe('deepRemoveEmpty', () => { }); it('should handle nulls', () => { - expect(deepRemoveEmpty(null)).toBe(undefined); + expect(deepRemoveEmpty(null as unknown as object)).toBe(undefined); }); it('should shake objects', () => { diff --git a/packages/core/tests/utils/formatDate.test.ts b/packages/core/tests/utils/formatDate.test.ts new file mode 100644 index 00000000..0b311013 --- /dev/null +++ b/packages/core/tests/utils/formatDate.test.ts @@ -0,0 +1,11 @@ +import { formatDate } from '../../src/utils/formatDate'; + +describe('format date', () => { + it('should format correctly without pickTime option', () => { + expect(formatDate(new Date(2023, 8, 12), false)).toBe('2023.09.12'); + }); + + it('should format correctly with pickTime option', () => { + expect(formatDate(new Date(2023, 8, 12, 17, 27, 42, 42), true)).toBe('2023.09.12 17:27'); + }); +}); diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json index 298bf57c..36fe341d 100644 --- a/packages/core/tsconfig.json +++ b/packages/core/tsconfig.json @@ -1,24 +1,23 @@ { "compilerOptions": { - "outDir": "dist", - "module": "esnext", - "lib": ["dom", "esnext"], - "moduleResolution": "node", - "jsx": "react", - "sourceMap": true, + "target": "es6", + "jsx": "react-jsx", + "module": "CommonJS", "declaration": true, + "strict": true, "esModuleInterop": true, - "noImplicitReturns": false, - "noImplicitThis": true, - "noImplicitAny": true, - "strictNullChecks": true, - "suppressImplicitAnyIndexErrors": true, - "noUnusedLocals": true, - "noUnusedParameters": true, + "lib": ["es2022", "DOM"], + "noLib": false, + "skipLibCheck": true, "allowSyntheticDefaultImports": true, + "moduleResolution": "node", + "allowJs": true, + "forceConsistentCasingInFileNames": true, "noFallthroughCasesInSwitch": true, - "strict": true, - "importHelpers": true + "resolveJsonModule": true, + "isolatedModules": true, + "outDir": "dist", + "rootDir": "src" }, - "include": ["src", "tests"] + "include": ["src"] } diff --git a/packages/core/tsconfig.test.json b/packages/core/tsconfig.test.json index effc4db5..9b92b823 100644 --- a/packages/core/tsconfig.test.json +++ b/packages/core/tsconfig.test.json @@ -1,4 +1,5 @@ { + "extends": "./tsconfig.json", "compilerOptions": { "module": "commonjs", "noImplicitReturns": false, diff --git a/packages/x/.eslintignore b/packages/x/.eslintignore deleted file mode 100644 index b9404616..00000000 --- a/packages/x/.eslintignore +++ /dev/null @@ -1,3 +0,0 @@ -dist -node_modules -*.config.js \ No newline at end of file diff --git a/packages/x/.eslintrc.json b/packages/x/.eslintrc.json deleted file mode 100644 index fcd7da38..00000000 --- a/packages/x/.eslintrc.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "@reactive-tools/eslint-config" -} diff --git a/packages/x/CHANGELOG.md b/packages/x/CHANGELOG.md deleted file mode 100644 index 23bdda3a..00000000 --- a/packages/x/CHANGELOG.md +++ /dev/null @@ -1,56 +0,0 @@ -# @reactive-forms/x - -## 0.10.4 - -### Patch Changes - -- 524f4d2: Created useConverterField hook in @reactive-forms/x package - -## 0.10.3 - -## 0.10.2 - -### Patch Changes - -- de9d96e: Configured provenance - -## 0.10.1 - -## 0.10.0 - -## 0.9.1 - -## 0.9.0 - -## 0.8.16 - -### Patch Changes - -- @reactive-forms/core@0.8.16 - -## 0.8.15 - -### Patch Changes - -- @reactive-forms/core@0.8.15 - -## 0.8.14 - -### Patch Changes - -- 708a17b: Test release script -- Updated dependencies [708a17b] - - @reactive-forms/core@0.8.14 - -## 0.8.13 - -### Patch Changes - -- @reactive-forms/core@0.8.13 - -## 0.8.12 - -### Patch Changes - -- Updated dependencies [b4ded44] - - @reactive-forms/core@0.8.12 diff --git a/packages/x/LICENSE b/packages/x/LICENSE deleted file mode 100644 index b5ac9ad2..00000000 --- a/packages/x/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2021 Artiom Tretjakovas - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file diff --git a/packages/x/README.md b/packages/x/README.md deleted file mode 100644 index f0737d92..00000000 --- a/packages/x/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# `@reactive-forms/x` - -> TODO: description - -## Usage - -``` -const x = require('@reactive-forms/x'); - -// TODO: DEMONSTRATE API -``` diff --git a/packages/x/aqu.config.json b/packages/x/aqu.config.json deleted file mode 100644 index 79ee5a56..00000000 --- a/packages/x/aqu.config.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "buildOptions": { - "target": ["es2019", "chrome58", "firefox57", "safari11", "edge18", "node12"], - "preserveSymlinks": false - }, - "dtsBundleGeneratorOptions": { - "libraries": { - "importedLibraries": ["stocked", "react", "@reactive-forms/core", "pxth"], - "allowedTypesLibraries": [] - } - } -} diff --git a/packages/x/jest.config.js b/packages/x/jest.config.js deleted file mode 100644 index 0bbb8647..00000000 --- a/packages/x/jest.config.js +++ /dev/null @@ -1,24 +0,0 @@ -/** - * @type {import('ts-jest').JestConfigWithTsJest} - */ -const config = { - transformIgnorePatterns: ["[/\\\\]node_modules[/\\\\].+\\.(js|cjs|jsx)$'"], - preset: 'ts-jest', - moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'cjs', 'mjs', 'json', 'node'], - collectCoverageFrom: ['src/**/*.{ts,tsx,js,jsx,cjs,mjs}'], - testMatch: ['/**/*.(spec|test).{ts,tsx,js,jsx,cjs,mjs}'], - testEnvironmentOptions: { - url: 'http://localhost', - }, - testEnvironment: 'jsdom', - transform: { - '^.+\\.tsx?$': [ - 'ts-jest', - { - tsconfig: './tsconfig.test.json', - }, - ], - }, -}; - -module.exports = config; diff --git a/packages/x/package.json b/packages/x/package.json deleted file mode 100644 index bd351ab8..00000000 --- a/packages/x/package.json +++ /dev/null @@ -1,53 +0,0 @@ -{ - "name": "@reactive-forms/x", - "description": "Advanced Reactive Forms components for rich eXperience", - "version": "0.10.6", - "main": "dist/index.js", - "module": "dist/x.esm.js", - "types": "dist/x.d.ts", - "bugs": "https://github.com/fracht/reactive-forms/issues", - "homepage": "https://github.com/fracht/reactive-forms#readme", - "repository": "fracht/reactive-forms.git", - "author": "ReactiveForms team", - "license": "MIT", - "publishConfig": { - "access": "public", - "directory": "prepared-package" - }, - "scripts": { - "build": "aqu build && rimraf ./prepared-package && clean-publish", - "lint": "eslint .", - "lint:fix": "npm run lint --fix", - "start": "aqu watch", - "test": "jest --passWithNoTests", - "test:log-coverage": "jest --passWithNoTests --coverage --silent --ci --coverageReporters=text", - "test:watch": "jest --passWithNoTests --watch" - }, - "devDependencies": { - "@babel/core": "7.19.6", - "@reactive-forms/core": "workspace:*", - "@reactive-tools/eslint-config": "workspace:*", - "@testing-library/react": "13.4.0", - "@types/jest": "26.0.24", - "@types/lodash": "4.14.161", - "@types/react": "18.0.23", - "aqu": "0.4.3", - "jest": "29.2.2", - "react": "18.2.0", - "rimraf": "3.0.2", - "ts-jest": "29.0.3", - "tslib": "2.3.1", - "typescript": "4.8.4" - }, - "peerDependencies": { - "@reactive-forms/core": "< 1.0.0", - "react": ">=16" - }, - "files": [ - "dist" - ], - "source": "src/index.ts", - "dependencies": { - "lodash": "4.17.21" - } -} diff --git a/packages/x/src/index.ts b/packages/x/src/index.ts deleted file mode 100644 index 8eb012ea..00000000 --- a/packages/x/src/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './plugin'; -export * from './useConverterField'; diff --git a/packages/x/src/plugin.ts b/packages/x/src/plugin.ts deleted file mode 100644 index 5bb1f848..00000000 --- a/packages/x/src/plugin.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { FormConfig, FormShared, Plugin } from '@reactive-forms/core'; - -export const xPlugin: Plugin = { - token: Symbol.for('x'), - useConfigDecorator: (config: FormConfig) => config, - useBagDecorator: (form: FormShared) => form, -}; diff --git a/packages/x/tsconfig.json b/packages/x/tsconfig.json deleted file mode 100644 index 97f79bdb..00000000 --- a/packages/x/tsconfig.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "compilerOptions": { - "outDir": "dist", - "module": "esnext", - "lib": ["dom", "esnext"], - "moduleResolution": "node", - "jsx": "react", - "sourceMap": true, - "declaration": true, - "esModuleInterop": true, - "noImplicitReturns": true, - "noImplicitThis": true, - "noImplicitAny": true, - "strictNullChecks": true, - "suppressImplicitAnyIndexErrors": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "allowSyntheticDefaultImports": true, - "noFallthroughCasesInSwitch": true, - "rootDir": "./src", - "strict": true, - "importHelpers": true - }, - "include": ["src", "tests"] -} diff --git a/packages/x/tsconfig.test.json b/packages/x/tsconfig.test.json deleted file mode 100644 index 0c2dfa50..00000000 --- a/packages/x/tsconfig.test.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "module": "commonjs", - "noImplicitReturns": false, - "noImplicitThis": false, - "noImplicitAny": false, - "strictNullChecks": false, - "suppressImplicitAnyIndexErrors": false, - "noUnusedLocals": false, - "noUnusedParameters": false, - "strict": false, - "alwaysStrict": false - } -} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 60d132fe..bd06dddd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -22,7 +22,7 @@ importers: version: 8.1.0(size-limit@8.1.0) aqu: specifier: 0.4.3 - version: 0.4.3(@babel/core@7.19.6)(@types/node@18.11.18) + version: 0.4.3(@babel/core@7.19.6) clean-publish: specifier: 4.2.0 version: 4.2.0 @@ -68,10 +68,10 @@ importers: version: 18.0.23 aqu: specifier: 0.4.3 - version: 0.4.3(@babel/core@7.19.6)(@types/node@18.11.18) + version: 0.4.3(@babel/core@7.19.6) jest: specifier: 29.2.2 - version: 29.2.2(@types/node@18.11.18) + version: 29.2.2 react: specifier: 18.2.0 version: 18.2.0 @@ -88,6 +88,9 @@ importers: packages/core: dependencies: + dayjs: + specifier: ^1.11.9 + version: 1.11.9 lodash: specifier: 4.17.21 version: 4.17.21 @@ -111,11 +114,11 @@ importers: specifier: workspace:* version: link:../../tools/eslint-config '@testing-library/react': - specifier: 13.4.0 - version: 13.4.0(react-dom@18.2.0)(react@18.2.0) + specifier: 14.0.0 + version: 14.0.0(react-dom@18.2.0)(react@18.2.0) '@types/jest': - specifier: 26.0.24 - version: 26.0.24 + specifier: 29.5.4 + version: 29.5.4 '@types/lodash': specifier: 4.14.161 version: 4.14.161 @@ -125,12 +128,9 @@ importers: '@types/react': specifier: 18.0.23 version: 18.0.23 - aqu: - specifier: 0.4.3 - version: 0.4.3(@babel/core@7.19.6)(@types/node@18.11.18) jest: - specifier: 29.2.2 - version: 29.2.2(@types/node@18.11.18) + specifier: 29.7.0 + version: 29.7.0(@types/node@18.11.18) react: specifier: 18.2.0 version: 18.2.0 @@ -141,8 +141,8 @@ importers: specifier: 3.0.2 version: 3.0.2 ts-jest: - specifier: 29.0.3 - version: 29.0.3(@babel/core@7.19.6)(esbuild@0.15.12)(jest@29.2.2)(typescript@4.8.4) + specifier: 29.1.1 + version: 29.1.1(@babel/core@7.19.6)(esbuild@0.15.12)(jest@29.7.0)(typescript@4.8.4) tslib: specifier: 2.3.1 version: 2.3.1 @@ -189,13 +189,13 @@ importers: version: 18.0.6 aqu: specifier: 0.4.3 - version: 0.4.3(@babel/core@7.19.6)(@types/node@18.11.18) + version: 0.4.3(@babel/core@7.19.6) cpy-cli: specifier: ^5.0.0 version: 5.0.0 jest: specifier: 29.2.2 - version: 29.2.2(@types/node@18.11.18) + version: 29.2.2 jest-environment-jsdom: specifier: 29.2.1 version: 29.2.1 @@ -219,56 +219,6 @@ importers: version: 4.8.4 publishDirectory: prepared-package - packages/x: - dependencies: - lodash: - specifier: 4.17.21 - version: 4.17.21 - devDependencies: - '@babel/core': - specifier: 7.19.6 - version: 7.19.6 - '@reactive-forms/core': - specifier: workspace:* - version: link:../core/prepared-package - '@reactive-tools/eslint-config': - specifier: workspace:* - version: link:../../tools/eslint-config - '@testing-library/react': - specifier: 13.4.0 - version: 13.4.0(react-dom@18.2.0)(react@18.2.0) - '@types/jest': - specifier: 26.0.24 - version: 26.0.24 - '@types/lodash': - specifier: 4.14.161 - version: 4.14.161 - '@types/react': - specifier: 18.0.23 - version: 18.0.23 - aqu: - specifier: 0.4.3 - version: 0.4.3(@babel/core@7.19.6)(@types/node@18.11.18) - jest: - specifier: 29.2.2 - version: 29.2.2(@types/node@18.11.18) - react: - specifier: 18.2.0 - version: 18.2.0 - rimraf: - specifier: 3.0.2 - version: 3.0.2 - ts-jest: - specifier: 29.0.3 - version: 29.0.3(@babel/core@7.19.6)(esbuild@0.15.12)(jest@29.2.2)(typescript@4.8.4) - tslib: - specifier: 2.3.1 - version: 2.3.1 - typescript: - specifier: 4.8.4 - version: 4.8.4 - publishDirectory: prepared-package - tools/eslint-config: dependencies: '@typescript-eslint/eslint-plugin': @@ -1878,6 +1828,18 @@ packages: slash: 3.0.0 dev: true + /@jest/console@29.7.0: + resolution: {integrity: sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + '@types/node': 18.11.18 + chalk: 4.1.2 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + slash: 3.0.0 + dev: true + /@jest/core@29.2.2: resolution: {integrity: sha512-susVl8o2KYLcZhhkvSB+b7xX575CX3TmSvxfeDjpRko7KmT89rHkXj6XkDkNpSeFMBzIENw5qIchO9HC9Sem+A==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -1920,6 +1882,49 @@ packages: - ts-node dev: true + /@jest/core@29.7.0: + resolution: {integrity: sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@jest/console': 29.7.0 + '@jest/reporters': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 18.11.18 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + ci-info: 3.5.0 + exit: 0.1.2 + graceful-fs: 4.2.10 + jest-changed-files: 29.7.0 + jest-config: 29.7.0(@types/node@18.11.18) + jest-haste-map: 29.7.0 + jest-message-util: 29.7.0 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-resolve-dependencies: 29.7.0 + jest-runner: 29.7.0 + jest-runtime: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + jest-watcher: 29.7.0 + micromatch: 4.0.5 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-ansi: 6.0.1 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + - ts-node + dev: true + /@jest/environment@29.2.2: resolution: {integrity: sha512-OWn+Vhu0I1yxuGBJEFFekMYc8aGBGrY4rt47SOh/IFaI+D7ZHCk7pKRiSoZ2/Ml7b0Ony3ydmEHRx/tEOC7H1A==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -1930,6 +1935,16 @@ packages: jest-mock: 29.2.2 dev: true + /@jest/environment@29.7.0: + resolution: {integrity: sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/fake-timers': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 18.11.18 + jest-mock: 29.7.0 + dev: true + /@jest/expect-utils@29.2.2: resolution: {integrity: sha512-vwnVmrVhTmGgQzyvcpze08br91OL61t9O0lJMDyb6Y/D8EKQ9V7rGUb/p7PDt0GPzK0zFYqXWFo4EO2legXmkg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -1937,6 +1952,13 @@ packages: jest-get-type: 29.2.0 dev: true + /@jest/expect-utils@29.7.0: + resolution: {integrity: sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + jest-get-type: 29.6.3 + dev: true + /@jest/expect@29.2.2: resolution: {integrity: sha512-zwblIZnrIVt8z/SiEeJ7Q9wKKuB+/GS4yZe9zw7gMqfGf4C5hBLGrVyxu1SzDbVSqyMSlprKl3WL1r80cBNkgg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -1947,6 +1969,16 @@ packages: - supports-color dev: true + /@jest/expect@29.7.0: + resolution: {integrity: sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + expect: 29.7.0 + jest-snapshot: 29.7.0 + transitivePeerDependencies: + - supports-color + dev: true + /@jest/fake-timers@29.2.2: resolution: {integrity: sha512-nqaW3y2aSyZDl7zQ7t1XogsxeavNpH6kkdq+EpXncIDvAkjvFD7hmhcIs1nWloengEWUoWqkqSA6MSbf9w6DgA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -1959,6 +1991,18 @@ packages: jest-util: 29.2.1 dev: true + /@jest/fake-timers@29.7.0: + resolution: {integrity: sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + '@sinonjs/fake-timers': 10.3.0 + '@types/node': 18.11.18 + jest-message-util: 29.7.0 + jest-mock: 29.7.0 + jest-util: 29.7.0 + dev: true + /@jest/globals@29.2.2: resolution: {integrity: sha512-/nt+5YMh65kYcfBhj38B3Hm0Trk4IsuMXNDGKE/swp36yydBWfz3OXkLqkSvoAtPW8IJMSJDFCbTM2oj5SNprw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -1971,6 +2015,18 @@ packages: - supports-color dev: true + /@jest/globals@29.7.0: + resolution: {integrity: sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/environment': 29.7.0 + '@jest/expect': 29.7.0 + '@jest/types': 29.6.3 + jest-mock: 29.7.0 + transitivePeerDependencies: + - supports-color + dev: true + /@jest/reporters@29.2.2: resolution: {integrity: sha512-AzjL2rl2zJC0njIzcooBvjA4sJjvdoq98sDuuNs4aNugtLPSQ+91nysGKRF0uY1to5k0MdGMdOBggUsPqvBcpA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -2008,6 +2064,43 @@ packages: - supports-color dev: true + /@jest/reporters@29.7.0: + resolution: {integrity: sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@bcoe/v8-coverage': 0.2.3 + '@jest/console': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@jridgewell/trace-mapping': 0.3.19 + '@types/node': 18.11.18 + chalk: 4.1.2 + collect-v8-coverage: 1.0.1 + exit: 0.1.2 + glob: 7.2.3 + graceful-fs: 4.2.10 + istanbul-lib-coverage: 3.2.0 + istanbul-lib-instrument: 6.0.0 + istanbul-lib-report: 3.0.0 + istanbul-lib-source-maps: 4.0.1 + istanbul-reports: 3.1.5 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + jest-worker: 29.7.0 + slash: 3.0.0 + string-length: 4.0.2 + strip-ansi: 6.0.1 + v8-to-istanbul: 9.0.1 + transitivePeerDependencies: + - supports-color + dev: true + /@jest/schemas@29.0.0: resolution: {integrity: sha512-3Ab5HgYIIAnS0HjqJHQYZS+zXc4tUmTmBH3z83ajI6afXp8X3ZtdLX+nXx+I7LNkJD7uN9LAVhgnjDgZa2z0kA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -2015,6 +2108,13 @@ packages: '@sinclair/typebox': 0.24.51 dev: true + /@jest/schemas@29.6.3: + resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@sinclair/typebox': 0.27.8 + dev: true + /@jest/source-map@29.2.0: resolution: {integrity: sha512-1NX9/7zzI0nqa6+kgpSdKPK+WU1p+SJk3TloWZf5MzPbxri9UEeXX5bWZAPCzbQcyuAzubcdUHA7hcNznmRqWQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -2024,6 +2124,15 @@ packages: graceful-fs: 4.2.10 dev: true + /@jest/source-map@29.6.3: + resolution: {integrity: sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jridgewell/trace-mapping': 0.3.19 + callsites: 3.1.0 + graceful-fs: 4.2.10 + dev: true + /@jest/test-result@29.2.1: resolution: {integrity: sha512-lS4+H+VkhbX6z64tZP7PAUwPqhwj3kbuEHcaLuaBuB+riyaX7oa1txe0tXgrFj5hRWvZKvqO7LZDlNWeJ7VTPA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -2034,6 +2143,16 @@ packages: collect-v8-coverage: 1.0.1 dev: true + /@jest/test-result@29.7.0: + resolution: {integrity: sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/console': 29.7.0 + '@jest/types': 29.6.3 + '@types/istanbul-lib-coverage': 2.0.4 + collect-v8-coverage: 1.0.1 + dev: true + /@jest/test-sequencer@29.2.2: resolution: {integrity: sha512-Cuc1znc1pl4v9REgmmLf0jBd3Y65UXJpioGYtMr/JNpQEIGEzkmHhy6W6DLbSsXeUA13TDzymPv0ZGZ9jH3eIw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -2044,6 +2163,16 @@ packages: slash: 3.0.0 dev: true + /@jest/test-sequencer@29.7.0: + resolution: {integrity: sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/test-result': 29.7.0 + graceful-fs: 4.2.10 + jest-haste-map: 29.7.0 + slash: 3.0.0 + dev: true + /@jest/transform@29.2.2: resolution: {integrity: sha512-aPe6rrletyuEIt2axxgdtxljmzH8O/nrov4byy6pDw9S8inIrTV+2PnjyP/oFHMSynzGxJ2s6OHowBNMXp/Jzg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -2067,6 +2196,29 @@ packages: - supports-color dev: true + /@jest/transform@29.7.0: + resolution: {integrity: sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@babel/core': 7.19.6 + '@jest/types': 29.6.3 + '@jridgewell/trace-mapping': 0.3.19 + babel-plugin-istanbul: 6.1.1 + chalk: 4.1.2 + convert-source-map: 2.0.0 + fast-json-stable-stringify: 2.1.0 + graceful-fs: 4.2.10 + jest-haste-map: 29.7.0 + jest-regex-util: 29.6.3 + jest-util: 29.7.0 + micromatch: 4.0.5 + pirates: 4.0.5 + slash: 3.0.0 + write-file-atomic: 4.0.2 + transitivePeerDependencies: + - supports-color + dev: true + /@jest/types@26.6.2: resolution: {integrity: sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==} engines: {node: '>= 10.14.2'} @@ -2090,6 +2242,18 @@ packages: chalk: 4.1.2 dev: true + /@jest/types@29.6.3: + resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/schemas': 29.6.3 + '@types/istanbul-lib-coverage': 2.0.4 + '@types/istanbul-reports': 3.0.1 + '@types/node': 18.11.18 + '@types/yargs': 17.0.13 + chalk: 4.1.2 + dev: true + /@jridgewell/gen-mapping@0.1.1: resolution: {integrity: sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==} engines: {node: '>=6.0.0'} @@ -2128,6 +2292,13 @@ packages: '@jridgewell/sourcemap-codec': 1.4.14 dev: true + /@jridgewell/trace-mapping@0.3.19: + resolution: {integrity: sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==} + dependencies: + '@jridgewell/resolve-uri': 3.1.0 + '@jridgewell/sourcemap-codec': 1.4.14 + dev: true + /@manypkg/find-root@1.1.0: resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==} dependencies: @@ -2411,12 +2582,28 @@ packages: resolution: {integrity: sha512-1P1OROm/rdubP5aFDSZQILU0vrLCJ4fvHt6EoqHEM+2D/G5MK3bIaymUKLit8Js9gbns5UyJnkP/TZROLw4tUA==} dev: true + /@sinclair/typebox@0.27.8: + resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} + dev: true + /@sinonjs/commons@1.8.3: resolution: {integrity: sha512-xkNcLAn/wZaX14RPlwizcKicDk9G3F8m2nU3L7Ukm5zBgTwiT0wsoFAHx9Jq56fJA1z/7uKGtCRu16sOUCLIHQ==} dependencies: type-detect: 4.0.8 dev: true + /@sinonjs/commons@3.0.0: + resolution: {integrity: sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==} + dependencies: + type-detect: 4.0.8 + dev: true + + /@sinonjs/fake-timers@10.3.0: + resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==} + dependencies: + '@sinonjs/commons': 3.0.0 + dev: true + /@sinonjs/fake-timers@9.1.2: resolution: {integrity: sha512-BPS4ynJW/o92PUR4wgriz2Ud5gpST5vz6GQfMixEDK0Z8ZCUv2M7SkBLykH56T++Xs+8ln9zTGbOvNGIe02/jw==} dependencies: @@ -2468,6 +2655,20 @@ packages: pretty-format: 27.5.1 dev: true + /@testing-library/dom@9.3.1: + resolution: {integrity: sha512-0DGPd9AR3+iDTjGoMpxIkAsUihHZ3Ai6CneU6bRRrffXMgzCdlNk43jTrD2/5LT6CBb3MWTP8v510JzYtahD2w==} + engines: {node: '>=14'} + dependencies: + '@babel/code-frame': 7.18.6 + '@babel/runtime': 7.20.7 + '@types/aria-query': 5.0.1 + aria-query: 5.1.3 + chalk: 4.1.2 + dom-accessibility-api: 0.5.14 + lz-string: 1.5.0 + pretty-format: 27.5.1 + dev: true + /@testing-library/react@13.4.0(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-sXOGON+WNTh3MLE9rve97ftaZukN3oNf2KjDy7YTx6hcTO2uuLHuCGynMDhFwGw/jYf4OJ2Qk0i4i79qMNNkyw==} engines: {node: '>=12'} @@ -2482,6 +2683,20 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: true + /@testing-library/react@14.0.0(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-S04gSNJbYE30TlIMLTzv6QCTzt9AqIF5y6s6SzVFILNcNvbV/jU96GeiTPillGQo+Ny64M/5PV7klNYYgv5Dfg==} + engines: {node: '>=14'} + peerDependencies: + react: ^18.0.0 + react-dom: ^18.0.0 + dependencies: + '@babel/runtime': 7.20.7 + '@testing-library/dom': 9.3.1 + '@types/react-dom': 18.0.6 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: true + /@tootallnate/once@2.0.0: resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==} engines: {node: '>= 10'} @@ -2491,6 +2706,10 @@ packages: resolution: {integrity: sha512-HnYpAE1Y6kRyKM/XkEuiRQhTHvkzMBurTHnpFLYLBGPIylZNPs9jJcuOOYWxPLJCSEtmZT0Y8rHDokKN7rRTig==} dev: true + /@types/aria-query@5.0.1: + resolution: {integrity: sha512-XTIieEY+gvJ39ChLcB4If5zHtPxt3Syj5rgZR+e1ctpmK8NjPf0zFqsz4JpLJT0xla9GFDKjy8Cpu331nrmE1Q==} + dev: true + /@types/babel__core@7.1.19: resolution: {integrity: sha512-WEOTgRsbYkvA/KCsDwVEGkd7WAr1e3g31VHQ8zy5gul/V1qKullU/BU5I68X5v7V3GnB9eotmom4v5a5gjxorw==} dependencies: @@ -2555,6 +2774,13 @@ packages: pretty-format: 26.6.2 dev: true + /@types/jest@29.5.4: + resolution: {integrity: sha512-PhglGmhWeD46FYOVLt3X7TiWjzwuVGW9wG/4qocPevXMjCmrIc5b6db9WjeGE4QYVpUAWMDv3v0IiBwObY289A==} + dependencies: + expect: 29.2.2 + pretty-format: 29.2.1 + dev: true + /@types/jsdom@20.0.0: resolution: {integrity: sha512-YfAchFs0yM1QPDrLm2VHe+WHGtqms3NXnXAMolrgrVP6fgBHHXy1ozAbo/dFtPNtZC/m66bPiCTWYmqp1F14gA==} dependencies: @@ -3013,7 +3239,7 @@ packages: picomatch: 2.3.1 dev: true - /aqu@0.4.3(@babel/core@7.19.6)(@types/node@18.11.18): + /aqu@0.4.3(@babel/core@7.19.6): resolution: {integrity: sha512-3Jq4rKSL0lRqkMnUtjbFMliGPpCeiOTmWtpY0ryFH1M10igx6Go+TOoa+StADJjgiwgMg6hYUdam+YcnBsn7kQ==} engines: {node: ^10.12.0 || >=12.0.0} hasBin: true @@ -3036,7 +3262,7 @@ packages: fs-extra: 10.1.0 github-username: 6.0.0 inquirer: 7.3.3 - jest: 29.2.2(@types/node@18.11.18) + jest: 29.2.2 jest-watch-typeahead: 2.2.0(jest@29.2.2) lodash: 4.17.21 ora: 5.4.1 @@ -3072,6 +3298,12 @@ packages: deep-equal: 2.0.5 dev: true + /aria-query@5.1.3: + resolution: {integrity: sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==} + dependencies: + deep-equal: 2.0.5 + dev: true + /arr-diff@4.0.0: resolution: {integrity: sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA==} engines: {node: '>=0.10.0'} @@ -3190,6 +3422,24 @@ packages: - supports-color dev: true + /babel-jest@29.7.0(@babel/core@7.19.6): + resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@babel/core': ^7.8.0 + dependencies: + '@babel/core': 7.19.6 + '@jest/transform': 29.7.0 + '@types/babel__core': 7.1.19 + babel-plugin-istanbul: 6.1.1 + babel-preset-jest: 29.6.3(@babel/core@7.19.6) + chalk: 4.1.2 + graceful-fs: 4.2.10 + slash: 3.0.0 + transitivePeerDependencies: + - supports-color + dev: true + /babel-plugin-istanbul@6.1.1: resolution: {integrity: sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==} engines: {node: '>=8'} @@ -3213,6 +3463,16 @@ packages: '@types/babel__traverse': 7.18.2 dev: true + /babel-plugin-jest-hoist@29.6.3: + resolution: {integrity: sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@babel/template': 7.18.10 + '@babel/types': 7.19.4 + '@types/babel__core': 7.1.19 + '@types/babel__traverse': 7.18.2 + dev: true + /babel-plugin-polyfill-corejs2@0.3.3(@babel/core@7.19.6): resolution: {integrity: sha512-8hOdmFYFSZhqg2C/JgLUQ+t52o5nirNwaWM2B9LWteozwIvM14VSwdsCAUET10qT+kmySAlseadmfeeSWFCy+Q==} peerDependencies: @@ -3280,13 +3540,24 @@ packages: babel-preset-current-node-syntax: 1.0.1(@babel/core@7.19.6) dev: true - /balanced-match@1.0.2: - resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - - /base64-js@1.5.1: - resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - dev: true - + /babel-preset-jest@29.6.3(@babel/core@7.19.6): + resolution: {integrity: sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.19.6 + babel-plugin-jest-hoist: 29.6.3 + babel-preset-current-node-syntax: 1.0.1(@babel/core@7.19.6) + dev: true + + /balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + /base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + dev: true + /base@0.11.2: resolution: {integrity: sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==} engines: {node: '>=0.10.0'} @@ -3665,6 +3936,10 @@ packages: resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} dev: true + /convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + dev: true + /copy-descriptor@0.1.1: resolution: {integrity: sha512-XgZ0pFcakEUlbwQEVNg3+QAis1FyTL3Qel9FYy8pSkQqoG3PNoT0bOCQtOXcOkur21r2Eq2kI+IE+gsmAEVlYw==} engines: {node: '>=0.10.0'} @@ -3725,6 +4000,25 @@ packages: p-map: 6.0.0 dev: true + /create-jest@29.7.0(@types/node@18.11.18): + resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + dependencies: + '@jest/types': 29.6.3 + chalk: 4.1.2 + exit: 0.1.2 + graceful-fs: 4.2.10 + jest-config: 29.7.0(@types/node@18.11.18) + jest-util: 29.7.0 + prompts: 2.4.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + dev: true + /cross-spawn@5.1.0: resolution: {integrity: sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==} dependencies: @@ -3848,6 +4142,10 @@ packages: whatwg-url: 11.0.0 dev: true + /dayjs@1.11.9: + resolution: {integrity: sha512-QvzAURSbQ0pKdIye2txOzNaHmxtUBXerpY0FJsFXUMKbIZeFm5ht1LS/jFsrncjnmtv8HsG0W2g6c0zUjZWmpA==} + dev: false + /debug@2.6.9: resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} peerDependencies: @@ -3918,6 +4216,15 @@ packages: resolution: {integrity: sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==} dev: true + /dedent@1.5.1: + resolution: {integrity: sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg==} + peerDependencies: + babel-plugin-macros: ^3.1.0 + peerDependenciesMeta: + babel-plugin-macros: + optional: true + dev: true + /deep-equal@2.0.5: resolution: {integrity: sha512-nPiRgmbAtm1a3JsnLCf6/SLfXcjyN5v8L1TXzdCmHrXJ4hx+gW/w1YCcn7z8gJtSiDArZCgYtbao3QqLm/N1Sw==} dependencies: @@ -4010,6 +4317,11 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dev: true + /diff-sequences@29.6.3: + resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dev: true + /dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} @@ -4790,6 +5102,17 @@ packages: jest-util: 29.2.1 dev: true + /expect@29.7.0: + resolution: {integrity: sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/expect-utils': 29.7.0 + jest-get-type: 29.6.3 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + dev: true + /extend-shallow@2.0.1: resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} engines: {node: '>=0.10.0'} @@ -5786,6 +6109,19 @@ packages: - supports-color dev: true + /istanbul-lib-instrument@6.0.0: + resolution: {integrity: sha512-x58orMzEVfzPUKqlbLd1hXCnySCxKdDKa6Rjg97CwuLLRI4g3FHTdnExu1OqffVFay6zeMW+T6/DowFLndWnIw==} + engines: {node: '>=10'} + dependencies: + '@babel/core': 7.19.6 + '@babel/parser': 7.19.6 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-coverage: 3.2.0 + semver: 7.5.4 + transitivePeerDependencies: + - supports-color + dev: true + /istanbul-lib-report@3.0.0: resolution: {integrity: sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==} engines: {node: '>=8'} @@ -5822,6 +6158,15 @@ packages: p-limit: 3.1.0 dev: true + /jest-changed-files@29.7.0: + resolution: {integrity: sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + execa: 5.1.1 + jest-util: 29.7.0 + p-limit: 3.1.0 + dev: true + /jest-circus@29.2.2: resolution: {integrity: sha512-upSdWxx+Mh4DV7oueuZndJ1NVdgtTsqM4YgywHEx05UMH5nxxA2Qu9T9T9XVuR021XxqSoaKvSmmpAbjwwwxMw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -5849,7 +6194,36 @@ packages: - supports-color dev: true - /jest-cli@29.2.2(@types/node@18.11.18): + /jest-circus@29.7.0: + resolution: {integrity: sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/environment': 29.7.0 + '@jest/expect': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 18.11.18 + chalk: 4.1.2 + co: 4.6.0 + dedent: 1.5.1 + is-generator-fn: 2.1.0 + jest-each: 29.7.0 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-runtime: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + p-limit: 3.1.0 + pretty-format: 29.7.0 + pure-rand: 6.0.3 + slash: 3.0.0 + stack-utils: 2.0.5 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + dev: true + + /jest-cli@29.2.2: resolution: {integrity: sha512-R45ygnnb2CQOfd8rTPFR+/fls0d+1zXS6JPYTBBrnLPrhr58SSuPTiA5Tplv8/PXpz4zXR/AYNxmwIj6J6nrvg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true @@ -5877,6 +6251,34 @@ packages: - ts-node dev: true + /jest-cli@29.7.0(@types/node@18.11.18): + resolution: {integrity: sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@jest/core': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + chalk: 4.1.2 + create-jest: 29.7.0(@types/node@18.11.18) + exit: 0.1.2 + import-local: 3.1.0 + jest-config: 29.7.0(@types/node@18.11.18) + jest-util: 29.7.0 + jest-validate: 29.7.0 + yargs: 17.6.0 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + dev: true + /jest-config@29.2.2(@types/node@18.11.18): resolution: {integrity: sha512-Q0JX54a5g1lP63keRfKR8EuC7n7wwny2HoTRDb8cx78IwQOiaYUVZAdjViY3WcTxpR02rPUpvNVmZ1fkIlZPcw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -5916,6 +6318,46 @@ packages: - supports-color dev: true + /jest-config@29.7.0(@types/node@18.11.18): + resolution: {integrity: sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@types/node': '*' + ts-node: '>=9.0.0' + peerDependenciesMeta: + '@types/node': + optional: true + ts-node: + optional: true + dependencies: + '@babel/core': 7.19.6 + '@jest/test-sequencer': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 18.11.18 + babel-jest: 29.7.0(@babel/core@7.19.6) + chalk: 4.1.2 + ci-info: 3.5.0 + deepmerge: 4.2.2 + glob: 7.2.3 + graceful-fs: 4.2.10 + jest-circus: 29.7.0 + jest-environment-node: 29.7.0 + jest-get-type: 29.6.3 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-runner: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + micromatch: 4.0.5 + parse-json: 5.2.0 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + dev: true + /jest-diff@26.6.2: resolution: {integrity: sha512-6m+9Z3Gv9wN0WFVasqjCL/06+EFCMTqDEUl/b87HYK2rAPTyfz4ZIuSlPhY51PIQRWx5TaxeF1qmXKe9gfN3sA==} engines: {node: '>= 10.14.2'} @@ -5936,6 +6378,16 @@ packages: pretty-format: 29.2.1 dev: true + /jest-diff@29.7.0: + resolution: {integrity: sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + chalk: 4.1.2 + diff-sequences: 29.6.3 + jest-get-type: 29.6.3 + pretty-format: 29.7.0 + dev: true + /jest-docblock@29.2.0: resolution: {integrity: sha512-bkxUsxTgWQGbXV5IENmfiIuqZhJcyvF7tU4zJ/7ioTutdz4ToB5Yx6JOFBpgI+TphRY4lhOyCWGNH/QFQh5T6A==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -5943,6 +6395,13 @@ packages: detect-newline: 3.1.0 dev: true + /jest-docblock@29.7.0: + resolution: {integrity: sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + detect-newline: 3.1.0 + dev: true + /jest-each@29.2.1: resolution: {integrity: sha512-sGP86H/CpWHMyK3qGIGFCgP6mt+o5tu9qG4+tobl0LNdgny0aitLXs9/EBacLy3Bwqy+v4uXClqJgASJWcruYw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -5954,6 +6413,17 @@ packages: pretty-format: 29.2.1 dev: true + /jest-each@29.7.0: + resolution: {integrity: sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + chalk: 4.1.2 + jest-get-type: 29.6.3 + jest-util: 29.7.0 + pretty-format: 29.7.0 + dev: true + /jest-environment-jsdom@29.2.1: resolution: {integrity: sha512-MipBdmrjgzEdQMkK7b7wBShOfv1VqO6FVwa9S43bZwKYLC4dlWnPiCgNpZX3ypNEpJO8EMpMhg4HrUkWUZXGiw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -5989,6 +6459,18 @@ packages: jest-util: 29.2.1 dev: true + /jest-environment-node@29.7.0: + resolution: {integrity: sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/environment': 29.7.0 + '@jest/fake-timers': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 18.11.18 + jest-mock: 29.7.0 + jest-util: 29.7.0 + dev: true + /jest-get-type@26.3.0: resolution: {integrity: sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig==} engines: {node: '>= 10.14.2'} @@ -5999,6 +6481,11 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dev: true + /jest-get-type@29.6.3: + resolution: {integrity: sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dev: true + /jest-haste-map@29.2.1: resolution: {integrity: sha512-wF460rAFmYc6ARcCFNw4MbGYQjYkvjovb9GBT+W10Um8q5nHq98jD6fHZMDMO3tA56S8XnmNkM8GcA8diSZfnA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -6018,6 +6505,25 @@ packages: fsevents: 2.3.3 dev: true + /jest-haste-map@29.7.0: + resolution: {integrity: sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + '@types/graceful-fs': 4.1.5 + '@types/node': 18.11.18 + anymatch: 3.1.2 + fb-watchman: 2.0.2 + graceful-fs: 4.2.10 + jest-regex-util: 29.6.3 + jest-util: 29.7.0 + jest-worker: 29.7.0 + micromatch: 4.0.5 + walker: 1.0.8 + optionalDependencies: + fsevents: 2.3.3 + dev: true + /jest-leak-detector@29.2.1: resolution: {integrity: sha512-1YvSqYoiurxKOJtySc+CGVmw/e1v4yNY27BjWTVzp0aTduQeA7pdieLiW05wTYG/twlKOp2xS/pWuikQEmklug==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -6026,6 +6532,14 @@ packages: pretty-format: 29.2.1 dev: true + /jest-leak-detector@29.7.0: + resolution: {integrity: sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + jest-get-type: 29.6.3 + pretty-format: 29.7.0 + dev: true + /jest-matcher-utils@29.2.2: resolution: {integrity: sha512-4DkJ1sDPT+UX2MR7Y3od6KtvRi9Im1ZGLGgdLFLm4lPexbTaCgJW5NN3IOXlQHF7NSHY/VHhflQ+WoKtD/vyCw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -6036,6 +6550,16 @@ packages: pretty-format: 29.2.1 dev: true + /jest-matcher-utils@29.7.0: + resolution: {integrity: sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + chalk: 4.1.2 + jest-diff: 29.7.0 + jest-get-type: 29.6.3 + pretty-format: 29.7.0 + dev: true + /jest-message-util@29.2.1: resolution: {integrity: sha512-Dx5nEjw9V8C1/Yj10S/8ivA8F439VS8vTq1L7hEgwHFn9ovSKNpYW/kwNh7UglaEgXO42XxzKJB+2x0nSglFVw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -6051,6 +6575,21 @@ packages: stack-utils: 2.0.5 dev: true + /jest-message-util@29.7.0: + resolution: {integrity: sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@babel/code-frame': 7.18.6 + '@jest/types': 29.6.3 + '@types/stack-utils': 2.0.1 + chalk: 4.1.2 + graceful-fs: 4.2.10 + micromatch: 4.0.5 + pretty-format: 29.7.0 + slash: 3.0.0 + stack-utils: 2.0.5 + dev: true + /jest-mock@29.2.2: resolution: {integrity: sha512-1leySQxNAnivvbcx0sCB37itu8f4OX2S/+gxLAV4Z62shT4r4dTG9tACDywUAEZoLSr36aYUTsVp3WKwWt4PMQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -6060,6 +6599,15 @@ packages: jest-util: 29.2.1 dev: true + /jest-mock@29.7.0: + resolution: {integrity: sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + '@types/node': 18.11.18 + jest-util: 29.7.0 + dev: true + /jest-pnp-resolver@1.2.2(jest-resolve@29.2.2): resolution: {integrity: sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w==} engines: {node: '>=6'} @@ -6072,11 +6620,28 @@ packages: jest-resolve: 29.2.2 dev: true + /jest-pnp-resolver@1.2.2(jest-resolve@29.7.0): + resolution: {integrity: sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w==} + engines: {node: '>=6'} + peerDependencies: + jest-resolve: '*' + peerDependenciesMeta: + jest-resolve: + optional: true + dependencies: + jest-resolve: 29.7.0 + dev: true + /jest-regex-util@29.2.0: resolution: {integrity: sha512-6yXn0kg2JXzH30cr2NlThF+70iuO/3irbaB4mh5WyqNIvLLP+B6sFdluO1/1RJmslyh/f9osnefECflHvTbwVA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dev: true + /jest-regex-util@29.6.3: + resolution: {integrity: sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dev: true + /jest-resolve-dependencies@29.2.2: resolution: {integrity: sha512-wWOmgbkbIC2NmFsq8Lb+3EkHuW5oZfctffTGvwsA4JcJ1IRk8b2tg+hz44f0lngvRTeHvp3Kyix9ACgudHH9aQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -6087,6 +6652,16 @@ packages: - supports-color dev: true + /jest-resolve-dependencies@29.7.0: + resolution: {integrity: sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + jest-regex-util: 29.6.3 + jest-snapshot: 29.7.0 + transitivePeerDependencies: + - supports-color + dev: true + /jest-resolve@29.2.2: resolution: {integrity: sha512-3gaLpiC3kr14rJR3w7vWh0CBX2QAhfpfiQTwrFPvVrcHe5VUBtIXaR004aWE/X9B2CFrITOQAp5gxLONGrk6GA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -6102,6 +6677,21 @@ packages: slash: 3.0.0 dev: true + /jest-resolve@29.7.0: + resolution: {integrity: sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + chalk: 4.1.2 + graceful-fs: 4.2.10 + jest-haste-map: 29.7.0 + jest-pnp-resolver: 1.2.2(jest-resolve@29.7.0) + jest-util: 29.7.0 + jest-validate: 29.7.0 + resolve: 1.22.1 + resolve.exports: 2.0.2 + slash: 3.0.0 + dev: true + /jest-runner@29.2.2: resolution: {integrity: sha512-1CpUxXDrbsfy9Hr9/1zCUUhT813kGGK//58HeIw/t8fa/DmkecEwZSWlb1N/xDKXg3uCFHQp1GCvlSClfImMxg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -6131,6 +6721,35 @@ packages: - supports-color dev: true + /jest-runner@29.7.0: + resolution: {integrity: sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/console': 29.7.0 + '@jest/environment': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 18.11.18 + chalk: 4.1.2 + emittery: 0.13.1 + graceful-fs: 4.2.10 + jest-docblock: 29.7.0 + jest-environment-node: 29.7.0 + jest-haste-map: 29.7.0 + jest-leak-detector: 29.7.0 + jest-message-util: 29.7.0 + jest-resolve: 29.7.0 + jest-runtime: 29.7.0 + jest-util: 29.7.0 + jest-watcher: 29.7.0 + jest-worker: 29.7.0 + p-limit: 3.1.0 + source-map-support: 0.5.13 + transitivePeerDependencies: + - supports-color + dev: true + /jest-runtime@29.2.2: resolution: {integrity: sha512-TpR1V6zRdLynckKDIQaY41od4o0xWL+KOPUCZvJK2bu5P1UXhjobt5nJ2ICNeIxgyj9NGkO0aWgDqYPVhDNKjA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -6161,6 +6780,36 @@ packages: - supports-color dev: true + /jest-runtime@29.7.0: + resolution: {integrity: sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/environment': 29.7.0 + '@jest/fake-timers': 29.7.0 + '@jest/globals': 29.7.0 + '@jest/source-map': 29.6.3 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 18.11.18 + chalk: 4.1.2 + cjs-module-lexer: 1.2.2 + collect-v8-coverage: 1.0.1 + glob: 7.2.3 + graceful-fs: 4.2.10 + jest-haste-map: 29.7.0 + jest-message-util: 29.7.0 + jest-mock: 29.7.0 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + slash: 3.0.0 + strip-bom: 4.0.0 + transitivePeerDependencies: + - supports-color + dev: true + /jest-snapshot@29.2.2: resolution: {integrity: sha512-GfKJrpZ5SMqhli3NJ+mOspDqtZfJBryGA8RIBxF+G+WbDoC7HCqKaeAss4Z/Sab6bAW11ffasx8/vGsj83jyjA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -6193,6 +6842,34 @@ packages: - supports-color dev: true + /jest-snapshot@29.7.0: + resolution: {integrity: sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@babel/core': 7.19.6 + '@babel/generator': 7.19.6 + '@babel/plugin-syntax-jsx': 7.18.6(@babel/core@7.19.6) + '@babel/plugin-syntax-typescript': 7.18.6(@babel/core@7.19.6) + '@babel/types': 7.19.4 + '@jest/expect-utils': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + babel-preset-current-node-syntax: 1.0.1(@babel/core@7.19.6) + chalk: 4.1.2 + expect: 29.7.0 + graceful-fs: 4.2.10 + jest-diff: 29.7.0 + jest-get-type: 29.6.3 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + natural-compare: 1.4.0 + pretty-format: 29.7.0 + semver: 7.5.4 + transitivePeerDependencies: + - supports-color + dev: true + /jest-util@29.2.1: resolution: {integrity: sha512-P5VWDj25r7kj7kl4pN2rG/RN2c1TLfYYYZYULnS/35nFDjBai+hBeo3MDrYZS7p6IoY3YHZnt2vq4L6mKnLk0g==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -6205,6 +6882,18 @@ packages: picomatch: 2.3.1 dev: true + /jest-util@29.7.0: + resolution: {integrity: sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + '@types/node': 18.11.18 + chalk: 4.1.2 + ci-info: 3.5.0 + graceful-fs: 4.2.10 + picomatch: 2.3.1 + dev: true + /jest-validate@29.2.2: resolution: {integrity: sha512-eJXATaKaSnOuxNfs8CLHgdABFgUrd0TtWS8QckiJ4L/QVDF4KVbZFBBOwCBZHOS0Rc5fOxqngXeGXE3nGQkpQA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -6217,6 +6906,18 @@ packages: pretty-format: 29.2.1 dev: true + /jest-validate@29.7.0: + resolution: {integrity: sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + camelcase: 6.3.0 + chalk: 4.1.2 + jest-get-type: 29.6.3 + leven: 3.1.0 + pretty-format: 29.7.0 + dev: true + /jest-watch-typeahead@2.2.0(jest@29.2.2): resolution: {integrity: sha512-cM3Qbw9P+jUYxqUSt53KdDDFRVBG96XA6bsIAG0zffl/gUkNK/kjWcCX7R559BgPWs2/UDrsJHPIw2f6b0qZCw==} engines: {node: ^14.17.0 || ^16.10.0 || >=18.0.0} @@ -6225,7 +6926,7 @@ packages: dependencies: ansi-escapes: 5.0.0 chalk: 4.1.2 - jest: 29.2.2(@types/node@18.11.18) + jest: 29.2.2 jest-regex-util: 29.2.0 jest-watcher: 29.2.2 slash: 4.0.0 @@ -6247,6 +6948,20 @@ packages: string-length: 4.0.2 dev: true + /jest-watcher@29.7.0: + resolution: {integrity: sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 18.11.18 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + emittery: 0.13.1 + jest-util: 29.7.0 + string-length: 4.0.2 + dev: true + /jest-worker@29.2.1: resolution: {integrity: sha512-ROHTZ+oj7sBrgtv46zZ84uWky71AoYi0vEV9CdEtc1FQunsoAGe5HbQmW76nI5QWdvECVPrSi1MCVUmizSavMg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -6257,7 +6972,17 @@ packages: supports-color: 8.1.1 dev: true - /jest@29.2.2(@types/node@18.11.18): + /jest-worker@29.7.0: + resolution: {integrity: sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@types/node': 18.11.18 + jest-util: 29.7.0 + merge-stream: 2.0.0 + supports-color: 8.1.1 + dev: true + + /jest@29.2.2: resolution: {integrity: sha512-r+0zCN9kUqoON6IjDdjbrsWobXM/09Nd45kIPRD8kloaRh1z5ZCMdVsgLXGxmlL7UpAJsvCYOQNO+NjvG/gqiQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true @@ -6270,13 +6995,34 @@ packages: '@jest/core': 29.2.2 '@jest/types': 29.2.1 import-local: 3.1.0 - jest-cli: 29.2.2(@types/node@18.11.18) + jest-cli: 29.2.2 transitivePeerDependencies: - '@types/node' - supports-color - ts-node dev: true + /jest@29.7.0(@types/node@18.11.18): + resolution: {integrity: sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@jest/core': 29.7.0 + '@jest/types': 29.6.3 + import-local: 3.1.0 + jest-cli: 29.7.0(@types/node@18.11.18) + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + dev: true + /js-sdsl@4.1.5: resolution: {integrity: sha512-08bOAKweV2NUC1wqTtf3qZlnpOX/R2DU9ikpjOHs0H+ibQv3zpncVQg6um4uYtRtrwIX8M4Nh3ytK4HGlYAq7Q==} dev: true @@ -6370,6 +7116,12 @@ packages: hasBin: true dev: true + /json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + dev: true + /jsonfile@4.0.0: resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} optionalDependencies: @@ -6669,6 +7421,11 @@ packages: hasBin: true dev: true + /lz-string@1.5.0: + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} + hasBin: true + dev: true + /macos-release@2.5.0: resolution: {integrity: sha512-EIgv+QZ9r+814gjJj0Bt5vSLJLzswGmSUbUpbi9AIr/fsN2IWFBl2NucV9PAiek+U1STK468tEkxmVYUtuAN3g==} engines: {node: '>=6'} @@ -7389,6 +8146,15 @@ packages: react-is: 18.2.0 dev: true + /pretty-format@29.7.0: + resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/schemas': 29.6.3 + ansi-styles: 5.2.0 + react-is: 18.2.0 + dev: true + /prettyjson@1.2.5: resolution: {integrity: sha512-rksPWtoZb2ZpT5OVgtmy0KHVM+Dca3iVwWY9ifwhcexfjebtgjg3wmrUt9PvJ59XIYBcknQeYHD8IAnVlh9lAw==} hasBin: true @@ -7441,6 +8207,10 @@ packages: resolution: {integrity: sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==} engines: {node: '>=6'} + /pure-rand@6.0.3: + resolution: {integrity: sha512-KddyFewCsO0j3+np81IQ+SweXLDnDQTs5s67BOnrYmYe/yNmUhttQyGsYzy8yUnoljGAQ9sl38YB4vH8ur7Y+w==} + dev: true + /pxth@0.7.0: resolution: {integrity: sha512-V2bNJNl7FIuvjsnreT7EWaBiqD5akAuGrKAVxgarjmTSDcDIGKLhY39ZFBqMkSTN8OHEBM4pszNUN+rfrojMfw==} dependencies: @@ -7685,6 +8455,11 @@ packages: engines: {node: '>=10'} dev: true + /resolve.exports@2.0.2: + resolution: {integrity: sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==} + engines: {node: '>=10'} + dev: true + /resolve@1.22.1: resolution: {integrity: sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==} hasBin: true @@ -7806,6 +8581,14 @@ packages: dependencies: lru-cache: 6.0.0 + /semver@7.5.4: + resolution: {integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==} + engines: {node: '>=10'} + hasBin: true + dependencies: + lru-cache: 6.0.0 + dev: true + /set-blocking@2.0.0: resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} dev: true @@ -8355,7 +9138,7 @@ packages: bs-logger: 0.2.6 esbuild: 0.15.12 fast-json-stable-stringify: 2.1.0 - jest: 29.2.2(@types/node@18.11.18) + jest: 29.2.2 jest-util: 29.2.1 json5: 2.2.1 lodash.memoize: 4.1.2 @@ -8365,6 +9148,41 @@ packages: yargs-parser: 21.1.1 dev: true + /ts-jest@29.1.1(@babel/core@7.19.6)(esbuild@0.15.12)(jest@29.7.0)(typescript@4.8.4): + resolution: {integrity: sha512-D6xjnnbP17cC85nliwGiL+tpoKN0StpgE0TeOjXQTU6MVCfsB4v7aW05CgQ/1OywGb0x/oy9hHFnN+sczTiRaA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + '@babel/core': '>=7.0.0-beta.0 <8' + '@jest/types': ^29.0.0 + babel-jest: ^29.0.0 + esbuild: '*' + jest: ^29.0.0 + typescript: '>=4.3 <6' + peerDependenciesMeta: + '@babel/core': + optional: true + '@jest/types': + optional: true + babel-jest: + optional: true + esbuild: + optional: true + dependencies: + '@babel/core': 7.19.6 + bs-logger: 0.2.6 + esbuild: 0.15.12 + fast-json-stable-stringify: 2.1.0 + jest: 29.7.0(@types/node@18.11.18) + jest-util: 29.2.1 + json5: 2.2.3 + lodash.memoize: 4.1.2 + make-error: 1.3.6 + semver: 7.5.4 + typescript: 4.8.4 + yargs-parser: 21.1.1 + dev: true + /tslib@1.14.1: resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==}