|
Lines 5-10
dayjs.extend(isSameOrBefore);
Link Here
|
| 5 |
describe("Booking Modal Date Picker Tests", () => { |
5 |
describe("Booking Modal Date Picker Tests", () => { |
| 6 |
let testData = {}; |
6 |
let testData = {}; |
| 7 |
|
7 |
|
|
|
8 |
// Prevent unhandled app errors (e.g. failed API calls during cleanup) from failing tests |
| 9 |
Cypress.on("uncaught:exception", () => false); |
| 10 |
|
| 8 |
// Ensure RESTBasicAuth is enabled before running tests |
11 |
// Ensure RESTBasicAuth is enabled before running tests |
| 9 |
before(() => { |
12 |
before(() => { |
| 10 |
cy.task("query", { |
13 |
cy.task("query", { |
|
Lines 24-43
describe("Booking Modal Date Picker Tests", () => {
Link Here
|
| 24 |
testData = objects; |
27 |
testData = objects; |
| 25 |
|
28 |
|
| 26 |
// Update items to be bookable with predictable itemtypes |
29 |
// Update items to be bookable with predictable itemtypes |
| 27 |
const itemUpdates = [ |
30 |
return cy.task("query", { |
| 28 |
// First item: BK (Books) |
31 |
sql: "UPDATE items SET bookable = 1, itype = 'BK', homebranch = 'CPL', enumchron = 'A', dateaccessioned = '2024-12-03' WHERE itemnumber = ?", |
| 29 |
cy.task("query", { |
32 |
values: [objects.items[0].item_id], |
| 30 |
sql: "UPDATE items SET bookable = 1, itype = 'BK', homebranch = 'CPL', enumchron = 'A', dateaccessioned = '2024-12-03' WHERE itemnumber = ?", |
33 |
}).then(() => cy.task("query", { |
| 31 |
values: [objects.items[0].item_id], |
34 |
sql: "UPDATE items SET bookable = 1, itype = 'CF', homebranch = 'CPL', enumchron = 'B', dateaccessioned = '2024-12-02' WHERE itemnumber = ?", |
| 32 |
}), |
35 |
values: [objects.items[1].item_id], |
| 33 |
// Second item: CF (Computer Files) |
36 |
})); |
| 34 |
cy.task("query", { |
|
|
| 35 |
sql: "UPDATE items SET bookable = 1, itype = 'CF', homebranch = 'CPL', enumchron = 'B', dateaccessioned = '2024-12-02' WHERE itemnumber = ?", |
| 36 |
values: [objects.items[1].item_id], |
| 37 |
}), |
| 38 |
]; |
| 39 |
|
| 40 |
return Promise.all(itemUpdates); |
| 41 |
}) |
37 |
}) |
| 42 |
.then(() => { |
38 |
.then(() => { |
| 43 |
// Create a test patron using upstream pattern |
39 |
// Create a test patron using upstream pattern |
|
Lines 101-143
describe("Booking Modal Date Picker Tests", () => {
Link Here
|
| 101 |
); |
97 |
); |
| 102 |
|
98 |
|
| 103 |
// Open the modal |
99 |
// Open the modal |
| 104 |
cy.get('[data-bs-target="#placeBookingModal"]').first().click(); |
100 |
cy.get("booking-modal-island .modal").should("exist"); |
| 105 |
cy.get("#placeBookingModal").should("be.visible"); |
101 |
cy.get("[data-booking-modal]").first().then($btn => $btn[0].click()); |
|
|
102 |
cy.get("booking-modal-island .modal", { timeout: 10000 }).should( |
| 103 |
"be.visible" |
| 104 |
); |
| 106 |
|
105 |
|
| 107 |
// Fill required fields to enable item selection |
106 |
// Fill required fields to enable item selection |
| 108 |
cy.selectFromSelect2( |
107 |
cy.vueSelect( |
| 109 |
"#booking_patron_id", |
108 |
"booking_patron", |
| 110 |
`${testData.patron.surname}, ${testData.patron.firstname}`, |
109 |
testData.patron.cardnumber, |
| 111 |
testData.patron.cardnumber |
110 |
`${testData.patron.surname} ${testData.patron.firstname}` |
| 112 |
); |
111 |
); |
| 113 |
cy.wait("@getPickupLocations"); |
112 |
cy.wait("@getPickupLocations"); |
| 114 |
|
113 |
|
| 115 |
cy.get("#pickup_library_id").should("not.be.disabled"); |
114 |
cy.vueSelectShouldBeEnabled("pickup_library_id"); |
| 116 |
cy.selectFromSelect2ByIndex("#pickup_library_id", 0); |
115 |
cy.vueSelectByIndex("pickup_library_id", 0); |
| 117 |
|
116 |
|
| 118 |
// Only auto-select item if not overridden |
117 |
// Only auto-select item if not overridden |
| 119 |
if (options.skipItemSelection !== true) { |
118 |
if (options.skipItemSelection !== true) { |
| 120 |
cy.get("#booking_item_id").should("not.be.disabled"); |
119 |
cy.vueSelectShouldBeEnabled("booking_item_id"); |
| 121 |
cy.selectFromSelect2ByIndex("#booking_item_id", 1); // Select first item |
120 |
cy.vueSelectByIndex("booking_item_id", 1); // Select second item (CF) |
| 122 |
cy.wait("@getCirculationRules"); |
121 |
cy.wait("@getCirculationRules"); |
| 123 |
|
122 |
|
| 124 |
// Verify date picker is now enabled |
123 |
// Verify date picker is now enabled |
| 125 |
cy.get("#period").should("not.be.disabled"); |
124 |
cy.get("#booking_period").should("not.be.disabled"); |
| 126 |
} |
125 |
} |
| 127 |
}; |
126 |
}; |
| 128 |
|
127 |
|
| 129 |
it("should initialize flatpickr with correct future-date constraints", () => { |
128 |
it("should initialize flatpickr with correct future-date constraints", () => { |
| 130 |
setupModalForDateTesting(); |
129 |
setupModalForDateTesting(); |
| 131 |
|
130 |
|
| 132 |
// Verify flatpickr is initialized with future-date attribute |
|
|
| 133 |
cy.get("#period").should( |
| 134 |
"have.attr", |
| 135 |
"data-flatpickr-futuredate", |
| 136 |
"true" |
| 137 |
); |
| 138 |
|
| 139 |
// Set up the flatpickr alias and open the calendar |
131 |
// Set up the flatpickr alias and open the calendar |
| 140 |
cy.get("#period").as("flatpickrInput"); |
132 |
cy.get("#booking_period").as("flatpickrInput"); |
| 141 |
cy.get("@flatpickrInput").openFlatpickr(); |
133 |
cy.get("@flatpickrInput").openFlatpickr(); |
| 142 |
|
134 |
|
| 143 |
// Verify past dates are disabled |
135 |
// Verify past dates are disabled |
|
Lines 183-190
describe("Booking Modal Date Picker Tests", () => {
Link Here
|
| 183 |
]; |
175 |
]; |
| 184 |
|
176 |
|
| 185 |
// Create existing bookings in the database for the same item we'll test with |
177 |
// Create existing bookings in the database for the same item we'll test with |
| 186 |
const bookingInsertPromises = existingBookings.map(booking => { |
178 |
existingBookings.forEach(booking => { |
| 187 |
return cy.task("query", { |
179 |
cy.task("query", { |
| 188 |
sql: `INSERT INTO bookings (biblio_id, item_id, patron_id, start_date, end_date, pickup_library_id, status) |
180 |
sql: `INSERT INTO bookings (biblio_id, item_id, patron_id, start_date, end_date, pickup_library_id, status) |
| 189 |
VALUES (?, ?, ?, ?, ?, ?, '1')`, |
181 |
VALUES (?, ?, ?, ?, ?, ?, '1')`, |
| 190 |
values: [ |
182 |
values: [ |
|
Lines 198-219
describe("Booking Modal Date Picker Tests", () => {
Link Here
|
| 198 |
}); |
190 |
}); |
| 199 |
}); |
191 |
}); |
| 200 |
|
192 |
|
| 201 |
// Wait for all bookings to be created |
|
|
| 202 |
cy.wrap(Promise.all(bookingInsertPromises)); |
| 203 |
|
| 204 |
// Setup modal but skip auto-item selection so we can control which item to select |
193 |
// Setup modal but skip auto-item selection so we can control which item to select |
| 205 |
setupModalForDateTesting({ skipItemSelection: true }); |
194 |
setupModalForDateTesting({ skipItemSelection: true }); |
| 206 |
|
195 |
|
| 207 |
// Select the specific item that has the existing bookings |
196 |
// Select the specific item that has the existing bookings (by barcode, not index) |
| 208 |
cy.get("#booking_item_id").should("not.be.disabled"); |
197 |
cy.vueSelectShouldBeEnabled("booking_item_id"); |
| 209 |
cy.selectFromSelect2ByIndex("#booking_item_id", 1); // Select first actual item (not "Any item") |
198 |
cy.vueSelect("booking_item_id", testData.items[0].external_id, testData.items[0].external_id); |
| 210 |
cy.wait("@getCirculationRules"); |
199 |
cy.wait("@getCirculationRules"); |
| 211 |
|
200 |
|
| 212 |
// Verify date picker is now enabled |
201 |
// Verify date picker is now enabled |
| 213 |
cy.get("#period").should("not.be.disabled"); |
202 |
cy.get("#booking_period").should("not.be.disabled"); |
| 214 |
|
203 |
|
| 215 |
// Set up flatpickr alias and open the calendar |
204 |
// Set up flatpickr alias and open the calendar |
| 216 |
cy.get("#period").as("flatpickrInput"); |
205 |
cy.get("#booking_period").as("flatpickrInput"); |
| 217 |
cy.get("@flatpickrInput").openFlatpickr(); |
206 |
cy.get("@flatpickrInput").openFlatpickr(); |
| 218 |
|
207 |
|
| 219 |
cy.log( |
208 |
cy.log( |
|
Lines 310-327
describe("Booking Modal Date Picker Tests", () => {
Link Here
|
| 310 |
cy.log( |
299 |
cy.log( |
| 311 |
"=== PHASE 4: Testing different item bookings don't conflict ===" |
300 |
"=== PHASE 4: Testing different item bookings don't conflict ===" |
| 312 |
); |
301 |
); |
| 313 |
/* |
|
|
| 314 |
* DIFFERENT ITEM BOOKING TEST: |
| 315 |
* ============================ |
| 316 |
* Day: 34 35 36 37 38 39 40 41 42 |
| 317 |
* Our Item (Item 1): O O O O O O O O O |
| 318 |
* Other Item (Item 2): - X X X X X X - - |
| 319 |
* ^^^^^^^^^^^^^^^^^ |
| 320 |
* Different item booking |
| 321 |
* |
| 322 |
* Expected: Days 35-40 should be AVAILABLE for our item even though |
| 323 |
* they're booked for a different item (Item 2) |
| 324 |
*/ |
| 325 |
|
302 |
|
| 326 |
// Create a booking for the OTHER item (different from the one we're testing) |
303 |
// Create a booking for the OTHER item (different from the one we're testing) |
| 327 |
const differentItemBooking = { |
304 |
const differentItemBooking = { |
|
Lines 391-410
describe("Booking Modal Date Picker Tests", () => {
Link Here
|
| 391 |
const startDate = dayjs().add(2, "day"); |
368 |
const startDate = dayjs().add(2, "day"); |
| 392 |
const endDate = dayjs().add(5, "day"); |
369 |
const endDate = dayjs().add(5, "day"); |
| 393 |
|
370 |
|
| 394 |
cy.get("#period").selectFlatpickrDateRange(startDate, endDate); |
371 |
cy.get("#booking_period").selectFlatpickrDateRange(startDate, endDate); |
| 395 |
|
372 |
|
| 396 |
// Verify the dates were accepted (check that dates were set) |
373 |
// Verify the dates were accepted (check that period field has value) |
| 397 |
cy.get("#booking_start_date").should("not.have.value", ""); |
374 |
cy.get("#booking_period").should("not.have.value", ""); |
| 398 |
cy.get("#booking_end_date").should("not.have.value", ""); |
|
|
| 399 |
|
375 |
|
| 400 |
// Try to submit - should succeed with valid dates |
376 |
// Try to submit - should succeed with valid dates |
| 401 |
cy.get("#placeBookingForm button[type='submit']") |
377 |
cy.get('button[form="form-booking"][type="submit"]') |
| 402 |
.should("not.be.disabled") |
378 |
.should("not.be.disabled") |
| 403 |
.click(); |
379 |
.click(); |
| 404 |
|
380 |
|
| 405 |
// Should either succeed (modal closes) or show specific validation error |
381 |
// Should either succeed (modal closes) or show specific validation error |
| 406 |
cy.get("body").then($body => { |
382 |
cy.get("body").then($body => { |
| 407 |
if ($body.find("#placeBookingModal:visible").length > 0) { |
383 |
if ( |
|
|
384 |
$body.find("booking-modal-island .modal:visible").length > 0 |
| 385 |
) { |
| 408 |
// If modal is still visible, check for validation messages |
386 |
// If modal is still visible, check for validation messages |
| 409 |
cy.log( |
387 |
cy.log( |
| 410 |
"Modal still visible - checking for validation feedback" |
388 |
"Modal still visible - checking for validation feedback" |
|
Lines 428-434
describe("Booking Modal Date Picker Tests", () => {
Link Here
|
| 428 |
* 1. Maximum date calculation and enforcement [issue period + (renewal period * max renewals)] |
406 |
* 1. Maximum date calculation and enforcement [issue period + (renewal period * max renewals)] |
| 429 |
* 2. Bold date styling for issue length and renewal lengths |
407 |
* 2. Bold date styling for issue length and renewal lengths |
| 430 |
* 3. Date selection limits based on circulation rules |
408 |
* 3. Date selection limits based on circulation rules |
| 431 |
* 4. Visual feedback for different booking period phases |
|
|
| 432 |
* |
409 |
* |
| 433 |
* CIRCULATION RULES DATE CALCULATION: |
410 |
* CIRCULATION RULES DATE CALCULATION: |
| 434 |
* ================================== |
411 |
* ================================== |
|
Lines 438-465
describe("Booking Modal Date Picker Tests", () => {
Link Here
|
| 438 |
* - Renewals Allowed: 3 renewals |
415 |
* - Renewals Allowed: 3 renewals |
| 439 |
* - Renewal Period: 5 days each |
416 |
* - Renewal Period: 5 days each |
| 440 |
* - Total Maximum Period: 10 + (3 × 5) = 25 days |
417 |
* - Total Maximum Period: 10 + (3 × 5) = 25 days |
| 441 |
* |
|
|
| 442 |
* Clear Zone Date Layout (Starting Day 50): |
| 443 |
* ========================================== |
| 444 |
* Day: 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 |
| 445 |
* Period: O O S I I I I I I I I I R1 R1 R1 R1 R1 R2 R2 R2 R2 R2 R3 R3 R3 R3 R3 E O |
| 446 |
* ↑ ↑ ↑ ↑ ↑ ↑ ↑ |
| 447 |
* │ │ │ │ │ │ │ |
| 448 |
* │ └─ Start Date (Day 50) │ │ │ │ └─ Available (after max) |
| 449 |
* └─ Available (before start) │ │ │ └─ Max Date (Day 75) |
| 450 |
* │ │ └─ Renewal 3 Period (Days 70-74) |
| 451 |
* │ └─ Renewal 2 Period (Days 65-69) |
| 452 |
* └─ Renewal 1 Period (Days 60-64) |
| 453 |
* |
| 454 |
* Expected Visual Styling: |
| 455 |
* - Day 50: Bold (start date) |
| 456 |
* - Day 59: Bold (issue period) |
| 457 |
* - Day 64: Bold (renewal 1 period) |
| 458 |
* - Day 69: Bold (renewal 2 period) |
| 459 |
* - Day 75: Bold (renewal 3 period, Max selectable date) |
| 460 |
* - Day 76+: Not selectable (beyond max date) |
| 461 |
* |
| 462 |
* Legend: S = Start, I = Issue, R1/R2/R3 = Renewal periods, E = End, O = Available |
| 463 |
*/ |
418 |
*/ |
| 464 |
|
419 |
|
| 465 |
const today = dayjs().startOf("day"); |
420 |
const today = dayjs().startOf("day"); |
|
Lines 481-492
describe("Booking Modal Date Picker Tests", () => {
Link Here
|
| 481 |
setupModalForDateTesting({ skipItemSelection: true }); |
436 |
setupModalForDateTesting({ skipItemSelection: true }); |
| 482 |
|
437 |
|
| 483 |
// Select item to get circulation rules |
438 |
// Select item to get circulation rules |
| 484 |
cy.get("#booking_item_id").should("not.be.disabled"); |
439 |
cy.vueSelectShouldBeEnabled("booking_item_id"); |
| 485 |
cy.selectFromSelect2ByIndex("#booking_item_id", 1); |
440 |
cy.vueSelectByIndex("booking_item_id", 1); |
| 486 |
cy.wait("@getDateTestRules"); |
441 |
cy.wait("@getDateTestRules"); |
| 487 |
|
442 |
|
| 488 |
cy.get("#period").should("not.be.disabled"); |
443 |
cy.get("#booking_period").should("not.be.disabled"); |
| 489 |
cy.get("#period").as("dateTestFlatpickr"); |
444 |
cy.get("#booking_period").as("dateTestFlatpickr"); |
| 490 |
cy.get("@dateTestFlatpickr").openFlatpickr(); |
445 |
cy.get("@dateTestFlatpickr").openFlatpickr(); |
| 491 |
|
446 |
|
| 492 |
// ======================================================================== |
447 |
// ======================================================================== |
|
Lines 496-508
describe("Booking Modal Date Picker Tests", () => {
Link Here
|
| 496 |
"=== TEST 1: Testing maximum date calculation and enforcement ===" |
451 |
"=== TEST 1: Testing maximum date calculation and enforcement ===" |
| 497 |
); |
452 |
); |
| 498 |
|
453 |
|
| 499 |
/* |
|
|
| 500 |
* Maximum Date Calculation Test: |
| 501 |
* - Max period = issue (10) + renewals (3 × 5) = 25 days total |
| 502 |
* - If start date is Day 50, max end date should be Day 75 (50 + 25) |
| 503 |
* - Dates beyond Day 75 should not be selectable |
| 504 |
*/ |
| 505 |
|
| 506 |
// Test in clear zone starting at Day 50 to avoid conflicts |
454 |
// Test in clear zone starting at Day 50 to avoid conflicts |
| 507 |
const clearZoneStart = today.add(50, "day"); |
455 |
const clearZoneStart = today.add(50, "day"); |
| 508 |
const calculatedMaxDate = clearZoneStart.add( |
456 |
const calculatedMaxDate = clearZoneStart.add( |
|
Lines 554-571
describe("Booking Modal Date Picker Tests", () => {
Link Here
|
| 554 |
"=== TEST 2: Testing bold date styling for issue and renewal periods ===" |
502 |
"=== TEST 2: Testing bold date styling for issue and renewal periods ===" |
| 555 |
); |
503 |
); |
| 556 |
|
504 |
|
| 557 |
/* |
505 |
// Vue version uses "booking-loan-boundary" class instead of "title" |
| 558 |
* Bold Date Styling Test: |
|
|
| 559 |
* Bold dates appear at circulation period endpoints to indicate |
| 560 |
* when issue/renewal periods end. We test the "title" class |
| 561 |
* applied to these specific dates. |
| 562 |
*/ |
| 563 |
|
| 564 |
// Calculate expected bold dates based on circulation rules (like original test) |
| 565 |
// Bold dates occur at: the start date itself, plus period endpoints |
| 566 |
const expectedBoldDates = []; |
506 |
const expectedBoldDates = []; |
| 567 |
|
507 |
|
| 568 |
// Start date is always bold (see place_booking.js boldDates = [new Date(startDate)]) |
508 |
// Start date is always bold |
| 569 |
expectedBoldDates.push(clearZoneStart); |
509 |
expectedBoldDates.push(clearZoneStart); |
| 570 |
|
510 |
|
| 571 |
// Issue period end (after issuelength days) |
511 |
// Issue period end (after issuelength days) |
|
Lines 587-593
describe("Booking Modal Date Picker Tests", () => {
Link Here
|
| 587 |
`Expected bold dates: ${expectedBoldDates.map(d => d.format("YYYY-MM-DD")).join(", ")}` |
527 |
`Expected bold dates: ${expectedBoldDates.map(d => d.format("YYYY-MM-DD")).join(", ")}` |
| 588 |
); |
528 |
); |
| 589 |
|
529 |
|
| 590 |
// Test each expected bold date has the "title" class (like original test) |
530 |
// Test each expected bold date has the "booking-loan-boundary" class |
| 591 |
expectedBoldDates.forEach(boldDate => { |
531 |
expectedBoldDates.forEach(boldDate => { |
| 592 |
if ( |
532 |
if ( |
| 593 |
boldDate.month() === clearZoneStart.month() || |
533 |
boldDate.month() === clearZoneStart.month() || |
|
Lines 595-609
describe("Booking Modal Date Picker Tests", () => {
Link Here
|
| 595 |
) { |
535 |
) { |
| 596 |
cy.get("@dateTestFlatpickr") |
536 |
cy.get("@dateTestFlatpickr") |
| 597 |
.getFlatpickrDate(boldDate.toDate()) |
537 |
.getFlatpickrDate(boldDate.toDate()) |
| 598 |
.should("have.class", "title"); |
538 |
.should("have.class", "booking-loan-boundary"); |
| 599 |
cy.log( |
539 |
cy.log( |
| 600 |
`✓ Day ${boldDate.format("YYYY-MM-DD")}: Has 'title' class (bold)` |
540 |
`✓ Day ${boldDate.format("YYYY-MM-DD")}: Has 'booking-loan-boundary' class (bold)` |
| 601 |
); |
541 |
); |
| 602 |
} |
542 |
} |
| 603 |
}); |
543 |
}); |
| 604 |
|
544 |
|
| 605 |
// Verify that only expected dates are bold (have "title" class) |
545 |
// Verify that only expected dates are bold (have "booking-loan-boundary" class) |
| 606 |
cy.get(".flatpickr-day.title").each($el => { |
546 |
cy.get(".flatpickr-day.booking-loan-boundary").each($el => { |
| 607 |
const ariaLabel = $el.attr("aria-label"); |
547 |
const ariaLabel = $el.attr("aria-label"); |
| 608 |
const date = dayjs(ariaLabel, "MMMM D, YYYY"); |
548 |
const date = dayjs(ariaLabel, "MMMM D, YYYY"); |
| 609 |
const isExpected = expectedBoldDates.some(expected => |
549 |
const isExpected = expectedBoldDates.some(expected => |
|
Lines 623-660
describe("Booking Modal Date Picker Tests", () => {
Link Here
|
| 623 |
"=== TEST 3: Testing date range selection within circulation limits ===" |
563 |
"=== TEST 3: Testing date range selection within circulation limits ===" |
| 624 |
); |
564 |
); |
| 625 |
|
565 |
|
| 626 |
/* |
|
|
| 627 |
* Range Selection Test: |
| 628 |
* - Should be able to select valid range within max period |
| 629 |
* - Should accept full maximum range (25 days) |
| 630 |
* - Should populate start/end date fields correctly |
| 631 |
*/ |
| 632 |
|
| 633 |
// Clear the flatpickr selection from previous tests |
566 |
// Clear the flatpickr selection from previous tests |
| 634 |
cy.get("#period").clearFlatpickr(); |
567 |
cy.get("#booking_period").clearFlatpickr(); |
| 635 |
|
568 |
|
| 636 |
// Test selecting a mid-range period (issue + 1 renewal = 15 days) |
569 |
// Test selecting a mid-range period (issue + 1 renewal = 15 days) |
| 637 |
const midRangeEnd = clearZoneStart.add(15, "day"); |
570 |
const midRangeEnd = clearZoneStart.add(15, "day"); |
| 638 |
|
571 |
|
| 639 |
cy.get("#period").selectFlatpickrDateRange(clearZoneStart, midRangeEnd); |
572 |
cy.get("#booking_period").selectFlatpickrDateRange( |
|
|
573 |
clearZoneStart, |
| 574 |
midRangeEnd |
| 575 |
); |
| 640 |
|
576 |
|
| 641 |
// Verify dates were accepted |
577 |
// Verify dates were accepted (period field has value) |
| 642 |
cy.get("#booking_start_date").should("not.have.value", ""); |
578 |
cy.get("#booking_period").should("not.have.value", ""); |
| 643 |
cy.get("#booking_end_date").should("not.have.value", ""); |
|
|
| 644 |
|
579 |
|
| 645 |
cy.log( |
580 |
cy.log( |
| 646 |
`✓ Mid-range selection accepted: ${clearZoneStart.format("YYYY-MM-DD")} to ${midRangeEnd.format("YYYY-MM-DD")}` |
581 |
`✓ Mid-range selection accepted: ${clearZoneStart.format("YYYY-MM-DD")} to ${midRangeEnd.format("YYYY-MM-DD")}` |
| 647 |
); |
582 |
); |
| 648 |
|
583 |
|
| 649 |
// Test selecting full maximum range |
584 |
// Test selecting full maximum range |
| 650 |
cy.get("#period").selectFlatpickrDateRange( |
585 |
cy.get("#booking_period").selectFlatpickrDateRange( |
| 651 |
clearZoneStart, |
586 |
clearZoneStart, |
| 652 |
calculatedMaxDate |
587 |
calculatedMaxDate |
| 653 |
); |
588 |
); |
| 654 |
|
589 |
|
| 655 |
// Verify full range was accepted |
590 |
// Verify full range was accepted |
| 656 |
cy.get("#booking_start_date").should("not.have.value", ""); |
591 |
cy.get("#booking_period").should("not.have.value", ""); |
| 657 |
cy.get("#booking_end_date").should("not.have.value", ""); |
|
|
| 658 |
|
592 |
|
| 659 |
cy.log( |
593 |
cy.log( |
| 660 |
`✓ Full maximum range accepted: ${clearZoneStart.format("YYYY-MM-DD")} to ${calculatedMaxDate.format("YYYY-MM-DD")}` |
594 |
`✓ Full maximum range accepted: ${clearZoneStart.format("YYYY-MM-DD")} to ${calculatedMaxDate.format("YYYY-MM-DD")}` |
|
Lines 663-705
describe("Booking Modal Date Picker Tests", () => {
Link Here
|
| 663 |
cy.log( |
597 |
cy.log( |
| 664 |
"✓ CONFIRMED: Circulation rules date calculations and visual feedback working correctly" |
598 |
"✓ CONFIRMED: Circulation rules date calculations and visual feedback working correctly" |
| 665 |
); |
599 |
); |
| 666 |
cy.log( |
|
|
| 667 |
`✓ Validated: ${dateTestCirculationRules.issuelength}-day issue + ${dateTestCirculationRules.renewalsallowed} renewals × ${dateTestCirculationRules.renewalperiod} days = ${dateTestCirculationRules.issuelength + dateTestCirculationRules.renewalsallowed * dateTestCirculationRules.renewalperiod}-day maximum period` |
| 668 |
); |
| 669 |
}); |
600 |
}); |
| 670 |
|
601 |
|
| 671 |
it("should handle lead and trail periods", () => { |
602 |
it("should handle lead and trail periods", () => { |
| 672 |
/** |
603 |
/** |
| 673 |
* Lead and Trail Period Behaviour Tests (with Bidirectional Enhancement) |
604 |
* Lead and Trail Period Behaviour Tests |
| 674 |
* ====================================================================== |
605 |
* ====================================================================== |
| 675 |
* |
606 |
* |
| 676 |
* Test Coverage: |
607 |
* In the Vue version, lead/trail periods are indicated via: |
| 677 |
* 1. Lead period visual hints (CSS classes) in clear zone |
608 |
* - booking-day--hover-lead / booking-day--hover-trail classes on hover |
| 678 |
* 2. Trail period visual hints (CSS classes) in clear zone |
609 |
* - flatpickr-disabled class for dates that cannot be selected |
| 679 |
* 3a. Lead period conflicts with past dates (leadDisable) |
610 |
* - booking-marker-dot--lead / booking-marker-dot--trail for marker dots |
| 680 |
* 3b. Lead period conflicts with existing booking ACTUAL dates (leadDisable) |
|
|
| 681 |
* 3c. NEW BIDIRECTIONAL: Lead period conflicts with existing booking TRAIL period (leadDisable) |
| 682 |
* 4a. Trail period conflicts with existing booking ACTUAL dates (trailDisable) |
| 683 |
* 4b. NEW BIDIRECTIONAL: Trail period conflicts with existing booking LEAD period (trailDisable) |
| 684 |
* 5. Max date selectable when trail period is clear of existing booking |
| 685 |
* |
| 686 |
* CRITICAL ENHANCEMENT: Bidirectional Lead/Trail Period Checking |
| 687 |
* ============================================================== |
| 688 |
* This test validates that lead/trail periods work in BOTH directions: |
| 689 |
* - New booking's LEAD period must not conflict with existing booking's TRAIL period |
| 690 |
* - New booking's TRAIL period must not conflict with existing booking's LEAD period |
| 691 |
* - This ensures full "protected periods" around existing bookings are respected |
| 692 |
* |
611 |
* |
| 693 |
* PROTECTED PERIOD CONCEPT: |
612 |
* The Vue version disables dates with lead/trail conflicts via the |
| 694 |
* ======================== |
613 |
* disable function rather than applying leadDisable/trailDisable classes. |
| 695 |
* Each existing booking has a "protected period" = Lead + Actual + Trail |
|
|
| 696 |
* New bookings must ensure their Lead + Actual + Trail does not overlap |
| 697 |
* with ANY part of existing bookings' protected periods. |
| 698 |
* |
614 |
* |
| 699 |
* Fixed Date Setup: |
615 |
* Fixed Date Setup: |
| 700 |
* ================ |
616 |
* ================ |
| 701 |
* - Today: June 10, 2026 (Wednesday) |
617 |
* - Today: June 10, 2026 (Wednesday) |
| 702 |
* - Timezone: Europe/London |
|
|
| 703 |
* - Lead Period: 2 days |
618 |
* - Lead Period: 2 days |
| 704 |
* - Trail Period: 3 days |
619 |
* - Trail Period: 3 days |
| 705 |
* - Issue Length: 3 days |
620 |
* - Issue Length: 3 days |
|
Lines 708-742
describe("Booking Modal Date Picker Tests", () => {
Link Here
|
| 708 |
* - Max Booking Period: 3 + (2 × 2) = 7 days |
623 |
* - Max Booking Period: 3 + (2 × 2) = 7 days |
| 709 |
* |
624 |
* |
| 710 |
* Blocker Booking: June 25-27, 2026 |
625 |
* Blocker Booking: June 25-27, 2026 |
| 711 |
* - Blocker's LEAD period: June 23-24 (2 days before start) |
|
|
| 712 |
* - Blocker's ACTUAL dates: June 25-27 |
| 713 |
* - Blocker's TRAIL period: June 28-30 (3 days after end) |
| 714 |
* - Total PROTECTED period: June 23-30 |
| 715 |
* |
| 716 |
* Timeline: |
| 717 |
* ========= |
| 718 |
* June/July 2026 |
| 719 |
* Sun Mon Tue Wed Thu Fri Sat |
| 720 |
* 8 9 10 11 12 13 ← 10 = TODAY |
| 721 |
* 14 15 16 17 18 19 20 |
| 722 |
* 21 22 23 24 25 26 27 ← 23-24 = BLOCKER LEAD, 25-27 = BLOCKER ACTUAL |
| 723 |
* 28 29 30 1 2 3 4 ← 28-30 = BLOCKER TRAIL, July 3 = first clear after |
| 724 |
* |
| 725 |
* Test Scenarios: |
| 726 |
* ============== |
| 727 |
* Phase 1: Hover June 13 → Lead June 11-12 (clear) → no leadDisable |
| 728 |
* Phase 2: Select June 13, hover June 16 → Trail June 17-19 (clear) → no trailDisable |
| 729 |
* Phase 3a: Hover June 11 → Lead June 9-10, June 9 is past → leadDisable |
| 730 |
* Phase 3b: Hover June 29 → Lead June 27-28, June 27 is in blocker ACTUAL → leadDisable |
| 731 |
* Phase 3c: NEW - Hover July 1 → Lead June 29-30, overlaps blocker TRAIL → leadDisable |
| 732 |
* Phase 3d: NEW - Hover July 2 → Lead June 30-July 1, June 30 in blocker TRAIL → leadDisable |
| 733 |
* Phase 4a: Select June 20, hover June 23 → Trail June 24-26 overlaps blocker ACTUAL → trailDisable |
| 734 |
* Phase 4b: NEW - Select June 13, hover June 21 → Trail June 22-24, overlaps blocker LEAD → trailDisable |
| 735 |
* Phase 5: Select June 13, hover June 20 (max) → Trail June 21-23 (clear) → selectable |
| 736 |
*/ |
626 |
*/ |
| 737 |
|
627 |
|
| 738 |
// Fix the browser Date object to June 10, 2026 at 09:00 Europe/London |
628 |
// Fix the browser Date object to June 10, 2026 at 09:00 Europe/London |
| 739 |
// Using ["Date"] to avoid freezing timers which breaks Select2 async operations |
|
|
| 740 |
const fixedToday = new Date("2026-06-10T08:00:00Z"); // 09:00 BST (UTC+1) |
629 |
const fixedToday = new Date("2026-06-10T08:00:00Z"); // 09:00 BST (UTC+1) |
| 741 |
cy.clock(fixedToday, ["Date"]); |
630 |
cy.clock(fixedToday, ["Date"]); |
| 742 |
cy.log(`Fixed today: June 10, 2026`); |
631 |
cy.log(`Fixed today: June 10, 2026`); |
|
Lines 765-771
describe("Booking Modal Date Picker Tests", () => {
Link Here
|
| 765 |
|
654 |
|
| 766 |
cy.task("query", { |
655 |
cy.task("query", { |
| 767 |
sql: `INSERT INTO bookings (biblio_id, item_id, patron_id, start_date, end_date, pickup_library_id, status) |
656 |
sql: `INSERT INTO bookings (biblio_id, item_id, patron_id, start_date, end_date, pickup_library_id, status) |
| 768 |
VALUES (?, ?, ?, ?, ?, ?, '1')`, |
657 |
VALUES (?, ?, ?, ?, ?, ?, ?)`, |
| 769 |
values: [ |
658 |
values: [ |
| 770 |
testData.biblio.biblio_id, |
659 |
testData.biblio.biblio_id, |
| 771 |
testData.items[0].item_id, |
660 |
testData.items[0].item_id, |
|
Lines 773-778
describe("Booking Modal Date Picker Tests", () => {
Link Here
|
| 773 |
blockerStart, |
662 |
blockerStart, |
| 774 |
blockerEnd, |
663 |
blockerEnd, |
| 775 |
testData.libraries[0].library_id, |
664 |
testData.libraries[0].library_id, |
|
|
665 |
"new", |
| 776 |
], |
666 |
], |
| 777 |
}); |
667 |
}); |
| 778 |
cy.log(`Blocker booking created: June 25-27, 2026`); |
668 |
cy.log(`Blocker booking created: June 25-27, 2026`); |
|
Lines 780-1079
describe("Booking Modal Date Picker Tests", () => {
Link Here
|
| 780 |
// Setup modal |
670 |
// Setup modal |
| 781 |
setupModalForDateTesting({ skipItemSelection: true }); |
671 |
setupModalForDateTesting({ skipItemSelection: true }); |
| 782 |
|
672 |
|
| 783 |
cy.get("#booking_item_id").should("not.be.disabled"); |
673 |
// Select the item that has the blocker booking (items[0] = index 0) |
| 784 |
cy.selectFromSelect2ByIndex("#booking_item_id", 1); |
674 |
cy.vueSelectShouldBeEnabled("booking_item_id"); |
|
|
675 |
cy.vueSelectByIndex("booking_item_id", 0); |
| 785 |
cy.wait("@getFixedDateRules"); |
676 |
cy.wait("@getFixedDateRules"); |
| 786 |
|
677 |
|
| 787 |
cy.get("#period").should("not.be.disabled"); |
678 |
cy.get("#booking_period").should("not.be.disabled"); |
| 788 |
cy.get("#period").as("fp"); |
679 |
cy.get("#booking_period").as("fp"); |
| 789 |
cy.get("@fp").openFlatpickr(); |
680 |
cy.get("@fp").openFlatpickr(); |
| 790 |
|
681 |
|
| 791 |
// Helper to get a specific date element by ISO date string |
682 |
// Helpers that use native events to avoid detached DOM errors from Vue re-renders |
|
|
683 |
const monthNames = ["January", "February", "March", "April", "May", "June", |
| 684 |
"July", "August", "September", "October", "November", "December"]; |
| 685 |
const getDateSelector = (isoDate: string) => { |
| 686 |
const d = dayjs(isoDate); |
| 687 |
return `.flatpickr-day[aria-label="${monthNames[d.month()]} ${d.date()}, ${d.year()}"]`; |
| 688 |
}; |
| 689 |
const hoverDateByISO = (isoDate: string) => { |
| 690 |
cy.get(getDateSelector(isoDate)) |
| 691 |
.should("be.visible") |
| 692 |
.then($el => { |
| 693 |
$el[0].dispatchEvent(new MouseEvent("mouseover", { bubbles: true })); |
| 694 |
}); |
| 695 |
}; |
| 696 |
const clickDateByISO = (isoDate: string) => { |
| 697 |
cy.get(getDateSelector(isoDate)) |
| 698 |
.should("be.visible") |
| 699 |
.then($el => $el[0].click()); |
| 700 |
}; |
| 792 |
const getDateByISO = (isoDate: string) => { |
701 |
const getDateByISO = (isoDate: string) => { |
| 793 |
const date = new Date(isoDate); |
702 |
const date = new Date(isoDate); |
| 794 |
return cy.get("@fp").getFlatpickrDate(date); |
703 |
return cy.get("@fp").getFlatpickrDate(date); |
| 795 |
}; |
704 |
}; |
| 796 |
|
705 |
|
| 797 |
// ======================================================================== |
706 |
// ======================================================================== |
| 798 |
// PHASE 1: Lead Period Clear - Visual Classes |
707 |
// PHASE 1: Lead Period - Hover shows lead markers |
| 799 |
// ======================================================================== |
708 |
// ======================================================================== |
| 800 |
cy.log("=== PHASE 1: Lead period visual hints in clear zone ==="); |
709 |
cy.log("=== PHASE 1: Lead period visual hints on hover ==="); |
| 801 |
|
|
|
| 802 |
/** |
| 803 |
* Hover June 13 as potential start date |
| 804 |
* Lead period: June 11-12 (both after today June 10, no booking conflict) |
| 805 |
* Expected: leadRangeStart on June 11, leadRange on June 12, no leadDisable on June 13 |
| 806 |
*/ |
| 807 |
|
| 808 |
getDateByISO("2026-06-13").trigger("mouseover"); |
| 809 |
|
| 810 |
// Check lead period classes |
| 811 |
getDateByISO("2026-06-11") |
| 812 |
.should("have.class", "leadRangeStart") |
| 813 |
.and("have.class", "leadRange"); |
| 814 |
|
710 |
|
| 815 |
getDateByISO("2026-06-12") |
711 |
// Hover June 13 as potential start date |
| 816 |
.should("have.class", "leadRange") |
712 |
// Lead period: June 11-12 (both after today June 10, no booking conflict) |
| 817 |
.and("have.class", "leadRangeEnd"); |
713 |
hoverDateByISO("2026-06-13"); |
| 818 |
|
714 |
|
| 819 |
// Hovered date should NOT have leadDisable (lead period is clear) |
715 |
// June 11-12 are clear, so June 13 should NOT be disabled |
| 820 |
getDateByISO("2026-06-13").should("not.have.class", "leadDisable"); |
716 |
getDateByISO("2026-06-13").should( |
|
|
717 |
"not.have.class", |
| 718 |
"flatpickr-disabled" |
| 719 |
); |
| 821 |
|
720 |
|
| 822 |
// ======================================================================== |
721 |
// ======================================================================== |
| 823 |
// PHASE 2: Trail Period Clear - Visual Classes |
722 |
// PHASE 2: Trail Period - Hover shows trail markers |
| 824 |
// ======================================================================== |
723 |
// ======================================================================== |
| 825 |
cy.log("=== PHASE 2: Trail period visual hints in clear zone ==="); |
724 |
cy.log("=== PHASE 2: Trail period visual hints on hover ==="); |
| 826 |
|
725 |
|
| 827 |
/** |
726 |
// Select June 13 as start date |
| 828 |
* Select June 13 as start date (lead June 11-12 is clear) |
727 |
clickDateByISO("2026-06-13"); |
| 829 |
* Then hover June 16 as potential end date |
|
|
| 830 |
* Trail period calculation: trailStart = hoverDate + 1, trailEnd = hoverDate + 3 |
| 831 |
* So: trailStart = June 17, trailEnd = June 19 |
| 832 |
* Classes: June 17 = trailRangeStart + trailRange, June 18 = trailRange, June 19 = trailRange + trailRangeEnd |
| 833 |
*/ |
| 834 |
|
| 835 |
// Select June 13 as start date (same date we just hovered - lead is clear) |
| 836 |
getDateByISO("2026-06-13").click(); |
| 837 |
|
728 |
|
| 838 |
// Hover June 16 as potential end date |
729 |
// Hover June 16 as potential end date |
| 839 |
getDateByISO("2026-06-16").trigger("mouseover"); |
730 |
// Trail period: June 17-19 (clear of any bookings) |
|
|
731 |
hoverDateByISO("2026-06-16"); |
| 840 |
|
732 |
|
| 841 |
// Check trail period classes |
733 |
// June 16 should not be disabled (trail is clear) |
| 842 |
getDateByISO("2026-06-17") |
734 |
getDateByISO("2026-06-16").should( |
| 843 |
.should("have.class", "trailRangeStart") |
735 |
"not.have.class", |
| 844 |
.and("have.class", "trailRange"); |
736 |
"flatpickr-disabled" |
| 845 |
|
737 |
); |
| 846 |
getDateByISO("2026-06-18").should("have.class", "trailRange"); |
|
|
| 847 |
|
| 848 |
getDateByISO("2026-06-19") |
| 849 |
.should("have.class", "trailRangeEnd") |
| 850 |
.and("have.class", "trailRange"); |
| 851 |
|
| 852 |
// Hovered date (June 16) should NOT have trailDisable (trail period is clear) |
| 853 |
getDateByISO("2026-06-16").should("not.have.class", "trailDisable"); |
| 854 |
|
738 |
|
| 855 |
// Clear selection for next phase |
739 |
// Clear selection for next phase |
| 856 |
cy.get("#period").clearFlatpickr(); |
740 |
cy.get("#booking_period").clearFlatpickr(); |
| 857 |
cy.get("@fp").openFlatpickr(); |
741 |
cy.get("@fp").openFlatpickr(); |
| 858 |
|
742 |
|
| 859 |
// ======================================================================== |
743 |
// ======================================================================== |
| 860 |
// PHASE 3: Lead Period Conflict - Past Dates and Existing bookings |
744 |
// PHASE 3: Lead Period Conflict - Existing bookings |
| 861 |
// ======================================================================== |
745 |
// ======================================================================== |
| 862 |
cy.log("=== PHASE 3: Lead period conflicts ==="); |
746 |
cy.log("=== PHASE 3: Lead period conflicts ==="); |
| 863 |
|
747 |
|
| 864 |
/** |
748 |
// Hover June 11 - Lead period (June 9-10), June 9 is past |
| 865 |
* Hover June 11 as potential start date |
749 |
// Vue version only disables when lead period has BOOKING conflicts, |
| 866 |
* Lead period: June 9-10 |
750 |
// not when lead dates are in the past. No bookings on June 9-10. |
| 867 |
* June 9 is in the past (before today June 10) |
751 |
hoverDateByISO("2026-06-11"); |
| 868 |
* Expected: leadDisable on June 11 because lead period extends into past |
752 |
getDateByISO("2026-06-11").should( |
| 869 |
*/ |
753 |
"not.have.class", |
| 870 |
|
754 |
"flatpickr-disabled" |
| 871 |
getDateByISO("2026-06-11").trigger("mouseover"); |
755 |
); |
| 872 |
|
|
|
| 873 |
// June 11 should have leadDisable because lead period (June 9-10) includes past date |
| 874 |
getDateByISO("2026-06-11").should("have.class", "leadDisable"); |
| 875 |
|
| 876 |
/** |
| 877 |
* Hover June 29 as potential start date |
| 878 |
* Lead period: June 27-28 |
| 879 |
* June 27 is in the existing booking (25-27 June) |
| 880 |
* Expected: leadDisable on June 29 because lead period extends into existing booking |
| 881 |
*/ |
| 882 |
|
| 883 |
getDateByISO("2026-06-29").trigger("mouseover"); |
| 884 |
|
756 |
|
| 885 |
// June 29 should have leadDisable because lead period (June 27-28) includes existing booking date |
757 |
// Hover June 29 - Lead period (June 27-28), June 27 is in blocker booking |
| 886 |
getDateByISO("2026-06-29").should("have.class", "leadDisable"); |
758 |
hoverDateByISO("2026-06-29"); |
|
|
759 |
getDateByISO("2026-06-29").should( |
| 760 |
"have.class", |
| 761 |
"flatpickr-disabled" |
| 762 |
); |
| 887 |
|
763 |
|
| 888 |
// ======================================================================== |
764 |
// ======================================================================== |
| 889 |
// PHASE 3c: BIDIRECTIONAL - Lead Period Conflicts with Existing Booking TRAIL |
765 |
// PHASE 3c: BIDIRECTIONAL - Lead Period Conflicts with Existing Booking TRAIL |
| 890 |
// ======================================================================== |
766 |
// ======================================================================== |
| 891 |
|
767 |
|
| 892 |
/** |
768 |
// July 1: Lead June 29-30 → overlaps blocker trail (June 28-30) → DISABLED |
| 893 |
* NEW BIDIRECTIONAL Conflict Scenario: |
769 |
hoverDateByISO("2026-07-01"); |
| 894 |
* Blocker booking end: June 27 |
770 |
getDateByISO("2026-07-01").should( |
| 895 |
* Blocker's TRAIL period: June 28-30 (3 days after end) |
771 |
"have.class", |
| 896 |
* |
772 |
"flatpickr-disabled" |
| 897 |
* Test start dates where NEW booking's lead overlaps with blocker's TRAIL: |
773 |
); |
| 898 |
* - July 1: Lead June 29-30 → June 29-30 are in blocker trail (June 28-30) → DISABLED |
|
|
| 899 |
* - July 2: Lead June 30-July 1 → June 30 is in blocker trail → DISABLED |
| 900 |
* |
| 901 |
* This is the KEY enhancement: respecting existing booking's trail period! |
| 902 |
*/ |
| 903 |
|
| 904 |
// Hover July 1 - lead period (June 29-30) overlaps blocker's trail (June 28-30) |
| 905 |
getDateByISO("2026-07-01").trigger("mouseover"); |
| 906 |
getDateByISO("2026-07-01").should("have.class", "leadDisable"); |
| 907 |
|
774 |
|
| 908 |
// Hover July 2 - lead period (June 30-July 1) still overlaps blocker's trail at June 30 |
775 |
// July 2: Lead June 30-July 1 → June 30 in blocker trail → DISABLED |
| 909 |
getDateByISO("2026-07-02").trigger("mouseover"); |
776 |
hoverDateByISO("2026-07-02"); |
| 910 |
getDateByISO("2026-07-02").should("have.class", "leadDisable"); |
777 |
getDateByISO("2026-07-02").should( |
|
|
778 |
"have.class", |
| 779 |
"flatpickr-disabled" |
| 780 |
); |
| 911 |
|
781 |
|
| 912 |
// ======================================================================== |
782 |
// ======================================================================== |
| 913 |
// PHASE 3d: First Clear Start Date After Blocker's Protected Period |
783 |
// PHASE 3d: First Clear Start Date After Blocker's Protected Period |
| 914 |
// ======================================================================== |
784 |
// ======================================================================== |
| 915 |
|
785 |
|
| 916 |
/** |
786 |
// July 3: Lead July 1-2 → clear of blocker trail → NOT disabled |
| 917 |
* Verify that July 3 is the first selectable start date after blocker: |
787 |
hoverDateByISO("2026-07-03"); |
| 918 |
* - July 3: Lead July 1-2 → completely clear of blocker trail (ends June 30) → no leadDisable |
788 |
getDateByISO("2026-07-03").should( |
| 919 |
*/ |
789 |
"not.have.class", |
| 920 |
|
790 |
"flatpickr-disabled" |
| 921 |
getDateByISO("2026-07-03").trigger("mouseover"); |
791 |
); |
| 922 |
getDateByISO("2026-07-03").should("not.have.class", "leadDisable"); |
|
|
| 923 |
|
792 |
|
| 924 |
// ======================================================================== |
793 |
// ======================================================================== |
| 925 |
// PHASE 4a: Trail Period Conflict - Existing Booking ACTUAL Dates |
794 |
// PHASE 4a: Trail Period Conflict - Existing Booking ACTUAL Dates |
| 926 |
// ======================================================================== |
795 |
// ======================================================================== |
| 927 |
|
796 |
|
| 928 |
/** |
797 |
// Select June 20 as start date (lead June 18-19, both clear) |
| 929 |
* Select June 20 as start date (lead June 18-19, both clear) |
798 |
clickDateByISO("2026-06-20"); |
| 930 |
* Then hover June 23 as potential end date |
|
|
| 931 |
* Trail period: June 24-26 |
| 932 |
* Blocker booking ACTUAL: June 25-27 (partial overlap) |
| 933 |
* Expected: trailDisable on June 23 |
| 934 |
*/ |
| 935 |
|
| 936 |
// Select June 20 as start date |
| 937 |
getDateByISO("2026-06-20").click(); |
| 938 |
|
799 |
|
| 939 |
// Hover June 23 as potential end date |
800 |
// Hover June 23 - Trail (June 24-26) overlaps blocker ACTUAL (June 25-27) |
| 940 |
getDateByISO("2026-06-23").trigger("mouseover"); |
801 |
hoverDateByISO("2026-06-23"); |
| 941 |
|
802 |
getDateByISO("2026-06-23").should( |
| 942 |
// June 23 should have trailDisable because trail period (June 24-26) overlaps blocker ACTUAL (June 25-27) |
803 |
"have.class", |
| 943 |
getDateByISO("2026-06-23").should("have.class", "trailDisable"); |
804 |
"flatpickr-disabled" |
|
|
805 |
); |
| 944 |
|
806 |
|
| 945 |
// Clear selection for next phase |
807 |
// Clear selection for next phase |
| 946 |
cy.get("#period").clearFlatpickr(); |
808 |
cy.get("#booking_period").clearFlatpickr(); |
| 947 |
cy.get("@fp").openFlatpickr(); |
809 |
cy.get("@fp").openFlatpickr(); |
| 948 |
|
810 |
|
| 949 |
// ======================================================================== |
811 |
// ======================================================================== |
| 950 |
// PHASE 4b: BIDIRECTIONAL - Trail Period Conflicts with Existing Booking LEAD |
812 |
// PHASE 4b: BIDIRECTIONAL - Trail Period Conflicts with Existing Booking LEAD |
| 951 |
// ======================================================================== |
813 |
// ======================================================================== |
| 952 |
|
814 |
|
| 953 |
/** |
815 |
// Select June 13 as start (lead June 11-12, both clear) |
| 954 |
* NEW BIDIRECTIONAL Conflict Scenario: |
816 |
clickDateByISO("2026-06-13"); |
| 955 |
* Blocker booking start: June 25 |
|
|
| 956 |
* Blocker's LEAD period: June 23-24 (2 days before start) |
| 957 |
* |
| 958 |
* Test end dates where NEW booking's trail overlaps with blocker's LEAD: |
| 959 |
* - Select June 13 as start, hover June 21 as end |
| 960 |
* - Trail period: June 22-24 (3 days after June 21) |
| 961 |
* - June 23-24 overlap with blocker LEAD (June 23-24) → DISABLED |
| 962 |
* |
| 963 |
* This is the KEY enhancement: respecting existing booking's lead period! |
| 964 |
*/ |
| 965 |
|
| 966 |
// Select June 13 as start date (lead June 11-12, both clear) |
| 967 |
getDateByISO("2026-06-13").click(); |
| 968 |
|
817 |
|
| 969 |
// Hover June 21 as potential end date |
818 |
// Hover June 21 - Trail (June 22-24) overlaps blocker LEAD (June 23-24) → DISABLED |
| 970 |
// Trail period: June 22-24, Blocker LEAD: June 23-24 |
819 |
hoverDateByISO("2026-06-21"); |
| 971 |
// Overlap at June 23-24 → should have trailDisable |
820 |
getDateByISO("2026-06-21").should( |
| 972 |
getDateByISO("2026-06-21").trigger("mouseover"); |
821 |
"have.class", |
| 973 |
getDateByISO("2026-06-21").should("have.class", "trailDisable"); |
822 |
"flatpickr-disabled" |
|
|
823 |
); |
| 974 |
|
824 |
|
| 975 |
// Also test June 20 - trail June 21-23, June 23 overlaps blocker lead |
825 |
// June 20 - Trail (June 21-23), June 23 overlaps blocker lead → DISABLED |
| 976 |
getDateByISO("2026-06-20").trigger("mouseover"); |
826 |
hoverDateByISO("2026-06-20"); |
| 977 |
getDateByISO("2026-06-20").should("have.class", "trailDisable"); |
827 |
getDateByISO("2026-06-20").should( |
|
|
828 |
"have.class", |
| 829 |
"flatpickr-disabled" |
| 830 |
); |
| 978 |
|
831 |
|
| 979 |
// Verify June 19 is clear - trail June 20-22, doesn't reach blocker lead (starts June 23) |
832 |
// June 19 - Trail (June 20-22) doesn't reach blocker lead (starts June 23) → NOT disabled |
| 980 |
getDateByISO("2026-06-19").trigger("mouseover"); |
833 |
hoverDateByISO("2026-06-19"); |
| 981 |
getDateByISO("2026-06-19").should("not.have.class", "trailDisable"); |
834 |
getDateByISO("2026-06-19").should( |
|
|
835 |
"not.have.class", |
| 836 |
"flatpickr-disabled" |
| 837 |
); |
| 982 |
|
838 |
|
| 983 |
// Clear selection for next phase |
839 |
// Clear selection for next phase |
| 984 |
cy.get("#period").clearFlatpickr(); |
840 |
cy.get("#booking_period").clearFlatpickr(); |
| 985 |
cy.get("@fp").openFlatpickr(); |
841 |
cy.get("@fp").openFlatpickr(); |
| 986 |
|
842 |
|
| 987 |
// ======================================================================== |
843 |
// ======================================================================== |
| 988 |
// PHASE 5: Max Date Selectable When Trail is Clear |
844 |
// PHASE 5: Max Date Selectable When Trail is Clear |
| 989 |
// ======================================================================== |
845 |
// ======================================================================== |
| 990 |
|
846 |
|
| 991 |
/** |
|
|
| 992 |
* Select June 13 as start date (lead June 11-12, both clear) |
| 993 |
* Max end date by circulation rules: June 20 (13 + 7 days) |
| 994 |
* But June 20's trail period (June 21-23) overlaps blocker's lead (June 23-24) at June 23 |
| 995 |
* So June 20 WILL have trailDisable |
| 996 |
* |
| 997 |
* June 19's trail period (June 20-22) is clear of blocker's lead (June 23-24) |
| 998 |
* So June 19 should be selectable (no trailDisable) |
| 999 |
*/ |
| 1000 |
|
| 1001 |
// Select June 13 as start date |
847 |
// Select June 13 as start date |
| 1002 |
getDateByISO("2026-06-13").click(); |
848 |
clickDateByISO("2026-06-13"); |
| 1003 |
|
|
|
| 1004 |
// First, verify June 20 HAS trailDisable (trail June 21-23 overlaps blocker lead June 23-24) |
| 1005 |
getDateByISO("2026-06-20").trigger("mouseover"); |
| 1006 |
getDateByISO("2026-06-20").should("have.class", "trailDisable"); |
| 1007 |
|
849 |
|
| 1008 |
// June 19 should NOT have trailDisable (trail June 20-22 is clear of blocker lead) |
850 |
// June 20: trail (June 21-23) overlaps blocker lead (June 23-24) → DISABLED |
| 1009 |
getDateByISO("2026-06-19").trigger("mouseover"); |
851 |
hoverDateByISO("2026-06-20"); |
| 1010 |
getDateByISO("2026-06-19").should("not.have.class", "trailDisable"); |
852 |
getDateByISO("2026-06-20").should( |
|
|
853 |
"have.class", |
| 854 |
"flatpickr-disabled" |
| 855 |
); |
| 1011 |
|
856 |
|
| 1012 |
// June 19 should not be disabled by flatpickr |
857 |
// June 19: trail (June 20-22) clear of blocker lead → NOT disabled |
|
|
858 |
hoverDateByISO("2026-06-19"); |
| 1013 |
getDateByISO("2026-06-19").should( |
859 |
getDateByISO("2026-06-19").should( |
| 1014 |
"not.have.class", |
860 |
"not.have.class", |
| 1015 |
"flatpickr-disabled" |
861 |
"flatpickr-disabled" |
| 1016 |
); |
862 |
); |
| 1017 |
|
863 |
|
| 1018 |
// Actually select June 19 to confirm booking can be made |
864 |
// Actually select June 19 to confirm booking can be made |
| 1019 |
getDateByISO("2026-06-19").click(); |
865 |
clickDateByISO("2026-06-19"); |
| 1020 |
|
866 |
|
| 1021 |
// Verify dates were accepted in the form |
867 |
// Verify dates were accepted in the form |
| 1022 |
cy.get("#booking_start_date").should("not.have.value", ""); |
868 |
cy.get("#booking_period").should("not.have.value", ""); |
| 1023 |
cy.get("#booking_end_date").should("not.have.value", ""); |
|
|
| 1024 |
|
869 |
|
| 1025 |
cy.log( |
870 |
cy.log( |
| 1026 |
"✓ CONFIRMED: Lead/trail period behavior with bidirectional conflict detection working correctly" |
871 |
"✓ CONFIRMED: Lead/trail period behavior with bidirectional conflict detection working correctly" |
| 1027 |
); |
872 |
); |
| 1028 |
}); |
873 |
}); |
| 1029 |
|
874 |
|
| 1030 |
it("should show event dots for dates with existing bookings", () => { |
875 |
it("should show booking marker dots for dates with existing bookings", () => { |
| 1031 |
/** |
876 |
/** |
| 1032 |
* Comprehensive Event Dots Visual Indicator Test |
877 |
* Booking Marker Dots Visual Indicator Test |
| 1033 |
* ============================================== |
878 |
* ========================================== |
| 1034 |
* |
|
|
| 1035 |
* This test validates the visual booking indicators (event dots) displayed on calendar dates |
| 1036 |
* to show users which dates already have existing bookings. |
| 1037 |
* |
| 1038 |
* Test Coverage: |
| 1039 |
* 1. Single booking event dots (one dot per date) |
| 1040 |
* 2. Multiple bookings on same date (multiple dots) |
| 1041 |
* 3. Dates without bookings (no dots) |
| 1042 |
* 4. Item-specific dot styling with correct CSS classes |
| 1043 |
* 5. Event dot container structure and attributes |
| 1044 |
* |
| 1045 |
* EVENT DOTS FUNCTIONALITY: |
| 1046 |
* ========================= |
| 1047 |
* |
| 1048 |
* Algorithm Overview: |
| 1049 |
* 1. Bookings array is processed into bookingsByDate hash (date -> [item_ids]) |
| 1050 |
* 2. onDayCreate hook checks bookingsByDate[dateString] for each calendar day |
| 1051 |
* 3. If bookings exist, creates .event-dots container with .event.item_{id} children |
| 1052 |
* 4. Sets data attributes for booking metadata and item-specific information |
| 1053 |
* |
| 1054 |
* Visual Structure: |
| 1055 |
* <span class="flatpickr-day"> |
| 1056 |
* <div class="event-dots"> |
| 1057 |
* <div class="event item_301" data-item-id="301"></div> |
| 1058 |
* <div class="event item_302" data-item-id="302"></div> |
| 1059 |
* </div> |
| 1060 |
* </span> |
| 1061 |
* |
| 1062 |
* Event Dot Test Layout: |
| 1063 |
* ====================== |
| 1064 |
* Day: 5 6 7 8 9 10 11 12 13 14 15 16 17 |
| 1065 |
* Booking: MM O O O O S S S O O T O O |
| 1066 |
* Dots: •• - - - - • • • - - • - - |
| 1067 |
* |
879 |
* |
| 1068 |
* Legend: MM = Multiple bookings (items 301+302), S = Single booking (item 303), |
880 |
* Vue version uses .booking-marker-grid with .booking-marker-dot children |
| 1069 |
* T = Test booking (item 301), O = Available, - = No dots, • = Event dot |
881 |
* instead of the jQuery .event-dots / .event pattern. |
| 1070 |
*/ |
882 |
*/ |
| 1071 |
|
883 |
|
| 1072 |
const today = dayjs().startOf("day"); |
884 |
const today = dayjs().startOf("day"); |
| 1073 |
|
885 |
|
| 1074 |
// Set up circulation rules for event dots testing |
886 |
// Set up circulation rules for marker testing |
| 1075 |
const eventDotsCirculationRules = { |
887 |
const markerCirculationRules = { |
| 1076 |
bookings_lead_period: 1, // Minimal to avoid conflicts |
888 |
bookings_lead_period: 1, |
| 1077 |
bookings_trail_period: 1, |
889 |
bookings_trail_period: 1, |
| 1078 |
issuelength: 7, |
890 |
issuelength: 7, |
| 1079 |
renewalsallowed: 1, |
891 |
renewalsallowed: 1, |
|
Lines 1081-1112
describe("Booking Modal Date Picker Tests", () => {
Link Here
|
| 1081 |
}; |
893 |
}; |
| 1082 |
|
894 |
|
| 1083 |
cy.intercept("GET", "/api/v1/circulation_rules*", { |
895 |
cy.intercept("GET", "/api/v1/circulation_rules*", { |
| 1084 |
body: [eventDotsCirculationRules], |
896 |
body: [markerCirculationRules], |
| 1085 |
}).as("getEventDotsRules"); |
897 |
}).as("getMarkerRules"); |
| 1086 |
|
898 |
|
| 1087 |
// Create strategic bookings for event dots testing |
899 |
// Create strategic bookings for marker testing |
| 1088 |
const testBookings = [ |
900 |
const testBookings = [ |
| 1089 |
// Multiple bookings on same dates (Days 5-6): Items 301 + 302 |
901 |
// Multiple bookings on same dates (Days 5-6): Items 0 + 1 |
| 1090 |
{ |
902 |
{ |
| 1091 |
item_id: testData.items[0].item_id, // Will be item 301 equivalent |
903 |
item_id: testData.items[0].item_id, |
| 1092 |
start: today.add(5, "day"), |
904 |
start: today.add(5, "day"), |
| 1093 |
end: today.add(6, "day"), |
905 |
end: today.add(6, "day"), |
| 1094 |
name: "Multi-booking 1", |
906 |
name: "Multi-booking 1", |
| 1095 |
}, |
907 |
}, |
| 1096 |
{ |
908 |
{ |
| 1097 |
item_id: testData.items[1].item_id, // Will be item 302 equivalent |
909 |
item_id: testData.items[1].item_id, |
| 1098 |
start: today.add(5, "day"), |
910 |
start: today.add(5, "day"), |
| 1099 |
end: today.add(6, "day"), |
911 |
end: today.add(6, "day"), |
| 1100 |
name: "Multi-booking 2", |
912 |
name: "Multi-booking 2", |
| 1101 |
}, |
913 |
}, |
| 1102 |
// Single booking spanning multiple days (Days 10-12): Item 303 |
914 |
// Single booking spanning multiple days (Days 10-12): Item 0 |
| 1103 |
{ |
915 |
{ |
| 1104 |
item_id: testData.items[0].item_id, // Reuse first item |
916 |
item_id: testData.items[0].item_id, |
| 1105 |
start: today.add(10, "day"), |
917 |
start: today.add(10, "day"), |
| 1106 |
end: today.add(12, "day"), |
918 |
end: today.add(12, "day"), |
| 1107 |
name: "Single span booking", |
919 |
name: "Single span booking", |
| 1108 |
}, |
920 |
}, |
| 1109 |
// Isolated single booking (Day 15): Item 301 |
921 |
// Isolated single booking (Day 15): Item 0 |
| 1110 |
{ |
922 |
{ |
| 1111 |
item_id: testData.items[0].item_id, |
923 |
item_id: testData.items[0].item_id, |
| 1112 |
start: today.add(15, "day"), |
924 |
start: today.add(15, "day"), |
|
Lines 1133-1152
describe("Booking Modal Date Picker Tests", () => {
Link Here
|
| 1133 |
|
945 |
|
| 1134 |
setupModalForDateTesting({ skipItemSelection: true }); |
946 |
setupModalForDateTesting({ skipItemSelection: true }); |
| 1135 |
|
947 |
|
| 1136 |
// Select item to trigger event dots loading |
948 |
// Select item to trigger marker loading |
| 1137 |
cy.get("#booking_item_id").should("not.be.disabled"); |
949 |
cy.vueSelectShouldBeEnabled("booking_item_id"); |
| 1138 |
cy.selectFromSelect2ByIndex("#booking_item_id", 1); // Select first actual item |
950 |
cy.vueSelectByIndex("booking_item_id", 1); // Select first actual item |
| 1139 |
cy.wait("@getEventDotsRules"); |
951 |
cy.wait("@getMarkerRules"); |
| 1140 |
|
952 |
|
| 1141 |
cy.get("#period").should("not.be.disabled"); |
953 |
cy.get("#booking_period").should("not.be.disabled"); |
| 1142 |
cy.get("#period").as("eventDotsFlatpickr"); |
954 |
cy.get("#booking_period").as("markerFlatpickr"); |
| 1143 |
cy.get("@eventDotsFlatpickr").openFlatpickr(); |
955 |
cy.get("@markerFlatpickr").openFlatpickr(); |
| 1144 |
|
956 |
|
| 1145 |
// ======================================================================== |
957 |
// ======================================================================== |
| 1146 |
// TEST 1: Single Booking Event Dots (Days 10, 11, 12) |
958 |
// TEST 1: Single Booking Marker Dots (Days 10, 11, 12) |
| 1147 |
// ======================================================================== |
959 |
// ======================================================================== |
| 1148 |
|
960 |
|
| 1149 |
// Days 10-12 have single booking from same item - should create one event dot each |
|
|
| 1150 |
const singleDotDates = [ |
961 |
const singleDotDates = [ |
| 1151 |
today.add(10, "day"), |
962 |
today.add(10, "day"), |
| 1152 |
today.add(11, "day"), |
963 |
today.add(11, "day"), |
|
Lines 1158-1172
describe("Booking Modal Date Picker Tests", () => {
Link Here
|
| 1158 |
date.month() === today.month() || |
969 |
date.month() === today.month() || |
| 1159 |
date.month() === today.add(1, "month").month() |
970 |
date.month() === today.add(1, "month").month() |
| 1160 |
) { |
971 |
) { |
| 1161 |
cy.get("@eventDotsFlatpickr") |
972 |
cy.get("@markerFlatpickr") |
| 1162 |
.getFlatpickrDate(date.toDate()) |
973 |
.getFlatpickrDate(date.toDate()) |
| 1163 |
.within(() => { |
974 |
.within(() => { |
| 1164 |
cy.get(".event-dots") |
975 |
cy.get(".booking-marker-grid") |
| 1165 |
.should("exist") |
976 |
.should("exist") |
| 1166 |
.and("have.length", 1); |
977 |
.and("have.length", 1); |
| 1167 |
cy.get(".event-dots .event") |
978 |
cy.get(".booking-marker-grid .booking-marker-dot") |
| 1168 |
.should("exist") |
979 |
.should("exist") |
| 1169 |
.and("have.length", 1); |
980 |
.and("have.length.at.least", 1); |
| 1170 |
}); |
981 |
}); |
| 1171 |
} |
982 |
} |
| 1172 |
}); |
983 |
}); |
|
Lines 1175-1181
describe("Booking Modal Date Picker Tests", () => {
Link Here
|
| 1175 |
// TEST 2: Multiple Bookings on Same Date (Days 5-6) |
986 |
// TEST 2: Multiple Bookings on Same Date (Days 5-6) |
| 1176 |
// ======================================================================== |
987 |
// ======================================================================== |
| 1177 |
|
988 |
|
| 1178 |
// Days 5-6 have TWO different bookings (different items) - should create two dots |
|
|
| 1179 |
const multipleDotDates = [today.add(5, "day"), today.add(6, "day")]; |
989 |
const multipleDotDates = [today.add(5, "day"), today.add(6, "day")]; |
| 1180 |
|
990 |
|
| 1181 |
multipleDotDates.forEach(date => { |
991 |
multipleDotDates.forEach(date => { |
|
Lines 1183-1202
describe("Booking Modal Date Picker Tests", () => {
Link Here
|
| 1183 |
date.month() === today.month() || |
993 |
date.month() === today.month() || |
| 1184 |
date.month() === today.add(1, "month").month() |
994 |
date.month() === today.add(1, "month").month() |
| 1185 |
) { |
995 |
) { |
| 1186 |
cy.get("@eventDotsFlatpickr") |
996 |
cy.get("@markerFlatpickr") |
| 1187 |
.getFlatpickrDate(date.toDate()) |
997 |
.getFlatpickrDate(date.toDate()) |
| 1188 |
.within(() => { |
998 |
.within(() => { |
| 1189 |
cy.get(".event-dots").should("exist"); |
999 |
cy.get(".booking-marker-grid").should("exist"); |
| 1190 |
cy.get(".event-dots .event").should("have.length", 2); |
1000 |
// Dots are aggregated by type (booked/checked-out), not per-booking. |
|
|
1001 |
// 2 bookings of type "booked" = 1 dot with count 2. |
| 1002 |
cy.get( |
| 1003 |
".booking-marker-grid .booking-marker-dot" |
| 1004 |
).should("have.length.at.least", 1); |
| 1191 |
}); |
1005 |
}); |
| 1192 |
} |
1006 |
} |
| 1193 |
}); |
1007 |
}); |
| 1194 |
|
1008 |
|
| 1195 |
// ======================================================================== |
1009 |
// ======================================================================== |
| 1196 |
// TEST 3: Dates Without Bookings (No Event Dots) |
1010 |
// TEST 3: Dates Without Bookings (No Marker Dots) |
| 1197 |
// ======================================================================== |
1011 |
// ======================================================================== |
| 1198 |
|
1012 |
|
| 1199 |
// Dates without bookings should have no .event-dots container |
|
|
| 1200 |
const emptyDates = [ |
1013 |
const emptyDates = [ |
| 1201 |
today.add(3, "day"), // Before any bookings |
1014 |
today.add(3, "day"), // Before any bookings |
| 1202 |
today.add(8, "day"), // Between booking periods |
1015 |
today.add(8, "day"), // Between booking periods |
|
Lines 1209-1218
describe("Booking Modal Date Picker Tests", () => {
Link Here
|
| 1209 |
date.month() === today.month() || |
1022 |
date.month() === today.month() || |
| 1210 |
date.month() === today.add(1, "month").month() |
1023 |
date.month() === today.add(1, "month").month() |
| 1211 |
) { |
1024 |
) { |
| 1212 |
cy.get("@eventDotsFlatpickr") |
1025 |
cy.get("@markerFlatpickr") |
| 1213 |
.getFlatpickrDate(date.toDate()) |
1026 |
.getFlatpickrDate(date.toDate()) |
| 1214 |
.within(() => { |
1027 |
.within(() => { |
| 1215 |
cy.get(".event-dots").should("not.exist"); |
1028 |
cy.get(".booking-marker-grid").should("not.exist"); |
| 1216 |
}); |
1029 |
}); |
| 1217 |
} |
1030 |
} |
| 1218 |
}); |
1031 |
}); |
|
Lines 1221-1254
describe("Booking Modal Date Picker Tests", () => {
Link Here
|
| 1221 |
// TEST 4: Isolated Single Booking (Day 15) - Boundary Detection |
1034 |
// TEST 4: Isolated Single Booking (Day 15) - Boundary Detection |
| 1222 |
// ======================================================================== |
1035 |
// ======================================================================== |
| 1223 |
|
1036 |
|
| 1224 |
// Day 15 has booking (should have dot), adjacent days 14 and 16 don't (no dots) |
|
|
| 1225 |
const isolatedBookingDate = today.add(15, "day"); |
1037 |
const isolatedBookingDate = today.add(15, "day"); |
| 1226 |
|
1038 |
|
| 1227 |
if ( |
1039 |
if ( |
| 1228 |
isolatedBookingDate.month() === today.month() || |
1040 |
isolatedBookingDate.month() === today.month() || |
| 1229 |
isolatedBookingDate.month() === today.add(1, "month").month() |
1041 |
isolatedBookingDate.month() === today.add(1, "month").month() |
| 1230 |
) { |
1042 |
) { |
| 1231 |
// Verify isolated booking day HAS dot |
1043 |
// Verify isolated booking day HAS marker dot |
| 1232 |
cy.get("@eventDotsFlatpickr") |
1044 |
cy.get("@markerFlatpickr") |
| 1233 |
.getFlatpickrDate(isolatedBookingDate.toDate()) |
1045 |
.getFlatpickrDate(isolatedBookingDate.toDate()) |
| 1234 |
.within(() => { |
1046 |
.within(() => { |
| 1235 |
cy.get(".event-dots").should("exist"); |
1047 |
cy.get(".booking-marker-grid").should("exist"); |
| 1236 |
cy.get(".event-dots .event") |
1048 |
cy.get(".booking-marker-grid .booking-marker-dot") |
| 1237 |
.should("exist") |
1049 |
.should("exist") |
| 1238 |
.and("have.length", 1); |
1050 |
.and("have.length.at.least", 1); |
| 1239 |
}); |
1051 |
}); |
| 1240 |
|
1052 |
|
| 1241 |
// Verify adjacent dates DON'T have dots |
1053 |
// Verify adjacent dates DON'T have marker dots |
| 1242 |
[today.add(14, "day"), today.add(16, "day")].forEach( |
1054 |
[today.add(14, "day"), today.add(16, "day")].forEach( |
| 1243 |
adjacentDate => { |
1055 |
adjacentDate => { |
| 1244 |
if ( |
1056 |
if ( |
| 1245 |
adjacentDate.month() === today.month() || |
1057 |
adjacentDate.month() === today.month() || |
| 1246 |
adjacentDate.month() === today.add(1, "month").month() |
1058 |
adjacentDate.month() === today.add(1, "month").month() |
| 1247 |
) { |
1059 |
) { |
| 1248 |
cy.get("@eventDotsFlatpickr") |
1060 |
cy.get("@markerFlatpickr") |
| 1249 |
.getFlatpickrDate(adjacentDate.toDate()) |
1061 |
.getFlatpickrDate(adjacentDate.toDate()) |
| 1250 |
.within(() => { |
1062 |
.within(() => { |
| 1251 |
cy.get(".event-dots").should("not.exist"); |
1063 |
cy.get(".booking-marker-grid").should( |
|
|
1064 |
"not.exist" |
| 1065 |
); |
| 1252 |
}); |
1066 |
}); |
| 1253 |
} |
1067 |
} |
| 1254 |
} |
1068 |
} |
|
Lines 1256-1262
describe("Booking Modal Date Picker Tests", () => {
Link Here
|
| 1256 |
} |
1070 |
} |
| 1257 |
|
1071 |
|
| 1258 |
cy.log( |
1072 |
cy.log( |
| 1259 |
"✓ CONFIRMED: Event dots display correctly (single, multiple, empty dates, boundaries)" |
1073 |
"✓ CONFIRMED: Booking marker dots display correctly (single, multiple, empty dates, boundaries)" |
| 1260 |
); |
1074 |
); |
| 1261 |
}); |
1075 |
}); |
| 1262 |
|
1076 |
|
|
Lines 1266-1281
describe("Booking Modal Date Picker Tests", () => {
Link Here
|
| 1266 |
* |
1080 |
* |
| 1267 |
* Key principle: Once an item is removed from the pool (becomes unavailable), |
1081 |
* Key principle: Once an item is removed from the pool (becomes unavailable), |
| 1268 |
* it is NEVER re-added even if it becomes available again later. |
1082 |
* it is NEVER re-added even if it becomes available again later. |
| 1269 |
* |
|
|
| 1270 |
* Booking pattern: |
| 1271 |
* - ITEM 0: Booked days 10-15 |
| 1272 |
* - ITEM 1: Booked days 13-20 |
| 1273 |
* - ITEM 2: Booked days 18-25 |
| 1274 |
* - ITEM 3: Booked days 1-7, then 23-30 |
| 1275 |
*/ |
1083 |
*/ |
| 1276 |
|
1084 |
|
| 1277 |
// Fix the browser Date object to June 10, 2026 at 09:00 Europe/London |
1085 |
// Fix the browser Date object to June 10, 2026 at 09:00 Europe/London |
| 1278 |
// Using ["Date"] to avoid freezing timers which breaks Select2 async operations |
|
|
| 1279 |
const fixedToday = new Date("2026-06-10T08:00:00Z"); // 09:00 BST (UTC+1) |
1086 |
const fixedToday = new Date("2026-06-10T08:00:00Z"); // 09:00 BST (UTC+1) |
| 1280 |
cy.clock(fixedToday, ["Date"]); |
1087 |
cy.clock(fixedToday, ["Date"]); |
| 1281 |
const today = dayjs(fixedToday); |
1088 |
const today = dayjs(fixedToday); |
|
Lines 1299-1316
describe("Booking Modal Date Picker Tests", () => {
Link Here
|
| 1299 |
testBiblio = objects.biblio; |
1106 |
testBiblio = objects.biblio; |
| 1300 |
testItems = objects.items; |
1107 |
testItems = objects.items; |
| 1301 |
|
1108 |
|
| 1302 |
const itemUpdates = testItems.map((item, index) => { |
1109 |
let chain = cy.wrap(null); |
|
|
1110 |
testItems.forEach((item, index) => { |
| 1303 |
const enumchron = String.fromCharCode(65 + index); |
1111 |
const enumchron = String.fromCharCode(65 + index); |
| 1304 |
return cy.task("query", { |
1112 |
chain = chain.then(() => cy.task("query", { |
| 1305 |
sql: "UPDATE items SET bookable = 1, itype = 'BK', homebranch = 'CPL', enumchron = ?, dateaccessioned = ? WHERE itemnumber = ?", |
1113 |
sql: "UPDATE items SET bookable = 1, itype = 'BK', homebranch = 'CPL', enumchron = ?, dateaccessioned = ? WHERE itemnumber = ?", |
| 1306 |
values: [ |
1114 |
values: [ |
| 1307 |
enumchron, |
1115 |
enumchron, |
| 1308 |
`2024-12-0${4 - index}`, |
1116 |
`2024-12-0${4 - index}`, |
| 1309 |
item.item_id, |
1117 |
item.item_id, |
| 1310 |
], |
1118 |
], |
| 1311 |
}); |
1119 |
})); |
| 1312 |
}); |
1120 |
}); |
| 1313 |
return Promise.all(itemUpdates); |
1121 |
return chain; |
| 1314 |
}) |
1122 |
}) |
| 1315 |
.then(() => { |
1123 |
.then(() => { |
| 1316 |
return cy.task("buildSampleObject", { |
1124 |
return cy.task("buildSampleObject", { |
|
Lines 1341-1420
describe("Booking Modal Date Picker Tests", () => {
Link Here
|
| 1341 |
}); |
1149 |
}); |
| 1342 |
}) |
1150 |
}) |
| 1343 |
.then(() => { |
1151 |
.then(() => { |
| 1344 |
// Create strategic bookings |
1152 |
// Create strategic bookings sequentially |
| 1345 |
const bookingInserts = [ |
1153 |
const bookings = [ |
| 1346 |
// ITEM 0: Booked 10-15 |
1154 |
{ item: testItems[0], start: 10, end: 15 }, // ITEM 0 |
| 1347 |
cy.task("query", { |
1155 |
{ item: testItems[1], start: 13, end: 20 }, // ITEM 1 |
| 1348 |
sql: `INSERT INTO bookings (biblio_id, patron_id, item_id, pickup_library_id, start_date, end_date, status) |
1156 |
{ item: testItems[2], start: 18, end: 25 }, // ITEM 2 |
| 1349 |
VALUES (?, ?, ?, ?, ?, ?, ?)`, |
1157 |
{ item: testItems[3], start: 1, end: 7 }, // ITEM 3 |
| 1350 |
values: [ |
1158 |
{ item: testItems[3], start: 23, end: 30 }, // ITEM 3 |
| 1351 |
testBiblio.biblio_id, |
1159 |
]; |
| 1352 |
testPatron.patron_id, |
1160 |
let chain = cy.wrap(null); |
| 1353 |
testItems[0].item_id, |
1161 |
bookings.forEach(b => { |
| 1354 |
"CPL", |
1162 |
chain = chain.then(() => cy.task("query", { |
| 1355 |
today.add(10, "day").format("YYYY-MM-DD"), |
|
|
| 1356 |
today.add(15, "day").format("YYYY-MM-DD"), |
| 1357 |
"new", |
| 1358 |
], |
| 1359 |
}), |
| 1360 |
// ITEM 1: Booked 13-20 |
| 1361 |
cy.task("query", { |
| 1362 |
sql: `INSERT INTO bookings (biblio_id, patron_id, item_id, pickup_library_id, start_date, end_date, status) |
| 1363 |
VALUES (?, ?, ?, ?, ?, ?, ?)`, |
| 1364 |
values: [ |
| 1365 |
testBiblio.biblio_id, |
| 1366 |
testPatron.patron_id, |
| 1367 |
testItems[1].item_id, |
| 1368 |
"CPL", |
| 1369 |
today.add(13, "day").format("YYYY-MM-DD"), |
| 1370 |
today.add(20, "day").format("YYYY-MM-DD"), |
| 1371 |
"new", |
| 1372 |
], |
| 1373 |
}), |
| 1374 |
// ITEM 2: Booked 18-25 |
| 1375 |
cy.task("query", { |
| 1376 |
sql: `INSERT INTO bookings (biblio_id, patron_id, item_id, pickup_library_id, start_date, end_date, status) |
| 1377 |
VALUES (?, ?, ?, ?, ?, ?, ?)`, |
| 1378 |
values: [ |
| 1379 |
testBiblio.biblio_id, |
| 1380 |
testPatron.patron_id, |
| 1381 |
testItems[2].item_id, |
| 1382 |
"CPL", |
| 1383 |
today.add(18, "day").format("YYYY-MM-DD"), |
| 1384 |
today.add(25, "day").format("YYYY-MM-DD"), |
| 1385 |
"new", |
| 1386 |
], |
| 1387 |
}), |
| 1388 |
// ITEM 3: Booked 1-7 |
| 1389 |
cy.task("query", { |
| 1390 |
sql: `INSERT INTO bookings (biblio_id, patron_id, item_id, pickup_library_id, start_date, end_date, status) |
| 1391 |
VALUES (?, ?, ?, ?, ?, ?, ?)`, |
| 1392 |
values: [ |
| 1393 |
testBiblio.biblio_id, |
| 1394 |
testPatron.patron_id, |
| 1395 |
testItems[3].item_id, |
| 1396 |
"CPL", |
| 1397 |
today.add(1, "day").format("YYYY-MM-DD"), |
| 1398 |
today.add(7, "day").format("YYYY-MM-DD"), |
| 1399 |
"new", |
| 1400 |
], |
| 1401 |
}), |
| 1402 |
// ITEM 3: Booked 23-30 |
| 1403 |
cy.task("query", { |
| 1404 |
sql: `INSERT INTO bookings (biblio_id, patron_id, item_id, pickup_library_id, start_date, end_date, status) |
1163 |
sql: `INSERT INTO bookings (biblio_id, patron_id, item_id, pickup_library_id, start_date, end_date, status) |
| 1405 |
VALUES (?, ?, ?, ?, ?, ?, ?)`, |
1164 |
VALUES (?, ?, ?, ?, ?, ?, ?)`, |
| 1406 |
values: [ |
1165 |
values: [ |
| 1407 |
testBiblio.biblio_id, |
1166 |
testBiblio.biblio_id, |
| 1408 |
testPatron.patron_id, |
1167 |
testPatron.patron_id, |
| 1409 |
testItems[3].item_id, |
1168 |
b.item.item_id, |
| 1410 |
"CPL", |
1169 |
"CPL", |
| 1411 |
today.add(23, "day").format("YYYY-MM-DD"), |
1170 |
today.add(b.start, "day").format("YYYY-MM-DD"), |
| 1412 |
today.add(30, "day").format("YYYY-MM-DD"), |
1171 |
today.add(b.end, "day").format("YYYY-MM-DD"), |
| 1413 |
"new", |
1172 |
"new", |
| 1414 |
], |
1173 |
], |
| 1415 |
}), |
1174 |
})); |
| 1416 |
]; |
1175 |
}); |
| 1417 |
return Promise.all(bookingInserts); |
1176 |
return chain; |
| 1418 |
}) |
1177 |
}) |
| 1419 |
.then(() => { |
1178 |
.then(() => { |
| 1420 |
cy.intercept( |
1179 |
cy.intercept( |
|
Lines 1429-1456
describe("Booking Modal Date Picker Tests", () => {
Link Here
|
| 1429 |
`/cgi-bin/koha/catalogue/detail.pl?biblionumber=${testBiblio.biblio_id}` |
1188 |
`/cgi-bin/koha/catalogue/detail.pl?biblionumber=${testBiblio.biblio_id}` |
| 1430 |
); |
1189 |
); |
| 1431 |
|
1190 |
|
| 1432 |
cy.get('[data-bs-target="#placeBookingModal"]').first().click(); |
1191 |
cy.get("booking-modal-island .modal").should("exist"); |
| 1433 |
cy.get("#placeBookingModal").should("be.visible"); |
1192 |
cy.get("[data-booking-modal]").first().then($btn => $btn[0].click()); |
|
|
1193 |
cy.get("booking-modal-island .modal", { |
| 1194 |
timeout: 10000, |
| 1195 |
}).should("be.visible"); |
| 1434 |
|
1196 |
|
| 1435 |
cy.selectFromSelect2( |
1197 |
cy.vueSelect( |
| 1436 |
"#booking_patron_id", |
1198 |
"booking_patron", |
| 1437 |
`${testPatron.surname}, ${testPatron.firstname}`, |
1199 |
testPatron.cardnumber, |
| 1438 |
testPatron.cardnumber |
1200 |
`${testPatron.surname} ${testPatron.firstname}` |
| 1439 |
); |
1201 |
); |
| 1440 |
cy.wait("@getPickupLocations"); |
1202 |
cy.wait("@getPickupLocations"); |
| 1441 |
|
1203 |
|
| 1442 |
cy.get("#pickup_library_id").should("not.be.disabled"); |
1204 |
cy.vueSelectShouldBeEnabled("pickup_library_id"); |
| 1443 |
cy.selectFromSelect2ByIndex("#pickup_library_id", 0); |
1205 |
cy.vueSelectByIndex("pickup_library_id", 0); |
| 1444 |
|
1206 |
|
| 1445 |
cy.get("#booking_itemtype").should("not.be.disabled"); |
1207 |
cy.vueSelectShouldBeEnabled("booking_itemtype"); |
| 1446 |
cy.selectFromSelect2ByIndex("#booking_itemtype", 0); |
1208 |
cy.vueSelectByIndex("booking_itemtype", 0); |
| 1447 |
cy.wait("@getCirculationRules"); |
1209 |
cy.wait("@getCirculationRules"); |
| 1448 |
|
1210 |
|
| 1449 |
cy.selectFromSelect2ByIndex("#booking_item_id", 0); // "Any item" |
1211 |
// "Any item" = no item selected (null) = leave dropdown at placeholder |
| 1450 |
cy.get("#period").should("not.be.disabled"); |
1212 |
cy.get("#booking_period").should("not.be.disabled"); |
| 1451 |
cy.get("#period").as("flatpickrInput"); |
1213 |
cy.get("#booking_period").as("flatpickrInput"); |
| 1452 |
|
1214 |
|
| 1453 |
// Helper to check date availability - checks boundaries + random middle date |
1215 |
// Helper to check date availability |
| 1454 |
const checkDatesAvailable = (fromDay, toDay) => { |
1216 |
const checkDatesAvailable = (fromDay, toDay) => { |
| 1455 |
const daysToCheck = [fromDay, toDay]; |
1217 |
const daysToCheck = [fromDay, toDay]; |
| 1456 |
if (toDay - fromDay > 1) { |
1218 |
if (toDay - fromDay > 1) { |
|
Lines 1484-1524
describe("Booking Modal Date Picker Tests", () => {
Link Here
|
| 1484 |
}; |
1246 |
}; |
| 1485 |
|
1247 |
|
| 1486 |
// SCENARIO 1: Start day 5 |
1248 |
// SCENARIO 1: Start day 5 |
| 1487 |
// Pool starts: ITEM0, ITEM1, ITEM2 (ITEM3 booked 1-7) |
|
|
| 1488 |
// Day 10: lose ITEM0, Day 13: lose ITEM1, Day 18: lose ITEM2 → disabled |
| 1489 |
cy.log("=== Scenario 1: Start day 5 ==="); |
1249 |
cy.log("=== Scenario 1: Start day 5 ==="); |
| 1490 |
cy.get("@flatpickrInput").openFlatpickr(); |
1250 |
cy.get("@flatpickrInput").openFlatpickr(); |
| 1491 |
cy.get("@flatpickrInput") |
1251 |
cy.get("@flatpickrInput") |
| 1492 |
.getFlatpickrDate(today.add(5, "day").toDate()) |
1252 |
.selectFlatpickrDate(today.add(5, "day").toDate()); |
| 1493 |
.click(); |
|
|
| 1494 |
|
1253 |
|
| 1495 |
checkDatesAvailable(6, 17); // Available through day 17 |
1254 |
checkDatesAvailable(6, 17); |
| 1496 |
checkDatesDisabled(18, 20); // Disabled from day 18 |
1255 |
checkDatesDisabled(18, 20); |
| 1497 |
|
1256 |
|
| 1498 |
// SCENARIO 2: Start day 8 |
1257 |
// SCENARIO 2: Start day 8 |
| 1499 |
// Pool starts: ALL 4 items (ITEM3 booking 1-7 ended) |
1258 |
cy.log( |
| 1500 |
// Progressive reduction until day 23 when ITEM3's second booking starts |
1259 |
"=== Scenario 2: Start day 8 (all items available) ===" |
| 1501 |
cy.log("=== Scenario 2: Start day 8 (all items available) ==="); |
1260 |
); |
| 1502 |
cy.get("@flatpickrInput").clearFlatpickr(); |
1261 |
cy.get("@flatpickrInput").clearFlatpickr(); |
| 1503 |
cy.get("@flatpickrInput").openFlatpickr(); |
1262 |
cy.get("@flatpickrInput").openFlatpickr(); |
| 1504 |
cy.get("@flatpickrInput") |
1263 |
cy.get("@flatpickrInput") |
| 1505 |
.getFlatpickrDate(today.add(8, "day").toDate()) |
1264 |
.selectFlatpickrDate(today.add(8, "day").toDate()); |
| 1506 |
.click(); |
|
|
| 1507 |
|
1265 |
|
| 1508 |
checkDatesAvailable(9, 22); // Can book through day 22 |
1266 |
checkDatesAvailable(9, 22); |
| 1509 |
checkDatesDisabled(23, 25); // Disabled from day 23 |
1267 |
checkDatesDisabled(23, 25); |
| 1510 |
|
1268 |
|
| 1511 |
// SCENARIO 3: Start day 19 |
1269 |
// SCENARIO 3: Start day 19 |
| 1512 |
// Pool starts: ITEM0 (booking ended day 15), ITEM3 |
|
|
| 1513 |
// ITEM0 stays available indefinitely, ITEM3 loses at day 23 |
| 1514 |
cy.log("=== Scenario 3: Start day 19 ==="); |
1270 |
cy.log("=== Scenario 3: Start day 19 ==="); |
| 1515 |
cy.get("@flatpickrInput").clearFlatpickr(); |
1271 |
cy.get("@flatpickrInput").clearFlatpickr(); |
| 1516 |
cy.get("@flatpickrInput").openFlatpickr(); |
1272 |
cy.get("@flatpickrInput").openFlatpickr(); |
| 1517 |
cy.get("@flatpickrInput") |
1273 |
cy.get("@flatpickrInput") |
| 1518 |
.getFlatpickrDate(today.add(19, "day").toDate()) |
1274 |
.selectFlatpickrDate(today.add(19, "day").toDate()); |
| 1519 |
.click(); |
|
|
| 1520 |
|
1275 |
|
| 1521 |
// ITEM0 remains in pool, so dates stay available past day 23 |
|
|
| 1522 |
checkDatesAvailable(20, 25); |
1276 |
checkDatesAvailable(20, 25); |
| 1523 |
}); |
1277 |
}); |
| 1524 |
|
1278 |
|
|
Lines 1546-1588
describe("Booking Modal Date Picker Tests", () => {
Link Here
|
| 1546 |
it("should correctly handle lead/trail period conflicts for 'any item' bookings", () => { |
1300 |
it("should correctly handle lead/trail period conflicts for 'any item' bookings", () => { |
| 1547 |
/** |
1301 |
/** |
| 1548 |
* Bug 37707: Lead/Trail Period Conflict Detection for "Any Item" Bookings |
1302 |
* Bug 37707: Lead/Trail Period Conflict Detection for "Any Item" Bookings |
| 1549 |
* ======================================================================== |
|
|
| 1550 |
* |
| 1551 |
* This test validates that lead/trail period conflict detection works correctly |
| 1552 |
* when "any item of itemtype X" is selected. The key principle is: |
| 1553 |
* |
| 1554 |
* - Only block date selection when ALL items of the itemtype have conflicts |
| 1555 |
* - Allow selection when at least one item is free from lead/trail conflicts |
| 1556 |
* |
| 1557 |
* The bug occurred because the mouseover handler was checking conflicts against |
| 1558 |
* ALL bookings regardless of itemtype, rather than tracking per-item conflicts. |
| 1559 |
* |
| 1560 |
* Test Setup: |
| 1561 |
* =========== |
| 1562 |
* - Fixed date: June 1, 2026 (keeps all test dates in same month) |
| 1563 |
* - 3 items of itemtype BK |
| 1564 |
* - Lead period: 2 days, Trail period: 2 days |
| 1565 |
* - ITEM 0: Booking on days 10-12 (June 11-13, trail period: June 14-15) |
| 1566 |
* - ITEM 1: Booking on days 10-12 (same as item 0) |
| 1567 |
* - ITEM 2: No bookings (always available) |
| 1568 |
* |
| 1569 |
* Test Scenarios: |
| 1570 |
* ============== |
| 1571 |
* 1. Hover day 15 (June 16): ITEM 0 and ITEM 1 have trail period conflict |
| 1572 |
* (lead period June 14-15 overlaps their trail June 14-15), but ITEM 2 is free |
| 1573 |
* → Should NOT be blocked (at least one item available) |
| 1574 |
* |
| 1575 |
* 2. Create booking on ITEM 2 for days 10-12, then hover day 15 again: |
| 1576 |
* → ALL items now have trail period conflicts |
| 1577 |
* → Should BE blocked |
| 1578 |
* |
| 1579 |
* 3. Visual feedback: Check existingBookingTrail on days 13-14 (June 14-15) |
| 1580 |
* |
| 1581 |
* 4. Visual feedback: Check existingBookingLead on days 8-9 (June 9-10) |
| 1582 |
*/ |
1303 |
*/ |
| 1583 |
|
1304 |
|
| 1584 |
// Fix the browser Date object to June 1, 2026 at 09:00 Europe/London |
1305 |
// Fix the browser Date object to June 1, 2026 at 09:00 Europe/London |
| 1585 |
// This ensures all test dates (days 5-17) fall within June |
|
|
| 1586 |
const fixedToday = new Date("2026-06-01T08:00:00Z"); // 09:00 BST (UTC+1) |
1306 |
const fixedToday = new Date("2026-06-01T08:00:00Z"); // 09:00 BST (UTC+1) |
| 1587 |
cy.clock(fixedToday, ["Date"]); |
1307 |
cy.clock(fixedToday, ["Date"]); |
| 1588 |
|
1308 |
|
|
Lines 1609-1626
describe("Booking Modal Date Picker Tests", () => {
Link Here
|
| 1609 |
testLibraries = objects.libraries; |
1329 |
testLibraries = objects.libraries; |
| 1610 |
|
1330 |
|
| 1611 |
// Make all items the same itemtype (BK) |
1331 |
// Make all items the same itemtype (BK) |
| 1612 |
const itemUpdates = testItems.map((item, index) => { |
1332 |
let chain = cy.wrap(null); |
|
|
1333 |
testItems.forEach((item, index) => { |
| 1613 |
const enumchron = String.fromCharCode(65 + index); |
1334 |
const enumchron = String.fromCharCode(65 + index); |
| 1614 |
return cy.task("query", { |
1335 |
chain = chain.then(() => cy.task("query", { |
| 1615 |
sql: "UPDATE items SET bookable = 1, itype = 'BK', homebranch = 'CPL', enumchron = ?, dateaccessioned = ? WHERE itemnumber = ?", |
1336 |
sql: "UPDATE items SET bookable = 1, itype = 'BK', homebranch = 'CPL', enumchron = ?, dateaccessioned = ? WHERE itemnumber = ?", |
| 1616 |
values: [ |
1337 |
values: [ |
| 1617 |
enumchron, |
1338 |
enumchron, |
| 1618 |
`2024-12-0${4 - index}`, |
1339 |
`2024-12-0${4 - index}`, |
| 1619 |
item.item_id, |
1340 |
item.item_id, |
| 1620 |
], |
1341 |
], |
| 1621 |
}); |
1342 |
})); |
| 1622 |
}); |
1343 |
}); |
| 1623 |
return Promise.all(itemUpdates); |
1344 |
return chain; |
| 1624 |
}) |
1345 |
}) |
| 1625 |
.then(() => { |
1346 |
.then(() => { |
| 1626 |
return cy.task("buildSampleObject", { |
1347 |
return cy.task("buildSampleObject", { |
|
Lines 1652-1690
describe("Booking Modal Date Picker Tests", () => {
Link Here
|
| 1652 |
}) |
1373 |
}) |
| 1653 |
.then(() => { |
1374 |
.then(() => { |
| 1654 |
// Create bookings on ITEM 0 and ITEM 1 for days 10-12 |
1375 |
// Create bookings on ITEM 0 and ITEM 1 for days 10-12 |
| 1655 |
// ITEM 2 remains free |
1376 |
return cy.task("query", { |
| 1656 |
const bookingInserts = [ |
1377 |
sql: `INSERT INTO bookings (biblio_id, patron_id, item_id, pickup_library_id, start_date, end_date, status) |
| 1657 |
// ITEM 0: Booked days 10-12 |
1378 |
VALUES (?, ?, ?, ?, ?, ?, ?)`, |
| 1658 |
cy.task("query", { |
1379 |
values: [ |
| 1659 |
sql: `INSERT INTO bookings (biblio_id, patron_id, item_id, pickup_library_id, start_date, end_date, status) |
1380 |
testBiblio.biblio_id, |
| 1660 |
VALUES (?, ?, ?, ?, ?, ?, ?)`, |
1381 |
testPatron.patron_id, |
| 1661 |
values: [ |
1382 |
testItems[0].item_id, |
| 1662 |
testBiblio.biblio_id, |
1383 |
"CPL", |
| 1663 |
testPatron.patron_id, |
1384 |
today.add(10, "day").format("YYYY-MM-DD"), |
| 1664 |
testItems[0].item_id, |
1385 |
today.add(12, "day").format("YYYY-MM-DD"), |
| 1665 |
"CPL", |
1386 |
"new", |
| 1666 |
today.add(10, "day").format("YYYY-MM-DD"), |
1387 |
], |
| 1667 |
today.add(12, "day").format("YYYY-MM-DD"), |
1388 |
}).then(() => cy.task("query", { |
| 1668 |
"new", |
1389 |
sql: `INSERT INTO bookings (biblio_id, patron_id, item_id, pickup_library_id, start_date, end_date, status) |
| 1669 |
], |
1390 |
VALUES (?, ?, ?, ?, ?, ?, ?)`, |
| 1670 |
}), |
1391 |
values: [ |
| 1671 |
// ITEM 1: Booked days 10-12 (same period) |
1392 |
testBiblio.biblio_id, |
| 1672 |
cy.task("query", { |
1393 |
testPatron.patron_id, |
| 1673 |
sql: `INSERT INTO bookings (biblio_id, patron_id, item_id, pickup_library_id, start_date, end_date, status) |
1394 |
testItems[1].item_id, |
| 1674 |
VALUES (?, ?, ?, ?, ?, ?, ?)`, |
1395 |
"CPL", |
| 1675 |
values: [ |
1396 |
today.add(10, "day").format("YYYY-MM-DD"), |
| 1676 |
testBiblio.biblio_id, |
1397 |
today.add(12, "day").format("YYYY-MM-DD"), |
| 1677 |
testPatron.patron_id, |
1398 |
"new", |
| 1678 |
testItems[1].item_id, |
1399 |
], |
| 1679 |
"CPL", |
1400 |
})); |
| 1680 |
today.add(10, "day").format("YYYY-MM-DD"), |
|
|
| 1681 |
today.add(12, "day").format("YYYY-MM-DD"), |
| 1682 |
"new", |
| 1683 |
], |
| 1684 |
}), |
| 1685 |
// ITEM 2: No booking - remains free |
| 1686 |
]; |
| 1687 |
return Promise.all(bookingInserts); |
| 1688 |
}) |
1401 |
}) |
| 1689 |
.then(() => { |
1402 |
.then(() => { |
| 1690 |
cy.intercept( |
1403 |
cy.intercept( |
|
Lines 1699-1728
describe("Booking Modal Date Picker Tests", () => {
Link Here
|
| 1699 |
`/cgi-bin/koha/catalogue/detail.pl?biblionumber=${testBiblio.biblio_id}` |
1412 |
`/cgi-bin/koha/catalogue/detail.pl?biblionumber=${testBiblio.biblio_id}` |
| 1700 |
); |
1413 |
); |
| 1701 |
|
1414 |
|
| 1702 |
cy.get('[data-bs-target="#placeBookingModal"]').first().click(); |
1415 |
cy.get("booking-modal-island .modal").should("exist"); |
| 1703 |
cy.get("#placeBookingModal").should("be.visible"); |
1416 |
cy.get("[data-booking-modal]").first().then($btn => $btn[0].click()); |
|
|
1417 |
cy.get("booking-modal-island .modal", { |
| 1418 |
timeout: 10000, |
| 1419 |
}).should("be.visible"); |
| 1704 |
|
1420 |
|
| 1705 |
cy.selectFromSelect2( |
1421 |
cy.vueSelect( |
| 1706 |
"#booking_patron_id", |
1422 |
"booking_patron", |
| 1707 |
`${testPatron.surname}, ${testPatron.firstname}`, |
1423 |
testPatron.cardnumber, |
| 1708 |
testPatron.cardnumber |
1424 |
`${testPatron.surname} ${testPatron.firstname}` |
| 1709 |
); |
1425 |
); |
| 1710 |
cy.wait("@getPickupLocations"); |
1426 |
cy.wait("@getPickupLocations"); |
| 1711 |
|
1427 |
|
| 1712 |
cy.get("#pickup_library_id").should("not.be.disabled"); |
1428 |
cy.vueSelectShouldBeEnabled("pickup_library_id"); |
| 1713 |
cy.selectFromSelect2ByIndex("#pickup_library_id", 0); |
1429 |
cy.vueSelectByIndex("pickup_library_id", 0); |
| 1714 |
|
1430 |
|
| 1715 |
// Select itemtype BK |
1431 |
// Select itemtype BK |
| 1716 |
cy.get("#booking_itemtype").should("not.be.disabled"); |
1432 |
cy.vueSelectShouldBeEnabled("booking_itemtype"); |
| 1717 |
cy.selectFromSelect2("#booking_itemtype", "Books"); |
1433 |
cy.vueSelectByIndex("booking_itemtype", 0); |
| 1718 |
cy.wait("@getCirculationRules"); |
1434 |
cy.wait("@getCirculationRules"); |
| 1719 |
|
1435 |
|
| 1720 |
// Select "Any item" (index 0) |
1436 |
// "Any item" = no item selected (null) = leave dropdown at placeholder |
| 1721 |
cy.selectFromSelect2ByIndex("#booking_item_id", 0); |
|
|
| 1722 |
cy.get("#booking_item_id").should("have.value", "0"); |
| 1723 |
|
1437 |
|
| 1724 |
cy.get("#period").should("not.be.disabled"); |
1438 |
cy.get("#booking_period").should("not.be.disabled"); |
| 1725 |
cy.get("#period").as("flatpickrInput"); |
1439 |
cy.get("#booking_period").as("flatpickrInput"); |
| 1726 |
|
1440 |
|
| 1727 |
// ================================================================ |
1441 |
// ================================================================ |
| 1728 |
// SCENARIO 1: Hover day 15 - ITEM 2 is free, should NOT be blocked |
1442 |
// SCENARIO 1: Hover day 15 - ITEM 2 is free, should NOT be blocked |
|
Lines 1733-1756
describe("Booking Modal Date Picker Tests", () => {
Link Here
|
| 1733 |
|
1447 |
|
| 1734 |
cy.get("@flatpickrInput").openFlatpickr(); |
1448 |
cy.get("@flatpickrInput").openFlatpickr(); |
| 1735 |
cy.get("@flatpickrInput") |
1449 |
cy.get("@flatpickrInput") |
| 1736 |
.getFlatpickrDate(today.add(15, "day").toDate()) |
1450 |
.hoverFlatpickrDate(today.add(15, "day").toDate()); |
| 1737 |
.trigger("mouseover"); |
|
|
| 1738 |
|
1451 |
|
| 1739 |
// Day 15 should NOT have leadDisable class (at least one item is free) |
1452 |
// Day 15 should NOT be disabled (at least one item is free) |
| 1740 |
cy.get("@flatpickrInput") |
1453 |
cy.get("@flatpickrInput") |
| 1741 |
.getFlatpickrDate(today.add(15, "day").toDate()) |
1454 |
.getFlatpickrDate(today.add(15, "day").toDate()) |
| 1742 |
.should("not.have.class", "leadDisable"); |
1455 |
.should("not.have.class", "flatpickr-disabled"); |
| 1743 |
|
1456 |
|
| 1744 |
// Actually click day 15 to verify it's selectable |
1457 |
// Actually click day 15 to verify it's selectable |
| 1745 |
cy.get("@flatpickrInput") |
1458 |
cy.get("@flatpickrInput") |
| 1746 |
.getFlatpickrDate(today.add(15, "day").toDate()) |
1459 |
.selectFlatpickrDate(today.add(15, "day").toDate()); |
| 1747 |
.should("not.have.class", "flatpickr-disabled") |
|
|
| 1748 |
.click(); |
| 1749 |
|
| 1750 |
// Verify day 15 was selected as start date |
| 1751 |
cy.get("@flatpickrInput") |
| 1752 |
.getFlatpickrDate(today.add(15, "day").toDate()) |
| 1753 |
.should("have.class", "selected"); |
| 1754 |
|
1460 |
|
| 1755 |
// Reset for next scenario |
1461 |
// Reset for next scenario |
| 1756 |
cy.get("@flatpickrInput").clearFlatpickr(); |
1462 |
cy.get("@flatpickrInput").clearFlatpickr(); |
|
Lines 1781-1856
describe("Booking Modal Date Picker Tests", () => {
Link Here
|
| 1781 |
`/cgi-bin/koha/catalogue/detail.pl?biblionumber=${testBiblio.biblio_id}` |
1487 |
`/cgi-bin/koha/catalogue/detail.pl?biblionumber=${testBiblio.biblio_id}` |
| 1782 |
); |
1488 |
); |
| 1783 |
|
1489 |
|
| 1784 |
cy.get('[data-bs-target="#placeBookingModal"]') |
1490 |
cy.get("booking-modal-island .modal").should("exist"); |
| 1785 |
.first() |
1491 |
cy.get("[data-booking-modal]").first().then($btn => $btn[0].click()); |
| 1786 |
.click(); |
1492 |
cy.get("booking-modal-island .modal", { |
| 1787 |
cy.get("#placeBookingModal").should("be.visible"); |
1493 |
timeout: 10000, |
|
|
1494 |
}).should("be.visible"); |
| 1788 |
|
1495 |
|
| 1789 |
cy.selectFromSelect2( |
1496 |
cy.vueSelect( |
| 1790 |
"#booking_patron_id", |
1497 |
"booking_patron", |
| 1791 |
`${testPatron.surname}, ${testPatron.firstname}`, |
1498 |
testPatron.cardnumber, |
| 1792 |
testPatron.cardnumber |
1499 |
`${testPatron.surname} ${testPatron.firstname}` |
| 1793 |
); |
1500 |
); |
| 1794 |
cy.wait("@getPickupLocations"); |
1501 |
cy.wait("@getPickupLocations"); |
| 1795 |
|
1502 |
|
| 1796 |
cy.get("#pickup_library_id").should("not.be.disabled"); |
1503 |
cy.vueSelectShouldBeEnabled("pickup_library_id"); |
| 1797 |
cy.selectFromSelect2ByIndex("#pickup_library_id", 0); |
1504 |
cy.vueSelectByIndex("pickup_library_id", 0); |
| 1798 |
|
1505 |
|
| 1799 |
// Select itemtype BK |
1506 |
// Select itemtype BK |
| 1800 |
cy.get("#booking_itemtype").should("not.be.disabled"); |
1507 |
cy.vueSelectShouldBeEnabled("booking_itemtype"); |
| 1801 |
cy.selectFromSelect2("#booking_itemtype", "Books"); |
1508 |
cy.vueSelectByIndex("booking_itemtype", 0); |
| 1802 |
cy.wait("@getCirculationRules"); |
1509 |
cy.wait("@getCirculationRules"); |
| 1803 |
|
1510 |
|
| 1804 |
// Select "Any item" (index 0) |
1511 |
// "Any item" = no item selected (null) = leave dropdown at placeholder |
| 1805 |
cy.selectFromSelect2ByIndex("#booking_item_id", 0); |
|
|
| 1806 |
cy.get("#booking_item_id").should("have.value", "0"); |
| 1807 |
|
1512 |
|
| 1808 |
cy.get("#period").should("not.be.disabled"); |
1513 |
cy.get("#booking_period").should("not.be.disabled"); |
| 1809 |
cy.get("#period").as("flatpickrInput2"); |
1514 |
cy.get("#booking_period").as("flatpickrInput2"); |
| 1810 |
|
1515 |
|
| 1811 |
cy.get("@flatpickrInput2").openFlatpickr(); |
1516 |
cy.get("@flatpickrInput2").openFlatpickr(); |
| 1812 |
cy.get("@flatpickrInput2") |
1517 |
cy.get("@flatpickrInput2") |
| 1813 |
.getFlatpickrDate(today.add(15, "day").toDate()) |
1518 |
.hoverFlatpickrDate(today.add(15, "day").toDate()); |
| 1814 |
.trigger("mouseover"); |
|
|
| 1815 |
|
1519 |
|
| 1816 |
// Day 15 should NOW have leadDisable class (all items have conflicts) |
1520 |
// Day 15 should NOW be disabled (all items have conflicts) |
| 1817 |
cy.get("@flatpickrInput2") |
1521 |
cy.get("@flatpickrInput2") |
| 1818 |
.getFlatpickrDate(today.add(15, "day").toDate()) |
1522 |
.getFlatpickrDate(today.add(15, "day").toDate()) |
| 1819 |
.should("have.class", "leadDisable"); |
1523 |
.should("have.class", "flatpickr-disabled"); |
| 1820 |
|
1524 |
|
| 1821 |
// ================================================================ |
1525 |
// ================================================================ |
| 1822 |
// SCENARIO 3: Visual feedback - existingBookingTrail for days 13-14 |
1526 |
// SCENARIO 3: Visual feedback - booking marker dots for trail period |
| 1823 |
// ================================================================ |
1527 |
// ================================================================ |
| 1824 |
cy.log( |
1528 |
cy.log( |
| 1825 |
"=== Scenario 3: Visual feedback - Trail period display ===" |
1529 |
"=== Scenario 3: Visual feedback - Trail period marker dots ===" |
| 1826 |
); |
1530 |
); |
| 1827 |
|
1531 |
|
|
|
1532 |
// Days 13-14 should have trail marker dots |
| 1828 |
cy.get("@flatpickrInput2") |
1533 |
cy.get("@flatpickrInput2") |
| 1829 |
.getFlatpickrDate(today.add(13, "day").toDate()) |
1534 |
.getFlatpickrDate(today.add(13, "day").toDate()) |
| 1830 |
.should("have.class", "existingBookingTrail"); |
1535 |
.within(() => { |
|
|
1536 |
cy.get( |
| 1537 |
".booking-marker-grid .booking-marker-dot" |
| 1538 |
).should("exist"); |
| 1539 |
}); |
| 1831 |
|
1540 |
|
| 1832 |
cy.get("@flatpickrInput2") |
1541 |
cy.get("@flatpickrInput2") |
| 1833 |
.getFlatpickrDate(today.add(14, "day").toDate()) |
1542 |
.getFlatpickrDate(today.add(14, "day").toDate()) |
| 1834 |
.should("have.class", "existingBookingTrail"); |
1543 |
.within(() => { |
|
|
1544 |
cy.get( |
| 1545 |
".booking-marker-grid .booking-marker-dot" |
| 1546 |
).should("exist"); |
| 1547 |
}); |
| 1835 |
|
1548 |
|
| 1836 |
// ================================================================ |
1549 |
// ================================================================ |
| 1837 |
// SCENARIO 4: Visual feedback - existingBookingLead for days 8-9 |
1550 |
// SCENARIO 4: Visual feedback - booking marker dots for lead period |
| 1838 |
// ================================================================ |
1551 |
// ================================================================ |
| 1839 |
cy.log( |
1552 |
cy.log( |
| 1840 |
"=== Scenario 4: Visual feedback - Lead period display ===" |
1553 |
"=== Scenario 4: Visual feedback - Lead period marker dots ===" |
| 1841 |
); |
1554 |
); |
| 1842 |
|
1555 |
|
| 1843 |
cy.get("@flatpickrInput2") |
1556 |
cy.get("@flatpickrInput2") |
| 1844 |
.getFlatpickrDate(today.add(5, "day").toDate()) |
1557 |
.hoverFlatpickrDate(today.add(5, "day").toDate()); |
| 1845 |
.trigger("mouseover"); |
|
|
| 1846 |
|
1558 |
|
|
|
1559 |
// Days 8-9 should have lead marker dots |
| 1847 |
cy.get("@flatpickrInput2") |
1560 |
cy.get("@flatpickrInput2") |
| 1848 |
.getFlatpickrDate(today.add(8, "day").toDate()) |
1561 |
.getFlatpickrDate(today.add(8, "day").toDate()) |
| 1849 |
.should("have.class", "existingBookingLead"); |
1562 |
.within(() => { |
|
|
1563 |
cy.get( |
| 1564 |
".booking-marker-grid .booking-marker-dot" |
| 1565 |
).should("exist"); |
| 1566 |
}); |
| 1850 |
|
1567 |
|
| 1851 |
cy.get("@flatpickrInput2") |
1568 |
cy.get("@flatpickrInput2") |
| 1852 |
.getFlatpickrDate(today.add(9, "day").toDate()) |
1569 |
.getFlatpickrDate(today.add(9, "day").toDate()) |
| 1853 |
.should("have.class", "existingBookingLead"); |
1570 |
.within(() => { |
|
|
1571 |
cy.get( |
| 1572 |
".booking-marker-grid .booking-marker-dot" |
| 1573 |
).should("exist"); |
| 1574 |
}); |
| 1854 |
}); |
1575 |
}); |
| 1855 |
}); |
1576 |
}); |
| 1856 |
|
1577 |
|