|
Lines 418-425
describe("Booking Modal Basic Tests", () => {
Link Here
|
| 418 |
cy.get("#period").should("not.be.disabled"); |
418 |
cy.get("#period").should("not.be.disabled"); |
| 419 |
|
419 |
|
| 420 |
// Use the flatpickr helper to select date range |
420 |
// Use the flatpickr helper to select date range |
| 421 |
const startDate = dayjs().add(1, "day"); |
421 |
// Note: Add enough days to account for lead period (3 days) to avoid past-date constraint |
| 422 |
const endDate = dayjs().add(7, "days"); |
422 |
const startDate = dayjs().add(5, "day"); |
|
|
423 |
const endDate = dayjs().add(10, "days"); |
| 423 |
|
424 |
|
| 424 |
cy.get("#period").selectFlatpickrDateRange(startDate, endDate); |
425 |
cy.get("#period").selectFlatpickrDateRange(startDate, endDate); |
| 425 |
|
426 |
|
|
Lines 435-440
describe("Booking Modal Basic Tests", () => {
Link Here
|
| 435 |
); |
436 |
); |
| 436 |
}); |
437 |
}); |
| 437 |
|
438 |
|
|
|
439 |
it("should successfully submit an 'Any item' booking with server-side optimal item selection", () => { |
| 440 |
/** |
| 441 |
* TEST: Bug 40134 - Server-Side Optimal Item Selection for "Any Item" Bookings |
| 442 |
* |
| 443 |
* This test validates that: |
| 444 |
* 1. "Any item" bookings can be successfully submitted with itemtype_id |
| 445 |
* 2. The server performs optimal item selection based on future availability |
| 446 |
* 3. An appropriate item is automatically assigned by the server |
| 447 |
* |
| 448 |
* When submitting an "any item" booking, the client sends itemtype_id |
| 449 |
* (or item_id if only one item is available) and the server selects |
| 450 |
* the optimal item with the longest future availability. |
| 451 |
*/ |
| 452 |
|
| 453 |
cy.visit( |
| 454 |
`/cgi-bin/koha/catalogue/detail.pl?biblionumber=${testData.biblio.biblio_id}` |
| 455 |
); |
| 456 |
|
| 457 |
// Open the modal |
| 458 |
cy.get('[data-bs-target="#placeBookingModal"]').first().click(); |
| 459 |
cy.get("#placeBookingModal").should("be.visible"); |
| 460 |
|
| 461 |
// Step 1: Select patron |
| 462 |
cy.selectFromSelect2( |
| 463 |
"#booking_patron_id", |
| 464 |
`${testData.patron.surname}, ${testData.patron.firstname}`, |
| 465 |
testData.patron.cardnumber |
| 466 |
); |
| 467 |
|
| 468 |
// Step 2: Select pickup location |
| 469 |
cy.get("#pickup_library_id").should("not.be.disabled"); |
| 470 |
cy.selectFromSelect2ByIndex("#pickup_library_id", 0); |
| 471 |
|
| 472 |
// Step 3: Select itemtype (to enable "Any item" for that type) |
| 473 |
cy.get("#booking_itemtype").should("not.be.disabled"); |
| 474 |
cy.selectFromSelect2ByIndex("#booking_itemtype", 0); // Select first itemtype |
| 475 |
|
| 476 |
// Step 4: Select "Any item" option (index 0) |
| 477 |
cy.get("#booking_item_id").should("not.be.disabled"); |
| 478 |
cy.selectFromSelect2ByIndex("#booking_item_id", 0); // "Any item" option |
| 479 |
|
| 480 |
// Verify "Any item" is selected |
| 481 |
cy.get("#booking_item_id").should("have.value", "0"); |
| 482 |
|
| 483 |
// Step 5: Set dates using flatpickr |
| 484 |
cy.get("#period").should("not.be.disabled"); |
| 485 |
|
| 486 |
// Note: Add enough days to account for lead period (3 days) to avoid past-date constraint |
| 487 |
const startDate = dayjs().add(5, "day"); |
| 488 |
const endDate = dayjs().add(10, "days"); |
| 489 |
|
| 490 |
cy.get("#period").selectFlatpickrDateRange(startDate, endDate); |
| 491 |
|
| 492 |
// Wait a moment for onChange handlers to populate hidden fields |
| 493 |
cy.wait(500); |
| 494 |
|
| 495 |
// Step 6: Submit the form |
| 496 |
// This will send either item_id (if only one available) or itemtype_id |
| 497 |
// to the server for optimal item selection |
| 498 |
cy.get("#placeBookingForm button[type='submit']") |
| 499 |
.should("not.be.disabled") |
| 500 |
.click(); |
| 501 |
|
| 502 |
// Verify success - modal should close without errors |
| 503 |
cy.get("#placeBookingModal", { timeout: 10000 }).should( |
| 504 |
"not.be.visible" |
| 505 |
); |
| 506 |
|
| 507 |
// Verify that a booking was created and the server assigned an optimal item |
| 508 |
cy.task("query", { |
| 509 |
sql: `SELECT * FROM bookings |
| 510 |
WHERE biblio_id = ? |
| 511 |
AND patron_id = ? |
| 512 |
AND start_date = ? |
| 513 |
ORDER BY booking_id DESC |
| 514 |
LIMIT 1`, |
| 515 |
values: [ |
| 516 |
testData.biblio.biblio_id, |
| 517 |
testData.patron.patron_id, |
| 518 |
startDate.format("YYYY-MM-DD"), |
| 519 |
], |
| 520 |
}).then(result => { |
| 521 |
expect(result).to.have.length(1); |
| 522 |
const booking = result[0]; |
| 523 |
|
| 524 |
// Verify the booking has an item_id assigned (not null) |
| 525 |
expect(booking.item_id).to.not.be.null; |
| 526 |
expect(booking.item_id).to.be.oneOf([ |
| 527 |
testData.items[0].item_id, |
| 528 |
testData.items[1].item_id, |
| 529 |
]); |
| 530 |
|
| 531 |
// Verify booking dates match what we selected |
| 532 |
expect(booking.start_date).to.include( |
| 533 |
startDate.format("YYYY-MM-DD") |
| 534 |
); |
| 535 |
expect(booking.end_date).to.include(endDate.format("YYYY-MM-DD")); |
| 536 |
|
| 537 |
// Clean up the test booking |
| 538 |
cy.task("query", { |
| 539 |
sql: "DELETE FROM bookings WHERE booking_id = ?", |
| 540 |
values: [booking.booking_id], |
| 541 |
}); |
| 542 |
}); |
| 543 |
|
| 544 |
cy.log("✓ CONFIRMED: Any item booking submitted successfully"); |
| 545 |
cy.log("✓ CONFIRMED: Server-side optimal item selection completed"); |
| 546 |
cy.log("✓ CONFIRMED: Optimal item automatically assigned by server"); |
| 547 |
}); |
| 548 |
|
| 438 |
it("should handle basic form interactions correctly", () => { |
549 |
it("should handle basic form interactions correctly", () => { |
| 439 |
cy.visit( |
550 |
cy.visit( |
| 440 |
`/cgi-bin/koha/catalogue/detail.pl?biblionumber=${testData.biblio.biblio_id}` |
551 |
`/cgi-bin/koha/catalogue/detail.pl?biblionumber=${testData.biblio.biblio_id}` |
|
Lines 1079-1082
describe("Booking Modal Basic Tests", () => {
Link Here
|
| 1079 |
"✓ Validated: API errors, user feedback, form preservation, and retry functionality" |
1190 |
"✓ Validated: API errors, user feedback, form preservation, and retry functionality" |
| 1080 |
); |
1191 |
); |
| 1081 |
}); |
1192 |
}); |
|
|
1193 |
|
| 1194 |
it("should maximize booking window by dynamically reducing available items during overlaps", () => { |
| 1195 |
/** |
| 1196 |
* COMPREHENSIVE TEST: Dynamic Item Assignment & Date Restriction |
| 1197 |
* |
| 1198 |
* This test validates the core "smart window maximization" algorithm for "any item of itemtype X" bookings. |
| 1199 |
* |
| 1200 |
* KEY ALGORITHM PRINCIPLE: "Never Re-add Items to Pool" |
| 1201 |
* Once an item is removed from the available pool because it becomes unavailable, |
| 1202 |
* it is NEVER re-added even if it becomes available again later in the booking period. |
| 1203 |
* This ensures optimal resource allocation and maximum booking windows. |
| 1204 |
* |
| 1205 |
* TEST SCENARIOS COVERED: |
| 1206 |
* 1. Day 5 start: Tests item pool reduction as items become unavailable |
| 1207 |
* 2. Day 8 start: Tests maximum window with all items initially available |
| 1208 |
* 3. Day 14 start: Tests window maximization with reduced initial pool |
| 1209 |
* 4. Day 19 start: Tests multi-item window extension through non-re-addition principle |
| 1210 |
* 5. Cross-scenario consistency validation |
| 1211 |
* |
| 1212 |
* EXPECTED ALGORITHM BEHAVIOR: |
| 1213 |
* - Start with items available on the selected start date |
| 1214 |
* - Walk through each day from start to potential end date |
| 1215 |
* - Remove items from pool when they become unavailable (bookings start) |
| 1216 |
* - NEVER re-add items even if they become available again (bookings end) |
| 1217 |
* - Return false (disable date) when no items remain in pool |
| 1218 |
* |
| 1219 |
* This maximizes booking windows by ensuring optimal resource utilization. |
| 1220 |
*/ |
| 1221 |
|
| 1222 |
const today = dayjs(); |
| 1223 |
|
| 1224 |
// Create custom test data with 4 items of the same itemtype for this specific test |
| 1225 |
let testItems = []; |
| 1226 |
let testBiblio = null; |
| 1227 |
let testPatron = null; |
| 1228 |
|
| 1229 |
// Setup: Create biblio with 4 TABLET items |
| 1230 |
cy.task("insertSampleBiblio", { |
| 1231 |
item_count: 4, |
| 1232 |
}) |
| 1233 |
.then(objects => { |
| 1234 |
testBiblio = objects.biblio; |
| 1235 |
testItems = objects.items; |
| 1236 |
|
| 1237 |
// Update all 4 items to be TABLET itemtype and bookable |
| 1238 |
// Set enumchron to control API ordering (A, B, C, D) |
| 1239 |
const itemUpdates = testItems.map((item, index) => { |
| 1240 |
const enumchron = String.fromCharCode(65 + index); // A, B, C, D |
| 1241 |
return cy.task("query", { |
| 1242 |
sql: "UPDATE items SET bookable = 1, itype = 'BK', homebranch = 'CPL', enumchron = ?, dateaccessioned = ? WHERE itemnumber = ?", |
| 1243 |
values: [ |
| 1244 |
enumchron, |
| 1245 |
`2024-12-0${4 - index}`, // Newest to oldest |
| 1246 |
item.item_id, |
| 1247 |
], |
| 1248 |
}); |
| 1249 |
}); |
| 1250 |
|
| 1251 |
return Promise.all(itemUpdates); |
| 1252 |
}) |
| 1253 |
.then(() => { |
| 1254 |
// Create a test patron |
| 1255 |
return cy.task("buildSampleObject", { |
| 1256 |
object: "patron", |
| 1257 |
values: { |
| 1258 |
firstname: "John", |
| 1259 |
surname: "Doe", |
| 1260 |
cardnumber: `TEST${Date.now()}`, |
| 1261 |
category_id: "PT", |
| 1262 |
library_id: "CPL", |
| 1263 |
}, |
| 1264 |
}); |
| 1265 |
}) |
| 1266 |
.then(mockPatron => { |
| 1267 |
testPatron = mockPatron; |
| 1268 |
|
| 1269 |
// Insert the patron into the database |
| 1270 |
return cy.task("query", { |
| 1271 |
sql: `INSERT INTO borrowers (borrowernumber, firstname, surname, cardnumber, categorycode, branchcode, dateofbirth) |
| 1272 |
VALUES (?, ?, ?, ?, ?, ?, ?)`, |
| 1273 |
values: [ |
| 1274 |
mockPatron.patron_id, |
| 1275 |
mockPatron.firstname, |
| 1276 |
mockPatron.surname, |
| 1277 |
mockPatron.cardnumber, |
| 1278 |
mockPatron.category_id, |
| 1279 |
mockPatron.library_id, |
| 1280 |
"1990-01-01", |
| 1281 |
], |
| 1282 |
}); |
| 1283 |
}) |
| 1284 |
.then(() => { |
| 1285 |
/** |
| 1286 |
* STRATEGIC BOOKING PATTERN DESIGN: |
| 1287 |
* |
| 1288 |
* This booking pattern creates a perfect test case for validating the |
| 1289 |
* "never re-add items to pool" algorithm across multiple scenarios: |
| 1290 |
* |
| 1291 |
* ITEM 0 (enumchron A): Available 5-9, BOOKED 10-15, Available again 16+ |
| 1292 |
* ITEM 1 (enumchron B): Available 5-12, BOOKED 13-20, Available again 21+ |
| 1293 |
* ITEM 2 (enumchron C): Available 5-17, BOOKED 18-25, Available again 26+ |
| 1294 |
* ITEM 3 (enumchron D): BOOKED 1-7, Available 8-22, BOOKED 23-30 |
| 1295 |
* |
| 1296 |
* This pattern allows testing: |
| 1297 |
* - Progressive item removal from pool (items become unavailable at different times) |
| 1298 |
* - Non-re-addition principle (items that become available again are not re-added) |
| 1299 |
* - Window maximization through optimal item selection |
| 1300 |
* - Cross-scenario consistency (different start dates should produce consistent results) |
| 1301 |
*/ |
| 1302 |
|
| 1303 |
// Create strategic bookings in the database |
| 1304 |
const bookingInserts = [ |
| 1305 |
// ITEM 0: Available days 5-9, then booked 10-15, then available again 16+ |
| 1306 |
// Algorithm should NOT re-add this item to pool after day 15 even though it becomes available |
| 1307 |
cy.task("query", { |
| 1308 |
sql: `INSERT INTO bookings (biblio_id, patron_id, item_id, pickup_library_id, start_date, end_date, status) |
| 1309 |
VALUES (?, ?, ?, ?, ?, ?, ?)`, |
| 1310 |
values: [ |
| 1311 |
testBiblio.biblio_id, |
| 1312 |
testPatron.patron_id, |
| 1313 |
testItems[0].item_id, |
| 1314 |
"CPL", |
| 1315 |
today.add(10, "day").format("YYYY-MM-DD"), |
| 1316 |
today.add(15, "day").format("YYYY-MM-DD"), |
| 1317 |
"new", |
| 1318 |
], |
| 1319 |
}), |
| 1320 |
// ITEM 1: Available days 5-12, then booked 13-20, then available 21+ |
| 1321 |
// Tests item removal during active booking period |
| 1322 |
cy.task("query", { |
| 1323 |
sql: `INSERT INTO bookings (biblio_id, patron_id, item_id, pickup_library_id, start_date, end_date, status) |
| 1324 |
VALUES (?, ?, ?, ?, ?, ?, ?)`, |
| 1325 |
values: [ |
| 1326 |
testBiblio.biblio_id, |
| 1327 |
testPatron.patron_id, |
| 1328 |
testItems[1].item_id, |
| 1329 |
"CPL", |
| 1330 |
today.add(13, "day").format("YYYY-MM-DD"), |
| 1331 |
today.add(20, "day").format("YYYY-MM-DD"), |
| 1332 |
"new", |
| 1333 |
], |
| 1334 |
}), |
| 1335 |
// ITEM 2: Available days 5-17, then booked 18-25, then available 26+ |
| 1336 |
// Tests final item removal that should trigger date disabling |
| 1337 |
cy.task("query", { |
| 1338 |
sql: `INSERT INTO bookings (biblio_id, patron_id, item_id, pickup_library_id, start_date, end_date, status) |
| 1339 |
VALUES (?, ?, ?, ?, ?, ?, ?)`, |
| 1340 |
values: [ |
| 1341 |
testBiblio.biblio_id, |
| 1342 |
testPatron.patron_id, |
| 1343 |
testItems[2].item_id, |
| 1344 |
"CPL", |
| 1345 |
today.add(18, "day").format("YYYY-MM-DD"), |
| 1346 |
today.add(25, "day").format("YYYY-MM-DD"), |
| 1347 |
"new", |
| 1348 |
], |
| 1349 |
}), |
| 1350 |
// ITEM 3: Booked early 1-7, then available 8-22, then booked 23-30 |
| 1351 |
// Tests item that starts unavailable but becomes available within the test period |
| 1352 |
cy.task("query", { |
| 1353 |
sql: `INSERT INTO bookings (biblio_id, patron_id, item_id, pickup_library_id, start_date, end_date, status) |
| 1354 |
VALUES (?, ?, ?, ?, ?, ?, ?)`, |
| 1355 |
values: [ |
| 1356 |
testBiblio.biblio_id, |
| 1357 |
testPatron.patron_id, |
| 1358 |
testItems[3].item_id, |
| 1359 |
"CPL", |
| 1360 |
today.add(1, "day").format("YYYY-MM-DD"), |
| 1361 |
today.add(7, "day").format("YYYY-MM-DD"), |
| 1362 |
"new", |
| 1363 |
], |
| 1364 |
}), |
| 1365 |
// ITEM 3 second booking: Tests item that has gaps in availability |
| 1366 |
cy.task("query", { |
| 1367 |
sql: `INSERT INTO bookings (biblio_id, patron_id, item_id, pickup_library_id, start_date, end_date, status) |
| 1368 |
VALUES (?, ?, ?, ?, ?, ?, ?)`, |
| 1369 |
values: [ |
| 1370 |
testBiblio.biblio_id, |
| 1371 |
testPatron.patron_id, |
| 1372 |
testItems[3].item_id, |
| 1373 |
"CPL", |
| 1374 |
today.add(23, "day").format("YYYY-MM-DD"), |
| 1375 |
today.add(30, "day").format("YYYY-MM-DD"), |
| 1376 |
"new", |
| 1377 |
], |
| 1378 |
}), |
| 1379 |
]; |
| 1380 |
|
| 1381 |
return Promise.all(bookingInserts); |
| 1382 |
}) |
| 1383 |
.then(() => { |
| 1384 |
// Setup API intercepts for this test |
| 1385 |
cy.intercept( |
| 1386 |
"GET", |
| 1387 |
`/api/v1/biblios/${testBiblio.biblio_id}/pickup_locations*` |
| 1388 |
).as("getPickupLocations"); |
| 1389 |
cy.intercept("GET", "/api/v1/circulation_rules*").as( |
| 1390 |
"getCirculationRules" |
| 1391 |
); |
| 1392 |
|
| 1393 |
// Navigate to the biblio detail page |
| 1394 |
cy.visit( |
| 1395 |
`/cgi-bin/koha/catalogue/detail.pl?biblionumber=${testBiblio.biblio_id}` |
| 1396 |
); |
| 1397 |
|
| 1398 |
// Open the booking modal |
| 1399 |
cy.get('[data-bs-target="#placeBookingModal"]').first().click(); |
| 1400 |
cy.get("#placeBookingModal").should("be.visible"); |
| 1401 |
|
| 1402 |
// Select patron |
| 1403 |
cy.selectFromSelect2( |
| 1404 |
"#booking_patron_id", |
| 1405 |
`${testPatron.surname}, ${testPatron.firstname}`, |
| 1406 |
testPatron.cardnumber |
| 1407 |
); |
| 1408 |
cy.wait("@getPickupLocations"); |
| 1409 |
|
| 1410 |
// Ensure pickup location field is enabled |
| 1411 |
cy.get("#pickup_library_id").should("not.be.disabled"); |
| 1412 |
|
| 1413 |
// Select pickup location |
| 1414 |
cy.selectFromSelect2ByIndex("#pickup_library_id", 0); |
| 1415 |
|
| 1416 |
// Ensure itemtype field is enabled |
| 1417 |
cy.get("#booking_itemtype").should("not.be.disabled"); |
| 1418 |
|
| 1419 |
// Select BK itemtype (all our test items are BK) |
| 1420 |
cy.selectFromSelect2ByIndex("#booking_itemtype", 0); // Select first available itemtype |
| 1421 |
cy.wait("@getCirculationRules"); |
| 1422 |
|
| 1423 |
// Select "Any item" option (index 0) |
| 1424 |
cy.selectFromSelect2ByIndex("#booking_item_id", 0); |
| 1425 |
cy.get("#period").should("not.be.disabled"); |
| 1426 |
|
| 1427 |
cy.get("#period").as("flatpickrInput"); |
| 1428 |
|
| 1429 |
/** |
| 1430 |
* TEST SCENARIO 1: Start Date Day 5 - Progressive Item Pool Reduction |
| 1431 |
* |
| 1432 |
* This scenario tests the core "never re-add items to pool" principle. |
| 1433 |
* Starting from day 5, we expect the algorithm to: |
| 1434 |
* |
| 1435 |
* 1. Begin with initial pool: ITEM0, ITEM1, ITEM2 |
| 1436 |
* (ITEM3 excluded because it's booked days 1-7) |
| 1437 |
* |
| 1438 |
* 2. Days 5-9: All 3 items remain available in pool |
| 1439 |
* |
| 1440 |
* 3. Day 10: ITEM0 becomes unavailable (booking starts) |
| 1441 |
* → Remove ITEM0 from pool (never to be re-added) |
| 1442 |
* → Pool now: ITEM1, ITEM2 |
| 1443 |
* |
| 1444 |
* 4. Day 13: ITEM1 becomes unavailable (booking starts) |
| 1445 |
* → Remove ITEM1 from pool (never to be re-added) |
| 1446 |
* → Pool now: ITEM2 only |
| 1447 |
* |
| 1448 |
* 5. Day 18: ITEM2 becomes unavailable (booking starts) |
| 1449 |
* → Remove ITEM2 from pool |
| 1450 |
* → Pool now: EMPTY → Disable all dates from day 18 onwards |
| 1451 |
* |
| 1452 |
* CRITICAL: Even though ITEM0 becomes available again on day 16, |
| 1453 |
* it should NOT be re-added to the pool. This is the key algorithm principle. |
| 1454 |
* |
| 1455 |
* Expected result: Can book from day 5 through day 17, day 18+ disabled |
| 1456 |
*/ |
| 1457 |
|
| 1458 |
cy.log( |
| 1459 |
"=== Testing Start Date Day 5 (Maximize through item pool reduction) ===" |
| 1460 |
); |
| 1461 |
|
| 1462 |
cy.get("@flatpickrInput").openFlatpickr(); |
| 1463 |
|
| 1464 |
const startDate1 = today.add(5, "day"); |
| 1465 |
cy.log("Selecting start date of ", startDate1.toString()); |
| 1466 |
cy.get("@flatpickrInput") |
| 1467 |
.getFlatpickrDate(startDate1.toDate()) |
| 1468 |
.click(); |
| 1469 |
|
| 1470 |
cy.log( |
| 1471 |
"Checking maximized end date availability through item reduction" |
| 1472 |
); |
| 1473 |
|
| 1474 |
// Days 6-9 should be available (3 items available: ITEM0, ITEM1, ITEM2) |
| 1475 |
for (let day = 6; day <= 9; day++) { |
| 1476 |
const endDate = today.add(day, "day"); |
| 1477 |
cy.get("@flatpickrInput") |
| 1478 |
.getFlatpickrDate(endDate.toDate()) |
| 1479 |
.should("not.have.class", "flatpickr-disabled") |
| 1480 |
.should("be.visible"); |
| 1481 |
} |
| 1482 |
|
| 1483 |
// Days 10-12 should still be available (3 items: ITEM1, ITEM2, ITEM3) |
| 1484 |
for (let day = 10; day <= 12; day++) { |
| 1485 |
const endDate = today.add(day, "day"); |
| 1486 |
cy.get("@flatpickrInput") |
| 1487 |
.getFlatpickrDate(endDate.toDate()) |
| 1488 |
.should("not.have.class", "flatpickr-disabled"); |
| 1489 |
} |
| 1490 |
|
| 1491 |
// Days 13-17 should still be available (1 item: ITEM2 only) |
| 1492 |
for (let day = 13; day <= 17; day++) { |
| 1493 |
const endDate = today.add(day, "day"); |
| 1494 |
cy.get("@flatpickrInput") |
| 1495 |
.getFlatpickrDate(endDate.toDate()) |
| 1496 |
.should("not.have.class", "flatpickr-disabled"); |
| 1497 |
} |
| 1498 |
|
| 1499 |
// Days 18+ should be disabled (no items available - ITEM2 becomes unavailable) |
| 1500 |
for (let day = 18; day <= 20; day++) { |
| 1501 |
const endDate = today.add(day, "day"); |
| 1502 |
cy.get("@flatpickrInput") |
| 1503 |
.getFlatpickrDate(endDate.toDate()) |
| 1504 |
.should("have.class", "flatpickr-disabled"); |
| 1505 |
} |
| 1506 |
|
| 1507 |
/** |
| 1508 |
* TEST SCENARIO 2: Start Date Day 8 - All Items Available, Maximum Window |
| 1509 |
* |
| 1510 |
* Expected availability progression from day 8: |
| 1511 |
* Days 8-9: Items available: ALL 4 items |
| 1512 |
* Days 10-12: Items available: ITEM1, ITEM2, ITEM3 (lose ITEM0) |
| 1513 |
* Days 13-17: Items available: ITEM2, ITEM3 (lose ITEM1) |
| 1514 |
* Days 18-22: Items available: ITEM3 only (lose ITEM2) |
| 1515 |
* Days 23+: No items available |
| 1516 |
* |
| 1517 |
* Maximum window should extend to day 22 |
| 1518 |
*/ |
| 1519 |
|
| 1520 |
cy.log( |
| 1521 |
"=== Testing Start Date Day 8 (All items available initially) ===" |
| 1522 |
); |
| 1523 |
|
| 1524 |
cy.get("@flatpickrInput").clearFlatpickr(); |
| 1525 |
cy.get("@flatpickrInput").openFlatpickr(); |
| 1526 |
|
| 1527 |
const startDate2 = today.add(8, "day"); |
| 1528 |
cy.log("Selecting start date of ", startDate2.toString()); |
| 1529 |
cy.get("@flatpickrInput") |
| 1530 |
.getFlatpickrDate(startDate2.toDate()) |
| 1531 |
.click(); |
| 1532 |
|
| 1533 |
cy.log("Checking maximum window from optimal start date"); |
| 1534 |
|
| 1535 |
// Should be able to book all the way to day 22 |
| 1536 |
for (let day = 9; day <= 22; day++) { |
| 1537 |
const endDate = today.add(day, "day"); |
| 1538 |
cy.get("@flatpickrInput") |
| 1539 |
.getFlatpickrDate(endDate.toDate()) |
| 1540 |
.should("not.have.class", "flatpickr-disabled"); |
| 1541 |
} |
| 1542 |
|
| 1543 |
// Days 23+ should be disabled |
| 1544 |
for (let day = 23; day <= 25; day++) { |
| 1545 |
const endDate = today.add(day, "day"); |
| 1546 |
cy.get("@flatpickrInput") |
| 1547 |
.getFlatpickrDate(endDate.toDate()) |
| 1548 |
.should("have.class", "flatpickr-disabled"); |
| 1549 |
} |
| 1550 |
|
| 1551 |
/** |
| 1552 |
* TEST SCENARIO 3: Start Date Day 14 - Reduced Initial Pool, Still Maximize |
| 1553 |
* |
| 1554 |
* Expected availability progression from day 14: |
| 1555 |
* Days 14-17: Items available: ITEM2, ITEM3 (ITEM0 & ITEM1 booked) |
| 1556 |
* Days 18-22: Items available: ITEM3 only (ITEM2 becomes unavailable) |
| 1557 |
* Days 23+: No items available |
| 1558 |
* |
| 1559 |
* Should still extend to day 22 by using ITEM3 |
| 1560 |
*/ |
| 1561 |
|
| 1562 |
cy.log( |
| 1563 |
"=== Testing Start Date Day 14 (Reduced pool but maximize window) ===" |
| 1564 |
); |
| 1565 |
|
| 1566 |
cy.get("@flatpickrInput").clearFlatpickr(); |
| 1567 |
cy.get("@flatpickrInput").openFlatpickr(); |
| 1568 |
|
| 1569 |
const startDate3 = today.add(14, "day"); |
| 1570 |
cy.log("Selecting start date of ", startDate3.toString()); |
| 1571 |
cy.get("@flatpickrInput") |
| 1572 |
.getFlatpickrDate(startDate3.toDate()) |
| 1573 |
.click(); |
| 1574 |
|
| 1575 |
cy.log( |
| 1576 |
"Checking window maximization with reduced initial pool" |
| 1577 |
); |
| 1578 |
|
| 1579 |
// Days 15-22 should all be available |
| 1580 |
for (let day = 15; day <= 22; day++) { |
| 1581 |
const endDate = today.add(day, "day"); |
| 1582 |
cy.get("@flatpickrInput") |
| 1583 |
.getFlatpickrDate(endDate.toDate()) |
| 1584 |
.should("not.have.class", "flatpickr-disabled"); |
| 1585 |
} |
| 1586 |
|
| 1587 |
// Days 23+ should be disabled |
| 1588 |
for (let day = 23; day <= 25; day++) { |
| 1589 |
const endDate = today.add(day, "day"); |
| 1590 |
cy.get("@flatpickrInput") |
| 1591 |
.getFlatpickrDate(endDate.toDate()) |
| 1592 |
.should("have.class", "flatpickr-disabled"); |
| 1593 |
} |
| 1594 |
|
| 1595 |
/** |
| 1596 |
* TEST SCENARIO 4: Start Date Day 19 - Multi-Item Window |
| 1597 |
* |
| 1598 |
* Expected availability from day 19: |
| 1599 |
* Days 19-22: Items available: ITEM0, ITEM3 (ITEM0 booking 10-15 is over) |
| 1600 |
* Days 23+: Items available: ITEM0 only (ITEM3 becomes booked 23-30) |
| 1601 |
* |
| 1602 |
* Algorithm should keep ITEM0 available throughout since it was available |
| 1603 |
* on start date and never becomes unavailable again |
| 1604 |
*/ |
| 1605 |
|
| 1606 |
cy.log( |
| 1607 |
"=== Testing Start Date Day 19 (Multi-item available) ===" |
| 1608 |
); |
| 1609 |
|
| 1610 |
cy.get("@flatpickrInput").clearFlatpickr(); |
| 1611 |
cy.get("@flatpickrInput").openFlatpickr(); |
| 1612 |
|
| 1613 |
const startDate4 = today.add(19, "day"); |
| 1614 |
cy.log("Selecting start date of ", startDate4.toString()); |
| 1615 |
cy.get("@flatpickrInput") |
| 1616 |
.getFlatpickrDate(startDate4.toDate()) |
| 1617 |
.click(); |
| 1618 |
|
| 1619 |
cy.log("Checking multi-item window maximization from day 19"); |
| 1620 |
|
| 1621 |
// Days 20-22 should be available (ITEM0 and ITEM3 both available) |
| 1622 |
for (let day = 20; day <= 22; day++) { |
| 1623 |
const endDate = today.add(day, "day"); |
| 1624 |
cy.get("@flatpickrInput") |
| 1625 |
.getFlatpickrDate(endDate.toDate()) |
| 1626 |
.should("not.have.class", "flatpickr-disabled"); |
| 1627 |
} |
| 1628 |
|
| 1629 |
// Days 23-25 should still be available (ITEM0 remains in pool) |
| 1630 |
for (let day = 23; day <= 25; day++) { |
| 1631 |
const endDate = today.add(day, "day"); |
| 1632 |
cy.get("@flatpickrInput") |
| 1633 |
.getFlatpickrDate(endDate.toDate()) |
| 1634 |
.should("not.have.class", "flatpickr-disabled"); |
| 1635 |
} |
| 1636 |
|
| 1637 |
/** |
| 1638 |
* TEST SCENARIO 5: Cross-Scenario Consistency Validation |
| 1639 |
* |
| 1640 |
* Verify that changing start dates properly recalculates maximum windows |
| 1641 |
*/ |
| 1642 |
|
| 1643 |
cy.log("=== Verifying cross-scenario consistency ==="); |
| 1644 |
|
| 1645 |
cy.get("@flatpickrInput").clearFlatpickr(); |
| 1646 |
cy.get("@flatpickrInput").openFlatpickr(); |
| 1647 |
|
| 1648 |
// Go to day 16 where ITEM0, ITEM2 and ITEM3 are available initially |
| 1649 |
const startDate6 = today.add(16, "day"); |
| 1650 |
cy.get("@flatpickrInput") |
| 1651 |
.getFlatpickrDate(startDate6.toDate()) |
| 1652 |
.click(); |
| 1653 |
|
| 1654 |
// Should be able to book until day 22 via multiple items |
| 1655 |
const endDate6 = today.add(22, "day"); |
| 1656 |
cy.get("@flatpickrInput") |
| 1657 |
.getFlatpickrDate(endDate6.toDate()) |
| 1658 |
.should("not.have.class", "flatpickr-disabled"); |
| 1659 |
|
| 1660 |
// Day 23+ should still be available because ITEM0 remains in pool |
| 1661 |
const endDate6_available = today.add(23, "day"); |
| 1662 |
cy.get("@flatpickrInput") |
| 1663 |
.getFlatpickrDate(endDate6_available.toDate()) |
| 1664 |
.should("not.have.class", "flatpickr-disabled"); |
| 1665 |
|
| 1666 |
cy.log( |
| 1667 |
"Dynamic item pool reduction and window maximization test completed" |
| 1668 |
); |
| 1669 |
}); |
| 1670 |
|
| 1671 |
// Cleanup: Delete test data |
| 1672 |
cy.then(() => { |
| 1673 |
if (testBiblio) { |
| 1674 |
cy.task("query", { |
| 1675 |
sql: "DELETE FROM bookings WHERE biblio_id = ?", |
| 1676 |
values: [testBiblio.biblio_id], |
| 1677 |
}); |
| 1678 |
cy.task("deleteSampleObjects", { |
| 1679 |
biblio: testBiblio, |
| 1680 |
items: testItems, |
| 1681 |
}); |
| 1682 |
} |
| 1683 |
if (testPatron) { |
| 1684 |
cy.task("query", { |
| 1685 |
sql: "DELETE FROM borrowers WHERE borrowernumber = ?", |
| 1686 |
values: [testPatron.patron_id], |
| 1687 |
}); |
| 1688 |
} |
| 1689 |
}); |
| 1690 |
}); |
| 1082 |
}); |
1691 |
}); |
| 1083 |
- |
|
|