diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/point_of_sale/static/tests/tours/helpers | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/point_of_sale/static/tests/tours/helpers')
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 }; +}); |
