Bugzilla – Attachment 189350 Details for
Bug 41129
Migrate place_booking.js to a Vue island.
Home
|
New
|
Browse
|
Search
|
[?]
|
Reports
|
Help
|
New Account
|
Log In
[x]
|
Forgot Password
Login:
[x]
[patch]
Bug 41129: Add cypress tests
Bug-41129-Add-cypress-tests.patch (text/plain), 32.08 KB, created by
Paul Derscheid
on 2025-11-07 16:56:12 UTC
(
hide
)
Description:
Bug 41129: Add cypress tests
Filename:
MIME Type:
Creator:
Paul Derscheid
Created:
2025-11-07 16:56:12 UTC
Size:
32.08 KB
patch
obsolete
>From 41c083401898d262c9a3881bcd8ddb5940e471bc Mon Sep 17 00:00:00 2001 >From: Paul Derscheid <paul.derscheid@lmscloud.de> >Date: Fri, 7 Nov 2025 15:38:48 +0000 >Subject: [PATCH] Bug 41129: Add cypress tests > >To test: >- Run `yarn cypress run --spec t/cypress/integration/Bookings/BookingModal_spec.ts` >--- > .../integration/Bookings/BookingModal_spec.ts | 867 ++++++++++++++++++ > 1 file changed, 867 insertions(+) > create mode 100644 t/cypress/integration/Bookings/BookingModal_spec.ts > >diff --git a/t/cypress/integration/Bookings/BookingModal_spec.ts b/t/cypress/integration/Bookings/BookingModal_spec.ts >new file mode 100644 >index 00000000000..d95ad9e70b1 >--- /dev/null >+++ b/t/cypress/integration/Bookings/BookingModal_spec.ts >@@ -0,0 +1,867 @@ >+import dayjs = require("dayjs"); >+ >+interface BookingTestContext { >+ biblio: any; >+ patron: any; >+ bookingsToCleanup: Array<{ booking_id: number }>; >+} >+ >+interface BookingInterceptOverrides { >+ bookableItems?: Cypress.RouteHandler; >+ loadBookings?: Cypress.RouteHandler; >+ loadCheckouts?: Cypress.RouteHandler; >+ pickupLocations?: Cypress.RouteHandler; >+ searchPatrons?: Cypress.RouteHandler; >+ circulationRules?: Cypress.RouteHandler; >+} >+ >+const ensureBookableInventory = (biblio: any) => { >+ const itemTypeId = biblio.item_type.item_type_id; >+ const itemNumbers = biblio.items.map((item: any) => item.item_id); >+ >+ const updateItemType = cy.task("query", { >+ sql: "UPDATE itemtypes SET bookable=1 WHERE itemtype=?", >+ values: [itemTypeId], >+ }); >+ >+ if (!itemNumbers.length) { >+ return updateItemType; >+ } >+ >+ const placeholders = itemNumbers.map(() => "?").join(","); >+ return updateItemType.then(() => >+ cy.task("query", { >+ sql: `UPDATE items SET bookable=1 WHERE itemnumber IN (${placeholders})`, >+ values: itemNumbers, >+ }) >+ ); >+}; >+ >+const ensureBookingCapacity = ({ >+ libraryId, >+ categoryId, >+ itemTypeId, >+}: { >+ libraryId: string; >+ categoryId: string; >+ itemTypeId: string; >+}) => { >+ const rules = [ >+ { name: "issuelength", value: 7 }, >+ { name: "renewalsallowed", value: 1 }, >+ { name: "renewalperiod", value: 7 }, >+ { name: "bookings_lead_period", value: 0 }, >+ { name: "bookings_trail_period", value: 0 }, >+ ]; >+ const ruleNames = rules.map(rule => rule.name); >+ const deletePlaceholders = ruleNames.map(() => "?").join(","); >+ const insertPlaceholders = rules.map(() => "(?, ?, ?, ?, ?)").join(","); >+ const insertValues: Array<string | number> = []; >+ rules.forEach(rule => { >+ insertValues.push( >+ libraryId, >+ categoryId, >+ itemTypeId, >+ rule.name, >+ String(rule.value) >+ ); >+ }); >+ >+ return cy >+ .task("query", { >+ sql: `DELETE FROM circulation_rules WHERE branchcode=? AND categorycode=? AND itemtype=? AND rule_name IN (${deletePlaceholders})`, >+ values: [libraryId, categoryId, itemTypeId, ...ruleNames], >+ }) >+ .then(() => >+ cy.task("query", { >+ sql: `INSERT INTO circulation_rules (branchcode, categorycode, itemtype, rule_name, rule_value) VALUES ${insertPlaceholders}`, >+ values: insertValues, >+ }) >+ ); >+}; >+ >+const deleteBookings = (bookingIds: number[]) => { >+ if (!bookingIds.length) { >+ return cy.wrap(null); >+ } >+ >+ const placeholders = bookingIds.map(() => "?").join(","); >+ return cy.task("query", { >+ sql: `DELETE FROM bookings WHERE booking_id IN (${placeholders})`, >+ values: bookingIds, >+ }); >+}; >+ >+const waitForBookingIslandReady = () => { >+ cy.get("booking-modal-island").should("exist"); >+ cy.get("booking-modal-island .modal").should("exist"); >+ return cy.wait(100); >+}; >+ >+const ensurePatronSearchQueryBuilder = () => >+ cy.window().then(win => { >+ const globalWin = win as Window & { >+ buildPatronSearchQuery?: ( >+ term: string, >+ options?: Record<string, any> >+ ) => any; >+ }; >+ if (typeof globalWin.buildPatronSearchQuery === "function") return; >+ globalWin.buildPatronSearchQuery = ( >+ term: string, >+ options: Record<string, any> = {} >+ ) => { >+ if (!term) return []; >+ const table_prefix = options.table_prefix || "me"; >+ const search_fields = options.search_fields >+ ? options.search_fields >+ .split(",") >+ .map((field: string) => field.trim()) >+ : ["surname", "firstname", "cardnumber", "userid"]; >+ >+ const queries: Record<string, unknown>[] = []; >+ search_fields.forEach(field => { >+ queries.push({ >+ [`${table_prefix}.${field}`]: { >+ like: `%${term}%`, >+ }, >+ }); >+ }); >+ >+ return [{ "-or": queries }]; >+ }; >+ }); >+ >+const prepareBookingModalPage = () => >+ waitForBookingIslandReady().then(() => ensurePatronSearchQueryBuilder()); >+ >+const setDateRange = (startDate: dayjs.Dayjs, endDate: dayjs.Dayjs) => { >+ cy.get(".modal.show #booking_period").click({ force: true }); >+ cy.get(".flatpickr-calendar", { timeout: 5000 }).should("be.visible"); >+ >+ const startDateStr = startDate.format("MMMM D, YYYY"); >+ cy.get(".flatpickr-calendar") >+ .find(`.flatpickr-day[aria-label="${startDateStr}"]`) >+ .click({ force: true }); >+ >+ const endDateStr = endDate.format("MMMM D, YYYY"); >+ cy.get(".flatpickr-calendar") >+ .find(`.flatpickr-day[aria-label="${endDateStr}"]`) >+ .click({ force: true }); >+ >+ cy.wait(500); >+}; >+ >+const interceptBookingModalData = ( >+ biblionumber: number | string, >+ overrides: BookingInterceptOverrides = {} >+) => { >+ const itemsUrl = `/api/v1/biblios/${biblionumber}/items*`; >+ if (overrides.bookableItems) { >+ cy.intercept("GET", itemsUrl, overrides.bookableItems).as( >+ "bookableItems" >+ ); >+ } else { >+ cy.intercept("GET", itemsUrl).as("bookableItems"); >+ } >+ >+ const bookingsUrl = `/api/v1/biblios/${biblionumber}/bookings*`; >+ if (overrides.loadBookings) { >+ cy.intercept("GET", bookingsUrl, overrides.loadBookings).as( >+ "loadBookings" >+ ); >+ } else { >+ cy.intercept("GET", bookingsUrl).as("loadBookings"); >+ } >+ >+ const checkoutsUrl = `/api/v1/biblios/${biblionumber}/checkouts*`; >+ if (overrides.loadCheckouts) { >+ cy.intercept("GET", checkoutsUrl, overrides.loadCheckouts).as( >+ "loadCheckouts" >+ ); >+ } else { >+ cy.intercept("GET", checkoutsUrl).as("loadCheckouts"); >+ } >+ >+ const pickupsUrl = `/api/v1/biblios/${biblionumber}/pickup_locations*`; >+ if (overrides.pickupLocations) { >+ cy.intercept("GET", pickupsUrl, overrides.pickupLocations).as( >+ "pickupLocations" >+ ); >+ } else { >+ cy.intercept("GET", pickupsUrl).as("pickupLocations"); >+ } >+ >+ const searchUrl = /\/api\/v1\/patrons\?.*q=.*/; >+ if (overrides.searchPatrons) { >+ cy.intercept("GET", searchUrl, overrides.searchPatrons).as( >+ "searchPatrons" >+ ); >+ } else { >+ cy.intercept("GET", searchUrl).as("searchPatrons"); >+ } >+ >+ const rulesUrl = /\/api\/v1\/circulation_rules.*/; >+ if (overrides.circulationRules) { >+ cy.intercept("GET", rulesUrl, overrides.circulationRules).as( >+ "circulationRules" >+ ); >+ } else { >+ cy.intercept("GET", rulesUrl).as("circulationRules"); >+ } >+}; >+ >+const openBookingModalFromList = (biblionumber: number | string) => { >+ cy.window().its("openBookingModal").should("be.a", "function"); >+ cy.contains("button[data-booking-modal]", /Place booking/i).should( >+ "be.visible" >+ ); >+ >+ cy.window().then(win => { >+ const globalWin = win as Window & { >+ openBookingModal?: (props: Record<string, any>) => void; >+ }; >+ if (typeof globalWin.openBookingModal === "function") { >+ globalWin.openBookingModal({ biblionumber: String(biblionumber) }); >+ } else { >+ throw new Error("window.openBookingModal is not available"); >+ } >+ }); >+ >+ cy.wait(["@bookableItems", "@loadBookings"], { timeout: 20000 }); >+ cy.get(".modal", { timeout: 15000 }) >+ .should("exist") >+ .and("have.class", "show") >+ .and("be.visible"); >+}; >+ >+const selectVsOption = (text: string) => { >+ cy.get(".modal.show .vs__dropdown-menu li:not(.vs__no-options)", { >+ timeout: 10000, >+ }) >+ .contains(text) >+ .should("be.visible") >+ .click({ force: true }); >+}; >+ >+const selectPatronAndInventory = ( >+ patron: any, >+ pickupLibrary: any, >+ item: any, >+ options: { skipItemSelection?: boolean } = {} >+) => { >+ const searchTerm = patron.cardnumber.slice(0, 4); >+ >+ cy.get(".modal.show #booking_patron") >+ .clear() >+ .type(searchTerm, { delay: 0 }); >+ cy.wait("@searchPatrons"); >+ cy.wait(200); >+ selectVsOption(patron.cardnumber); >+ >+ cy.wait("@pickupLocations"); >+ cy.get(".modal.show #pickup_library_id").should( >+ "not.have.attr", >+ "disabled" >+ ); >+ cy.get(".modal.show #pickup_library_id").click({ force: true }); >+ cy.wait(200); >+ selectVsOption(pickupLibrary.name); >+ >+ if (!options.skipItemSelection) { >+ cy.get(".modal.show #booking_item_id").click({ force: true }); >+ cy.wait(200); >+ selectVsOption(item.external_id); >+ cy.wait("@circulationRules"); >+ } >+}; >+ >+describe("BookingModal integration", () => { >+ beforeEach(function (this: BookingTestContext) { >+ cy.login(); >+ cy.title().should("eq", "Koha staff interface"); >+ >+ return cy >+ .task("insertSampleBiblio", { item_count: 1 }) >+ .then(biblio => { >+ this.biblio = biblio; >+ return ensureBookableInventory(biblio); >+ }) >+ .then(() => >+ cy.task("insertSamplePatron", { >+ library: this.biblio.libraries[0], >+ }) >+ ) >+ .then(patron => { >+ this.patron = patron; >+ this.bookingsToCleanup = []; >+ return ensureBookingCapacity({ >+ libraryId: this.biblio.libraries[0].library_id, >+ categoryId: patron.patron.category_id, >+ itemTypeId: this.biblio.item_type.item_type_id, >+ }); >+ }); >+ }); >+ >+ afterEach(function (this: BookingTestContext) { >+ const bookingIds = >+ this.bookingsToCleanup?.map(booking => booking.booking_id) || []; >+ if (bookingIds.length) { >+ deleteBookings(bookingIds); >+ } >+ >+ const cleanupTargets = []; >+ if (this.patron) cleanupTargets.push(this.patron); >+ if (this.biblio) cleanupTargets.push(this.biblio); >+ if (cleanupTargets.length) { >+ cy.task("deleteSampleObjects", cleanupTargets); >+ } >+ }); >+ >+ it("Creates a booking", function (this: BookingTestContext) { >+ const ctx = this as BookingTestContext; >+ const biblionumber = ctx.biblio.biblio.biblio_id; >+ const patron = ctx.patron.patron; >+ const pickupLibrary = ctx.biblio.libraries[0]; >+ const item = ctx.biblio.items[0]; >+ const startDate = dayjs().add(3, "day").startOf("day"); >+ const endDate = startDate.add(2, "day"); >+ >+ cy.visit(`/cgi-bin/koha/bookings/list.pl?biblionumber=${biblionumber}`); >+ cy.get("#bookings_table").should("exist"); >+ prepareBookingModalPage(); >+ >+ interceptBookingModalData(biblionumber); >+ cy.intercept("POST", "/api/v1/bookings").as("createBooking"); >+ >+ openBookingModalFromList(biblionumber); >+ >+ cy.get(".modal-title").should("contain", "Place booking"); >+ cy.contains(".step-header", "Select Patron").should("exist"); >+ cy.get("button[form='form-booking']").should("be.disabled"); >+ cy.get("#pickup_library_id").should("have.attr", "disabled"); >+ >+ selectPatronAndInventory(patron, pickupLibrary, item); >+ cy.get("#booking_period").should("not.be.disabled"); >+ setDateRange(startDate, endDate); >+ cy.wait(1000); >+ >+ cy.get("button[form='form-booking']", { timeout: 15000 }) >+ .should("not.be.disabled") >+ .contains("Place booking") >+ .click(); >+ >+ cy.wait("@createBooking").then(({ response }) => { >+ expect(response?.statusCode).to.eq(201); >+ if (response?.body) { >+ ctx.bookingsToCleanup.push(response.body); >+ } >+ }); >+ >+ cy.get("body").should("not.have.class", "modal-open"); >+ cy.contains("#bookings_table tbody tr", item.external_id).should( >+ "exist" >+ ); >+ cy.contains("#bookings_table tbody tr", patron.cardnumber).should( >+ "exist" >+ ); >+ }); >+ >+ it("Updates a booking", function (this: BookingTestContext) { >+ const ctx = this as BookingTestContext; >+ const biblionumber = ctx.biblio.biblio.biblio_id; >+ const patron = ctx.patron.patron; >+ const pickupLibrary = ctx.biblio.libraries[0]; >+ const item = ctx.biblio.items[0]; >+ const initialStart = dayjs().add(5, "day").startOf("day"); >+ const initialEnd = initialStart.add(1, "day"); >+ const updatedStart = initialStart.add(3, "day"); >+ const updatedEnd = updatedStart.add(2, "day"); >+ >+ cy.task("apiPost", { >+ endpoint: "/api/v1/bookings", >+ body: { >+ biblio_id: biblionumber, >+ patron_id: patron.patron_id, >+ pickup_library_id: pickupLibrary.library_id, >+ item_id: item.item_id, >+ start_date: initialStart.toISOString(), >+ end_date: initialEnd.toISOString(), >+ }, >+ }).then(booking => { >+ ctx.bookingsToCleanup.push(booking); >+ cy.wrap(booking).as("existingBookingRecord"); >+ }); >+ >+ cy.visit(`/cgi-bin/koha/bookings/list.pl?biblionumber=${biblionumber}`); >+ cy.get("#bookings_table").should("exist"); >+ prepareBookingModalPage(); >+ >+ cy.get("@existingBookingRecord").then((booking: any) => { >+ interceptBookingModalData(biblionumber); >+ cy.intercept("GET", /\/api\/v1\/patrons\?patron_id=.*/).as( >+ "prefillPatron" >+ ); >+ cy.intercept("GET", /\/api\/v1\/circulation_rules.*/).as( >+ "circulationRules" >+ ); >+ cy.intercept("PUT", `/api/v1/bookings/${booking.booking_id}`).as( >+ "updateBooking" >+ ); >+ >+ cy.contains("#bookings_table tbody tr", `(${booking.booking_id})`, { >+ timeout: 10000, >+ }) >+ .should("exist") >+ .find("button.edit-action") >+ .click(); >+ }); >+ >+ cy.wait(["@bookableItems", "@loadBookings"]); >+ cy.wait("@prefillPatron"); >+ cy.wait("@pickupLocations"); >+ cy.wait("@circulationRules"); >+ cy.get(".modal.show", { timeout: 10000 }).should("be.visible"); >+ >+ cy.get(".modal-title").should("contain", "Edit booking"); >+ cy.get("button[form='form-booking']").should( >+ "contain", >+ "Update booking" >+ ); >+ cy.contains(".vs__selected", patron.cardnumber).should("exist"); >+ cy.contains(".vs__selected", pickupLibrary.name).should("exist"); >+ cy.contains(".vs__selected", item.external_id).should("exist"); >+ >+ cy.get("#booking_period").then($input => { >+ const fp = ($input[0] as any)?._flatpickr; >+ expect(fp?.selectedDates?.length).to.eq(2); >+ expect(dayjs(fp.selectedDates[0]).isSame(initialStart, "day")).to.be >+ .true; >+ }); >+ >+ setDateRange(updatedStart, updatedEnd); >+ cy.wait(1000); >+ >+ cy.get("button[form='form-booking']", { timeout: 30000 }) >+ .should("not.be.disabled") >+ .and("contain", "Update booking") >+ .click(); >+ >+ cy.wait("@updateBooking").its("response.statusCode").should("eq", 200); >+ >+ cy.get("@existingBookingRecord").then((booking: any) => { >+ cy.task("query", { >+ sql: "SELECT start_date, end_date FROM bookings WHERE booking_id=?", >+ values: [booking.booking_id], >+ }).then(rows => { >+ expect(rows).to.have.length(1); >+ expect(dayjs(rows[0].start_date).isSame(updatedStart, "day")).to >+ .be.true; >+ expect(dayjs(rows[0].end_date).isSame(updatedEnd, "day")).to.be >+ .true; >+ }); >+ }); >+ }); >+ >+ it("Resets the modal state after canceling", function (this: BookingTestContext) { >+ const ctx = this as BookingTestContext; >+ const biblionumber = ctx.biblio.biblio.biblio_id; >+ const patron = ctx.patron.patron; >+ const pickupLibrary = ctx.biblio.libraries[0]; >+ const item = ctx.biblio.items[0]; >+ const startDate = dayjs().add(2, "day").startOf("day"); >+ const endDate = startDate.add(1, "day"); >+ >+ cy.visit(`/cgi-bin/koha/bookings/list.pl?biblionumber=${biblionumber}`); >+ cy.get("#bookings_table").should("exist"); >+ prepareBookingModalPage(); >+ >+ interceptBookingModalData(biblionumber); >+ >+ openBookingModalFromList(biblionumber); >+ cy.get("button[form='form-booking']").should("be.disabled"); >+ cy.get("#pickup_library_id").should("have.attr", "disabled"); >+ >+ selectPatronAndInventory(patron, pickupLibrary, item); >+ setDateRange(startDate, endDate); >+ cy.wait(1000); >+ >+ cy.get("button[form='form-booking']", { timeout: 15000 }).should( >+ "not.be.disabled" >+ ); >+ >+ cy.contains(".modal.show button", /Cancel/i).click(); >+ cy.get(".modal.show").should("not.exist"); >+ cy.get("body").should("not.have.class", "modal-open"); >+ >+ openBookingModalFromList(biblionumber); >+ cy.get(".modal.show").within(() => { >+ cy.get("#booking_patron").should("have.value", ""); >+ cy.get("#pickup_library_id").should("have.attr", "disabled"); >+ cy.get("#booking_period").should("have.value", ""); >+ cy.get("button[form='form-booking']").should("be.disabled"); >+ }); >+ }); >+ >+ it("Shows capacity warning for zero-day circulation rules", function (this: BookingTestContext) { >+ const ctx = this as BookingTestContext; >+ const biblionumber = ctx.biblio.biblio.biblio_id; >+ const patron = ctx.patron.patron; >+ const pickupLibrary = ctx.biblio.libraries[0]; >+ const item = ctx.biblio.items[0]; >+ >+ cy.visit(`/cgi-bin/koha/bookings/list.pl?biblionumber=${biblionumber}`); >+ cy.get("#bookings_table").should("exist"); >+ prepareBookingModalPage(); >+ >+ interceptBookingModalData(biblionumber, { >+ circulationRules: req => { >+ req.reply({ >+ statusCode: 200, >+ body: [ >+ { >+ library_id: pickupLibrary.library_id, >+ item_type_id: item.item_type_id, >+ patron_category_id: patron.category_id, >+ issuelength: 0, >+ renewalsallowed: 0, >+ renewalperiod: 0, >+ bookings_lead_period: 0, >+ bookings_trail_period: 0, >+ calculated_period_days: 0, >+ }, >+ ], >+ }); >+ }, >+ }); >+ >+ openBookingModalFromList(biblionumber); >+ selectPatronAndInventory(patron, pickupLibrary, item); >+ >+ cy.get(".modal.show .alert-warning") >+ .scrollIntoView() >+ .should("be.visible") >+ .and("contain", "Bookings are not permitted for this combination"); >+ cy.get("#booking_period").should("be.disabled"); >+ cy.get("button[form='form-booking']").should("be.disabled"); >+ }); >+ >+ it("Creates a booking without selecting a specific item", function (this: BookingTestContext) { >+ const ctx = this as BookingTestContext; >+ const biblionumber = ctx.biblio.biblio.biblio_id; >+ const patron = ctx.patron.patron; >+ const pickupLibrary = ctx.biblio.libraries[0]; >+ const startDate = dayjs().add(3, "day").startOf("day"); >+ const endDate = startDate.add(2, "day"); >+ >+ cy.visit(`/cgi-bin/koha/bookings/list.pl?biblionumber=${biblionumber}`); >+ cy.get("#bookings_table").should("exist"); >+ prepareBookingModalPage(); >+ >+ interceptBookingModalData(biblionumber); >+ cy.intercept("POST", "/api/v1/bookings").as("createBooking"); >+ >+ openBookingModalFromList(biblionumber); >+ >+ selectPatronAndInventory(patron, pickupLibrary, null, { >+ skipItemSelection: true, >+ }); >+ cy.get("#booking_period").should("not.be.disabled"); >+ setDateRange(startDate, endDate); >+ cy.wait(1000); >+ >+ cy.get("button[form='form-booking']", { timeout: 15000 }) >+ .should("not.be.disabled") >+ .contains("Place booking") >+ .click(); >+ >+ cy.wait("@createBooking").then(({ request, response }) => { >+ expect(response?.statusCode).to.eq(201); >+ expect(request?.body.item_id).to.be.null; >+ expect(response?.body.item_id).to.not.be.null; >+ if (response?.body) { >+ ctx.bookingsToCleanup.push(response.body); >+ } >+ }); >+ >+ cy.get("body").should("not.have.class", "modal-open"); >+ }); >+ >+ it("Shows error message when booking creation fails with 400", function (this: BookingTestContext) { >+ const ctx = this as BookingTestContext; >+ const biblionumber = ctx.biblio.biblio.biblio_id; >+ const patron = ctx.patron.patron; >+ const pickupLibrary = ctx.biblio.libraries[0]; >+ const item = ctx.biblio.items[0]; >+ const startDate = dayjs().add(3, "day").startOf("day"); >+ const endDate = startDate.add(2, "day"); >+ >+ cy.visit(`/cgi-bin/koha/bookings/list.pl?biblionumber=${biblionumber}`); >+ cy.get("#bookings_table").should("exist"); >+ prepareBookingModalPage(); >+ >+ interceptBookingModalData(biblionumber); >+ cy.intercept("POST", "/api/v1/bookings", { >+ statusCode: 400, >+ body: { >+ error: "Invalid booking period", >+ }, >+ }).as("createBooking"); >+ >+ openBookingModalFromList(biblionumber); >+ selectPatronAndInventory(patron, pickupLibrary, item); >+ setDateRange(startDate, endDate); >+ cy.wait(1000); >+ >+ cy.get("button[form='form-booking']", { timeout: 15000 }) >+ .should("not.be.disabled") >+ .click(); >+ >+ cy.wait("@createBooking"); >+ cy.get(".modal.show .alert-danger, .modal.show .alert-warning") >+ .scrollIntoView() >+ .should("be.visible"); >+ cy.get(".modal.show").should("exist"); >+ }); >+ >+ it("Shows error message when booking creation fails with 409 conflict", function (this: BookingTestContext) { >+ const ctx = this as BookingTestContext; >+ const biblionumber = ctx.biblio.biblio.biblio_id; >+ const patron = ctx.patron.patron; >+ const pickupLibrary = ctx.biblio.libraries[0]; >+ const item = ctx.biblio.items[0]; >+ const startDate = dayjs().add(3, "day").startOf("day"); >+ const endDate = startDate.add(2, "day"); >+ >+ cy.visit(`/cgi-bin/koha/bookings/list.pl?biblionumber=${biblionumber}`); >+ cy.get("#bookings_table").should("exist"); >+ prepareBookingModalPage(); >+ >+ interceptBookingModalData(biblionumber); >+ cy.intercept("POST", "/api/v1/bookings", { >+ statusCode: 409, >+ body: { >+ error: "Booking conflict detected", >+ }, >+ }).as("createBooking"); >+ >+ openBookingModalFromList(biblionumber); >+ selectPatronAndInventory(patron, pickupLibrary, item); >+ setDateRange(startDate, endDate); >+ cy.wait(1000); >+ >+ cy.get("button[form='form-booking']", { timeout: 15000 }) >+ .should("not.be.disabled") >+ .click(); >+ >+ cy.wait("@createBooking"); >+ cy.get(".modal.show .alert-danger, .modal.show .alert-warning") >+ .scrollIntoView() >+ .should("be.visible"); >+ cy.get(".modal.show").should("exist"); >+ }); >+ >+ it("Shows error message when booking creation fails with 500", function (this: BookingTestContext) { >+ const ctx = this as BookingTestContext; >+ const biblionumber = ctx.biblio.biblio.biblio_id; >+ const patron = ctx.patron.patron; >+ const pickupLibrary = ctx.biblio.libraries[0]; >+ const item = ctx.biblio.items[0]; >+ const startDate = dayjs().add(3, "day").startOf("day"); >+ const endDate = startDate.add(2, "day"); >+ >+ cy.visit(`/cgi-bin/koha/bookings/list.pl?biblionumber=${biblionumber}`); >+ cy.get("#bookings_table").should("exist"); >+ prepareBookingModalPage(); >+ >+ interceptBookingModalData(biblionumber); >+ cy.intercept("POST", "/api/v1/bookings", { >+ statusCode: 500, >+ body: { >+ error: "Internal server error", >+ }, >+ }).as("createBooking"); >+ >+ openBookingModalFromList(biblionumber); >+ selectPatronAndInventory(patron, pickupLibrary, item); >+ setDateRange(startDate, endDate); >+ cy.wait(1000); >+ >+ cy.get("button[form='form-booking']", { timeout: 15000 }) >+ .should("not.be.disabled") >+ .click(); >+ >+ cy.wait("@createBooking"); >+ cy.get(".modal.show .alert-danger, .modal.show .alert-warning") >+ .scrollIntoView() >+ .should("be.visible"); >+ cy.get(".modal.show").should("exist"); >+ }); >+ >+ it("Shows message when patron search returns no results", function (this: BookingTestContext) { >+ const ctx = this as BookingTestContext; >+ const biblionumber = ctx.biblio.biblio.biblio_id; >+ >+ cy.visit(`/cgi-bin/koha/bookings/list.pl?biblionumber=${biblionumber}`); >+ cy.get("#bookings_table").should("exist"); >+ prepareBookingModalPage(); >+ >+ interceptBookingModalData(biblionumber, { >+ searchPatrons: { >+ statusCode: 200, >+ body: [], >+ headers: { >+ "X-Base-Total-Count": "0", >+ "X-Total-Count": "0", >+ }, >+ }, >+ }); >+ >+ openBookingModalFromList(biblionumber); >+ >+ cy.get(".modal.show #booking_patron") >+ .clear() >+ .type("NONEXISTENT", { delay: 0 }); >+ cy.wait("@searchPatrons"); >+ cy.wait(500); >+ >+ cy.get(".modal.show .vs__no-options").should("be.visible"); >+ }); >+ >+ it("Shows no options when no pickup locations are available", function (this: BookingTestContext) { >+ const ctx = this as BookingTestContext; >+ const biblionumber = ctx.biblio.biblio.biblio_id; >+ const patron = ctx.patron.patron; >+ >+ cy.visit(`/cgi-bin/koha/bookings/list.pl?biblionumber=${biblionumber}`); >+ cy.get("#bookings_table").should("exist"); >+ prepareBookingModalPage(); >+ >+ interceptBookingModalData(biblionumber, { >+ pickupLocations: { >+ statusCode: 200, >+ body: [], >+ headers: { >+ "X-Base-Total-Count": "0", >+ "X-Total-Count": "0", >+ }, >+ }, >+ }); >+ >+ openBookingModalFromList(biblionumber); >+ >+ const searchTerm = patron.cardnumber.slice(0, 4); >+ cy.get(".modal.show #booking_patron") >+ .clear() >+ .type(searchTerm, { delay: 0 }); >+ cy.wait("@searchPatrons"); >+ cy.wait(200); >+ selectVsOption(patron.cardnumber); >+ >+ cy.wait("@pickupLocations"); >+ cy.wait(500); >+ >+ cy.get(".modal.show #pickup_library_id").click({ force: true }); >+ cy.wait(200); >+ cy.get(".modal.show .vs__no-options").should("be.visible"); >+ }); >+ >+ it("Shows conflicting dates when an item is already booked", function (this: BookingTestContext) { >+ const ctx = this as BookingTestContext; >+ const biblionumber = ctx.biblio.biblio.biblio_id; >+ const patron = ctx.patron.patron; >+ const pickupLibrary = ctx.biblio.libraries[0]; >+ const item = ctx.biblio.items[0]; >+ const conflictStart = dayjs().add(5, "day").startOf("day"); >+ const conflictEnd = conflictStart.add(2, "day"); >+ >+ cy.task("apiPost", { >+ endpoint: "/api/v1/bookings", >+ body: { >+ biblio_id: biblionumber, >+ patron_id: patron.patron_id, >+ pickup_library_id: pickupLibrary.library_id, >+ item_id: item.item_id, >+ start_date: conflictStart.toISOString(), >+ end_date: conflictEnd.toISOString(), >+ }, >+ }).then(booking => { >+ ctx.bookingsToCleanup.push(booking); >+ }); >+ >+ cy.visit(`/cgi-bin/koha/bookings/list.pl?biblionumber=${biblionumber}`); >+ cy.get("#bookings_table").should("exist"); >+ prepareBookingModalPage(); >+ >+ interceptBookingModalData(biblionumber); >+ >+ openBookingModalFromList(biblionumber); >+ selectPatronAndInventory(patron, pickupLibrary, item); >+ >+ cy.get(".modal.show #booking_period").click({ force: true }); >+ cy.get(".flatpickr-calendar", { timeout: 5000 }).should("be.visible"); >+ >+ const conflictDateStr = conflictStart.format("MMMM D, YYYY"); >+ cy.get(".flatpickr-calendar") >+ .find(`.flatpickr-day[aria-label="${conflictDateStr}"]`) >+ .should("have.class", "flatpickr-disabled"); >+ }); >+ >+ it("Creates a booking when item type is auto-selected", function (this: BookingTestContext) { >+ const ctx = this as BookingTestContext; >+ const biblionumber = ctx.biblio.biblio.biblio_id; >+ const patron = ctx.patron.patron; >+ const pickupLibrary = ctx.biblio.libraries[0]; >+ const startDate = dayjs().add(3, "day").startOf("day"); >+ const endDate = startDate.add(2, "day"); >+ >+ cy.visit(`/cgi-bin/koha/bookings/list.pl?biblionumber=${biblionumber}`); >+ cy.get("#bookings_table").should("exist"); >+ prepareBookingModalPage(); >+ >+ interceptBookingModalData(biblionumber); >+ cy.intercept("POST", "/api/v1/bookings").as("createBooking"); >+ >+ openBookingModalFromList(biblionumber); >+ >+ const searchTerm = patron.cardnumber.slice(0, 4); >+ cy.get(".modal.show #booking_patron") >+ .clear() >+ .type(searchTerm, { delay: 0 }); >+ cy.wait("@searchPatrons"); >+ cy.wait(200); >+ selectVsOption(patron.cardnumber); >+ >+ cy.wait("@pickupLocations"); >+ cy.get(".modal.show #pickup_library_id").should( >+ "not.have.attr", >+ "disabled" >+ ); >+ cy.get(".modal.show #pickup_library_id").click({ force: true }); >+ cy.wait(200); >+ selectVsOption(pickupLibrary.name); >+ >+ cy.wait("@circulationRules"); >+ cy.wait(500); >+ >+ cy.get("#booking_period").should("not.be.disabled"); >+ setDateRange(startDate, endDate); >+ cy.wait(1000); >+ >+ cy.get("button[form='form-booking']", { timeout: 15000 }) >+ .should("not.be.disabled") >+ .contains("Place booking") >+ .click(); >+ >+ cy.wait("@createBooking").then(({ request, response }) => { >+ expect(response?.statusCode).to.eq(201); >+ expect(request?.body.item_id).to.be.null; >+ expect(response?.body.item_id).to.not.be.null; >+ if (response?.body) { >+ ctx.bookingsToCleanup.push(response.body); >+ } >+ }); >+ >+ cy.get("body").should("not.have.class", "modal-open"); >+ }); >+}); >-- >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 41129
:
188546
|
188551
|
189348
| 189350