summaryrefslogtreecommitdiff
path: root/addons/point_of_sale/static/tests/tours
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
commit3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch)
treea44932296ef4a9b71d5f010906253d8c53727726 /addons/point_of_sale/static/tests/tours
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/point_of_sale/static/tests/tours')
-rw-r--r--addons/point_of_sale/static/tests/tours/Chrome.tour.js103
-rw-r--r--addons/point_of_sale/static/tests/tours/OrderManagementScreen.tour.js138
-rw-r--r--addons/point_of_sale/static/tests/tours/PaymentScreen.tour.js70
-rw-r--r--addons/point_of_sale/static/tests/tours/ProductConfigurator.tour.js66
-rw-r--r--addons/point_of_sale/static/tests/tours/ProductScreen.tour.js105
-rw-r--r--addons/point_of_sale/static/tests/tours/ReceiptScreen.tour.js61
-rw-r--r--addons/point_of_sale/static/tests/tours/TicketScreen.tour.js54
-rw-r--r--addons/point_of_sale/static/tests/tours/helpers/ChromeTourMethods.js42
-rw-r--r--addons/point_of_sale/static/tests/tours/helpers/ClientListScreenTourMethods.js57
-rw-r--r--addons/point_of_sale/static/tests/tours/helpers/CompositeTourMethods.js23
-rw-r--r--addons/point_of_sale/static/tests/tours/helpers/ErrorPopupTourMethods.js30
-rw-r--r--addons/point_of_sale/static/tests/tours/helpers/NumberPopupTourMethods.js72
-rw-r--r--addons/point_of_sale/static/tests/tours/helpers/OrderManagementScreenTourMethods.js180
-rw-r--r--addons/point_of_sale/static/tests/tours/helpers/PaymentScreenTourMethods.js215
-rw-r--r--addons/point_of_sale/static/tests/tours/helpers/ProductConfiguratorTourMethods.js77
-rw-r--r--addons/point_of_sale/static/tests/tours/helpers/ProductScreenTourMethods.js254
-rw-r--r--addons/point_of_sale/static/tests/tours/helpers/ReceiptScreenTourMethods.js79
-rw-r--r--addons/point_of_sale/static/tests/tours/helpers/SelectionPopupTourMethods.js39
-rw-r--r--addons/point_of_sale/static/tests/tours/helpers/TicketScreenTourMethods.js107
-rw-r--r--addons/point_of_sale/static/tests/tours/helpers/utils.js153
-rw-r--r--addons/point_of_sale/static/tests/tours/point_of_sale.js436
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);
+
+});