View | Details | Raw Unified | Return to bug 39916
Collapse All | Expand All

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

Return to bug 39916