From 6aa891301e842db49581d51b8d7c53e354b177f2 Mon Sep 17 00:00:00 2001 From: Chris Pymm Date: Thu, 7 Nov 2024 11:55:38 +0000 Subject: [PATCH 1/5] fix(date picker): fix for the weekstartday config value being able to be set to an invalid value it was possible to set the weekStartDay config value to a day other than sunday or monday. this fix correctly forces the value to always be monday unless "sunday" is passed --- src/moj/components/date-picker/date-picker.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/moj/components/date-picker/date-picker.js b/src/moj/components/date-picker/date-picker.js index c30e7a6b..e0fa8466 100644 --- a/src/moj/components/date-picker/date-picker.js +++ b/src/moj/components/date-picker/date-picker.js @@ -178,7 +178,7 @@ Datepicker.prototype.initControls = function () { ); this.$dialog.addEventListener("keydown", (event) => { - if (event.key == "Escape") { + if (event.key === "Escape") { this.closeDialog(); event.preventDefault(); event.stopPropagation(); @@ -422,7 +422,7 @@ Datepicker.prototype.setWeekStartDay = function () { // Rotate dayLabels array to put Sunday as the first item this.dayLabels.unshift(this.dayLabels.pop()); } - if (weekStartDayParam?.toLowerCase() === "monday") { + else { this.config.weekStartDay = "monday"; } }; From 6133c7970797ccba244d22eb4209defc700f463f Mon Sep 17 00:00:00 2001 From: Chris Pymm Date: Thu, 7 Nov 2024 11:56:44 +0000 Subject: [PATCH 2/5] test(date picker): add tests for js config of the datepicker component --- package-lock.json | 9 +- package.json | 1 + .../date-picker/date-picker.spec.js | 293 ++++++++++++++++-- 3 files changed, 282 insertions(+), 21 deletions(-) diff --git a/package-lock.json b/package-lock.json index dbf2c328..ce6ffa01 100644 --- a/package-lock.json +++ b/package-lock.json @@ -40,6 +40,7 @@ "copy-webpack-plugin": "^12.0.0", "cssnano": "^6.0.0", "cz-conventional-changelog": "^3.3.0", + "dayjs": "^1.11.13", "gulp-postcss": "^10.0.0", "gulp-rename": "^2.0.0", "gulp-sass": "^5.0.0", @@ -9444,6 +9445,12 @@ "node": ">=12" } }, + "node_modules/dayjs": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", + "dev": true + }, "node_modules/debug": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", @@ -30570,7 +30577,7 @@ }, "package": { "name": "@ministryofjustice/frontend", - "version": "3.0.0", + "version": "3.0.2", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 3d3689a8..df75d1d8 100644 --- a/package.json +++ b/package.json @@ -70,6 +70,7 @@ "copy-webpack-plugin": "^12.0.0", "cssnano": "^6.0.0", "cz-conventional-changelog": "^3.3.0", + "dayjs": "^1.11.13", "gulp-postcss": "^10.0.0", "gulp-rename": "^2.0.0", "gulp-sass": "^5.0.0", diff --git a/src/moj/components/date-picker/date-picker.spec.js b/src/moj/components/date-picker/date-picker.spec.js index 814fe54d..0730bcdd 100644 --- a/src/moj/components/date-picker/date-picker.spec.js +++ b/src/moj/components/date-picker/date-picker.spec.js @@ -9,6 +9,7 @@ const { screen, } = require("@testing-library/dom"); const { userEvent } = require("@testing-library/user-event"); +const dayjs = require("dayjs"); const { configureAxe, toHaveNoViolations } = require("jest-axe"); expect.extend(toHaveNoViolations); @@ -23,19 +24,37 @@ const axe = configureAxe({ }, }); -const createComponent = () => { - const html = ` -
-
- -
- For example, 17/5/2024. +const kebabize = (str) => { + return str.replace( + /[A-Z]+(?![a-z])|[A-Z]/g, + ($, ofset) => (ofset ? "-" : "") + $.toLowerCase(), + ); +}; + +const configToDataAttributes = (config) => { + let attributes = ""; + for (let [key, value] of Object.entries(config)) { + attributes += `data-${kebabize(key)}="${value}" `; + } + return attributes; +}; + +const createComponent = (config = {}, html) => { + const dataAttributes = configToDataAttributes(config); + if (typeof html === "undefined") { + html = ` +
+
+ +
+ For example, 17/5/2024. +
+
- -
-
`; +
`; + } document.body.insertAdjacentHTML("afterbegin", html); component = document.querySelector('[data-module="moj-date-picker"]'); @@ -78,6 +97,32 @@ const getLastDayOfWeek = (dateObject, lastDayOfWeekIndex) => { return lastDayOfWeek; }; +const getDateInCurrentMonth = (excluding = []) => { + const today = dayjs().date(); + excluding.push(today); + const lastDayOfMonth = dayjs().endOf("month").date(); + const days = range(1, lastDayOfMonth).filter((x) => !excluding.includes(x)); + + return days[Math.floor(Math.random() * days.length)]; +}; + +const getDateRangeInCurrentMonth = (startDay, endDay) => { + let date = dayjs().date(startDay); // Convert the start date to a Day.js object + const endDate = dayjs().date(endDay+1) + const dates = []; + + while (date.isBefore(endDate)) { + dates.push(date); + date = date.add(1, "day"); + } + + return dates; +}; + +const range = (start, end) => { + return [...Array(end - start + 1).keys()].map((x) => x + start); +}; + describe("Date picker with defaults", () => { let component; let calendarButton; @@ -559,13 +604,221 @@ describe("Date picker with defaults", () => { expect(await axe(document.body)).toHaveNoViolations(); }); }); +}); + +describe("button menu JS API", () => { + let component; + + beforeEach(() => { + component = createComponent(); + }); - //test component API - JS and data-attribute - //open with date in input - //min date - //max date - //excluded dates - //excluded days - //leadingZeros - test home and end keys - //weekStartDay + afterEach(() => { + document.body.innerHTML = ""; + }); + + describe("config", () => { + test("default config values", () => { + const datePicker = new MOJFrontend.DatePicker(component, {}); + datePicker.init(); + + expect(datePicker.config).toStrictEqual({ + leadingZeros: false, + weekStartDay: "monday", + }); + }); + + test("leadingZeros", () => { + const config = { leadingZeros: true }; + const datePicker = new MOJFrontend.DatePicker(component, config); + datePicker.init(); + + expect(datePicker.config.leadingZeros).toBe(true); + }); + + test("weekStartDay can be set to sunday", () => { + const config = { weekStartDay: "Sunday" }; + const datePicker = new MOJFrontend.DatePicker(component, config); + datePicker.init(); + + expect(datePicker.config.weekStartDay).toBe("sunday"); + expect(datePicker.dayLabels[0]).toBe("Sunday"); + }); + + test("weekStartDay can't be set to other days", () => { + const config = { weekStartDay: "friday" }; + const datePicker = new MOJFrontend.DatePicker(component, config); + datePicker.init(); + + expect(datePicker.config.weekStartDay).toBe("monday"); + }); + + test("minDate", () => { + const minDate = dayjs().subtract("1", "week").startOf("day"); + const config = { minDate: minDate.format("D/M/YYYY") }; + const datePicker = new MOJFrontend.DatePicker(component, config); + datePicker.init(); + + expect(datePicker.minDate).toStrictEqual(minDate.toDate()); + }); + + test("future minDate sets currentDate to minDate", () => { + const minDate = dayjs().add("1", "week").startOf("day"); + const config = { minDate: minDate.format("D/M/YYYY") }; + const datePicker = new MOJFrontend.DatePicker(component, config); + datePicker.init(); + + expect(datePicker.minDate).toStrictEqual(minDate.toDate()); + expect(datePicker.currentDate).toStrictEqual(minDate.toDate()); + }); + + test("maxDate", () => { + const maxDate = dayjs().add("1", "week").startOf("day"); + const config = { maxDate: maxDate.format("D/M/YYYY") }; + const datePicker = new MOJFrontend.DatePicker(component, config); + datePicker.init(); + + expect(datePicker.maxDate).toStrictEqual(maxDate.toDate()); + }); + + test("past maxDate sets currentDate to maxDate", () => { + const maxDate = dayjs().subtract("1", "week").startOf("day"); + const config = { maxDate: maxDate.format("D/M/YYYY") }; + const datePicker = new MOJFrontend.DatePicker(component, config); + datePicker.init(); + + expect(datePicker.maxDate).toStrictEqual(maxDate.toDate()); + expect(datePicker.currentDate).toStrictEqual(maxDate.toDate()); + }); + + test("excludedDays", () => { + const config = { excludedDays: "sunday thursday" }; + const datePicker = new MOJFrontend.DatePicker(component, config); + datePicker.init(); + + expect(datePicker.excludedDays).toEqual([0, 4]); + }); + + describe("excludedDates", () => { + test("excluding a day", () => { + const dateToExclude = dayjs() + .date(getDateInCurrentMonth()) + .startOf("day"); + config = { excludedDates: dateToExclude.format("D/M/YYYY") }; + const datePicker = new MOJFrontend.DatePicker(component, config); + datePicker.init(); + + expect(datePicker.excludedDates).toStrictEqual([ + dateToExclude.toDate(), + ]); + }); + + test("excluding multiple dates", () => { + const firstDateToExclude = dayjs() + .date(getDateInCurrentMonth()) + .startOf("day"); + const secondDateToExclude = dayjs() + .date(getDateInCurrentMonth([firstDateToExclude.date()])) + .startOf("day"); + config = { + excludedDates: `${firstDateToExclude.format("D/M/YYYY")} ${secondDateToExclude.format("D/M/YYYY")}`, + }; + const datePicker = new MOJFrontend.DatePicker(component, config); + datePicker.init(); + + expect(datePicker.excludedDates.length).toEqual(2); + expect(datePicker.excludedDates).toStrictEqual([ + firstDateToExclude.toDate(), + secondDateToExclude.toDate(), + ]); + }); + + test("excluding a range of days", () => { + let datesToExclude; + if (dayjs().date() < 15) { + datesToExclude = getDateRangeInCurrentMonth(18, 20); + } else { + datesToExclude = getDateRangeInCurrentMonth(3,5) + } + datesToExclude = datesToExclude.map((date) => date.startOf("day")); + config = { + excludedDates: `${datesToExclude.at(0).format("D/M/YYYY")}-${datesToExclude.at(-1).format("D/M/YYYY")}` + }; + const datePicker = new MOJFrontend.DatePicker(component, config); + datePicker.init(); + + expect(datePicker.excludedDates.length).toEqual(3); + expect(datePicker.excludedDates).toStrictEqual( + datesToExclude.map((date => date.toDate())), + ); + }); + + test("excluding individual dates and a range of days", () => { + let datesToExclude; + if (dayjs().date() < 15) { + datesToExclude = getDateRangeInCurrentMonth(18, 20); + datesToExclude.push(dayjs().date(22)) + datesToExclude.push(dayjs().date(25)) + } else { + datesToExclude = getDateRangeInCurrentMonth(3,5) + datesToExclude.push(dayjs().date(7)) + datesToExclude.push(dayjs().date(11)) + } + datesToExclude = datesToExclude.map((date) => date.startOf("day")); + config = { + excludedDates: `${datesToExclude.at(0).format("D/M/YYYY")}-${datesToExclude.at(2).format("D/M/YYYY")} ${datesToExclude.at(3).format("D/M/YYYY")} ${datesToExclude.at(4).format("D/M/YYYY")} ` + }; + const datePicker = new MOJFrontend.DatePicker(component, config); + datePicker.init(); + + expect(datePicker.excludedDates.length).toEqual(5); + expect(datePicker.excludedDates).toStrictEqual( + datesToExclude.map((date => date.toDate())), + ); + }); + }); + }); + + describe("UI", () => { + let calendarButton + let input + + test("with leadingZeros false", async () => { + calendarButton = queryByText(component, "Choose date")?.closest("button"); + input = screen.getByLabelText("Date"); + + const config = { leadingZeros: false }; + new MOJFrontend.DatePicker(component, config).init(); + const dateToSelect = screen.queryByText( "9")?.closest("button"); + const selectedDate = dayjs().date(9) + + await user.click(calendarButton); + await user.click(dateToSelect) + + expect(input).toHaveValue(selectedDate.format("D/M/YYYY")); + }); + + test("with leadingZeros true", async () => { + calendarButton = queryByText(component, "Choose date")?.closest("button"); + input = screen.getByLabelText("Date"); + + const config = { leadingZeros: true }; + new MOJFrontend.DatePicker(component, config).init(); + const dateToSelect = screen.queryByText( "9")?.closest("button"); + const selectedDate = dayjs().date(9) + + await user.click(calendarButton); + await user.click(dateToSelect) + + expect(input).toHaveValue(selectedDate.format("DD/MM/YYYY")); + }); + }); }); +//test component API - JS and data-attribute +//open with date in input +//min date +//max date +//excluded dates +//excluded days +//leadingZeros - test home and end keys +//weekStartDay From bb45d737119b24cc1ee54cc8e088518cc98de890 Mon Sep 17 00:00:00 2001 From: Chris Pymm Date: Fri, 8 Nov 2024 10:08:13 +0000 Subject: [PATCH 3/5] test(date picker): wip - further tests --- .../components/date-picker/date-picker.spec.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/moj/components/date-picker/date-picker.spec.js b/src/moj/components/date-picker/date-picker.spec.js index 0730bcdd..3bab00e0 100644 --- a/src/moj/components/date-picker/date-picker.spec.js +++ b/src/moj/components/date-picker/date-picker.spec.js @@ -812,6 +812,23 @@ describe("button menu JS API", () => { expect(input).toHaveValue(selectedDate.format("DD/MM/YYYY")); }); + + test("minDate", async () => { + calendarButton = queryByText(component, "Choose date")?.closest("button") + + + const minDate = dayjs().date(2).startOf("day"); + const config = { minDate: minDate.format("D/M/YYYY") }; + new MOJFrontend.DatePicker(component, config).init(); + + await user.click(calendarButton); + + expect(component.querySelectorAll("button[aria-disabled]").length).toBe(2) + + + + +}) }); }); //test component API - JS and data-attribute From 81efe61e9952421d561e532dafc4df7c69a36fd8 Mon Sep 17 00:00:00 2001 From: Chris Pymm Date: Thu, 14 Nov 2024 14:29:04 +0000 Subject: [PATCH 4/5] test(date picker): complete js config tests --- src/moj/components/date-picker/date-picker.js | 5 + .../date-picker/date-picker.spec.js | 175 +++++++++++++++--- 2 files changed, 152 insertions(+), 28 deletions(-) diff --git a/src/moj/components/date-picker/date-picker.js b/src/moj/components/date-picker/date-picker.js index e0fa8466..9710df1e 100644 --- a/src/moj/components/date-picker/date-picker.js +++ b/src/moj/components/date-picker/date-picker.js @@ -435,10 +435,13 @@ Datepicker.prototype.setWeekStartDay = function () { * */ Datepicker.prototype.isExcludedDate = function (date) { + // This comparison does not work correctly - it will exclude the mindate itself + // see: https://github.com/ministryofjustice/moj-frontend/issues/923 if (this.minDate && this.minDate > date) { return true; } + // This comparison works as expected - the maxdate will not be excluded if (this.maxDate && this.maxDate < date) { return true; } @@ -878,6 +881,7 @@ DSCalendarDay.prototype.update = function (day, hidden, disabled) { } else { this.button.style.display = "block"; } + this.button.setAttribute("data-testid", this.picker.formattedDateFromDate(day)) this.button.innerHTML = `${accessibleLabel}`; this.date = new Date(day); @@ -891,6 +895,7 @@ DSCalendarDay.prototype.click = function (event) { event.preventDefault(); }; + DSCalendarDay.prototype.keyPress = function (event) { let calendarNavKey = true; diff --git a/src/moj/components/date-picker/date-picker.spec.js b/src/moj/components/date-picker/date-picker.spec.js index 3bab00e0..1370cfbf 100644 --- a/src/moj/components/date-picker/date-picker.spec.js +++ b/src/moj/components/date-picker/date-picker.spec.js @@ -2,6 +2,7 @@ * @jest-environment jsdom */ const { + getAllByRole, getByText, getByRole, queryByRole, @@ -108,7 +109,7 @@ const getDateInCurrentMonth = (excluding = []) => { const getDateRangeInCurrentMonth = (startDay, endDay) => { let date = dayjs().date(startDay); // Convert the start date to a Day.js object - const endDate = dayjs().date(endDay+1) + const endDate = dayjs().date(endDay + 1); const dates = []; while (date.isBefore(endDate)) { @@ -738,18 +739,18 @@ describe("button menu JS API", () => { if (dayjs().date() < 15) { datesToExclude = getDateRangeInCurrentMonth(18, 20); } else { - datesToExclude = getDateRangeInCurrentMonth(3,5) + datesToExclude = getDateRangeInCurrentMonth(3, 5); } datesToExclude = datesToExclude.map((date) => date.startOf("day")); config = { - excludedDates: `${datesToExclude.at(0).format("D/M/YYYY")}-${datesToExclude.at(-1).format("D/M/YYYY")}` + excludedDates: `${datesToExclude.at(0).format("D/M/YYYY")}-${datesToExclude.at(-1).format("D/M/YYYY")}`, }; const datePicker = new MOJFrontend.DatePicker(component, config); datePicker.init(); expect(datePicker.excludedDates.length).toEqual(3); expect(datePicker.excludedDates).toStrictEqual( - datesToExclude.map((date => date.toDate())), + datesToExclude.map((date) => date.toDate()), ); }); @@ -757,78 +758,196 @@ describe("button menu JS API", () => { let datesToExclude; if (dayjs().date() < 15) { datesToExclude = getDateRangeInCurrentMonth(18, 20); - datesToExclude.push(dayjs().date(22)) - datesToExclude.push(dayjs().date(25)) + datesToExclude.push(dayjs().date(22)); + datesToExclude.push(dayjs().date(25)); } else { - datesToExclude = getDateRangeInCurrentMonth(3,5) - datesToExclude.push(dayjs().date(7)) - datesToExclude.push(dayjs().date(11)) + datesToExclude = getDateRangeInCurrentMonth(3, 5); + datesToExclude.push(dayjs().date(7)); + datesToExclude.push(dayjs().date(11)); } datesToExclude = datesToExclude.map((date) => date.startOf("day")); config = { - excludedDates: `${datesToExclude.at(0).format("D/M/YYYY")}-${datesToExclude.at(2).format("D/M/YYYY")} ${datesToExclude.at(3).format("D/M/YYYY")} ${datesToExclude.at(4).format("D/M/YYYY")} ` + excludedDates: `${datesToExclude.at(0).format("D/M/YYYY")}-${datesToExclude.at(2).format("D/M/YYYY")} ${datesToExclude.at(3).format("D/M/YYYY")} ${datesToExclude.at(4).format("D/M/YYYY")} `, }; const datePicker = new MOJFrontend.DatePicker(component, config); datePicker.init(); expect(datePicker.excludedDates.length).toEqual(5); expect(datePicker.excludedDates).toStrictEqual( - datesToExclude.map((date => date.toDate())), + datesToExclude.map((date) => date.toDate()), ); }); }); }); describe("UI", () => { - let calendarButton - let input + let calendarButton; + let input; test("with leadingZeros false", async () => { - calendarButton = queryByText(component, "Choose date")?.closest("button"); input = screen.getByLabelText("Date"); const config = { leadingZeros: false }; new MOJFrontend.DatePicker(component, config).init(); - const dateToSelect = screen.queryByText( "9")?.closest("button"); - const selectedDate = dayjs().date(9) + calendarButton = screen.getByRole("button", { name: "Choose date" }); + const dateToSelect = screen.queryByText("9")?.closest("button"); + const selectedDate = dayjs().date(9); await user.click(calendarButton); - await user.click(dateToSelect) + await user.click(dateToSelect); expect(input).toHaveValue(selectedDate.format("D/M/YYYY")); }); test("with leadingZeros true", async () => { - calendarButton = queryByText(component, "Choose date")?.closest("button"); input = screen.getByLabelText("Date"); const config = { leadingZeros: true }; new MOJFrontend.DatePicker(component, config).init(); - const dateToSelect = screen.queryByText( "9")?.closest("button"); - const selectedDate = dayjs().date(9) + calendarButton = screen.getByRole("button", { name: "Choose date" }); + const dateToSelect = screen.queryByText("9")?.closest("button"); + const selectedDate = dayjs().date(9); await user.click(calendarButton); - await user.click(dateToSelect) + await user.click(dateToSelect); expect(input).toHaveValue(selectedDate.format("DD/MM/YYYY")); }); - test("minDate", async () => { - calendarButton = queryByText(component, "Choose date")?.closest("button") - + test.skip.failing("minDate", async () => { + const minDay = 3; + const lastDayinMonth = dayjs().endOf("month").date(); + const minDate = dayjs().date(minDay); + const config = { minDate: minDate.format("DD/MM/YYYY") }; - const minDate = dayjs().date(2).startOf("day"); - const config = { minDate: minDate.format("D/M/YYYY") }; new MOJFrontend.DatePicker(component, config).init(); + calendarButton = screen.getByRole("button", { name: "Choose date" }); + await user.click(calendarButton); + + for (let i = 1; i <= lastDayinMonth; i++) { + const testId = dayjs().date(i).startOf("day").format("D/M/YYYY"); + const dayButton = screen.getByTestId(testId); + + if (i <= minDay) { + expect(dayButton).toHaveAttribute("aria-disabled", "true"); + } else { + expect(dayButton).not.toHaveAttribute("aria-disabled"); + } + } + }); + + test("maxDate", async () => { + const maxDay = 21; + const lastDayinMonth = dayjs().endOf("month").date(); + const maxDate = dayjs().date(maxDay); + const config = { maxDate: maxDate.format("DD/MM/YYYY") }; + new MOJFrontend.DatePicker(component, config).init(); + calendarButton = screen.getByRole("button", { name: "Choose date" }); await user.click(calendarButton); - expect(component.querySelectorAll("button[aria-disabled]").length).toBe(2) + for (let i = 1; i <= lastDayinMonth; i++) { + const testId = dayjs().date(i).startOf("day").format("D/M/YYYY"); + const dayButton = screen.getByTestId(testId); + + if (i > maxDay) { + expect(dayButton).toHaveAttribute("aria-disabled", "true"); + } else { + expect(dayButton).not.toHaveAttribute("aria-disabled"); + } + } + }); + + describe("excludedDates", () => { + test("excluding a day", async () => { + const dateToExclude = dayjs() + .date(getDateInCurrentMonth()) + .startOf("day"); + const excludedDay = dateToExclude.date(); + const config = { excludedDates: dateToExclude.format("D/M/YYYY") }; + + const lastDayinMonth = dayjs().endOf("month").date(); + + new MOJFrontend.DatePicker(component, config).init(); + calendarButton = screen.getByRole("button", { name: "Choose date" }); + await user.click(calendarButton); + + for (let i = 1; i <= lastDayinMonth; i++) { + const testId = dayjs().date(i).startOf("day").format("D/M/YYYY"); + const dayButton = screen.getByTestId(testId); + + if (i == excludedDay) { + expect(dayButton).toHaveAttribute("aria-disabled", "true"); + } else { + expect(dayButton).not.toHaveAttribute("aria-disabled"); + } + } + }); + + test("excluding a range of days", async () => { + let datesToExclude; + if (dayjs().date() < 15) { + datesToExclude = getDateRangeInCurrentMonth(18, 20); + } else { + datesToExclude = getDateRangeInCurrentMonth(3, 5); + } + datesToExclude = datesToExclude.map((date) => date.startOf("day")); + let daysToExclude = datesToExclude.map((date) => date.date()); + const lastDayinMonth = dayjs().endOf("month").date(); + config = { + excludedDates: `${datesToExclude.at(0).format("D/M/YYYY")}-${datesToExclude.at(-1).format("D/M/YYYY")}`, + }; + datePicker = new MOJFrontend.DatePicker(component, config).init(); + calendarButton = screen.getByRole("button", { name: "Choose date" }); + await user.click(calendarButton); + for (let i = 1; i <= lastDayinMonth; i++) { + const testId = dayjs().date(i).startOf("day").format("D/M/YYYY"); + const dayButton = screen.getByTestId(testId); + if (daysToExclude.includes(i)) { + expect(dayButton).toHaveAttribute("aria-disabled", "true"); + } else { + expect(dayButton).not.toHaveAttribute("aria-disabled"); + } + } + }); + }); -}) + test("excludedDays", async () => { + const config = { excludedDays: "sunday" }; + const lastDayinMonth = dayjs().endOf("month").date(); + let excludedDays = []; + for (let i = 1; i <= lastDayinMonth; i++) { + if (dayjs().date(i).day() === 0) { + excludedDays.push(i); + } + } + new MOJFrontend.DatePicker(component, config).init(); + calendarButton = screen.getByRole("button", { name: "Choose date" }); + await user.click(calendarButton); + + for (let i = 1; i <= lastDayinMonth; i++) { + const testId = dayjs().date(i).startOf("day").format("D/M/YYYY"); + const dayButton = screen.getByTestId(testId); + + if (excludedDays.includes(i)) { + expect(dayButton).toHaveAttribute("aria-disabled", "true"); + } else { + expect(dayButton).not.toHaveAttribute("aria-disabled"); + } + } + }); + + test("weekStartDay", async () => { + new MOJFrontend.DatePicker(component, {}).init(); + calendarButton = screen.getByRole("button", { name: "Choose date" }); + await user.click(calendarButton); + const headers = getAllByRole(component, "columnheader"); + + expect(headers[0]).toHaveAccessibleName("Monday"); + }); }); }); //test component API - JS and data-attribute From 7068d790fe4cc38a92c0d1353692175e4c9af913 Mon Sep 17 00:00:00 2001 From: Chris Pymm Date: Mon, 18 Nov 2024 12:36:10 +0000 Subject: [PATCH 5/5] test(datepicker): add tests for data attribute api --- src/moj/components/date-picker/date-picker.js | 11 +- .../date-picker/date-picker.spec.js | 209 +++++++++++++++++- 2 files changed, 203 insertions(+), 17 deletions(-) diff --git a/src/moj/components/date-picker/date-picker.js b/src/moj/components/date-picker/date-picker.js index 9710df1e..5949c5fc 100644 --- a/src/moj/components/date-picker/date-picker.js +++ b/src/moj/components/date-picker/date-picker.js @@ -15,7 +15,7 @@ * @param {DatepickerConfig} config - config object * @constructor */ -function Datepicker($module, config) { +function Datepicker($module, config = {}) { if (!$module) { return this; } @@ -421,8 +421,7 @@ Datepicker.prototype.setWeekStartDay = function () { this.config.weekStartDay = "sunday"; // Rotate dayLabels array to put Sunday as the first item this.dayLabels.unshift(this.dayLabels.pop()); - } - else { + } else { this.config.weekStartDay = "monday"; } }; @@ -881,7 +880,10 @@ DSCalendarDay.prototype.update = function (day, hidden, disabled) { } else { this.button.style.display = "block"; } - this.button.setAttribute("data-testid", this.picker.formattedDateFromDate(day)) + this.button.setAttribute( + "data-testid", + this.picker.formattedDateFromDate(day), + ); this.button.innerHTML = `${accessibleLabel}`; this.date = new Date(day); @@ -895,7 +897,6 @@ DSCalendarDay.prototype.click = function (event) { event.preventDefault(); }; - DSCalendarDay.prototype.keyPress = function (event) { let calendarNavKey = true; diff --git a/src/moj/components/date-picker/date-picker.spec.js b/src/moj/components/date-picker/date-picker.spec.js index 1370cfbf..b06aa006 100644 --- a/src/moj/components/date-picker/date-picker.spec.js +++ b/src/moj/components/date-picker/date-picker.spec.js @@ -743,7 +743,7 @@ describe("button menu JS API", () => { } datesToExclude = datesToExclude.map((date) => date.startOf("day")); config = { - excludedDates: `${datesToExclude.at(0).format("D/M/YYYY")}-${datesToExclude.at(-1).format("D/M/YYYY")}`, + excludedDates: `${datesToExclude[0].format("D/M/YYYY")}-${datesToExclude[datesToExclude.length-1].format("D/M/YYYY")}`, }; const datePicker = new MOJFrontend.DatePicker(component, config); datePicker.init(); @@ -767,7 +767,7 @@ describe("button menu JS API", () => { } datesToExclude = datesToExclude.map((date) => date.startOf("day")); config = { - excludedDates: `${datesToExclude.at(0).format("D/M/YYYY")}-${datesToExclude.at(2).format("D/M/YYYY")} ${datesToExclude.at(3).format("D/M/YYYY")} ${datesToExclude.at(4).format("D/M/YYYY")} `, + excludedDates: `${datesToExclude[0].format("D/M/YYYY")}-${datesToExclude[2].format("D/M/YYYY")} ${datesToExclude[3].format("D/M/YYYY")} ${datesToExclude[4].format("D/M/YYYY")} `, }; const datePicker = new MOJFrontend.DatePicker(component, config); datePicker.init(); @@ -895,7 +895,7 @@ describe("button menu JS API", () => { let daysToExclude = datesToExclude.map((date) => date.date()); const lastDayinMonth = dayjs().endOf("month").date(); config = { - excludedDates: `${datesToExclude.at(0).format("D/M/YYYY")}-${datesToExclude.at(-1).format("D/M/YYYY")}`, + excludedDates: `${datesToExclude[0].format("D/M/YYYY")}-${datesToExclude[datesToExclude.length-1].format("D/M/YYYY")}`, }; datePicker = new MOJFrontend.DatePicker(component, config).init(); @@ -940,7 +940,7 @@ describe("button menu JS API", () => { } }); - test("weekStartDay", async () => { + test("default weekStartDay", async () => { new MOJFrontend.DatePicker(component, {}).init(); calendarButton = screen.getByRole("button", { name: "Choose date" }); await user.click(calendarButton); @@ -948,13 +948,198 @@ describe("button menu JS API", () => { expect(headers[0]).toHaveAccessibleName("Monday"); }); + + test("weekStartDay Sunday", async () => { + new MOJFrontend.DatePicker(component, { weekStartDay: "sunday" }).init(); + calendarButton = screen.getByRole("button", { name: "Choose date" }); + await user.click(calendarButton); + const headers = getAllByRole(component, "columnheader"); + + expect(headers[0]).toHaveAccessibleName("Sunday"); + }); + }); +}); + +describe("Datepicker data-attributes API", () => { + let component; + let calendarButton; + let input; + + beforeEach(() => {}); + + afterEach(() => { + document.body.innerHTML = ""; + }); + + test("with leadingZeros false", async () => { + component = createComponent({ leadingZeros: "false" }); + new MOJFrontend.DatePicker(component).init(); + + input = screen.getByLabelText("Date"); + calendarButton = screen.getByRole("button", { name: "Choose date" }); + const dateToSelect = screen.queryByText("9")?.closest("button"); + const selectedDate = dayjs().date(9); + + await user.click(calendarButton); + await user.click(dateToSelect); + + expect(input).toHaveValue(selectedDate.format("D/M/YYYY")); + }); + + test("with leadingZeros true", async () => { + const component = createComponent({ leadingZeros: "true" }); + new MOJFrontend.DatePicker(component).init(); + + input = screen.getByLabelText("Date"); + calendarButton = screen.getByRole("button", { name: "Choose date" }); + const dateToSelect = screen.queryByText("9")?.closest("button"); + const selectedDate = dayjs().date(9); + + await user.click(calendarButton); + await user.click(dateToSelect); + + expect(input).toHaveValue(selectedDate.format("DD/MM/YYYY")); + }); + + test.skip.failing("minDate", async () => { + const minDay = 3; + const lastDayinMonth = dayjs().endOf("month").date(); + const minDate = dayjs().date(minDay); + const component = createComponent({ + minDate: minDate.format("DD/MM/YYYY"), + }); + new MOJFrontend.DatePicker(component).init(); + calendarButton = screen.getByRole("button", { name: "Choose date" }); + + await user.click(calendarButton); + + for (let i = 1; i <= lastDayinMonth; i++) { + const testId = dayjs().date(i).startOf("day").format("D/M/YYYY"); + const dayButton = screen.getByTestId(testId); + + if (i <= minDay) { + expect(dayButton).toHaveAttribute("aria-disabled", "true"); + } else { + expect(dayButton).not.toHaveAttribute("aria-disabled"); + } + } + }); + + test("maxDate", async () => { + const maxDay = 21; + const lastDayinMonth = dayjs().endOf("month").date(); + const maxDate = dayjs().date(maxDay); + const component = createComponent({ + maxDate: maxDate.format("DD/MM/YYYY"), + }); + new MOJFrontend.DatePicker(component).init(); + calendarButton = screen.getByRole("button", { name: "Choose date" }); + + await user.click(calendarButton); + + for (let i = 1; i <= lastDayinMonth; i++) { + const testId = dayjs().date(i).startOf("day").format("D/M/YYYY"); + const dayButton = screen.getByTestId(testId); + + if (i > maxDay) { + expect(dayButton).toHaveAttribute("aria-disabled", "true"); + } else { + expect(dayButton).not.toHaveAttribute("aria-disabled"); + } + } + }); + + describe("excludedDates", () => { + test("excluding a day", async () => { + const dateToExclude = dayjs() + .date(getDateInCurrentMonth()) + .startOf("day"); + const excludedDay = dateToExclude.date(); + const lastDayinMonth = dayjs().endOf("month").date(); + const component = createComponent({ + excludedDates: dateToExclude.format("D/M/YYYY"), + }); + new MOJFrontend.DatePicker(component).init(); + calendarButton = screen.getByRole("button", { name: "Choose date" }); + + await user.click(calendarButton); + + for (let i = 1; i <= lastDayinMonth; i++) { + const testId = dayjs().date(i).startOf("day").format("D/M/YYYY"); + const dayButton = screen.getByTestId(testId); + + if (i == excludedDay) { + expect(dayButton).toHaveAttribute("aria-disabled", "true"); + } else { + expect(dayButton).not.toHaveAttribute("aria-disabled"); + } + } + }); + + test("excluding a range of days", async () => { + let datesToExclude; + if (dayjs().date() < 15) { + datesToExclude = getDateRangeInCurrentMonth(18, 20); + } else { + datesToExclude = getDateRangeInCurrentMonth(3, 5); + } + datesToExclude = datesToExclude.map((date) => date.startOf("day")); + let daysToExclude = datesToExclude.map((date) => date.date()); + const lastDayinMonth = dayjs().endOf("month").date(); + component = createComponent({ + excludedDates: `${datesToExclude[0].format("D/M/YYYY")}-${datesToExclude[datesToExclude.length-1].format("D/M/YYYY")}`, + }); + datePicker = new MOJFrontend.DatePicker(component).init(); + calendarButton = screen.getByRole("button", { name: "Choose date" }); + + await user.click(calendarButton); + + for (let i = 1; i <= lastDayinMonth; i++) { + const testId = dayjs().date(i).startOf("day").format("D/M/YYYY"); + const dayButton = screen.getByTestId(testId); + + if (daysToExclude.includes(i)) { + expect(dayButton).toHaveAttribute("aria-disabled", "true"); + } else { + expect(dayButton).not.toHaveAttribute("aria-disabled"); + } + } + }); + }); + + test("excludedDays", async () => { + const component = createComponent({ excludedDays: "sunday" }); + const lastDayinMonth = dayjs().endOf("month").date(); + let excludedDays = []; + for (let i = 1; i <= lastDayinMonth; i++) { + if (dayjs().date(i).day() === 0) { + excludedDays.push(i); + } + } + new MOJFrontend.DatePicker(component).init(); + calendarButton = screen.getByRole("button", { name: "Choose date" }); + + await user.click(calendarButton); + + for (let i = 1; i <= lastDayinMonth; i++) { + const testId = dayjs().date(i).startOf("day").format("D/M/YYYY"); + const dayButton = screen.getByTestId(testId); + + if (excludedDays.includes(i)) { + expect(dayButton).toHaveAttribute("aria-disabled", "true"); + } else { + expect(dayButton).not.toHaveAttribute("aria-disabled"); + } + } + }); + + test("weekStartDay", async () => { + component = createComponent({ weekStartDay: "sunday" }); + new MOJFrontend.DatePicker(component).init(); + calendarButton = screen.getByRole("button", { name: "Choose date" }); + await user.click(calendarButton); + const headers = getAllByRole(component, "columnheader"); + + expect(headers[0]).toHaveAccessibleName("Sunday"); }); }); -//test component API - JS and data-attribute -//open with date in input -//min date -//max date -//excluded dates -//excluded days -//leadingZeros - test home and end keys -//weekStartDay