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

chore(a11y): Improve granularity of DateInput errors #2864

Merged
merged 2 commits into from
Mar 7, 2024
Merged
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
53 changes: 0 additions & 53 deletions editor.planx.uk/src/@planx/components/DateInput/Public.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { axe, setup } from "testUtils";

import { ERROR_MESSAGE } from "../shared/constants";
import { fillInFieldsUsingPlaceholder } from "../shared/testHelpers";
import { dateRangeSchema, dateSchema, paddedDate } from "./model";
import DateInput from "./Public";

test("submits a date", async () => {
Expand Down Expand Up @@ -139,58 +138,6 @@ test("date fields have a max length set", async () => {
expect(year.maxLength).toBe(4);
});

test("padding on input", () => {
// Adds zero to single digits greater than 3 on input
expect(paddedDate("2021-12-6", "input")).toBe("2021-12-06");
expect(paddedDate("2021-4-22", "input")).toBe("2021-04-22");
expect(paddedDate("2021-8-4", "input")).toBe("2021-08-04");

// Leaves valid dates alone
expect(paddedDate("2021-01-06", "input")).toBe("2021-01-06");
expect(paddedDate("2021-04-22", "input")).toBe("2021-04-22");
expect(paddedDate("2021-08-04", "input")).toBe("2021-08-04");

// Leaves single 0 alone
expect(paddedDate("2021-0-4", "input")).toBe("2021-0-04");
expect(paddedDate("2021-10-0", "input")).toBe("2021-10-0");
});

test("padding on blur", () => {
// Adds zero to single digits less than or equal to 3 on blur
expect(paddedDate("2021-12-1", "blur")).toBe("2021-12-01");
expect(paddedDate("2021-3-22", "blur")).toBe("2021-03-22");
expect(paddedDate("2021-2-2", "blur")).toBe("2021-02-02");

// Leaves valid dates alone
expect(paddedDate("2021-01-06", "blur")).toBe("2021-01-06");
expect(paddedDate("2021-04-22", "blur")).toBe("2021-04-22");
expect(paddedDate("2021-08-04", "blur")).toBe("2021-08-04");

// Leaves single 0 alone
expect(paddedDate("2021-0-2", "blur")).toBe("2021-0-02");
expect(paddedDate("2021-10-0", "blur")).toBe("2021-10-0");
});

test("validation", async () => {
expect(await dateSchema().isValid("2021-03-23")).toBe(true);
expect(await dateSchema().isValid("2021-23-03")).toBe(false);
expect(
await dateRangeSchema({ min: "1990-01-01", max: "1999-12-31" }).isValid(
"1995-06-15",
),
).toBe(true);
expect(
await dateRangeSchema({ min: "1990-01-01", max: "1999-12-31" }).isValid(
"2021-06-15",
),
).toBe(false);
expect(
await dateRangeSchema({ min: "1990-01-01", max: "1999-12-31" }).isValid(
"1980-06-15",
),
).toBe(false);
});

it("should not have any accessibility violations upon initial load", async () => {
const { container } = setup(
<DateInput id="123" title="Test title" description="description" />,
Expand Down
148 changes: 148 additions & 0 deletions editor.planx.uk/src/@planx/components/DateInput/model.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import { dateRangeSchema, dateSchema, paddedDate, parseDate } from "./model";

describe("parseDate helper function", () => {
it("returns a day value", () => {
const result = parseDate("2024-02-12");
expect(result.day).toBeDefined();
expect(result.day).toEqual(12);
});

it("returns a month value", () => {
const result = parseDate("2024-02-12");
expect(result.month).toBeDefined();
expect(result.month).toEqual(2);
});

it("returns a year value", () => {
const result = parseDate("2024-02-12");
expect(result.year).toBeDefined();
expect(result.year).toEqual(2024);
});

it("handles undefined inputs", () => {
const result = parseDate(undefined);
expect(result.day).not.toBeDefined();
expect(result.month).not.toBeDefined();
expect(result.year).not.toBeDefined();
});

it("handles invalid inputs", () => {
const result = parseDate("not a date");
expect(result.day).not.toBeDefined();
expect(result.month).not.toBeDefined();
expect(result.year).not.toBeDefined();
});

it("handles partial inputs", () => {
const result = parseDate("2024-MM-05");
expect(result.day).toEqual(5);
expect(result.month).not.toBeDefined();
expect(result.year).toEqual(2024);
});
});

describe("dateSchema", () => {
test("basic validation", async () => {
expect(await dateSchema().isValid("2021-03-23")).toBe(true);
expect(await dateSchema().isValid("2021-23-03")).toBe(false);
});

const validate = async (date?: string) => await dateSchema().validate(date).catch((err) => err.errors);

it("throws an error for an undefined value (empty form)", async () => {
const errors = await validate(undefined);
expect(errors[0]).toMatch(/Date must include a day/);
});

it("throws an error for an nonsensical value", async () => {
const errors = await validate("ab-cd-efgh");
expect(errors[0]).toMatch(/Date must include a day/);
});

it("throws an error for a missing day", async () => {
const errors = await validate("2024-12-");
expect(errors[0]).toMatch(/Date must include a day/);
});

it("throws an error for a missing month", async () => {
const errors = await validate("2024--25");
expect(errors[0]).toMatch(/Date must include a month/);
});

it("throws an error for a missing year", async () => {
const errors = await validate("-12-23");
expect(errors[0]).toMatch(/Date must include a year/);
});

it("throws an error for an invalid day", async () => {
const errors = await validate("2024-12-32");
expect(errors[0]).toMatch(/Day must be valid/);
});

it("throws an error for an invalid month", async () => {
const errors = await validate("2024-13-25");
expect(errors[0]).toMatch(/Month must be valid/);
});

it("throws an error for an invalid date (30th Feb)", async () => {
const errors = await validate("2024-02-30");
expect(errors[0]).toMatch(/Enter a valid date in DD.MM.YYYY format/);
});

it("throws an error for an invalid date (not a leap year)", async () => {
const errors = await validate("2023-02-29");
expect(errors[0]).toMatch(/Enter a valid date in DD.MM.YYYY format/);
});
});

describe("dateRangeSchema", () => {
test("basic validation", async () => {
expect(
await dateRangeSchema({ min: "1990-01-01", max: "1999-12-31" }).isValid(
"1995-06-15",
),
).toBe(true);
expect(
await dateRangeSchema({ min: "1990-01-01", max: "1999-12-31" }).isValid(
"2021-06-15",
),
).toBe(false);
expect(
await dateRangeSchema({ min: "1990-01-01", max: "1999-12-31" }).isValid(
"1980-06-15",
),
).toBe(false);
})
});

test("padding on input", () => {
// Adds zero to single digits greater than 3 on input
expect(paddedDate("2021-12-6", "input")).toBe("2021-12-06");
expect(paddedDate("2021-4-22", "input")).toBe("2021-04-22");
expect(paddedDate("2021-8-4", "input")).toBe("2021-08-04");

// Leaves valid dates alone
expect(paddedDate("2021-01-06", "input")).toBe("2021-01-06");
expect(paddedDate("2021-04-22", "input")).toBe("2021-04-22");
expect(paddedDate("2021-08-04", "input")).toBe("2021-08-04");

// Leaves single 0 alone
expect(paddedDate("2021-0-4", "input")).toBe("2021-0-04");
expect(paddedDate("2021-10-0", "input")).toBe("2021-10-0");
});

test("padding on blur", () => {
// Adds zero to single digits less than or equal to 3 on blur
expect(paddedDate("2021-12-1", "blur")).toBe("2021-12-01");
expect(paddedDate("2021-3-22", "blur")).toBe("2021-03-22");
expect(paddedDate("2021-2-2", "blur")).toBe("2021-02-02");

// Leaves valid dates alone
expect(paddedDate("2021-01-06", "blur")).toBe("2021-01-06");
expect(paddedDate("2021-04-22", "blur")).toBe("2021-04-22");
expect(paddedDate("2021-08-04", "blur")).toBe("2021-08-04");

// Leaves single 0 alone
expect(paddedDate("2021-0-2", "blur")).toBe("2021-0-02");
expect(paddedDate("2021-10-0", "blur")).toBe("2021-10-0");
});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Helpful split 👍

102 changes: 68 additions & 34 deletions editor.planx.uk/src/@planx/components/DateInput/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,7 @@ export interface DateInput extends MoreInformation {
max?: string;
}

export const isDateValid = (date: string) => {
// make sure we've got DD, MM, & YYYY
const isComplete = date.split("-").filter((n) => n.length > 0).length === 3;

return isComplete && isValid(parseISO(date));
};
const isDateValid = (date: string) => isValid(parseISO(date));

export const paddedDate = (
date: string,
Expand Down Expand Up @@ -64,41 +59,80 @@ const displayDate = (date: string): string | undefined => {
return `${day}.${month}.${year}`;
};

export const parseDate = (date?: string) => {
const [year, month, day] = date?.split("-").map((val) => parseInt(val) || undefined) || [];
return { year, month, day };
}

export const dateSchema = () => {
return string().test(
"valid",
"Enter a valid date in DD.MM.YYYY format",
(date: string | undefined) => {
// test runs regardless of required status, so don't fail it if it's undefined
return Boolean(!date || isDateValid(date));
},
);
return string()
.test(
"missing day",
"Date must include a day",
(date?: string) => {
const { day } = parseDate(date);
return day !== undefined;
})
.test(
"missing month",
"Date must include a month",
(date?: string) => {
const { month } = parseDate(date);
return month !== undefined;
})
.test(
"missing year",
"Date must include a year",
(date?: string) => {
const { year } = parseDate(date);
return year !== undefined;
})
.test(
"invalid day",
"Day must be valid",
(date?: string) => {
const { day } = parseDate(date);
return Boolean(day && day <= 31)
})
.test(
"invalid month",
"Month must be valid",
(date?: string) => {
const { month } = parseDate(date);
return Boolean(month && month <= 12);
})
.test(
"valid",
"Enter a valid date in DD.MM.YYYY format",
(date: string | undefined) => {
// test runs regardless of required status, so don't fail it if it's undefined
return Boolean(!date || isDateValid(date));
},
);
};

export const dateRangeSchema: (params: {
min?: string;
max?: string;
}) => SchemaOf<string> = (params) =>
dateSchema()
.required("Enter a valid date in DD.MM.YYYY format")
.test({
name: "too soon",
message: `Enter a date later than ${
params.min && displayDate(params.min)
}`,
test: (date: string | undefined) => {
return Boolean(date && !(params.min && date < params.min));
},
})
.test({
name: "too late",
message: `Enter a date earlier than ${
params.max && displayDate(params.max)
}`,
test: (date: string | undefined) => {
return Boolean(date && !(params.max && date > params.max));
},
});
dateSchema()
.required("Enter a valid date in DD.MM.YYYY format")
.test({
name: "too soon",
message: `Enter a date later than ${params.min && displayDate(params.min)
}`,
test: (date: string | undefined) => {
return Boolean(date && !(params.min && date < params.min));
},
})
.test({
name: "too late",
message: `Enter a date earlier than ${params.max && displayDate(params.max)
}`,
test: (date: string | undefined) => {
return Boolean(date && !(params.max && date > params.max));
},
});

export const parseDateInput = (
data: Record<string, any> | undefined,
Expand Down
Loading