From df4fd4e96671407bf2566ffb2a8a6bdf9911add9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20L=2E=20Redrejo=20Rodr=C3=ADguez?= Date: Mon, 4 Nov 2024 09:43:16 +0100 Subject: [PATCH] Converting machine states to version 5 of xstate --- lib/KDateRange/ValidationMachine.js | 115 ++++++++++----- .../__tests__/ValidationMachine.spec.js | 137 ++++++++++++------ lib/KDateRange/index.vue | 41 ++++-- package.json | 2 +- 4 files changed, 198 insertions(+), 97 deletions(-) diff --git a/lib/KDateRange/ValidationMachine.js b/lib/KDateRange/ValidationMachine.js index 59d099c39..416c03753 100644 --- a/lib/KDateRange/ValidationMachine.js +++ b/lib/KDateRange/ValidationMachine.js @@ -1,4 +1,4 @@ -import { createMachine, assign } from 'xstate'; +import { setup, assign } from 'xstate'; import { isAfter, startOfDay, isBefore } from 'date-fns'; import validationConstants from './validationConstants'; @@ -7,7 +7,7 @@ import validationConstants from './validationConstants'; * Returns if the given prop is equal to the placeholder **/ function isPlaceholder(dateStr) { - return dateStr === null; + return dateStr === null || dateStr === undefined; } /** @@ -15,7 +15,8 @@ function isPlaceholder(dateStr) { * Returns if the given prop matches the constant dateFormat RegExp pattern **/ const isCorrectFormat = dateStr => { - return dateFormat.test(dateStr) || dateStr === null; + if (isPlaceholder(dateStr)) return true; + return dateFormat.test(dateStr); }; /** @@ -24,13 +25,19 @@ const isCorrectFormat = dateStr => { * Returns if the end date is after the start date **/ const isEndDateAfterStart = (startDate, endDate) => { - if (startDate && endDate != null) { + if (isPlaceholder(startDate) || isPlaceholder(endDate)) { + return false; + } + + try { const [startYear, startMonth, startDay] = startDate.split('-'); const newStartDate = startOfDay(new Date(startYear, startMonth - 1, startDay)); const [endYear, endMonth, endDay] = endDate.split('-'); const newEndDate = startOfDay(new Date(endYear, endMonth - 1, endDay)); return isAfter(newStartDate, newEndDate); + } catch (e) { + return false; } }; @@ -40,10 +47,16 @@ const isEndDateAfterStart = (startDate, endDate) => { * Returns if the given date string is after the last allowed date **/ const isDateAfterLastAllowed = (dateStr, lastAllowedDate) => { - if (!isPlaceholder(dateStr)) { + if (isPlaceholder(dateStr) || !lastAllowedDate) { + return false; + } + + try { const [year, month, day] = dateStr.split('-'); const newDate = startOfDay(new Date(year, month - 1, day)); return isAfter(newDate, lastAllowedDate); + } catch (e) { + return false; } }; @@ -53,10 +66,16 @@ const isDateAfterLastAllowed = (dateStr, lastAllowedDate) => { * Returns if the given date string is before the first allowed date **/ const isDateBeforeFirstAllowed = (dateStr, firstAllowedDate) => { - if (!isPlaceholder(dateStr)) { + if (isPlaceholder(dateStr) || !firstAllowedDate) { + return false; + } + + try { const [year, month, day] = dateStr.split('-'); const newDate = startOfDay(new Date(year, month - 1, day)); return isBefore(newDate, firstAllowedDate); + } catch (e) { + return false; } }; @@ -66,27 +85,34 @@ const isDateBeforeFirstAllowed = (dateStr, firstAllowedDate) => { **/ export const validate = ({ startDate, endDate, firstAllowedDate, lastAllowedDate }) => { const validatedContext = { startDateInvalid: false, endDateInvalid: false }; + + // Check format first if (!isCorrectFormat(startDate)) { validatedContext.startDateInvalid = validationConstants.MALFORMED; } if (!isCorrectFormat(endDate)) { validatedContext.endDateInvalid = validationConstants.MALFORMED; } - if (isEndDateAfterStart(startDate, endDate)) { - validatedContext.startDateInvalid = validationConstants.START_DATE_AFTER_END_DATE; - } - if (isDateAfterLastAllowed(startDate, lastAllowedDate)) { - validatedContext.startDateInvalid = validationConstants.FUTURE_DATE; - } - if (isDateBeforeFirstAllowed(startDate, firstAllowedDate)) { - validatedContext.startDateInvalid = validationConstants.DATE_BEFORE_FIRST_ALLOWED; - } - if (isDateAfterLastAllowed(endDate, lastAllowedDate)) { - validatedContext.endDateInvalid = validationConstants.FUTURE_DATE; - } - if (isDateBeforeFirstAllowed(endDate, firstAllowedDate)) { - validatedContext.endDateInvalid = validationConstants.DATE_BEFORE_FIRST_ALLOWED; + + // Only continue with other validations if format is correct + if (!validatedContext.startDateInvalid && !validatedContext.endDateInvalid) { + if (isEndDateAfterStart(startDate, endDate)) { + validatedContext.startDateInvalid = validationConstants.START_DATE_AFTER_END_DATE; + } + if (isDateAfterLastAllowed(startDate, lastAllowedDate)) { + validatedContext.startDateInvalid = validationConstants.FUTURE_DATE; + } + if (isDateBeforeFirstAllowed(startDate, firstAllowedDate)) { + validatedContext.startDateInvalid = validationConstants.DATE_BEFORE_FIRST_ALLOWED; + } + if (isDateAfterLastAllowed(endDate, lastAllowedDate)) { + validatedContext.endDateInvalid = validationConstants.FUTURE_DATE; + } + if (isDateBeforeFirstAllowed(endDate, firstAllowedDate)) { + validatedContext.endDateInvalid = validationConstants.DATE_BEFORE_FIRST_ALLOWED; + } } + return validatedContext; }; @@ -103,51 +129,62 @@ export const initialContext = { firstAllowedDate: null, }; -export const validationMachine = createMachine({ - predictableActionArguments: true, +export const validationMachine = setup({ id: 'fetch', + actions: { + clearValidation: assign({ + startDateInvalid: false, + endDateInvalid: false, + }), + validateDates: assign(context => validate(context)), + updateDates: assign((context, event) => ({ + ...context, + ...event, + startDateInvalid: false, + endDateInvalid: false, + })), + }, + guards: { + areDatesPlaceholders: context => + isPlaceholder(context.startDate) && isPlaceholder(context.endDate), + hasValidationErrors: context => + Boolean(context.startDateInvalid) || Boolean(context.endDateInvalid), + }, +}).createMachine({ + id: 'dateValidation', initial: 'placeholder', context: initialContext, states: { placeholder: { always: [ { - cond: context => isPlaceholder(context.startDate) && isPlaceholder(context.endDate), + guard: 'areDatesPlaceholders', target: 'success', - actions: assign({ - startDateInvalid: false, - endDateInvalid: false, - }), + actions: 'clearValidation', }, { target: 'validation', - actions: assign(context => validate(context)), + actions: 'validateDates', }, ], }, validation: { always: [ { - cond: context => context.startDateInvalid || context.endDateInvalid, + guard: 'hasValidationErrors', target: 'failure', }, { target: 'success', - actions: assign({ - startDateInvalid: false, - endDateInvalid: false, - }), + actions: 'clearValidation', }, ], }, - success: { on: { REVALIDATE: { target: 'placeholder', - actions: assign((_, event) => { - return { ...event, startDateInvalid: false, endDateInvalid: false }; - }), + actions: 'updateDates', }, }, }, @@ -155,9 +192,7 @@ export const validationMachine = createMachine({ on: { REVALIDATE: { target: 'placeholder', - actions: assign((_, event) => { - return { ...event, startDateInvalid: false, endDateInvalid: false }; - }), + actions: 'updateDates', }, }, }, diff --git a/lib/KDateRange/__tests__/ValidationMachine.spec.js b/lib/KDateRange/__tests__/ValidationMachine.spec.js index 6f6e1e80e..13bdeb368 100644 --- a/lib/KDateRange/__tests__/ValidationMachine.spec.js +++ b/lib/KDateRange/__tests__/ValidationMachine.spec.js @@ -1,71 +1,124 @@ -import { interpret } from 'xstate'; +import { createActor } from 'xstate'; import validationConstants from '../validationConstants'; import { validationMachine, initialContext } from '../ValidationMachine'; +// Create a date that will be valid for all tests +const today = new Date(); +const lastAllowedDate = new Date(today.getFullYear(), today.getMonth(), today.getDate()); +const firstAllowedDate = new Date(2022, 0, 1); + const currentContext = { startDate: '2022-01-09', endDate: '2022-01-10', - lastAllowedDate: new Date(), - firstAllowedDate: new Date(2022, 0, 1), + lastAllowedDate, + firstAllowedDate, }; + describe('Validation Machine', () => { - let validateService; - beforeAll(() => { - validateService = interpret( - validationMachine.withContext({ ...initialContext, ...currentContext }) - ); - validateService.start(); + let validateActor; + + beforeEach(() => { + // Initialize with null dates first + validateActor = createActor(validationMachine, { + input: { + ...initialContext, + lastAllowedDate, + firstAllowedDate + } + }).start(); + + // Then send the actual dates + validateActor.send({ + type: 'REVALIDATE', + startDate: currentContext.startDate, + endDate: currentContext.endDate + }); + }); + + afterEach(() => { + validateActor.stop(); }); - it('validation machine should be in success state when given correct props', async () => { - expect(validateService._state.value).toEqual('success'); + it('validation machine should be in success state when given correct props', () => { + const snapshot = validateActor.getSnapshot(); + expect(snapshot.value).toEqual('success'); }); - it('returns startDateInvalid error message when start date is malformed', async () => { - validateService.send('REVALIDATE', { startDate: 'aaaaaaa' }); - expect(validateService._state.value).toEqual('failure'); - expect(validateService._state.context.startDateInvalid).toEqual(validationConstants.MALFORMED); - expect(validateService._state.context.endDateInvalid).toBeFalsy(); + it('returns startDateInvalid error message when start date is malformed', () => { + validateActor.send({ + type: 'REVALIDATE', + startDate: 'aaaaaaa', + endDate: currentContext.endDate + }); + const snapshot = validateActor.getSnapshot(); + expect(snapshot.value).toEqual('failure'); + expect(snapshot.context.startDateInvalid).toEqual(validationConstants.MALFORMED); + expect(snapshot.context.endDateInvalid).toBeFalsy(); }); - it('returns endDateInvalid error message when end date is malformed', async () => { - validateService.send('REVALIDATE', { startDate: '2022-01-09', endDate: 'aaaaaaa' }); - expect(validateService._state.value).toEqual('failure'); - expect(validateService._state.context.endDateInvalid).toEqual(validationConstants.MALFORMED); - expect(validateService._state.context.startDateInvalid).toBeFalsy(); + it('returns endDateInvalid error message when end date is malformed', () => { + validateActor.send({ + type: 'REVALIDATE', + startDate: currentContext.startDate, + endDate: 'aaaaaaa' + }); + const snapshot = validateActor.getSnapshot(); + expect(snapshot.value).toEqual('failure'); + expect(snapshot.context.endDateInvalid).toEqual(validationConstants.MALFORMED); + expect(snapshot.context.startDateInvalid).toBeFalsy(); }); - it('returns startDateInvalid error message when end date is before start date', async () => { - validateService.send('REVALIDATE', { startDate: '2022-01-09', endDate: '2022-01-06' }); - expect(validateService._state.value).toEqual('failure'); - expect(validateService._state.context.startDateInvalid).toEqual( + it('returns startDateInvalid error message when end date is before start date', () => { + validateActor.send({ + type: 'REVALIDATE', + startDate: '2022-01-09', + endDate: '2022-01-06' + }); + const snapshot = validateActor.getSnapshot(); + expect(snapshot.value).toEqual('failure'); + expect(snapshot.context.startDateInvalid).toEqual( validationConstants.START_DATE_AFTER_END_DATE ); - expect(validateService._state.context.endDateInvalid).toBeFalsy(); + expect(snapshot.context.endDateInvalid).toBeFalsy(); }); - it('returns startDateInvalid error message when start date is before the first allowed date and endDateInvalid error message when end date is malformed', async () => { - validateService.send('REVALIDATE', { startDate: '2019-01-12', endDate: 'aaaaaa' }); - expect(validateService._state.value).toEqual('failure'); - expect(validateService._state.context.startDateInvalid).toEqual( + it('returns startDateInvalid error message when start date is before the first allowed date and endDateInvalid error message when end date is malformed', () => { + validateActor.send({ + type: 'REVALIDATE', + startDate: '2019-01-12', + endDate: 'aaaaaa' + }); + const snapshot = validateActor.getSnapshot(); + expect(snapshot.value).toEqual('failure'); + expect(snapshot.context.startDateInvalid).toEqual( validationConstants.DATE_BEFORE_FIRST_ALLOWED ); - expect(validateService._state.context.endDateInvalid).toEqual(validationConstants.MALFORMED); + expect(snapshot.context.endDateInvalid).toEqual(validationConstants.MALFORMED); }); - it('returns endDateInvalid error message when end date is before first allowed and startDateInvalid error message when start date is malformed', async () => { - validateService.send('REVALIDATE', { startDate: 'invalid', endDate: '2019-01-06' }); - expect(validateService._state.value).toEqual('failure'); - expect(validateService._state.context.startDateInvalid).toEqual(validationConstants.MALFORMED); - expect(validateService._state.context.endDateInvalid).toEqual( + it('returns endDateInvalid error message when end date is before first allowed and startDateInvalid error message when start date is malformed', () => { + validateActor.send({ + type: 'REVALIDATE', + startDate: 'invalid', + endDate: '2019-01-06' + }); + const snapshot = validateActor.getSnapshot(); + expect(snapshot.value).toEqual('failure'); + expect(snapshot.context.startDateInvalid).toEqual(validationConstants.MALFORMED); + expect(snapshot.context.endDateInvalid).toEqual( validationConstants.DATE_BEFORE_FIRST_ALLOWED ); }); - it('validation in success state after revalidating with correct props', async () => { - validateService.send('REVALIDATE', currentContext); - expect(validateService._state.value).toEqual('success'); - expect(validateService._state.context.startDateInvalid).toBeFalsy(); - expect(validateService._state.context.endDateInvalid).toBeFalsy(); + it('validation in success state after revalidating with correct props', () => { + validateActor.send({ + type: 'REVALIDATE', + startDate: currentContext.startDate, + endDate: currentContext.endDate + }); + const snapshot = validateActor.getSnapshot(); + expect(snapshot.value).toEqual('success'); + expect(snapshot.context.startDateInvalid).toBeFalsy(); + expect(snapshot.context.endDateInvalid).toBeFalsy(); }); -}); +}); \ No newline at end of file diff --git a/lib/KDateRange/index.vue b/lib/KDateRange/index.vue index 917cbb372..2570a32f8 100644 --- a/lib/KDateRange/index.vue +++ b/lib/KDateRange/index.vue @@ -60,7 +60,7 @@ diff --git a/package.json b/package.json index 2b2c2aa16..33156341a 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "tippy.js": "4.2.1", "vue-intl": "3.1.0", "vue2-teleport": "1.1.4", - "xstate": "4.38.3" + "xstate": "5.18.2" }, "peerDependencies": {}, "devDependencies": {