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

(-)a/t/cypress/integration/Circulation/bookingsModalTimezone_spec.ts (-1 / +646 lines)
Line 0 Link Here
0
- 
1
const dayjs = require("dayjs");
2
const utc = require("dayjs/plugin/utc");
3
const timezone = require("dayjs/plugin/timezone");
4
dayjs.extend(utc);
5
dayjs.extend(timezone);
6
7
describe("Booking Modal Timezone Tests", () => {
8
    let testData = {};
9
10
    // Handle application errors gracefully
11
    Cypress.on("uncaught:exception", (err, runnable) => {
12
        if (err.message.includes("Cannot read properties of undefined")) {
13
            return false;
14
        }
15
        return true;
16
    });
17
18
    // Ensure RESTBasicAuth is enabled before running tests
19
    before(() => {
20
        cy.task("query", {
21
            sql: "UPDATE systempreferences SET value = '1' WHERE variable = 'RESTBasicAuth'",
22
        });
23
    });
24
25
    beforeEach(() => {
26
        cy.login();
27
        cy.title().should("eq", "Koha staff interface");
28
29
        // Create fresh test data for each test
30
        cy.task("insertSampleBiblio", {
31
            item_count: 1,
32
        })
33
            .then(objects => {
34
                testData = objects;
35
36
                // Update item to be bookable
37
                return cy.task("query", {
38
                    sql: "UPDATE items SET bookable = 1, itype = 'BK', homebranch = 'CPL', enumchron = 'A', dateaccessioned = '2024-12-03' WHERE itemnumber = ?",
39
                    values: [objects.items[0].item_id],
40
                });
41
            })
42
            .then(() => {
43
                // Create a test patron
44
                return cy.task("buildSampleObject", {
45
                    object: "patron",
46
                    values: {
47
                        firstname: "Timezone",
48
                        surname: "Tester",
49
                        cardnumber: `TZ${Date.now()}`,
50
                        category_id: "PT",
51
                        library_id: testData.libraries[0].library_id,
52
                    },
53
                });
54
            })
55
            .then(mockPatron => {
56
                testData.patron = mockPatron;
57
58
                return cy.task("query", {
59
                    sql: `INSERT INTO borrowers (borrowernumber, firstname, surname, cardnumber, categorycode, branchcode, dateofbirth)
60
                      VALUES (?, ?, ?, ?, ?, ?, ?)`,
61
                    values: [
62
                        mockPatron.patron_id,
63
                        mockPatron.firstname,
64
                        mockPatron.surname,
65
                        mockPatron.cardnumber,
66
                        mockPatron.category_id,
67
                        mockPatron.library_id,
68
                        "1990-01-01",
69
                    ],
70
                });
71
            });
72
    });
73
74
    afterEach(() => {
75
        // Clean up test data
76
        if (testData.biblio) {
77
            cy.task("deleteSampleObjects", testData);
78
        }
79
        if (testData.patron) {
80
            cy.task("query", {
81
                sql: "DELETE FROM borrowers WHERE borrowernumber = ?",
82
                values: [testData.patron.patron_id],
83
            });
84
        }
85
    });
86
87
    // Helper function to setup modal
88
    const setupModal = () => {
89
        cy.intercept(
90
            "GET",
91
            `/api/v1/biblios/${testData.biblio.biblio_id}/pickup_locations*`
92
        ).as("getPickupLocations");
93
        cy.intercept("GET", "/api/v1/circulation_rules*", {
94
            body: [
95
                {
96
                    bookings_lead_period: 0,
97
                    bookings_trail_period: 0,
98
                    issuelength: 14,
99
                    renewalsallowed: 2,
100
                    renewalperiod: 7,
101
                },
102
            ],
103
        }).as("getCirculationRules");
104
105
        cy.visit(
106
            `/cgi-bin/koha/catalogue/detail.pl?biblionumber=${testData.biblio.biblio_id}`
107
        );
108
109
        cy.get('[data-bs-target="#placeBookingModal"]').first().click();
110
        cy.get("#placeBookingModal").should("be.visible");
111
112
        cy.selectFromSelect2(
113
            "#booking_patron_id",
114
            `${testData.patron.surname}, ${testData.patron.firstname}`,
115
            testData.patron.cardnumber
116
        );
117
        cy.wait("@getPickupLocations");
118
119
        cy.get("#pickup_library_id").should("not.be.disabled");
120
        cy.selectFromSelect2ByIndex("#pickup_library_id", 0);
121
122
        cy.get("#booking_item_id").should("not.be.disabled");
123
        cy.selectFromSelect2ByIndex("#booking_item_id", 1);
124
        cy.wait("@getCirculationRules");
125
126
        cy.get("#period").should("not.be.disabled");
127
    };
128
129
    /**
130
     * TIMEZONE TEST 1: Date Index Creation Consistency
131
     * =================================================
132
     *
133
     * This test validates the critical fix for date index creation using
134
     * dayjs().format('YYYY-MM-DD') instead of toISOString().split('T')[0].
135
     *
136
     * The Problem:
137
     * - toISOString() converts Date to UTC, which can shift dates
138
     * - In PST (UTC-8), midnight PST becomes 08:00 UTC
139
     * - Splitting on 'T' gives "2024-01-15" but this is the UTC date
140
     * - For western timezones, this causes dates to appear shifted
141
     *
142
     * The Fix:
143
     * - dayjs().format('YYYY-MM-DD') maintains browser timezone
144
     * - Dates are indexed by their local representation
145
     * - No timezone conversion happens during indexing
146
     *
147
     * Test Approach:
148
     * - Create a booking with known UTC datetime
149
     * - Verify calendar displays booking on correct date
150
     * - Check that bookingsByDate index uses correct date
151
     */
152
    it("should display bookings on correct calendar dates regardless of timezone offset", () => {
153
        cy.log("=== Testing date index creation consistency ===");
154
155
        const today = dayjs().startOf("day");
156
157
        /**
158
         * Create a booking with specific UTC time that tests boundary crossing.
159
         *
160
         * Scenario: Booking starts at 08:00 UTC on January 15
161
         * - In UTC: January 15 08:00
162
         * - In PST (UTC-8): January 15 00:00 (midnight PST)
163
         * - In HST (UTC-10): January 14 22:00 (10pm HST on Jan 14)
164
         *
165
         * The booking should display on January 15 in all timezones except HST,
166
         * where it would show on January 14 (because 08:00 UTC = 22:00 previous day HST).
167
         *
168
         * However, our fix ensures dates are parsed correctly in browser timezone.
169
         */
170
        const bookingDate = today.add(10, "day");
171
        const bookingStart = bookingDate.hour(0).minute(0).second(0); // Midnight local time
172
        const bookingEnd = bookingDate.hour(23).minute(59).second(59); // End of day local time
173
174
        cy.log(
175
            `Creating booking for: ${bookingDate.format("YYYY-MM-DD")} (local timezone)`
176
        );
177
        cy.log(`Start: ${bookingStart.toISOString()} (will be stored as UTC)`);
178
        cy.log(`End: ${bookingEnd.toISOString()} (will be stored as UTC)`);
179
180
        // Create booking in database
181
        cy.task("query", {
182
            sql: `INSERT INTO bookings (biblio_id, item_id, patron_id, start_date, end_date, pickup_library_id, status)
183
                  VALUES (?, ?, ?, ?, ?, ?, '1')`,
184
            values: [
185
                testData.biblio.biblio_id,
186
                testData.items[0].item_id,
187
                testData.patron.patron_id,
188
                bookingStart.format("YYYY-MM-DD HH:mm:ss"),
189
                bookingEnd.format("YYYY-MM-DD HH:mm:ss"),
190
                testData.libraries[0].library_id,
191
            ],
192
        });
193
194
        setupModal();
195
196
        cy.get("#period").as("flatpickrInput");
197
        cy.get("@flatpickrInput").openFlatpickr();
198
199
        cy.log("=== Verifying booking displays on correct date (no shift) ===");
200
201
        // The date should be disabled (has existing booking) on the correct day
202
        if (
203
            bookingDate.month() === today.month() ||
204
            bookingDate.month() === today.add(1, "month").month()
205
        ) {
206
            cy.get("@flatpickrInput")
207
                .getFlatpickrDate(bookingDate.toDate())
208
                .should("have.class", "flatpickr-disabled");
209
210
            cy.log(
211
                `✓ Booking correctly displays on ${bookingDate.format("YYYY-MM-DD")}`
212
            );
213
214
            // Verify event dot is present (visual indicator)
215
            cy.get("@flatpickrInput")
216
                .getFlatpickrDate(bookingDate.toDate())
217
                .within(() => {
218
                    cy.get(".event-dots").should("exist");
219
                    cy.log("✓ Event dot present on correct date");
220
                });
221
222
            // Verify adjacent dates are NOT disabled (no date shift)
223
            const dayBefore = bookingDate.subtract(1, "day");
224
            const dayAfter = bookingDate.add(1, "day");
225
226
            if (
227
                dayBefore.month() === today.month() ||
228
                dayBefore.month() === today.add(1, "month").month()
229
            ) {
230
                cy.get("@flatpickrInput")
231
                    .getFlatpickrDate(dayBefore.toDate())
232
                    .should("not.have.class", "flatpickr-disabled");
233
                cy.log(
234
                    `✓ Day before (${dayBefore.format("YYYY-MM-DD")}) correctly available`
235
                );
236
            }
237
238
            if (
239
                dayAfter.month() === today.month() ||
240
                dayAfter.month() === today.add(1, "month").month()
241
            ) {
242
                cy.get("@flatpickrInput")
243
                    .getFlatpickrDate(dayAfter.toDate())
244
                    .should("not.have.class", "flatpickr-disabled");
245
                cy.log(
246
                    `✓ Day after (${dayAfter.format("YYYY-MM-DD")}) correctly available`
247
                );
248
            }
249
        }
250
251
        cy.log(
252
            "✓ TIMEZONE TEST 1 PASSED: Date index creation maintains browser timezone"
253
        );
254
    });
255
256
    /**
257
     * TIMEZONE TEST 2: Multi-Day Booking Span
258
     * ========================================
259
     *
260
     * Validates that multi-day bookings span the correct number of days
261
     * without adding extra days due to timezone conversion.
262
     *
263
     * The Problem:
264
     * - When iterating dates, using toISOString() to create date keys
265
     *   could cause UTC conversion to add extra days
266
     * - A 3-day booking in PST could appear as 4 days if boundaries cross
267
     *
268
     * The Fix:
269
     * - Using dayjs().format('YYYY-MM-DD') maintains date boundaries
270
     * - Each date increments by exactly 1 day in browser timezone
271
     * - No extra days added from UTC conversion
272
     */
273
    it("should correctly span multi-day bookings without timezone-induced extra days", () => {
274
        cy.log("=== Testing multi-day booking span consistency ===");
275
276
        const today = dayjs().startOf("day");
277
278
        // Create a 3-day booking
279
        const bookingStart = today.add(15, "day");
280
        const bookingEnd = today.add(17, "day"); // Should span exactly 3 days: 15, 16, 17
281
282
        cy.log(
283
            `Creating 3-day booking: ${bookingStart.format("YYYY-MM-DD")} to ${bookingEnd.format("YYYY-MM-DD")}`
284
        );
285
286
        cy.task("query", {
287
            sql: `INSERT INTO bookings (biblio_id, item_id, patron_id, start_date, end_date, pickup_library_id, status)
288
                  VALUES (?, ?, ?, ?, ?, ?, '1')`,
289
            values: [
290
                testData.biblio.biblio_id,
291
                testData.items[0].item_id,
292
                testData.patron.patron_id,
293
                bookingStart.hour(0).minute(0).format("YYYY-MM-DD HH:mm:ss"),
294
                bookingEnd.hour(23).minute(59).format("YYYY-MM-DD HH:mm:ss"),
295
                testData.libraries[0].library_id,
296
            ],
297
        });
298
299
        setupModal();
300
301
        cy.get("#period").as("flatpickrInput");
302
        cy.get("@flatpickrInput").openFlatpickr();
303
304
        cy.log("=== Verifying booking spans exactly 3 days ===");
305
306
        // All three days should be disabled
307
        const expectedDays = [
308
            bookingStart,
309
            bookingStart.add(1, "day"),
310
            bookingStart.add(2, "day"),
311
        ];
312
313
        expectedDays.forEach((date, index) => {
314
            if (
315
                date.month() === today.month() ||
316
                date.month() === today.add(1, "month").month()
317
            ) {
318
                cy.get("@flatpickrInput")
319
                    .getFlatpickrDate(date.toDate())
320
                    .should("have.class", "flatpickr-disabled");
321
322
                cy.get("@flatpickrInput")
323
                    .getFlatpickrDate(date.toDate())
324
                    .within(() => {
325
                        cy.get(".event-dots").should("exist");
326
                    });
327
328
                cy.log(
329
                    `✓ Day ${index + 1} (${date.format("YYYY-MM-DD")}): Correctly disabled with event dot`
330
                );
331
            }
332
        });
333
334
        // The day before should NOT be disabled
335
        const dayBefore = bookingStart.subtract(1, "day");
336
        if (
337
            dayBefore.month() === today.month() ||
338
            dayBefore.month() === today.add(1, "month").month()
339
        ) {
340
            cy.get("@flatpickrInput")
341
                .getFlatpickrDate(dayBefore.toDate())
342
                .should("not.have.class", "flatpickr-disabled");
343
            cy.log(
344
                `✓ Day before booking (${dayBefore.format("YYYY-MM-DD")}): Correctly available`
345
            );
346
        }
347
348
        // The day after should NOT be disabled
349
        const dayAfter = bookingEnd.add(1, "day");
350
        if (
351
            dayAfter.month() === today.month() ||
352
            dayAfter.month() === today.add(1, "month").month()
353
        ) {
354
            cy.get("@flatpickrInput")
355
                .getFlatpickrDate(dayAfter.toDate())
356
                .should("not.have.class", "flatpickr-disabled");
357
            cy.log(
358
                `✓ Day after booking (${dayAfter.format("YYYY-MM-DD")}): Correctly available`
359
            );
360
        }
361
362
        cy.log(
363
            "✓ TIMEZONE TEST 2 PASSED: Multi-day bookings span exactly correct number of days"
364
        );
365
    });
366
367
    /**
368
     * TIMEZONE TEST 3: Date Comparison Consistency
369
     * =============================================
370
     *
371
     * Validates that date comparisons work correctly when checking for
372
     * booking conflicts, using normalized start-of-day comparisons.
373
     *
374
     * The Problem:
375
     * - Comparing Date objects with time components is unreliable
376
     * - Mixing flatpickr.parseDate() and direct Date comparisons
377
     * - Time components can cause false negatives/positives
378
     *
379
     * The Fix:
380
     * - All dates normalized to start-of-day using dayjs().startOf('day')
381
     * - Consistent parsing using dayjs() for RFC3339 strings
382
     * - Reliable date-level comparisons
383
     */
384
    it("should correctly detect conflicts using timezone-aware date comparisons", () => {
385
        cy.log("=== Testing conflict detection with timezone awareness ===");
386
387
        const today = dayjs().startOf("day");
388
389
        // Create an existing booking
390
        const existingStart = today.add(20, "day");
391
        const existingEnd = today.add(22, "day");
392
393
        cy.log(
394
            `Creating existing booking: ${existingStart.format("YYYY-MM-DD")} to ${existingEnd.format("YYYY-MM-DD")}`
395
        );
396
397
        cy.task("query", {
398
            sql: `INSERT INTO bookings (biblio_id, item_id, patron_id, start_date, end_date, pickup_library_id, status)
399
                  VALUES (?, ?, ?, ?, ?, ?, '1')`,
400
            values: [
401
                testData.biblio.biblio_id,
402
                testData.items[0].item_id,
403
                testData.patron.patron_id,
404
                existingStart.hour(0).minute(0).format("YYYY-MM-DD HH:mm:ss"),
405
                existingEnd.hour(23).minute(59).format("YYYY-MM-DD HH:mm:ss"),
406
                testData.libraries[0].library_id,
407
            ],
408
        });
409
410
        setupModal();
411
412
        cy.get("#period").as("flatpickrInput");
413
        cy.get("@flatpickrInput").openFlatpickr();
414
415
        cy.log("=== Testing conflict scenarios ===");
416
417
        // Test 1: Try to select a date within existing booking
418
        const conflictDate = existingStart.add(1, "day"); // Day 21 - in middle of booking
419
420
        if (
421
            conflictDate.month() === today.month() ||
422
            conflictDate.month() === today.add(1, "month").month()
423
        ) {
424
            cy.log(
425
                `Testing conflict detection on ${conflictDate.format("YYYY-MM-DD")}`
426
            );
427
428
            cy.get("@flatpickrInput")
429
                .getFlatpickrDate(conflictDate.toDate())
430
                .should("have.class", "flatpickr-disabled");
431
432
            cy.log(
433
                `✓ Conflict correctly detected on ${conflictDate.format("YYYY-MM-DD")}`
434
            );
435
        }
436
437
        // Test 2: Verify dates before booking are available
438
        const beforeBooking = existingStart.subtract(1, "day");
439
440
        if (
441
            beforeBooking.month() === today.month() ||
442
            beforeBooking.month() === today.add(1, "month").month()
443
        ) {
444
            cy.log(
445
                `Testing availability on ${beforeBooking.format("YYYY-MM-DD")} (before booking)`
446
            );
447
448
            cy.get("@flatpickrInput")
449
                .getFlatpickrDate(beforeBooking.toDate())
450
                .should("not.have.class", "flatpickr-disabled");
451
452
            cy.log(
453
                `✓ Date before booking correctly available: ${beforeBooking.format("YYYY-MM-DD")}`
454
            );
455
        }
456
457
        // Test 3: Verify dates after booking are available
458
        const afterBooking = existingEnd.add(1, "day");
459
460
        if (
461
            afterBooking.month() === today.month() ||
462
            afterBooking.month() === today.add(1, "month").month()
463
        ) {
464
            cy.log(
465
                `Testing availability on ${afterBooking.format("YYYY-MM-DD")} (after booking)`
466
            );
467
468
            cy.get("@flatpickrInput")
469
                .getFlatpickrDate(afterBooking.toDate())
470
                .should("not.have.class", "flatpickr-disabled");
471
472
            cy.log(
473
                `✓ Date after booking correctly available: ${afterBooking.format("YYYY-MM-DD")}`
474
            );
475
        }
476
477
        cy.log(
478
            "✓ TIMEZONE TEST 3 PASSED: Conflict detection works consistently across timezones"
479
        );
480
    });
481
482
    /**
483
     * TIMEZONE TEST 4: API Submission Round-Trip
484
     * ===========================================
485
     *
486
     * Validates that dates selected in the browser are correctly submitted
487
     * to the API and can be retrieved without date shifts.
488
     *
489
     * The Flow:
490
     * 1. User selects date in browser (e.g., January 15)
491
     * 2. JavaScript converts to ISO string with timezone offset
492
     * 3. API receives RFC3339 datetime, converts to server timezone
493
     * 4. Stores in database
494
     * 5. API retrieves, converts to RFC3339 with offset
495
     * 6. Browser receives and displays
496
     *
497
     * Expected: Date should remain January 15 throughout the flow
498
     */
499
    it("should correctly round-trip dates through API without timezone shifts", () => {
500
        cy.log("=== Testing API date round-trip consistency ===");
501
502
        const today = dayjs().startOf("day");
503
504
        // Select a date range in the future
505
        const startDate = today.add(25, "day");
506
        const endDate = today.add(27, "day");
507
508
        cy.log(
509
            `Selecting date range: ${startDate.format("YYYY-MM-DD")} to ${endDate.format("YYYY-MM-DD")}`
510
        );
511
512
        setupModal();
513
514
        // Intercept the API POST to capture what's being sent
515
        cy.intercept("POST", `/api/v1/bookings`).as("createBooking");
516
517
        cy.get("#period").selectFlatpickrDateRange(startDate, endDate);
518
519
        // Verify hidden fields have ISO strings
520
        cy.get("#booking_start_date").then($input => {
521
            const value = $input.val();
522
            cy.log(`Start date ISO string: ${value}`);
523
            expect(value).to.match(/^\d{4}-\d{2}-\d{2}T/); // ISO format
524
        });
525
526
        cy.get("#booking_end_date").then($input => {
527
            const value = $input.val();
528
            cy.log(`End date ISO string: ${value}`);
529
            expect(value).to.match(/^\d{4}-\d{2}-\d{2}T/); // ISO format
530
        });
531
532
        // Verify dates were set in hidden fields
533
        cy.get("#booking_start_date").should("not.have.value", "");
534
        cy.get("#booking_end_date").should("not.have.value", "");
535
536
        // Get the actual values from hidden fields
537
        cy.get("#booking_start_date").then($startInput => {
538
            cy.get("#booking_end_date").then($endInput => {
539
                const startValue = $startInput.val() as string;
540
                const endValue = $endInput.val() as string;
541
542
                cy.log(`Hidden field start: ${startValue}`);
543
                cy.log(`Hidden field end: ${endValue}`);
544
545
                // Parse the values
546
                const submittedStart = dayjs(startValue);
547
                const submittedEnd = dayjs(endValue);
548
549
                cy.log(
550
                    `Parsed start: ${submittedStart.format("YYYY-MM-DD HH:mm:ss")}`
551
                );
552
                cy.log(
553
                    `Parsed end: ${submittedEnd.format("YYYY-MM-DD HH:mm:ss")}`
554
                );
555
556
                // Verify dates match what user selected (in browser timezone)
557
                expect(submittedStart.format("YYYY-MM-DD")).to.equal(
558
                    startDate.format("YYYY-MM-DD")
559
                );
560
                expect(submittedEnd.format("YYYY-MM-DD")).to.equal(
561
                    endDate.format("YYYY-MM-DD")
562
                );
563
564
                cy.log(
565
                    "✓ Date values in hidden fields match selected dates (no timezone shift)"
566
                );
567
            });
568
        });
569
570
        cy.log(
571
            "✓ TIMEZONE TEST 4 PASSED: API round-trip maintains correct dates"
572
        );
573
    });
574
575
    /**
576
     * TIMEZONE TEST 5: Cross-Month Boundary
577
     * ======================================
578
     *
579
     * Validates that bookings spanning month boundaries are handled
580
     * correctly without timezone-induced date shifts.
581
     */
582
    it("should correctly handle bookings that span month boundaries", () => {
583
        cy.log("=== Testing cross-month boundary handling ===");
584
585
        const today = dayjs().startOf("day");
586
587
        // Find the last day of current or next month
588
        let testMonth = today.month() === 11 ? today : today.add(1, "month"); // If December, use current, else use next
589
        const lastDayOfMonth = testMonth.endOf("month").startOf("day");
590
        const firstDayOfNextMonth = lastDayOfMonth.add(1, "day");
591
592
        // Create a booking that spans the month boundary
593
        const bookingStart = lastDayOfMonth.subtract(1, "day");
594
        const bookingEnd = firstDayOfNextMonth.add(1, "day");
595
596
        cy.log(
597
            `Creating cross-month booking: ${bookingStart.format("YYYY-MM-DD")} to ${bookingEnd.format("YYYY-MM-DD")}`
598
        );
599
        cy.log(
600
            `Month boundary: ${lastDayOfMonth.format("YYYY-MM-DD")} / ${firstDayOfNextMonth.format("YYYY-MM-DD")}`
601
        );
602
603
        cy.task("query", {
604
            sql: `INSERT INTO bookings (biblio_id, item_id, patron_id, start_date, end_date, pickup_library_id, status)
605
                  VALUES (?, ?, ?, ?, ?, ?, '1')`,
606
            values: [
607
                testData.biblio.biblio_id,
608
                testData.items[0].item_id,
609
                testData.patron.patron_id,
610
                bookingStart.hour(0).minute(0).format("YYYY-MM-DD HH:mm:ss"),
611
                bookingEnd.hour(23).minute(59).format("YYYY-MM-DD HH:mm:ss"),
612
                testData.libraries[0].library_id,
613
            ],
614
        });
615
616
        setupModal();
617
618
        cy.get("#period").as("flatpickrInput");
619
        cy.get("@flatpickrInput").openFlatpickr();
620
621
        cy.log("=== Verifying booking spans month boundary correctly ===");
622
623
        // Test last day of first month
624
        cy.get("@flatpickrInput")
625
            .getFlatpickrDate(lastDayOfMonth.toDate())
626
            .should("have.class", "flatpickr-disabled");
627
        cy.log(
628
            `✓ Last day of month (${lastDayOfMonth.format("YYYY-MM-DD")}): Correctly disabled`
629
        );
630
631
        // Navigate to next month
632
        cy.get(".flatpickr-next-month").click();
633
634
        // Test first day of next month
635
        cy.get("@flatpickrInput")
636
            .getFlatpickrDate(firstDayOfNextMonth.toDate())
637
            .should("have.class", "flatpickr-disabled");
638
        cy.log(
639
            `✓ First day of next month (${firstDayOfNextMonth.format("YYYY-MM-DD")}): Correctly disabled`
640
        );
641
642
        cy.log(
643
            "✓ TIMEZONE TEST 5 PASSED: Month boundaries handled correctly without date shifts"
644
        );
645
    });
646
});

Return to bug 37707