Line 0
Link Here
|
0 |
- |
1 |
// flatpickrHelpers.js - Enhanced Reusable Cypress functions for Flatpickr date pickers |
|
|
2 |
|
3 |
/** |
4 |
* Enhanced helper functions for interacting with Flatpickr date picker components in Cypress tests |
5 |
* Uses click-driven interactions with improved reliability and native retry mechanisms |
6 |
* Supports all standard Flatpickr operations including date selection, range selection, |
7 |
* navigation, hover interactions, and assertions. |
8 |
* |
9 |
* CHAINABILITY: |
10 |
* All Flatpickr helper commands are fully chainable. You can: |
11 |
* - Chain multiple Flatpickr operations (open, navigate, select) |
12 |
* - Chain Flatpickr commands with standard Cypress commands |
13 |
* - Split complex interactions into multiple steps for better reliability |
14 |
* |
15 |
* Examples: |
16 |
* cy.get('#myDatepicker') |
17 |
* .openFlatpickr() |
18 |
* .selectFlatpickrDate('2023-05-15'); |
19 |
* |
20 |
* cy.get('#rangePicker') |
21 |
* .openFlatpickr() |
22 |
* .selectFlatpickrDateRange('2023-06-01', '2023-06-15') |
23 |
* .should('have.value', '2023-06-01 to 2023-06-15'); |
24 |
*/ |
25 |
|
26 |
// --- Internal Utility Functions --- |
27 |
|
28 |
const monthNames = [ |
29 |
"January", |
30 |
"February", |
31 |
"March", |
32 |
"April", |
33 |
"May", |
34 |
"June", |
35 |
"July", |
36 |
"August", |
37 |
"September", |
38 |
"October", |
39 |
"November", |
40 |
"December", |
41 |
]; |
42 |
|
43 |
/** |
44 |
* Generates a Cypress selector for a specific day element within the Flatpickr calendar |
45 |
* based on its aria-label. This is a low-level internal helper. |
46 |
*/ |
47 |
const _getFlatpickrDateSelector = date => { |
48 |
const month = monthNames[date.getMonth()]; |
49 |
const day = date.getDate(); |
50 |
const year = date.getFullYear(); |
51 |
const formattedLabel = `${month} ${day}, ${year}`; |
52 |
return `.flatpickr-day[aria-label="${formattedLabel}"]`; |
53 |
}; |
54 |
|
55 |
/** |
56 |
* Ensures the Flatpickr calendar is open. If not, it clicks the input to open it. |
57 |
* Uses Cypress's built-in retry mechanism for reliability. |
58 |
*/ |
59 |
const ensureCalendarIsOpen = ($el, timeout = 10000) => { |
60 |
return $el.then($input => { |
61 |
const inputToClick = $input.is(":visible") |
62 |
? $input |
63 |
: $input.parents().find(".flatpickr-input:visible").first(); |
64 |
|
65 |
if (!inputToClick.length) { |
66 |
throw new Error( |
67 |
`Flatpickr: Could not find visible input element for selector '${$input.selector}' to open calendar.` |
68 |
); |
69 |
} |
70 |
|
71 |
// Use Cypress's retry mechanism to check if calendar is already open |
72 |
return cy.get("body").then(() => { |
73 |
return cy.get(".flatpickr-calendar").then($calendar => { |
74 |
const isVisible = |
75 |
$calendar.length > 0 && |
76 |
$calendar.hasClass("open") && |
77 |
$calendar.is(":visible"); |
78 |
|
79 |
if (!isVisible) { |
80 |
cy.wrap(inputToClick).click(); |
81 |
} |
82 |
|
83 |
// Wait for calendar to be open and visible with retry |
84 |
return cy |
85 |
.get(".flatpickr-calendar.open", { timeout }) |
86 |
.should("be.visible") |
87 |
.then(() => cy.wrap($input)); |
88 |
}); |
89 |
}); |
90 |
}); |
91 |
}; |
92 |
|
93 |
/** |
94 |
* Ensures the specified date is visible in the current calendar view. |
95 |
* Navigates to the correct month/year if necessary. |
96 |
*/ |
97 |
const ensureDateIsVisible = (targetDate, $input, timeout = 10000) => { |
98 |
const targetYear = targetDate.getFullYear(); |
99 |
const targetMonth = targetDate.getMonth(); |
100 |
|
101 |
return cy |
102 |
.get(".flatpickr-calendar.open", { timeout }) |
103 |
.should("be.visible") |
104 |
.then($calendar => { |
105 |
const fpInstance = $input[0]._flatpickr; |
106 |
if (!fpInstance) { |
107 |
throw new Error( |
108 |
`Flatpickr: Cannot find flatpickr instance on element. Make sure it's initialized with flatpickr.` |
109 |
); |
110 |
} |
111 |
const currentMonth = fpInstance.currentMonth; |
112 |
const currentYear = fpInstance.currentYear; |
113 |
|
114 |
// Check if we need to navigate |
115 |
if (currentMonth !== targetMonth || currentYear !== targetYear) { |
116 |
return navigateToMonthAndYear(targetDate, $input, timeout); |
117 |
} |
118 |
|
119 |
// Already in correct month/year, just verify the date is visible |
120 |
const selector = _getFlatpickrDateSelector(targetDate); |
121 |
return cy.get(selector, { timeout: 5000 }).should("be.visible"); |
122 |
}); |
123 |
}; |
124 |
|
125 |
/** |
126 |
* Navigates the Flatpickr calendar to the target month and year. |
127 |
* Uses native retry mechanisms and verifies target date is in view. |
128 |
*/ |
129 |
const navigateToMonthAndYear = (targetDate, $input, timeout = 10000) => { |
130 |
const targetYear = targetDate.getFullYear(); |
131 |
const targetMonth = targetDate.getMonth(); |
132 |
|
133 |
return cy |
134 |
.get(".flatpickr-calendar.open", { timeout }) |
135 |
.should("be.visible") |
136 |
.then($calendar => { |
137 |
const fpInstance = $input[0]._flatpickr; |
138 |
if (!fpInstance) { |
139 |
throw new Error( |
140 |
`Flatpickr: Cannot find flatpickr instance on element. Make sure it's initialized with flatpickr.` |
141 |
); |
142 |
} |
143 |
const currentMonth = fpInstance.currentMonth; |
144 |
const currentYear = fpInstance.currentYear; |
145 |
|
146 |
const monthDiff = |
147 |
(targetYear - currentYear) * 12 + (targetMonth - currentMonth); |
148 |
|
149 |
if (monthDiff === 0) { |
150 |
// Already in correct month, verify target date is visible |
151 |
const selector = _getFlatpickrDateSelector(targetDate); |
152 |
return cy.get(selector, { timeout: 5000 }).should("be.visible"); |
153 |
} |
154 |
|
155 |
// Use flatpickr's changeMonth method for faster navigation |
156 |
fpInstance.changeMonth(monthDiff, true); |
157 |
|
158 |
// Verify navigation succeeded by checking target date is now visible |
159 |
const selector = _getFlatpickrDateSelector(targetDate); |
160 |
return cy |
161 |
.get(selector, { timeout: 5000 }) |
162 |
.should("be.visible") |
163 |
.should($el => { |
164 |
// Ensure the element is actually the date we want |
165 |
expect($el).to.have.length(1); |
166 |
expect($el.attr("aria-label")).to.contain( |
167 |
targetDate.getDate().toString() |
168 |
); |
169 |
}); |
170 |
}); |
171 |
}; |
172 |
|
173 |
// --- User-Facing Helper Commands --- |
174 |
|
175 |
/** |
176 |
* Helper to open a Flatpickr calendar. |
177 |
*/ |
178 |
Cypress.Commands.add( |
179 |
"openFlatpickr", |
180 |
{ prevSubject: "optional" }, |
181 |
(subject, selector, timeout = 10000) => { |
182 |
const $el = subject ? cy.wrap(subject) : cy.get(selector); |
183 |
return ensureCalendarIsOpen($el, timeout); |
184 |
} |
185 |
); |
186 |
|
187 |
/** |
188 |
* Helper to close an open Flatpickr calendar. |
189 |
*/ |
190 |
Cypress.Commands.add( |
191 |
"closeFlatpickr", |
192 |
{ prevSubject: "optional" }, |
193 |
(subject, selector) => { |
194 |
const chain = subject |
195 |
? cy.wrap(subject) |
196 |
: selector |
197 |
? cy.get(selector) |
198 |
: cy.get("body"); |
199 |
|
200 |
return chain.then($el => { |
201 |
// Click outside to close, using body coordinates that should always be safe |
202 |
cy.get("body").click(0, 0); |
203 |
|
204 |
// Wait for calendar to close with retry mechanism |
205 |
cy.get(".flatpickr-calendar.open").should("not.exist", { |
206 |
timeout: 5000, |
207 |
}); |
208 |
|
209 |
return cy.wrap($el); |
210 |
}); |
211 |
} |
212 |
); |
213 |
|
214 |
/** |
215 |
* Helper to navigate to a specific month and year in a Flatpickr calendar. |
216 |
*/ |
217 |
Cypress.Commands.add( |
218 |
"navigateToFlatpickrMonth", |
219 |
{ prevSubject: true }, |
220 |
(subject, targetDate, timeout = 10000) => { |
221 |
return ensureCalendarIsOpen(cy.wrap(subject), timeout).then($input => { |
222 |
const dateObj = |
223 |
typeof targetDate === "string" |
224 |
? new Date(targetDate) |
225 |
: targetDate; |
226 |
return navigateToMonthAndYear(dateObj, $input, timeout).then(() => |
227 |
cy.wrap($input) |
228 |
); |
229 |
}); |
230 |
} |
231 |
); |
232 |
|
233 |
/** |
234 |
* Helper to get the Flatpickr mode ('single', 'range', 'multiple'). |
235 |
*/ |
236 |
Cypress.Commands.add("getFlatpickrMode", { prevSubject: true }, subject => { |
237 |
return cy.wrap(subject).then($input => { |
238 |
const fpInstance = $input[0]._flatpickr; |
239 |
if (!fpInstance) { |
240 |
throw new Error( |
241 |
`Flatpickr: Cannot find flatpickr instance on element ${$input.selector}. Make sure it's initialized with flatpickr.` |
242 |
); |
243 |
} |
244 |
return fpInstance.config.mode; |
245 |
}); |
246 |
}); |
247 |
|
248 |
/** |
249 |
* Helper to select a specific date in a Flatpickr. |
250 |
*/ |
251 |
Cypress.Commands.add( |
252 |
"selectFlatpickrDate", |
253 |
{ prevSubject: true }, |
254 |
(subject, date, timeout = 10000) => { |
255 |
return ensureCalendarIsOpen(cy.wrap(subject), timeout).then($input => { |
256 |
const dateObj = typeof date === "string" ? new Date(date) : date; |
257 |
|
258 |
return ensureDateIsVisible(dateObj, $input, timeout).then(() => { |
259 |
// Click the date - break chain to avoid DOM detachment |
260 |
cy.get(_getFlatpickrDateSelector(dateObj)) |
261 |
.should("be.visible") |
262 |
.click(); |
263 |
|
264 |
// Re-query and validate selection based on mode |
265 |
return cy |
266 |
.wrap($input) |
267 |
.getFlatpickrMode() |
268 |
.then(mode => { |
269 |
if (mode === "single") { |
270 |
const expectedDate = `${dateObj.getFullYear()}-${(dateObj.getMonth() + 1).toString().padStart(2, "0")}-${dateObj.getDate().toString().padStart(2, "0")}`; |
271 |
|
272 |
cy.wrap($input).should("have.value", expectedDate); |
273 |
cy.get(".flatpickr-calendar.open").should( |
274 |
"not.exist", |
275 |
{ timeout: 5000 } |
276 |
); |
277 |
return cy.wrap($input); |
278 |
} else if (mode === "range") { |
279 |
// In range mode, first selection keeps calendar open - re-query element |
280 |
// Add small wait to allow for complex date recalculations (e.g., booking availability) |
281 |
cy.wait(200); |
282 |
cy.get(_getFlatpickrDateSelector(dateObj)).should( |
283 |
"have.class", |
284 |
"selected" |
285 |
); |
286 |
cy.get(".flatpickr-calendar.open").should( |
287 |
"be.visible" |
288 |
); |
289 |
return cy.wrap($input); |
290 |
} |
291 |
|
292 |
return cy.wrap($input); |
293 |
}); |
294 |
}); |
295 |
}); |
296 |
} |
297 |
); |
298 |
|
299 |
/** |
300 |
* Helper to select a date range in a Flatpickr range picker. |
301 |
*/ |
302 |
Cypress.Commands.add( |
303 |
"selectFlatpickrDateRange", |
304 |
{ prevSubject: true }, |
305 |
(subject, startDate, endDate, timeout = 10000) => { |
306 |
return ensureCalendarIsOpen(cy.wrap(subject), timeout).then($input => { |
307 |
const startDateObj = |
308 |
typeof startDate === "string" ? new Date(startDate) : startDate; |
309 |
const endDateObj = |
310 |
typeof endDate === "string" ? new Date(endDate) : endDate; |
311 |
|
312 |
// Validate range mode first |
313 |
return cy |
314 |
.wrap($input) |
315 |
.getFlatpickrMode() |
316 |
.then(mode => { |
317 |
if (mode !== "range") { |
318 |
throw new Error( |
319 |
`Flatpickr: This flatpickr instance is not in range mode. Current mode: ${mode}. Cannot select range.` |
320 |
); |
321 |
} |
322 |
|
323 |
// Select start date - break chain to avoid DOM detachment |
324 |
return ensureDateIsVisible( |
325 |
startDateObj, |
326 |
$input, |
327 |
timeout |
328 |
).then(() => { |
329 |
cy.get(_getFlatpickrDateSelector(startDateObj)) |
330 |
.should("be.visible") |
331 |
.click(); |
332 |
|
333 |
// Add small wait to allow for complex date recalculations (e.g., booking availability) |
334 |
cy.wait(200); |
335 |
// Re-query element after DOM update |
336 |
cy.get(_getFlatpickrDateSelector(startDateObj)) |
337 |
.should("have.class", "selected") |
338 |
.and("have.class", "startRange"); |
339 |
|
340 |
// Ensure calendar stays open |
341 |
cy.get(".flatpickr-calendar.open").should("be.visible"); |
342 |
|
343 |
// Navigate to end date and select it |
344 |
return ensureDateIsVisible( |
345 |
endDateObj, |
346 |
$input, |
347 |
timeout |
348 |
).then(() => { |
349 |
cy.get(_getFlatpickrDateSelector(endDateObj)) |
350 |
.should("be.visible") |
351 |
.click(); |
352 |
|
353 |
// Validate final range selection |
354 |
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")}`; |
355 |
|
356 |
cy.wrap($input).should("have.value", expectedRange); |
357 |
cy.get(".flatpickr-calendar.open").should( |
358 |
"not.exist", |
359 |
{ timeout: 5000 } |
360 |
); |
361 |
|
362 |
return cy.wrap($input); |
363 |
}); |
364 |
}); |
365 |
}); |
366 |
}); |
367 |
} |
368 |
); |
369 |
|
370 |
/** |
371 |
* Helper to hover over a specific date in a Flatpickr calendar. |
372 |
*/ |
373 |
Cypress.Commands.add( |
374 |
"hoverFlatpickrDate", |
375 |
{ prevSubject: true }, |
376 |
(subject, date, timeout = 10000) => { |
377 |
return ensureCalendarIsOpen(cy.wrap(subject), timeout).then($input => { |
378 |
const dateObj = typeof date === "string" ? new Date(date) : date; |
379 |
|
380 |
return ensureDateIsVisible(dateObj, $input, timeout).then(() => { |
381 |
cy.get(_getFlatpickrDateSelector(dateObj)) |
382 |
.should("be.visible") |
383 |
.trigger("mouseover"); |
384 |
|
385 |
return cy.wrap($input); |
386 |
}); |
387 |
}); |
388 |
} |
389 |
); |
390 |
|
391 |
// --- Enhanced Assertion Commands --- |
392 |
|
393 |
/** |
394 |
* Helper to get a specific Flatpickr day element by its date. |
395 |
*/ |
396 |
Cypress.Commands.add( |
397 |
"getFlatpickrDate", |
398 |
{ prevSubject: true }, |
399 |
(subject, date, timeout = 10000) => { |
400 |
const dateObj = typeof date === "string" ? new Date(date) : date; |
401 |
|
402 |
if (!(dateObj instanceof Date && !isNaN(dateObj.getTime()))) { |
403 |
throw new Error( |
404 |
`getFlatpickrDate: Invalid date provided. Received: ${date}` |
405 |
); |
406 |
} |
407 |
|
408 |
return ensureCalendarIsOpen(cy.wrap(subject), timeout).then($input => { |
409 |
return ensureDateIsVisible(dateObj, $input, timeout).then(() => { |
410 |
// Instead of returning the element directly, return a function that re-queries |
411 |
// This ensures subsequent .should() calls get a fresh element reference |
412 |
const selector = _getFlatpickrDateSelector(dateObj); |
413 |
return cy |
414 |
.get(selector, { timeout: 5000 }) |
415 |
.should("be.visible") |
416 |
.should($el => { |
417 |
// Ensure the element is actually the date we want |
418 |
expect($el).to.have.length(1); |
419 |
expect($el.attr("aria-label")).to.contain( |
420 |
dateObj.getDate().toString() |
421 |
); |
422 |
}); |
423 |
}); |
424 |
}); |
425 |
} |
426 |
); |
427 |
|
428 |
/** |
429 |
* Assertion helper to check if a Flatpickr date is disabled. |
430 |
*/ |
431 |
Cypress.Commands.add( |
432 |
"flatpickrDateShouldBeDisabled", |
433 |
{ prevSubject: true }, |
434 |
(subject, date) => { |
435 |
return cy |
436 |
.wrap(subject) |
437 |
.getFlatpickrDate(date) |
438 |
.should("have.class", "flatpickr-disabled") |
439 |
.then(() => cy.wrap(subject)); |
440 |
} |
441 |
); |
442 |
|
443 |
/** |
444 |
* Assertion helper to check if a Flatpickr date is enabled. |
445 |
*/ |
446 |
Cypress.Commands.add( |
447 |
"flatpickrDateShouldBeEnabled", |
448 |
{ prevSubject: true }, |
449 |
(subject, date) => { |
450 |
return cy |
451 |
.wrap(subject) |
452 |
.getFlatpickrDate(date) |
453 |
.should("not.have.class", "flatpickr-disabled") |
454 |
.then(() => cy.wrap(subject)); |
455 |
} |
456 |
); |
457 |
|
458 |
/** |
459 |
* Assertion helper to check if a Flatpickr date is selected. |
460 |
*/ |
461 |
Cypress.Commands.add( |
462 |
"flatpickrDateShouldBeSelected", |
463 |
{ prevSubject: true }, |
464 |
(subject, date) => { |
465 |
return cy |
466 |
.wrap(subject) |
467 |
.getFlatpickrDate(date) |
468 |
.should("have.class", "selected") |
469 |
.then(() => cy.wrap(subject)); |
470 |
} |
471 |
); |
472 |
|
473 |
/** |
474 |
* Assertion helper to check if a Flatpickr date is not selected. |
475 |
*/ |
476 |
Cypress.Commands.add( |
477 |
"flatpickrDateShouldNotBeSelected", |
478 |
{ prevSubject: true }, |
479 |
(subject, date) => { |
480 |
return cy |
481 |
.wrap(subject) |
482 |
.getFlatpickrDate(date) |
483 |
.should("not.have.class", "selected") |
484 |
.then(() => cy.wrap(subject)); |
485 |
} |
486 |
); |
487 |
|
488 |
/** |
489 |
* Assertion helper to check if a Flatpickr date is today. |
490 |
*/ |
491 |
Cypress.Commands.add( |
492 |
"flatpickrDateShouldBeToday", |
493 |
{ prevSubject: true }, |
494 |
(subject, date) => { |
495 |
return cy |
496 |
.wrap(subject) |
497 |
.getFlatpickrDate(date) |
498 |
.should("have.class", "today") |
499 |
.then(() => cy.wrap(subject)); |
500 |
} |
501 |
); |
502 |
|
503 |
/** |
504 |
* Assertion helper to check if a Flatpickr date is in a range (has inRange class). |
505 |
*/ |
506 |
Cypress.Commands.add( |
507 |
"flatpickrDateShouldBeInRange", |
508 |
{ prevSubject: true }, |
509 |
(subject, date) => { |
510 |
return cy |
511 |
.wrap(subject) |
512 |
.getFlatpickrDate(date) |
513 |
.should("have.class", "inRange") |
514 |
.then(() => cy.wrap(subject)); |
515 |
} |
516 |
); |
517 |
|
518 |
/** |
519 |
* Assertion helper to check if a Flatpickr date is the start of a range. |
520 |
*/ |
521 |
Cypress.Commands.add( |
522 |
"flatpickrDateShouldBeRangeStart", |
523 |
{ prevSubject: true }, |
524 |
(subject, date) => { |
525 |
return cy |
526 |
.wrap(subject) |
527 |
.getFlatpickrDate(date) |
528 |
.should("have.class", "startRange") |
529 |
.then(() => cy.wrap(subject)); |
530 |
} |
531 |
); |
532 |
|
533 |
/** |
534 |
* Assertion helper to check if a Flatpickr date is the end of a range. |
535 |
*/ |
536 |
Cypress.Commands.add( |
537 |
"flatpickrDateShouldBeRangeEnd", |
538 |
{ prevSubject: true }, |
539 |
(subject, date) => { |
540 |
return cy |
541 |
.wrap(subject) |
542 |
.getFlatpickrDate(date) |
543 |
.should("have.class", "endRange") |
544 |
.then(() => cy.wrap(subject)); |
545 |
} |
546 |
); |
547 |
|
548 |
/** |
549 |
* Helper to clear a Flatpickr input by setting its value to empty. |
550 |
* Works with hidden inputs by using the Flatpickr API directly. |
551 |
*/ |
552 |
Cypress.Commands.add("clearFlatpickr", { prevSubject: true }, subject => { |
553 |
return cy.wrap(subject).then($input => { |
554 |
// Wait for flatpickr to be initialized and then clear it |
555 |
return cy |
556 |
.wrap($input) |
557 |
.should($el => { |
558 |
expect($el[0]).to.have.property("_flatpickr"); |
559 |
}) |
560 |
.then(() => { |
561 |
$input[0]._flatpickr.clear(); |
562 |
return cy.wrap(subject); |
563 |
}); |
564 |
}); |
565 |
}); |