Skip to content

Commit

Permalink
Converting machine states to version 5 of xstate
Browse files Browse the repository at this point in the history
  • Loading branch information
jredrejo committed Nov 4, 2024
1 parent ea611a5 commit df4fd4e
Show file tree
Hide file tree
Showing 4 changed files with 198 additions and 97 deletions.
115 changes: 75 additions & 40 deletions lib/KDateRange/ValidationMachine.js
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -7,15 +7,16 @@ 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;
}

/**
* @params dateStr - The input date string value
* 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);
};

/**
Expand All @@ -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;
}
};

Expand All @@ -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;
}
};

Expand All @@ -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;
}
};

Expand All @@ -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;
};

Expand All @@ -103,61 +129,70 @@ 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',
},
},
},
failure: {
on: {
REVALIDATE: {
target: 'placeholder',
actions: assign((_, event) => {
return { ...event, startDateInvalid: false, endDateInvalid: false };
}),
actions: 'updateDates',
},
},
},
Expand Down
137 changes: 95 additions & 42 deletions lib/KDateRange/__tests__/ValidationMachine.spec.js
Original file line number Diff line number Diff line change
@@ -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();
});
});
});
Loading

0 comments on commit df4fd4e

Please sign in to comment.