Bugzilla – Attachment 183207 Details for
Bug 39916
The 'Place booking' modal should have cypress tests
Home
|
New
|
Browse
|
Search
|
[?]
|
Reports
|
Help
|
New Account
|
Log In
[x]
|
Forgot Password
Login:
[x]
[patch]
Bug 39916: Add flatpickr helpers to cypress
Bug-39916-Add-flatpickr-helpers-to-cypress.patch (text/plain), 20.91 KB, created by
Martin Renvoize (ashimema)
on 2025-06-12 14:51:45 UTC
(
hide
)
Description:
Bug 39916: Add flatpickr helpers to cypress
Filename:
MIME Type:
Creator:
Martin Renvoize (ashimema)
Created:
2025-06-12 14:51:45 UTC
Size:
20.91 KB
patch
obsolete
>From 4e78a1f83b1969b57c843c9cdbefa648ede2f67c Mon Sep 17 00:00:00 2001 >From: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Date: Mon, 19 May 2025 13:29:49 +0100 >Subject: [PATCH] Bug 39916: Add flatpickr helpers to cypress > >This adds helper methods for cypress to work with flatpickr date pickers: >- openFlatpickr() - open the calendar >- selectFlatpickrDate() - select a single date >- selectFlatpickrDateRange() - select date range >- clearFlatpickr() - clear the selected dates >- getFlatpickrDate() - get specific date element >- navigateToFlatpickrMonth() - navigate calendar > >These helpers simplify date picker testing when using flatpickr. >--- > t/cypress/support/e2e.js | 1 + > t/cypress/support/flatpickr.js | 565 +++++++++++++++++++++++++++++++++ > 2 files changed, 566 insertions(+) > create mode 100644 t/cypress/support/flatpickr.js > >diff --git a/t/cypress/support/e2e.js b/t/cypress/support/e2e.js >index 07f8e58e4fd..8722aa5c65a 100644 >--- a/t/cypress/support/e2e.js >+++ b/t/cypress/support/e2e.js >@@ -26,6 +26,7 @@ > > // Import Select2 helpers > import "./select2"; >+import "./flatpickr.js"; > > function get_fallback_login_value(param) { > var env_var = param == "username" ? "KOHA_USER" : "KOHA_PASS"; >diff --git a/t/cypress/support/flatpickr.js b/t/cypress/support/flatpickr.js >new file mode 100644 >index 00000000000..7a313d5cc8c >--- /dev/null >+++ b/t/cypress/support/flatpickr.js >@@ -0,0 +1,565 @@ >+// flatpickrHelpers.js - Enhanced Reusable Cypress functions for Flatpickr date pickers >+ >+/** >+ * Enhanced helper functions for interacting with Flatpickr date picker components in Cypress tests >+ * Uses click-driven interactions with improved reliability and native retry mechanisms >+ * Supports all standard Flatpickr operations including date selection, range selection, >+ * navigation, hover interactions, and assertions. >+ * >+ * CHAINABILITY: >+ * All Flatpickr helper commands are fully chainable. You can: >+ * - Chain multiple Flatpickr operations (open, navigate, select) >+ * - Chain Flatpickr commands with standard Cypress commands >+ * - Split complex interactions into multiple steps for better reliability >+ * >+ * Examples: >+ * cy.get('#myDatepicker') >+ * .openFlatpickr() >+ * .selectFlatpickrDate('2023-05-15'); >+ * >+ * cy.get('#rangePicker') >+ * .openFlatpickr() >+ * .selectFlatpickrDateRange('2023-06-01', '2023-06-15') >+ * .should('have.value', '2023-06-01 to 2023-06-15'); >+ */ >+ >+// --- Internal Utility Functions --- >+ >+const monthNames = [ >+ "January", >+ "February", >+ "March", >+ "April", >+ "May", >+ "June", >+ "July", >+ "August", >+ "September", >+ "October", >+ "November", >+ "December", >+]; >+ >+/** >+ * Generates a Cypress selector for a specific day element within the Flatpickr calendar >+ * based on its aria-label. This is a low-level internal helper. >+ */ >+const _getFlatpickrDateSelector = date => { >+ const month = monthNames[date.getMonth()]; >+ const day = date.getDate(); >+ const year = date.getFullYear(); >+ const formattedLabel = `${month} ${day}, ${year}`; >+ return `.flatpickr-day[aria-label="${formattedLabel}"]`; >+}; >+ >+/** >+ * Ensures the Flatpickr calendar is open. If not, it clicks the input to open it. >+ * Uses Cypress's built-in retry mechanism for reliability. >+ */ >+const ensureCalendarIsOpen = ($el, timeout = 10000) => { >+ return $el.then($input => { >+ const inputToClick = $input.is(":visible") >+ ? $input >+ : $input.parents().find(".flatpickr-input:visible").first(); >+ >+ if (!inputToClick.length) { >+ throw new Error( >+ `Flatpickr: Could not find visible input element for selector '${$input.selector}' to open calendar.` >+ ); >+ } >+ >+ // Use Cypress's retry mechanism to check if calendar is already open >+ return cy.get("body").then(() => { >+ return cy.get(".flatpickr-calendar").then($calendar => { >+ const isVisible = >+ $calendar.length > 0 && >+ $calendar.hasClass("open") && >+ $calendar.is(":visible"); >+ >+ if (!isVisible) { >+ cy.wrap(inputToClick).click(); >+ } >+ >+ // Wait for calendar to be open and visible with retry >+ return cy >+ .get(".flatpickr-calendar.open", { timeout }) >+ .should("be.visible") >+ .then(() => cy.wrap($input)); >+ }); >+ }); >+ }); >+}; >+ >+/** >+ * Ensures the specified date is visible in the current calendar view. >+ * Navigates to the correct month/year if necessary. >+ */ >+const ensureDateIsVisible = (targetDate, $input, timeout = 10000) => { >+ const targetYear = targetDate.getFullYear(); >+ const targetMonth = targetDate.getMonth(); >+ >+ return cy >+ .get(".flatpickr-calendar.open", { timeout }) >+ .should("be.visible") >+ .then($calendar => { >+ const fpInstance = $input[0]._flatpickr; >+ if (!fpInstance) { >+ throw new Error( >+ `Flatpickr: Cannot find flatpickr instance on element. Make sure it's initialized with flatpickr.` >+ ); >+ } >+ const currentMonth = fpInstance.currentMonth; >+ const currentYear = fpInstance.currentYear; >+ >+ // Check if we need to navigate >+ if (currentMonth !== targetMonth || currentYear !== targetYear) { >+ return navigateToMonthAndYear(targetDate, $input, timeout); >+ } >+ >+ // Already in correct month/year, just verify the date is visible >+ const selector = _getFlatpickrDateSelector(targetDate); >+ return cy.get(selector, { timeout: 5000 }).should("be.visible"); >+ }); >+}; >+ >+/** >+ * Navigates the Flatpickr calendar to the target month and year. >+ * Uses native retry mechanisms and verifies target date is in view. >+ */ >+const navigateToMonthAndYear = (targetDate, $input, timeout = 10000) => { >+ const targetYear = targetDate.getFullYear(); >+ const targetMonth = targetDate.getMonth(); >+ >+ return cy >+ .get(".flatpickr-calendar.open", { timeout }) >+ .should("be.visible") >+ .then($calendar => { >+ const fpInstance = $input[0]._flatpickr; >+ if (!fpInstance) { >+ throw new Error( >+ `Flatpickr: Cannot find flatpickr instance on element. Make sure it's initialized with flatpickr.` >+ ); >+ } >+ const currentMonth = fpInstance.currentMonth; >+ const currentYear = fpInstance.currentYear; >+ >+ const monthDiff = >+ (targetYear - currentYear) * 12 + (targetMonth - currentMonth); >+ >+ if (monthDiff === 0) { >+ // Already in correct month, verify target date is visible >+ const selector = _getFlatpickrDateSelector(targetDate); >+ return cy.get(selector, { timeout: 5000 }).should("be.visible"); >+ } >+ >+ // Use flatpickr's changeMonth method for faster navigation >+ fpInstance.changeMonth(monthDiff, true); >+ >+ // Verify navigation succeeded by checking target date is now visible >+ const selector = _getFlatpickrDateSelector(targetDate); >+ return cy >+ .get(selector, { timeout: 5000 }) >+ .should("be.visible") >+ .should($el => { >+ // Ensure the element is actually the date we want >+ expect($el).to.have.length(1); >+ expect($el.attr("aria-label")).to.contain( >+ targetDate.getDate().toString() >+ ); >+ }); >+ }); >+}; >+ >+// --- User-Facing Helper Commands --- >+ >+/** >+ * Helper to open a Flatpickr calendar. >+ */ >+Cypress.Commands.add( >+ "openFlatpickr", >+ { prevSubject: "optional" }, >+ (subject, selector, timeout = 10000) => { >+ const $el = subject ? cy.wrap(subject) : cy.get(selector); >+ return ensureCalendarIsOpen($el, timeout); >+ } >+); >+ >+/** >+ * Helper to close an open Flatpickr calendar. >+ */ >+Cypress.Commands.add( >+ "closeFlatpickr", >+ { prevSubject: "optional" }, >+ (subject, selector) => { >+ const chain = subject >+ ? cy.wrap(subject) >+ : selector >+ ? cy.get(selector) >+ : cy.get("body"); >+ >+ return chain.then($el => { >+ // Click outside to close, using body coordinates that should always be safe >+ cy.get("body").click(0, 0); >+ >+ // Wait for calendar to close with retry mechanism >+ cy.get(".flatpickr-calendar.open").should("not.exist", { >+ timeout: 5000, >+ }); >+ >+ return cy.wrap($el); >+ }); >+ } >+); >+ >+/** >+ * Helper to navigate to a specific month and year in a Flatpickr calendar. >+ */ >+Cypress.Commands.add( >+ "navigateToFlatpickrMonth", >+ { prevSubject: true }, >+ (subject, targetDate, timeout = 10000) => { >+ return ensureCalendarIsOpen(cy.wrap(subject), timeout).then($input => { >+ const dateObj = >+ typeof targetDate === "string" >+ ? new Date(targetDate) >+ : targetDate; >+ return navigateToMonthAndYear(dateObj, $input, timeout).then(() => >+ cy.wrap($input) >+ ); >+ }); >+ } >+); >+ >+/** >+ * Helper to get the Flatpickr mode ('single', 'range', 'multiple'). >+ */ >+Cypress.Commands.add("getFlatpickrMode", { prevSubject: true }, subject => { >+ return cy.wrap(subject).then($input => { >+ const fpInstance = $input[0]._flatpickr; >+ if (!fpInstance) { >+ throw new Error( >+ `Flatpickr: Cannot find flatpickr instance on element ${$input.selector}. Make sure it's initialized with flatpickr.` >+ ); >+ } >+ return fpInstance.config.mode; >+ }); >+}); >+ >+/** >+ * Helper to select a specific date in a Flatpickr. >+ */ >+Cypress.Commands.add( >+ "selectFlatpickrDate", >+ { prevSubject: true }, >+ (subject, date, timeout = 10000) => { >+ return ensureCalendarIsOpen(cy.wrap(subject), timeout).then($input => { >+ const dateObj = typeof date === "string" ? new Date(date) : date; >+ >+ return ensureDateIsVisible(dateObj, $input, timeout).then(() => { >+ // Click the date - break chain to avoid DOM detachment >+ cy.get(_getFlatpickrDateSelector(dateObj)) >+ .should("be.visible") >+ .click(); >+ >+ // Re-query and validate selection based on mode >+ return cy >+ .wrap($input) >+ .getFlatpickrMode() >+ .then(mode => { >+ if (mode === "single") { >+ const expectedDate = `${dateObj.getFullYear()}-${(dateObj.getMonth() + 1).toString().padStart(2, "0")}-${dateObj.getDate().toString().padStart(2, "0")}`; >+ >+ cy.wrap($input).should("have.value", expectedDate); >+ cy.get(".flatpickr-calendar.open").should( >+ "not.exist", >+ { timeout: 5000 } >+ ); >+ return cy.wrap($input); >+ } else if (mode === "range") { >+ // In range mode, first selection keeps calendar open - re-query element >+ // Add small wait to allow for complex date recalculations (e.g., booking availability) >+ cy.wait(200); >+ cy.get(_getFlatpickrDateSelector(dateObj)).should( >+ "have.class", >+ "selected" >+ ); >+ cy.get(".flatpickr-calendar.open").should( >+ "be.visible" >+ ); >+ return cy.wrap($input); >+ } >+ >+ return cy.wrap($input); >+ }); >+ }); >+ }); >+ } >+); >+ >+/** >+ * Helper to select a date range in a Flatpickr range picker. >+ */ >+Cypress.Commands.add( >+ "selectFlatpickrDateRange", >+ { prevSubject: true }, >+ (subject, startDate, endDate, timeout = 10000) => { >+ return ensureCalendarIsOpen(cy.wrap(subject), timeout).then($input => { >+ const startDateObj = >+ typeof startDate === "string" ? new Date(startDate) : startDate; >+ const endDateObj = >+ typeof endDate === "string" ? new Date(endDate) : endDate; >+ >+ // Validate range mode first >+ return cy >+ .wrap($input) >+ .getFlatpickrMode() >+ .then(mode => { >+ if (mode !== "range") { >+ throw new Error( >+ `Flatpickr: This flatpickr instance is not in range mode. Current mode: ${mode}. Cannot select range.` >+ ); >+ } >+ >+ // Select start date - break chain to avoid DOM detachment >+ return ensureDateIsVisible( >+ startDateObj, >+ $input, >+ timeout >+ ).then(() => { >+ cy.get(_getFlatpickrDateSelector(startDateObj)) >+ .should("be.visible") >+ .click(); >+ >+ // Add small wait to allow for complex date recalculations (e.g., booking availability) >+ cy.wait(200); >+ // Re-query element after DOM update >+ cy.get(_getFlatpickrDateSelector(startDateObj)) >+ .should("have.class", "selected") >+ .and("have.class", "startRange"); >+ >+ // Ensure calendar stays open >+ cy.get(".flatpickr-calendar.open").should("be.visible"); >+ >+ // Navigate to end date and select it >+ return ensureDateIsVisible( >+ endDateObj, >+ $input, >+ timeout >+ ).then(() => { >+ cy.get(_getFlatpickrDateSelector(endDateObj)) >+ .should("be.visible") >+ .click(); >+ >+ // Validate final range selection >+ const expectedRange = `${startDateObj.getFullYear()}-${(startDateObj.getMonth() + 1).toString().padStart(2, "0")}-${startDateObj.getDate().toString().padStart(2, "0")} to ${endDateObj.getFullYear()}-${(endDateObj.getMonth() + 1).toString().padStart(2, "0")}-${endDateObj.getDate().toString().padStart(2, "0")}`; >+ >+ cy.wrap($input).should("have.value", expectedRange); >+ cy.get(".flatpickr-calendar.open").should( >+ "not.exist", >+ { timeout: 5000 } >+ ); >+ >+ return cy.wrap($input); >+ }); >+ }); >+ }); >+ }); >+ } >+); >+ >+/** >+ * Helper to hover over a specific date in a Flatpickr calendar. >+ */ >+Cypress.Commands.add( >+ "hoverFlatpickrDate", >+ { prevSubject: true }, >+ (subject, date, timeout = 10000) => { >+ return ensureCalendarIsOpen(cy.wrap(subject), timeout).then($input => { >+ const dateObj = typeof date === "string" ? new Date(date) : date; >+ >+ return ensureDateIsVisible(dateObj, $input, timeout).then(() => { >+ cy.get(_getFlatpickrDateSelector(dateObj)) >+ .should("be.visible") >+ .trigger("mouseover"); >+ >+ return cy.wrap($input); >+ }); >+ }); >+ } >+); >+ >+// --- Enhanced Assertion Commands --- >+ >+/** >+ * Helper to get a specific Flatpickr day element by its date. >+ */ >+Cypress.Commands.add( >+ "getFlatpickrDate", >+ { prevSubject: true }, >+ (subject, date, timeout = 10000) => { >+ const dateObj = typeof date === "string" ? new Date(date) : date; >+ >+ if (!(dateObj instanceof Date && !isNaN(dateObj.getTime()))) { >+ throw new Error( >+ `getFlatpickrDate: Invalid date provided. Received: ${date}` >+ ); >+ } >+ >+ return ensureCalendarIsOpen(cy.wrap(subject), timeout).then($input => { >+ return ensureDateIsVisible(dateObj, $input, timeout).then(() => { >+ // Instead of returning the element directly, return a function that re-queries >+ // This ensures subsequent .should() calls get a fresh element reference >+ const selector = _getFlatpickrDateSelector(dateObj); >+ return cy >+ .get(selector, { timeout: 5000 }) >+ .should("be.visible") >+ .should($el => { >+ // Ensure the element is actually the date we want >+ expect($el).to.have.length(1); >+ expect($el.attr("aria-label")).to.contain( >+ dateObj.getDate().toString() >+ ); >+ }); >+ }); >+ }); >+ } >+); >+ >+/** >+ * Assertion helper to check if a Flatpickr date is disabled. >+ */ >+Cypress.Commands.add( >+ "flatpickrDateShouldBeDisabled", >+ { prevSubject: true }, >+ (subject, date) => { >+ return cy >+ .wrap(subject) >+ .getFlatpickrDate(date) >+ .should("have.class", "flatpickr-disabled") >+ .then(() => cy.wrap(subject)); >+ } >+); >+ >+/** >+ * Assertion helper to check if a Flatpickr date is enabled. >+ */ >+Cypress.Commands.add( >+ "flatpickrDateShouldBeEnabled", >+ { prevSubject: true }, >+ (subject, date) => { >+ return cy >+ .wrap(subject) >+ .getFlatpickrDate(date) >+ .should("not.have.class", "flatpickr-disabled") >+ .then(() => cy.wrap(subject)); >+ } >+); >+ >+/** >+ * Assertion helper to check if a Flatpickr date is selected. >+ */ >+Cypress.Commands.add( >+ "flatpickrDateShouldBeSelected", >+ { prevSubject: true }, >+ (subject, date) => { >+ return cy >+ .wrap(subject) >+ .getFlatpickrDate(date) >+ .should("have.class", "selected") >+ .then(() => cy.wrap(subject)); >+ } >+); >+ >+/** >+ * Assertion helper to check if a Flatpickr date is not selected. >+ */ >+Cypress.Commands.add( >+ "flatpickrDateShouldNotBeSelected", >+ { prevSubject: true }, >+ (subject, date) => { >+ return cy >+ .wrap(subject) >+ .getFlatpickrDate(date) >+ .should("not.have.class", "selected") >+ .then(() => cy.wrap(subject)); >+ } >+); >+ >+/** >+ * Assertion helper to check if a Flatpickr date is today. >+ */ >+Cypress.Commands.add( >+ "flatpickrDateShouldBeToday", >+ { prevSubject: true }, >+ (subject, date) => { >+ return cy >+ .wrap(subject) >+ .getFlatpickrDate(date) >+ .should("have.class", "today") >+ .then(() => cy.wrap(subject)); >+ } >+); >+ >+/** >+ * Assertion helper to check if a Flatpickr date is in a range (has inRange class). >+ */ >+Cypress.Commands.add( >+ "flatpickrDateShouldBeInRange", >+ { prevSubject: true }, >+ (subject, date) => { >+ return cy >+ .wrap(subject) >+ .getFlatpickrDate(date) >+ .should("have.class", "inRange") >+ .then(() => cy.wrap(subject)); >+ } >+); >+ >+/** >+ * Assertion helper to check if a Flatpickr date is the start of a range. >+ */ >+Cypress.Commands.add( >+ "flatpickrDateShouldBeRangeStart", >+ { prevSubject: true }, >+ (subject, date) => { >+ return cy >+ .wrap(subject) >+ .getFlatpickrDate(date) >+ .should("have.class", "startRange") >+ .then(() => cy.wrap(subject)); >+ } >+); >+ >+/** >+ * Assertion helper to check if a Flatpickr date is the end of a range. >+ */ >+Cypress.Commands.add( >+ "flatpickrDateShouldBeRangeEnd", >+ { prevSubject: true }, >+ (subject, date) => { >+ return cy >+ .wrap(subject) >+ .getFlatpickrDate(date) >+ .should("have.class", "endRange") >+ .then(() => cy.wrap(subject)); >+ } >+); >+ >+/** >+ * Helper to clear a Flatpickr input by setting its value to empty. >+ * Works with hidden inputs by using the Flatpickr API directly. >+ */ >+Cypress.Commands.add("clearFlatpickr", { prevSubject: true }, subject => { >+ return cy.wrap(subject).then($input => { >+ // Wait for flatpickr to be initialized and then clear it >+ return cy >+ .wrap($input) >+ .should($el => { >+ expect($el[0]).to.have.property("_flatpickr"); >+ }) >+ .then(() => { >+ $input[0]._flatpickr.clear(); >+ return cy.wrap(subject); >+ }); >+ }); >+}); >-- >2.49.0
You cannot view the attachment while viewing its details because your browser does not support IFRAMEs.
View the attachment on a separate page
.
View Attachment As Diff
View Attachment As Raw
Actions:
View
|
Diff
|
Splinter Review
Attachments on
bug 39916
:
182515
|
182516
|
182545
|
182546
|
182635
|
182636
|
182637
|
182771
|
182772
|
182773
|
183206
| 183207 |
183208
|
183209
|
183210