diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/point_of_sale/static/tests/tours | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/point_of_sale/static/tests/tours')
21 files changed, 2361 insertions, 0 deletions
diff --git a/addons/point_of_sale/static/tests/tours/Chrome.tour.js b/addons/point_of_sale/static/tests/tours/Chrome.tour.js new file mode 100644 index 00000000..a1c992de --- /dev/null +++ b/addons/point_of_sale/static/tests/tours/Chrome.tour.js @@ -0,0 +1,103 @@ +odoo.define('point_of_sale.tour.Chrome', function (require) { + 'use strict'; + + const { ProductScreen } = require('point_of_sale.tour.ProductScreenTourMethods'); + const { ReceiptScreen } = require('point_of_sale.tour.ReceiptScreenTourMethods'); + const { PaymentScreen } = require('point_of_sale.tour.PaymentScreenTourMethods'); + const { TicketScreen } = require('point_of_sale.tour.TicketScreenTourMethods'); + const { Chrome } = require('point_of_sale.tour.ChromeTourMethods'); + const { getSteps, startSteps } = require('point_of_sale.tour.utils'); + var Tour = require('web_tour.tour'); + + startSteps(); + + // Order 1 is at Product Screen + ProductScreen.do.clickHomeCategory(); + ProductScreen.exec.addOrderline('Desk Pad', '1', '2', '2.0'); + Chrome.do.clickTicketButton(); + TicketScreen.check.checkStatus('-0001', 'Ongoing'); + + // Order 2 is at Payment Screen + TicketScreen.do.clickNewTicket(); + ProductScreen.exec.addOrderline('Monitor Stand', '3', '4', '12.0'); + ProductScreen.do.clickPayButton(); + PaymentScreen.check.isShown(); + Chrome.do.clickTicketButton(); + TicketScreen.check.checkStatus('-0002', 'Payment'); + + // Order 3 is at Receipt Screen + TicketScreen.do.clickNewTicket(); + ProductScreen.exec.addOrderline('Whiteboard Pen', '5', '6', '30.0'); + ProductScreen.do.clickPayButton(); + PaymentScreen.do.clickPaymentMethod('Bank'); + PaymentScreen.check.remainingIs('0.0'); + PaymentScreen.check.validateButtonIsHighlighted(true); + PaymentScreen.do.clickValidate(); + ReceiptScreen.check.isShown(); + Chrome.do.clickTicketButton(); + TicketScreen.check.checkStatus('-0003', 'Receipt'); + + // Select order 1, should be at Product Screen + TicketScreen.do.selectOrder('-0001'); + ProductScreen.check.productIsDisplayed('Desk Pad'); + ProductScreen.check.selectedOrderlineHas('Desk Pad', '1.0', '2.0'); + + // Select order 2, should be at Payment Screen + Chrome.do.clickTicketButton(); + TicketScreen.do.selectOrder('-0002'); + PaymentScreen.check.emptyPaymentlines('12.0'); + PaymentScreen.check.validateButtonIsHighlighted(false); + + // Select order 3, should be at Receipt Screen + Chrome.do.clickTicketButton(); + TicketScreen.do.selectOrder('-0003'); + ReceiptScreen.check.totalAmountContains('30.0'); + + // Pay order 1, with change + Chrome.do.clickTicketButton(); + TicketScreen.do.selectOrder('-0001'); + ProductScreen.do.clickPayButton(); + PaymentScreen.do.clickPaymentMethod('Cash'); + PaymentScreen.do.pressNumpad('2 0'); + PaymentScreen.check.remainingIs('0.0'); + PaymentScreen.check.validateButtonIsHighlighted(true); + PaymentScreen.do.clickValidate(); + ReceiptScreen.check.totalAmountContains('2.0'); + + // Order 1 now should have Receipt status + Chrome.do.clickTicketButton(); + TicketScreen.check.checkStatus('-0001', 'Receipt'); + + // Select order 3, should still be at Receipt Screen + // and the total amount doesn't change. + TicketScreen.do.selectOrder('-0003'); + ReceiptScreen.check.totalAmountContains('30.0'); + + // click next screen on order 3 + // then delete the new empty order + ReceiptScreen.do.clickNextOrder(); + ProductScreen.check.orderIsEmpty(); + Chrome.do.clickTicketButton(); + TicketScreen.do.deleteOrder('-0004'); + TicketScreen.do.deleteOrder('-0001'); + + // After deleting order 1 above, order 2 became + // the 2nd-row order and it has payment status + TicketScreen.check.nthRowContains(2, 'Payment') + TicketScreen.do.deleteOrder('-0002'); + Chrome.do.confirmPopup(); + TicketScreen.do.clickNewTicket(); + + // Invoice an order + ProductScreen.exec.addOrderline('Whiteboard Pen', '5', '6'); + ProductScreen.do.clickCustomerButton(); + ProductScreen.do.clickCustomer('Nicole Ford'); + ProductScreen.do.clickSetCustomer(); + ProductScreen.do.clickPayButton(); + PaymentScreen.do.clickPaymentMethod('Bank'); + PaymentScreen.do.clickInvoiceButton(); + PaymentScreen.do.clickValidate(); + ReceiptScreen.check.isShown(); + + Tour.register('ChromeTour', { test: true, url: '/pos/ui' }, getSteps()); +}); diff --git a/addons/point_of_sale/static/tests/tours/OrderManagementScreen.tour.js b/addons/point_of_sale/static/tests/tours/OrderManagementScreen.tour.js new file mode 100644 index 00000000..cfd6483a --- /dev/null +++ b/addons/point_of_sale/static/tests/tours/OrderManagementScreen.tour.js @@ -0,0 +1,138 @@ +odoo.define('point_of_sale.tour.OrderManagementScreen', function (require) { + 'use strict'; + + const { OrderManagementScreen } = require('point_of_sale.tour.OrderManagementScreenTourMethods'); + const { ProductScreen } = require('point_of_sale.tour.ProductScreenTourMethods'); + const { PaymentScreen } = require('point_of_sale.tour.PaymentScreenTourMethods'); + const { ClientListScreen } = require('point_of_sale.tour.ClientListScreenTourMethods'); + const { TicketScreen } = require('point_of_sale.tour.TicketScreenTourMethods'); + const { Chrome } = require('point_of_sale.tour.ChromeTourMethods'); + const { makeFullOrder } = require('point_of_sale.tour.CompositeTourMethods'); + const { getSteps, startSteps } = require('point_of_sale.tour.utils'); + var Tour = require('web_tour.tour'); + + // signal to start generating steps + // when finished, steps can be taken from getSteps + startSteps(); + + // Go by default to home category + ProductScreen.do.clickHomeCategory(); + + // make one order and check if it can be seen from the management screen. + // order 0001 + makeFullOrder({ orderlist: [['Whiteboard Pen', '5', '6']], payment: ['Cash', '30'] }); + Chrome.do.clickOrderManagementButton(); + OrderManagementScreen.check.isShown(); + OrderManagementScreen.check.orderlistHas({ orderName: '-0001', total: '30' }); + + OrderManagementScreen.do.clickBack(); + + // make multiple orders and check them in the management screen. + // order 0002 + makeFullOrder({ + orderlist: [ + ['Desk Pad', '1', '2'], + ['Monitor Stand', '3', '4'], + ['Whiteboard Pen', '5', '6'], + ], + payment: ['Bank', '44'], + }); + // order 0003 + makeFullOrder({ + orderlist: [ + ['Desk Pad', '1', '2'], + ['Whiteboard Pen', '5', '6'], + ], + customer: 'Colleen Diaz', + payment: ['Cash', '50'], + }); + // order 0004 + makeFullOrder({ + orderlist: [ + ['Monitor Stand', '3', '4'], + ['Whiteboard Pen', '5', '6'], + ], + payment: ['Bank', '42'], + }); + + Chrome.do.clickOrderManagementButton(); + OrderManagementScreen.check.isShown(); + OrderManagementScreen.check.orderlistHas({ orderName: '-0002', total: '44' }); + OrderManagementScreen.check.orderlistHas({ + orderName: '0003', + total: '32', + customer: 'Colleen Diaz', + }); + OrderManagementScreen.check.orderlistHas({ orderName: '-0004', total: '42' }); + + // click the currently active order + OrderManagementScreen.do.clickOrder('-0005'); + ProductScreen.check.isShown(); + + // Add 2 orders, they should appear in order management screen + // order 0006 + Chrome.do.clickTicketButton(); + TicketScreen.do.clickNewTicket(); + ProductScreen.exec.addOrderline('Whiteboard Pen', '66', '6'); + + // order 0007, should be at payment screen + Chrome.do.clickTicketButton(); + TicketScreen.do.clickNewTicket(); + ProductScreen.exec.addOrderline('Monitor Stand', '55', '5'); + ProductScreen.do.clickCustomerButton(); + ClientListScreen.exec.setClient('Azure Interior'); + ProductScreen.do.clickPayButton(); + + Chrome.do.clickOrderManagementButton(); + OrderManagementScreen.check.orderlistHas({ orderName: '-0006', total: '396' }); + OrderManagementScreen.check.orderlistHas({ + orderName: '-0007', + total: '275', + customer: 'Azure Interior', + }); + + // select a paid order, order row should be highlighted and should show order details + OrderManagementScreen.do.clickOrder('-0004'); + OrderManagementScreen.check.highlightedOrderRowHas('-0004'); + OrderManagementScreen.check.orderDetailsHas({ + lines: [ + { product: 'Monitor Stand', quantity: '3' }, + { product: 'Whiteboard Pen', quantity: '5' }, + ], + total: '42', + }); + OrderManagementScreen.do.clickOrder('-0001'); + OrderManagementScreen.check.highlightedOrderRowHas('-0001'); + // 0004 should not be highlighted anymore + OrderManagementScreen.check.orderRowIsNotHighlighted('-0004'); + OrderManagementScreen.check.orderDetailsHas({ + lines: [{ product: 'Whiteboard Pen', quantity: '5' }], + total: '30', + }); + + // Select a paid order then invoice it. The selected order should remain selected + // and will contain a new customer. After invoice, the current customer should be removed. + // TODO: enable the following steps once the issue in invoicing is solved. + // OrderManagementScreen.do.clickInvoiceButton(); + // Chrome.do.confirmPopup(); + // ClientListScreen.check.isShown(); + // ClientListScreen.exec.setClient('Jesse Brown'); + // OrderManagementScreen.check.highlightedOrderRowHas('Jesse Brown'); + + // Check if order 0007 is selected, it should be at payment screen + OrderManagementScreen.do.clickOrder('-0007'); + PaymentScreen.check.isShown(); + + Chrome.do.clickOrderManagementButton(); + OrderManagementScreen.check.isShown(); + OrderManagementScreen.do.clickOrder('-0003'); + OrderManagementScreen.do.clickPrintReceiptButton(); + OrderManagementScreen.check.reprintReceiptIsShown(); + OrderManagementScreen.check.receiptChangeIs('18.0'); + OrderManagementScreen.check.receiptOrderDataContains('-0003'); + OrderManagementScreen.check.receiptAmountIs('32.0'); + OrderManagementScreen.do.closeReceipt(); + OrderManagementScreen.check.isNotHidden(); + + Tour.register('OrderManagementScreenTour', { test: true, url: '/pos/ui' }, getSteps()); +}); diff --git a/addons/point_of_sale/static/tests/tours/PaymentScreen.tour.js b/addons/point_of_sale/static/tests/tours/PaymentScreen.tour.js new file mode 100644 index 00000000..296fbd55 --- /dev/null +++ b/addons/point_of_sale/static/tests/tours/PaymentScreen.tour.js @@ -0,0 +1,70 @@ +odoo.define('point_of_sale.tour.PaymentScreen', function (require) { + 'use strict'; + + const { ProductScreen } = require('point_of_sale.tour.ProductScreenTourMethods'); + const { PaymentScreen } = require('point_of_sale.tour.PaymentScreenTourMethods'); + const { getSteps, startSteps } = require('point_of_sale.tour.utils'); + var Tour = require('web_tour.tour'); + + startSteps(); + + ProductScreen.exec.addOrderline('Letter Tray', '10'); + ProductScreen.check.selectedOrderlineHas('Letter Tray', '10.0'); + ProductScreen.do.clickPayButton(); + PaymentScreen.check.emptyPaymentlines('52.8'); + + PaymentScreen.do.clickPaymentMethod('Cash'); + PaymentScreen.do.pressNumpad('1 1'); + PaymentScreen.check.selectedPaymentlineHas('Cash', '11.00'); + PaymentScreen.check.remainingIs('41.8'); + PaymentScreen.check.changeIs('0.0'); + PaymentScreen.check.validateButtonIsHighlighted(false); + // remove the selected paymentline with multiple backspace presses + PaymentScreen.do.pressNumpad('Backspace Backspace'); + PaymentScreen.check.selectedPaymentlineHas('Cash', '0.00'); + PaymentScreen.do.pressNumpad('Backspace'); + PaymentScreen.check.emptyPaymentlines('52.8'); + + // Pay with bank, the selected line should have full amount + PaymentScreen.do.clickPaymentMethod('Bank'); + PaymentScreen.check.remainingIs('0.0'); + PaymentScreen.check.changeIs('0.0'); + PaymentScreen.check.validateButtonIsHighlighted(true); + // remove the line using the delete button + PaymentScreen.do.clickPaymentlineDelButton('Bank', '52.8'); + + // Use +10 and +50 to increment the amount of the paymentline + PaymentScreen.do.clickPaymentMethod('Cash'); + PaymentScreen.do.pressNumpad('+10'); + PaymentScreen.check.remainingIs('42.8'); + PaymentScreen.check.changeIs('0.0'); + PaymentScreen.check.validateButtonIsHighlighted(false); + PaymentScreen.do.pressNumpad('+50'); + PaymentScreen.check.remainingIs('0.0'); + PaymentScreen.check.changeIs('7.2'); + PaymentScreen.check.validateButtonIsHighlighted(true); + PaymentScreen.do.clickPaymentlineDelButton('Cash', '60.0'); + + // Multiple paymentlines + PaymentScreen.do.clickPaymentMethod('Cash'); + PaymentScreen.do.pressNumpad('1'); + PaymentScreen.check.remainingIs('51.8'); + PaymentScreen.check.changeIs('0.0'); + PaymentScreen.check.validateButtonIsHighlighted(false); + PaymentScreen.do.clickPaymentMethod('Cash'); + PaymentScreen.do.pressNumpad('5'); + PaymentScreen.check.remainingIs('46.8'); + PaymentScreen.check.changeIs('0.0'); + PaymentScreen.check.validateButtonIsHighlighted(false); + PaymentScreen.do.clickPaymentMethod('Bank'); + PaymentScreen.do.pressNumpad('2 0'); + PaymentScreen.check.remainingIs('26.8'); + PaymentScreen.check.changeIs('0.0'); + PaymentScreen.check.validateButtonIsHighlighted(false); + PaymentScreen.do.clickPaymentMethod('Bank'); + PaymentScreen.check.remainingIs('0.0'); + PaymentScreen.check.changeIs('0.0'); + PaymentScreen.check.validateButtonIsHighlighted(true); + + Tour.register('PaymentScreenTour', { test: true, url: '/pos/ui' }, getSteps()); +}); diff --git a/addons/point_of_sale/static/tests/tours/ProductConfigurator.tour.js b/addons/point_of_sale/static/tests/tours/ProductConfigurator.tour.js new file mode 100644 index 00000000..d3acf388 --- /dev/null +++ b/addons/point_of_sale/static/tests/tours/ProductConfigurator.tour.js @@ -0,0 +1,66 @@ +odoo.define('point_of_sale.tour.ProductConfigurator', function (require) { + 'use strict'; + + const { ProductScreen } = require('point_of_sale.tour.ProductScreenTourMethods'); + const { ProductConfigurator } = require('point_of_sale.tour.ProductConfiguratorTourMethods'); + const { getSteps, startSteps } = require('point_of_sale.tour.utils'); + var Tour = require('web_tour.tour'); + + // signal to start generating steps + // when finished, steps can be taken from getSteps + startSteps(); + + // Go by default to home category + ProductScreen.do.clickHomeCategory(); + + // Click on Configurable Chair product + ProductScreen.do.clickDisplayedProduct('Configurable Chair'); + ProductConfigurator.check.isShown(); + + // Cancel configuration, not product should be in order + ProductConfigurator.do.cancelAttributes(); + ProductScreen.check.orderIsEmpty(); + + // Click on Configurable Chair product + ProductScreen.do.clickDisplayedProduct('Configurable Chair'); + ProductConfigurator.check.isShown(); + + // Pick Color + ProductConfigurator.do.pickColor('Red'); + + // Pick Radio + ProductConfigurator.do.pickSelect('Metal'); + + // Pick Select + ProductConfigurator.do.pickRadio('Other'); + + // Fill in custom attribute + ProductConfigurator.do.fillCustomAttribute('Custom Fabric'); + + // Confirm configuration + ProductConfigurator.do.confirmAttributes(); + + // Check that the product has been added to the order with correct attributes and price + ProductScreen.check.selectedOrderlineHas('Configurable Chair (Red, Metal, Other: Custom Fabric)', '1.0', '11.0'); + + // Orderlines with the same attributes should be merged + ProductScreen.do.clickHomeCategory(); + ProductScreen.do.clickDisplayedProduct('Configurable Chair'); + ProductConfigurator.do.pickColor('Red'); + ProductConfigurator.do.pickSelect('Metal'); + ProductConfigurator.do.pickRadio('Other'); + ProductConfigurator.do.fillCustomAttribute('Custom Fabric'); + ProductConfigurator.do.confirmAttributes(); + ProductScreen.check.selectedOrderlineHas('Configurable Chair (Red, Metal, Other: Custom Fabric)', '2.0', '22.0'); + + // Orderlines with different attributes shouldn't be merged + ProductScreen.do.clickHomeCategory(); + ProductScreen.do.clickDisplayedProduct('Configurable Chair'); + ProductConfigurator.do.pickColor('Blue'); + ProductConfigurator.do.pickSelect('Metal'); + ProductConfigurator.do.pickRadio('Leather'); + ProductConfigurator.do.confirmAttributes(); + ProductScreen.check.selectedOrderlineHas('Configurable Chair (Blue, Metal, Leather)', '1.0', '10.0'); + + Tour.register('ProductConfiguratorTour', { test: true, url: '/pos/ui' }, getSteps()); +}); diff --git a/addons/point_of_sale/static/tests/tours/ProductScreen.tour.js b/addons/point_of_sale/static/tests/tours/ProductScreen.tour.js new file mode 100644 index 00000000..9d3dcc3f --- /dev/null +++ b/addons/point_of_sale/static/tests/tours/ProductScreen.tour.js @@ -0,0 +1,105 @@ +odoo.define('point_of_sale.tour.ProductScreen', function (require) { + 'use strict'; + + const { ProductScreen } = require('point_of_sale.tour.ProductScreenTourMethods'); + const { getSteps, startSteps } = require('point_of_sale.tour.utils'); + var Tour = require('web_tour.tour'); + + // signal to start generating steps + // when finished, steps can be taken from getSteps + startSteps(); + + // Go by default to home category + ProductScreen.do.clickHomeCategory(); + + // Clicking product multiple times should increment quantity + ProductScreen.do.clickDisplayedProduct('Desk Organizer'); + ProductScreen.check.selectedOrderlineHas('Desk Organizer', '1.0', '5.10'); + ProductScreen.do.clickDisplayedProduct('Desk Organizer'); + ProductScreen.check.selectedOrderlineHas('Desk Organizer', '2.0', '10.20'); + + // Clicking product should add new orderline and select the orderline + // If orderline exists, increment the quantity + ProductScreen.do.clickDisplayedProduct('Letter Tray'); + ProductScreen.check.selectedOrderlineHas('Letter Tray', '1.0', '4.80'); + ProductScreen.do.clickDisplayedProduct('Desk Organizer'); + ProductScreen.check.selectedOrderlineHas('Desk Organizer', '3.0', '15.30'); + + // Check effects of clicking numpad buttons + ProductScreen.do.clickOrderline('Letter Tray', '1'); + ProductScreen.check.selectedOrderlineHas('Letter Tray', '1.0'); + ProductScreen.do.pressNumpad('Backspace'); + ProductScreen.check.selectedOrderlineHas('Letter Tray', '0.0', '0.0'); + ProductScreen.do.pressNumpad('Backspace'); + ProductScreen.check.selectedOrderlineHas('Desk Organizer', '3', '15.30'); + ProductScreen.do.pressNumpad('Backspace'); + ProductScreen.check.selectedOrderlineHas('Desk Organizer', '0.0', '0.0'); + ProductScreen.do.pressNumpad('1'); + ProductScreen.check.selectedOrderlineHas('Desk Organizer', '1.0', '5.1'); + ProductScreen.do.pressNumpad('2'); + ProductScreen.check.selectedOrderlineHas('Desk Organizer', '12.0', '61.2'); + ProductScreen.do.pressNumpad('3'); + ProductScreen.check.selectedOrderlineHas('Desk Organizer', '123.0', '627.3'); + ProductScreen.do.pressNumpad('. 5'); + ProductScreen.check.selectedOrderlineHas('Desk Organizer', '123.5', '629.85'); + ProductScreen.do.pressNumpad('Price'); + ProductScreen.do.pressNumpad('1'); + ProductScreen.check.selectedOrderlineHas('Desk Organizer', '123.5', '123.5'); + ProductScreen.do.pressNumpad('1 .'); + ProductScreen.check.selectedOrderlineHas('Desk Organizer', '123.5', '1,358.5'); + ProductScreen.do.pressNumpad('Disc'); + ProductScreen.do.pressNumpad('5 .'); + ProductScreen.check.selectedOrderlineHas('Desk Organizer', '123.5', '1,290.58'); + ProductScreen.do.pressNumpad('Qty'); + ProductScreen.do.pressNumpad('Backspace'); + ProductScreen.do.pressNumpad('Backspace'); + ProductScreen.check.orderIsEmpty(); + + // Check different subcategories + ProductScreen.do.clickSubcategory('Desks'); + ProductScreen.check.productIsDisplayed('Desk Pad'); + ProductScreen.do.clickHomeCategory(); + ProductScreen.do.clickSubcategory('Miscellaneous'); + ProductScreen.check.productIsDisplayed('Whiteboard Pen'); + ProductScreen.do.clickHomeCategory(); + ProductScreen.do.clickSubcategory('Chairs'); + ProductScreen.check.productIsDisplayed('Letter Tray'); + ProductScreen.do.clickHomeCategory(); + + // Add multiple orderlines then delete each of them until empty + ProductScreen.do.clickDisplayedProduct('Whiteboard Pen'); + ProductScreen.do.clickDisplayedProduct('Wall Shelf Unit'); + ProductScreen.do.clickDisplayedProduct('Small Shelf'); + ProductScreen.do.clickDisplayedProduct('Magnetic Board'); + ProductScreen.do.clickDisplayedProduct('Monitor Stand'); + ProductScreen.do.clickOrderline('Whiteboard Pen', '1.0'); + ProductScreen.check.selectedOrderlineHas('Whiteboard Pen', '1.0'); + ProductScreen.do.pressNumpad('Backspace'); + ProductScreen.check.selectedOrderlineHas('Whiteboard Pen', '0.0'); + ProductScreen.do.pressNumpad('Backspace'); + ProductScreen.check.selectedOrderlineHas('Monitor Stand', '1.0'); + ProductScreen.do.clickOrderline('Wall Shelf Unit', '1.0'); + ProductScreen.check.selectedOrderlineHas('Wall Shelf Unit', '1.0'); + ProductScreen.do.pressNumpad('Backspace'); + ProductScreen.check.selectedOrderlineHas('Wall Shelf Unit', '0.0'); + ProductScreen.do.pressNumpad('Backspace'); + ProductScreen.check.selectedOrderlineHas('Monitor Stand', '1.0'); + ProductScreen.do.clickOrderline('Small Shelf', '1.0'); + ProductScreen.check.selectedOrderlineHas('Small Shelf', '1.0'); + ProductScreen.do.pressNumpad('Backspace'); + ProductScreen.check.selectedOrderlineHas('Small Shelf', '0.0'); + ProductScreen.do.pressNumpad('Backspace'); + ProductScreen.check.selectedOrderlineHas('Monitor Stand', '1.0'); + ProductScreen.do.clickOrderline('Magnetic Board', '1.0'); + ProductScreen.check.selectedOrderlineHas('Magnetic Board', '1.0'); + ProductScreen.do.pressNumpad('Backspace'); + ProductScreen.check.selectedOrderlineHas('Magnetic Board', '0.0'); + ProductScreen.do.pressNumpad('Backspace'); + ProductScreen.check.selectedOrderlineHas('Monitor Stand', '1.0'); + ProductScreen.do.pressNumpad('Backspace'); + ProductScreen.check.selectedOrderlineHas('Monitor Stand', '0.0'); + ProductScreen.do.pressNumpad('Backspace'); + ProductScreen.check.orderIsEmpty(); + + Tour.register('ProductScreenTour', { test: true, url: '/pos/ui' }, getSteps()); +}); diff --git a/addons/point_of_sale/static/tests/tours/ReceiptScreen.tour.js b/addons/point_of_sale/static/tests/tours/ReceiptScreen.tour.js new file mode 100644 index 00000000..2e330a9a --- /dev/null +++ b/addons/point_of_sale/static/tests/tours/ReceiptScreen.tour.js @@ -0,0 +1,61 @@ +odoo.define('point_of_sale.tour.ReceiptScreen', function (require) { + 'use strict'; + + const { ProductScreen } = require('point_of_sale.tour.ProductScreenTourMethods'); + const { ReceiptScreen } = require('point_of_sale.tour.ReceiptScreenTourMethods'); + const { PaymentScreen } = require('point_of_sale.tour.PaymentScreenTourMethods'); + const { NumberPopup } = require('point_of_sale.tour.NumberPopupTourMethods'); + const { getSteps, startSteps } = require('point_of_sale.tour.utils'); + const Tour = require('web_tour.tour'); + + startSteps(); + + // press close button in receipt screen + ProductScreen.exec.addOrderline('Letter Tray', '10', '5'); + ProductScreen.check.selectedOrderlineHas('Letter Tray', '10'); + ProductScreen.do.clickPayButton(); + PaymentScreen.do.clickPaymentMethod('Bank'); + PaymentScreen.check.validateButtonIsHighlighted(true); + PaymentScreen.do.clickValidate(); + ReceiptScreen.check.receiptIsThere(); + // letter tray has 10% tax (search SRC) + ReceiptScreen.check.totalAmountContains('55.0'); + ReceiptScreen.do.clickNextOrder(); + + // send email in receipt screen + ProductScreen.do.clickHomeCategory(); + ProductScreen.exec.addOrderline('Desk Pad', '6', '5', '30.0'); + ProductScreen.exec.addOrderline('Whiteboard Pen', '6', '6', '36.0'); + ProductScreen.exec.addOrderline('Monitor Stand', '6', '1', '6.0'); + ProductScreen.do.clickPayButton(); + PaymentScreen.do.clickPaymentMethod('Cash'); + PaymentScreen.do.pressNumpad('7 0'); + PaymentScreen.check.remainingIs('2.0'); + PaymentScreen.do.pressNumpad('0'); + PaymentScreen.check.remainingIs('0.00'); + PaymentScreen.check.changeIs('628.0'); + PaymentScreen.do.clickValidate(); + ReceiptScreen.check.receiptIsThere(); + ReceiptScreen.check.totalAmountContains('72.0'); + ReceiptScreen.do.setEmail('test@receiptscreen.com'); + ReceiptScreen.do.clickSend(); + ReceiptScreen.check.emailIsSuccessful(); + ReceiptScreen.do.clickNextOrder(); + + // order with tip + // check if tip amount is displayed + ProductScreen.exec.addOrderline('Desk Pad', '6', '5'); + ProductScreen.do.clickPayButton(); + PaymentScreen.do.clickTipButton(); + NumberPopup.do.pressNumpad('1'); + NumberPopup.check.inputShownIs('1'); + NumberPopup.do.clickConfirm(); + PaymentScreen.check.emptyPaymentlines('31.0'); + PaymentScreen.do.clickPaymentMethod('Cash'); + PaymentScreen.do.clickValidate(); + ReceiptScreen.check.receiptIsThere(); + ReceiptScreen.check.totalAmountContains('$ 30.00 + $ 1.00 tip'); + ReceiptScreen.do.clickNextOrder(); + + Tour.register('ReceiptScreenTour', { test: true, url: '/pos/ui' }, getSteps()); +}); diff --git a/addons/point_of_sale/static/tests/tours/TicketScreen.tour.js b/addons/point_of_sale/static/tests/tours/TicketScreen.tour.js new file mode 100644 index 00000000..a26c0b36 --- /dev/null +++ b/addons/point_of_sale/static/tests/tours/TicketScreen.tour.js @@ -0,0 +1,54 @@ +odoo.define('point_of_sale.tour.TicketScreen', function (require) { + 'use strict'; + + const { ProductScreen } = require('point_of_sale.tour.ProductScreenTourMethods'); + const { ReceiptScreen } = require('point_of_sale.tour.ReceiptScreenTourMethods'); + const { PaymentScreen } = require('point_of_sale.tour.PaymentScreenTourMethods'); + const { TicketScreen } = require('point_of_sale.tour.TicketScreenTourMethods'); + const { Chrome } = require('point_of_sale.tour.ChromeTourMethods'); + const { getSteps, startSteps } = require('point_of_sale.tour.utils'); + var Tour = require('web_tour.tour'); + + startSteps(); + + ProductScreen.do.clickHomeCategory(); + ProductScreen.exec.addOrderline('Desk Pad', '1', '2'); + ProductScreen.do.clickCustomerButton(); + ProductScreen.do.clickCustomer('Nicole Ford'); + ProductScreen.do.clickSetCustomer(); + Chrome.do.clickTicketButton(); + TicketScreen.check.nthRowContains(2, 'Nicole Ford'); + TicketScreen.do.clickNewTicket(); + ProductScreen.exec.addOrderline('Desk Pad', '1', '3'); + ProductScreen.do.clickCustomerButton(); + ProductScreen.do.clickCustomer('Brandon Freeman'); + ProductScreen.do.clickSetCustomer(); + ProductScreen.do.clickPayButton(); + PaymentScreen.check.isShown(); + Chrome.do.clickTicketButton(); + TicketScreen.check.nthRowContains(3, 'Brandon Freeman'); + TicketScreen.do.clickNewTicket(); + ProductScreen.exec.addOrderline('Desk Pad', '1', '4'); + ProductScreen.do.clickPayButton(); + PaymentScreen.do.clickPaymentMethod('Bank'); + PaymentScreen.do.clickValidate(); + ReceiptScreen.check.isShown(); + Chrome.do.clickTicketButton(); + TicketScreen.check.nthRowContains(4, 'Receipt'); + TicketScreen.do.selectFilter('Receipt'); + TicketScreen.check.nthRowContains(2, 'Receipt'); + TicketScreen.do.selectFilter('Payment'); + TicketScreen.check.nthRowContains(2, 'Payment'); + TicketScreen.do.selectFilter('Ongoing'); + TicketScreen.check.nthRowContains(2, 'Ongoing'); + TicketScreen.do.selectFilter('All'); + TicketScreen.check.nthRowContains(4, 'Receipt'); + TicketScreen.do.search('Customer', 'Nicole'); + TicketScreen.check.nthRowContains(2, 'Nicole'); + TicketScreen.do.search('Customer', 'Brandon'); + TicketScreen.check.nthRowContains(2, 'Brandon'); + TicketScreen.do.search('Receipt Number', '-0003'); + TicketScreen.check.nthRowContains(2, 'Receipt'); + + Tour.register('TicketScreenTour', { test: true, url: '/pos/ui' }, getSteps()); +}); diff --git a/addons/point_of_sale/static/tests/tours/helpers/ChromeTourMethods.js b/addons/point_of_sale/static/tests/tours/helpers/ChromeTourMethods.js new file mode 100644 index 00000000..30609a9f --- /dev/null +++ b/addons/point_of_sale/static/tests/tours/helpers/ChromeTourMethods.js @@ -0,0 +1,42 @@ +odoo.define('point_of_sale.tour.ChromeTourMethods', function (require) { + 'use strict'; + + const { createTourMethods } = require('point_of_sale.tour.utils'); + + class Do { + confirmPopup() { + return [ + { + content: 'confirm popup', + trigger: '.popups .modal-dialog .button.confirm', + }, + ]; + } + clickOrderManagementButton() { + return [ + { + content: 'check order management button is shown', + trigger: '.pos .pos-rightheader .order-management', + run: () => {}, + }, + { + content: 'click order management button', + trigger: '.pos .pos-rightheader .order-management', + }, + ]; + } + clickTicketButton() { + return [ + { + trigger: '.pos-topheader .ticket-button', + }, + { + trigger: '.subwindow .ticket-screen', + run: () => {}, + }, + ]; + } + } + + return createTourMethods('Chrome', Do); +}); diff --git a/addons/point_of_sale/static/tests/tours/helpers/ClientListScreenTourMethods.js b/addons/point_of_sale/static/tests/tours/helpers/ClientListScreenTourMethods.js new file mode 100644 index 00000000..d6be643e --- /dev/null +++ b/addons/point_of_sale/static/tests/tours/helpers/ClientListScreenTourMethods.js @@ -0,0 +1,57 @@ +odoo.define('point_of_sale.tour.ClientListScreenTourMethods', function (require) { + 'use strict'; + + const { createTourMethods } = require('point_of_sale.tour.utils'); + + class Do { + clickClient(name) { + return [ + { + content: `click client '${name}' from client list screen`, + trigger: `.clientlist-screen .client-list-contents .client-line td:contains("${name}")`, + }, + { + content: `check if client '${name}' is highlighted`, + trigger: `.clientlist-screen .client-list-contents .client-line.highlight td:contains("${name}")`, + run: () => {}, + }, + ]; + } + clickSet() { + return [ + { + content: 'check if set button shown', + trigger: '.clientlist-screen .button.next.highlight', + run: () => {}, + }, + { + content: 'click set button', + trigger: '.clientlist-screen .button.next.highlight', + }, + ]; + } + } + + class Check { + isShown() { + return [ + { + content: 'client list screen is shown', + trigger: '.pos-content .clientlist-screen', + run: () => {}, + }, + ]; + } + } + + class Execute { + setClient(name) { + const steps = []; + steps.push(...this._do.clickClient(name)); + steps.push(...this._do.clickSet()); + return steps; + } + } + + return createTourMethods('ClientListScreen', Do, Check, Execute); +}); diff --git a/addons/point_of_sale/static/tests/tours/helpers/CompositeTourMethods.js b/addons/point_of_sale/static/tests/tours/helpers/CompositeTourMethods.js new file mode 100644 index 00000000..c361a532 --- /dev/null +++ b/addons/point_of_sale/static/tests/tours/helpers/CompositeTourMethods.js @@ -0,0 +1,23 @@ +odoo.define('point_of_sale.tour.CompositeTourMethods', function (require) { + 'use strict'; + + const { ProductScreen } = require('point_of_sale.tour.ProductScreenTourMethods'); + const { ReceiptScreen } = require('point_of_sale.tour.ReceiptScreenTourMethods'); + const { PaymentScreen } = require('point_of_sale.tour.PaymentScreenTourMethods'); + const { ClientListScreen } = require('point_of_sale.tour.ClientListScreenTourMethods'); + + function makeFullOrder({ orderlist, customer, payment, ntimes = 1 }) { + for (let i = 0; i < ntimes; i++) { + ProductScreen.exec.addMultiOrderlines(...orderlist); + if (customer) { + ProductScreen.do.clickCustomerButton(); + ClientListScreen.exec.setClient(customer); + } + ProductScreen.do.clickPayButton(); + PaymentScreen.exec.pay(...payment); + ReceiptScreen.exec.nextOrder(); + } + } + + return { makeFullOrder }; +}); diff --git a/addons/point_of_sale/static/tests/tours/helpers/ErrorPopupTourMethods.js b/addons/point_of_sale/static/tests/tours/helpers/ErrorPopupTourMethods.js new file mode 100644 index 00000000..3d8c07cf --- /dev/null +++ b/addons/point_of_sale/static/tests/tours/helpers/ErrorPopupTourMethods.js @@ -0,0 +1,30 @@ +odoo.define('point_of_sale.tour.ErrorPopupTourMethods', function (require) { + 'use strict'; + + const { createTourMethods } = require('point_of_sale.tour.utils'); + + class Do { + clickConfirm() { + return [ + { + content: 'click confirm button', + trigger: '.popup-error .footer .cancel', + }, + ]; + } + } + + class Check { + isShown() { + return [ + { + content: 'error popup is shown', + trigger: '.modal-dialog .popup-error', + run: () => {}, + }, + ]; + } + } + + return createTourMethods('ErrorPopup', Do, Check); +}); diff --git a/addons/point_of_sale/static/tests/tours/helpers/NumberPopupTourMethods.js b/addons/point_of_sale/static/tests/tours/helpers/NumberPopupTourMethods.js new file mode 100644 index 00000000..c12d0d02 --- /dev/null +++ b/addons/point_of_sale/static/tests/tours/helpers/NumberPopupTourMethods.js @@ -0,0 +1,72 @@ +odoo.define('point_of_sale.tour.NumberPopupTourMethods', function (require) { + 'use strict'; + + const { createTourMethods } = require('point_of_sale.tour.utils'); + + class Do { + /** + * Note: Maximum of 2 characters because NumberBuffer only allows 2 consecutive + * fast inputs. Fast inputs is the case in tours. + * + * @param {String} keys space-separated input keys + */ + pressNumpad(keys) { + const numberChars = '0 1 2 3 4 5 6 7 8 9 C'.split(' '); + const modeButtons = '+1 +10 +2 +20 +5 +50'.split(' '); + const decimalSeparators = ', .'.split(' '); + function generateStep(key) { + let trigger; + if (numberChars.includes(key)) { + trigger = `.popup-numpad .number-char:contains("${key}")`; + } else if (modeButtons.includes(key)) { + trigger = `.popup-numpad .mode-button:contains("${key}")`; + } else if (key === 'Backspace') { + trigger = `.popup-numpad .numpad-backspace`; + } else if (decimalSeparators.includes(key)) { + trigger = `.popup-numpad .number-char.dot`; + } + return { + content: `'${key}' pressed in numpad`, + trigger, + }; + } + return keys.split(' ').map(generateStep); + } + clickConfirm() { + return [ + { + content: 'click confirm button', + trigger: '.popup-number .footer .confirm', + }, + ]; + } + } + + class Check { + isShown() { + return [ + { + content: 'number popup is shown', + trigger: '.modal-dialog .popup-number', + run: () => {}, + }, + ]; + } + inputShownIs(val) { + return [ + { + content: 'number input element check', + trigger: '.modal-dialog .popup-number .popup-input', + run: () => {}, + }, + { + content: `input shown is '${val}'`, + trigger: `.modal-dialog .popup-number .popup-input:contains("${val}")`, + run: () => {}, + }, + ]; + } + } + + return createTourMethods('NumberPopup', Do, Check); +}); diff --git a/addons/point_of_sale/static/tests/tours/helpers/OrderManagementScreenTourMethods.js b/addons/point_of_sale/static/tests/tours/helpers/OrderManagementScreenTourMethods.js new file mode 100644 index 00000000..26e48589 --- /dev/null +++ b/addons/point_of_sale/static/tests/tours/helpers/OrderManagementScreenTourMethods.js @@ -0,0 +1,180 @@ +odoo.define('point_of_sale.tour.OrderManagementScreenTourMethods', function (require) { + 'use strict'; + + const { createTourMethods } = require('point_of_sale.tour.utils'); + + class Do { + clickBack() { + return [ + { + content: 'order management screen, click back button', + trigger: '.order-management-screen .control-panel .button.back', + }, + ]; + } + clickOrder(name, [otherCol, otherColVal] = [null, null]) { + let trigger = `.order-management-screen .order-list .order-row .item.name:contains("${name}")`; + if (otherCol) { + trigger = `${trigger} ~ .item.${otherCol}:contains("${otherColVal}")`; + } + return [ + { + content: `clicking order '${name}' from orderlist`, + trigger, + }, + ]; + } + clickInvoiceButton() { + return [ + { + content: 'click invoice button', + trigger: '.order-management-screen .control-button span:contains("Invoice")', + }, + ]; + } + clickPrintReceiptButton() { + return [ + { + content: 'click reprint receipt button', + trigger: '.order-management-screen .control-button span:contains("Print Receipt")' + } + ] + } + clickCustomerButton() { + return [ + { + content: 'click customer button', + trigger: '.order-management-screen .actionpad .button.set-customer', + }, + ]; + } + closeReceipt() { + return [ + { + content: 'close receipt', + trigger: '.receipt-screen .button.back', + } + ] + } + } + + class Check { + isShown() { + return [ + { + content: 'order management screen is shown', + trigger: '.pos .pos-content .order-management-screen', + run: () => {}, + }, + ]; + } + orderlistHas({ orderName, total, customer }) { + const steps = []; + steps.push({ + content: `order list has row having: name '${orderName}', total '${total}'`, + trigger: `.order-list .order-row .item:contains("${orderName}") ~ .item:contains("${total}")`, + run: () => {}, + }); + if (customer) { + steps.push({ + content: `order list has row having: name '${orderName}', customer '${customer}'`, + trigger: `.order-list .order-row .item:contains("${orderName}") ~ .item:contains("${customer}")`, + run: () => {}, + }); + } + return steps; + } + highlightedOrderRowHas(name) { + return [ + { + content: `order '${name}' in orderlist is highligted`, + trigger: `.order-list .order-row.highlight:has(> .item:contains("${name}"))`, + run: () => {}, + }, + ]; + } + orderRowIsNotHighlighted(name) { + return [ + { + content: `order '${name}' in orderlist is not highligted`, + trigger: `.order-list .order-row:not(:has(.highlight)):has(> .item:contains("${name}"))`, + run: () => {}, + }, + ]; + } + orderDetailsHas({ lines, total }) { + const steps = []; + for (let { product, quantity } of lines) { + steps.push({ + content: `order details has product '${product}' and quantity '${quantity}'`, + trigger: `.orderlines .product-name:contains("${product}") ~ .info strong:contains("${quantity}")`, + run: () => {}, + }); + } + if (total) { + steps.push({ + content: `order details has total amount of ${total}`, + trigger: `.order-container .summary .total .value:contains("${total}")`, + run: () => {}, + }); + } + return steps; + } + customerIs(name) { + return [ + { + content: `set customer is '${name}'`, + trigger: `.order-management-screen .actionpad .set-customer:contains("${name}")`, + run: () => {}, + }, + ]; + } + reprintReceiptIsShown() { + return [ + { + content: 'reprint receipt screen is shown', + trigger: '.pos .receipt-screen', + run: () => {}, + } + ] + } + receiptChangeIs(amount) { + return [ + { + content: `receipt change is ${amount}`, + trigger: `.pos-receipt-amount.receipt-change:contains("${amount}")`, + run: () => {}, + } + ] + } + receiptOrderDataContains(orderInfo) { + return [ + { + content: `order data contains ${orderInfo}`, + trigger: `.pos-receipt-order-data:contains("${orderInfo}")`, + run: () => {}, + } + ] + } + receiptAmountIs(amount) { + return [ + { + content: `receipt amount is ${amount}`, + trigger: `.pos-receipt-amount:contains("${amount}")`, + run: () => {}, + } + ] + } + isNotHidden() { + return [ + { + content: 'order management screen is not hidden', + trigger: `.order-management-screen:not(:has(.oe_hidden))`, + run: () => {}, + } + ] + } + } + + return createTourMethods('OrderManagementScreen', Do, Check); +}); diff --git a/addons/point_of_sale/static/tests/tours/helpers/PaymentScreenTourMethods.js b/addons/point_of_sale/static/tests/tours/helpers/PaymentScreenTourMethods.js new file mode 100644 index 00000000..93a5cef6 --- /dev/null +++ b/addons/point_of_sale/static/tests/tours/helpers/PaymentScreenTourMethods.js @@ -0,0 +1,215 @@ +odoo.define('point_of_sale.tour.PaymentScreenTourMethods', function (require) { + 'use strict'; + + const { createTourMethods } = require('point_of_sale.tour.utils'); + + class Do { + clickPaymentMethod(name) { + return [ + { + content: `click '${name}' payment method`, + trigger: `.paymentmethods .button.paymentmethod:contains("${name}")`, + }, + ]; + } + + /** + * Delete the paymentline having the given payment method name and amount. + * @param {String} name payment method + * @param {String} amount + */ + clickPaymentlineDelButton(name, amount) { + return [ + { + content: `delete ${name} paymentline with ${amount} amount`, + trigger: `.paymentlines .paymentline .payment-name:contains("${name}") ~ .delete-button`, + }, + ]; + } + + clickEmailButton() { + return [ + { + content: `click email button`, + trigger: `.payment-buttons .js_email`, + }, + ]; + } + + clickTipButton() { + return [ + { + trigger: `.payment-buttons .js_tip`, + }, + ]; + } + + clickInvoiceButton() { + return [{ content: 'click invoice button', trigger: '.payment-buttons .js_invoice' }]; + } + + clickValidate() { + return [ + { + content: 'validate payment', + trigger: `.payment-screen .button.next.highlight`, + }, + ]; + } + + /** + * Press the numpad in sequence based on the given space-separated keys. + * Note: Maximum of 2 characters because NumberBuffer only allows 2 consecutive + * fast inputs. Fast inputs is the case in tours. + * + * @param {String} keys space-separated numpad keys + */ + pressNumpad(keys) { + const numberChars = '. +/- 0 1 2 3 4 5 6 7 8 9'.split(' '); + const modeButtons = '+10 +20 +50'.split(' '); + function generateStep(key) { + let trigger; + if (numberChars.includes(key)) { + trigger = `.payment-numpad .number-char:contains("${key}")`; + } else if (modeButtons.includes(key)) { + trigger = `.payment-numpad .mode-button:contains("${key}")`; + } else if (key === 'Backspace') { + trigger = `.payment-numpad .number-char img[alt="Backspace"]`; + } + return { + content: `'${key}' pressed in payment numpad`, + trigger, + }; + } + return keys.split(' ').map(generateStep); + } + + clickBack() { + return [ + { + content: 'click back button', + trigger: '.payment-screen .button.back', + }, + ]; + } + + clickTipButton() { + return [ + { + trigger: '.payment-screen .button.js_tip', + }, + ] + } + } + + class Check { + isShown() { + return [ + { + content: 'payment screen is shown', + trigger: '.pos .payment-screen', + run: () => {}, + }, + ]; + } + /** + * Check if change is the provided amount. + * @param {String} amount + */ + changeIs(amount) { + return [ + { + content: `change is ${amount}`, + trigger: `.payment-status-change .amount:contains("${amount}")`, + run: () => {}, + }, + ]; + } + + /** + * Check if the remaining is the provided amount. + * @param {String} amount + */ + remainingIs(amount) { + return [ + { + content: `remaining amount is ${amount}`, + trigger: `.payment-status-remaining .amount:contains("${amount}")`, + run: () => {}, + }, + ]; + } + + /** + * Check if validate button is highlighted. + * @param {Boolean} isHighlighted + */ + validateButtonIsHighlighted(isHighlighted = true) { + return [ + { + content: `validate button is ${ + isHighlighted ? 'highlighted' : 'not highligted' + }`, + trigger: isHighlighted + ? `.payment-screen .button.next.highlight` + : `.payment-screen .button.next:not(:has(.highlight))`, + run: () => {}, + }, + ]; + } + + /** + * Check if the paymentlines are empty. Also provide the amount to pay. + * @param {String} amountToPay + */ + emptyPaymentlines(amountToPay) { + return [ + { + content: `there are no paymentlines`, + trigger: `.paymentlines-empty`, + run: () => {}, + }, + { + content: `amount to pay is '${amountToPay}'`, + trigger: `.paymentlines-empty .total:contains("${amountToPay}")`, + run: () => {}, + }, + ]; + } + + /** + * Check if the selected paymentline has the given payment method and amount. + * @param {String} paymentMethodName + * @param {String} amount + */ + selectedPaymentlineHas(paymentMethodName, amount) { + return [ + { + content: `line paid via '${paymentMethodName}' is selected`, + trigger: `.paymentlines .paymentline.selected .payment-name:contains("${paymentMethodName}")`, + run: () => {}, + }, + { + content: `amount tendered in the line is '${amount}'`, + trigger: `.paymentlines .paymentline.selected .payment-amount:contains("${amount}")`, + run: () => {}, + }, + ]; + } + } + + class Execute { + pay(method, amount) { + const steps = []; + steps.push(...this._do.clickPaymentMethod(method)); + for (let char of amount.split('')) { + steps.push(...this._do.pressNumpad(char)); + } + steps.push(...this._check.validateButtonIsHighlighted()); + steps.push(...this._do.clickValidate()); + return steps; + } + } + + return createTourMethods('PaymentScreen', Do, Check, Execute); +}); diff --git a/addons/point_of_sale/static/tests/tours/helpers/ProductConfiguratorTourMethods.js b/addons/point_of_sale/static/tests/tours/helpers/ProductConfiguratorTourMethods.js new file mode 100644 index 00000000..5d10f9fd --- /dev/null +++ b/addons/point_of_sale/static/tests/tours/helpers/ProductConfiguratorTourMethods.js @@ -0,0 +1,77 @@ +odoo.define('point_of_sale.tour.ProductConfiguratorTourMethods', function (require) { + 'use strict'; + + const { createTourMethods } = require('point_of_sale.tour.utils'); + + class Do { + pickRadio(name) { + return [ + { + content: `picking radio attribute with name ${name}`, + trigger: `.product-configurator-popup .radio_attribute_label:contains('${name}')`, + }, + ]; + } + + pickSelect(name) { + return [ + { + content: `picking select attribute with name ${name}`, + trigger: `.product-configurator-popup .configurator_select:has(option:contains('${name}'))`, + run: `text ${name}`, + }, + ]; + } + + pickColor(name) { + return [ + { + content: `picking color attribute with name ${name}`, + trigger: `.product-configurator-popup .configurator_color[data-color='${name}']`, + }, + ]; + } + + fillCustomAttribute(value) { + return [ + { + content: `filling custom attribute with value ${value}`, + trigger: `.product-configurator-popup .custom_value`, + run: `text ${value}`, + }, + ]; + } + + confirmAttributes() { + return [ + { + content: `confirming product configuration`, + trigger: `.product-configurator-popup .button.confirm`, + }, + ]; + } + + cancelAttributes() { + return [ + { + content: `canceling product configuration`, + trigger: `.product-configurator-popup .button.cancel`, + }, + ]; + } + } + + class Check { + isShown() { + return [ + { + content: 'product configurator is shown', + trigger: '.product-configurator-popup:not(:has(.oe_hidden))', + run: () => {}, + }, + ]; + } + } + + return createTourMethods('ProductConfigurator', Do, Check); +}); diff --git a/addons/point_of_sale/static/tests/tours/helpers/ProductScreenTourMethods.js b/addons/point_of_sale/static/tests/tours/helpers/ProductScreenTourMethods.js new file mode 100644 index 00000000..69aab18b --- /dev/null +++ b/addons/point_of_sale/static/tests/tours/helpers/ProductScreenTourMethods.js @@ -0,0 +1,254 @@ +odoo.define('point_of_sale.tour.ProductScreenTourMethods', function (require) { + 'use strict'; + + const { createTourMethods } = require('point_of_sale.tour.utils'); + + class Do { + clickDisplayedProduct(name) { + return [ + { + content: `click product '${name}'`, + trigger: `.product-list .product-name:contains("${name}")`, + }, + ]; + } + + clickOrderline(name, quantity) { + return [ + { + content: `selecting orderline with product '${name}' and quantity '${quantity}'`, + trigger: `.order .orderline:not(:has(.selected)) .product-name:contains("${name}") ~ .info-list em:contains("${quantity}")`, + }, + { + content: `orderline with product '${name}' and quantity '${quantity}' has been selected`, + trigger: `.order .orderline.selected .product-name:contains("${name}") ~ .info-list em:contains("${quantity}")`, + run: () => {}, + }, + ]; + } + + clickSubcategory(name) { + return [ + { + content: `selecting '${name}' subcategory`, + trigger: `.products-widget > .products-widget-control .category-simple-button:contains("${name}")`, + }, + { + content: `'${name}' subcategory selected`, + trigger: `.breadcrumbs .breadcrumb-button:contains("${name}")`, + run: () => {}, + }, + ]; + } + + clickHomeCategory() { + return [ + { + content: `click Home subcategory`, + trigger: `.breadcrumbs .breadcrumb-home`, + }, + ]; + } + + /** + * Press the numpad in sequence based on the given space-separated keys. + * NOTE: Maximum of 2 characters because NumberBuffer only allows 2 consecutive + * fast inputs. Fast inputs is the case in tours. + * + * @param {String} keys space-separated numpad keys + */ + pressNumpad(keys) { + const numberChars = '. 0 1 2 3 4 5 6 7 8 9'.split(' '); + const modeButtons = 'Qty Price Disc'.split(' '); + function generateStep(key) { + let trigger; + if (numberChars.includes(key)) { + trigger = `.numpad .number-char:contains("${key}")`; + } else if (modeButtons.includes(key)) { + trigger = `.numpad .mode-button:contains("${key}")`; + } else if (key === 'Backspace') { + trigger = `.numpad .numpad-backspace`; + } else if (key === '+/-') { + trigger = `.numpad .numpad-minus`; + } + return { + content: `'${key}' pressed in product screen numpad`, + trigger, + }; + } + return keys.split(' ').map(generateStep); + } + + clickPayButton() { + return [ + { content: 'click pay button', trigger: '.actionpad .button.pay' }, + { + content: 'now in payment screen', + trigger: '.pos-content .payment-screen', + run: () => {}, + }, + ]; + } + + clickCustomerButton() { + return [ + { content: 'click customer button', trigger: '.actionpad .button.set-customer' }, + { + content: 'customer screen is shown', + trigger: '.pos-content .clientlist-screen', + run: () => {}, + }, + ]; + } + + clickCustomer(name) { + return [ + { + content: `select customer '${name}'`, + trigger: `.clientlist-screen .client-line td:contains("${name}")`, + }, + { + content: `client line '${name}' is highlighted`, + trigger: `.clientlist-screen .client-line.highlight td:contains("${name}")`, + run: () => {}, + }, + ]; + } + + clickSetCustomer() { + return [ + { + content: 'click set customer', + trigger: '.clientlist-screen .button.next.highlight', + }, + ]; + } + } + + class Check { + isShown() { + return [ + { + content: 'product screen is shown', + trigger: '.product-screen:not(:has(.oe_hidden))', + run: () => {}, + }, + ]; + } + selectedOrderlineHas(name, quantity, price) { + const res = [ + { + // check first if the order widget is there and has orderlines + content: 'order widget has orderlines', + trigger: '.order .orderlines', + run: () => {}, + }, + { + content: `'${name}' is selected`, + trigger: `.order .orderline.selected .product-name:contains("${name}")`, + run: function () {}, // it's a check + }, + ]; + if (quantity) { + res.push({ + content: `selected line has ${quantity} quantity`, + trigger: `.order .orderline.selected .product-name:contains("${name}") ~ .info-list em:contains("${quantity}")`, + run: function () {}, // it's a check + }); + } + if (price) { + res.push({ + content: `selected line has total price of ${price}`, + trigger: `.order .orderline.selected .product-name:contains("${name}") ~ .price:contains("${price}")`, + run: function () {}, // it's a check + }); + } + return res; + } + orderIsEmpty() { + return [ + { + content: `order is empty`, + trigger: `.order .order-empty`, + run: () => {}, + }, + ]; + } + + productIsDisplayed(name) { + return [ + { + content: `'${name}' should be displayed`, + trigger: `.product-list .product-name:contains("${name}")`, + run: () => {}, + }, + ]; + } + totalAmountIs(amount) { + return [ + { + content: `order total amount is '${amount}'`, + trigger: `.order-container .order .summary .value:contains("${amount}")`, + run: () => {}, + } + ] + } + modeIsActive(mode) { + return [ + { + content: `'${mode}' is active`, + trigger: `.numpad button.selected-mode:contains('${mode}')`, + run: function () {}, + }, + ]; + } + } + + class Execute { + /** + * Create an orderline for the given `productName` and `quantity`. + * - If `unitPrice` is provided, price of the product of the created line + * is changed to that value. + * - If `expectedTotal` is provided, the created orderline (which is the currently + * selected orderline) is checked if it contains the correct quantity and total + * price. + * + * @param {string} productName + * @param {string} quantity + * @param {string} unitPrice + * @param {string} expectedTotal + */ + addOrderline(productName, quantity, unitPrice = undefined, expectedTotal = undefined) { + const res = this._do.clickDisplayedProduct(productName); + if (unitPrice) { + res.push(...this._do.pressNumpad('Price')); + res.push(...this._check.modeIsActive('Price')); + res.push(...this._do.pressNumpad(unitPrice.toString().split('').join(' '))); + res.push(...this._do.pressNumpad('Qty')); + res.push(...this._check.modeIsActive('Qty')); + } + for (let char of quantity.toString()) { + if ('.0123456789'.includes(char)) { + res.push(...this._do.pressNumpad(char)); + } else if ('-'.includes(char)) { + res.push(...this._do.pressNumpad('+/-')); + } + } + if (expectedTotal) { + res.push(...this._check.selectedOrderlineHas(productName, quantity, expectedTotal)); + } else { + res.push(...this._check.selectedOrderlineHas(productName, quantity)); + } + return res; + } + addMultiOrderlines(...list) { + const steps = []; + for (let [product, qty, price] of list) { + steps.push(...this.addOrderline(product, qty, price)); + } + return steps; + } + } + + return createTourMethods('ProductScreen', Do, Check, Execute); +}); diff --git a/addons/point_of_sale/static/tests/tours/helpers/ReceiptScreenTourMethods.js b/addons/point_of_sale/static/tests/tours/helpers/ReceiptScreenTourMethods.js new file mode 100644 index 00000000..49c26703 --- /dev/null +++ b/addons/point_of_sale/static/tests/tours/helpers/ReceiptScreenTourMethods.js @@ -0,0 +1,79 @@ +odoo.define('point_of_sale.tour.ReceiptScreenTourMethods', function (require) { + 'use strict'; + + const { createTourMethods } = require('point_of_sale.tour.utils'); + + class Do { + clickNextOrder() { + return [ + { + content: 'go to next screen', + trigger: '.receipt-screen .button.next.highlight', + }, + ]; + } + setEmail(email) { + return [ + { + trigger: '.receipt-screen .input-email input', + run: `text ${email}`, + }, + ]; + } + clickSend(isHighlighted = true) { + return [ + { + trigger: `.receipt-screen .input-email .send${isHighlighted ? '.highlight' : ''}`, + }, + ]; + } + } + + class Check { + isShown() { + return [ + { + content: 'receipt screen is shown', + trigger: '.pos .receipt-screen', + run: () => {}, + }, + ]; + } + + receiptIsThere() { + return [ + { + content: 'there should be the receipt', + trigger: '.receipt-screen .pos-receipt', + run: () => {}, + }, + ]; + } + + totalAmountContains(value) { + return [ + { + trigger: `.receipt-screen .top-content h1:contains("${value}")`, + run: () => {}, + }, + ]; + } + + emailIsSuccessful() { + return [ + { + trigger: `.receipt-screen .notice.successful`, + run: () => {}, + }, + ]; + } + } + + class Execute { + nextOrder() { + return [...this._check.isShown(), ...this._do.clickNextOrder()]; + } + } + + return createTourMethods('ReceiptScreen', Do, Check, Execute); +}); diff --git a/addons/point_of_sale/static/tests/tours/helpers/SelectionPopupTourMethods.js b/addons/point_of_sale/static/tests/tours/helpers/SelectionPopupTourMethods.js new file mode 100644 index 00000000..bbe4fc2d --- /dev/null +++ b/addons/point_of_sale/static/tests/tours/helpers/SelectionPopupTourMethods.js @@ -0,0 +1,39 @@ +odoo.define('point_of_sale.tour.SelectionPopupTourMethods', function (require) { + 'use strict'; + + const { createTourMethods } = require('point_of_sale.tour.utils'); + + class Do { + clickItem(name) { + return [ + { + content: `click selection '${name}'`, + trigger: `.selection-item:contains("${name}")`, + }, + ]; + } + } + + class Check { + hasSelectionItem(name) { + return [ + { + content: `selection popup has '${name}'`, + trigger: `.selection-item:contains("${name}")`, + run: () => {}, + }, + ]; + } + isShown() { + return [ + { + content: 'selection popup is shown', + trigger: '.modal-dialog .popup-selection', + run: () => {}, + }, + ]; + } + } + + return createTourMethods('SelectionPopup', Do, Check); +}); diff --git a/addons/point_of_sale/static/tests/tours/helpers/TicketScreenTourMethods.js b/addons/point_of_sale/static/tests/tours/helpers/TicketScreenTourMethods.js new file mode 100644 index 00000000..fe8f8127 --- /dev/null +++ b/addons/point_of_sale/static/tests/tours/helpers/TicketScreenTourMethods.js @@ -0,0 +1,107 @@ +odoo.define('point_of_sale.tour.TicketScreenTourMethods', function (require) { + 'use strict'; + + const { createTourMethods } = require('point_of_sale.tour.utils'); + + class Do { + clickNewTicket() { + return [{ trigger: '.ticket-screen .highlight' }]; + } + clickDiscard() { + return [{ trigger: '.ticket-screen button.discard' }]; + } + selectOrder(orderName) { + return [ + { + trigger: `.ticket-screen .order-row > .col:nth-child(2):contains("${orderName}")`, + }, + ]; + } + deleteOrder(orderName) { + return [ + { + trigger: `.ticket-screen .orders > .order-row > .col:contains("${orderName}") ~ .col[name="delete"]`, + }, + ]; + } + selectFilter(name) { + return [ + { + trigger: `.pos-search-bar .filter`, + }, + { + trigger: `.pos-search-bar .filter ul`, + run: () => {}, + }, + { + trigger: `.pos-search-bar .filter ul li:contains("${name}")`, + }, + ]; + } + search(field, searchWord) { + return [ + { + trigger: '.pos-search-bar input', + run: `text ${searchWord}`, + }, + { + /** + * Manually trigger keydown event to show the search field list + * because the previous step do not trigger keydown event. + */ + trigger: '.pos-search-bar input', + run: function () { + document + .querySelector('.pos-search-bar input') + .dispatchEvent(new KeyboardEvent('keydown', { key: '' })); + }, + }, + { + trigger: `.pos-search-bar .search ul li:contains("${field}")`, + }, + ]; + } + settleTips() { + return [ + { + trigger: '.ticket-screen .buttons .settle-tips', + }, + ]; + } + } + + class Check { + checkStatus(orderName, status) { + return [ + { + trigger: `.ticket-screen .order-row > .col:nth-child(2):contains("${orderName}") ~ .col:nth-child(6):contains(${status})`, + run: () => {}, + }, + ]; + } + /** + * Check if the nth row contains the given string. + * Note that 1st row is the header-row. + */ + nthRowContains(n, string) { + return [ + { + trigger: `.ticket-screen .orders > .order-row:nth-child(${n}):contains("${string}")`, + run: () => {}, + }, + ]; + } + noNewTicketButton() { + return [ + { + trigger: '.ticket-screen .controls .buttons:nth-child(1):has(.discard)', + run: () => {}, + }, + ]; + } + } + + class Execute {} + + return createTourMethods('TicketScreen', Do, Check, Execute); +}); diff --git a/addons/point_of_sale/static/tests/tours/helpers/utils.js b/addons/point_of_sale/static/tests/tours/helpers/utils.js new file mode 100644 index 00000000..e8fcc591 --- /dev/null +++ b/addons/point_of_sale/static/tests/tours/helpers/utils.js @@ -0,0 +1,153 @@ +odoo.define('point_of_sale.tour.utils', function (require) { + 'use strict'; + + const config = require('web.config'); + + /** + * USAGE + * ----- + * + * ``` + * const { startSteps, getSteps, createTourMethods } = require('point_of_sale.utils'); + * const { Other } = require('point_of_sale.tour.OtherMethods'); + * + * // 1. Define classes Do, Check and Execute having methods that + * // each return array of tour steps. + * class Do { + * click() { + * return [{ content: 'click button', trigger: '.button' }]; + * } + * } + * class Check { + * isHighligted() { + * return [{ content: 'button is highlighted', trigger: '.button.highlight', run: () => {} }]; + * } + * } + * // Notice that Execute has access to methods defined in Do and Check classes + * // Also, we can compose steps from other module. + * class Execute { + * complexSteps() { + * return [...this._do.click(), ...this._check.isHighlighted(), ...Other._exec.complicatedSteps()]; + * } + * } + * + * // 2. Instantiate these class definitions using `createTourMethods`. + * // The returned object gives access to the defined methods above + * // thru the do, check and exec properties. + * // - do gives access to the methods defined in Do class + * // - check gives access to the methods defined in Check class + * // - exec gives access to the methods defined in Execute class + * const Screen = createTourMethods('Screen', Do, Check, Execute); + * + * // 3. Call `startSteps` to start empty steps. + * startSteps(); + * + * // 4. Call the tour methods to populate the steps created by `startSteps`. + * Screen.do.click(); // return of this method call is added to steps created by startSteps + * Screen.check.isHighlighted() // same as above + * Screen.exec.complexSteps() // same as above + * + * // 5. Call `getSteps` which returns the generated tour steps. + * const steps = getSteps(); + * ``` + */ + let steps = []; + + function startSteps() { + // always start by waiting for loading to finish + steps = [ + { + content: 'wait for loading to finish', + trigger: 'body:not(:has(.loader))', + run: function () {}, + }, + ]; + } + + function getSteps() { + return steps; + } + + // this is the method decorator + // when the method is called, the generated steps are added + // to steps + const methodProxyHandler = { + apply(target, thisArg, args) { + const res = target.call(thisArg, ...args); + if (config.isDebug()) { + // This step is added before the real steps. + // Very useful when debugging because we know which + // method call failed and what were the parameters. + const constructor = thisArg.constructor.name.split(' ')[1]; + const methodName = target.name.split(' ')[1]; + const argList = args + .map((a) => (typeof a === 'string' ? `'${a}'` : `${a}`)) + .join(', '); + steps.push({ + content: `DOING "${constructor}.${methodName}(${argList})"`, + trigger: '.pos', + run: () => {}, + }); + } + steps.push(...res); + return res; + }, + }; + + // we proxy get of the method to decorate the method call + const proxyHandler = { + get(target, key) { + const method = target[key]; + if (!method) { + throw new Error(`Tour method '${key}' is not available.`); + } + return new Proxy(method.bind(target), methodProxyHandler); + }, + }; + + /** + * Creates an object with `do`, `check` and `exec` properties which are instances of + * the given `Do`, `Check` and `Execute` classes, respectively. Calling methods + * automatically adds the returned steps to the steps created by `startSteps`. + * + * There are however underscored version (_do, _check, _exec). + * Calling methods thru the underscored version does not automatically + * add the returned steps to the current steps array. Useful when composing + * steps from other methods. + * + * @param {String} name + * @param {Function} Do class containing methods which return array of tour steps + * @param {Function} Check similar to Do class but the steps are mainly for checking + * @param {Function} Execute class containing methods which return array of tour steps + * but has access to methods of Do and Check classes via .do and .check, + * respectively. Here, we define methods that return tour steps based + * on the combination of steps from Do and Check. + */ + function createTourMethods(name, Do, Check = class {}, Execute = class {}) { + Object.defineProperty(Do, 'name', { value: `${name}.do` }); + Object.defineProperty(Check, 'name', { value: `${name}.check` }); + Object.defineProperty(Execute, 'name', { + value: `${name}.exec`, + }); + const methods = { do: new Do(), check: new Check(), exec: new Execute() }; + // Allow Execute to have access to methods defined in Do and Check + // via do and exec, respectively. + methods.exec._do = methods.do; + methods.exec._check = methods.check; + return { + Do, + Check, + Execute, + [name]: { + do: new Proxy(methods.do, proxyHandler), + check: new Proxy(methods.check, proxyHandler), + exec: new Proxy(methods.exec, proxyHandler), + _do: methods.do, + _check: methods.check, + _exec: methods.exec, + }, + }; + } + + return { startSteps, getSteps, createTourMethods }; +}); diff --git a/addons/point_of_sale/static/tests/tours/point_of_sale.js b/addons/point_of_sale/static/tests/tours/point_of_sale.js new file mode 100644 index 00000000..25f88d3a --- /dev/null +++ b/addons/point_of_sale/static/tests/tours/point_of_sale.js @@ -0,0 +1,436 @@ +odoo.define('point_of_sale.tour.pricelist', function (require) { + "use strict"; + + var Tour = require('web_tour.tour'); + var rpc = require('web.rpc'); + var utils = require('web.utils'); + var round_di = utils.round_decimals; + + function assert (condition, message) { + if (! condition) { + throw message || "Assertion failed"; + } + } + + function _build_pricelist_context (pricelist, quantity, date) { + return { + pricelist: pricelist.id, + quantity: quantity, + }; + } + + function compare_backend_frontend (product, pricelist_name, quantity) { + return function () { + var pricelist = _.findWhere(posmodel.pricelists, {name: pricelist_name}); + var frontend_price = product.get_price(pricelist, quantity); + // ORM applies digits= on non-stored computed field when + // reading. It does not however truncate like it does when + // storing the field. + frontend_price = round_di(frontend_price, posmodel.dp['Product Price']); + + var context = _build_pricelist_context(pricelist, quantity); + return rpc.query({model: 'product.product', method: 'read', args: [[product.id], ['price']], context: context}) + .then(function (backend_result) { + var debug_info = _.extend(context, { + product: product.id, + product_display_name: product.display_name, + pricelist_name: pricelist.name, + }); + var backend_price = backend_result[0].price; + assert(frontend_price === backend_price, + JSON.stringify(debug_info) + ' DOESN\'T MATCH -> ' + backend_price + ' (backend) != ' + frontend_price + ' (frontend)'); + return Promise.resolve(); + }); + }; + } + + // The global posmodel is only present when the posmodel is instanciated + // So, wait for everythiong to be loaded + var steps = [{ // Leave category displayed by default + content: 'waiting for loading to finish', + extra_trigger: 'body .pos:not(:has(.loader))', // Pos has finished loading + trigger: 'body:not(.oe_wait)', // WebClient has finished Loading + run: function () { + var product_wall_shelf = posmodel.db.search_product_in_category(0, 'Wall Shelf Unit')[0]; + var product_small_shelf = posmodel.db.search_product_in_category(0, 'Small Shelf')[0]; + var product_magnetic_board = posmodel.db.search_product_in_category(0, 'Magnetic Board')[0]; + var product_monitor_stand = posmodel.db.search_product_in_category(0, 'Monitor Stand')[0]; + var product_desk_pad = posmodel.db.search_product_in_category(0, 'Desk Pad')[0]; + var product_letter_tray = posmodel.db.search_product_in_category(0, 'Letter Tray')[0]; + var product_whiteboard = posmodel.db.search_product_in_category(0, 'Whiteboard')[0]; + + compare_backend_frontend(product_letter_tray, 'Public Pricelist', 0, undefined)() + .then(compare_backend_frontend(product_letter_tray, 'Public Pricelist', 1, undefined)) + .then(compare_backend_frontend(product_letter_tray, 'Fixed', 1, undefined)) + .then(compare_backend_frontend(product_wall_shelf, 'Fixed', 1, undefined)) + .then(compare_backend_frontend(product_small_shelf, 'Fixed', 1, undefined)) + .then(compare_backend_frontend(product_wall_shelf, 'Percentage', 1, undefined)) + .then(compare_backend_frontend(product_small_shelf, 'Percentage', 1, undefined)) + .then(compare_backend_frontend(product_magnetic_board, 'Percentage', 1, undefined)) + .then(compare_backend_frontend(product_wall_shelf, 'Formula', 1, undefined)) + .then(compare_backend_frontend(product_small_shelf, 'Formula', 1, undefined)) + .then(compare_backend_frontend(product_magnetic_board, 'Formula', 1, undefined)) + .then(compare_backend_frontend(product_monitor_stand, 'Formula', 1, undefined)) + .then(compare_backend_frontend(product_desk_pad, 'Formula', 1, undefined)) + .then(compare_backend_frontend(product_wall_shelf, 'min_quantity ordering', 1, undefined)) + .then(compare_backend_frontend(product_wall_shelf, 'min_quantity ordering', 2, undefined)) + .then(compare_backend_frontend(product_letter_tray, 'Category vs no category', 1, undefined)) + .then(compare_backend_frontend(product_letter_tray, 'Category', 1, undefined)) + .then(compare_backend_frontend(product_wall_shelf, 'Product template', 1, undefined)) + .then(compare_backend_frontend(product_wall_shelf, 'Dates', 1, undefined)) + .then(compare_backend_frontend(product_small_shelf, 'Pricelist base rounding', 1, undefined)) + .then(compare_backend_frontend(product_whiteboard, 'Public Pricelist', 1, undefined)) + .then(function () { + $('.pos').addClass('done-testing'); + }); + }, + }]; + + steps = steps.concat([{ + content: "wait for unit tests to finish", + trigger: ".pos.done-testing", + run: function () {}, // it's a check + }, { + content: "click category switch", + trigger: ".breadcrumb-home", + run: 'click', + }, { + content: "click pricelist button", + trigger: ".control-button.o_pricelist_button", + }, { + content: "verify default pricelist is set", + trigger: ".selection-item.selected:contains('Public Pricelist')", + run: function () {}, // it's a check + }, { + content: "select fixed pricelist", + trigger: ".selection-item:contains('Fixed')", + }, { + content: "open customer list", + trigger: "button.set-customer", + }, { + content: "select Deco Addict", + trigger: ".client-line:contains('Deco Addict')", + }, { + content: "confirm selection", + trigger: ".clientlist-screen .next", + }, { + content: "click pricelist button", + trigger: ".control-button.o_pricelist_button", + }, { + content: "verify pricelist changed", + trigger: ".selection-item.selected:contains('Public Pricelist')", + run: function () {}, // it's a check + }, { + content: "cancel pricelist dialog", + trigger: ".button.cancel:visible", + }, { + content: "open customer list", + trigger: "button.set-customer", + }, { + content: "select Lumber Inc", + trigger: ".client-line:contains('Lumber Inc')", + }, { + content: "confirm selection", + trigger: ".clientlist-screen .next", + }, { + content: "click pricelist button", + trigger: ".control-button.o_pricelist_button", + }, { + content: "verify pricelist remained public pricelist ('Not loaded' is not available)", + trigger: ".selection-item.selected:contains('Public Pricelist')", + run: function () {}, // it's a check + }, { + content: "cancel pricelist dialog", + trigger: ".button.cancel:visible", + }, { + content: "click pricelist button", + trigger: ".control-button.o_pricelist_button", + }, { + content: "select fixed pricelist", + trigger: ".selection-item:contains('min_quantity ordering')", + }, { + content: "order 1 kg shelf", + trigger: ".product:contains('Wall Shelf')", + }, { + content: "change qty to 2 kg", + trigger: ".numpad button.input-button:visible:contains('2')", + }, { + content: "qty of Wall Shelf line should be 2", + trigger: ".order-container .orderlines .orderline.selected .product-name:contains('Wall Shelf')", + extra_trigger: ".order-container .orderlines .orderline.selected .product-name:contains('Wall Shelf') ~ .info-list .info em:contains('2.0')", + run: function() {}, + }, { + content: "verify that unit price of shelf changed to $1", + trigger: ".total > .value:contains('$ 2.00')", + run: function() {}, + }, { + content: "order different shelf", + trigger: ".product:contains('Small Shelf')", + }, { + content: "Small Shelf line should be selected with quantity 1", + trigger: ".order-container .orderlines .orderline.selected .product-name:contains('Small Shelf')", + extra_trigger: ".order-container .orderlines .orderline.selected .product-name:contains('Small Shelf') ~ .info-list .info em:contains('1.0')", + run: function() {} + }, { + content: "change to price mode", + trigger: ".numpad button:contains('Price')", + }, { + content: "make sure price mode is activated", + trigger: ".numpad button.selected-mode:contains('Price')", + run: function() {}, + }, { + content: "manually override the unit price of these shelf to $5", + trigger: ".numpad button.input-button:visible:contains('5')", + }, { + content: "Small Shelf line should be selected with unit price of 5", + trigger: ".order-container .orderlines .orderline.selected .product-name:contains('Small Shelf')", + extra_trigger: ".order-container .orderlines .orderline.selected .product-name:contains('Small Shelf') ~ .price:contains('5.0')", + }, { + content: "change back to qty mode", + trigger: ".numpad button:contains('Qty')", + }, { + content: "make sure qty mode is activated", + trigger: ".numpad button.selected-mode:contains('Qty')", + run: function() {}, + }, { + content: "click pricelist button", + trigger: ".control-button.o_pricelist_button", + }, { + content: "select public pricelist", + trigger: ".selection-item:contains('Public Pricelist')", + }, { + content: "verify that the boni shelf have been recomputed and the shelf have not (their price was manually overridden)", + trigger: ".total > .value:contains('$ 8.96')", + }, { + content: "click pricelist button", + trigger: ".control-button.o_pricelist_button", + }, { + content: "select fixed pricelist", + trigger: ".selection-item:contains('min_quantity ordering')", + }, { + content: "close the Point of Sale frontend", + trigger: ".header-button", + }, { + content: "confirm closing the frontend", + trigger: ".header-button", + run: function() {}, //it's a check, + }]); + + Tour.register('pos_pricelist', { test: true, url: '/pos/ui' }, steps); +}); + +odoo.define('point_of_sale.tour.acceptance', function (require) { + "use strict"; + + var Tour = require("web_tour.tour"); + + function add_product_to_order(product_name) { + return [{ + content: 'buy ' + product_name, + trigger: '.product-list .product-name:contains("' + product_name + '")', + }, { + content: 'the ' + product_name + ' have been added to the order', + trigger: '.order .product-name:contains("' + product_name + '")', + run: function () {}, + }]; + } + + function set_fiscal_position_on_order(fp_name) { + return [{ + content: 'set fiscal position', + trigger: '.control-button.o_fiscal_position_button', + }, { + content: 'choose fiscal position ' + fp_name + ' to add to the order', + trigger: '.popups .popup .selection .selection-item:contains("' + fp_name + '")', + }, { + content: 'the fiscal position ' + fp_name + ' has been set to the order', + trigger: '.control-button.o_fiscal_position_button:contains("' + fp_name + '")', + run: function () {}, + }]; + } + + function generate_keypad_steps(amount_str, keypad_selector) { + var i, steps = [], current_char; + for (i = 0; i < amount_str.length; ++i) { + current_char = amount_str[i]; + steps.push({ + content: 'press ' + current_char + ' on payment keypad', + trigger: keypad_selector + ' .input-button:contains("' + current_char + '"):visible' + }); + } + return steps; + } + + function press_payment_numpad(val) { + return [{ + content: `press ${val} on payment screen numpad`, + trigger: `.payment-numpad .input-button:contains("${val}"):visible`, + }] + } + + function press_product_numpad(val) { + return [{ + content: `press ${val} on product screen numpad`, + trigger: `.numpad .input-button:contains("${val}"):visible`, + }] + } + + function selected_payment_has(name, val) { + return [{ + content: `selected payment is ${name} and has ${val}`, + trigger: `.paymentlines .paymentline.selected .payment-name:contains("${name}")`, + extra_trigger: `.paymentlines .paymentline.selected .payment-name:contains("${name}") ~ .payment-amount:contains("${val}")`, + run: function () {}, + }] + } + + function selected_orderline_has({ product, price = null, quantity = null }) { + const result = []; + if (price !== null) { + result.push({ + content: `Selected line has product '${product}' and price '${price}'`, + trigger: `.order-container .orderlines .orderline.selected .product-name:contains("${product}") ~ span.price:contains("${price}")`, + run: function () {}, + }); + } + if (quantity !== null) { + result.push({ + content: `Selected line has product '${product}' and quantity '${quantity}'`, + trigger: `.order-container .orderlines .orderline.selected .product-name:contains('${product}') ~ .info-list .info em:contains('${quantity}')`, + run: function () {}, + }); + } + return result; + } + + function verify_order_total(total_str) { + return [{ + content: 'order total contains ' + total_str, + trigger: '.order .total .value:contains("' + total_str + '")', + run: function () {}, // it's a check + }]; + } + + function goto_payment_screen_and_select_payment_method() { + return [{ + content: "go to payment screen", + trigger: '.button.pay', + }, { + content: "pay with cash", + trigger: '.paymentmethod:contains("Cash")', + }]; + } + + function finish_order() { + return [{ + content: "validate the order", + trigger: '.payment-screen .button.next.highlight:visible', + }, { + content: "verify that the order has been successfully sent to the backend", + trigger: ".js_connected:visible", + run: function () {}, + }, { + content: "click Next Order", + trigger: '.receipt-screen .button.next.highlight:visible', + }, { + content: "check if we left the receipt screen", + trigger: '.pos-content .screen:not(:has(.receipt-screen))', + run: function () {}, + }]; + } + + var steps = [{ + content: 'waiting for loading to finish', + trigger: 'body:not(:has(.loader))', + run: function () {}, + }, { // Leave category displayed by default + content: "click category switch", + trigger: ".breadcrumb-home", + }]; + + steps = steps.concat(add_product_to_order('Desk Organizer')); + steps = steps.concat(verify_order_total('5.10')); + + steps = steps.concat(add_product_to_order('Desk Organizer')); + steps = steps.concat(verify_order_total('10.20')); + steps = steps.concat(goto_payment_screen_and_select_payment_method()); + + /* add payment line of only 5.20 + status: + order-total := 10.20 + total-payment := 11.70 + expect: + remaining := 0.00 + change := 1.50 + */ + steps = steps.concat(press_payment_numpad('5')); + steps = steps.concat(selected_payment_has('Cash', '5.0')); + steps = steps.concat([{ + content: "verify remaining", + trigger: '.payment-status-remaining .amount:contains("5.20")', + run: function () {}, + }, { + content: "verify change", + trigger: '.payment-status-change .amount:contains("0.00")', + run: function () {}, + }]); + + /* make additional payment line of 6.50 + status: + order-total := 10.20 + total-payment := 11.70 + expect: + remaining := 0.00 + change := 1.50 + */ + steps = steps.concat([{ + content: "pay with cash", + trigger: '.paymentmethod:contains("Cash")', + }]); + steps = steps.concat(selected_payment_has('Cash', '5.2')); + steps = steps.concat(press_payment_numpad('6')) + steps = steps.concat(selected_payment_has('Cash', '6.0')); + steps = steps.concat([{ + content: "verify remaining", + trigger: '.payment-status-remaining .amount:contains("0.00")', + run: function () {}, + }, { + content: "verify change", + trigger: '.payment-status-change .amount:contains("0.80")', + run: function () {}, + }]); + + steps = steps.concat(finish_order()); + + // test opw-672118 orderline subtotal rounding + steps = steps.concat(add_product_to_order('Desk Organizer')); + steps = steps.concat(selected_orderline_has({product: 'Desk Organizer', quantity: '1.0'})); + steps = steps.concat(press_product_numpad('.')) + steps = steps.concat(selected_orderline_has({product: 'Desk Organizer', quantity: '0.0', price: '0.0'})); + steps = steps.concat(press_product_numpad('9')) + steps = steps.concat(selected_orderline_has({product: 'Desk Organizer', quantity: '0.9', price: '4.59'})); + steps = steps.concat(press_product_numpad('9')) + steps = steps.concat(selected_orderline_has({product: 'Desk Organizer', quantity: '0.99', price: '5.05'})); + steps = steps.concat(goto_payment_screen_and_select_payment_method()); + steps = steps.concat(selected_payment_has('Cash', '5.05')); + steps = steps.concat(finish_order()); + + // Test fiscal position one2many map (align with backend) + steps = steps.concat(add_product_to_order('Letter Tray')); + steps = steps.concat(selected_orderline_has({product: 'Letter Tray', quantity: '1.0'})); + steps = steps.concat(verify_order_total('5.28')); + steps = steps.concat(set_fiscal_position_on_order('FP-POS-2M')); + steps = steps.concat(verify_order_total('5.52')); + + steps = steps.concat([{ + content: "close the Point of Sale frontend", + trigger: ".header-button", + }, { + content: "confirm closing the frontend", + trigger: ".header-button.confirm", + run: function() {}, //it's a check, + }]); + + Tour.register('pos_basic_order', { test: true, url: '/pos/ui' }, steps); + +}); |
