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

(-)a/Koha/REST/V1/Libraries.pm (+1 lines)
Lines 18-23 package Koha::REST::V1::Libraries; Link Here
18
use Modern::Perl;
18
use Modern::Perl;
19
19
20
use Mojo::Base 'Mojolicious::Controller';
20
use Mojo::Base 'Mojolicious::Controller';
21
use C4::Context;
21
use Koha::Libraries;
22
use Koha::Libraries;
22
use Koha::Calendar;
23
use Koha::Calendar;
23
use Koha::DateUtils qw( dt_from_string );
24
use Koha::DateUtils qw( dt_from_string );
(-)a/koha-tmpl/intranet-tmpl/prog/en/includes/modals/booking/island.inc (-1 / +1 lines)
Lines 6-12 Link Here
6
        show-patron-select
6
        show-patron-select
7
        show-item-details-selects
7
        show-item-details-selects
8
        show-pickup-location-select
8
        show-pickup-location-select
9
        date-range-constraint="[% (Koha.Preference('BookingDateRangeConstraint') || 'issuelength_with_renewals') | html %]" [%# FIXME: issuelength_with_renewals needs to be the db default, don't constrain needs a value %]
9
        date-range-constraint="issuelength_with_renewals"
10
    ></booking-modal-island>
10
    ></booking-modal-island>
11
</div>
11
</div>
12
[% SET islands = Asset.js("js/vue/dist/islands.esm.js").match('(src="([^"]+)")').1 %] <script src="[% islands %]" type="module"></script>
12
[% SET islands = Asset.js("js/vue/dist/islands.esm.js").match('(src="([^"]+)")').1 %] <script src="[% islands %]" type="module"></script>
(-)a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Bookings/BookingModal.vue (-2 / +6 lines)
Lines 272-279 const { Link Here
272
    loading,
272
    loading,
273
} = storeToRefs(store);
273
} = storeToRefs(store);
274
274
275
const { canSubmit: canSubmitReactive } = useBookingValidation(store);
276
277
const modalState = reactive({
275
const modalState = reactive({
278
    isOpen: props.open,
276
    isOpen: props.open,
279
    step: 1,
277
    step: 1,
Lines 297-302 const showPickupLocationSelect = computed(() => { Link Here
297
    return props.showPickupLocationSelect;
295
    return props.showPickupLocationSelect;
298
});
296
});
299
297
298
const { canSubmit: canSubmitReactive } = useBookingValidation(store, {
299
    showPatronSelect: computed(() => props.showPatronSelect),
300
    showItemDetailsSelects: computed(() => props.showItemDetailsSelects),
301
    showPickupLocationSelect,
302
});
303
300
const stepNumber = computed(() => {
304
const stepNumber = computed(() => {
301
    return calculateStepNumbers(
305
    return calculateStepNumbers(
302
        props.showPatronSelect,
306
        props.showPatronSelect,
(-)a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Bookings/composables/useBookingValidation.mjs (-42 / +7 lines)
Lines 5-22 Link Here
5
5
6
import { computed } from "vue";
6
import { computed } from "vue";
7
import { storeToRefs } from "pinia";
7
import { storeToRefs } from "pinia";
8
import {
8
import { canSubmitBooking } from "../lib/booking/validation.mjs";
9
    canProceedToStep3,
10
    canSubmitBooking,
11
} from "../lib/booking/validation.mjs";
12
import { handleBookingDateChange } from "../lib/booking/availability.mjs";
13
9
14
/**
10
/**
15
 * Composable for booking validation with reactive state
11
 * Composable for booking validation with reactive state
16
 * @param {Object} store - Pinia booking store instance
12
 * @param {Object} store - Pinia booking store instance
17
 * @returns {Object} Reactive validation properties and methods
13
 * @param {{ showPatronSelect: import('vue').Ref<boolean>|boolean, showItemDetailsSelects: import('vue').Ref<boolean>|boolean, showPickupLocationSelect: import('vue').Ref<boolean>|boolean }} uiFlags - Reactive UI visibility flags (props or refs)
14
 * @returns {{ canSubmit: import('vue').ComputedRef<boolean> }}
18
 */
15
 */
19
export function useBookingValidation(store) {
16
export function useBookingValidation(store, uiFlags = {}) {
20
    const {
17
    const {
21
        bookingPatron,
18
        bookingPatron,
22
        pickupLibraryId,
19
        pickupLibraryId,
Lines 25-56 export function useBookingValidation(store) { Link Here
25
        bookingItemId,
22
        bookingItemId,
26
        bookableItems,
23
        bookableItems,
27
        selectedDateRange,
24
        selectedDateRange,
28
        bookings,
29
        checkouts,
30
        circulationRules,
31
        bookingId,
32
    } = storeToRefs(store);
25
    } = storeToRefs(store);
33
26
34
    const canProceedToStep3Computed = computed(() => {
35
        return canProceedToStep3({
36
            showPatronSelect: store.showPatronSelect,
37
            bookingPatron: bookingPatron.value,
38
            showItemDetailsSelects: store.showItemDetailsSelects,
39
            showPickupLocationSelect: store.showPickupLocationSelect,
40
            pickupLibraryId: pickupLibraryId.value,
41
            bookingItemtypeId: bookingItemtypeId.value,
42
            itemtypeOptions: itemTypes.value,
43
            bookingItemId: bookingItemId.value,
44
            bookableItems: bookableItems.value,
45
        });
46
    });
47
48
    const canSubmitComputed = computed(() => {
27
    const canSubmitComputed = computed(() => {
49
        const validationData = {
28
        const validationData = {
50
            showPatronSelect: store.showPatronSelect,
29
            showPatronSelect: uiFlags.showPatronSelect?.value ?? uiFlags.showPatronSelect ?? false,
51
            bookingPatron: bookingPatron.value,
30
            bookingPatron: bookingPatron.value,
52
            showItemDetailsSelects: store.showItemDetailsSelects,
31
            showItemDetailsSelects: uiFlags.showItemDetailsSelects?.value ?? uiFlags.showItemDetailsSelects ?? false,
53
            showPickupLocationSelect: store.showPickupLocationSelect,
32
            showPickupLocationSelect: uiFlags.showPickupLocationSelect?.value ?? uiFlags.showPickupLocationSelect ?? false,
54
            pickupLibraryId: pickupLibraryId.value,
33
            pickupLibraryId: pickupLibraryId.value,
55
            bookingItemtypeId: bookingItemtypeId.value,
34
            bookingItemtypeId: bookingItemtypeId.value,
56
            itemtypeOptions: itemTypes.value,
35
            itemtypeOptions: itemTypes.value,
Lines 60-80 export function useBookingValidation(store) { Link Here
60
        return canSubmitBooking(validationData, selectedDateRange.value);
39
        return canSubmitBooking(validationData, selectedDateRange.value);
61
    });
40
    });
62
41
63
    const validateDates = selectedDates => {
64
        return handleBookingDateChange(
65
            selectedDates,
66
            circulationRules.value,
67
            bookings.value,
68
            checkouts.value,
69
            bookableItems.value,
70
            bookingItemId.value,
71
            bookingId.value
72
        );
73
    };
74
75
    return {
42
    return {
76
        canProceedToStep3: canProceedToStep3Computed,
77
        canSubmit: canSubmitComputed,
43
        canSubmit: canSubmitComputed,
78
        validateDates,
79
    };
44
    };
80
}
45
}
(-)a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Bookings/lib/adapters/api/opac.js (-29 / +9 lines)
Lines 231-266 export async function fetchCirculationRules(params = {}) { Link Here
231
}
231
}
232
232
233
/**
233
/**
234
 * Fetches holidays (closed days) for a library
234
 * Fetches holidays (closed days) for a library.
235
 * @param {string} libraryId - The library branchcode
235
 * Stub — no public holidays endpoint exists yet; returns empty array
236
 * @param {string} [from] - Start date (ISO format), defaults to today
236
 * so the calendar renders without holiday highlighting in OPAC context.
237
 * @param {string} [to] - End date (ISO format), defaults to 3 months from start
237
 * @param {string} _libraryId - The library branchcode (unused)
238
 * @returns {Promise<string[]>} Array of holiday dates in YYYY-MM-DD format
238
 * @param {string} [_from] - Start date (unused)
239
 * @throws {Error} If the request fails or returns a non-OK status
239
 * @param {string} [_to] - End date (unused)
240
 * @returns {Promise<string[]>} Always returns empty array
240
 */
241
 */
241
export async function fetchHolidays(libraryId, from, to) {
242
export async function fetchHolidays(_libraryId, _from, _to) {
242
    if (!libraryId) {
243
    return [];
243
        return [];
244
    }
245
246
    const params = new URLSearchParams();
247
    if (from) params.set("from", from);
248
    if (to) params.set("to", to);
249
250
    const url = `/api/v1/public/libraries/${encodeURIComponent(
251
        libraryId
252
    )}/holidays${params.toString() ? `?${params.toString()}` : ""}`;
253
254
    const response = await fetch(url);
255
256
    if (!response.ok) {
257
        throw bookingValidation.validationError("fetch_holidays_failed", {
258
            status: response.status,
259
            statusText: response.statusText,
260
        });
261
    }
262
263
    return await response.json();
264
}
244
}
265
245
266
export async function createBooking() {
246
export async function createBooking() {
(-)a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Bookings/lib/adapters/external-dependents.mjs (-9 / +14 lines)
Lines 152-162 function updateBookingCounts(isUpdate, dependencies) { Link Here
152
        let updatedCount = 0;
152
        let updatedCount = 0;
153
153
154
        countEls.forEach(el => {
154
        countEls.forEach(el => {
155
            const html = el.innerHTML;
155
            const current = el.textContent.match(/(\d+)/);
156
            const match = html.match(/(\d+)/);
156
            if (current) {
157
            if (match) {
157
                el.textContent = el.textContent.replace(/(\d+)/, String(parseInt(current[1], 10) + 1));
158
                const newCount = parseInt(match[1], 10) + 1;
159
                el.innerHTML = html.replace(/(\d+)/, String(newCount));
160
                updatedCount++;
158
                updatedCount++;
161
            }
159
            }
162
        });
160
        });
Lines 191-200 function showTransientSuccess(isUpdate, dependencies) { Link Here
191
            : $__("Booking successfully placed");
189
            : $__("Booking successfully placed");
192
190
193
        const el = container[0] || container;
191
        const el = container[0] || container;
194
        el.innerHTML = `<div class="alert alert-success alert-dismissible fade show" role="alert">
192
        const alert = document.createElement("div");
195
            ${msg}
193
        alert.className = "alert alert-success alert-dismissible fade show";
196
            <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
194
        alert.setAttribute("role", "alert");
197
        </div>`;
195
        alert.textContent = msg;
196
        const closeBtn = document.createElement("button");
197
        closeBtn.type = "button";
198
        closeBtn.className = "btn-close";
199
        closeBtn.setAttribute("data-bs-dismiss", "alert");
200
        closeBtn.setAttribute("aria-label", "Close");
201
        alert.appendChild(closeBtn);
202
        el.replaceChildren(alert);
198
203
199
        return { success: true };
204
        return { success: true };
200
    } catch (error) {
205
    } catch (error) {
(-)a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Bookings/types/bookings.d.ts (-6 lines)
Lines 97-108 export type CalendarMarker = { Link Here
97
    barcode: string | null;
97
    barcode: string | null;
98
};
98
};
99
99
100
/** Minimal item type shape used in constraints */
101
export type ItemType = {
102
    item_type_id: string;
103
    name?: string;
104
};
105
106
/**
100
/**
107
 * Result of availability calculation: Flatpickr disable function + daily map.
101
 * Result of availability calculation: Flatpickr disable function + daily map.
108
 */
102
 */
(-)a/t/cypress/integration/Circulation/bookingsModalDatePicker_spec.ts (-70 / +72 lines)
Lines 453-475 describe("Booking Modal Date Picker Tests", () => { Link Here
453
453
454
        // Test in clear zone starting at Day 50 to avoid conflicts
454
        // Test in clear zone starting at Day 50 to avoid conflicts
455
        const clearZoneStart = today.add(50, "day");
455
        const clearZoneStart = today.add(50, "day");
456
        const calculatedMaxDate = clearZoneStart.add(
456
        // Start date counts as day 1 (Koha convention), so max end = start + (maxPeriod - 1)
457
        const maxPeriod =
457
            dateTestCirculationRules.issuelength +
458
            dateTestCirculationRules.issuelength +
458
                dateTestCirculationRules.renewalsallowed *
459
            dateTestCirculationRules.renewalsallowed *
459
                    dateTestCirculationRules.renewalperiod,
460
                dateTestCirculationRules.renewalperiod; // 10 + 3*5 = 25
461
        const calculatedMaxDate = clearZoneStart.add(
462
            maxPeriod - 1,
460
            "day"
463
            "day"
461
        ); // Day 50 + 25 = Day 75
464
        ); // Day 50 + 24 = Day 74
462
465
463
        const beyondMaxDate = calculatedMaxDate.add(1, "day"); // Day 76
466
        const beyondMaxDate = calculatedMaxDate.add(1, "day"); // Day 75
464
467
465
        cy.log(
468
        cy.log(
466
            `Clear zone start: ${clearZoneStart.format("YYYY-MM-DD")} (Day 50)`
469
            `Clear zone start: ${clearZoneStart.format("YYYY-MM-DD")} (Day 50)`
467
        );
470
        );
468
        cy.log(
471
        cy.log(
469
            `Calculated max date: ${calculatedMaxDate.format("YYYY-MM-DD")} (Day 75)`
472
            `Calculated max date: ${calculatedMaxDate.format("YYYY-MM-DD")} (Day 74)`
470
        );
473
        );
471
        cy.log(
474
        cy.log(
472
            `Beyond max date: ${beyondMaxDate.format("YYYY-MM-DD")} (Day 76 - should be disabled)`
475
            `Beyond max date: ${beyondMaxDate.format("YYYY-MM-DD")} (Day 75 - should be disabled)`
473
        );
476
        );
474
477
475
        // Select the start date to establish context for bold date calculation
478
        // Select the start date to establish context for bold date calculation
Lines 477-497 describe("Booking Modal Date Picker Tests", () => { Link Here
477
            clearZoneStart.toDate()
480
            clearZoneStart.toDate()
478
        );
481
        );
479
482
480
        // Verify max date is selectable
483
        // Verify max date enforcement via the flatpickr disable function.
481
        cy.get("@dateTestFlatpickr")
484
        // We query the disable function directly rather than navigating the
482
            .getFlatpickrDate(calculatedMaxDate.toDate())
485
        // calendar DOM, because the booking modal's reactive system
483
            .should("not.have.class", "flatpickr-disabled")
486
        // (onMonthChange → visibleRangeRef → syncInstanceDatesFromStore →
484
            .and("be.visible");
487
        // fp.jumpToDate) races with test navigation and pulls the calendar
485
488
        // back to the start date's month.
486
        // Verify beyond max date is disabled (if in visible month range)
489
        cy.get("@dateTestFlatpickr").should($el => {
487
        if (
490
            const fp = $el[0]._flatpickr;
488
            beyondMaxDate.month() === clearZoneStart.month() ||
491
            const disableFn = fp.config.disable[0];
489
            beyondMaxDate.month() === clearZoneStart.add(1, "month").month()
492
            expect(disableFn(calculatedMaxDate.toDate())).to.eq(
490
        ) {
493
                false,
491
            cy.get("@dateTestFlatpickr")
494
                `Max date ${calculatedMaxDate.format("YYYY-MM-DD")} should be selectable`
492
                .getFlatpickrDate(beyondMaxDate.toDate())
495
            );
493
                .should("have.class", "flatpickr-disabled");
496
            expect(disableFn(beyondMaxDate.toDate())).to.eq(
494
        }
497
                true,
498
                `Beyond max date ${beyondMaxDate.format("YYYY-MM-DD")} should be disabled`
499
            );
500
        });
495
501
496
        cy.log("✓ Maximum date calculation enforced correctly");
502
        cy.log("✓ Maximum date calculation enforced correctly");
497
503
Lines 527-548 describe("Booking Modal Date Picker Tests", () => { Link Here
527
            `Expected bold dates: ${expectedBoldDates.map(d => d.format("YYYY-MM-DD")).join(", ")}`
533
            `Expected bold dates: ${expectedBoldDates.map(d => d.format("YYYY-MM-DD")).join(", ")}`
528
        );
534
        );
529
535
530
        // Test each expected bold date has the "booking-loan-boundary" class
536
        // Verify bold dates are registered in the instance's loan boundary
531
        expectedBoldDates.forEach(boldDate => {
537
        // cache. We check the cache directly to avoid cross-month navigation
532
            if (
538
        // races (the booking modal's onMonthChange handler asynchronously
533
                boldDate.month() === clearZoneStart.month() ||
539
        // pulls the calendar back to the start date's month).
534
                boldDate.month() === clearZoneStart.add(1, "month").month()
540
        cy.get("@dateTestFlatpickr").should($el => {
535
            ) {
541
            const fp = $el[0]._flatpickr;
542
            const boundaryTimes = fp._loanBoundaryTimes;
543
            // Cross-frame: instanceof Set fails across iframe boundaries,
544
            // so check for Set-like interface instead
545
            expect(boundaryTimes).to.exist;
546
            expect(typeof boundaryTimes.has).to.eq("function");
547
548
            expectedBoldDates.forEach(boldDate => {
549
                const ts = boldDate.toDate().getTime();
550
                expect(
551
                    boundaryTimes.has(ts),
552
                    `Expected ${boldDate.format("YYYY-MM-DD")} to be a loan boundary`
553
                ).to.be.true;
554
            });
555
        });
556
557
        // For dates in the currently visible month, also verify the DOM class
558
        expectedBoldDates
559
            .filter(d => d.month() === clearZoneStart.month())
560
            .forEach(boldDate => {
536
                cy.get("@dateTestFlatpickr")
561
                cy.get("@dateTestFlatpickr")
537
                    .getFlatpickrDate(boldDate.toDate())
562
                    .getFlatpickrDate(boldDate.toDate())
538
                    .should("have.class", "booking-loan-boundary");
563
                    .should("have.class", "booking-loan-boundary");
539
                cy.log(
564
                cy.log(
540
                    `✓ Day ${boldDate.format("YYYY-MM-DD")}: Has 'booking-loan-boundary' class (bold)`
565
                    `✓ Day ${boldDate.format("YYYY-MM-DD")}: Has 'booking-loan-boundary' class (bold)`
541
                );
566
                );
542
            }
567
            });
543
        });
544
568
545
        // Verify that only expected dates are bold (have "booking-loan-boundary" class)
569
        // Verify that only expected dates are bold in the current view
546
        cy.get(".flatpickr-day.booking-loan-boundary").each($el => {
570
        cy.get(".flatpickr-day.booking-loan-boundary").each($el => {
547
            const ariaLabel = $el.attr("aria-label");
571
            const ariaLabel = $el.attr("aria-label");
548
            const date = dayjs(ariaLabel, "MMMM D, YYYY");
572
            const date = dayjs(ariaLabel, "MMMM D, YYYY");
Lines 745-755 describe("Booking Modal Date Picker Tests", () => { Link Here
745
        // ========================================================================
769
        // ========================================================================
746
        cy.log("=== PHASE 3: Lead period conflicts ===");
770
        cy.log("=== PHASE 3: Lead period conflicts ===");
747
771
748
        // Hover June 11 - Lead period (June 9-10), June 9 is past
772
        // Hover June 14 - Lead period (June 12-13), both after today, no booking conflict
749
        // Vue version only disables when lead period has BOOKING conflicts,
773
        // Note: June 11 would be disabled by minimum advance booking (today+leadDays=June 12),
750
        // not when lead dates are in the past. No bookings on June 9-10.
774
        // so we test with June 14 which is past the minimum advance period.
751
        hoverDateByISO("2026-06-11");
775
        hoverDateByISO("2026-06-14");
752
        getDateByISO("2026-06-11").should(
776
        getDateByISO("2026-06-14").should(
753
            "not.have.class",
777
            "not.have.class",
754
            "flatpickr-disabled"
778
            "flatpickr-disabled"
755
        );
779
        );
Lines 1523-1577 describe("Booking Modal Date Picker Tests", () => { Link Here
1523
                        .should("have.class", "flatpickr-disabled");
1547
                        .should("have.class", "flatpickr-disabled");
1524
1548
1525
                    // ================================================================
1549
                    // ================================================================
1526
                    // SCENARIO 3: Visual feedback - booking marker dots for trail period
1550
                    // SCENARIO 3: Visual feedback - trail period hover classes
1527
                    // ================================================================
1551
                    // ================================================================
1528
                    cy.log(
1552
                    cy.log(
1529
                        "=== Scenario 3: Visual feedback - Trail period marker dots ==="
1553
                        "=== Scenario 3: Visual feedback - Trail period hover classes ==="
1530
                    );
1554
                    );
1531
1555
1532
                    // Days 13-14 should have trail marker dots
1556
                    // Hover a date whose trail overlaps with bookings
1557
                    // Day 13 trail should get hover-trail class on hover
1533
                    cy.get("@flatpickrInput2")
1558
                    cy.get("@flatpickrInput2")
1534
                        .getFlatpickrDate(today.add(13, "day").toDate())
1559
                        .hoverFlatpickrDate(today.add(13, "day").toDate());
1535
                        .within(() => {
1536
                            cy.get(
1537
                                ".booking-marker-grid .booking-marker-dot"
1538
                            ).should("exist");
1539
                        });
1540
1541
                    cy.get("@flatpickrInput2")
1560
                    cy.get("@flatpickrInput2")
1542
                        .getFlatpickrDate(today.add(14, "day").toDate())
1561
                        .getFlatpickrDate(today.add(13, "day").toDate())
1543
                        .within(() => {
1562
                        .should("have.class", "booking-day--hover-trail");
1544
                            cy.get(
1545
                                ".booking-marker-grid .booking-marker-dot"
1546
                            ).should("exist");
1547
                        });
1548
1563
1549
                    // ================================================================
1564
                    // ================================================================
1550
                    // SCENARIO 4: Visual feedback - booking marker dots for lead period
1565
                    // SCENARIO 4: Visual feedback - lead period hover classes
1551
                    // ================================================================
1566
                    // ================================================================
1552
                    cy.log(
1567
                    cy.log(
1553
                        "=== Scenario 4: Visual feedback - Lead period marker dots ==="
1568
                        "=== Scenario 4: Visual feedback - Lead period hover classes ==="
1554
                    );
1569
                    );
1555
1570
1571
                    // Hover a date whose lead period overlaps with bookings
1556
                    cy.get("@flatpickrInput2")
1572
                    cy.get("@flatpickrInput2")
1557
                        .hoverFlatpickrDate(today.add(5, "day").toDate());
1573
                        .hoverFlatpickrDate(today.add(8, "day").toDate());
1558
1559
                    // Days 8-9 should have lead marker dots
1560
                    cy.get("@flatpickrInput2")
1574
                    cy.get("@flatpickrInput2")
1561
                        .getFlatpickrDate(today.add(8, "day").toDate())
1575
                        .getFlatpickrDate(today.add(8, "day").toDate())
1562
                        .within(() => {
1576
                        .should("have.class", "booking-day--hover-lead");
1563
                            cy.get(
1564
                                ".booking-marker-grid .booking-marker-dot"
1565
                            ).should("exist");
1566
                        });
1567
1568
                    cy.get("@flatpickrInput2")
1569
                        .getFlatpickrDate(today.add(9, "day").toDate())
1570
                        .within(() => {
1571
                            cy.get(
1572
                                ".booking-marker-grid .booking-marker-dot"
1573
                            ).should("exist");
1574
                        });
1575
                });
1577
                });
1576
            });
1578
            });
1577
1579
(-)a/t/cypress/support/e2e.js (-13 lines)
Lines 55-73 Cypress.on("window:before:load", win => { Link Here
55
    };
55
    };
56
});
56
});
57
57
58
// Handle common application errors gracefully in booking modal tests
59
// This prevents test failures from known JS errors that don't affect functionality
60
Cypress.on("uncaught:exception", (err, runnable) => {
61
    // Return false to prevent the error from failing the test
62
    // These errors can occur when the booking modal JS has timing issues
63
    if (
64
        err.message.includes("Cannot read properties of undefined") ||
65
        err.message.includes("Cannot convert undefined or null to object")
66
    ) {
67
        return false;
68
    }
69
    return true;
70
});
71
58
72
function get_fallback_login_value(param) {
59
function get_fallback_login_value(param) {
73
    var env_var = param == "username" ? "KOHA_USER" : "KOHA_PASS";
60
    var env_var = param == "username" ? "KOHA_USER" : "KOHA_PASS";
(-)a/t/cypress/support/flatpickr.js (-21 / +25 lines)
Lines 322-328 Cypress.Commands.add( Link Here
322
                        );
322
                        );
323
                    }
323
                    }
324
324
325
                    // Select start date - use native click to avoid DOM detachment from Vue re-renders
325
                    // Click start date
326
                    return ensureDateIsVisible(
326
                    return ensureDateIsVisible(
327
                        startDayjsDate,
327
                        startDayjsDate,
328
                        $input,
328
                        $input,
Lines 332-352 Cypress.Commands.add( Link Here
332
                            .should("be.visible")
332
                            .should("be.visible")
333
                            .then($el => $el[0].click());
333
                            .then($el => $el[0].click());
334
334
335
                        // Wait for complex date recalculations (e.g., booking availability) to complete
335
                        // Validate start date registered via instance state
336
                        cy.get(
336
                        cy.wrap($input).should($el => {
337
                            _getFlatpickrDateSelector(startDayjsDate)
337
                            const fp = $el[0]._flatpickr;
338
                        ).should(
338
                            expect(fp.selectedDates).to.have.length(1);
339
                            $el => {
339
                            expect(
340
                                expect($el).to.have.class("selected");
340
                                dayjs(fp.selectedDates[0]).format("YYYY-MM-DD")
341
                                expect($el).to.have.class("startRange");
341
                            ).to.eq(startDayjsDate.format("YYYY-MM-DD"));
342
                            },
342
                        });
343
                            { timeout: 5000 }
344
                        );
345
343
346
                        // Ensure calendar stays open
344
                        // Wait for async side effects to settle — the booking
347
                        cy.get(".flatpickr-calendar.open").should("be.visible");
345
                        // modal's onChange handler schedules a delayed calendar
346
                        // navigation (navigateCalendarIfNeeded, 100ms) that can
347
                        // jump the view to a different month.
348
                        // eslint-disable-next-line cypress/no-unnecessary-waiting
349
                        cy.wait(150);
348
350
349
                        // Navigate to end date and select it
351
                        // Navigate to end date (may need to switch months if
352
                        // the calendar navigated away) and click it
350
                        return ensureDateIsVisible(
353
                        return ensureDateIsVisible(
351
                            endDayjsDate,
354
                            endDayjsDate,
352
                            $input,
355
                            $input,
Lines 356-367 Cypress.Commands.add( Link Here
356
                                .should("be.visible")
359
                                .should("be.visible")
357
                                .then($el => $el[0].click());
360
                                .then($el => $el[0].click());
358
361
359
                            cy.get(".flatpickr-calendar.open").should(
362
                            // Validate range completed via instance state
360
                                "not.exist",
361
                                { timeout: 5000 }
362
                            );
363
364
                            // Validate via flatpickr instance (format-agnostic)
365
                            cy.wrap($input).should($el => {
363
                            cy.wrap($input).should($el => {
366
                                const fp = $el[0]._flatpickr;
364
                                const fp = $el[0]._flatpickr;
367
                                expect(fp.selectedDates.length).to.eq(2);
365
                                expect(fp.selectedDates.length).to.eq(2);
Lines 369-374 Cypress.Commands.add( Link Here
369
                                expect(dayjs(fp.selectedDates[1]).format("YYYY-MM-DD")).to.eq(endDayjsDate.format("YYYY-MM-DD"));
367
                                expect(dayjs(fp.selectedDates[1]).format("YYYY-MM-DD")).to.eq(endDayjsDate.format("YYYY-MM-DD"));
370
                            });
368
                            });
371
369
370
                            // Close calendar if it stayed open
371
                            cy.get("body").then($body => {
372
                                if ($body.find(".flatpickr-calendar.open").length > 0) {
373
                                    $input[0]._flatpickr.close();
374
                                }
375
                            });
376
372
                            return cy.wrap($input);
377
                            return cy.wrap($input);
373
                        });
378
                        });
374
                    });
379
                    });
375
- 

Return to bug 41129