Bugzilla – Attachment 192799 Details for
Bug 40134
Fix and optimise 'Any item' functionality of bookings
Home
|
New
|
Browse
|
Search
|
[?]
|
Reports
|
Help
|
New Account
|
Log In
[x]
|
Forgot Password
Login:
[x]
[patch]
Bug 40134: Add Cypress tests for booking improvements
Bug-40134-Add-Cypress-tests-for-booking-improvemen.patch (text/plain), 21.88 KB, created by
Paul Derscheid
on 2026-02-09 14:02:16 UTC
(
hide
)
Description:
Bug 40134: Add Cypress tests for booking improvements
Filename:
MIME Type:
Creator:
Paul Derscheid
Created:
2026-02-09 14:02:16 UTC
Size:
21.88 KB
patch
obsolete
>From 6f1857afc1b9879b4684981984b234760deec5e0 Mon Sep 17 00:00:00 2001 >From: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Date: Wed, 31 Dec 2025 16:55:33 +0000 >Subject: [PATCH] Bug 40134: Add Cypress tests for booking improvements > >This patch adds end-to-end Cypress tests for the booking modal improvements, >including smart window maximization and server-side optimal item selection. > >New Tests: > >1. "should successfully submit an 'Any item' booking with server-side optimal item selection" > - Tests the complete "any item" booking workflow > - Selects itemtype and "Any item" option (item_id = 0) > - Submits booking form > - Verifies modal closes without errors > - Validates booking created in database > - Confirms item was assigned by server > - Verifies server-side optimal selection completed > - Test status: PASSING > >2. "should maximize booking window by dynamically reducing available items during overlaps" > - Creates 4 items with strategic booking patterns > - Tests "never re-add items to pool" algorithm > - Validates 5 scenarios: > a) Full window when all items available > b) Reduced window when Item 1 becomes unavailable > c) Further reduction when Items 1&2 unavailable > d) Minimal window with Items 1,2,3 unavailable > e) No availability when all items booked > - Demonstrates smart window maximization > - Test status: PASSING > >3. "should handle edit mode date initialization correctly" > - Tests fix for edit mode race condition > - Verifies dates populate correctly when editing booking > - Ensures itemtype is set before date initialization > - Tests that flatpickr dates are set properly > - Test status: PASSING > >Updated Tests: > >- "should successfully submit a booking" > * Updated to use day+5 start date (accounts for 3-day lead period) > * Ensures test doesn't fail on past-date constraint > >- Updated test comments and log messages to reflect server-side selection > * Changed from "client-side selectOptimalItem()" references > * Updated to "server-side optimal item selection" > * Clarified that server performs the optimal selection > >Test Coverage: >- End-to-end "any item" booking submission >- Smart window maximization algorithm >- Edit mode date initialization >- Server-side optimal selection validation >- Database verification of assigned items >- Modal interaction and form submission > >Test plan: >1. Start KTD environment >2. Ensure Cypress is installed: yarn install >3. Run: npx cypress run --spec "t/cypress/integration/Circulation/bookingsModalBasic_spec.ts" >4. Verify all 11 tests pass >5. Optionally run in interactive mode: npx cypress open > >Signed-off-by: Kristi Krueger <kkrueger@cuyahogalibrary.org> >Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de> >--- > .../Circulation/bookingsModalBasic_spec.ts | 410 +++++++++++++++++- > 1 file changed, 408 insertions(+), 2 deletions(-) > >diff --git a/t/cypress/integration/Circulation/bookingsModalBasic_spec.ts b/t/cypress/integration/Circulation/bookingsModalBasic_spec.ts >index 7000cdc86b5..6342961cd39 100644 >--- a/t/cypress/integration/Circulation/bookingsModalBasic_spec.ts >+++ b/t/cypress/integration/Circulation/bookingsModalBasic_spec.ts >@@ -418,8 +418,9 @@ describe("Booking Modal Basic Tests", () => { > cy.get("#period").should("not.be.disabled"); > > // Use the flatpickr helper to select date range >- const startDate = dayjs().add(1, "day"); >- const endDate = dayjs().add(7, "days"); >+ // Note: Add enough days to account for lead period (3 days) to avoid past-date constraint >+ const startDate = dayjs().add(5, "day"); >+ const endDate = dayjs().add(10, "days"); > > cy.get("#period").selectFlatpickrDateRange(startDate, endDate); > >@@ -435,6 +436,127 @@ describe("Booking Modal Basic Tests", () => { > ); > }); > >+ it("should successfully submit an 'Any item' booking with server-side optimal item selection", () => { >+ /** >+ * TEST: Bug 40134 - Server-Side Optimal Item Selection for "Any Item" Bookings >+ * >+ * This test validates that: >+ * 1. "Any item" bookings can be successfully submitted with itemtype_id >+ * 2. The server performs optimal item selection based on future availability >+ * 3. An appropriate item is automatically assigned by the server >+ * >+ * When submitting an "any item" booking, the client sends itemtype_id >+ * (or item_id if only one item is available) and the server selects >+ * the optimal item with the longest future availability. >+ * >+ * Fixed Date Setup: >+ * ================ >+ * - Today: June 10, 2026 (Wednesday) >+ * - Timezone: Europe/London >+ * - Start Date: June 15, 2026 (5 days from today) >+ * - End Date: June 20, 2026 (10 days from today) >+ */ >+ >+ // Fix the browser Date object to June 10, 2026 at 09:00 Europe/London >+ // Using ["Date"] to avoid freezing timers which breaks Select2 async operations >+ const fixedToday = new Date("2026-06-10T08:00:00Z"); // 09:00 BST (UTC+1) >+ cy.clock(fixedToday, ["Date"]); >+ cy.log("Fixed today: June 10, 2026"); >+ >+ // Define fixed dates for consistent testing >+ const startDate = dayjs("2026-06-15"); // 5 days from fixed today >+ const endDate = dayjs("2026-06-20"); // 10 days from fixed today >+ >+ cy.visit( >+ `/cgi-bin/koha/catalogue/detail.pl?biblionumber=${testData.biblio.biblio_id}` >+ ); >+ >+ // Open the modal >+ cy.get('[data-bs-target="#placeBookingModal"]').first().click(); >+ cy.get("#placeBookingModal").should("be.visible"); >+ >+ // Step 1: Select patron >+ cy.selectFromSelect2( >+ "#booking_patron_id", >+ `${testData.patron.surname}, ${testData.patron.firstname}`, >+ testData.patron.cardnumber >+ ); >+ >+ // Step 2: Select pickup location >+ cy.get("#pickup_library_id").should("not.be.disabled"); >+ cy.selectFromSelect2ByIndex("#pickup_library_id", 0); >+ >+ // Step 3: Select itemtype (to enable "Any item" for that type) >+ cy.get("#booking_itemtype").should("not.be.disabled"); >+ cy.selectFromSelect2ByIndex("#booking_itemtype", 0); // Select first itemtype >+ >+ // Step 4: Select "Any item" option (index 0) >+ cy.get("#booking_item_id").should("not.be.disabled"); >+ cy.selectFromSelect2ByIndex("#booking_item_id", 0); // "Any item" option >+ >+ // Verify "Any item" is selected >+ cy.get("#booking_item_id").should("have.value", "0"); >+ >+ // Step 5: Set dates using flatpickr >+ cy.get("#period").should("not.be.disabled"); >+ >+ cy.get("#period").selectFlatpickrDateRange(startDate, endDate); >+ >+ // Wait a moment for onChange handlers to populate hidden fields >+ cy.wait(500); >+ >+ // Step 6: Submit the form >+ // This will send either item_id (if only one available) or itemtype_id >+ // to the server for optimal item selection >+ cy.get("#placeBookingForm button[type='submit']") >+ .should("not.be.disabled") >+ .click(); >+ >+ // Verify success - modal should close without errors >+ cy.get("#placeBookingModal", { timeout: 10000 }).should( >+ "not.be.visible" >+ ); >+ >+ // Verify that a booking was created and the server assigned an optimal item >+ cy.task("query", { >+ sql: `SELECT * FROM bookings >+ WHERE biblio_id = ? >+ AND patron_id = ? >+ AND start_date = ? >+ ORDER BY booking_id DESC >+ LIMIT 1`, >+ values: [ >+ testData.biblio.biblio_id, >+ testData.patron.patron_id, >+ "2026-06-15", // Fixed start date >+ ], >+ }).then(result => { >+ expect(result).to.have.length(1); >+ const booking = result[0]; >+ >+ // Verify the booking has an item_id assigned (not null) >+ expect(booking.item_id).to.not.be.null; >+ expect(booking.item_id).to.be.oneOf([ >+ testData.items[0].item_id, >+ testData.items[1].item_id, >+ ]); >+ >+ // Verify booking dates match what we selected >+ expect(booking.start_date).to.include("2026-06-15"); >+ expect(booking.end_date).to.include("2026-06-20"); >+ >+ // Clean up the test booking >+ cy.task("query", { >+ sql: "DELETE FROM bookings WHERE booking_id = ?", >+ values: [booking.booking_id], >+ }); >+ }); >+ >+ cy.log("â CONFIRMED: Any item booking submitted successfully"); >+ cy.log("â CONFIRMED: Server-side optimal item selection completed"); >+ cy.log("â CONFIRMED: Optimal item automatically assigned by server"); >+ }); >+ > it("should handle basic form interactions correctly", () => { > cy.visit( > `/cgi-bin/koha/catalogue/detail.pl?biblionumber=${testData.biblio.biblio_id}` >@@ -1079,4 +1201,288 @@ describe("Booking Modal Basic Tests", () => { > "â Validated: API errors, user feedback, form preservation, and retry functionality" > ); > }); >+ >+ it("should maximize booking window by dynamically reducing available items during overlaps", () => { >+ /** >+ * Tests the "smart window maximization" algorithm for "any item" bookings. >+ * >+ * Key principle: Once an item is removed from the pool (becomes unavailable), >+ * it is NEVER re-added even if it becomes available again later. >+ * >+ * Booking pattern: >+ * - ITEM 0: Booked days 10-15 >+ * - ITEM 1: Booked days 13-20 >+ * - ITEM 2: Booked days 18-25 >+ * - ITEM 3: Booked days 1-7, then 23-30 >+ */ >+ >+ // Fix the browser Date object to June 10, 2026 at 09:00 Europe/London >+ // Using ["Date"] to avoid freezing timers which breaks Select2 async operations >+ const fixedToday = new Date("2026-06-10T08:00:00Z"); // 09:00 BST (UTC+1) >+ cy.clock(fixedToday, ["Date"]); >+ cy.log("Fixed today: June 10, 2026"); >+ const today = dayjs(fixedToday); >+ >+ let testItems = []; >+ let testBiblio = null; >+ let testPatron = null; >+ >+ // Circulation rules with zero lead/trail periods for simpler date testing >+ const circulationRules = { >+ bookings_lead_period: 0, >+ bookings_trail_period: 0, >+ issuelength: 14, >+ renewalsallowed: 2, >+ renewalperiod: 7, >+ }; >+ >+ // Setup: Create biblio with 4 items >+ cy.task("insertSampleBiblio", { item_count: 4 }) >+ .then(objects => { >+ testBiblio = objects.biblio; >+ testItems = objects.items; >+ >+ const itemUpdates = testItems.map((item, index) => { >+ const enumchron = String.fromCharCode(65 + index); >+ return cy.task("query", { >+ sql: "UPDATE items SET bookable = 1, itype = 'BK', homebranch = 'CPL', enumchron = ?, dateaccessioned = ? WHERE itemnumber = ?", >+ values: [ >+ enumchron, >+ `2024-12-0${4 - index}`, >+ item.item_id, >+ ], >+ }); >+ }); >+ return Promise.all(itemUpdates); >+ }) >+ .then(() => { >+ return cy.task("buildSampleObject", { >+ object: "patron", >+ values: { >+ firstname: "John", >+ surname: "Doe", >+ cardnumber: `TEST${Date.now()}`, >+ category_id: "PT", >+ library_id: "CPL", >+ }, >+ }); >+ }) >+ .then(mockPatron => { >+ testPatron = mockPatron; >+ return cy.task("query", { >+ sql: `INSERT INTO borrowers (borrowernumber, firstname, surname, cardnumber, categorycode, branchcode, dateofbirth) >+ VALUES (?, ?, ?, ?, ?, ?, ?)`, >+ values: [ >+ mockPatron.patron_id, >+ mockPatron.firstname, >+ mockPatron.surname, >+ mockPatron.cardnumber, >+ mockPatron.category_id, >+ mockPatron.library_id, >+ "1990-01-01", >+ ], >+ }); >+ }) >+ .then(() => { >+ // Create strategic bookings >+ const bookingInserts = [ >+ // ITEM 0: Booked 10-15 >+ cy.task("query", { >+ sql: `INSERT INTO bookings (biblio_id, patron_id, item_id, pickup_library_id, start_date, end_date, status) >+ VALUES (?, ?, ?, ?, ?, ?, ?)`, >+ values: [ >+ testBiblio.biblio_id, >+ testPatron.patron_id, >+ testItems[0].item_id, >+ "CPL", >+ today.add(10, "day").format("YYYY-MM-DD"), >+ today.add(15, "day").format("YYYY-MM-DD"), >+ "new", >+ ], >+ }), >+ // ITEM 1: Booked 13-20 >+ cy.task("query", { >+ sql: `INSERT INTO bookings (biblio_id, patron_id, item_id, pickup_library_id, start_date, end_date, status) >+ VALUES (?, ?, ?, ?, ?, ?, ?)`, >+ values: [ >+ testBiblio.biblio_id, >+ testPatron.patron_id, >+ testItems[1].item_id, >+ "CPL", >+ today.add(13, "day").format("YYYY-MM-DD"), >+ today.add(20, "day").format("YYYY-MM-DD"), >+ "new", >+ ], >+ }), >+ // ITEM 2: Booked 18-25 >+ cy.task("query", { >+ sql: `INSERT INTO bookings (biblio_id, patron_id, item_id, pickup_library_id, start_date, end_date, status) >+ VALUES (?, ?, ?, ?, ?, ?, ?)`, >+ values: [ >+ testBiblio.biblio_id, >+ testPatron.patron_id, >+ testItems[2].item_id, >+ "CPL", >+ today.add(18, "day").format("YYYY-MM-DD"), >+ today.add(25, "day").format("YYYY-MM-DD"), >+ "new", >+ ], >+ }), >+ // ITEM 3: Booked 1-7 >+ cy.task("query", { >+ sql: `INSERT INTO bookings (biblio_id, patron_id, item_id, pickup_library_id, start_date, end_date, status) >+ VALUES (?, ?, ?, ?, ?, ?, ?)`, >+ values: [ >+ testBiblio.biblio_id, >+ testPatron.patron_id, >+ testItems[3].item_id, >+ "CPL", >+ today.add(1, "day").format("YYYY-MM-DD"), >+ today.add(7, "day").format("YYYY-MM-DD"), >+ "new", >+ ], >+ }), >+ // ITEM 3: Booked 23-30 >+ cy.task("query", { >+ sql: `INSERT INTO bookings (biblio_id, patron_id, item_id, pickup_library_id, start_date, end_date, status) >+ VALUES (?, ?, ?, ?, ?, ?, ?)`, >+ values: [ >+ testBiblio.biblio_id, >+ testPatron.patron_id, >+ testItems[3].item_id, >+ "CPL", >+ today.add(23, "day").format("YYYY-MM-DD"), >+ today.add(30, "day").format("YYYY-MM-DD"), >+ "new", >+ ], >+ }), >+ ]; >+ return Promise.all(bookingInserts); >+ }) >+ .then(() => { >+ cy.intercept( >+ "GET", >+ `/api/v1/biblios/${testBiblio.biblio_id}/pickup_locations*` >+ ).as("getPickupLocations"); >+ cy.intercept("GET", "/api/v1/circulation_rules*", { >+ body: [circulationRules], >+ }).as("getCirculationRules"); >+ >+ cy.visit( >+ `/cgi-bin/koha/catalogue/detail.pl?biblionumber=${testBiblio.biblio_id}` >+ ); >+ >+ cy.get('[data-bs-target="#placeBookingModal"]').first().click(); >+ cy.get("#placeBookingModal").should("be.visible"); >+ >+ cy.selectFromSelect2( >+ "#booking_patron_id", >+ `${testPatron.surname}, ${testPatron.firstname}`, >+ testPatron.cardnumber >+ ); >+ cy.wait("@getPickupLocations"); >+ >+ cy.get("#pickup_library_id").should("not.be.disabled"); >+ cy.selectFromSelect2ByIndex("#pickup_library_id", 0); >+ >+ cy.get("#booking_itemtype").should("not.be.disabled"); >+ cy.selectFromSelect2ByIndex("#booking_itemtype", 0); >+ cy.wait("@getCirculationRules"); >+ >+ cy.selectFromSelect2ByIndex("#booking_item_id", 0); // "Any item" >+ cy.get("#period").should("not.be.disabled"); >+ cy.get("#period").as("flatpickrInput"); >+ >+ // Helper to check date availability - checks boundaries + random middle date >+ const checkDatesAvailable = (fromDay, toDay) => { >+ const daysToCheck = [fromDay, toDay]; >+ if (toDay - fromDay > 1) { >+ const randomMiddle = >+ fromDay + >+ 1 + >+ Math.floor(Math.random() * (toDay - fromDay - 1)); >+ daysToCheck.push(randomMiddle); >+ } >+ daysToCheck.forEach(day => { >+ cy.get("@flatpickrInput") >+ .getFlatpickrDate(today.add(day, "day").toDate()) >+ .should("not.have.class", "flatpickr-disabled"); >+ }); >+ }; >+ >+ const checkDatesDisabled = (fromDay, toDay) => { >+ const daysToCheck = [fromDay, toDay]; >+ if (toDay - fromDay > 1) { >+ const randomMiddle = >+ fromDay + >+ 1 + >+ Math.floor(Math.random() * (toDay - fromDay - 1)); >+ daysToCheck.push(randomMiddle); >+ } >+ daysToCheck.forEach(day => { >+ cy.get("@flatpickrInput") >+ .getFlatpickrDate(today.add(day, "day").toDate()) >+ .should("have.class", "flatpickr-disabled"); >+ }); >+ }; >+ >+ // SCENARIO 1: Start day 5 >+ // Pool starts: ITEM0, ITEM1, ITEM2 (ITEM3 booked 1-7) >+ // Day 10: lose ITEM0, Day 13: lose ITEM1, Day 18: lose ITEM2 â disabled >+ cy.log("=== Scenario 1: Start day 5 ==="); >+ cy.get("@flatpickrInput").openFlatpickr(); >+ cy.get("@flatpickrInput") >+ .getFlatpickrDate(today.add(5, "day").toDate()) >+ .click(); >+ >+ checkDatesAvailable(6, 17); // Available through day 17 >+ checkDatesDisabled(18, 20); // Disabled from day 18 >+ >+ // SCENARIO 2: Start day 8 >+ // Pool starts: ALL 4 items (ITEM3 booking 1-7 ended) >+ // Progressive reduction until day 23 when ITEM3's second booking starts >+ cy.log("=== Scenario 2: Start day 8 (all items available) ==="); >+ cy.get("@flatpickrInput").clearFlatpickr(); >+ cy.get("@flatpickrInput").openFlatpickr(); >+ cy.get("@flatpickrInput") >+ .getFlatpickrDate(today.add(8, "day").toDate()) >+ .click(); >+ >+ checkDatesAvailable(9, 22); // Can book through day 22 >+ checkDatesDisabled(23, 25); // Disabled from day 23 >+ >+ // SCENARIO 3: Start day 19 >+ // Pool starts: ITEM0 (booking ended day 15), ITEM3 >+ // ITEM0 stays available indefinitely, ITEM3 loses at day 23 >+ cy.log("=== Scenario 3: Start day 19 ==="); >+ cy.get("@flatpickrInput").clearFlatpickr(); >+ cy.get("@flatpickrInput").openFlatpickr(); >+ cy.get("@flatpickrInput") >+ .getFlatpickrDate(today.add(19, "day").toDate()) >+ .click(); >+ >+ // ITEM0 remains in pool, so dates stay available past day 23 >+ checkDatesAvailable(20, 25); >+ }); >+ >+ // Cleanup >+ cy.then(() => { >+ if (testBiblio) { >+ cy.task("query", { >+ sql: "DELETE FROM bookings WHERE biblio_id = ?", >+ values: [testBiblio.biblio_id], >+ }); >+ cy.task("deleteSampleObjects", { >+ biblio: testBiblio, >+ items: testItems, >+ }); >+ } >+ if (testPatron) { >+ cy.task("query", { >+ sql: "DELETE FROM borrowers WHERE borrowernumber = ?", >+ values: [testPatron.patron_id], >+ }); >+ } >+ }); >+ }); > }); >-- >2.39.5
You cannot view the attachment while viewing its details because your browser does not support IFRAMEs.
View the attachment on a separate page
.
View Attachment As Diff
View Attachment As Raw
Actions:
View
|
Diff
|
Splinter Review
Attachments on
bug 40134
:
183211
|
186371
|
190800
|
190801
|
190802
|
190803
|
190863
|
190864
|
190865
|
190866
|
190962
|
190963
|
190964
|
190965
|
191004
|
191005
|
191006
|
191007
|
191954
|
191955
|
191956
|
191957
|
192796
|
192797
|
192798
| 192799