Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Converting machine states to version 5 of xstate #814

Draft
wants to merge 1 commit into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading