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 - getFlapickrSelectedDates() - get flatpickr selected dates array - navigateToFlatpickrMonth() - navigate calendar These helpers simplify date picker testing when using flatpickr. Signed-off-by: Kristi Krueger <kkrueger@cuyahogalibrary.org> Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com> Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de> Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
This commit is contained in:
parent
de4badb166
commit
7c4f5eeb56
|
|
@ -26,6 +26,7 @@
|
|||
|
||||
// Import Select2 helpers
|
||||
import "./select2";
|
||||
import "./flatpickr.js";
|
||||
|
||||
// Error on JS warnings
|
||||
function safeToString(arg) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,593 @@
|
|||
// flatpickrHelpers.js - Enhanced Reusable Cypress functions for Flatpickr date pickers
|
||||
|
||||
// Import dayjs for date handling
|
||||
const dayjs = require("dayjs");
|
||||
|
||||
// Note: Using browser's natural timezone to match flatpickr behavior
|
||||
// The modal handles timezone conversions between API (UTC) and flatpickr (local)
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @param {dayjs.Dayjs|string|Date} date - The date to generate selector for
|
||||
*/
|
||||
const _getFlatpickrDateSelector = date => {
|
||||
const dayjsDate = dayjs(date);
|
||||
const month = monthNames[dayjsDate.month()];
|
||||
const day = dayjsDate.date();
|
||||
const year = dayjsDate.year();
|
||||
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.
|
||||
* @param {dayjs.Dayjs|string|Date} targetDate - The target date
|
||||
*/
|
||||
const ensureDateIsVisible = (targetDate, $input, timeout = 10000) => {
|
||||
const dayjsDate = dayjs(targetDate);
|
||||
const targetYear = dayjsDate.year();
|
||||
const targetMonth = dayjsDate.month();
|
||||
|
||||
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(dayjsDate, $input, timeout);
|
||||
}
|
||||
|
||||
// Already in correct month/year, just verify the date is visible
|
||||
const selector = _getFlatpickrDateSelector(dayjsDate);
|
||||
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.
|
||||
* @param {dayjs.Dayjs|string|Date} targetDate - The target date
|
||||
*/
|
||||
const navigateToMonthAndYear = (targetDate, $input, timeout = 10000) => {
|
||||
const dayjsDate = dayjs(targetDate);
|
||||
const targetYear = dayjsDate.year();
|
||||
const targetMonth = dayjsDate.month();
|
||||
|
||||
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(dayjsDate);
|
||||
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(dayjsDate);
|
||||
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(
|
||||
dayjsDate.date().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: true }, subject => {
|
||||
return cy.wrap(subject).then($input => {
|
||||
// Wait for flatpickr to be initialized and then close it
|
||||
return cy
|
||||
.wrap($input)
|
||||
.should($el => {
|
||||
expect($el[0]).to.have.property("_flatpickr");
|
||||
})
|
||||
.then(() => {
|
||||
$input[0]._flatpickr.close();
|
||||
return cy.wrap(subject);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 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 dayjsDate = dayjs(targetDate);
|
||||
return navigateToMonthAndYear(dayjsDate, $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 dayjsDate = dayjs(date);
|
||||
|
||||
return ensureDateIsVisible(dayjsDate, $input, timeout).then(() => {
|
||||
// Click the date - break chain to avoid DOM detachment
|
||||
cy.get(_getFlatpickrDateSelector(dayjsDate))
|
||||
.should("be.visible")
|
||||
.click();
|
||||
|
||||
// Re-query and validate selection based on mode
|
||||
return cy
|
||||
.wrap($input)
|
||||
.getFlatpickrMode()
|
||||
.then(mode => {
|
||||
if (mode === "single") {
|
||||
const expectedDate = dayjsDate.format("YYYY-MM-DD");
|
||||
|
||||
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
|
||||
// Wait for complex date recalculations (e.g., booking availability) to complete
|
||||
cy.get(_getFlatpickrDateSelector(dayjsDate)).should(
|
||||
$el => {
|
||||
expect($el).to.have.class("selected");
|
||||
},
|
||||
{ timeout: 5000 }
|
||||
);
|
||||
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 startDayjsDate = dayjs(startDate);
|
||||
const endDayjsDate = dayjs(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(
|
||||
startDayjsDate,
|
||||
$input,
|
||||
timeout
|
||||
).then(() => {
|
||||
cy.get(_getFlatpickrDateSelector(startDayjsDate))
|
||||
.should("be.visible")
|
||||
.click();
|
||||
|
||||
// Wait for complex date recalculations (e.g., booking availability) to complete
|
||||
cy.get(
|
||||
_getFlatpickrDateSelector(startDayjsDate)
|
||||
).should(
|
||||
$el => {
|
||||
expect($el).to.have.class("selected");
|
||||
expect($el).to.have.class("startRange");
|
||||
},
|
||||
{ timeout: 5000 }
|
||||
);
|
||||
|
||||
// Ensure calendar stays open
|
||||
cy.get(".flatpickr-calendar.open").should("be.visible");
|
||||
|
||||
// Navigate to end date and select it
|
||||
return ensureDateIsVisible(
|
||||
endDayjsDate,
|
||||
$input,
|
||||
timeout
|
||||
).then(() => {
|
||||
cy.get(_getFlatpickrDateSelector(endDayjsDate))
|
||||
.should("be.visible")
|
||||
.click();
|
||||
|
||||
cy.get(".flatpickr-calendar.open").should(
|
||||
"not.exist",
|
||||
{ timeout: 5000 }
|
||||
);
|
||||
|
||||
// Validate final range selection
|
||||
const expectedRange = `${startDayjsDate.format("YYYY-MM-DD")} to ${endDayjsDate.format("YYYY-MM-DD")}`;
|
||||
cy.wrap($input).should("have.value", expectedRange);
|
||||
|
||||
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 dayjsDate = dayjs(date);
|
||||
|
||||
return ensureDateIsVisible(dayjsDate, $input, timeout).then(() => {
|
||||
cy.get(_getFlatpickrDateSelector(dayjsDate))
|
||||
.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 dayjsDate = dayjs(date);
|
||||
|
||||
if (!dayjsDate.isValid()) {
|
||||
throw new Error(
|
||||
`getFlatpickrDate: Invalid date provided. Received: ${date}`
|
||||
);
|
||||
}
|
||||
|
||||
return ensureCalendarIsOpen(cy.wrap(subject), timeout).then($input => {
|
||||
return ensureDateIsVisible(dayjsDate, $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(dayjsDate);
|
||||
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(
|
||||
dayjsDate.date().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 get the selected dates from a Flatpickr instance.
|
||||
* Returns the selected dates as an array of YYYY-MM-DD formatted strings.
|
||||
*/
|
||||
Cypress.Commands.add(
|
||||
"getFlatpickrSelectedDates",
|
||||
{ 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. Make sure it's initialized with flatpickr.`
|
||||
);
|
||||
}
|
||||
|
||||
const selectedDates = fpInstance.selectedDates.map(date =>
|
||||
dayjs(date).format("YYYY-MM-DD")
|
||||
);
|
||||
|
||||
return selectedDates;
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue