/** @odoo-module **/ import { addBusServicesToRegistry } from "@bus/../tests/helpers/test_utils"; import { mountRoomBookingView } from "@room/../tests/room_booking_tests_utils"; import { click, editInput, mockTimeout, nextTick, patchDate, triggerEvent, } from "@web/../tests/helpers/utils"; import { makeFakeDialogService, makeFakeNotificationService, } from "@web/../tests/helpers/mock_services"; import { registry } from "@web/core/registry"; /** * Assert that the current time displayed on the view is the expected one */ const assertDisplayedTime = (assert, target, expectedTime) => { assert.containsOnce( target, `.o_room_top:contains('${luxon.DateTime.fromSQL(expectedTime).toFormat("TDDDD")}')`, ); }; /** * Assert that the room status is the expected one (right background color, right "busy" or "free" * status (icon), expected remaining time if there is an ongoing booking, and correct number of * bookings in the sidebar) * @param {Object} assert * @param {HTMLElement} target * @param {Object|boolean} remainingTime: remaining time of the current booking or false if no booking * @param {number} nbBookings: number of bookings in the sidebar */ const assertRoomStatus = (assert, target, remainingTime, nbBookings) => { if (remainingTime) { assert.strictEqual( target.querySelector(".o_room_remaining_time").innerText, luxon.Duration.fromObject(remainingTime).toFormat("hh:mm:ss"), ); // Check that the room uses the busy background color as there is an ongoing booking assert.hasAttrValue( target.querySelector(".o_room_booking_main > div"), "style", "background-image: linear-gradient(#FF0000DD, #FF0000DD)", ); // Check that the "Booked" icon is shown assert.containsOnce(target, "i.fa-calendar-times-o.fa-3x"); } else { assert.containsNone(target, ".o_room_remaining_time"); // Check that the room uses the available background color as there is no ongoing booking assert.hasAttrValue( target.querySelector(".o_room_booking_main > div"), "style", "background-image: linear-gradient(#00FF00DD, #00FF00DD)", ); // Check that the "available" icon is shown assert.containsOnce(target, "i.fa-check-circle.fa-3x"); } assert.containsN(target, ".o_room_sidebar .list-group-item", nbBookings); }; /** * Click on the quick book button and wait for the next tick. * @param {HTMLElement} target * @returns {Promise} */ const clickQuickBook = async (target) => { click(target, ".btn-dark i.fa-rocket"); await nextTick(); }; QUnit.module("Room Booking View", (hooks) => { hooks.beforeEach(async () => { addBusServicesToRegistry(); registry.category("services").add("dialog", makeFakeDialogService()); registry.category("services").add("notification", makeFakeNotificationService()); }); // Test UI when there is no meeting scheduled QUnit.test("Room Booking View - no meeting scheduled", async (assert) => { const mockRPC = (route, args) => { if (route === "/room/room_test/get_existing_bookings") { return []; } }; patchDate(2023, 5, 17, 13, 0, 0); const { target } = await mountRoomBookingView(mockRPC); assertDisplayedTime(assert, target, "2023-06-17 13:00:00"); assertRoomStatus(assert, target, false, 0); // Check that room description is shown and formatted assert.containsOnce(target, ".o_room_sidebar p.o_test_description.text-danger"); }); // Test UI when there is an ongoing meeting QUnit.test("Room Booking View - ongoing meeting", async (assert) => { const mockRPC = (route, args) => { if (route === "/room/room_test/get_existing_bookings") { return [ { id: 1, name: "Booking 1", start_datetime: "2023-06-17 10:00:00", stop_datetime: "2023-06-17 11:00:00", }, ]; } }; patchDate(2023, 5, 17, 11, 15, 0); const { target } = await mountRoomBookingView(mockRPC); assertDisplayedTime(assert, target, "2023-06-17 11:15:00"); assertRoomStatus(assert, target, { minutes: 44, seconds: 59 }, 1); }); // Test quick booking flow QUnit.test("Room Booking View - Quick Booking", async (assert) => { assert.expect(7); let expectedCreateArgs; const buttonsSelector = "input[placeholder='Booking Name'] + div button"; const mockRPC = (route, args) => { if (route === "/room/room_test/get_existing_bookings") { return [ { id: 1, name: "Booking 1", start_datetime: "2023-06-17 10:00:00", stop_datetime: "2023-06-17 11:00:00", }, ]; } else if (route === "/room/room_test/booking/create") { assert.deepEqual(args, expectedCreateArgs); return true; } }; patchDate(2023, 5, 17, 9, 59, 0); const { target } = await mountRoomBookingView(mockRPC); await clickQuickBook(target); await editInput(target, "input[placeholder='Booking Name']", "Meeting"); // Next booking starts in more than 1h, show the 3 quick booking buttons assert.containsN(target, buttonsSelector, 3); // Last button should book the room for 1h expectedCreateArgs = { name: "Meeting", start_datetime: "2023-06-17 08:59:00", stop_datetime: "2023-06-17 09:59:00", }; click(target, buttonsSelector + ":last-child"); await nextTick(); patchDate(2023, 5, 17, 10, 25, 0); await clickQuickBook(target); // Next booking starts in less than 1h but more than 30 min, show 2 buttons assert.containsN(target, buttonsSelector, 2); // Last button should book the room for 30 min, and name should have been reset to default expectedCreateArgs = { name: "Public Booking", start_datetime: "2023-06-17 09:25:00", stop_datetime: "2023-06-17 09:55:00", }; click(target, buttonsSelector + ":last-child"); await nextTick(); patchDate(2023, 5, 17, 10, 43, 0); await clickQuickBook(target); // Next booking starts in less than 30 min but more than 15 min, show 1 button assert.containsOnce(target, buttonsSelector); // Only button should book the room for 15 min expectedCreateArgs = { name: "Public Booking", start_datetime: "2023-06-17 09:43:00", stop_datetime: "2023-06-17 09:58:00", }; click(target, buttonsSelector); await nextTick(); patchDate(2023, 5, 17, 10, 59, 0); await clickQuickBook(target); // Next booking starts in less than 15 min, show no button assert.containsNone(target, buttonsSelector); }); // Check that the UI adapts correctly when going from no ongoing booking to ongoing booking QUnit.test("Room Booking View - Booking Started", async (assert) => { const mockRPC = (route, args) => { if (route === "/room/room_test/get_existing_bookings") { return [ { id: 1, name: "Booking 1", start_datetime: "2023-06-17 10:00:00", stop_datetime: "2023-06-17 11:00:00", }, ]; } }; patchDate(2023, 5, 17, 10, 59, 0); const { execIntervals, target } = await mountRoomBookingView(mockRPC); assertRoomStatus(assert, target, false, 1); patchDate(2023, 5, 17, 11, 0, 0); await nextTick(); execIntervals(); await nextTick(); assertRoomStatus(assert, target, { minutes: 59, seconds: 59 }, 1); // Make sure there is no error with the following intervals execIntervals(); await nextTick(); }); // Check that the UI adapts correctly when going from ongoing booking to no ongoing booking QUnit.test("Room Booking View - Booking Ended", async (assert) => { const mockRPC = (route, args) => { if (route === "/room/room_test/get_existing_bookings") { return [ { id: 1, name: "Booking 1", start_datetime: "2023-06-17 09:00:00", stop_datetime: "2023-06-17 10:00:00", }, ]; } }; patchDate(2023, 5, 17, 10, 59, 59); const { execIntervals, target } = await mountRoomBookingView(mockRPC); assertRoomStatus(assert, target, { seconds: 0 }, 1); patchDate(2023, 5, 17, 11, 0, 0); await nextTick(); execIntervals(); await nextTick(); assertRoomStatus(assert, target, false, 0); // Make sure there is no error with the following intervals execIntervals(); await nextTick(); }); // Check that the UI adapts correctly when the date changes QUnit.test("Room Booking View - Day Change", async (assert) => { const mockRPC = (route, args) => { if (route === "/room/room_test/get_existing_bookings") { return [ { id: 1, name: "Booking 1", start_datetime: "2023-06-18 09:00:00", stop_datetime: "2023-06-18 10:00:00", }, ]; } }; patchDate(2023, 5, 17, 23, 59, 59); const { execIntervals, target } = await mountRoomBookingView(mockRPC); // Today should be shown even if there is no booking planned today assert.strictEqual( target.querySelector(".o_room_sidebar h6:first-of-type").innerText, "Today", ); // Since today is shown and since there is a booking planned tomorrow, // there should be 2 dates shown in the sidebar assert.containsN(target, ".o_room_sidebar h6", 2); patchDate(2023, 5, 18, 0, 0, 0); await nextTick(); execIntervals(); await nextTick(); assert.strictEqual( target.querySelector(".o_room_sidebar h6:first-of-type").innerText, "Today", ); // Only date shown in the sidebar is today assert.containsOnce(target, ".o_room_sidebar h6", 1); }); // Check that after some inactivity, the main view displaying the room status is shown // (see INACTIVITY_TIMEOUT @room/static/src/js/views/room_booking_view.js) QUnit.test("Room Booking View - Inactivity reset", async (assert) => { const mockRPC = (route, args) => { if (route === "/room/room_test/get_existing_bookings") { return []; } }; const inactivity_timeout = 120000; const { advanceTime } = mockTimeout(); patchDate(2023, 5, 17, 10, 0, 0); const { target } = await mountRoomBookingView(mockRPC); await clickQuickBook(target); await advanceTime(inactivity_timeout); await clickQuickBook(target); await advanceTime(inactivity_timeout - 1); // Writing the title should reset the timeout triggerEvent(target, "", "keydown", { key: "s" }); await advanceTime(inactivity_timeout - 1); assert.containsNone(target, ".fa-check-circle.fa-3x"); // Clicking anywhere should reset the timeout click(target, ".o_room_booking_main"); await advanceTime(inactivity_timeout - 1); assert.containsNone(target, ".fa-check-circle.fa-3x"); // Make sure timeout still occurs await advanceTime(1); assert.containsOnce(target, "i.fa-check-circle.fa-3x"); }); // Check that the UI adapts correctly when a booking ends but is followed immediately by another QUnit.test("Room Booking View - Consecutive Bookings", async (assert) => { const mockRPC = (route, args) => { if (route === "/room/room_test/get_existing_bookings") { return [ { id: 1, name: "Booking 1", start_datetime: "2023-06-17 09:00:00", stop_datetime: "2023-06-17 10:00:00", }, { id: 2, name: "Booking 2", start_datetime: "2023-06-17 10:00:00", stop_datetime: "2023-06-17 11:00:00", }, ]; } }; patchDate(2023, 5, 17, 10, 59, 59); const { execIntervals, target } = await mountRoomBookingView(mockRPC); assertRoomStatus(assert, target, { seconds: 0 }, 2); patchDate(2023, 5, 17, 11, 0, 0); await nextTick(); execIntervals(); await nextTick(); assertRoomStatus(assert, target, { minutes: 59, seconds: 59 }, 1); // Make sure next interval does not remove current booking execIntervals(); await nextTick(); assertRoomStatus(assert, target, { minutes: 59, seconds: 59 }, 1); }); // Check that the UI adapts correctly when a new booking notification is received QUnit.test("Room Booking View - Receiving new booking through bus", async (assert) => { const mockRPC = (route, args) => { if (route === "/room/room_test/get_existing_bookings") { return [ { id: 1, name: "Booking 1", start_datetime: "2023-06-17 09:00:00", stop_datetime: "2023-06-17 10:00:00", }, { id: 2, name: "Booking 2", start_datetime: "2023-06-17 13:00:00", stop_datetime: "2023-06-17 14:00:00", }, ]; } }; patchDate(2023, 5, 17, 8, 30, 0); const { notifyView, target } = await mountRoomBookingView(mockRPC, true); assertRoomStatus(assert, target, false, 2); // Send a new booking notification (for later today) await notifyView("booking/create", [ { id: 3, name: "Booking 3", start_datetime: "2023-06-17 12:00:00", stop_datetime: "2023-06-17 12:30:00", }, ]); assertRoomStatus(assert, target, false, 3); // Check that the booking is at the right place in the sidebar assert.containsOnce( target, ".o_room_sidebar .list-group-item:nth-child(2):contains('Booking 3')", ); // Send a new booking notification (for now) await notifyView("booking/create", [ { id: 4, name: "Booking 4", start_datetime: "2023-06-17 07:30:00", stop_datetime: "2023-06-17 08:00:00", }, ]); assert.containsOnce( target, ".o_room_sidebar .list-group-item:first-child:contains('Booking 4')", ); assertRoomStatus(assert, target, { minutes: 29, seconds: 59 }, 4); // Send a new booking notification (for in the past - can be done from backend) await notifyView("booking/create", [ { id: 5, name: "Booking 5", start_datetime: "2023-06-17 06:00:00", stop_datetime: "2023-06-17 06:30:00", }, ]); assert.containsNone(target, ".o_room_sidebar .list-group-item:contains('Booking 5')"); }); // Check that the UI adapts correctly when a notification of a booking update is received QUnit.test("Room Booking View - Receiving booking update through bus", async (assert) => { const mockRPC = (route, args) => { if (route === "/room/room_test/get_existing_bookings") { return [ { id: 1, name: "Booking 1", start_datetime: "2023-06-17 09:00:00", stop_datetime: "2023-06-17 10:00:00", }, ]; } }; patchDate(2023, 5, 17, 10, 30, 0); const { execIntervals, notifyView, target } = await mountRoomBookingView(mockRPC, true); assertRoomStatus(assert, target, { minutes: 29, seconds: 59 }, 1); // Send notification to reschedule booking later and change its name await notifyView("booking/update", [ { id: 1, name: "Booking 1 rescheduled", start_datetime: "2023-06-17 10:30:00", stop_datetime: "2023-06-17 11:00:00", }, ]); assertRoomStatus(assert, target, false, 1); assert.containsOnce( target, ".o_room_sidebar .list-group-item:contains('Booking 1 rescheduled')", ); // Send notification to reschedule now a booking that already ended (i.e. we update // a booking that is not in the list of bookings of the view) await notifyView("booking/update", [ { id: 2, name: "Ended to Current", start_datetime: "2023-06-17 09:00:00", stop_datetime: "2023-06-17 10:00:00", }, ]); assert.containsOnce( target, ".o_room_sidebar .list-group-item:contains('Ended to Current')", ); // Check remaining time is correct to make sure it will be updated at the next step assertRoomStatus(assert, target, { minutes: 29, seconds: 59 }, 2); // Send a notification to extend the current meeting await notifyView("booking/update", [ { id: 2, name: "Ended to Current", start_datetime: "2023-06-17 09:00:00", stop_datetime: "2023-06-17 10:30:00", }, ]); // There should still be 2 meetings assertRoomStatus(assert, target, { minutes: 59, seconds: 59 }, 2); // Send notification to reschedule a meeting in the past (it makes no sense but it should // not crash the view) await notifyView("booking/update", [ { id: 1, name: "Booking 1 in the past", start_datetime: "2023-06-17 08:00:00", stop_datetime: "2023-06-17 08:30:00", }, ]); await execIntervals(); await nextTick(); assertRoomStatus(assert, target, { minutes: 59, seconds: 59 }, 1); }); // Check that the UI adapts correctly when a notification of a booking deletion is received QUnit.test("Room Booking View - Receiving booking deletion through bus", async (assert) => { const mockRPC = (route, args) => { if (route === "/room/room_test/get_existing_bookings") { return [ { id: 1, name: "Booking 1", start_datetime: "2023-06-17 10:00:00", stop_datetime: "2023-06-17 11:00:00", }, { id: 2, name: "Booking 2", start_datetime: "2023-06-17 11:00:00", stop_datetime: "2023-06-17 12:00:00", }, ]; } }; patchDate(2023, 5, 17, 11, 15, 0); const { notifyView, target } = await mountRoomBookingView(mockRPC, true); await nextTick(); assertRoomStatus(assert, target, { minutes: 44, seconds: 59 }, 2); // send notification to delete the current booking await notifyView("booking/delete", [{ id: 1 }]); assertRoomStatus(assert, target, false, 1); assert.containsOnce(target, ".o_room_sidebar .list-group-item:contains('Booking 2')"); // send notification to delete the remaining booking await notifyView("booking/delete", [{ id: 2 }]); assertRoomStatus(assert, target, false, 0); }); // Check that the UI behaves correctly when a booking spans several days (can be done from // the backend) QUnit.test("Room Booking View - Booking spanning several days", async (assert) => { const mockRPC = (route, args) => { if (route === "/room/room_test/get_existing_bookings") { return [ { id: 1, name: "Booking 1", start_datetime: "2023-06-17 10:00:00", stop_datetime: "2023-06-19 10:00:00", }, ]; } }; patchDate(2023, 5, 18, 10, 0, 0); const { execIntervals, target } = await mountRoomBookingView(mockRPC); // Even if the booking started yesterday, it should be in the sidebar assertDisplayedTime(assert, target, "2023-06-18 10:00:00"); assertRoomStatus(assert, target, { hours: 24, minutes: 59, seconds: 59 }, 1); patchDate(2023, 5, 19, 10, 0, 0); await nextTick(); execIntervals(); await nextTick(); assertDisplayedTime(assert, target, "2023-06-19 10:00:00"); // Booking should still be in the sidebar assertRoomStatus(assert, target, { minutes: 59, seconds: 59 }, 1); patchDate(2023, 5, 19, 12, 0, 0); await nextTick(); execIntervals(); await nextTick(); assertDisplayedTime(assert, target, "2023-06-19 12:00:00"); // It shouldn't be in the sidebar anymore assertRoomStatus(assert, target, false, 0); }); // If multiple tabs are opened, some undesired behaviors may happen. Therefore, we // check that a warning message is shown when it is the case. QUnit.test("Room Booking View - Warn if multiple tabs", async (assert) => { const mockRPC = (route, args) => { if (route === "/room/room_test/get_existing_bookings") { return []; } }; let { target } = await mountRoomBookingView(mockRPC); assert.containsNone(target, ".alert-warning:contains('multiple tabs opened')"); ({ target } = await mountRoomBookingView(mockRPC)); assert.containsOnce(target, ".alert-warning:contains('multiple tabs opened')"); }); });