summaryrefslogtreecommitdiff
path: root/addons/point_of_sale/static/tests/unit
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/unit
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/point_of_sale/static/tests/unit')
-rw-r--r--addons/point_of_sale/static/tests/unit/helpers/test_env.js46
-rw-r--r--addons/point_of_sale/static/tests/unit/helpers/test_main.js23
-rw-r--r--addons/point_of_sale/static/tests/unit/test_ChromeWidgets.js89
-rw-r--r--addons/point_of_sale/static/tests/unit/test_ComponentRegistry.js414
-rw-r--r--addons/point_of_sale/static/tests/unit/test_NumberBuffer.js65
-rw-r--r--addons/point_of_sale/static/tests/unit/test_PaymentScreen.js309
-rw-r--r--addons/point_of_sale/static/tests/unit/test_ProductScreen.js603
-rw-r--r--addons/point_of_sale/static/tests/unit/test_popups.js180
8 files changed, 1729 insertions, 0 deletions
diff --git a/addons/point_of_sale/static/tests/unit/helpers/test_env.js b/addons/point_of_sale/static/tests/unit/helpers/test_env.js
new file mode 100644
index 00000000..c4b0b3ec
--- /dev/null
+++ b/addons/point_of_sale/static/tests/unit/helpers/test_env.js
@@ -0,0 +1,46 @@
+odoo.define('point_of_sale.test_env', async function (require) {
+ 'use strict';
+
+ /**
+ * Many components in PoS are dependent on the PosModel instance (pos).
+ * Therefore, for unit tests that require pos in the Components' env, we
+ * prepared here a test env maker (makePosTestEnv) based on
+ * makeTestEnvironment of web.
+ */
+
+ const makeTestEnvironment = require('web.test_env');
+ const env = require('web.env');
+ const models = require('point_of_sale.models');
+ const Registries = require('point_of_sale.Registries');
+
+ Registries.Component.add(owl.misc.Portal);
+
+ await env.session.is_bound;
+ const pos = new models.PosModel({
+ rpc: env.services.rpc,
+ session: env.session,
+ do_action: async () => {},
+ setLoadingMessage: () => {},
+ setLoadingProgress: () => {},
+ showLoadingSkip: () => {},
+ });
+ await pos.ready;
+
+ /**
+ * @param {Object} env default env
+ * @param {Function} providedRPC mock rpc
+ * @param {Function} providedDoAction mock do_action
+ */
+ function makePosTestEnv(env = {}, providedRPC = null, providedDoAction = null) {
+ env = Object.assign(env, { pos });
+ let posEnv = makeTestEnvironment(env, providedRPC);
+ // Replace rpc in the PosModel instance after loading
+ // data from the server so that every succeeding rpc calls
+ // made by pos are mocked by the providedRPC.
+ pos.rpc = posEnv.rpc;
+ pos.do_action = providedDoAction;
+ return posEnv;
+ }
+
+ return makePosTestEnv;
+});
diff --git a/addons/point_of_sale/static/tests/unit/helpers/test_main.js b/addons/point_of_sale/static/tests/unit/helpers/test_main.js
new file mode 100644
index 00000000..f42e01cb
--- /dev/null
+++ b/addons/point_of_sale/static/tests/unit/helpers/test_main.js
@@ -0,0 +1,23 @@
+odoo.define('web.web_client', function (require) {
+ // this module is required by the test
+ const { bus } = require('web.core');
+ const WebClient = require('web.AbstractWebClient');
+
+ // listen to unhandled rejected promises, and when the rejection is not due
+ // to a crash, prevent the browser from displaying an 'unhandledrejection'
+ // error in the console, which would make tests crash on each Promise.reject()
+ // something similar is done by the CrashManagerService, but by default, it
+ // isn't deployed in tests
+ bus.on('crash_manager_unhandledrejection', this, function (ev) {
+ if (!ev.reason || !(ev.reason instanceof Error)) {
+ ev.stopPropagation();
+ ev.stopImmediatePropagation();
+ ev.preventDefault();
+ }
+ });
+
+ owl.config.mode = "dev";
+
+ const webClient = new WebClient();
+ return webClient;
+});
diff --git a/addons/point_of_sale/static/tests/unit/test_ChromeWidgets.js b/addons/point_of_sale/static/tests/unit/test_ChromeWidgets.js
new file mode 100644
index 00000000..a0df97fd
--- /dev/null
+++ b/addons/point_of_sale/static/tests/unit/test_ChromeWidgets.js
@@ -0,0 +1,89 @@
+odoo.define('point_of_sale.tests.ChromeWidgets', function (require) {
+ 'use strict';
+
+ const PosComponent = require('point_of_sale.PosComponent');
+ const PopupControllerMixin = require('point_of_sale.PopupControllerMixin');
+ const testUtils = require('web.test_utils');
+ const makePosTestEnv = require('point_of_sale.test_env');
+ const { xml } = owl.tags;
+
+ QUnit.module('unit tests for Chrome Widgets', {});
+
+ QUnit.test('CashierName', async function (assert) {
+ assert.expect(1);
+
+ class Parent extends PosComponent {}
+ Parent.env = makePosTestEnv();
+ Parent.template = xml/* html */ `
+ <div><CashierName></CashierName></div>
+ `;
+ Parent.env.pos.employee.name = 'Test Employee';
+
+ const parent = new Parent();
+ await parent.mount(testUtils.prepareTarget());
+
+ assert.strictEqual(parent.el.querySelector('span.username').innerText, 'Test Employee');
+
+ parent.unmount();
+ parent.destroy();
+ });
+
+ QUnit.test('HeaderButton', async function (assert) {
+ assert.expect(1);
+
+ class Parent extends PosComponent {}
+ Parent.env = makePosTestEnv();
+ Parent.template = xml/* html */ `
+ <div><HeaderButton></HeaderButton></div>
+ `;
+
+ const parent = new Parent();
+ await parent.mount(testUtils.prepareTarget());
+
+ const headerButton = parent.el.querySelector('.header-button');
+ await testUtils.dom.click(headerButton);
+ await testUtils.nextTick();
+ assert.ok(headerButton.classList.contains('confirm'));
+
+ parent.unmount();
+ parent.destroy();
+ });
+
+ QUnit.test('SyncNotification', async function (assert) {
+ assert.expect(5);
+
+ class Parent extends PosComponent {}
+ Parent.env = makePosTestEnv();
+ Parent.template = xml/* html */ `
+ <div>
+ <SyncNotification></SyncNotification>
+ </div>
+ `;
+
+ const pos = Parent.env.pos;
+ pos.set('synch', { status: 'connected', pending: false });
+
+ const parent = new Parent();
+ await parent.mount(testUtils.prepareTarget());
+ assert.ok(parent.el.querySelector('i.fa').parentElement.classList.contains('js_connected'));
+
+ pos.set('synch', { status: 'connecting', pending: false });
+ await testUtils.nextTick();
+ assert.ok(parent.el.querySelector('i.fa').parentElement.classList.contains('js_connecting'));
+
+ pos.set('synch', { status: 'disconnected', pending: false });
+ await testUtils.nextTick();
+ assert.ok(parent.el.querySelector('i.fa').parentElement.classList.contains('js_disconnected'));
+
+ pos.set('synch', { status: 'error', pending: false });
+ await testUtils.nextTick();
+ assert.ok(parent.el.querySelector('i.fa').parentElement.classList.contains('js_error'));
+
+ pos.set('synch', { status: 'error', pending: 10 });
+ await testUtils.nextTick();
+ assert.ok(parent.el.querySelector('.js_msg').innerText.includes('10'));
+
+ parent.unmount();
+ parent.destroy();
+ });
+});
diff --git a/addons/point_of_sale/static/tests/unit/test_ComponentRegistry.js b/addons/point_of_sale/static/tests/unit/test_ComponentRegistry.js
new file mode 100644
index 00000000..4b2217cb
--- /dev/null
+++ b/addons/point_of_sale/static/tests/unit/test_ComponentRegistry.js
@@ -0,0 +1,414 @@
+odoo.define('point_of_sale.tests.ComponentRegistry', function(require) {
+ 'use strict';
+
+ const Registries = require('point_of_sale.Registries');
+
+ QUnit.module('unit tests for ComponentRegistry', {
+ before() {},
+ });
+
+ QUnit.test('basic extend', async function(assert) {
+ assert.expect(5);
+
+ class A {
+ constructor() {
+ assert.step('A');
+ }
+ }
+ Registries.Component.add(A);
+
+ let A1 = x =>
+ class extends x {
+ constructor() {
+ super();
+ assert.step('A1');
+ }
+ };
+ Registries.Component.extend(A, A1);
+
+ Registries.Component.freeze();
+
+ const RegA = Registries.Component.get(A);
+ let a = new RegA();
+ assert.verifySteps(['A', 'A1']);
+ assert.ok(a instanceof RegA);
+ assert.ok(RegA.name === 'A');
+ });
+
+ QUnit.test('addByExtending', async function(assert) {
+ assert.expect(8);
+
+ class A {
+ constructor() {
+ assert.step('A');
+ }
+ }
+ Registries.Component.add(A);
+
+ let B = x =>
+ class extends x {
+ constructor() {
+ super();
+ assert.step('B');
+ }
+ };
+ Registries.Component.addByExtending(B, A);
+
+ let A1 = x =>
+ class extends x {
+ constructor() {
+ super();
+ assert.step('A1');
+ }
+ };
+ Registries.Component.extend(A, A1);
+
+ let A2 = x =>
+ class extends x {
+ constructor() {
+ super();
+ assert.step('A2');
+ }
+ };
+ Registries.Component.extend(A, A2);
+
+ Registries.Component.freeze();
+
+ const RegA = Registries.Component.get(A);
+ const RegB = Registries.Component.get(B);
+ let b = new RegB();
+ assert.verifySteps(['A', 'A1', 'A2', 'B']);
+ assert.ok(b instanceof RegA);
+ assert.ok(b instanceof RegB);
+ assert.ok(RegB.name === 'B');
+ });
+
+ QUnit.test('extend the one that is added by extending', async function(assert) {
+ assert.expect(6);
+
+ class A {
+ constructor() {
+ assert.step('A');
+ }
+ }
+ Registries.Component.add(A);
+
+ let B = x =>
+ class extends x {
+ constructor() {
+ super();
+ assert.step('B');
+ }
+ };
+ Registries.Component.addByExtending(B, A);
+
+ let B1 = x =>
+ class extends x {
+ constructor() {
+ super();
+ assert.step('B1');
+ }
+ };
+ Registries.Component.extend(B, B1);
+
+ let B2 = x =>
+ class extends x {
+ constructor() {
+ super();
+ assert.step('B2');
+ }
+ };
+ Registries.Component.extend(B, B2);
+
+ let A1 = x =>
+ class extends x {
+ constructor() {
+ super();
+ assert.step('A1');
+ }
+ };
+ Registries.Component.extend(A, A1);
+
+ Registries.Component.freeze();
+
+ const RegB = Registries.Component.get(B);
+ new RegB();
+ assert.verifySteps(['A', 'A1', 'B', 'B1', 'B2']);
+ });
+
+ QUnit.test('addByExtending based on added by extending', async function(assert) {
+ assert.expect(10);
+
+ class A {
+ constructor() {
+ assert.step('A');
+ }
+ }
+ Registries.Component.add(A);
+
+ let B = x =>
+ class extends x {
+ constructor() {
+ super();
+ assert.step('B');
+ }
+ };
+ Registries.Component.addByExtending(B, A);
+
+ let A1 = x =>
+ class extends x {
+ constructor() {
+ super();
+ assert.step('A1');
+ }
+ };
+ Registries.Component.extend(A, A1);
+
+ let C = x =>
+ class extends x {
+ constructor() {
+ super();
+ assert.step('C');
+ }
+ };
+ Registries.Component.addByExtending(C, B);
+
+ let B7 = x =>
+ class extends x {
+ constructor() {
+ super();
+ assert.step('B7');
+ }
+ };
+ Registries.Component.extend(B, B7);
+
+ Registries.Component.freeze();
+
+ const RegA = Registries.Component.get(A);
+ const RegB = Registries.Component.get(B);
+ const RegC = Registries.Component.get(C);
+ let c = new RegC();
+ assert.verifySteps(['A', 'A1', 'B', 'B7', 'C']);
+ assert.ok(c instanceof RegA);
+ assert.ok(c instanceof RegB);
+ assert.ok(c instanceof RegC);
+ assert.ok(RegC.name === 'C');
+ });
+
+ QUnit.test('deeper inheritance', async function(assert) {
+ assert.expect(9);
+
+ class A {
+ constructor() {
+ assert.step('A');
+ }
+ }
+ Registries.Component.add(A);
+
+ let B = x =>
+ class extends x {
+ constructor() {
+ super();
+ assert.step('B');
+ }
+ };
+ Registries.Component.addByExtending(B, A);
+
+ let A1 = x =>
+ class extends x {
+ constructor() {
+ super();
+ assert.step('A1');
+ }
+ };
+ Registries.Component.extend(A, A1);
+
+ let C = x =>
+ class extends x {
+ constructor() {
+ super();
+ assert.step('C');
+ }
+ };
+ Registries.Component.addByExtending(C, B);
+
+ let B2 = x =>
+ class extends x {
+ constructor() {
+ super();
+ assert.step('B2');
+ }
+ };
+ Registries.Component.extend(B, B2);
+
+ let B3 = x =>
+ class extends x {
+ constructor() {
+ super();
+ assert.step('B3');
+ }
+ };
+ Registries.Component.extend(B, B3);
+
+ let A9 = x =>
+ class extends x {
+ constructor() {
+ super();
+ assert.step('A9');
+ }
+ };
+ Registries.Component.extend(A, A9);
+
+ let E = x =>
+ class extends x {
+ constructor() {
+ super();
+ assert.step('E');
+ }
+ };
+ Registries.Component.addByExtending(E, C);
+
+ Registries.Component.freeze();
+
+ // |A| => A9 -> A1 -> A
+ // |B| => B3 -> B2 -> B -> |A|
+ // |C| => C -> |B|
+ // |E| => E -> |C|
+
+ new (Registries.Component.get(E))();
+ assert.verifySteps(['A', 'A1', 'A9', 'B', 'B2', 'B3', 'C', 'E']);
+ });
+
+ QUnit.test('mixins?', async function(assert) {
+ assert.expect(12);
+
+ class A {
+ constructor() {
+ assert.step('A');
+ }
+ }
+ Registries.Component.add(A);
+
+ let Mixin = x =>
+ class extends x {
+ constructor() {
+ super();
+ assert.step('Mixin');
+ }
+ mixinMethod() {
+ return 'mixinMethod';
+ }
+ get mixinGetter() {
+ return 'mixinGetter';
+ }
+ };
+
+ // use the mixin when declaring B.
+ let B = x =>
+ class extends Mixin(x) {
+ constructor() {
+ super();
+ assert.step('B');
+ }
+ };
+ Registries.Component.addByExtending(B, A);
+
+ let A1 = x =>
+ class extends x {
+ constructor() {
+ super();
+ assert.step('A1');
+ }
+ };
+ Registries.Component.extend(A, A1);
+
+ Registries.Component.freeze();
+
+ B = Registries.Component.get(B);
+ const b = new B();
+ assert.verifySteps(['A', 'A1', 'Mixin', 'B']);
+ // instance of B should have the mixin properties
+ assert.strictEqual(b.mixinMethod(), 'mixinMethod');
+ assert.strictEqual(b.mixinGetter, 'mixinGetter');
+
+ // instance of A should not have the mixin properties
+ A = Registries.Component.get(A);
+ const a = new A();
+ assert.verifySteps(['A', 'A1']);
+ assert.notOk(a.mixinMethod);
+ assert.notOk(a.mixinGetter);
+ });
+
+ QUnit.test('extending methods', async function(assert) {
+ assert.expect(16);
+
+ class A {
+ foo() {
+ assert.step('A foo');
+ }
+ }
+ Registries.Component.add(A);
+
+ let B = x =>
+ class extends x {
+ bar() {
+ assert.step('B bar');
+ }
+ };
+ Registries.Component.addByExtending(B, A);
+
+ let A1 = x =>
+ class extends x {
+ bar() {
+ assert.step('A1 bar');
+ // should only be for A.
+ }
+ };
+ Registries.Component.extend(A, A1);
+
+ let B1 = x =>
+ class extends x {
+ foo() {
+ super.foo();
+ assert.step('B1 foo');
+ }
+ };
+ Registries.Component.extend(B, B1);
+
+ let C = x =>
+ class extends x {
+ foo() {
+ super.foo();
+ assert.step('C foo');
+ }
+ bar() {
+ super.bar();
+ assert.step('C bar');
+ }
+ };
+ Registries.Component.addByExtending(C, B);
+
+ Registries.Component.freeze();
+
+ A = Registries.Component.get(A);
+ B = Registries.Component.get(B);
+ C = Registries.Component.get(C);
+ const a = new A();
+ const b = new B();
+ const c = new C();
+
+ a.foo();
+ assert.verifySteps(['A foo']);
+ b.foo();
+ assert.verifySteps(['A foo', 'B1 foo']);
+ c.foo();
+ assert.verifySteps(['A foo', 'B1 foo', 'C foo']);
+
+ a.bar();
+ assert.verifySteps(['A1 bar']);
+ b.bar();
+ assert.verifySteps(['B bar']);
+ c.bar();
+ assert.verifySteps(['B bar', 'C bar']);
+ });
+});
diff --git a/addons/point_of_sale/static/tests/unit/test_NumberBuffer.js b/addons/point_of_sale/static/tests/unit/test_NumberBuffer.js
new file mode 100644
index 00000000..1e9da1e6
--- /dev/null
+++ b/addons/point_of_sale/static/tests/unit/test_NumberBuffer.js
@@ -0,0 +1,65 @@
+odoo.define('point_of_sale.tests.NumberBuffer', function(require) {
+ 'use strict';
+
+ const { Component, useState } = owl;
+ const { xml } = owl.tags;
+ const NumberBuffer = require('point_of_sale.NumberBuffer');
+ const makeTestEnvironment = require('web.test_env');
+ const testUtils = require('web.test_utils');
+
+ QUnit.module('unit tests for NumberBuffer', {
+ before() {},
+ });
+
+ QUnit.test('simple fast inputs with capture in between', async function(assert) {
+ assert.expect(3);
+
+ class Root extends Component {
+ constructor() {
+ super();
+ this.state = useState({ buffer: '' });
+ NumberBuffer.activate();
+ NumberBuffer.use({
+ nonKeyboardInputEvent: 'numpad-click-input',
+ state: this.state,
+ });
+ }
+ resetBuffer() {
+ NumberBuffer.capture();
+ NumberBuffer.reset();
+ }
+ }
+ Root.env = makeTestEnvironment();
+ Root.template = xml/* html */ `
+ <div>
+ <p><t t-esc="state.buffer" /></p>
+ <button class="one" t-on-click="trigger('numpad-click-input', { key: '1' })">1</button>
+ <button class="two" t-on-click="trigger('numpad-click-input', { key: '2' })">2</button>
+ <button class="reset" t-on-click="resetBuffer">reset</button>
+ </div>
+ `;
+
+ const root = new Root();
+ await root.mount(testUtils.prepareTarget());
+
+ const oneButton = root.el.querySelector('button.one');
+ const twoButton = root.el.querySelector('button.two');
+ const resetButton = root.el.querySelector('button.reset');
+ const bufferEl = root.el.querySelector('p');
+
+ testUtils.dom.click(oneButton);
+ testUtils.dom.click(twoButton);
+ await testUtils.nextTick();
+ assert.strictEqual(bufferEl.textContent, '12');
+ testUtils.dom.click(resetButton);
+ await testUtils.nextTick();
+ assert.strictEqual(bufferEl.textContent, '');
+ testUtils.dom.click(twoButton);
+ testUtils.dom.click(oneButton);
+ await testUtils.nextTick();
+ assert.strictEqual(bufferEl.textContent, '21');
+
+ root.unmount();
+ root.destroy();
+ });
+});
diff --git a/addons/point_of_sale/static/tests/unit/test_PaymentScreen.js b/addons/point_of_sale/static/tests/unit/test_PaymentScreen.js
new file mode 100644
index 00000000..48d3b55d
--- /dev/null
+++ b/addons/point_of_sale/static/tests/unit/test_PaymentScreen.js
@@ -0,0 +1,309 @@
+odoo.define('point_of_sale.tests.PaymentScreen', function (require) {
+ 'use strict';
+
+ const PosComponent = require('point_of_sale.PosComponent');
+ const { useListener } = require('web.custom_hooks');
+ const testUtils = require('web.test_utils');
+ const makePosTestEnv = require('point_of_sale.test_env');
+ const { xml } = owl.tags;
+ const { useState } = owl;
+
+ QUnit.module('unit tests for PaymentScreen components', {});
+
+ QUnit.test('PaymentMethodButton', async function (assert) {
+ assert.expect(2);
+
+ class Parent extends PosComponent {
+ constructor() {
+ super(...arguments);
+ useListener('new-payment-line', this._newPaymentLine);
+ }
+ _newPaymentLine() {
+ assert.step('new-payment-line');
+ }
+ }
+ Parent.env = makePosTestEnv();
+ Parent.template = xml/* html */ `
+ <div>
+ <PaymentMethodButton paymentMethod="{ name: 'Cash', id: 1 }" />
+ </div>
+ `;
+
+ const parent = new Parent();
+ await parent.mount(testUtils.prepareTarget());
+
+ const button = parent.el.querySelector('.paymentmethod');
+ await testUtils.dom.click(button);
+ assert.verifySteps(['new-payment-line']);
+
+ parent.unmount();
+ parent.destroy();
+ });
+
+ QUnit.test('PSNumpadInputButton', async function (assert) {
+ assert.expect(15);
+
+ class Parent extends PosComponent {
+ constructor({ value, text, changeClassTo }) {
+ super();
+ this.state = useState({ value, text, changeClassTo });
+ useListener('input-from-numpad', this._inputFromNumpad);
+ }
+ _inputFromNumpad({ detail: { key } }) {
+ assert.step(`${key}-input`);
+ }
+ setState(obj) {
+ Object.assign(this.state, obj);
+ }
+ }
+ Parent.env = makePosTestEnv();
+ Parent.template = xml/* html */ `
+ <div>
+ <PSNumpadInputButton value="state.value" text="state.text" changeClassTo="state.changeClassTo" />
+ </div>
+ `;
+
+ let parent = new Parent({ value: '1' });
+ await parent.mount(testUtils.prepareTarget());
+
+ let button = parent.el.querySelector('button');
+ assert.ok(button.textContent.includes('1'));
+ assert.ok(button.classList.contains('number-char'));
+ await testUtils.dom.click(button);
+ await testUtils.nextTick();
+ assert.verifySteps(['1-input']);
+
+ parent.setState({ value: '2', text: 'Two' });
+ await testUtils.nextTick();
+ assert.ok(button.textContent.includes('Two'));
+ await testUtils.dom.click(button);
+ await testUtils.nextTick();
+ assert.verifySteps(['2-input']);
+
+ parent.setState({ value: '+12', text: null, changeClassTo: 'not-number-char' });
+ await testUtils.nextTick();
+ assert.ok(button.textContent.includes('+12'));
+ assert.ok(button.classList.contains('not-number-char'));
+ // class number-char should have been replaced
+ assert.notOk(button.classList.contains('number-char'));
+ await testUtils.dom.click(button);
+ await testUtils.nextTick();
+ assert.verifySteps(['+12-input']);
+
+ parent.unmount();
+ parent.destroy();
+
+ // using the slot should ignore value and text props of the component
+ Parent.template = xml/* html */ `
+ <div>
+ <PSNumpadInputButton value="state.value" text="state.text" changeClassTo="state.changeClassTo">
+ <span>UseSlot</span>
+ </PSNumpadInputButton>
+ </div>
+ `;
+ parent = new Parent({ value: 'slotted', text: 'Text' });
+ await parent.mount(testUtils.prepareTarget());
+
+ button = parent.el.querySelector('button');
+ assert.ok(button.textContent.includes('UseSlot'));
+ await testUtils.dom.click(button);
+ await testUtils.nextTick();
+ assert.verifySteps(['slotted-input']);
+
+ parent.unmount();
+ parent.destroy();
+ });
+
+ QUnit.test('PaymentScreenPaymentLines', async function (assert) {
+ assert.expect(12);
+
+ class Parent extends PosComponent {
+ constructor() {
+ super();
+ useListener('delete-payment-line', this._onDeletePaymentLine);
+ useListener('select-payment-line', this._onSelectPaymentLine);
+ }
+ get paymentLines() {
+ return this.order.get_paymentlines();
+ }
+ get order() {
+ return this.env.pos.get_order();
+ }
+ mounted() {
+ this.order.paymentlines.on('change', this.render, this);
+ }
+ willUnmount() {
+ this.order.paymentlines.off('change', null, this);
+ }
+ _onDeletePaymentLine() {
+ assert.step('delete-click');
+ }
+ _onSelectPaymentLine() {
+ assert.step('select-click');
+ }
+ }
+ Parent.env = makePosTestEnv();
+ Parent.template = xml/* html */ `
+ <div>
+ <PaymentScreenPaymentLines paymentLines="paymentLines" />
+ </div>
+ `;
+
+ let parent = new Parent();
+ await parent.mount(testUtils.prepareTarget());
+
+ const order = parent.env.pos.get_order();
+ const cashPM = { id: 0, name: 'Cash', is_cash_count: true, use_payment_terminal: false };
+ const bankPM = { id: 0, name: 'Bank', is_cash_count: false, use_payment_terminal: false };
+
+ let paymentline1 = order.add_paymentline(cashPM);
+ await testUtils.nextTick();
+
+ let statusContainer = parent.el.querySelector('.payment-status-container');
+ let linesEl = parent.el.querySelector('.paymentlines');
+ assert.ok(linesEl, 'payment lines are shown');
+ let newLine = linesEl.querySelector('.selected');
+ assert.ok(newLine, 'the new line is automatically selected');
+
+ let paymentline2 = order.add_paymentline(bankPM);
+ await testUtils.nextTick();
+ assert.notOk(
+ linesEl.querySelector('.selected') === newLine,
+ 'the previously added paymentline should not be selected anymore'
+ );
+ assert.ok(
+ linesEl.querySelectorAll('.paymentline:not(.heading)').length === 2,
+ 'there should be two paymentlines'
+ );
+
+ let paymentline3 = order.add_paymentline(cashPM);
+ await testUtils.nextTick();
+ assert.ok(
+ linesEl.querySelectorAll('.paymentline:not(.heading)').length === 3,
+ 'there should be three paymentlines'
+ );
+ assert.ok(
+ linesEl.querySelectorAll('.paymentline.selected').length === 1,
+ 'there should only be one selected paymentline'
+ );
+
+ await testUtils.dom.click(linesEl.querySelector('.paymentline.selected .delete-button'));
+ await testUtils.nextTick();
+ assert.verifySteps(['delete-click', 'select-click']);
+
+ // click the 2nd payment line
+ await testUtils.dom.click(linesEl.querySelectorAll('.paymentline:not(.heading)')[1]);
+ await testUtils.nextTick();
+ assert.verifySteps(['select-click']);
+
+ // remove paymentline3 (the selected)
+ order.remove_paymentline(paymentline3);
+ await testUtils.nextTick();
+ assert.notOk(
+ linesEl.querySelector('.paymentline.selected'),
+ 'no more selected payment line'
+ );
+
+ order.remove_paymentline(paymentline1);
+ order.remove_paymentline(paymentline2);
+
+ parent.unmount();
+ parent.destroy();
+ });
+
+ QUnit.test('PaymentScreenElectronicPayment', async function (assert) {
+ assert.expect(17);
+
+ class SimulatedPaymentLine extends Backbone.Model {
+ constructor() {
+ super();
+ this.payment_status = 'pending';
+ this.can_be_reversed = false;
+ }
+ canBeAdjusted() {
+ return false;
+ }
+ setPaymentStatus(status) {
+ this.payment_status = status;
+ this.trigger('change');
+ }
+ toggleCanBeReversed() {
+ this.can_be_reversed = !this.can_be_reversed;
+ this.trigger('change');
+ }
+ }
+
+ class Parent extends PosComponent {
+ constructor() {
+ super();
+ this.line = new SimulatedPaymentLine();
+ useListener('send-payment-request', () => assert.step('send-payment-request'));
+ useListener('send-force-done', () => assert.step('send-force-done'));
+ useListener('send-payment-cancel', () => assert.step('send-payment-cancel'));
+ useListener('send-payment-reverse', () => assert.step('send-payment-reverse'));
+ }
+ }
+ Parent.env = makePosTestEnv();
+ Parent.template = xml/* html */ `
+ <div>
+ <PaymentScreenElectronicPayment line="line" />
+ </div>
+ `;
+
+ let parent = new Parent();
+ await parent.mount(testUtils.prepareTarget());
+
+ assert.ok(parent.el.querySelector('.paymentline .send_payment_request'));
+ await testUtils.dom.click(parent.el.querySelector('.paymentline .send_payment_request'));
+ await testUtils.nextTick();
+ assert.verifySteps(['send-payment-request']);
+
+ parent.line.setPaymentStatus('retry');
+ await testUtils.nextTick();
+ await testUtils.dom.click(parent.el.querySelector('.paymentline .send_payment_request'));
+ await testUtils.nextTick();
+ assert.verifySteps(['send-payment-request']);
+
+ parent.line.setPaymentStatus('force_done');
+ await testUtils.nextTick();
+ await testUtils.dom.click(parent.el.querySelector('.paymentline .send_force_done'));
+ await testUtils.nextTick();
+ assert.verifySteps(['send-force-done']);
+
+ parent.line.setPaymentStatus('waitingCard');
+ await testUtils.nextTick();
+ await testUtils.dom.click(parent.el.querySelector('.paymentline .send_payment_cancel'));
+ await testUtils.nextTick();
+ assert.verifySteps(['send-payment-cancel']);
+
+ parent.line.setPaymentStatus('waiting');
+ await testUtils.nextTick();
+ assert.ok(parent.el.querySelector('.paymentline i.fa-spinner'));
+
+ parent.line.setPaymentStatus('waitingCancel');
+ await testUtils.nextTick();
+ assert.ok(parent.el.querySelector('.paymentline i.fa-spinner'));
+
+ parent.line.setPaymentStatus('reversing');
+ await testUtils.nextTick();
+ assert.ok(parent.el.querySelector('.paymentline i.fa-spinner'));
+
+ parent.line.setPaymentStatus('done');
+ await testUtils.nextTick();
+ assert.notOk(parent.el.querySelector('.paymentline .send_payment_reversal'));
+
+ parent.line.toggleCanBeReversed();
+ await testUtils.nextTick();
+ assert.ok(parent.el.querySelector('.paymentline .send_payment_reversal'));
+ await testUtils.dom.click(parent.el.querySelector('.paymentline .send_payment_reversal'));
+ await testUtils.nextTick();
+ assert.verifySteps(['send-payment-reverse']);
+
+ parent.line.setPaymentStatus('reversed');
+ await testUtils.nextTick();
+ assert.ok(parent.el.querySelector('.paymentline'));
+
+ parent.unmount();
+ parent.destroy();
+ });
+});
diff --git a/addons/point_of_sale/static/tests/unit/test_ProductScreen.js b/addons/point_of_sale/static/tests/unit/test_ProductScreen.js
new file mode 100644
index 00000000..bdd9b732
--- /dev/null
+++ b/addons/point_of_sale/static/tests/unit/test_ProductScreen.js
@@ -0,0 +1,603 @@
+odoo.define('point_of_sale.tests.ProductScreen', function (require) {
+ 'use strict';
+
+ const PosComponent = require('point_of_sale.PosComponent');
+ const Registries = require('point_of_sale.Registries');
+ const { useListener } = require('web.custom_hooks');
+ const testUtils = require('web.test_utils');
+ const makePosTestEnv = require('point_of_sale.test_env');
+ const { xml } = owl.tags;
+ const { useState } = owl;
+
+ QUnit.module('unit tests for ProductScreen components', {});
+
+ QUnit.test('ActionpadWidget', async function (assert) {
+ assert.expect(7);
+
+ class Parent extends PosComponent {
+ constructor() {
+ super();
+ useListener('click-customer', () => assert.step('click-customer'));
+ useListener('click-pay', () => assert.step('click-pay'));
+ this.state = useState({ client: null });
+ }
+ }
+ Parent.env = makePosTestEnv();
+ Parent.template = xml/* html */ `
+ <div>
+ <ActionpadWidget client="state.client" />
+ </div>
+ `;
+
+ const parent = new Parent();
+ await parent.mount(testUtils.prepareTarget());
+
+ const setCustomerButton = parent.el.querySelector('button.set-customer');
+ const payButton = parent.el.querySelector('button.pay');
+
+ await testUtils.nextTick();
+ assert.ok(setCustomerButton.innerText.includes('Customer'));
+
+ // change to customer with short name
+ parent.state.client = { name: 'Test' };
+ await testUtils.nextTick();
+ assert.ok(setCustomerButton.innerText.includes('Test'));
+
+ // change to customer with long name
+ parent.state.client = { name: 'Change Customer' };
+ await testUtils.nextTick();
+ assert.ok(setCustomerButton.classList.contains('decentered'));
+
+ parent.state.client = null;
+
+ // click set-customer button
+ await testUtils.dom.click(setCustomerButton);
+ await testUtils.nextTick();
+ assert.verifySteps(['click-customer']);
+
+ // click pay button
+ await testUtils.dom.click(payButton);
+ await testUtils.nextTick();
+ assert.verifySteps(['click-pay']);
+
+ parent.unmount();
+ parent.destroy();
+ });
+
+ QUnit.test('NumpadWidget', async function (assert) {
+ assert.expect(25);
+
+ class Parent extends PosComponent {
+ constructor() {
+ super(...arguments);
+ useListener('set-numpad-mode', this.setNumpadMode);
+ useListener('numpad-click-input', this.numpadClickInput);
+ this.state = useState({ mode: 'quantity' });
+ }
+ setNumpadMode({ detail: { mode } }) {
+ this.state.mode = mode;
+ assert.step(mode);
+ }
+ numpadClickInput({ detail: { key } }) {
+ assert.step(key);
+ }
+ }
+ Parent.env = makePosTestEnv();
+ Parent.template = xml/* html */ `
+ <div><NumpadWidget activeMode="state.mode"></NumpadWidget></div>
+ `;
+
+ const pos = Parent.env.pos;
+ // set this old values back after testing
+ const old_config = pos.config;
+ const old_cashier = pos.get('cashier');
+
+ // set dummy values in pos.config and pos.get('cashier')
+ pos.config = {
+ restrict_price_control: false,
+ manual_discount: true
+ };
+ pos.set('cashier', { role: 'manager' });
+
+ const parent = new Parent();
+ await parent.mount(testUtils.prepareTarget());
+
+ const modeButtons = parent.el.querySelectorAll('.mode-button');
+ let qtyButton, discButton, priceButton;
+ for (let button of modeButtons) {
+ if (button.textContent.includes('Qty')) {
+ qtyButton = button;
+ }
+ if (button.textContent.includes('Disc')) {
+ discButton = button;
+ }
+ if (button.textContent.includes('Price')) {
+ priceButton = button;
+ }
+ }
+
+ // initially, qty button is active
+ assert.ok(qtyButton.classList.contains('selected-mode'));
+ assert.ok(!discButton.classList.contains('selected-mode'));
+ assert.ok(!priceButton.classList.contains('selected-mode'));
+
+ await testUtils.dom.click(discButton);
+ await testUtils.nextTick();
+ assert.ok(!qtyButton.classList.contains('selected-mode'));
+ assert.ok(discButton.classList.contains('selected-mode'));
+ assert.ok(!priceButton.classList.contains('selected-mode'));
+ assert.verifySteps(['discount']);
+
+ await testUtils.dom.click(priceButton);
+ await testUtils.nextTick();
+ assert.ok(!qtyButton.classList.contains('selected-mode'));
+ assert.ok(!discButton.classList.contains('selected-mode'));
+ assert.ok(priceButton.classList.contains('selected-mode'));
+ assert.verifySteps(['price']);
+
+ const numpadOne = [...parent.el.querySelectorAll('.number-char').values()].find((el) =>
+ el.textContent.includes('1')
+ );
+ const numpadMinus = parent.el.querySelector('.numpad-minus');
+ const numpadBackspace = parent.el.querySelector('.numpad-backspace');
+
+ await testUtils.dom.click(numpadOne);
+ await testUtils.nextTick();
+ assert.verifySteps(['1']);
+
+ await testUtils.dom.click(numpadMinus);
+ await testUtils.nextTick();
+ assert.verifySteps(['-']);
+
+ await testUtils.dom.click(numpadBackspace);
+ await testUtils.nextTick();
+ assert.verifySteps(['Backspace']);
+
+ await testUtils.dom.click(priceButton);
+ await testUtils.nextTick();
+ assert.verifySteps(['price']);
+
+ // change to price control restriction and the cashier is not manager
+ pos.config.restrict_price_control = true;
+ pos.set('cashier', { role: 'not manager' });
+ await testUtils.nextTick();
+
+ assert.ok(priceButton.classList.contains('disabled-mode'));
+ assert.ok(qtyButton.classList.contains('selected-mode'));
+ // after the cashier is changed, since it is not a manager,
+ // the 'set-numpad-mode' is triggered, setting the mode to
+ // 'quantity'.
+ assert.verifySteps(['quantity']);
+
+ // reset old config and cashier values to pos
+ pos.config = old_config;
+ pos.set('cashier', old_cashier);
+
+ parent.unmount();
+ parent.destroy();
+ });
+
+ QUnit.test('ProductsWidgetControlPanel', async function (assert) {
+ assert.expect(32);
+
+ // This test incorporates the following components:
+ // CategoryBreadcrumb
+ // CategoryButton
+ // CategorySimpleButton
+ // HomeCategoryBreadcrumb
+
+ // Create dummy category data
+ //
+ // Root
+ // | Test1
+ // | | Test2
+ // | ` Test3
+ // | | Test5
+ // | ` Test6
+ // ` Test4
+
+ const rootCategory = { id: 0, name: 'Root', parent: null };
+ const testCategory1 = { id: 1, name: 'Test1', parent: 0 };
+ const testCategory2 = { id: 2, name: 'Test2', parent: 1 };
+ const testCategory3 = { id: 3, name: 'Test3', parent: 1 };
+ const testCategory4 = { id: 4, name: 'Test4', parent: 0 };
+ const testCategory5 = { id: 5, name: 'Test5', parent: 3 };
+ const testCategory6 = { id: 6, name: 'Test6', parent: 3 };
+ const categories = {
+ 0: rootCategory,
+ 1: testCategory1,
+ 2: testCategory2,
+ 3: testCategory3,
+ 4: testCategory4,
+ 5: testCategory5,
+ 6: testCategory6,
+ };
+
+ class Parent extends PosComponent {
+ constructor() {
+ super(...arguments);
+ this.state = useState({ selectedCategoryId: 0 });
+ useListener('switch-category', this.switchCategory);
+ useListener('update-search', this.updateSearch);
+ useListener('clear-search', this.clearSearch);
+ }
+ get breadcrumbs() {
+ if (this.state.selectedCategoryId === 0) return [];
+ let current = categories[this.state.selectedCategoryId];
+ const res = [current];
+ while (current.parent != 0) {
+ const toAdd = categories[current.parent];
+ res.push(toAdd);
+ current = toAdd;
+ }
+ return res.reverse();
+ }
+ get subcategories() {
+ return Object.values(categories).filter(
+ ({ parent }) => parent == this.state.selectedCategoryId
+ );
+ }
+ switchCategory({ detail: id }) {
+ this.state.selectedCategoryId = id;
+ assert.step(`${id}`);
+ }
+ updateSearch(event) {
+ assert.step(event.detail);
+ }
+ clearSearch() {
+ assert.step('cleared');
+ }
+ }
+ Parent.env = makePosTestEnv();
+ Parent.template = xml/* html */ `
+ <div class="pos">
+ <div class="search-bar-portal">
+ <ProductsWidgetControlPanel breadcrumbs="breadcrumbs" subcategories="subcategories" />
+ </div>
+ </div>
+ `;
+
+ const pos = Parent.env.pos;
+ const old_config = pos.config;
+ // set dummy config
+ pos.config = { iface_display_categ_images: false };
+
+ const parent = new Parent();
+ await parent.mount(testUtils.prepareTarget());
+
+ // The following tests the breadcrumbs and subcategory buttons
+
+ // check if HomeCategoryBreadcrumb is rendered
+ assert.ok(
+ parent.el.querySelector('.breadcrumb-home'),
+ 'Home category should always be there'
+ );
+ let subcategorySpans = [...parent.el.querySelectorAll('.category-simple-button')];
+ assert.ok(subcategorySpans.length === 2, 'There should be 2 subcategories for Root.');
+ assert.ok(subcategorySpans.find((span) => span.textContent.includes('Test1')));
+ assert.ok(subcategorySpans.find((span) => span.textContent.includes('Test4')));
+
+ // click Test1
+ let test1Span = subcategorySpans.find((span) => span.textContent.includes('Test1'));
+ await testUtils.dom.click(test1Span);
+ await testUtils.nextTick();
+ assert.verifySteps(['1']);
+ assert.ok(
+ [...parent.el.querySelectorAll('.breadcrumb-button')][1].textContent.includes('Test1')
+ );
+ subcategorySpans = [...parent.el.querySelectorAll('.category-simple-button')];
+ assert.ok(subcategorySpans.length === 2, 'There should be 2 subcategories for Root.');
+ assert.ok(subcategorySpans.find((span) => span.textContent.includes('Test2')));
+ assert.ok(subcategorySpans.find((span) => span.textContent.includes('Test3')));
+
+ // click Test2
+ let test2Span = subcategorySpans.find((span) => span.textContent.includes('Test2'));
+ await testUtils.dom.click(test2Span);
+ await testUtils.nextTick();
+ assert.verifySteps(['2']);
+ subcategorySpans = [...parent.el.querySelectorAll('.category-simple-button')];
+ assert.ok(subcategorySpans.length === 0, 'Test2 should not have subcategories');
+
+ // go back to Test1
+ let breadcrumb1 = [...parent.el.querySelectorAll('.breadcrumb-button')].find((el) =>
+ el.textContent.includes('Test1')
+ );
+ await testUtils.dom.click(breadcrumb1);
+ await testUtils.nextTick();
+ assert.verifySteps(['1']);
+
+ // click Test3
+ subcategorySpans = [...parent.el.querySelectorAll('.category-simple-button')];
+ let test3Span = subcategorySpans.find((span) => span.textContent.includes('Test3'));
+ await testUtils.dom.click(test3Span);
+ await testUtils.nextTick();
+ assert.verifySteps(['3']);
+ subcategorySpans = [...parent.el.querySelectorAll('.category-simple-button')];
+ assert.ok(subcategorySpans.length === 2);
+
+ // click Test6
+ let test6Span = subcategorySpans.find((span) => span.textContent.includes('Test6'));
+ await testUtils.dom.click(test6Span);
+ await testUtils.nextTick();
+ assert.verifySteps(['6']);
+ let breadcrumbButtons = [...parent.el.querySelectorAll('.breadcrumb-button')];
+ assert.ok(breadcrumbButtons.length === 4);
+
+ // Now check subcategory buttons with images
+ pos.config.iface_display_categ_images = true;
+
+ let breadcrumbHome = parent.el.querySelector('.breadcrumb-home');
+ await testUtils.dom.click(breadcrumbHome);
+ await testUtils.nextTick();
+ assert.verifySteps(['0']);
+ assert.ok(
+ !parent.el.querySelector('.category-list').classList.contains('simple'),
+ 'Category list should not have simple class'
+ );
+ let categoryButtons = [...parent.el.querySelectorAll('.category-button')];
+ assert.ok(categoryButtons.length === 2, 'There should be 2 subcategories for Root');
+
+ // The following tests the search bar
+
+ const wait = (ms) => {
+ return new Promise((resolve) => {
+ setTimeout(resolve, ms);
+ });
+ };
+
+ const inputEl = parent.el.querySelector('.search-box input');
+ await testUtils.dom.triggerEvent(inputEl, 'keyup', { key: 'A' });
+ // Triggering keyup event doesn't type the key to the input
+ // so we manually assign the value of the input.
+ inputEl.value = 'A';
+ await wait(30);
+ await testUtils.dom.triggerEvent(inputEl, 'keyup', { key: 'B' });
+ inputEl.value = 'AB';
+ await wait(30);
+ await testUtils.dom.triggerEvent(inputEl, 'keyup', { key: 'C' });
+ inputEl.value = 'ABC';
+ await wait(110);
+ // Only after waiting for more than 100ms that update-search is triggered
+ // because the method is debounced.
+ assert.verifySteps(['ABC']);
+ await testUtils.dom.triggerEvent(inputEl, 'keyup', { key: 'D' });
+ inputEl.value = 'ABCD';
+ await wait(110);
+ assert.verifySteps(['ABCD']);
+
+ // clear the search bar
+ await testUtils.dom.click(parent.el.querySelector('.search-box .clear-icon'));
+ await testUtils.nextTick();
+ assert.verifySteps(['cleared']);
+ assert.ok(inputEl.value === '', 'value of the input element should be empty');
+
+ pos.config = old_config;
+
+ parent.unmount();
+ parent.destroy();
+ });
+
+ QUnit.test('ProductList, ProductItem', async function (assert) {
+ assert.expect(10);
+
+ // patch imageUrl and price of ProductItem component
+ const MockProductItemExt = (X) =>
+ class extends X {
+ get imageUrl() {
+ return 'data:,';
+ }
+ get price() {
+ return this.props.product.price;
+ }
+ };
+
+ const extension = Registries.Component.extend('ProductItem', MockProductItemExt);
+ extension.compile();
+
+ const dummyProducts = [
+ { id: 0, display_name: 'Burger', price: '$10' },
+ { id: 1, display_name: 'Water', price: '$2' },
+ { id: 2, display_name: 'Chair', price: '$25' },
+ ];
+
+ class Parent extends PosComponent {
+ constructor() {
+ super(...arguments);
+ this.state = useState({ searchWord: '', products: dummyProducts });
+ useListener('click-product', this._clickProduct);
+ }
+ _clickProduct({ detail: product }) {
+ assert.step(product.display_name);
+ }
+ }
+ Parent.env = makePosTestEnv();
+ Parent.template = xml/* html */ `
+ <div>
+ <ProductList products="state.products" searchWord="state.searchWord" />
+ </div>
+ `;
+
+ const parent = new Parent();
+ await parent.mount(testUtils.prepareTarget());
+
+ // Check if there are 3 products listed
+ assert.strictEqual(
+ parent.el.querySelectorAll('article.product').length,
+ 3,
+ 'There should be 3 products listed'
+ );
+
+ // Check contents of product item and click
+ const product1el = parent.el.querySelector(
+ 'article.product[aria-labelledby="article_product_1"]'
+ );
+ assert.ok(product1el.querySelector('.product-img img[alt="Water"]'));
+ assert.ok(product1el.querySelector('.product-img .price-tag').textContent.includes('$2'));
+ await testUtils.dom.click(product1el);
+ await testUtils.nextTick();
+ assert.verifySteps(['Water']);
+
+ // Remove one product, check if only two is listed
+ parent.state.products.splice(0, 1);
+ await testUtils.nextTick();
+ assert.strictEqual(
+ parent.el.querySelectorAll('article.product').length,
+ 2,
+ 'There should be 2 products listed after removing the first item'
+ );
+
+ // Remove all products, check if empty message is There are no products in this category
+ parent.state.products.splice(0, parent.state.products.length);
+ await testUtils.nextTick();
+ assert.strictEqual(
+ parent.el.querySelectorAll('article.product').length,
+ 0,
+ 'There should be 0 products listed after removing everything'
+ );
+ assert.ok(
+ parent.el
+ .querySelector('.product-list-empty p')
+ .textContent.includes('There are no products in this category.')
+ );
+
+ // change the searchWord to 'something', check if empty message is No results found
+ parent.state.searchWord = 'something';
+ await testUtils.nextTick();
+ assert.ok(
+ parent.el
+ .querySelector('.product-list-empty p')
+ .textContent.includes('No results found for')
+ );
+ assert.ok(
+ parent.el.querySelector('.product-list-empty p b').textContent.includes('something')
+ );
+
+ extension.remove();
+
+ parent.unmount();
+ parent.destroy();
+ });
+
+ QUnit.test('Orderline', async function (assert) {
+ assert.expect(10);
+
+ class Parent extends PosComponent {
+ constructor(product) {
+ super();
+ useListener('select-line', this._selectLine);
+ useListener('edit-pack-lot-lines', this._editPackLotLines);
+ this.order.add_product(product);
+ }
+ get order() {
+ return this.env.pos.get_order();
+ }
+ get line() {
+ return this.env.pos.get_order().get_orderlines()[0];
+ }
+ _selectLine() {
+ assert.step('select-line');
+ }
+ _editPackLotLines() {
+ assert.step('edit-pack-lot-lines');
+ }
+ willUnmount() {
+ this.order.remove_orderline(this.line);
+ }
+ }
+ Parent.env = makePosTestEnv();
+ Parent.template = xml/* html */ `
+ <div>
+ <Orderline line="line" />
+ </div>
+ `;
+
+ const [chair1, chair2] = Parent.env.pos.db.search_product_in_category(0, 'Office Chair');
+ // patch chair2 to have tracking
+ chair2.tracking = 'serial';
+
+ // 1. Test orderline without lot icon
+
+ let parent = new Parent(chair1);
+ await parent.mount(testUtils.prepareTarget());
+
+ let line = parent.el.querySelector('li.orderline');
+ assert.ok(line);
+ assert.notOk(line.querySelector('.line-lot-icon'), 'there should be no lot icon');
+ await testUtils.dom.click(line);
+ assert.verifySteps(['select-line']);
+
+ parent.unmount();
+ parent.destroy();
+
+ // 2. Test orderline with lot icon
+
+ parent = new Parent(chair2);
+ await parent.mount(testUtils.prepareTarget());
+
+ line = parent.el.querySelector('li.orderline');
+ const lotIcon = line.querySelector('.line-lot-icon');
+ assert.ok(line);
+ assert.ok(lotIcon, 'there should be lot icon');
+ await testUtils.dom.click(line);
+ assert.verifySteps(['select-line']);
+ await testUtils.dom.click(lotIcon);
+ assert.verifySteps(['edit-pack-lot-lines']);
+
+ parent.unmount();
+ parent.destroy();
+ });
+
+ QUnit.test('OrderWidget', async function (assert) {
+ assert.expect(8);
+
+ // OrderWidget is dependent on its parent's rerendering
+ class Parent extends PosComponent {
+ mounted() {
+ this.env.pos.on('change:selectedOrder', this.render, this);
+ }
+ willUnmount() {
+ this.env.pos.off('change:selectedOrder', null, this);
+ }
+ }
+ Parent.env = makePosTestEnv();
+ Parent.template = xml/* html */ `
+ <div>
+ <OrderWidget />
+ </div>
+ `;
+
+ const [chair1, chair2] = Parent.env.pos.db.search_product_in_category(0, 'Office Chair');
+
+ let parent = new Parent();
+ await parent.mount(testUtils.prepareTarget());
+
+ // current order is empty
+ assert.notOk(parent.el.querySelector('.summary'));
+ assert.ok(parent.el.querySelector('.order-empty'));
+
+ // add line to the current order
+ const order1 = parent.env.pos.get_order();
+ order1.add_product(chair1);
+ await testUtils.nextTick();
+ assert.ok(parent.el.querySelector('.summary'));
+ assert.notOk(parent.el.querySelector('.order-empty'));
+
+ // selected new order, new order is empty
+ const order2 = parent.env.pos.add_new_order();
+ await testUtils.nextTick();
+ assert.notOk(parent.el.querySelector('.summary'));
+ assert.ok(parent.el.querySelector('.order-empty'));
+
+ // add line to the current order
+ order2.add_product(chair2);
+ await testUtils.nextTick();
+ assert.ok(parent.el.querySelector('.summary'));
+ assert.notOk(parent.el.querySelector('.order-empty'));
+
+ parent.env.pos.delete_current_order();
+ parent.env.pos.delete_current_order();
+
+ parent.unmount();
+ parent.destroy();
+ });
+});
diff --git a/addons/point_of_sale/static/tests/unit/test_popups.js b/addons/point_of_sale/static/tests/unit/test_popups.js
new file mode 100644
index 00000000..205d1b24
--- /dev/null
+++ b/addons/point_of_sale/static/tests/unit/test_popups.js
@@ -0,0 +1,180 @@
+odoo.define('point_of_sale.test_popups', function(require) {
+ 'use strict';
+
+ const Registries = require('point_of_sale.Registries');
+ const testUtils = require('web.test_utils');
+ const PosComponent = require('point_of_sale.PosComponent');
+ const PopupControllerMixin = require('point_of_sale.PopupControllerMixin');
+ const makePosTestEnv = require('point_of_sale.test_env');
+ const { xml } = owl.tags;
+
+ QUnit.module('unit tests for Popups', {
+ before() {
+ class Root extends PopupControllerMixin(PosComponent) {
+ static template = xml`
+ <div>
+ <t t-if="popup.isShown" t-component="popup.component" t-props="popupProps" t-key="popup.name" />
+ </div>
+ `;
+ }
+ Root.env = makePosTestEnv();
+ this.Root = Root;
+ Registries.Component.freeze();
+ },
+ });
+
+ QUnit.test('ConfirmPopup', async function(assert) {
+ assert.expect(6);
+
+ const root = new this.Root();
+ await root.mount(testUtils.prepareTarget());
+
+ let promResponse, userResponse;
+
+ // Step: show popup and confirm
+ promResponse = root.showPopup('ConfirmPopup', {});
+ await testUtils.nextTick();
+ testUtils.dom.click(root.el.querySelector('.confirm'));
+ await testUtils.nextTick();
+ userResponse = await promResponse;
+ assert.strictEqual(userResponse.confirmed, true);
+
+ // Step: show popup then cancel
+ promResponse = root.showPopup('ConfirmPopup', {});
+ await testUtils.nextTick();
+ testUtils.dom.click(root.el.querySelector('.cancel'));
+ await testUtils.nextTick();
+ userResponse = await promResponse;
+ assert.strictEqual(userResponse.confirmed, false);
+
+ // Step: check texts
+ promResponse = root.showPopup('ConfirmPopup', {
+ title: 'Are you sure?',
+ body: 'Are you having fun?',
+ confirmText: 'Hell Yeah!',
+ cancelText: 'Are you kidding me?',
+ });
+ await testUtils.nextTick();
+ assert.strictEqual(root.el.querySelector('.title').innerText.trim(), 'Are you sure?');
+ assert.strictEqual(root.el.querySelector('.body').innerText.trim(), 'Are you having fun?');
+ assert.strictEqual(root.el.querySelector('.confirm').innerText.trim(), 'Hell Yeah!');
+ assert.strictEqual(
+ root.el.querySelector('.cancel').innerText.trim(),
+ 'Are you kidding me?'
+ );
+
+ root.unmount();
+ root.destroy();
+ });
+
+ QUnit.test('NumberPopup', async function(assert) {
+ assert.expect(8);
+
+ const root = new this.Root();
+ await root.mount(testUtils.prepareTarget());
+
+ let promResponse, userResponse;
+
+ // Step: show NumberPopup and confirm with empty buffer
+ promResponse = root.showPopup('NumberPopup', {});
+ await testUtils.nextTick();
+ testUtils.dom.triggerEvent(root.el.querySelector('.confirm'), 'mousedown');
+ await testUtils.nextTick();
+ userResponse = await promResponse;
+ assert.strictEqual(userResponse.confirmed, true);
+ assert.strictEqual(userResponse.payload, "");
+
+ // Step: show NumberPopup and cancel
+ promResponse = root.showPopup('NumberPopup', {});
+ await testUtils.nextTick();
+ testUtils.dom.triggerEvent(root.el.querySelector('.cancel'), 'mousedown');
+ await testUtils.nextTick();
+ userResponse = await promResponse;
+ assert.strictEqual(userResponse.confirmed, false);
+
+ // Step: show NumberPopup and confirm with filled buffer, new title, new text
+ promResponse = root.showPopup('NumberPopup', {
+ title: 'Are you sure?',
+ confirmText: 'Hell Yeah!',
+ cancelText: 'Are you kidding me?',
+ });
+ await testUtils.nextTick();
+ let nodes = Array.from(root.el.querySelectorAll('button'));
+ testUtils.dom.triggerEvent(nodes.find(elem => elem.innerHTML === "7"), 'mousedown');
+ await testUtils.nextTick();
+ testUtils.dom.triggerEvent(nodes.find(elem => elem.innerHTML === "+10"), 'mousedown');
+ await testUtils.nextTick();
+ assert.strictEqual(root.el.querySelector('.title').innerText.trim(), 'Are you sure?');
+ assert.strictEqual(root.el.querySelector('.confirm').innerText.trim(), 'Hell Yeah!');
+ assert.strictEqual(root.el.querySelector('.cancel').innerText.trim(), 'Are you kidding me?');
+ testUtils.dom.triggerEvent(root.el.querySelector('.confirm'), 'mousedown');
+ await testUtils.nextTick();
+ userResponse = await promResponse;
+ assert.strictEqual(userResponse.confirmed, true);
+ assert.strictEqual(userResponse.payload, "17");
+
+ root.unmount();
+ root.destroy();
+ });
+
+ QUnit.test('EditListPopup', async function(assert) {
+ assert.expect(7);
+
+ const root = new this.Root();
+ await root.mount(testUtils.prepareTarget());
+
+ let promResponse, userResponse;
+
+ // Step: show popup and confirm
+ promResponse = root.showPopup('EditListPopup', {});
+ await testUtils.nextTick();
+ testUtils.dom.click(root.el.querySelector('.confirm'));
+ await testUtils.nextTick();
+ userResponse = await promResponse;
+ assert.strictEqual(userResponse.confirmed, true);
+ assert.strictEqual(JSON.stringify(userResponse.payload.newArray), JSON.stringify([]));
+
+ // Step: show popup and cancel
+ promResponse = root.showPopup('EditListPopup', {});
+ await testUtils.nextTick();
+ testUtils.dom.click(root.el.querySelector('.cancel'));
+ await testUtils.nextTick();
+ userResponse = await promResponse;
+ assert.strictEqual(userResponse.confirmed, false);
+
+ // Step: show popup and confirm with a default array
+ let defaultArray = ["Banana", "Cherry"];
+ promResponse = root.showPopup('EditListPopup', {
+ title: "Fruits",
+ isSingleItem: false,
+ array: defaultArray,
+ });
+ await testUtils.nextTick();
+ testUtils.dom.click(root.el.querySelector('.confirm'));
+ await testUtils.nextTick();
+ userResponse = await promResponse;
+
+ assert.strictEqual(userResponse.confirmed, true);
+ let i = 0;
+ defaultArray = defaultArray.map((item) => Object.assign({}, { _id: i++ }, { 'text': item}));
+ assert.strictEqual(JSON.stringify(userResponse.payload.newArray), JSON.stringify(defaultArray));
+
+ // Step: show popup and confirm with a new array
+ promResponse = root.showPopup('EditListPopup', {
+ title: "Fruits",
+ isSingleItem: false,
+ array: ["Banana", "Cherry"],
+ });
+ await testUtils.nextTick();
+ testUtils.dom.click(root.el.querySelector('.fa-trash-o'));
+ await testUtils.nextTick();
+ testUtils.dom.click(root.el.querySelector('.confirm'));
+ await testUtils.nextTick();
+ userResponse = await promResponse;
+ assert.strictEqual(userResponse.confirmed, true);
+ assert.strictEqual(JSON.stringify(userResponse.payload.newArray), JSON.stringify([{ _id: 1, text: "Cherry"}]));
+
+ root.unmount();
+ root.destroy();
+ });
+});