|
Line 0
Link Here
|
| 0 |
- |
1 |
const dayjs = require("dayjs"); /* Cannot use our calendar JS code, it's in an include file (!) |
|
|
2 |
Also note that moment.js is deprecated */ |
| 3 |
const isSameOrBefore = require("dayjs/plugin/isSameOrBefore"); |
| 4 |
dayjs.extend(isSameOrBefore); |
| 5 |
|
| 6 |
describe("Booking Modal Tests", () => { |
| 7 |
// Common test data used across all tests |
| 8 |
const testData = { |
| 9 |
biblionumber: 134, |
| 10 |
patronId: 19, |
| 11 |
pickupLibraryId: "CPL", |
| 12 |
itemNumber: 287, |
| 13 |
itemTypeId: "BK", |
| 14 |
itemTypeDescription: "Book", |
| 15 |
startDate: dayjs().add(1, "day").startOf("day").toDate(), |
| 16 |
endDate: dayjs().add(5, "day").endOf("day").toDate(), |
| 17 |
}; |
| 18 |
|
| 19 |
// Shared mock data - will be populated in before() hook |
| 20 |
const sharedMockData = { |
| 21 |
patrons: [], |
| 22 |
items: [], |
| 23 |
libraries: [], |
| 24 |
bookings: [], |
| 25 |
}; |
| 26 |
|
| 27 |
// Mock data generators using cy.task() pattern - optimized for minimal data |
| 28 |
const generateMockPatrons = (count = 3, overridesArray = []) => { |
| 29 |
let chain = cy.wrap([]); |
| 30 |
|
| 31 |
// Define minimal patron data for test scenarios |
| 32 |
const defaultPatrons = [ |
| 33 |
{ |
| 34 |
patron_id: testData.patronId, // "19" - main test patron |
| 35 |
surname: "Doe", |
| 36 |
firstname: "John", |
| 37 |
cardnumber: "12345", |
| 38 |
library_id: testData.pickupLibraryId, |
| 39 |
category_id: "ADULT", |
| 40 |
date_of_birth: "1990-01-01", |
| 41 |
}, |
| 42 |
{ |
| 43 |
patron_id: 456, // For edit test and booking scenarios |
| 44 |
surname: "Smith", |
| 45 |
firstname: "Jane", |
| 46 |
cardnumber: "23456", |
| 47 |
library_id: testData.pickupLibraryId, |
| 48 |
category_id: "ADULT", |
| 49 |
date_of_birth: "1985-05-15", |
| 50 |
}, |
| 51 |
{ |
| 52 |
patron_id: 457, // For booking scenarios |
| 53 |
surname: "Johnson", |
| 54 |
firstname: "Bob", |
| 55 |
cardnumber: "34567", |
| 56 |
library_id: testData.pickupLibraryId, |
| 57 |
category_id: "ADULT", |
| 58 |
date_of_birth: "1988-08-20", |
| 59 |
}, |
| 60 |
]; |
| 61 |
|
| 62 |
for (let i = 0; i < count; i++) { |
| 63 |
const values = { |
| 64 |
...defaultPatrons[i % defaultPatrons.length], // Cycle through defaults |
| 65 |
...(overridesArray[i] || {}), |
| 66 |
}; |
| 67 |
|
| 68 |
chain = chain.then(patrons => { |
| 69 |
return cy |
| 70 |
.task("buildSampleObject", { |
| 71 |
object: "patron", |
| 72 |
values, |
| 73 |
}) |
| 74 |
.then(newPatron => { |
| 75 |
return [...patrons, newPatron]; |
| 76 |
}); |
| 77 |
}); |
| 78 |
} |
| 79 |
|
| 80 |
return chain; |
| 81 |
}; |
| 82 |
|
| 83 |
const generateMockItems = (count = 3, overridesArray = []) => { |
| 84 |
let chain = cy.wrap([]); |
| 85 |
|
| 86 |
for (let i = 0; i < count; i++) { |
| 87 |
const values = { |
| 88 |
effective_item_type_id: testData.itemTypeId, |
| 89 |
bookable: 1, |
| 90 |
...(overridesArray[i] || {}), |
| 91 |
}; |
| 92 |
|
| 93 |
chain = chain.then(items => { |
| 94 |
return cy |
| 95 |
.task("buildSampleObject", { |
| 96 |
object: "item", |
| 97 |
values, |
| 98 |
}) |
| 99 |
.then(newItem => { |
| 100 |
return [...items, newItem]; |
| 101 |
}); |
| 102 |
}); |
| 103 |
} |
| 104 |
|
| 105 |
return chain; |
| 106 |
}; |
| 107 |
|
| 108 |
const generateMockLibraries = (count = 2, overridesArray = []) => { |
| 109 |
let chain = cy.wrap([]); |
| 110 |
|
| 111 |
for (let i = 0; i < count; i++) { |
| 112 |
const values = { |
| 113 |
...(overridesArray[i] || {}), |
| 114 |
}; |
| 115 |
|
| 116 |
chain = chain.then(libraries => { |
| 117 |
return cy |
| 118 |
.task("buildSampleObject", { |
| 119 |
object: "library", |
| 120 |
values, |
| 121 |
}) |
| 122 |
.then(newLibrary => { |
| 123 |
return [...libraries, newLibrary]; |
| 124 |
}); |
| 125 |
}); |
| 126 |
} |
| 127 |
|
| 128 |
return chain; |
| 129 |
}; |
| 130 |
|
| 131 |
const generateMockBookings = (count = 3, overridesArray = []) => { |
| 132 |
let chain = cy.wrap([]); |
| 133 |
|
| 134 |
for (let i = 0; i < count; i++) { |
| 135 |
const values = { |
| 136 |
biblio_id: testData.biblionumber, |
| 137 |
status: "pending", |
| 138 |
...(overridesArray[i] || {}), |
| 139 |
}; |
| 140 |
|
| 141 |
chain = chain.then(bookings => { |
| 142 |
return cy |
| 143 |
.task("buildSampleObject", { |
| 144 |
object: "booking", |
| 145 |
values, |
| 146 |
}) |
| 147 |
.then(newBooking => { |
| 148 |
return [...bookings, newBooking]; |
| 149 |
}); |
| 150 |
}); |
| 151 |
} |
| 152 |
|
| 153 |
return chain; |
| 154 |
}; |
| 155 |
|
| 156 |
// Helper functions for API intercepts using shared mock data |
| 157 |
const setupStaticApiIntercepts = () => { |
| 158 |
// Use shared mock data to set up intercepts |
| 159 |
cy.intercept("GET", "/api/v1/biblios/*/items?bookable=1&_per_page=-1", { |
| 160 |
body: sharedMockData.items, |
| 161 |
}).as("getBookableItems"); |
| 162 |
|
| 163 |
cy.intercept("GET", "/api/v1/biblios/*/checkouts?_per_page=-1", { |
| 164 |
body: [], // No checkouts for simplicity |
| 165 |
}).as("getCheckouts"); |
| 166 |
|
| 167 |
// Set up patron GET endpoint to return the appropriate patron by ID |
| 168 |
cy.intercept("GET", "/api/v1/patrons/*", req => { |
| 169 |
const patronId = req.url.split("/").pop(); |
| 170 |
const patron = sharedMockData.patrons.find( |
| 171 |
p => p.patron_id === patronId |
| 172 |
); |
| 173 |
req.reply({ |
| 174 |
body: patron || sharedMockData.patrons[0], // Fallback to first patron if not found |
| 175 |
}); |
| 176 |
}).as("getPatron"); |
| 177 |
|
| 178 |
// Mock patron search with all patrons |
| 179 |
cy.intercept("GET", "/api/v1/patrons*", { |
| 180 |
body: sharedMockData.patrons, |
| 181 |
pagination: { more: false }, |
| 182 |
}).as("searchPatrons"); |
| 183 |
|
| 184 |
cy.intercept("GET", "/api/v1/biblios/*/pickup_locations*", { |
| 185 |
body: sharedMockData.libraries, |
| 186 |
}).as("getPickupLocations"); |
| 187 |
|
| 188 |
// Mock circulation rules |
| 189 |
cy.intercept("GET", "/api/v1/circulation_rules*", { |
| 190 |
body: [ |
| 191 |
{ |
| 192 |
bookings_lead_period: 2, |
| 193 |
bookings_trail_period: 1, |
| 194 |
issuelength: 14, |
| 195 |
renewalsallowed: 2, |
| 196 |
renewalperiod: 7, |
| 197 |
}, |
| 198 |
], |
| 199 |
}).as("getCirculationRules"); |
| 200 |
}; |
| 201 |
|
| 202 |
const setupBookingIntercepts = () => { |
| 203 |
cy.intercept("POST", "/api/v1/bookings", { |
| 204 |
statusCode: 201, |
| 205 |
body: { |
| 206 |
booking_id: 1001, |
| 207 |
start_date: testData.startDate.toISOString(), |
| 208 |
end_date: testData.endDate.toISOString(), |
| 209 |
pickup_library_id: testData.pickupLibraryId, |
| 210 |
biblio_id: testData.biblionumber, |
| 211 |
item_id: testData.itemNumber, |
| 212 |
patron_id: testData.patronId, |
| 213 |
}, |
| 214 |
}).as("createBooking"); |
| 215 |
|
| 216 |
cy.intercept("PUT", "/api/v1/bookings/*", { |
| 217 |
statusCode: 200, |
| 218 |
body: { |
| 219 |
booking_id: 1001, |
| 220 |
start_date: testData.startDate.toISOString(), |
| 221 |
end_date: testData.endDate.toISOString(), |
| 222 |
pickup_library_id: testData.pickupLibraryId, |
| 223 |
biblio_id: testData.biblionumber, |
| 224 |
item_id: testData.itemNumber, |
| 225 |
patron_id: testData.patronId, |
| 226 |
}, |
| 227 |
}).as("updateBooking"); |
| 228 |
}; |
| 229 |
|
| 230 |
const processDefaultBookings = (bookings: any[], today: any) => { |
| 231 |
// Update the dates in the fixture data relative to today |
| 232 |
bookings[0].start_date = today |
| 233 |
.add(8, "day") |
| 234 |
.startOf("day") |
| 235 |
.toISOString(); // Today + 8 days at 00:00 |
| 236 |
bookings[0].end_date = today.add(13, "day").endOf("day").toISOString(); // Today + 13 days at 23:59 |
| 237 |
|
| 238 |
bookings[1].start_date = today |
| 239 |
.add(14, "day") |
| 240 |
.startOf("day") |
| 241 |
.toISOString(); // Today + 14 days at 00:00 |
| 242 |
bookings[1].end_date = today.add(18, "day").endOf("day").toISOString(); // Today + 18 days at 23:59 |
| 243 |
|
| 244 |
bookings[2].start_date = today |
| 245 |
.add(28, "day") |
| 246 |
.startOf("day") |
| 247 |
.toISOString(); // Today + 28 days at 00:00 |
| 248 |
bookings[2].end_date = today.add(33, "day").endOf("day").toISOString(); // Today + 33 days at 23:59 |
| 249 |
|
| 250 |
return bookings; |
| 251 |
}; |
| 252 |
|
| 253 |
const setupDynamicBookings = (customBookings?: any[]) => { |
| 254 |
const today = dayjs(); |
| 255 |
|
| 256 |
if (customBookings) { |
| 257 |
// Use provided bookings directly |
| 258 |
cy.intercept("GET", "/api/v1/bookings?biblio_id=*&_per_page=-1*", { |
| 259 |
body: customBookings, |
| 260 |
}).as("getBookings"); |
| 261 |
} else { |
| 262 |
// Use shared mock bookings with dynamic dates |
| 263 |
const processedBookings = processDefaultBookings( |
| 264 |
sharedMockData.bookings, |
| 265 |
today |
| 266 |
); |
| 267 |
cy.intercept("GET", "/api/v1/bookings?biblio_id=*&_per_page=-1*", { |
| 268 |
body: processedBookings, |
| 269 |
}).as("getBookings"); |
| 270 |
} |
| 271 |
}; |
| 272 |
|
| 273 |
const setupModalTriggerButton = () => { |
| 274 |
cy.document().then(doc => { |
| 275 |
const button = doc.createElement("button"); |
| 276 |
button.setAttribute("data-bs-toggle", "modal"); |
| 277 |
button.setAttribute("data-bs-target", "#placeBookingModal"); |
| 278 |
button.setAttribute("data-biblionumber", testData.biblionumber); |
| 279 |
button.setAttribute("id", "placebooking"); |
| 280 |
doc.body.appendChild(button); |
| 281 |
}); |
| 282 |
}; |
| 283 |
|
| 284 |
const setupCustomItems = ( |
| 285 |
items: any[], |
| 286 |
itemTypeId: string, |
| 287 |
itemTypeDescription: string |
| 288 |
) => { |
| 289 |
const itemPromises = items.map(item => { |
| 290 |
return cy |
| 291 |
.task("buildSampleObject", { |
| 292 |
object: "item", |
| 293 |
values: { |
| 294 |
item_id: item.item_id, |
| 295 |
external_id: item.barcode, |
| 296 |
effective_item_type_id: itemTypeId, |
| 297 |
bookable: 1, |
| 298 |
location: "Main Library", |
| 299 |
callnumber: "TEST.CALL.NUMBER", |
| 300 |
status: "Available", |
| 301 |
}, |
| 302 |
}) |
| 303 |
.then(mockItem => { |
| 304 |
// Add item type info |
| 305 |
mockItem.item_type = { |
| 306 |
item_type_id: itemTypeId, |
| 307 |
description: itemTypeDescription, |
| 308 |
}; |
| 309 |
return mockItem; |
| 310 |
}); |
| 311 |
}); |
| 312 |
|
| 313 |
// Wait for all items to be generated, then set up intercept |
| 314 |
return cy.wrap(Promise.all(itemPromises)).then(mockItems => { |
| 315 |
cy.intercept("GET", "/api/v1/biblios/*/items?bookable=1*", { |
| 316 |
body: mockItems, |
| 317 |
}).as("getBookableItems"); |
| 318 |
}); |
| 319 |
}; |
| 320 |
|
| 321 |
const setupCustomPickupLocations = (items: any[]) => { |
| 322 |
return generateMockLibraries(2, { |
| 323 |
library_id: ["MAIN", "BRANCH"], |
| 324 |
name: ["Main Library", "Branch Library"], |
| 325 |
needs_override: [false, false], |
| 326 |
}).then(mockLocations => { |
| 327 |
// Add pickup items to each location |
| 328 |
const modifiedLocations = mockLocations.map(location => ({ |
| 329 |
...location, |
| 330 |
pickup_items: [ |
| 331 |
...items.map(item => parseInt(item.item_id, 10)), |
| 332 |
], |
| 333 |
})); |
| 334 |
|
| 335 |
cy.intercept("GET", "/api/v1/biblios/*/pickup_locations*", { |
| 336 |
body: modifiedLocations, |
| 337 |
}).as("getPickupLocations"); |
| 338 |
}); |
| 339 |
}; |
| 340 |
|
| 341 |
// Common setup functions for test workflows |
| 342 |
const openModalAndWait = ( |
| 343 |
apiCalls: string[] = [ |
| 344 |
"@getBookableItems", |
| 345 |
"@getBookings", |
| 346 |
"@getCheckouts", |
| 347 |
] |
| 348 |
) => { |
| 349 |
cy.get("#placebooking").click(); |
| 350 |
if (apiCalls.length > 0) { |
| 351 |
cy.wait(apiCalls); |
| 352 |
} |
| 353 |
}; |
| 354 |
|
| 355 |
const setupModalForBasicTesting = () => { |
| 356 |
openModalAndWait(); |
| 357 |
// Basic setup - just open modal and wait for initial data |
| 358 |
}; |
| 359 |
|
| 360 |
const setupModalForDateTesting = () => { |
| 361 |
openModalAndWait(); |
| 362 |
// Select patron, pickup location and item to enable date picker |
| 363 |
selectPatron(0, "John"); |
| 364 |
selectLocation(0); |
| 365 |
selectItem(1); |
| 366 |
}; |
| 367 |
|
| 368 |
const setupModalForFormSubmission = () => { |
| 369 |
setupModalForDateTesting(); |
| 370 |
// Set dates with flatpickr to prepare for submission |
| 371 |
cy.window().then(win => { |
| 372 |
const picker = win.document.getElementById("period")._flatpickr; |
| 373 |
const startDate = new Date(testData.startDate); |
| 374 |
const endDate = new Date(testData.endDate); |
| 375 |
picker.setDate([startDate, endDate], true); |
| 376 |
}); |
| 377 |
}; |
| 378 |
|
| 379 |
const selectPatron = ( |
| 380 |
patronIndex: number = 0, |
| 381 |
patronSearchTerm: string = "John" |
| 382 |
) => { |
| 383 |
cy.selectFromSelect2ByIndex( |
| 384 |
"#booking_patron_id", |
| 385 |
patronIndex, |
| 386 |
patronSearchTerm |
| 387 |
); |
| 388 |
// Wait for pickup locations API to complete |
| 389 |
cy.wait("@getPickupLocations"); |
| 390 |
}; |
| 391 |
|
| 392 |
const selectLocation = (locationIndex: number = 0) => { |
| 393 |
// Ensure the field is enabled before attempting selection |
| 394 |
cy.get("#pickup_library_id").should("not.be.disabled"); |
| 395 |
cy.selectFromSelect2ByIndex("#pickup_library_id", locationIndex); |
| 396 |
}; |
| 397 |
|
| 398 |
const selectItem = (itemIndex: number) => { |
| 399 |
// Ensure the field is enabled before attempting selection |
| 400 |
cy.get("#booking_item_id").should("not.be.disabled"); |
| 401 |
cy.selectFromSelect2ByIndex("#booking_item_id", itemIndex); |
| 402 |
// Wait for circulation rules API and period field to be enabled |
| 403 |
cy.wait("@getCirculationRules"); |
| 404 |
cy.get("#period").should("not.be.disabled"); |
| 405 |
}; |
| 406 |
|
| 407 |
const selectItemType = (itemTypeIndex: number) => { |
| 408 |
// Ensure the field is enabled before attempting selection |
| 409 |
cy.get("#booking_itemtype").should("not.be.disabled"); |
| 410 |
cy.selectFromSelect2ByIndex("#booking_itemtype", itemTypeIndex); |
| 411 |
// Wait for circulation rules API and period field to be enabled |
| 412 |
cy.wait("@getCirculationRules"); |
| 413 |
cy.get("#period").should("not.be.disabled"); |
| 414 |
}; |
| 415 |
|
| 416 |
const openModalWithoutWait = () => { |
| 417 |
cy.get("#placebooking").click(); |
| 418 |
// Only wait for modal to be visible, not for API calls |
| 419 |
cy.get("#placeBookingModal").should("be.visible"); |
| 420 |
}; |
| 421 |
|
| 422 |
const resetModalAndReopen = () => { |
| 423 |
cy.get('#placeBookingModal button[data-bs-dismiss="modal"]') |
| 424 |
.first() |
| 425 |
.click(); |
| 426 |
// Wait for modal to close |
| 427 |
cy.get("#placeBookingModal").should("not.be.visible"); |
| 428 |
// Reopen modal without waiting for API calls (they're cached) |
| 429 |
openModalWithoutWait(); |
| 430 |
}; |
| 431 |
|
| 432 |
// Generate shared mock data once before all tests |
| 433 |
before(() => { |
| 434 |
// Generate minimal shared mock data |
| 435 |
generateMockPatrons(3).then(patrons => { |
| 436 |
sharedMockData.patrons = patrons; |
| 437 |
}); |
| 438 |
|
| 439 |
generateMockItems(3, [ |
| 440 |
{ |
| 441 |
item_id: 789, |
| 442 |
external_id: "BARCODE789", |
| 443 |
effective_item_type_id: testData.itemTypeId, |
| 444 |
item_type: { |
| 445 |
item_type_id: testData.itemTypeId, |
| 446 |
description: testData.itemTypeDescription, |
| 447 |
}, |
| 448 |
}, |
| 449 |
{ |
| 450 |
item_id: 790, |
| 451 |
external_id: "BARCODE790", |
| 452 |
effective_item_type_id: testData.itemTypeId, |
| 453 |
item_type: { |
| 454 |
item_type_id: testData.itemTypeId, |
| 455 |
description: testData.itemTypeDescription, |
| 456 |
}, |
| 457 |
}, |
| 458 |
{ |
| 459 |
item_id: 791, |
| 460 |
external_id: "BARCODE791", |
| 461 |
effective_item_type_id: "DVD", |
| 462 |
item_type: { |
| 463 |
item_type_id: "DVD", |
| 464 |
description: "DVD", |
| 465 |
}, |
| 466 |
}, |
| 467 |
]).then(items => { |
| 468 |
sharedMockData.items = items; |
| 469 |
}); |
| 470 |
|
| 471 |
generateMockLibraries(2, [ |
| 472 |
{ |
| 473 |
library_id: "MAIN", |
| 474 |
name: "Main Library", |
| 475 |
pickup_items: [789, 790], |
| 476 |
}, |
| 477 |
{ |
| 478 |
library_id: "BRANCH", |
| 479 |
name: "Branch Library", |
| 480 |
pickup_items: [789, 791], |
| 481 |
}, |
| 482 |
]).then(libraries => { |
| 483 |
sharedMockData.libraries = libraries; |
| 484 |
}); |
| 485 |
|
| 486 |
generateMockBookings(3, [ |
| 487 |
{ |
| 488 |
booking_id: 1001, |
| 489 |
patron_id: 456, |
| 490 |
item_id: 789, |
| 491 |
pickup_library_id: "MAIN", |
| 492 |
}, |
| 493 |
{ |
| 494 |
booking_id: 1002, |
| 495 |
patron_id: 457, |
| 496 |
item_id: 790, |
| 497 |
pickup_library_id: "MAIN", |
| 498 |
}, |
| 499 |
{ |
| 500 |
booking_id: 1003, |
| 501 |
patron_id: 456, |
| 502 |
item_id: 791, |
| 503 |
pickup_library_id: "MAIN", |
| 504 |
}, |
| 505 |
]).then(bookings => { |
| 506 |
sharedMockData.bookings = bookings; |
| 507 |
}); |
| 508 |
}); |
| 509 |
|
| 510 |
beforeEach(() => { |
| 511 |
cy.login(); |
| 512 |
cy.title().should("eq", "Koha staff interface"); |
| 513 |
|
| 514 |
// Visit the page with the booking modal |
| 515 |
cy.visit( |
| 516 |
"/cgi-bin/koha/catalogue/detail.pl?biblionumber=" + |
| 517 |
testData.biblionumber |
| 518 |
); |
| 519 |
|
| 520 |
// Setup all API intercepts using helper functions |
| 521 |
setupStaticApiIntercepts(); |
| 522 |
setupDynamicBookings(); |
| 523 |
setupBookingIntercepts(); |
| 524 |
setupModalTriggerButton(); |
| 525 |
}); |
| 526 |
|
| 527 |
it("should load the booking modal correctly", () => { |
| 528 |
setupModalForBasicTesting(); |
| 529 |
|
| 530 |
// Check modal title |
| 531 |
cy.get("#placeBookingLabel").should("contain", "Place booking"); |
| 532 |
|
| 533 |
// Check form elements are present |
| 534 |
cy.get("#booking_patron_id").should("exist"); |
| 535 |
cy.get("#pickup_library_id").should("exist"); |
| 536 |
cy.get("#booking_itemtype").should("exist"); |
| 537 |
cy.get("#booking_item_id").should("exist"); |
| 538 |
cy.get("#period").should("exist"); |
| 539 |
|
| 540 |
// Check hidden fields |
| 541 |
cy.get("#booking_biblio_id").should( |
| 542 |
"have.value", |
| 543 |
testData.biblionumber |
| 544 |
); |
| 545 |
cy.get("#booking_start_date").should("have.value", ""); |
| 546 |
cy.get("#booking_end_date").should("have.value", ""); |
| 547 |
}); |
| 548 |
|
| 549 |
it("should enable fields in proper sequence", () => { |
| 550 |
// Open the booking modal and wait for initial data |
| 551 |
openModalAndWait(); |
| 552 |
|
| 553 |
// Initially only patron field should be enabled |
| 554 |
cy.get("#booking_patron_id").should("not.be.disabled"); |
| 555 |
cy.get("#pickup_library_id").should("be.disabled"); |
| 556 |
cy.get("#booking_itemtype").should("be.disabled"); |
| 557 |
cy.get("#booking_item_id").should("be.disabled"); |
| 558 |
cy.get("#period").should("be.disabled"); |
| 559 |
|
| 560 |
// Select patron |
| 561 |
cy.selectFromSelect2ByIndex("#booking_patron_id", 0, "John"); |
| 562 |
cy.wait("@getPickupLocations"); |
| 563 |
|
| 564 |
// After patron selection, pickup location, item type and item should be enabled |
| 565 |
cy.get("#pickup_library_id").should("not.be.disabled"); |
| 566 |
cy.get("#booking_itemtype").should("not.be.disabled"); |
| 567 |
cy.get("#booking_item_id").should("not.be.disabled"); |
| 568 |
cy.get("#period").should("be.disabled"); |
| 569 |
|
| 570 |
// Select pickup location |
| 571 |
cy.selectFromSelect2ByIndex("#pickup_library_id", 0); |
| 572 |
|
| 573 |
// Select item type, trigger circulation rules |
| 574 |
cy.selectFromSelect2ByIndex("#booking_itemtype", 0); |
| 575 |
cy.wait("@getCirculationRules"); |
| 576 |
|
| 577 |
// After patron, pickup location and itemtype/item selection, date picker should be enabled |
| 578 |
cy.get("#period").should("not.be.disabled"); |
| 579 |
|
| 580 |
// Clear item type and confirm period is disabled |
| 581 |
cy.clearSelect2("#booking_itemtype"); |
| 582 |
cy.get("#period").should("be.disabled"); |
| 583 |
|
| 584 |
// Select item, re-enable period |
| 585 |
cy.selectFromSelect2ByIndex("#booking_item_id", 1); |
| 586 |
cy.get("#period").should("not.be.disabled"); |
| 587 |
}); |
| 588 |
|
| 589 |
it("should handle item type and item dependencies correctly", () => { |
| 590 |
setupModalForBasicTesting(); |
| 591 |
|
| 592 |
// Select patron and pickup location first |
| 593 |
selectPatron(); |
| 594 |
selectLocation(); |
| 595 |
|
| 596 |
// Select an item first |
| 597 |
selectItem(1); |
| 598 |
|
| 599 |
// Verify that item type gets selected automatically |
| 600 |
cy.get("#booking_itemtype").should("have.value", testData.itemTypeId); |
| 601 |
|
| 602 |
// Verify that item type gets disabled |
| 603 |
cy.get("#booking_itemtype").should("be.disabled"); |
| 604 |
|
| 605 |
// Reset the modal |
| 606 |
resetModalAndReopen(); |
| 607 |
|
| 608 |
// Now select patron, pickup and item type first |
| 609 |
selectPatron(); |
| 610 |
selectLocation(); |
| 611 |
selectItemType(0); |
| 612 |
cy.wait(300); |
| 613 |
|
| 614 |
// Verify that only 'Any item' option and items of selected type are enabled |
| 615 |
cy.get("#booking_item_id > option").then($options => { |
| 616 |
const enabledOptions = $options.filter(":not(:disabled)"); |
| 617 |
enabledOptions.each(function () { |
| 618 |
const $option = cy.wrap(this); |
| 619 |
|
| 620 |
// Get both the value and the data-itemtype attribute to make decisions |
| 621 |
$option.invoke("val").then(value => { |
| 622 |
if (value === "0") { |
| 623 |
// We need to re-wrap the element since invoke('val') changed the subject |
| 624 |
cy.wrap(this).should("contain.text", "Any item"); |
| 625 |
} else { |
| 626 |
// Re-wrap the element again for this assertion |
| 627 |
cy.wrap(this).should( |
| 628 |
"have.attr", |
| 629 |
"data-itemtype", |
| 630 |
testData.itemTypeId |
| 631 |
); |
| 632 |
} |
| 633 |
}); |
| 634 |
}); |
| 635 |
}); |
| 636 |
}); |
| 637 |
|
| 638 |
it("should disable dates before today and between today and selected start date", () => { |
| 639 |
// Test-specific data for date validation |
| 640 |
const today = dayjs(); |
| 641 |
const dateTestData = { |
| 642 |
startDate: today.add(5, "day"), |
| 643 |
testRanges: { |
| 644 |
beforeToday: { start: today.subtract(7, "day"), end: today }, |
| 645 |
afterToday: { |
| 646 |
start: today.add(1, "day"), |
| 647 |
end: today.add(7, "day"), |
| 648 |
}, |
| 649 |
betweenTodayAndStart: { |
| 650 |
start: today.add(1, "day"), |
| 651 |
end: today.add(4, "day"), |
| 652 |
}, |
| 653 |
afterStartDate: { |
| 654 |
start: today.add(6, "day"), |
| 655 |
end: today.add(10, "day"), |
| 656 |
}, |
| 657 |
}, |
| 658 |
}; |
| 659 |
|
| 660 |
// Setup modal for date testing |
| 661 |
setupModalForDateTesting(); |
| 662 |
|
| 663 |
cy.get("#period").as("flatpickrInput"); |
| 664 |
cy.get("@flatpickrInput").openFlatpickr(); |
| 665 |
|
| 666 |
cy.get("@flatpickrInput").then($el => { |
| 667 |
// Phase 1: Test that all dates prior to today are disabled |
| 668 |
cy.log("Testing dates prior to today are disabled"); |
| 669 |
|
| 670 |
// Find the first visible date in the calendar to determine range |
| 671 |
cy.get(".flatpickr-day:not(.hidden)") |
| 672 |
.first() |
| 673 |
.then($firstDay => { |
| 674 |
const firstDate = dayjs($firstDay.attr("aria-label")); |
| 675 |
|
| 676 |
// Check all dates from first visible date up to today are disabled |
| 677 |
for ( |
| 678 |
let checkDate = firstDate; |
| 679 |
checkDate.isSameOrBefore(today, "day"); |
| 680 |
checkDate = checkDate.add(1, "day") |
| 681 |
) { |
| 682 |
cy.get("@flatpickrInput") |
| 683 |
.getFlatpickrDate(checkDate.toDate()) |
| 684 |
.should("have.class", "flatpickr-disabled"); |
| 685 |
} |
| 686 |
}); |
| 687 |
|
| 688 |
// Phase 2: Test that dates after today are initially enabled |
| 689 |
cy.log("Testing dates after today are initially enabled"); |
| 690 |
|
| 691 |
// Test a broader range of future dates for better coverage |
| 692 |
for ( |
| 693 |
let checkDate = today.add(1, "day"); |
| 694 |
checkDate.isSameOrBefore(today.add(7, "day"), "day"); |
| 695 |
checkDate = checkDate.add(1, "day") |
| 696 |
) { |
| 697 |
cy.get("@flatpickrInput") |
| 698 |
.getFlatpickrDate(checkDate.toDate()) |
| 699 |
.should("not.have.class", "flatpickr-disabled"); |
| 700 |
} |
| 701 |
}); |
| 702 |
|
| 703 |
// Phase 3: Select a start date |
| 704 |
cy.log( |
| 705 |
`Selecting start date (${dateTestData.startDate.format("YYYY-MM-DD")})` |
| 706 |
); |
| 707 |
cy.get("@flatpickrInput").selectFlatpickrDate( |
| 708 |
dateTestData.startDate.toDate() |
| 709 |
); |
| 710 |
|
| 711 |
// Phase 4: Verify dates between today and start date are now disabled |
| 712 |
cy.log("Testing dates between today and start date are disabled"); |
| 713 |
|
| 714 |
cy.get("@flatpickrInput").then($el => { |
| 715 |
for ( |
| 716 |
let checkDate = |
| 717 |
dateTestData.testRanges.betweenTodayAndStart.start; |
| 718 |
checkDate.isBefore(dateTestData.startDate, "day"); |
| 719 |
checkDate = checkDate.add(1, "day") |
| 720 |
) { |
| 721 |
cy.get("@flatpickrInput") |
| 722 |
.getFlatpickrDate(checkDate.toDate()) |
| 723 |
.should("have.class", "flatpickr-disabled"); |
| 724 |
} |
| 725 |
|
| 726 |
// Verify the selected start date itself is properly selected and not disabled |
| 727 |
cy.get("@flatpickrInput") |
| 728 |
.getFlatpickrDate(dateTestData.startDate.toDate()) |
| 729 |
.should("not.have.class", "flatpickr-disabled") |
| 730 |
.and("have.class", "selected"); |
| 731 |
}); |
| 732 |
|
| 733 |
// Phase 5: Verify dates after the start date remain enabled |
| 734 |
cy.log("Testing dates after start date remain enabled"); |
| 735 |
|
| 736 |
cy.get("@flatpickrInput").then($el => { |
| 737 |
for ( |
| 738 |
let checkDate = dateTestData.testRanges.afterStartDate.start; |
| 739 |
checkDate.isSameOrBefore( |
| 740 |
dateTestData.testRanges.afterStartDate.end, |
| 741 |
"day" |
| 742 |
); |
| 743 |
checkDate = checkDate.add(1, "day") |
| 744 |
) { |
| 745 |
cy.get("@flatpickrInput") |
| 746 |
.getFlatpickrDate(checkDate.toDate()) |
| 747 |
.should("not.have.class", "flatpickr-disabled"); |
| 748 |
} |
| 749 |
}); |
| 750 |
}); |
| 751 |
|
| 752 |
it("should disable dates with existing bookings for same item", () => { |
| 753 |
// Test-specific data |
| 754 |
const today = dayjs(); |
| 755 |
const testItemData = { |
| 756 |
itemId: 789, |
| 757 |
itemBarcode: "BARCODE789", |
| 758 |
bookingPeriods: [ |
| 759 |
{ |
| 760 |
start: today.add(8, "day"), |
| 761 |
end: today.add(13, "day"), |
| 762 |
name: "First booking period", |
| 763 |
}, |
| 764 |
{ |
| 765 |
start: today.add(14, "day"), |
| 766 |
end: today.add(18, "day"), |
| 767 |
name: "Second booking period", |
| 768 |
}, |
| 769 |
{ |
| 770 |
start: today.add(28, "day"), |
| 771 |
end: today.add(33, "day"), |
| 772 |
name: "Third booking period", |
| 773 |
}, |
| 774 |
{ |
| 775 |
start: today.add(35, "day"), |
| 776 |
end: today.add(37, "day"), |
| 777 |
name: "Fourth booking period", |
| 778 |
}, |
| 779 |
], |
| 780 |
otherItemBooking: { |
| 781 |
start: today.add(20, "day"), |
| 782 |
end: today.add(25, "day"), |
| 783 |
}, |
| 784 |
}; |
| 785 |
|
| 786 |
const TEST_ITEM_ID = testItemData.itemId; |
| 787 |
const TEST_ITEM_BARCODE = testItemData.itemBarcode; |
| 788 |
|
| 789 |
// Create custom bookings for this test using our helper |
| 790 |
const createCustomBookings = (baseBookings: any[]) => { |
| 791 |
// Modify existing bookings to create a comprehensive test scenario |
| 792 |
// All bookings will be for the same item to test date conflicts |
| 793 |
baseBookings[0].item_id = TEST_ITEM_ID; |
| 794 |
baseBookings[0].start_date = today |
| 795 |
.add(8, "day") |
| 796 |
.startOf("day") |
| 797 |
.toISOString(); |
| 798 |
baseBookings[0].end_date = today |
| 799 |
.add(13, "day") |
| 800 |
.endOf("day") |
| 801 |
.toISOString(); |
| 802 |
|
| 803 |
baseBookings[1].item_id = TEST_ITEM_ID; |
| 804 |
baseBookings[1].start_date = today |
| 805 |
.add(14, "day") |
| 806 |
.startOf("day") |
| 807 |
.toISOString(); |
| 808 |
baseBookings[1].end_date = today |
| 809 |
.add(18, "day") |
| 810 |
.endOf("day") |
| 811 |
.toISOString(); |
| 812 |
|
| 813 |
baseBookings[2].item_id = TEST_ITEM_ID; |
| 814 |
baseBookings[2].start_date = today |
| 815 |
.add(28, "day") |
| 816 |
.startOf("day") |
| 817 |
.toISOString(); |
| 818 |
baseBookings[2].end_date = today |
| 819 |
.add(33, "day") |
| 820 |
.endOf("day") |
| 821 |
.toISOString(); |
| 822 |
|
| 823 |
// Add additional bookings for comprehensive testing |
| 824 |
const additionalBookings = [ |
| 825 |
{ |
| 826 |
booking_id: 1004, |
| 827 |
biblio_id: 123, |
| 828 |
patron_id: 459, |
| 829 |
item_id: TEST_ITEM_ID, |
| 830 |
pickup_library_id: "MAIN", |
| 831 |
start_date: today |
| 832 |
.add(35, "day") |
| 833 |
.startOf("day") |
| 834 |
.toISOString(), |
| 835 |
end_date: today.add(37, "day").endOf("day").toISOString(), |
| 836 |
status: "pending", |
| 837 |
}, |
| 838 |
{ |
| 839 |
booking_id: 1005, |
| 840 |
biblio_id: 123, |
| 841 |
patron_id: 460, |
| 842 |
item_id: "different_item", // Different item - should not affect our test |
| 843 |
pickup_library_id: "MAIN", |
| 844 |
start_date: today |
| 845 |
.add(20, "day") |
| 846 |
.startOf("day") |
| 847 |
.toISOString(), |
| 848 |
end_date: today.add(25, "day").endOf("day").toISOString(), |
| 849 |
status: "pending", |
| 850 |
}, |
| 851 |
]; |
| 852 |
|
| 853 |
return [...baseBookings, ...additionalBookings]; |
| 854 |
}; |
| 855 |
|
| 856 |
// Setup custom bookings using our helper function |
| 857 |
generateMockBookings(3, [ |
| 858 |
{ |
| 859 |
booking_id: 1001, |
| 860 |
patron_id: 456, |
| 861 |
item_id: 789, |
| 862 |
}, |
| 863 |
{ |
| 864 |
booking_id: 1002, |
| 865 |
patron_id: 457, |
| 866 |
item_id: 790, |
| 867 |
}, |
| 868 |
{ |
| 869 |
booking_id: 1003, |
| 870 |
patron_id: 458, |
| 871 |
item_id: 791, |
| 872 |
}, |
| 873 |
]).then(baseBookings => { |
| 874 |
const customBookings = createCustomBookings(baseBookings); |
| 875 |
setupDynamicBookings(customBookings); |
| 876 |
}); |
| 877 |
|
| 878 |
// Setup modal for date testing |
| 879 |
setupModalForDateTesting(); |
| 880 |
|
| 881 |
// Select the specific item that has existing bookings (TEST_ITEM_BARCODE) |
| 882 |
cy.selectFromSelect2("#booking_item_id", TEST_ITEM_BARCODE); |
| 883 |
|
| 884 |
cy.get("#period").as("flatpickrInput"); |
| 885 |
cy.get("@flatpickrInput").openFlatpickr(); |
| 886 |
|
| 887 |
// Define booking periods for the selected item only |
| 888 |
const bookingPeriodsForSelectedItem = [ |
| 889 |
{ |
| 890 |
name: "First booking period", |
| 891 |
start: today.add(8, "day"), |
| 892 |
end: today.add(13, "day"), |
| 893 |
}, |
| 894 |
{ |
| 895 |
name: "Second booking period", |
| 896 |
start: today.add(14, "day"), |
| 897 |
end: today.add(18, "day"), |
| 898 |
}, |
| 899 |
{ |
| 900 |
name: "Third booking period", |
| 901 |
start: today.add(28, "day"), |
| 902 |
end: today.add(33, "day"), |
| 903 |
}, |
| 904 |
{ |
| 905 |
name: "Fourth booking period", |
| 906 |
start: today.add(35, "day"), |
| 907 |
end: today.add(37, "day"), |
| 908 |
}, |
| 909 |
]; |
| 910 |
|
| 911 |
cy.get("@flatpickrInput").then($el => { |
| 912 |
// Phase 1: Test dates before first booking period are available |
| 913 |
cy.log("Testing dates before first booking period are available"); |
| 914 |
|
| 915 |
for ( |
| 916 |
let checkDate = today.add(1, "day"); |
| 917 |
checkDate.isBefore( |
| 918 |
bookingPeriodsForSelectedItem[0].start, |
| 919 |
"day" |
| 920 |
); |
| 921 |
checkDate = checkDate.add(1, "day") |
| 922 |
) { |
| 923 |
cy.get("@flatpickrInput") |
| 924 |
.getFlatpickrDate(checkDate.toDate()) |
| 925 |
.should("not.have.class", "flatpickr-disabled"); |
| 926 |
} |
| 927 |
|
| 928 |
// Phase 2: Test each booked period individually |
| 929 |
bookingPeriodsForSelectedItem.forEach((period, index) => { |
| 930 |
cy.log(`Testing ${period.name} dates are disabled`); |
| 931 |
|
| 932 |
// Test dates within the booked range are disabled |
| 933 |
for ( |
| 934 |
let checkDate = period.start; |
| 935 |
checkDate.isSameOrBefore(period.end, "day"); |
| 936 |
checkDate = checkDate.add(1, "day") |
| 937 |
) { |
| 938 |
cy.get("@flatpickrInput") |
| 939 |
.getFlatpickrDate(checkDate.toDate()) |
| 940 |
.should("have.class", "flatpickr-disabled"); |
| 941 |
} |
| 942 |
}); |
| 943 |
|
| 944 |
// Phase 3: Test gaps between booking periods are available |
| 945 |
cy.log("Testing gaps between booking periods are available"); |
| 946 |
|
| 947 |
for (let i = 0; i < bookingPeriodsForSelectedItem.length - 1; i++) { |
| 948 |
const currentPeriod = bookingPeriodsForSelectedItem[i]; |
| 949 |
const nextPeriod = bookingPeriodsForSelectedItem[i + 1]; |
| 950 |
|
| 951 |
// Test dates in the gap between current and next booking period |
| 952 |
for ( |
| 953 |
let checkDate = currentPeriod.end.add(1, "day"); |
| 954 |
checkDate.isBefore(nextPeriod.start, "day"); |
| 955 |
checkDate = checkDate.add(1, "day") |
| 956 |
) { |
| 957 |
cy.get("@flatpickrInput") |
| 958 |
.getFlatpickrDate(checkDate.toDate()) |
| 959 |
.should("not.have.class", "flatpickr-disabled"); |
| 960 |
} |
| 961 |
} |
| 962 |
|
| 963 |
// Phase 4: Test that dates booked for different items are still available |
| 964 |
cy.log("Testing dates booked for different items remain available"); |
| 965 |
|
| 966 |
// The booking for different_item (days 20-25) should not affect our selected item |
| 967 |
const differentItemBookingStart = today.add(20, "day"); |
| 968 |
const differentItemBookingEnd = today.add(25, "day"); |
| 969 |
|
| 970 |
for ( |
| 971 |
let checkDate = differentItemBookingStart; |
| 972 |
checkDate.isSameOrBefore(differentItemBookingEnd, "day"); |
| 973 |
checkDate = checkDate.add(1, "day") |
| 974 |
) { |
| 975 |
cy.get("@flatpickrInput") |
| 976 |
.getFlatpickrDate(checkDate.toDate()) |
| 977 |
.should("not.have.class", "flatpickr-disabled"); |
| 978 |
} |
| 979 |
|
| 980 |
// Phase 5: Test dates after last booking period are available |
| 981 |
cy.log("Testing dates after last booking period are available"); |
| 982 |
|
| 983 |
const lastPeriod = |
| 984 |
bookingPeriodsForSelectedItem[ |
| 985 |
bookingPeriodsForSelectedItem.length - 1 |
| 986 |
]; |
| 987 |
for ( |
| 988 |
let checkDate = lastPeriod.end.add(1, "day"); |
| 989 |
checkDate.isSameOrBefore(lastPeriod.end.add(5, "day"), "day"); |
| 990 |
checkDate = checkDate.add(1, "day") |
| 991 |
) { |
| 992 |
cy.get("@flatpickrInput") |
| 993 |
.getFlatpickrDate(checkDate.toDate()) |
| 994 |
.should("not.have.class", "flatpickr-disabled"); |
| 995 |
} |
| 996 |
}); |
| 997 |
}); |
| 998 |
|
| 999 |
it("should handle lead and trail period hover highlighting", () => { |
| 1000 |
// Setup modal for date testing |
| 1001 |
setupModalForDateTesting(); |
| 1002 |
|
| 1003 |
// Open the flatpickr |
| 1004 |
cy.get("#period").as("flatpickrInput"); |
| 1005 |
cy.get("@flatpickrInput").openFlatpickr(); |
| 1006 |
|
| 1007 |
// Get a future date to hover over |
| 1008 |
let hoverDate = dayjs(); |
| 1009 |
hoverDate = hoverDate.add(5, "day"); |
| 1010 |
|
| 1011 |
// Hover over a date and check for lead/trail highlighting |
| 1012 |
cy.get("@flatpickrInput") |
| 1013 |
.getFlatpickrDate(hoverDate.toDate()) |
| 1014 |
.trigger("mouseover"); |
| 1015 |
cy.wait(100); |
| 1016 |
|
| 1017 |
// Check for lead range classes (assuming 2-day lead period from circulation rules) |
| 1018 |
cy.get(".leadRange, .leadRangeStart, .leadRangeEnd").should("exist"); |
| 1019 |
|
| 1020 |
// Check for trail range classes (assuming 2-day trail period) |
| 1021 |
cy.get(".trailRange, .trailRangeStart, .trailRangeEnd").should("exist"); |
| 1022 |
}); |
| 1023 |
|
| 1024 |
it("should disable click when lead/trail periods overlap with disabled dates", () => { |
| 1025 |
// Setup modal for date testing |
| 1026 |
setupModalForDateTesting(); |
| 1027 |
|
| 1028 |
// Open the flatpickr |
| 1029 |
cy.get("#period").as("flatpickrInput"); |
| 1030 |
cy.get("@flatpickrInput").openFlatpickr(); |
| 1031 |
|
| 1032 |
// Find a date that would have overlapping lead/trail with disabled dates |
| 1033 |
const today = dayjs(); |
| 1034 |
const problematicDate = today.add(7, "day"); // Just before a booked period |
| 1035 |
|
| 1036 |
cy.get("@flatpickrInput") |
| 1037 |
.getFlatpickrDate(problematicDate.toDate()) |
| 1038 |
.trigger("mouseover") |
| 1039 |
.should($el => { |
| 1040 |
expect( |
| 1041 |
$el.hasClass("leadDisable") || $el.hasClass("trailDisable"), |
| 1042 |
"element has either leadDisable or trailDisable" |
| 1043 |
).to.be.true; |
| 1044 |
}); |
| 1045 |
}); |
| 1046 |
|
| 1047 |
it("should show event dots for dates with existing bookings", () => { |
| 1048 |
// Setup modal for date testing |
| 1049 |
setupModalForDateTesting(); |
| 1050 |
|
| 1051 |
// Open the flatpickr |
| 1052 |
cy.get("#period").as("flatpickrInput"); |
| 1053 |
cy.get("@flatpickrInput").openFlatpickr(); |
| 1054 |
|
| 1055 |
// Check for event dots on dates with bookings |
| 1056 |
cy.get(".flatpickr-calendar").within(() => { |
| 1057 |
cy.get(".event-dots").should("exist"); |
| 1058 |
cy.get(".event-dots .event").should("exist"); |
| 1059 |
}); |
| 1060 |
}); |
| 1061 |
|
| 1062 |
it("should show only the correct bold dates for issue and renewal periods", () => { |
| 1063 |
// Setup modal for date testing |
| 1064 |
setupModalForDateTesting(); |
| 1065 |
|
| 1066 |
// Open the flatpickr |
| 1067 |
cy.get("#period").as("flatpickrInput"); |
| 1068 |
cy.get("@flatpickrInput").openFlatpickr(); |
| 1069 |
|
| 1070 |
const startDate = dayjs().add(3, "day").startOf("day"); |
| 1071 |
cy.get("@flatpickrInput").selectFlatpickrDate(startDate.toDate()); |
| 1072 |
|
| 1073 |
const expectedBoldDates = [ |
| 1074 |
startDate.add(14, "day"), |
| 1075 |
startDate.add(21, "day"), |
| 1076 |
startDate.add(28, "day"), |
| 1077 |
]; |
| 1078 |
|
| 1079 |
// Confirm each expected bold date is bold |
| 1080 |
expectedBoldDates.forEach(boldDate => { |
| 1081 |
cy.get("@flatpickrInput") |
| 1082 |
.getFlatpickrDate(boldDate.toDate()) |
| 1083 |
.should("have.class", "title"); |
| 1084 |
}); |
| 1085 |
|
| 1086 |
// Confirm that only expected dates are bold |
| 1087 |
cy.get(".flatpickr-day.title").each($el => { |
| 1088 |
const ariaLabel = $el.attr("aria-label"); |
| 1089 |
const date = dayjs(ariaLabel, "MMMM D, YYYY"); |
| 1090 |
const isExpected = expectedBoldDates.some(expected => |
| 1091 |
date.isSame(expected, "day") |
| 1092 |
); |
| 1093 |
expect(isExpected, `Unexpected bold date: ${ariaLabel}`).to.be.true; |
| 1094 |
}); |
| 1095 |
}); |
| 1096 |
|
| 1097 |
it("should set correct max date based on circulation rules", () => { |
| 1098 |
const today = dayjs(); |
| 1099 |
|
| 1100 |
// Setup modal for date testing |
| 1101 |
setupModalForDateTesting(); |
| 1102 |
|
| 1103 |
// Open the flatpickr |
| 1104 |
cy.get("#period").as("flatpickrInput"); |
| 1105 |
cy.get("@flatpickrInput").openFlatpickr(); |
| 1106 |
|
| 1107 |
// Select a start date |
| 1108 |
const startDate = today.add(3, "day"); |
| 1109 |
cy.get("@flatpickrInput").selectFlatpickrDate(startDate.toDate()); |
| 1110 |
|
| 1111 |
// Check that dates beyond the maximum allowed period are disabled |
| 1112 |
cy.get(".flatpickr-calendar").within(() => { |
| 1113 |
// Assuming circulation rules allow 14 days + renewals |
| 1114 |
const maxDate = startDate.add(30, "day"); // Beyond reasonable limit |
| 1115 |
|
| 1116 |
// Navigate to future months if needed and check disabled state |
| 1117 |
cy.get(".flatpickr-next-month").click(); |
| 1118 |
cy.get(".flatpickr-day.flatpickr-disabled").should("exist"); |
| 1119 |
}); |
| 1120 |
}); |
| 1121 |
|
| 1122 |
it("should handle visible and hidden fields on date selection", () => { |
| 1123 |
// Test-specific data for form field validation |
| 1124 |
const today = dayjs(); |
| 1125 |
const fieldTestData = { |
| 1126 |
selectedDates: { |
| 1127 |
startDate: today.add(3, "day"), |
| 1128 |
endDate: today.add(6, "day"), |
| 1129 |
}, |
| 1130 |
expectedFormats: { |
| 1131 |
displayFormat: "YYYY-MM-DD", // Format for visible input |
| 1132 |
isoFormat: "YYYY-MM-DDTHH:mm:ss.sssZ", // Format for hidden fields |
| 1133 |
}, |
| 1134 |
}; |
| 1135 |
|
| 1136 |
// Setup modal for date testing |
| 1137 |
setupModalForDateTesting(); |
| 1138 |
|
| 1139 |
cy.get("#period").as("flatpickrInput"); |
| 1140 |
|
| 1141 |
// Select date range using test data |
| 1142 |
cy.get("@flatpickrInput").selectFlatpickrDateRange( |
| 1143 |
fieldTestData.selectedDates.startDate.toDate(), |
| 1144 |
fieldTestData.selectedDates.endDate.toDate() |
| 1145 |
); |
| 1146 |
|
| 1147 |
// Use should with retry capability instead of a simple assertion |
| 1148 |
const format = date => |
| 1149 |
date.format(fieldTestData.expectedFormats.displayFormat); |
| 1150 |
const expectedDisplayValue = `${format(fieldTestData.selectedDates.startDate)} to ${format(fieldTestData.selectedDates.endDate)}`; |
| 1151 |
|
| 1152 |
cy.get("#period").should("have.value", expectedDisplayValue); |
| 1153 |
|
| 1154 |
// Verify the flatpickr visible input also has value |
| 1155 |
cy.get("#period").should("have.value", expectedDisplayValue); |
| 1156 |
|
| 1157 |
// Now check the hidden fields use ISO format |
| 1158 |
cy.get("#booking_start_date").should( |
| 1159 |
"have.value", |
| 1160 |
fieldTestData.selectedDates.startDate.startOf("day").toISOString() |
| 1161 |
); |
| 1162 |
cy.get("#booking_end_date").should( |
| 1163 |
"have.value", |
| 1164 |
fieldTestData.selectedDates.endDate.endOf("day").toISOString() |
| 1165 |
); |
| 1166 |
}); |
| 1167 |
|
| 1168 |
it("should submit a new booking successfully", () => { |
| 1169 |
// Setup modal for form submission |
| 1170 |
setupModalForFormSubmission(); |
| 1171 |
|
| 1172 |
// Submit the form |
| 1173 |
cy.get("#placeBookingForm").submit(); |
| 1174 |
cy.wait("@createBooking"); |
| 1175 |
|
| 1176 |
// Check success message |
| 1177 |
cy.get("#transient_result").should( |
| 1178 |
"contain", |
| 1179 |
"Booking successfully placed" |
| 1180 |
); |
| 1181 |
|
| 1182 |
// Check modal closes |
| 1183 |
cy.get("#placeBookingModal").should("not.be.visible"); |
| 1184 |
}); |
| 1185 |
|
| 1186 |
it("should edit an existing booking successfully", () => { |
| 1187 |
// Open edit booking modal |
| 1188 |
cy.get("#placebooking") |
| 1189 |
.invoke("attr", "data-booking", "1001") |
| 1190 |
.invoke("attr", "data-patron", "456") |
| 1191 |
.invoke("attr", "data-itemnumber", "789") |
| 1192 |
.invoke("attr", "data-pickup_library", "1") |
| 1193 |
.invoke("attr", "data-start_date", "2025-05-01T00:00:00.000Z") |
| 1194 |
.invoke("attr", "data-end_date", "2025-05-05T23:59:59.999Z") |
| 1195 |
.click(); |
| 1196 |
|
| 1197 |
// Wait for API calls that populate the modal when editing |
| 1198 |
cy.wait([ |
| 1199 |
"@getBookableItems", |
| 1200 |
"@getBookings", |
| 1201 |
"@getCheckouts", |
| 1202 |
"@getPickupLocations", |
| 1203 |
]); |
| 1204 |
|
| 1205 |
// Check modal title for edit |
| 1206 |
cy.get("#placeBookingLabel").should("contain", "Edit booking"); |
| 1207 |
|
| 1208 |
// Verify booking ID is set |
| 1209 |
cy.get("#booking_id").should("have.value", "1001"); |
| 1210 |
|
| 1211 |
// Verify patron ID is set |
| 1212 |
cy.get("#booking_patron_id").should("have.value", "456"); |
| 1213 |
|
| 1214 |
// Verify pickup_library is set |
| 1215 |
cy.get("#pickup_library_id").should("have.value", "MAIN"); |
| 1216 |
|
| 1217 |
// Verify itemnumber is set |
| 1218 |
cy.get("#booking_item_id").should("have.value", "789"); |
| 1219 |
|
| 1220 |
// Wait for item selection logic to complete and populate item type |
| 1221 |
cy.get("#booking_itemtype") |
| 1222 |
.should("not.have.value", "") |
| 1223 |
.and("not.have.value", null); |
| 1224 |
|
| 1225 |
// Verify item_type is set |
| 1226 |
cy.get("#booking_itemtype").should("have.value", "BK"); |
| 1227 |
|
| 1228 |
// Change pickup location |
| 1229 |
cy.selectFromSelect2ByIndex("#pickup_library_id", 1); |
| 1230 |
|
| 1231 |
// Submit the form |
| 1232 |
cy.get("#placeBookingForm").submit(); |
| 1233 |
cy.wait("@updateBooking"); |
| 1234 |
|
| 1235 |
// Check success message |
| 1236 |
cy.get("#transient_result").should( |
| 1237 |
"contain", |
| 1238 |
"Booking successfully updated" |
| 1239 |
); |
| 1240 |
|
| 1241 |
// Check modal closes |
| 1242 |
cy.get("#placeBookingModal").should("not.be.visible"); |
| 1243 |
}); |
| 1244 |
|
| 1245 |
it("should handle booking failure gracefully", () => { |
| 1246 |
// Override the create booking intercept to return an error |
| 1247 |
cy.intercept("POST", "/api/v1/bookings", { |
| 1248 |
statusCode: 400, |
| 1249 |
body: { |
| 1250 |
error: "Booking failed", |
| 1251 |
}, |
| 1252 |
}).as("failedBooking"); |
| 1253 |
|
| 1254 |
// Setup modal for form submission |
| 1255 |
setupModalForFormSubmission(); |
| 1256 |
|
| 1257 |
// Submit the form |
| 1258 |
cy.get("#placeBookingForm").submit(); |
| 1259 |
cy.wait("@failedBooking"); |
| 1260 |
|
| 1261 |
// Check error message |
| 1262 |
cy.get("#booking_result").should("contain", "Failure"); |
| 1263 |
|
| 1264 |
// Modal should remain open |
| 1265 |
cy.get("#placeBookingModal").should("be.visible"); |
| 1266 |
}); |
| 1267 |
|
| 1268 |
it("should reset form when modal is closed", () => { |
| 1269 |
// Setup modal for date testing |
| 1270 |
setupModalForDateTesting(); |
| 1271 |
|
| 1272 |
// Reset modal to test form clearing |
| 1273 |
resetModalAndReopen(); |
| 1274 |
|
| 1275 |
// Check fields are reset to initial state |
| 1276 |
cy.get("#booking_patron_id") |
| 1277 |
.should("not.be.disabled") |
| 1278 |
.and("have.value", null); |
| 1279 |
cy.get("#pickup_library_id").should("be.disabled"); |
| 1280 |
cy.get("#booking_itemtype").should("be.disabled"); |
| 1281 |
cy.get("#booking_item_id").should("be.disabled"); |
| 1282 |
cy.get("#period").should("be.disabled"); |
| 1283 |
cy.get("#booking_start_date").should("have.value", ""); |
| 1284 |
cy.get("#booking_end_date").should("have.value", ""); |
| 1285 |
cy.get("#booking_id").should("have.value", ""); |
| 1286 |
}); |
| 1287 |
}); |