summaryrefslogtreecommitdiff
path: root/addons/point_of_sale/static/tests/tours/helpers
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/helpers
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/point_of_sale/static/tests/tours/helpers')
-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
13 files changed, 1328 insertions, 0 deletions
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 };
+});