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
function get_fallback_login_value(param) {
31
function get_fallback_login_value(param) {
31
    var env_var = param == "username" ? "KOHA_USER" : "KOHA_PASS";
32
    var env_var = param == "username" ? "KOHA_USER" : "KOHA_PASS";
(-)a/t/cypress/support/flatpickr.js (-1 / +565 lines)
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
});

Return to bug 39916