1
0
mirror of synced 2024-11-25 14:56:03 +03:00

refactor: Moved utils to separated files, code cleanup

This commit is contained in:
Zaytsev Kirill 2020-10-26 00:07:23 +03:00
parent e814edf9fc
commit 3f5735299d
25 changed files with 380 additions and 383 deletions

View File

@ -5,8 +5,8 @@ import messages from '@/validation/messages'
import merge from '@/utils/merge'
import FormularioForm from '@/FormularioForm.vue'
import FormularioInput from '@/FormularioInput.vue'
import FormularioGrouping from '@/FormularioGrouping.vue'
import FormularioInput from '@/FormularioInput.vue'
import {
ValidationContext,

View File

@ -6,15 +6,8 @@
<script lang="ts">
import Vue from 'vue'
import {
Component,
Model,
Prop,
Provide,
Watch,
} from 'vue-property-decorator'
import { cloneDeep, getNested, has, setNested, shallowEqualObjects } from '@/libs/utils'
import merge from '@/utils/merge'
import { Component, Model, Prop, Provide, Watch } from 'vue-property-decorator'
import { clone, getNested, has, merge, setNested, shallowEqualObjects } from '@/utils'
import Registry from '@/form/registry'
import FormularioInput from '@/FormularioInput.vue'
@ -41,13 +34,22 @@ export default class FormularioForm extends Vue {
public proxy: Record<string, any> = {}
registry: Registry = new Registry(this)
private registry: Registry = new Registry(this)
private errorObserverRegistry = new ErrorObserverRegistry()
// Local error messages are temporal, they wiped each resetValidation call
private localFormErrors: string[] = []
private localFieldErrors: Record<string, string[]> = {}
get initialValues (): Record<string, any> {
if (this.hasModel && typeof this.formularioValue === 'object') {
// If there is a v-model on the form/group, use those values as first priority
return { ...this.formularioValue } // @todo - use a deep clone to detach reference types
}
return {}
}
get mergedFormErrors (): string[] {
return [...this.formErrors, ...this.localFormErrors]
}
@ -64,15 +66,6 @@ export default class FormularioForm extends Vue {
return this.formularioValue && typeof this.formularioValue === 'object'
}
get initialValues (): Record<string, any> {
if (this.hasModel && typeof this.formularioValue === 'object') {
// If there is a v-model on the form/group, use those values as first priority
return { ...this.formularioValue } // @todo - use a deep clone to detach reference types
}
return {}
}
@Watch('formularioValue', { deep: true })
onFormularioValueChanged (values: Record<string, any>): void {
if (this.hasModel && values && typeof values === 'object') {
@ -101,7 +94,7 @@ export default class FormularioForm extends Vue {
onFormSubmit (): Promise<void> {
return this.hasValidationErrors()
.then(hasErrors => hasErrors ? undefined : cloneDeep(this.proxy))
.then(hasErrors => hasErrors ? undefined : clone(this.proxy))
.then(data => {
if (typeof data !== 'undefined') {
this.$emit('submit', data)
@ -193,6 +186,11 @@ export default class FormularioForm extends Vue {
}
}
setErrors ({ formErrors, inputErrors }: { formErrors?: string[]; inputErrors?: Record<string, string[]> }): void {
this.localFormErrors = formErrors || []
this.localFieldErrors = inputErrors || {}
}
hasValidationErrors (): Promise<boolean> {
return Promise.all(this.registry.reduce((resolvers: Promise<boolean>[], input: FormularioInput) => {
resolvers.push(input.runValidation() && input.hasValidationErrors())
@ -200,11 +198,6 @@ export default class FormularioForm extends Vue {
}, [])).then(results => results.some(hasErrors => hasErrors))
}
setErrors ({ formErrors, inputErrors }: { formErrors?: string[]; inputErrors?: Record<string, string[]> }): void {
this.localFormErrors = formErrors || []
this.localFieldErrors = inputErrors || {}
}
resetValidation (): void {
this.localFormErrors = []
this.localFieldErrors = {}

View File

@ -13,7 +13,7 @@ import {
Prop,
Watch,
} from 'vue-property-decorator'
import { arrayify, has, shallowEqualObjects, snakeToCamel } from './libs/utils'
import { arrayify, has, shallowEqualObjects, snakeToCamel } from './utils'
import {
CheckRuleFn,
CreateMessageFn,

View File

@ -1,4 +1,4 @@
import { shallowEqualObjects, has, getNested } from '@/libs/utils'
import { shallowEqualObjects, has, getNested } from '@/utils'
import FormularioForm from '@/FormularioForm.vue'
import FormularioInput from '@/FormularioInput.vue'
@ -80,15 +80,6 @@ export default class Registry {
return result
}
/**
* Map over the registry (recursively).
*/
map (mapper: Function): Record<string, any> {
const value = {}
this.registry.forEach((component, field) => Object.assign(value, { [field]: mapper(component, field) }))
return value
}
/**
* Map over the registry (recursively).
*/
@ -137,11 +128,6 @@ export default class Registry {
// @ts-ignore
this.ctx.setFieldValue(field, component.proxy)
}
// @ts-ignore
if (this.ctx.childrenShouldShowErrors) {
// @ts-ignore
component.formShouldShowErrors = true
}
}
/**

View File

@ -1,203 +0,0 @@
export function shallowEqualObjects (objA: Record<string, any>, objB: Record<string, any>): boolean {
if (objA === objB) {
return true
}
if (!objA || !objB) {
return false
}
const aKeys = Object.keys(objA)
const bKeys = Object.keys(objB)
if (bKeys.length !== aKeys.length) {
return false
}
if (objA instanceof Date && objB instanceof Date) {
return objA.getTime() === objB.getTime()
}
if (aKeys.length === 0) {
return objA === objB
}
for (let i = 0; i < aKeys.length; i++) {
const key = aKeys[i]
if (objA[key] !== objB[key]) {
return false
}
}
return true
}
/**
* Given a string, convert snake_case to camelCase
* @param {String} string
*/
export function snakeToCamel (string: string | any): string | any {
if (typeof string === 'string') {
return string.replace(/([_][a-z0-9])/ig, ($1) => {
if (string.indexOf($1) !== 0 && string[string.indexOf($1) - 1] !== '_') {
return $1.toUpperCase().replace('_', '')
}
return $1
})
}
return string
}
/**
* Converts to array.
* If given parameter is not string, object ot array, result will be an empty array.
* @param {*} item
*/
export function arrayify (item: any): any[] {
if (!item) {
return []
}
if (typeof item === 'string') {
return [item]
}
if (Array.isArray(item)) {
return item
}
if (typeof item === 'object') {
return Object.values(item)
}
return []
}
/**
* Escape a string for use in regular expressions.
*/
export function escapeRegExp (string: string): string {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // $& means the whole matched string
}
/**
* Given a string format (date) return a regex to match against.
*/
export function regexForFormat (format: string): RegExp {
const escaped = `^${escapeRegExp(format)}$`
const formats: Record<string, string> = {
MM: '(0[1-9]|1[012])',
M: '([1-9]|1[012])',
DD: '([012][1-9]|3[01])',
D: '([012]?[1-9]|3[01])',
YYYY: '\\d{4}',
YY: '\\d{2}'
}
return new RegExp(Object.keys(formats).reduce((regex, format) => {
return regex.replace(format, formats[format])
}, escaped))
}
/**
* Check if
* @param {*} data
*/
export function isScalar (data: any): boolean {
switch (typeof data) {
case 'symbol':
case 'number':
case 'string':
case 'boolean':
case 'undefined':
return true
default:
return data === null
}
}
/**
* A simple (somewhat non-comprehensive) cloneDeep function, valid for our use
* case of needing to unbind reactive watchers.
*/
export function cloneDeep (value: any): any {
if (typeof value !== 'object') {
return value
}
const copy: any | Record<string, any> = Array.isArray(value) ? [] : {}
for (const key in value) {
if (Object.prototype.hasOwnProperty.call(value, key)) {
if (isScalar(value[key])) {
copy[key] = value[key]
} else {
copy[key] = cloneDeep(value[key])
}
}
}
return copy
}
/**
* Shorthand for Object.prototype.hasOwnProperty.call (space saving)
*/
export function has (ctx: Record<string, any>, prop: string): boolean {
return Object.prototype.hasOwnProperty.call(ctx, prop)
}
export function getNested (obj: Record<string, any>, field: string): any {
const fieldParts = field.split('.')
let result: Record<string, any> = obj
for (const key in fieldParts) {
const matches = fieldParts[key].match(/(.+)\[(\d+)\]$/)
if (result === undefined) {
return null
}
if (matches) {
result = result[matches[1]]
if (result === undefined) {
return null
}
result = result[matches[2]]
} else {
result = result[fieldParts[key]]
}
}
return result
}
export function setNested (obj: Record<string, any>, field: string, value: any): void {
const fieldParts = field.split('.')
let subProxy: Record<string, any> = obj
for (let i = 0; i < fieldParts.length; i++) {
const fieldPart = fieldParts[i]
const matches = fieldPart.match(/(.+)\[(\d+)\]$/)
if (matches) {
if (subProxy[matches[1]] === undefined) {
subProxy[matches[1]] = []
}
subProxy = subProxy[matches[1]]
if (i === fieldParts.length - 1) {
subProxy[matches[2]] = value
break
} else {
subProxy = subProxy[matches[2]]
}
} else {
if (i === fieldParts.length - 1) {
subProxy[fieldPart] = value
break
} else {
// eslint-disable-next-line max-depth
if (subProxy[fieldPart] === undefined) {
subProxy[fieldPart] = {}
}
subProxy = subProxy[fieldPart]
}
}
}
}

6
src/shims-ext.d.ts vendored
View File

@ -11,10 +11,4 @@ declare module 'vue/types/vue' {
interface VueRoute {
path: string;
}
interface FormularioForm extends Vue {
name: string | boolean;
proxy: Record<string, any>;
hasValidationErrors(): Promise<boolean>;
}
}

20
src/utils/arrayify.ts Normal file
View File

@ -0,0 +1,20 @@
/**
* Converts to array.
* If given parameter is not string, object ot array, result will be an empty array.
* @param {*} item
*/
export default function arrayify (item: any): any[] {
if (!item) {
return []
}
if (typeof item === 'string') {
return [item]
}
if (Array.isArray(item)) {
return item
}
if (typeof item === 'object') {
return Object.values(item)
}
return []
}

22
src/utils/clone.ts Normal file
View File

@ -0,0 +1,22 @@
import isScalar from '@/utils/isScalar'
import has from '@/utils/has'
/**
* A simple (somewhat non-comprehensive) clone function, valid for our use
* case of needing to unbind reactive watchers.
*/
export default function clone (value: any): any {
if (typeof value !== 'object') {
return value
}
const copy: any | Record<string, any> = Array.isArray(value) ? [] : {}
for (const key in value) {
if (has(value, key)) {
copy[key] = isScalar(value[key]) ? value[key] : clone(value[key])
}
}
return copy
}

6
src/utils/has.ts Normal file
View File

@ -0,0 +1,6 @@
/**
* Shorthand for Object.prototype.hasOwnProperty.call (space saving)
*/
export default function has (ctx: Record<string, any>|any[], prop: string|number): boolean {
return Object.prototype.hasOwnProperty.call(ctx, prop)
}

67
src/utils/index.ts Normal file
View File

@ -0,0 +1,67 @@
export { default as arrayify } from './arrayify'
export { default as clone } from './clone'
export { default as has } from './has'
export { default as isScalar } from './isScalar'
export { default as merge } from './merge'
export { default as regexForFormat } from './regexForFormat'
export { default as shallowEqualObjects } from './shallowEqualObjects'
export { default as snakeToCamel } from './snakeToCamel'
export function getNested (obj: Record<string, any>, field: string): any {
const fieldParts = field.split('.')
let result: Record<string, any> = obj
for (const key in fieldParts) {
const matches = fieldParts[key].match(/(.+)\[(\d+)\]$/)
if (result === undefined) {
return null
}
if (matches) {
result = result[matches[1]]
if (result === undefined) {
return null
}
result = result[matches[2]]
} else {
result = result[fieldParts[key]]
}
}
return result
}
export function setNested (obj: Record<string, any>, field: string, value: any): void {
const fieldParts = field.split('.')
let subProxy: Record<string, any> = obj
for (let i = 0; i < fieldParts.length; i++) {
const fieldPart = fieldParts[i]
const matches = fieldPart.match(/(.+)\[(\d+)\]$/)
if (matches) {
if (subProxy[matches[1]] === undefined) {
subProxy[matches[1]] = []
}
subProxy = subProxy[matches[1]]
if (i === fieldParts.length - 1) {
subProxy[matches[2]] = value
break
} else {
subProxy = subProxy[matches[2]]
}
} else {
if (i === fieldParts.length - 1) {
subProxy[fieldPart] = value
break
} else {
// eslint-disable-next-line max-depth
if (subProxy[fieldPart] === undefined) {
subProxy[fieldPart] = {}
}
subProxy = subProxy[fieldPart]
}
}
}
}

12
src/utils/isScalar.ts Normal file
View File

@ -0,0 +1,12 @@
export default function isScalar (data: any): boolean {
switch (typeof data) {
case 'symbol':
case 'number':
case 'string':
case 'boolean':
case 'undefined':
return true
default:
return data === null
}
}

View File

@ -1,5 +1,5 @@
import isPlainObject from 'is-plain-object'
import { has } from '@/libs/utils.ts'
import has from '@/utils/has.ts'
/**
* Create a new object by copying properties of base and mergeWith.

View File

@ -0,0 +1,25 @@
/**
* Escape a string for use in regular expressions.
*/
function escapeRegExp (string: string): string {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // $& means the whole matched string
}
/**
* Given a string format (date) return a regex to match against.
*/
export default function regexForFormat (format: string): RegExp {
const escaped = `^${escapeRegExp(format)}$`
const formats: Record<string, string> = {
MM: '(0[1-9]|1[012])',
M: '([1-9]|1[012])',
DD: '([012][1-9]|3[01])',
D: '([012]?[1-9]|3[01])',
YYYY: '\\d{4}',
YY: '\\d{2}'
}
return new RegExp(Object.keys(formats).reduce((regex, format) => {
return regex.replace(format, formats[format])
}, escaped))
}

View File

@ -0,0 +1,34 @@
export default function shallowEqualObjects (objA: Record<string, any>, objB: Record<string, any>): boolean {
if (objA === objB) {
return true
}
if (!objA || !objB) {
return false
}
const aKeys = Object.keys(objA)
const bKeys = Object.keys(objB)
if (bKeys.length !== aKeys.length) {
return false
}
if (objA instanceof Date && objB instanceof Date) {
return objA.getTime() === objB.getTime()
}
if (aKeys.length === 0) {
return objA === objB
}
for (let i = 0; i < aKeys.length; i++) {
const key = aKeys[i]
if (objA[key] !== objB[key]) {
return false
}
}
return true
}

11
src/utils/snakeToCamel.ts Normal file
View File

@ -0,0 +1,11 @@
/**
* Given a string, convert snake_case to camelCase
*/
export default function snakeToCamel (string: string): string {
return string.replace(/([_][a-z0-9])/ig, ($1) => {
if (string.indexOf($1) !== 0 && string[string.indexOf($1) - 1] !== '_') {
return $1.toUpperCase().replace('_', '')
}
return $1
})
}

View File

@ -1,4 +1,4 @@
import { has } from '@/libs/utils'
import { has } from '@/utils'
export interface ErrorHandler {
(errors: Record<string, any> | any[]): void;

View File

@ -1,5 +1,5 @@
import isUrl from 'is-url'
import { shallowEqualObjects, regexForFormat, has } from '@/libs/utils'
import { shallowEqualObjects, regexForFormat, has } from '@/utils'
import { ValidationContext } from '@/validation/validator'
interface DateValidationContext extends ValidationContext {

View File

@ -1,4 +1,4 @@
import { has, snakeToCamel } from '@/libs/utils'
import { has, snakeToCamel } from '@/utils'
export interface Validator {
(context: ValidationContext): Promise<Violation|null>;
@ -59,7 +59,7 @@ export function parseModifier (ruleName: string): [string, string|null] {
return [snakeToCamel(ruleName), null]
}
export function processArrayConstraint (
export function processSingleArrayConstraint (
constraint: any[],
rules: Record<string, CheckRuleFn>,
messages: Record<string, CreateMessageFn>
@ -93,7 +93,7 @@ export function processArrayConstraint (
throw new Error(`[Formulario] Can't create validator for constraint: ${JSON.stringify(constraint)}`)
}
export function processStringConstraint (
export function processSingleStringConstraint (
constraint: string,
rules: Record<string, CheckRuleFn>,
messages: Record<string, CreateMessageFn>
@ -117,12 +117,8 @@ export function processStringConstraint (
throw new Error(`[Formulario] Can't create validator for constraint: ${constraint}`)
}
/**
* Given a string or function, parse it and return an array in the format
* [fn, [...arguments]]
*/
export function processConstraint (
constraint: any,
export function processSingleConstraint (
constraint: string|Validator|[Validator|string, ...any[]],
rules: Record<string, CheckRuleFn>,
messages: Record<string, CreateMessageFn>
): [Validator, string|null, string|null] {
@ -131,11 +127,11 @@ export function processConstraint (
}
if (Array.isArray(constraint) && constraint.length) {
return processArrayConstraint(constraint, rules, messages)
return processSingleArrayConstraint(constraint, rules, messages)
}
if (typeof constraint === 'string') {
return processStringConstraint(constraint, rules, messages)
return processSingleStringConstraint(constraint, rules, messages)
}
return [(): Promise<Violation|null> => Promise.resolve(null), null, null]
@ -152,7 +148,7 @@ export function processConstraints (
if (!Array.isArray(constraints)) {
return []
}
return constraints.map(constraint => processConstraint(constraint, rules, messages))
return constraints.map(constraint => processSingleConstraint(constraint, rules, messages))
}
export function enlarge (groups: ValidatorGroup[]): ValidatorGroup[] {

View File

@ -1,116 +0,0 @@
import { cloneDeep, isScalar, regexForFormat, snakeToCamel } from '@/libs/utils'
describe('regexForFormat', () => {
it('allows MM format with other characters', () => expect(regexForFormat('abc/MM').test('abc/01')).toBe(true))
it('fails MM format with single digit', () => expect(regexForFormat('abc/MM').test('abc/1')).toBe(false))
it('allows M format with single digit', () => expect(regexForFormat('M/abc').test('1/abc')).toBe(true))
it('fails MM format when out of range', () => expect(regexForFormat('M/abc').test('13/abc')).toBe(false))
it('fails M format when out of range', () => expect(regexForFormat('M/abc').test('55/abc')).toBe(false))
it('Replaces double digits before singles', () => expect(regexForFormat('MMM').test('313131')).toBe(false))
it('allows DD format with zero digit', () => expect(regexForFormat('xyz/DD').test('xyz/01')).toBe(true))
it('fails DD format with single digit', () => expect(regexForFormat('xyz/DD').test('xyz/9')).toBe(false))
it('allows D format with single digit', () => expect(regexForFormat('xyz/D').test('xyz/9')).toBe(true))
it('fails D format with out of range digit', () => expect(regexForFormat('xyz/D').test('xyz/92')).toBe(false))
it('fails DD format with out of range digit', () => expect(regexForFormat('xyz/D').test('xyz/32')).toBe(false))
it('allows YY format with double zeros', () => expect(regexForFormat('YY').test('00')).toBe(true))
it('fails YY format with four zeros', () => expect(regexForFormat('YY').test('0000')).toBe(false))
it('allows YYYY format with four zeros', () => expect(regexForFormat('YYYY').test('0000')).toBe(true))
it('allows MD-YY', () => expect(regexForFormat('MD-YY').test('12-00')).toBe(true))
it('allows DM-YY', () => expect(regexForFormat('DM-YY').test('12-00')).toBe(true))
it('allows date like MM/DD/YYYY', () => expect(regexForFormat('MM/DD/YYYY').test('12/18/1987')).toBe(true))
it('allows date like YYYY-MM-DD', () => expect(regexForFormat('YYYY-MM-DD').test('1987-01-31')).toBe(true))
it('fails date like YYYY-MM-DD with out of bounds day', () => expect(regexForFormat('YYYY-MM-DD').test('1987-01-32')).toBe(false))
})
describe('isScalar', () => {
it('passes on strings', () => expect(isScalar('hello')).toBe(true))
it('passes on numbers', () => expect(isScalar(123)).toBe(true))
it('passes on booleans', () => expect(isScalar(false)).toBe(true))
it('passes on symbols', () => expect(isScalar(Symbol(123))).toBe(true))
it('passes on null', () => expect(isScalar(null)).toBe(true))
it('passes on undefined', () => expect(isScalar(undefined)).toBe(true))
it('fails on pojo', () => expect(isScalar({})).toBe(false))
})
describe('cloneDeep', () => {
it('basic objects stay the same', () => expect(cloneDeep({ a: 123, b: 'hello' })).toEqual({ a: 123, b: 'hello' }))
it('basic nested objects stay the same', () => {
expect(cloneDeep({ a: 123, b: { c: 'hello-world' } }))
.toEqual({ a: 123, b: { c: 'hello-world' } })
})
it('simple pojo reference types are re-created', () => {
const c = { c: 'hello-world' }
const clone = cloneDeep({ a: 123, b: c })
expect(clone.b === c).toBe(false)
})
it('retains array structures inside of a pojo', () => {
const obj = { a: 'abcd', d: ['first', 'second'] }
const clone = cloneDeep(obj)
expect(Array.isArray(clone.d)).toBe(true)
})
it('removes references inside array structures', () => {
const deepObj = {foo: 'bar'}
const obj = { a: 'abcd', d: ['first', deepObj] }
const clone = cloneDeep(obj)
expect(clone.d[1] === deepObj).toBe(false)
})
})
describe('snakeToCamel', () => {
it('converts underscore separated words to camelCase', () => {
expect(snakeToCamel('this_is_snake_case')).toBe('thisIsSnakeCase')
})
it('converts underscore separated words to camelCase even if they start with a number', () => {
expect(snakeToCamel('this_is_snake_case_2nd_example')).toBe('thisIsSnakeCase2ndExample')
})
it('has no effect on already camelCase words', () => {
expect(snakeToCamel('thisIsCamelCase')).toBe('thisIsCamelCase')
})
it('does not capitalize the first word or strip first underscore if a phrase starts with an underscore', () => {
expect(snakeToCamel('_this_starts_with_an_underscore')).toBe('_thisStartsWithAnUnderscore')
})
it('ignores double underscores anywhere in a word', () => {
expect(snakeToCamel('__unlikely__thing__')).toBe('__unlikely__thing__')
})
it('has no effect hyphenated words', () => {
expect(snakeToCamel('not-a-good-name')).toBe('not-a-good-name')
})
it('returns the same function if passed', () => {
const fn = () => {}
expect(snakeToCamel(fn)).toBe(fn)
})
})

View File

@ -0,0 +1,28 @@
import clone from '@/utils/clone'
describe('clone', () => {
it('Basic objects stay the same', () => {
const obj = { a: 123, b: 'hello' }
expect(clone(obj)).toEqual(obj)
})
it('Basic nested objects stay the same', () => {
const obj = { a: 123, b: { c: 'hello-world' } }
expect(clone(obj)).toEqual(obj)
})
it('Simple pojo reference types are re-created', () => {
const c = { c: 'hello-world' }
expect(clone({ a: 123, b: c }).b === c).toBe(false)
})
it('Retains array structures inside of a pojo', () => {
const obj = { a: 'abcd', d: ['first', 'second'] }
expect(Array.isArray(clone(obj).d)).toBe(true)
})
it('Removes references inside array structures', () => {
const obj = { a: 'abcd', d: ['first', { foo: 'bar' }] }
expect(clone(obj).d[1] === obj.d[1]).toBe(false)
})
})

View File

@ -0,0 +1,17 @@
import isScalar from '@/utils/isScalar'
describe('isScalar', () => {
it('Passes on strings', () => expect(isScalar('hello')).toBe(true))
it('Passes on numbers', () => expect(isScalar(123)).toBe(true))
it('Passes on booleans', () => expect(isScalar(false)).toBe(true))
it('Passes on symbols', () => expect(isScalar(Symbol(123))).toBe(true))
it('Passes on null', () => expect(isScalar(null)).toBe(true))
it('Passes on undefined', () => expect(isScalar(undefined)).toBe(true))
it('Fails on pojo', () => expect(isScalar({})).toBe(false))
})

View File

@ -0,0 +1,79 @@
import regexForFormat from '@/utils/regexForFormat'
describe('regexForFormat', () => {
it('Allows MM format with other characters', () => {
expect(regexForFormat('abc/MM').test('abc/01')).toBe(true)
})
it('Fails MM format with single digit', () => {
expect(regexForFormat('abc/MM').test('abc/1')).toBe(false)
})
it('Allows M format with single digit', () => {
expect(regexForFormat('M/abc').test('1/abc')).toBe(true)
})
it('Fails MM format when out of range', () => {
expect(regexForFormat('M/abc').test('13/abc')).toBe(false)
})
it('Fails M format when out of range', () => {
expect(regexForFormat('M/abc').test('55/abc')).toBe(false)
})
it('Replaces double digits before singles', () => {
expect(regexForFormat('MMM').test('313131')).toBe(false)
})
it('Allows DD format with zero digit', () => {
expect(regexForFormat('xyz/DD').test('xyz/01')).toBe(true)
})
it('Fails DD format with single digit', () => {
expect(regexForFormat('xyz/DD').test('xyz/9')).toBe(false)
})
it('Allows D format with single digit', () => {
expect(regexForFormat('xyz/D').test('xyz/9')).toBe(true)
})
it('Fails D format with out of range digit', () => {
expect(regexForFormat('xyz/D').test('xyz/92')).toBe(false)
})
it('Fails DD format with out of range digit', () => {
expect(regexForFormat('xyz/D').test('xyz/32')).toBe(false)
})
it('Allows YY format with double zeros', () => {
expect(regexForFormat('YY').test('00')).toBe(true)
})
it('Fails YY format with four zeros', () => {
expect(regexForFormat('YY').test('0000')).toBe(false)
})
it('Allows YYYY format with four zeros', () => {
expect(regexForFormat('YYYY').test('0000')).toBe(true)
})
it('Allows MD-YY', () => {
expect(regexForFormat('MD-YY').test('12-00')).toBe(true)
})
it('Allows DM-YY', () => {
expect(regexForFormat('DM-YY').test('12-00')).toBe(true)
})
it('Allows date like MM/DD/YYYY', () => {
expect(regexForFormat('MM/DD/YYYY').test('12/18/1987')).toBe(true)
})
it('Allows date like YYYY-MM-DD', () => {
expect(regexForFormat('YYYY-MM-DD').test('1987-01-31')).toBe(true)
})
it('Fails date like YYYY-MM-DD with out of bounds day', () => {
expect(regexForFormat('YYYY-MM-DD').test('1987-01-32')).toBe(false)
})
})

View File

@ -0,0 +1,27 @@
import snakeToCamel from '@/utils/snakeToCamel'
describe('snakeToCamel', () => {
it('Converts underscore separated words to camelCase', () => {
expect(snakeToCamel('this_is_snake_case')).toBe('thisIsSnakeCase')
})
it('Converts underscore separated words to camelCase even if they start with a number', () => {
expect(snakeToCamel('this_is_snake_case_2nd_example')).toBe('thisIsSnakeCase2ndExample')
})
it('Has no effect on already camelCase words', () => {
expect(snakeToCamel('thisIsCamelCase')).toBe('thisIsCamelCase')
})
it('Does not capitalize the first word or strip first underscore if a phrase starts with an underscore', () => {
expect(snakeToCamel('_this_starts_with_an_underscore')).toBe('_thisStartsWithAnUnderscore')
})
it('Ignores double underscores anywhere in a word', () => {
expect(snakeToCamel('__unlikely__thing__')).toBe('__unlikely__thing__')
})
it('Has no effect hyphenated words', () => {
expect(snakeToCamel('not-a-good-name')).toBe('not-a-good-name')
})
})

View File

@ -1,6 +1,5 @@
import rules from '@/validation/rules.ts'
/**
* Accepted rule
*/