From bdc0b4b892dc17c0a51e454740633edc26f3de6d Mon Sep 17 00:00:00 2001 From: Pierre Paridans <app@odoo.com> Date: Wed, 27 Feb 2019 11:53:47 +0000 Subject: [PATCH] [FIX] lunch: make it work on mobile This module didn't worked on mobile: user cannot select the products or see their cart. This commit reworks the LunchKanbanWidget template and introduces a dedicated template for mobile. This template introduces a button at the bottom of the screen to toggle the "cart" widget on mobile (hiding it by default). It tries to make the best use of the existing Bootstrap classes allowing to have a more responsive layout and reducing the custom CSS required by this module (both in desktop and in mobile). This commit adds a decent JS tests-suite (mobile & desktop) to this module as the existing one left substential parts of the UI untested (opening wizard when clicking on a kanban record, cart's lines content, clear cart button, widget structure and content based on the state...). Those tests are also less based on DOM nodes order/tag names and use instead dedicated classnames. It also modifies the LunchKanbanRecord click handling to had a dedicated event handler instead of overriding the default _onGlobalClick() and make it more testable. Task ID: 1945032 --- .../static/src/js/lunch_kanban_controller.js | 33 +- .../static/src/js/lunch_kanban_mobile.js | 74 ++ .../static/src/js/lunch_kanban_record.js | 46 +- .../lunch/static/src/js/lunch_kanban_view.js | 7 - .../lunch/static/src/scss/lunch_kanban.scss | 63 +- addons/lunch/static/src/xml/lunch_kanban.xml | 146 ++-- .../static/tests/lunch_kanban_mobile_tests.js | 208 +++++ .../lunch/static/tests/lunch_kanban_tests.js | 821 ++++++++++++++++++ addons/lunch/static/tests/lunch_test_utils.js | 58 ++ .../lunch/static/tests/test_lunch_kanban.js | 259 ------ addons/lunch/views/lunch_templates.xml | 13 +- .../static/src/scss/search_view_mobile.scss | 3 +- 12 files changed, 1334 insertions(+), 397 deletions(-) create mode 100644 addons/lunch/static/src/js/lunch_kanban_mobile.js create mode 100644 addons/lunch/static/tests/lunch_kanban_mobile_tests.js create mode 100644 addons/lunch/static/tests/lunch_kanban_tests.js create mode 100644 addons/lunch/static/tests/lunch_test_utils.js delete mode 100644 addons/lunch/static/tests/test_lunch_kanban.js diff --git a/addons/lunch/static/src/js/lunch_kanban_controller.js b/addons/lunch/static/src/js/lunch_kanban_controller.js index 6e1092a51f3c..5545d926be3f 100644 --- a/addons/lunch/static/src/js/lunch_kanban_controller.js +++ b/addons/lunch/static/src/js/lunch_kanban_controller.js @@ -67,7 +67,23 @@ var LunchKanbanController = KanbanController.extend({ }, }).then(function (data) { self.widgetData = data; - self.model._updateLocation(data.user_location[0]); + return self.model._updateLocation(data.user_location[0]); + }); + }, + /** + * Renders and appends the lunch banner widget. + * + * @private + */ + _renderLunchKanbanWidget: function () { + var self = this; + if (this.widget) { + this.widget.destroy(); + } + this.widgetData.wallet = parseFloat(this.widgetData.wallet).toFixed(2); + this.widget = new LunchKanbanWidget(this, _.extend(this.widgetData, {edit: this.editMode})); + return this.widget.appendTo(document.createDocumentFragment()).then(function () { + self.$('.o_lunch_kanban').prepend(self.widget.$el); }); }, _showPaymentDialog: function (title) { @@ -89,19 +105,8 @@ var LunchKanbanController = KanbanController.extend({ * @private */ _update: function () { - var self = this; - - var def = this._fetchWidgetData().then(function () { - if (self.widget) { - self.widget.destroy(); - } - self.widgetData.wallet = parseFloat(self.widgetData.wallet).toFixed(2); - self.widget = new LunchKanbanWidget(self, _.extend(self.widgetData, {edit: self.editMode})); - return self.widget.appendTo(document.createDocumentFragment()).then(function () { - self.$('.o_lunch_kanban').prepend(self.widget.$el); - }); - }); - return $.when(def, this._super.apply(self, arguments)); + var def = this._fetchWidgetData().then(this._renderLunchKanbanWidget.bind(this)); + return $.when(def, this._super.apply(this, arguments)); }, /** * Override to add the location domain (coming from the lunchKanbanWidget) diff --git a/addons/lunch/static/src/js/lunch_kanban_mobile.js b/addons/lunch/static/src/js/lunch_kanban_mobile.js new file mode 100644 index 000000000000..eddd22ac3518 --- /dev/null +++ b/addons/lunch/static/src/js/lunch_kanban_mobile.js @@ -0,0 +1,74 @@ +odoo.define('lunch.LunchKanbanMobile', function (require) { +"use strict"; + +var config = require('web.config'); +var LunchKanbanWidget = require('lunch.LunchKanbanWidget'); +var LunchKanbanController = require('lunch.LunchKanbanController'); + +if (!config.device.isMobile) { + return; +} + +LunchKanbanWidget.include({ + template: "LunchKanbanWidgetMobile", + + /** + * Override to set the toggle state allowing initially open it. + * + * @override + */ + init: function (parent, params) { + this._super.apply(this, arguments); + this.keepOpen = params.keepOpen || undefined; + }, +}); + +LunchKanbanController.include({ + /** + * @override + */ + init: function () { + this._super.apply(this, arguments); + this.openWidget = false; + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Override to add the widget's toggle state to its data. + * + * @override + * @private + */ + _renderLunchKanbanWidget: function () { + this.widgetData.keepOpen = this.openWidget; + this.openWidget = false; + return this._super.apply(this, arguments); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @override + * @private + */ + _onAddProduct: function () { + this.openWidget = true; + this._super.apply(this, arguments); + }, + + /** + * @override + * @private + */ + _onRemoveProduct: function () { + this.openWidget = true; + this._super.apply(this, arguments); + }, +}); + +}); diff --git a/addons/lunch/static/src/js/lunch_kanban_record.js b/addons/lunch/static/src/js/lunch_kanban_record.js index a0d60588faee..ce9272b34f07 100644 --- a/addons/lunch/static/src/js/lunch_kanban_record.js +++ b/addons/lunch/static/src/js/lunch_kanban_record.js @@ -1,22 +1,36 @@ odoo.define('lunch.LunchKanbanRecord', function (require) { -"use strict"; + "use strict"; -/** - * This file defines the KanbanRecord for the Lunch Kanban view. - */ + /** + * This file defines the KanbanRecord for the Lunch Kanban view. + */ -var KanbanRecord = require('web.KanbanRecord'); + var KanbanRecord = require('web.KanbanRecord'); -var LunchKanbanRecord = KanbanRecord.extend({ - _onGlobalClick: function (ev) { - ev.preventDefault(); - // ignore clicks on oe_kanban_action elements - if (!$(ev.target).hasClass('oe_kanban_action')) { - this.trigger_up('open_wizard', {productId: this.recordData.product_id}); - } - }, -}); + var LunchKanbanRecord = KanbanRecord.extend({ + events: _.extend({}, KanbanRecord.prototype.events, { + 'click': '_onSelectRecord', + }), -return LunchKanbanRecord; + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- -}); + /** + * Open the add product wizard + * + * @private + * @param {MouseEvent} ev Click event + */ + _onSelectRecord: function (ev) { + ev.preventDefault(); + // ignore clicks on oe_kanban_action elements + if (!$(ev.target).hasClass('oe_kanban_action')) { + this.trigger_up('open_wizard', {productId: this.recordData.product_id}); + } + }, + }); + + return LunchKanbanRecord; + + }); diff --git a/addons/lunch/static/src/js/lunch_kanban_view.js b/addons/lunch/static/src/js/lunch_kanban_view.js index 72b30634e871..a0cd6d3f3e08 100644 --- a/addons/lunch/static/src/js/lunch_kanban_view.js +++ b/addons/lunch/static/src/js/lunch_kanban_view.js @@ -5,19 +5,12 @@ var LunchKanbanController = require('lunch.LunchKanbanController'); var LunchKanbanModel = require('lunch.LunchKanbanModel'); var LunchKanbanRenderer = require('lunch.LunchKanbanRenderer'); -var config = require('web.config'); var core = require('web.core'); var KanbanView = require('web.KanbanView'); var view_registry = require('web.view_registry'); var _lt = core._lt; -if (config.device.isMobile) { - // use the classical KanbanView in mobile - view_registry.add('lunch_kanban', KanbanView); - return; -} - var LunchKanbanView = KanbanView.extend({ config: _.extend({}, KanbanView.prototype.config, { Controller: LunchKanbanController, diff --git a/addons/lunch/static/src/scss/lunch_kanban.scss b/addons/lunch/static/src/scss/lunch_kanban.scss index 7c50ae36358e..51acfd2b3f81 100644 --- a/addons/lunch/static/src/scss/lunch_kanban.scss +++ b/addons/lunch/static/src/scss/lunch_kanban.scss @@ -6,6 +6,8 @@ height: 100%; .o_lunch_kanban_banner { flex: 0 0 auto; + border-bottom: 1px solid #CED4DA; + background-color: white; } .o_kanban_view { flex: 1 1 100%; @@ -19,52 +21,47 @@ } .o_lunch_widget { - display: flex; - border-bottom: 1px solid #CED4DA; - background-color: white; min-height: 90px; max-height: 33vh; overflow-y: auto; - .o_lunch_widget_info { - padding: 5px 12px; - margin: 4px 8px; - width: 300px; - flex: 1 1 auto; - overflow-y: auto; - - .o_lunch_ordered { - color: white; - background-color: #F0D970; - padding: 1px 3px; - border-radius: .25em; - } - - .o_lunch_confirmed { - color: white; - background-color: #00BA4E; - padding: 1px 3px; - border-radius: .25em; + .o_lunch_widget_info.card { + &, .card-title, .card-body { + color: $o-main-text-color; + background-color: inherit !important; } - .o_lunch_widget_title { + .card-title { font-weight: bold; - font-size: initial; + margin-bottom: 0; } - .o_lunch_widget_order_button { - display: block; - width: 100%; - margin-bottom: 4px; - margin-top: 12px; + .card-body { + padding: 0.5rem 1rem; } - .o_lunch_open_wizard { - cursor: pointer; - &:hover { - font-weight: bolder; + .btn-link { + padding: 0; + &.o_lunch_open_wizard { + color: $o-main-text-color; + font-weight: normal; } } } } } + +@include media-breakpoint-down(sm) { + .o_lunch_kanban { + details summary { + // Hide the caret. For details see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/summary + list-style-type: none; + &::-webkit-details-marker { + display: none + } + } + .o_lunch_widget { + max-height: 100% + } + } +} diff --git a/addons/lunch/static/src/xml/lunch_kanban.xml b/addons/lunch/static/src/xml/lunch_kanban.xml index 62a2edd2900e..3b3b319ce956 100644 --- a/addons/lunch/static/src/xml/lunch_kanban.xml +++ b/addons/lunch/static/src/xml/lunch_kanban.xml @@ -1,84 +1,88 @@ <?xml version="1.0" encoding="UTF-8"?> <templates> - <div t-name="LunchKanbanWidget" class="o_lunch_kanban_banner"> - <div class="o_lunch_widget"> - <div class="o_lunch_widget_info d-flex flex-row"> - <div> - <img class="rounded-circle" t-attf-src="{{ widget.userimage }}"/> - </div> - <div class="w-50 ml-2 py-3"> - <div class="o_lunch_user_field"/> - <div class="o_lunch_location_field"/> - <div class="d-flex flex-row"> - <span class="flex-grow-1">Your Account</span> - <t t-call="currency_field"> - <t t-set="value" t-value="widget.wallet"/> - <t t-set="currency" t-value="widget.currency"/> - </t> + <div t-name="LunchKanbanWidget" class="o_lunch_kanban_banner container-fluid"> + <div class="o_lunch_widget row py-3 py-md-0"> + <div class="o_lunch_widget_info col-12 col-md-4 card border-0"> + <div class="card-body row no-gutters align-items-center"> + <div class="col-3 col-md-6 col-lg-4"> + <img class="rounded-circle" t-attf-src="{{ widget.userimage }}"/> + </div> + <div class="col-9 col-md-6 col-lg-8"> + <div class="pl-3"> + <div class="o_lunch_user_field py-1"/> + <div class="o_lunch_location_field py-1"/> + <div class="d-flex flex-row py-1"> + <span class="flex-grow-1">Your Account</span> + <t t-call="currency_field"> + <t t-set="value" t-value="widget.wallet"/> + <t t-set="currency" t-value="widget.currency"/> + </t> + </div> + </div> </div> </div> </div> - <div class="o_lunch_widget_info"> + <div class="o_lunch_widget_info col-12 col-md-4 card border-0"> <t t-if="!_.isEmpty(widget.lines)"> <t t-if="widget.raw_state == 'ordered'"> - <t t-set="state_class" t-value="'o_lunch_ordered'"/> + <t t-set="state_class" t-value="'badge-warning o_lunch_ordered'"/> </t> <t t-else="widget.raw_state == 'confirmed'"> - <t t-set="state_class" t-value="'o_lunch_confirmed'"/> + <t t-set="state_class" t-value="'badge-success o_lunch_confirmed'"/> </t> - <div class="d-flex flex-row"> - <div class="flex-grow-1"> - <span class="o_lunch_widget_title">Your order</span> - <a href="#"><i t-if="widget.raw_state != 'confirmed'" class="fa fa-trash o_lunch_widget_unlink"/></a> - </div> - <div t-if="widget.raw_state != 'new'" t-esc="widget.state" t-attf-class="flex-grow-0 {{ state_class }}"/> - </div> - <div t-foreach="widget.lines" t-as="line"> - <div class="d-flex flex-row"> - <div class="flex-grow-0"> - <a class="o_remove_product" t-if="widget.raw_state != 'confirmed'" t-attf-data-id="{{ line.id }}" href="#"><i class="fa fa-minus-circle"/></a> - <span t-esc="line.quantity"/> - <a class="o_add_product" t-if="widget.raw_state != 'confirmed'" t-attf-data-id="{{ line.id }}" href="#"><i class="fa fa-plus-circle"/></a> - </div> - <div class="flex-grow-1 pl-2"> - <a t-esc="line.product[1]" class="o_lunch_open_wizard" t-attf-data-product-id="{{ line.product[0] }}" t-attf-data-id="{{ line.id }}"/> - </div> - <div class="flex-grow-0"> - <t t-call="currency_field"> - <t t-set="value" t-value="line.product[2]"/> - <t t-set="currency" t-value="widget.currency"/> - </t> - </div> - </div> - <div t-foreach="line.toppings" t-as="topping" class="d-flex flex-row"> - <div class="flex-grow-1 pl-5"> - <span>+ <t t-esc="topping[0]"/></span> - </div> - <div class="flex-grow-0"> - <t t-call="currency_field"> - <t t-set="value" t-value="topping[1]"/> - <t t-set="currency" t-value="widget.currency"/> - </t> - </div> - </div> - <span t-if="line.note" t-esc="line.note" class="text-muted pl-5"/> + <div class="card-body"> + <h4 class="card-title"> + Your order + <button t-if="widget.raw_state != 'confirmed'" class="btn btn-sm btn-icon btn-link fa fa-trash o_lunch_widget_unlink"/> + <span t-if="widget.raw_state != 'new'" t-esc="widget.state" t-attf-class="badge badge-pill {{ state_class }}"/> + </h4> + <ul class="list-unstyled o_lunch_widget_lines"> + <li t-foreach="widget.lines" t-as="line"> + <div class="d-flex align-items-center"> + <div class="flex-grow-0 flex-shrink-0 o_lunch_product_quantity"> + <button class="btn btn-sm btn-icon btn-link fa fa-minus-circle o_remove_product" t-if="widget.raw_state != 'confirmed'" t-attf-data-id="{{ line.id }}"/> + <span t-esc="line.quantity"/> + <button class="btn btn-sm btn-icon btn-link fa fa-plus-circle o_add_product" t-if="widget.raw_state != 'confirmed'" t-attf-data-id="{{ line.id }}"/> + </div> + <div class="flex-grow-1 pl-2"> + <button t-esc="line.product[1]" class="btn btn-link o_lunch_open_wizard" t-attf-data-product-id="{{ line.product[0] }}" t-attf-data-id="{{ line.id }}"/> + </div> + <div class="flex-grow-0"> + <t t-call="currency_field"> + <t t-set="value" t-value="line.product[2]"/> + <t t-set="currency" t-value="widget.currency"/> + </t> + </div> + </div> + <div t-foreach="line.toppings" t-as="topping" class="d-flex flex-row"> + <div class="flex-grow-1 pl-5"> + <span>+ <t t-esc="topping[0]"/></span> + </div> + <div class="flex-grow-0"> + <t t-call="currency_field"> + <t t-set="value" t-value="topping[1]"/> + <t t-set="currency" t-value="widget.currency"/> + </t> + </div> + </div> + <span t-if="line.note" t-esc="line.note" class="text-muted pl-5"/> + </li> + </ul> </div> </t> </div> - <div class="o_lunch_widget_info"> + <div class="o_lunch_widget_info col-12 col-md-4 card border-0"> <t t-if="!_.isEmpty(widget.lines) && widget.raw_state == 'new'"> - <div class="d-flex flex-row"> - <div class="o_lunch_widget_title flex-grow-1"> - Total - </div> - <div class="flex-grow-0 pr-1"> + <div class="card-body d-flex flex-column justify-content-between"> + <h4 class="card-title d-flex py-1"> + <span class="flex-grow-1">Total</span> <t t-call="currency_field"> <t t-set="value" t-value="widget.total"/> <t t-set="currency" t-value="widget.currency"/> </t> - </div> + </h4> + <button t-if="widget.raw_state == 'new'" class="btn btn-primary w-100 o_lunch_widget_order_button">Order now</button> </div> - <button t-if="widget.raw_state == 'new'" class="btn btn-primary o_lunch_widget_order_button">ORDER NOW</button> </t> </div> </div> @@ -89,7 +93,7 @@ </t> </div> - <t t-name="currency_field"> + <span t-name="currency_field" class="o_field_monetary o_field_number o_field_widget"> <t t-js="ctx"> ctx.value = _.str.sprintf('%.2f', parseFloat(ctx.value)); </t> @@ -104,9 +108,23 @@ <t t-else=""> <t t-esc="value"/> </t> - </t> + </span> <div t-name="lunch.LunchPaymentDialog"> <span t-esc="widget.message"/> </div> + + <t t-name="LunchKanbanWidgetMobile"> + <details class="fixed-bottom" t-attf-open="#{widget.keepOpen}"> + <summary class="o_lunch_toggle_cart btn btn-primary w-100"> + <i class="fa fa-fw fa-shopping-cart"/> + Your cart + (<t t-call="currency_field"> + <t t-set="value" t-value="widget.total"/> + <t t-set="currency" t-value="widget.currency"/> + </t>) + </summary> + <t t-call="LunchKanbanWidget"/> + </details> + </t> </templates> diff --git a/addons/lunch/static/tests/lunch_kanban_mobile_tests.js b/addons/lunch/static/tests/lunch_kanban_mobile_tests.js new file mode 100644 index 000000000000..216d830bb479 --- /dev/null +++ b/addons/lunch/static/tests/lunch_kanban_mobile_tests.js @@ -0,0 +1,208 @@ +odoo.define('lunch.lunchKanbanMobileTests', function (require) { +"use strict"; + +const LunchKanbanView = require('lunch.LunchKanbanView'); + +const testUtils = require('web.test_utils'); +const {createLunchKanbanView, mockLunchRPC} = require('lunch.test_utils'); + +QUnit.module('Views'); + +QUnit.module('LunchKanbanView Mobile', { + beforeEach() { + const PORTAL_GROUP_ID = 1234; + + this.data = { + 'product': { + fields: { + is_available_at: {string: 'Product Availability', type: 'many2one', relation: 'lunch.location'}, + category_id: {string: 'Product Category', type: 'many2one', relation: 'lunch.product.category'}, + supplier_id: {string: 'Vendor', type: 'many2one', relation: 'lunch.supplier'}, + }, + records: [ + {id: 1, name: 'Tuna sandwich', is_available_at: 1}, + ], + }, + 'lunch.order': { + fields: {}, + update_quantity() { + return $.when(); + }, + }, + 'ir.model.data': { + fields: {}, + xmlid_to_res_id() { + return $.when(PORTAL_GROUP_ID); + }, + }, + 'lunch.location': { + fields: { + name: {string: 'Name', type: 'char'}, + }, + records: [ + {id: 1, name: "Office 1"}, + {id: 2, name: "Office 2"}, + ], + }, + }; + this.regularInfos = { + user_location: [2, "Office 2"], + }; + }, +}, function () { + QUnit.test('basic rendering', function (assert) { + assert.expect(9); + + const kanban = createLunchKanbanView({ + View: LunchKanbanView, + model: 'product', + data: this.data, + arch: ` + <kanban> + <templates> + <t t-name="kanban-box"> + <div><field name="name"/></div> + </t> + </templates> + </kanban> + `, + mockRPC: mockLunchRPC({ + infos: this.regularInfos, + userLocation: this.data['lunch.location'].records[0].id, + }), + }); + + assert.containsOnce(kanban, '.o_kanban_view .o_kanban_record:not(.o_kanban_ghost)', + "should have 1 records in the renderer"); + + // check view layout + assert.containsOnce(kanban, '.o_content > div', + "should have 1 column"); + assert.containsNone(kanban, '.o_content > div.o_search_panel', + "shouldn't have a 'lunch filters' column"); + assert.containsOnce(kanban, '.o_content > .o_lunch_kanban', + "should have a 'kanban lunch wrapper' column"); + assert.containsOnce(kanban, '.o_lunch_kanban > .o_kanban_view', + "should have a 'classical kanban view' column"); + assert.hasClass(kanban.$('.o_kanban_view'), 'o_lunch_kanban_view', + "should have classname 'o_lunch_kanban_view'"); + assert.containsOnce($('.o_lunch_kanban'), '> details', + "should have a 'lunch kanban' details/summary discolure panel"); + assert.hasClass($('.o_lunch_kanban > details'), 'fixed-bottom', + "should have classname 'fixed-bottom'"); + assert.isNotVisible($('.o_lunch_kanban > details .o_lunch_kanban_banner'), + "shouldn't have a visible 'lunch kanban' banner"); + + kanban.destroy(); + }); + + QUnit.module('LunchKanbanWidget', function () { + QUnit.test('toggle', function (assert) { + assert.expect(6); + + const kanban = createLunchKanbanView({ + View: LunchKanbanView, + model: 'product', + data: this.data, + arch: ` + <kanban> + <templates> + <t t-name="kanban-box"> + <div><field name="name"/></div> + </t> + </templates> + </kanban> + `, + mockRPC: mockLunchRPC({ + infos: Object.assign({}, this.regularInfos, { + total: "3.00", + }), + userLocation: this.data['lunch.location'].records[0].id, + }), + }); + + const $details = $('.o_lunch_kanban > details'); + assert.isNotVisible($details.find('.o_lunch_kanban_banner'), + "shouldn't have a visible 'lunch kanban' banner"); + assert.isVisible($details.find('> summary'), + "should hava a visible cart toggle button"); + assert.containsOnce($details, '> summary:contains(Your cart)', + "should have 'Your cart' in the button text"); + assert.containsOnce($details, '> summary:contains(3.00)', + "should have '3.00' in the button text"); + + testUtils.dom.click($details.find('> summary')); + assert.isVisible($details.find('.o_lunch_kanban_banner'), + "should have a visible 'lunch kanban' banner"); + + testUtils.dom.click($details.find('> summary')); + assert.isNotVisible($details.find('.o_lunch_kanban_banner'), + "shouldn't have a visible 'lunch kanban' banner"); + + kanban.destroy(); + }); + + QUnit.test('keep open when adding quantities', function (assert) { + assert.expect(6); + + const kanban = createLunchKanbanView({ + View: LunchKanbanView, + model: 'product', + data: this.data, + arch: ` + <kanban> + <templates> + <t t-name="kanban-box"> + <div><field name="name"/></div> + </t> + </templates> + </kanban> + `, + mockRPC: mockLunchRPC({ + infos: Object.assign({}, this.regularInfos, { + lines: [ + { + id: 6, + product: [1, "Tuna sandwich", "3.00"], + toppings: [], + quantity: 1.0, + }, + ], + }), + userLocation: this.data['lunch.location'].records[0].id, + }), + }); + + const $details = $('.o_lunch_kanban > details'); + assert.isNotVisible($details.find('.o_lunch_kanban_banner'), + "shouldn't have a visible 'lunch kanban' banner"); + assert.isVisible($details.find('> summary'), + "should hava a visible cart toggle button"); + + testUtils.dom.click($details.find('> summary')); + assert.isVisible($details.find('.o_lunch_kanban_banner'), + "should have a visible 'lunch kanban' banner"); + + const $widgetSecondColumn = kanban.$('.o_lunch_widget .o_lunch_widget_info:eq(1)'); + + assert.containsOnce($widgetSecondColumn, '.o_lunch_widget_lines > li', + "should have 1 order line"); + + let $firstLine = $widgetSecondColumn.find('.o_lunch_widget_lines > li:first'); + + testUtils.dom.click($firstLine.find('button.o_add_product')); + assert.isVisible($('.o_lunch_kanban > details .o_lunch_kanban_banner'), + "add quantity should keep 'lunch kanban' banner open"); + + $firstLine = kanban.$('.o_lunch_widget .o_lunch_widget_info:eq(1) .o_lunch_widget_lines > li:first'); + + testUtils.dom.click($firstLine.find('button.o_remove_product')); + assert.isVisible($('.o_lunch_kanban > details .o_lunch_kanban_banner'), + "remove quantity should keep 'lunch kanban' banner open"); + + kanban.destroy(); + }); + }); +}); + +}); diff --git a/addons/lunch/static/tests/lunch_kanban_tests.js b/addons/lunch/static/tests/lunch_kanban_tests.js new file mode 100644 index 000000000000..571b11ed52e3 --- /dev/null +++ b/addons/lunch/static/tests/lunch_kanban_tests.js @@ -0,0 +1,821 @@ +odoo.define('lunch.lunchKanbanTests', function (require) { +"use strict"; + +const LunchKanbanView = require('lunch.LunchKanbanView'); + +const testUtils = require('web.test_utils'); +const {createLunchKanbanView, mockLunchRPC} = require('lunch.test_utils'); + +QUnit.module('Views'); + +QUnit.module('LunchKanbanView', { + beforeEach() { + const PORTAL_GROUP_ID = 1234; + + this.data = { + 'product': { + fields: { + is_available_at: {string: 'Product Availability', type: 'many2one', relation: 'lunch.location'}, + category_id: {string: 'Product Category', type: 'many2one', relation: 'lunch.product.category'}, + supplier_id: {string: 'Vendor', type: 'many2one', relation: 'lunch.supplier'}, + }, + records: [ + {id: 1, name: 'Tuna sandwich', is_available_at: 1}, + ], + }, + 'lunch.order': { + fields: {}, + update_quantity() { + return $.when(); + }, + }, + 'lunch.product.category': { + fields: {}, + records: [], + }, + 'lunch.supplier': { + fields: {}, + records: [], + }, + 'ir.model.data': { + fields: {}, + xmlid_to_res_id() { + return $.when(PORTAL_GROUP_ID); + }, + }, + 'lunch.location': { + fields: { + name: {string: 'Name', type: 'char'}, + }, + records: [ + {id: 1, name: "Office 1"}, + {id: 2, name: "Office 2"}, + ], + }, + 'res.users': { + fields: { + name: {string: 'Name', type: 'char'}, + groups_id: {string: 'Groups', type: 'many2many'}, + }, + records: [ + {id: 1, name: "Mitchell Admin", groups_id: []}, + {id: 2, name: "Marc Demo", groups_id: []}, + {id: 3, name: "Jean-Luc Portal", groups_id: [PORTAL_GROUP_ID]}, + ], + }, + }; + this.regularInfos = { + username: "Marc Demo", + wallet: 36.5, + is_manager: false, + currency: { + symbol: "\u20ac", + position: "after" + }, + user_location: [2, "Office 2"], + }; + this.managerInfos = { + username: "Mitchell Admin", + wallet: 47.6, + is_manager: true, + currency: { + symbol: "\u20ac", + position: "after" + }, + user_location: [2, "Office 2"], + }; + }, +}, function () { + QUnit.test('basic rendering', function (assert) { + assert.expect(7); + + const kanban = createLunchKanbanView({ + View: LunchKanbanView, + model: 'product', + data: this.data, + arch: ` + <kanban> + <templates> + <t t-name="kanban-box"> + <div><field name="name"/></div> + </t> + </templates> + </kanban> + `, + mockRPC: mockLunchRPC({ + infos: this.regularInfos, + userLocation: this.data['lunch.location'].records[0].id, + }), + }); + + assert.containsOnce(kanban, '.o_kanban_view .o_kanban_record:not(.o_kanban_ghost)', + "should have 1 records in the renderer"); + + // check view layout + assert.containsN(kanban, '.o_content > div', 2, + "should have 2 columns"); + assert.containsOnce(kanban, '.o_content > div.o_search_panel', + "should have a 'lunch filters' column"); + assert.containsOnce(kanban, '.o_content > .o_lunch_kanban', + "should have a 'kanban lunch wrapper' column"); + assert.containsOnce(kanban, '.o_lunch_kanban > .o_kanban_view', + "should have a 'classical kanban view' column"); + assert.hasClass(kanban.$('.o_kanban_view'), 'o_lunch_kanban_view', + "should have classname 'o_lunch_kanban_view'"); + assert.containsOnce(kanban, '.o_lunch_kanban > .o_lunch_kanban_banner', + "should have a 'lunch kanban' banner"); + + kanban.destroy(); + }); + + QUnit.module('LunchKanbanWidget', function () { + + QUnit.test('empty cart', function (assert) { + assert.expect(3); + + const kanban = createLunchKanbanView({ + View: LunchKanbanView, + model: 'product', + data: this.data, + arch: ` + <kanban> + <templates> + <t t-name="kanban-box"> + <div><field name="name"/></div> + </t> + </templates> + </kanban> + `, + mockRPC: mockLunchRPC({ + infos: this.regularInfos, + userLocation: this.data['lunch.location'].records[0].id, + }), + }); + + const $kanbanWidget = kanban.$('.o_lunch_widget'); + + assert.containsN($kanbanWidget, '> .o_lunch_widget_info', 3, + "should have 3 columns"); + assert.isVisible($kanbanWidget.find('> .o_lunch_widget_info:first'), + "should have the first column visible"); + assert.strictEqual($kanbanWidget.find('> .o_lunch_widget_info:not(:first)').html().trim(), "", + "all columns but the first should be empty"); + + kanban.destroy(); + }); + + QUnit.test('non-empty cart', function (assert) { + assert.expect(17); + + const kanban = createLunchKanbanView({ + View: LunchKanbanView, + model: 'product', + data: this.data, + arch: ` + <kanban> + <templates> + <t t-name="kanban-box"> + <div><field name="name"/></div> + </t> + </templates> + </kanban> + `, + mockRPC: mockLunchRPC({ + infos: Object.assign({}, this.regularInfos, { + total: "3.00", + lines: [ + { + product: [1, "Tuna sandwich", "3.00"], + toppings: [], + quantity: 1.0, + }, + ], + }), + userLocation: this.data['lunch.location'].records[0].id, + }), + }); + + const $kanbanWidget = kanban.$('.o_lunch_widget'); + + assert.containsN($kanbanWidget, '> .o_lunch_widget_info', 3, + "should have 3 columns"); + + assert.containsOnce($kanbanWidget, '.o_lunch_widget_info:eq(1)', + "should have a second column"); + + const $widgetSecondColumn = $kanbanWidget.find('.o_lunch_widget_info:eq(1)'); + + assert.containsOnce($widgetSecondColumn, '.o_lunch_widget_unlink', + "should have a button to clear the order"); + + assert.containsOnce($widgetSecondColumn, '.o_lunch_widget_lines > li', + "should have 1 order line"); + + const $firstLine = $widgetSecondColumn.find('.o_lunch_widget_lines > li:first'); + assert.containsOnce($firstLine, 'button.o_remove_product', + "should have a button to remove a product quantity on each line"); + assert.containsOnce($firstLine, 'button.o_add_product', + "should have a button to add a product quantity on each line"); + assert.containsOnce($firstLine, '.o_lunch_product_quantity > :eq(1)', + "should have the line's quantity"); + assert.strictEqual($firstLine.find('.o_lunch_product_quantity > :eq(1)').text().trim(), "1", + "should have 1 as the line's quantity"); + assert.containsOnce($firstLine, '.o_lunch_open_wizard', + "should have the line's product name to open the wizard"); + assert.strictEqual($firstLine.find('.o_lunch_open_wizard').text().trim(), "Tuna sandwich", + "should have 'Tuna sandwich' as the line's product name"); + assert.containsOnce($firstLine, '.o_field_monetary', + "should have the line's amount"); + assert.strictEqual($firstLine.find('.o_field_monetary').text().trim(), "3.00€", + "should have '3.00€' as the line's amount"); + + assert.containsOnce($kanbanWidget, '.o_lunch_widget_info:eq(2)', + "should have a third column"); + + const $widgetThirdColumn = kanban.$('.o_lunch_widget .o_lunch_widget_info:eq(2)'); + + assert.containsOnce($widgetThirdColumn, '.o_field_monetary', + "should have an account balance"); + assert.strictEqual($widgetThirdColumn.find('.o_field_monetary').text().trim(), "3.00€", + "should have '3.00€' in the account balance"); + assert.containsOnce($widgetThirdColumn, '.o_lunch_widget_order_button', + "should have a button to validate the order"); + assert.strictEqual($widgetThirdColumn.find('.o_lunch_widget_order_button').text().trim(), "Order now", + "should have 'Order now' as the validate order button text"); + + kanban.destroy(); + }); + + QUnit.test('ordered cart', function (assert) { + assert.expect(15); + + const kanban = createLunchKanbanView({ + View: LunchKanbanView, + model: 'product', + data: this.data, + arch: ` + <kanban> + <templates> + <t t-name="kanban-box"> + <div><field name="name"/></div> + </t> + </templates> + </kanban> + `, + mockRPC: mockLunchRPC({ + infos: Object.assign({}, this.regularInfos, { + raw_state: "ordered", + state: "Ordered", + lines: [ + { + product: [1, "Tuna sandwich", "3.00"], + toppings: [], + quantity: 1.0, + }, + ], + }), + userLocation: this.data['lunch.location'].records[0].id, + }), + }); + + const $kanbanWidget = kanban.$('.o_lunch_widget'); + + assert.containsN($kanbanWidget, '> .o_lunch_widget_info', 3, + "should have 3 columns"); + + assert.containsOnce($kanbanWidget, '.o_lunch_widget_info:eq(1)', + "should have a second column"); + + const $widgetSecondColumn = $kanbanWidget.find('.o_lunch_widget_info:eq(1)'); + + assert.containsOnce($widgetSecondColumn, '.o_lunch_widget_unlink', + "should have a button to clear the order"); + assert.containsOnce($widgetSecondColumn, '.badge.badge-warning.o_lunch_ordered', + "should have an ordered state badge"); + assert.strictEqual($widgetSecondColumn.find('.o_lunch_ordered').text().trim(), "Ordered", + "should have 'Ordered' in the state badge"); + + assert.containsOnce($widgetSecondColumn, '.o_lunch_widget_lines > li', + "should have 1 order line"); + + const $firstLine = $widgetSecondColumn.find('.o_lunch_widget_lines > li:first'); + assert.containsOnce($firstLine, 'button.o_remove_product', + "should have a button to remove a product quantity on each line"); + assert.containsOnce($firstLine, 'button.o_add_product', + "should have a button to add a product quantity on each line"); + assert.containsOnce($firstLine, '.o_lunch_product_quantity > :eq(1)', + "should have the line's quantity"); + assert.strictEqual($firstLine.find('.o_lunch_product_quantity > :eq(1)').text().trim(), "1", + "should have 1 as the line's quantity"); + assert.containsOnce($firstLine, '.o_lunch_open_wizard', + "should have the line's product name to open the wizard"); + assert.strictEqual($firstLine.find('.o_lunch_open_wizard').text().trim(), "Tuna sandwich", + "should have 'Tuna sandwich' as the line's product name"); + assert.containsOnce($firstLine, '.o_field_monetary', + "should have the line's amount"); + assert.strictEqual($firstLine.find('.o_field_monetary').text().trim(), "3.00€", + "should have '3.00€' as the line's amount"); + + assert.strictEqual($kanbanWidget.find('> .o_lunch_widget_info:eq(2)').html().trim(), "", + "third column should be empty"); + + kanban.destroy(); + }); + + QUnit.test('confirmed cart', function (assert) { + assert.expect(15); + + const kanban = createLunchKanbanView({ + View: LunchKanbanView, + model: 'product', + data: this.data, + arch: ` + <kanban> + <templates> + <t t-name="kanban-box"> + <div><field name="name"/></div> + </t> + </templates> + </kanban> + `, + mockRPC: mockLunchRPC({ + infos: Object.assign({}, this.regularInfos, { + raw_state: "confirmed", + state: "Received", + lines: [ + { + product: [1, "Tuna sandwich", "3.00"], + toppings: [], + quantity: 1.0, + }, + ], + }), + userLocation: this.data['lunch.location'].records[0].id, + }), + }); + + const $kanbanWidget = kanban.$('.o_lunch_widget'); + + assert.containsN($kanbanWidget, '> .o_lunch_widget_info', 3, + "should have 3 columns"); + + assert.containsOnce($kanbanWidget, '.o_lunch_widget_info:eq(1)', + "should have a second column"); + + const $widgetSecondColumn = $kanbanWidget.find('.o_lunch_widget_info:eq(1)'); + + assert.containsNone($widgetSecondColumn, '.o_lunch_widget_unlink', + "shouldn't have a button to clear the order"); + assert.containsOnce($widgetSecondColumn, '.badge.badge-success.o_lunch_confirmed', + "should have a confirmed state badge"); + assert.strictEqual($widgetSecondColumn.find('.o_lunch_confirmed').text().trim(), "Received", + "should have 'Received' in the state badge"); + + assert.containsOnce($widgetSecondColumn, '.o_lunch_widget_lines > li', + "should have 1 order line"); + + const $firstLine = $widgetSecondColumn.find('.o_lunch_widget_lines > li:first'); + assert.containsNone($firstLine, 'button.o_remove_product', + "shouldn't have a button to remove a product quantity on each line"); + assert.containsNone($firstLine, 'button.o_add_product', + "shouldn't have a button to add a product quantity on each line"); + assert.containsOnce($firstLine, '.o_lunch_product_quantity', + "should have the line's quantity"); + assert.strictEqual($firstLine.find('.o_lunch_product_quantity').text().trim(), "1", + "should have 1 as the line's quantity"); + assert.containsOnce($firstLine, '.o_lunch_open_wizard', + "should have the line's product name to open the wizard"); + assert.strictEqual($firstLine.find('.o_lunch_open_wizard').text().trim(), "Tuna sandwich", + "should have 'Tuna sandwich' as the line's product name"); + assert.containsOnce($firstLine, '.o_field_monetary', + "should have the line's amount"); + assert.strictEqual($firstLine.find('.o_field_monetary').text().trim(), "3.00€", + "should have '3.00€' as the line's amount"); + + assert.strictEqual($kanbanWidget.find('> .o_lunch_widget_info:eq(2)').html().trim(), "", + "third column should be empty"); + + kanban.destroy(); + }); + + QUnit.test('regular user', function (assert) { + assert.expect(11); + + const kanban = createLunchKanbanView({ + View: LunchKanbanView, + model: 'product', + data: this.data, + arch: ` + <kanban> + <templates> + <t t-name="kanban-box"> + <div><field name="name"/></div> + </t> + </templates> + </kanban> + `, + mockRPC: mockLunchRPC({ + infos: this.regularInfos, + userLocation: this.data['lunch.location'].records[0].id, + }), + }); + + const $kanbanWidget = kanban.$('.o_lunch_widget'); + + assert.containsOnce($kanbanWidget, '.o_lunch_widget_info:first', + "should have a first column"); + + const $widgetFirstColumn = $kanbanWidget.find('.o_lunch_widget_info:first'); + + assert.containsOnce($widgetFirstColumn, 'img.rounded-circle', + "should have a rounded avatar image"); + + assert.containsOnce($widgetFirstColumn, '.o_lunch_user_field', + "should have a user field"); + assert.containsNone($widgetFirstColumn, '.o_lunch_user_field > .o_field_widget', + "shouldn't have a field widget in the user field"); + assert.strictEqual($widgetFirstColumn.find('.o_lunch_user_field').text().trim(), "Marc Demo", + "should have 'Marc Demo' in the user field"); + + assert.containsOnce($widgetFirstColumn, '.o_lunch_location_field', + "should have a location field"); + assert.containsOnce($widgetFirstColumn, '.o_lunch_location_field > .o_field_many2one[name="locations"]', + "should have a many2one in the location field"); + + testUtils.fields.many2one.clickOpenDropdown('locations'); + const $input = $widgetFirstColumn.find('.o_field_many2one[name="locations"] input'); + assert.containsN($input.autocomplete('widget'), 'li', 2, + "autocomplete dropdown should have 2 entries"); + assert.strictEqual($input.val(), "Office 2", + "locations input should have 'Office 2' as value"); + + assert.containsOnce($widgetFirstColumn, '.o_lunch_location_field + div', + "should have an account balance"); + assert.strictEqual($widgetFirstColumn.find('.o_lunch_location_field + div .o_field_monetary').text().trim(), "36.50€", + "should have '36.50€' in the account balance"); + + kanban.destroy(); + }); + + QUnit.test('manager user', function (assert) { + assert.expect(12); + + const kanban = createLunchKanbanView({ + View: LunchKanbanView, + model: 'product', + data: this.data, + arch: ` + <kanban> + <templates> + <t t-name="kanban-box"> + <div><field name="name"/></div> + </t> + </templates> + </kanban> + `, + mockRPC: mockLunchRPC({ + infos: this.managerInfos, + userLocation: this.data['lunch.location'].records[0].id, + }), + }); + + const $kanbanWidget = kanban.$('.o_lunch_widget'); + + assert.containsOnce($kanbanWidget, '.o_lunch_widget_info:first', + "should have a first column"); + + const $widgetFirstColumn = $kanbanWidget.find('.o_lunch_widget_info:first'); + + assert.containsOnce($widgetFirstColumn, 'img.rounded-circle', + "should have a rounded avatar image"); + + assert.containsOnce($widgetFirstColumn, '.o_lunch_user_field', + "should have a user field"); + assert.containsOnce($widgetFirstColumn, '.o_lunch_user_field > .o_field_many2one[name="users"]', + "shouldn't have a field widget in the user field"); + + testUtils.fields.many2one.clickOpenDropdown('users'); + const $userInput = $widgetFirstColumn.find('.o_field_many2one[name="users"] input'); + assert.containsN($userInput.autocomplete('widget'), 'li', 2, + "users autocomplete dropdown should have 2 entries"); + assert.strictEqual($userInput.val(), "Mitchell Admin", + "should have 'Mitchell Admin' as value in user field"); + + assert.containsOnce($widgetFirstColumn, '.o_lunch_location_field', + "should have a location field"); + assert.containsOnce($widgetFirstColumn, '.o_lunch_location_field > .o_field_many2one[name="locations"]', + "should have a many2one in the location field"); + + testUtils.fields.many2one.clickOpenDropdown('locations'); + const $locationInput = $widgetFirstColumn.find('.o_field_many2one[name="locations"] input'); + assert.containsN($locationInput.autocomplete('widget'), 'li', 2, + "locations autocomplete dropdown should have 2 entries"); + assert.strictEqual($locationInput.val(), "Office 2", + "should have 'Office 2' as value"); + + assert.containsOnce($widgetFirstColumn, '.o_lunch_location_field + div', + "should have an account balance"); + assert.strictEqual($widgetFirstColumn.find('.o_lunch_location_field + div .o_field_monetary').text().trim(), "47.60€", + "should have '47.60€' in the account balance"); + + kanban.destroy(); + }); + + QUnit.test('add a product', function (assert) { + assert.expect(1); + + const kanban = createLunchKanbanView({ + View: LunchKanbanView, + model: 'product', + data: this.data, + arch: ` + <kanban> + <templates> + <t t-name="kanban-box"> + <div><field name="name"/></div> + </t> + </templates> + </kanban> + `, + mockRPC: mockLunchRPC({ + infos: this.regularInfos, + userLocation: this.data['lunch.location'].records[0].id, + }), + intercepts: { + do_action: function (ev) { + assert.deepEqual(ev.data.action, { + res_model: 'lunch.order.temp', + type: 'ir.actions.act_window', + views: [[false, 'form']], + target: 'new', + context: { + default_product_id: undefined, + line_id: false, + }, + }, + "should open the wizard"); + }, + }, + }); + + testUtils.dom.click(kanban.$('.o_kanban_record:first')); + + kanban.destroy(); + }); + + QUnit.test('add product quantity', function (assert) { + assert.expect(3); + + const kanban = createLunchKanbanView({ + View: LunchKanbanView, + model: 'product', + data: Object.assign({}, this.data, { + 'lunch.order': { + fields: {}, + update_quantity([lineIds, increment]) { + assert.deepEqual(lineIds, [6], "should have [6] as lineId to update quantity"); + assert.strictEqual(increment, 1, "should have +1 as increment to update quantity"); + return $.when(); + }, + }, + }), + arch: ` + <kanban> + <templates> + <t t-name="kanban-box"> + <div><field name="name"/></div> + </t> + </templates> + </kanban> + `, + mockRPC: mockLunchRPC({ + infos: Object.assign({}, this.regularInfos, { + lines: [ + { + id: 6, + product: [1, "Tuna sandwich", "3.00"], + toppings: [], + quantity: 1.0, + }, + ], + }), + userLocation: this.data['lunch.location'].records[0].id, + }), + }); + + const $widgetSecondColumn = kanban.$('.o_lunch_widget .o_lunch_widget_info:eq(1)'); + + assert.containsOnce($widgetSecondColumn, '.o_lunch_widget_lines > li', + "should have 1 order line"); + + const $firstLine = $widgetSecondColumn.find('.o_lunch_widget_lines > li:first'); + + testUtils.dom.click($firstLine.find('button.o_add_product')); + + kanban.destroy(); + }); + + QUnit.test('remove product quantity', function (assert) { + assert.expect(3); + + const kanban = createLunchKanbanView({ + View: LunchKanbanView, + model: 'product', + data: Object.assign({}, this.data, { + 'lunch.order': { + fields: {}, + update_quantity([lineIds, increment]) { + assert.deepEqual(lineIds, [6], "should have [6] as lineId to update quantity"); + assert.strictEqual(increment, -1, "should have -1 as increment to update quantity"); + return $.when(); + }, + }, + }), + arch: ` + <kanban> + <templates> + <t t-name="kanban-box"> + <div><field name="name"/></div> + </t> + </templates> + </kanban> + `, + mockRPC: mockLunchRPC({ + infos: Object.assign({}, this.regularInfos, { + lines: [ + { + id: 6, + product: [1, "Tuna sandwich", "3.00"], + toppings: [], + quantity: 1.0, + }, + ], + }), + userLocation: this.data['lunch.location'].records[0].id, + }), + }); + + const $widgetSecondColumn = kanban.$('.o_lunch_widget .o_lunch_widget_info:eq(1)'); + + assert.containsOnce($widgetSecondColumn, '.o_lunch_widget_lines > li', + "should have 1 order line"); + + const $firstLine = $widgetSecondColumn.find('.o_lunch_widget_lines > li:first'); + + testUtils.dom.click($firstLine.find('button.o_remove_product')); + + kanban.destroy(); + }); + + QUnit.test('clear order', function (assert) { + assert.expect(1); + + const self = this; + const kanban = createLunchKanbanView({ + View: LunchKanbanView, + model: 'product', + data: this.data, + arch: ` + <kanban> + <templates> + <t t-name="kanban-box"> + <div><field name="name"/></div> + </t> + </templates> + </kanban> + `, + mockRPC: function (route) { + if (route.startsWith('/lunch')) { + if (route === '/lunch/trash') { + assert.ok('should perform clear order RPC call'); + return $.when(); + } + return mockLunchRPC({ + infos: Object.assign({}, self.regularInfos, { + lines: [ + { + product: [1, "Tuna sandwich", "3.00"], + toppings: [], + }, + ], + }), + userLocation: self.data['lunch.location'].records[0].id, + }).apply(this, arguments); + } + return this._super.apply(this, arguments); + }, + }); + + const $widgetSecondColumn = kanban.$('.o_lunch_widget .o_lunch_widget_info:eq(1)'); + + testUtils.dom.click($widgetSecondColumn.find('button.o_lunch_widget_unlink')); + + kanban.destroy(); + }); + + QUnit.test('validate order: success', function (assert) { + assert.expect(1); + + const self = this; + const kanban = createLunchKanbanView({ + View: LunchKanbanView, + model: 'product', + data: this.data, + arch: ` + <kanban> + <templates> + <t t-name="kanban-box"> + <div><field name="name"/></div> + </t> + </templates> + </kanban> + `, + mockRPC: function (route) { + if (route.startsWith('/lunch')) { + if (route === '/lunch/pay') { + assert.ok("should perform pay order RPC call"); + return $.when(true); + } + return mockLunchRPC({ + infos: Object.assign({}, self.regularInfos, { + lines: [ + { + product: [1, "Tuna sandwich", "3.00"], + toppings: [], + }, + ], + }), + userLocation: self.data['lunch.location'].records[0].id, + }).apply(this, arguments); + } + return this._super.apply(this, arguments); + }, + }); + + const $widgetThirdColumn = kanban.$('.o_lunch_widget .o_lunch_widget_info:eq(2)'); + + testUtils.dom.click($widgetThirdColumn.find('button.o_lunch_widget_order_button')); + + kanban.destroy(); + }); + + QUnit.test('validate order: failure', function (assert) { + assert.expect(5); + + const self = this; + const kanban = createLunchKanbanView({ + View: LunchKanbanView, + model: 'product', + data: this.data, + arch: ` + <kanban> + <templates> + <t t-name="kanban-box"> + <div><field name="name"/></div> + </t> + </templates> + </kanban> + `, + mockRPC: function (route) { + if (route.startsWith('/lunch')) { + if (route === '/lunch/pay') { + assert.ok('should perform pay order RPC call'); + return $.when(false); + } + if (route === '/lunch/payment_message') { + assert.ok('should perform payment message RPC call'); + return $.when({ message: 'This is a payment message.'}); + } + return mockLunchRPC({ + infos: Object.assign({}, self.regularInfos, { + lines: [ + { + product: [1, "Tuna sandwich", "3.00"], + toppings: [], + }, + ], + }), + userLocation: self.data['lunch.location'].records[0].id, + }).apply(this, arguments); + } + return this._super.apply(this, arguments); + }, + }); + + const $widgetThirdColumn = kanban.$('.o_lunch_widget .o_lunch_widget_info:eq(2)'); + + testUtils.dom.click($widgetThirdColumn.find('button.o_lunch_widget_order_button')); + + assert.containsOnce(document.body, '.modal', "should open a Dialog box"); + assert.strictEqual($('.modal-title').text().trim(), + "Not enough money in your wallet", "should have a Dialog's title"); + assert.strictEqual($('.modal-body').text().trim(), + "This is a payment message.", "should have a Dialog's message"); + + kanban.destroy(); + }); + + }); +}); + +}); diff --git a/addons/lunch/static/tests/lunch_test_utils.js b/addons/lunch/static/tests/lunch_test_utils.js new file mode 100644 index 000000000000..3c8b52d18e3e --- /dev/null +++ b/addons/lunch/static/tests/lunch_test_utils.js @@ -0,0 +1,58 @@ +odoo.define('lunch.test_utils', function (require) { +"use strict"; + +const AbstractStorageService = require('web.AbstractStorageService'); +const RamStorage = require('web.RamStorage'); +const {createView} = require('web.test_utils'); + +/** + * Helper to create a lunch kanban view with searchpanel + * + * @param {object} params + */ +function createLunchKanbanView(params) { + const archPieces = params.arch.split('</templates>'); + params.arch = ` + ${archPieces[0]}</templates> + <searchpanel> + <field name="category_id" select="multi" string="Categories"/> + <field name="supplier_id" select="multi" string="Vendors"/> + </searchpanel> + ${archPieces[1]} + `; + if (!params.services || !params.services.local_storage) { + // the searchPanel uses the localStorage to store/retrieve default + // active category value + params.services = params.services || {}; + const RamStorageService = AbstractStorageService.extend({ + storage: new RamStorage(), + }); + params.services.local_storage = RamStorageService; + } + return createView(params); +} + +/** + * Helper to generate a mockRPC function for the mandatory lunch routes (prefixed by '/lunch') + * + * @param {object} infos + * @param {integer} userLocation + */ +function mockLunchRPC({infos, userLocation}) { + return function (route) { + if (route === '/lunch/infos') { + return $.when(infos); + } + if (route === '/lunch/user_location_get') { + return $.when(userLocation); + } + return this._super.apply(this, arguments); + }; +} + +return { + createLunchKanbanView, + mockLunchRPC, +}; + +}); diff --git a/addons/lunch/static/tests/test_lunch_kanban.js b/addons/lunch/static/tests/test_lunch_kanban.js deleted file mode 100644 index 020c37481503..000000000000 --- a/addons/lunch/static/tests/test_lunch_kanban.js +++ /dev/null @@ -1,259 +0,0 @@ -odoo.define('lunch.lunchKanbanTests', function (require) { -"use strict"; - -var LunchKanbanView = require('lunch.LunchKanbanView'); -var LunchKanbanWidget = require('lunch.LunchKanbanWidget'); -var testUtils = require('web.test_utils'); - -var createView = testUtils.createView; - -QUnit.module('Views', { - beforeEach: function () { - this.data = { - product: { - fields: { - id: {string: 'ID', type: 'integer'}, - is_available_at: {string: 'available_location_ids', type: 'integer'}, - }, - records: [ - {id: 1, name: 'Tuna sandwich', price: 3.0, is_available_at: 1}, - ] - }, - }; - }, -}, function () { - QUnit.module('LunchKanbanView'); - - QUnit.test('Simple rendering of LunchKanbanView', function (assert) { - assert.expect(7); - - var lunchKanban = createView({ - View: LunchKanbanView, - model: 'product', - data: this.data, - arch: '<kanban><templates><t t-name="kanban-box">' + - '<div>' + - '</div>' + - '</t></templates></kanban>', - mockRPC: function(route, args) { - if (route === '/lunch/infos') { - return $.when({ - order: 1, - wallet: 20, - username: 'Marc Demo', - userimage: '', - is_manager: false, - users: [ - {id: 1, name: 'Mitchell Admin'}, - {id: 2, name: 'Marc Demo'}, - ], - total: 7.4, - state: 'new', - lines: [ - {id: 1, product: ['Pizza Italiana', 7.4], toppings: [], quantity: 1.0, price: 7.4} - ], - alerts: [], - locations: ['hello', 'hallo'], - user_location: [1, 'hello'], - }); - } else if (route === '/lunch/user_location_get') { - return $.when(1); - } else if (route === '/web/dataset/call_kw/ir.model.data/xmlid_to_res_id') { - return $.when(); - } else if (route.startsWith('data:image/png;base64,')) { - return $.when(); - } - return this._super(route, args); - }, - }); - - var $section = $(lunchKanban.$('.o_lunch_widget_info')[0]); - // username - assert.strictEqual($section.find('div:eq(1) div:eq(0)').text().trim(), 'Marc Demo', 'Username should have been Marc Demo'); - // your order section - $section = $(lunchKanban.$('.o_lunch_widget_info')[1]); - // only one line - assert.strictEqual($section.find('div:eq(2) div.d-flex').length, 1, 'There should be only one line'); - // quantity = 1 - assert.strictEqual($section.find('div:eq(2) div:eq(0) span:eq(0)').text().trim(), '1', 'The line should contain only one product'); - // buttons to remove and to add product - assert.strictEqual($section.find('.o_remove_product').length, 1, 'There should be a remove product button'); - assert.strictEqual($section.find('.o_add_product').length, 1, 'There should be a add product button'); - - $section = $(lunchKanban.$('.o_lunch_widget_info')[2]); - // total - assert.strictEqual($section.find('div:eq(0) div:eq(1)').text().trim(), '7.40', 'total should be of 7.40'); - // order now button - assert.strictEqual($section.find('button').length, 1, 'order now button should be available'); - - lunchKanban.destroy(); - }); - - QUnit.test('User interactions', function (assert) { - assert.expect(9); - - var state = 'new'; - - var lunchKanban = createView({ - View: LunchKanbanView, - model: 'product', - data: this.data, - arch: '<kanban><templates><t t-name="kanban-box">' + - '<div>' + - '</div>' + - '</t></templates></kanban>', - mockRPC: function(route, args) { - if (route === '/lunch/infos') { - return $.when({ - order: 1, - wallet: 20, - username: 'Marc Demo', - userimage: '', - is_manager: false, - users: [ - {id: 1, name: 'Mitchell Admin'}, - {id: 2, name: 'Marc Demo'}, - ], - total: 7.4, - raw_state: state, - state: 'New', - lines: [ - {id: 1, product: ['Pizza Italiana', 7.4], toppings: [], quantity: 1.0, price: 7.4} - ], - alerts: [], - locations: ['hello', 'hallo'], - user_location: [1, 'hello'], - }); - } else if (route === '/lunch/payment_message') { - return $.when({message: 'Hello'}); - } else if (route === '/lunch/pay') { - return $.when(true); - } else if (route === '/lunch/user_location_get') { - return $.when(1); - } else if (args.method === 'update_quantity') { - assert.step(args.args); - return $.when(); - } else if (route === '/web/dataset/call_kw/ir.model.data/xmlid_to_res_id') { - return $.when(); - } else if (route.startsWith('data:image/png;base64,')) { - return $.when(); - } - return this._super(route, args); - }, - }); - - lunchKanban.$('.o_add_product').click(); - lunchKanban.$('.o_remove_product').click(); - - state = 'ordered'; - lunchKanban.$('.o_lunch_widget_order_button').click(); - // state is shown - assert.strictEqual(lunchKanban.$('.o_lunch_ordered').length, 1, 'state should be shown as ordered'); - // buttons - assert.strictEqual(lunchKanban.$('.o_remove_product').length, 1, 'button to remove product should be shown'); - assert.strictEqual(lunchKanban.$('.o_add_product').length, 1, 'button to add product should be shown'); - state = 'confirmed'; - lunchKanban.reload(); - // state is updated - assert.strictEqual(lunchKanban.$('.o_lunch_confirmed').length, 1, 'state should be shown as confirmed'); - // Buttons not shown anymore - assert.strictEqual(lunchKanban.$('.o_remove_product').length, 0, 'button to remove product should not be shown'); - assert.strictEqual(lunchKanban.$('.o_add_product').length, 0, 'button to add product should not be shown'); - - - assert.verifySteps([ - [[1], 1], - [[1], -1], - ]); - - lunchKanban.destroy(); - }); - - QUnit.test('Manager interactions', function (assert) { - assert.expect(1); - - var lunchKanban = createView({ - View: LunchKanbanView, - model: 'product', - data: this.data, - arch: '<kanban><templates><t t-name="kanban-box">' + - '<div>' + - '</div>' + - '</t></templates></kanban>', - mockRPC: function(route, args) { - if (route === '/lunch/infos') { - return $.when({ - order: 1, - wallet: 20, - username: 'Marc Demo', - userimage: '', - is_manager: true, - users: [ - {id: 1, name: 'Mitchell Admin'}, - {id: 2, name: 'Marc Demo'}, - ], - total: 7.4, - raw_state: 'new', - state: 'New', - lines: [ - {id: 1, product: ['Pizza Italiana', 7.4], toppings: [], quantity: 1.0, price: 7.4} - ], - alerts: [], - locations: ['hello', 'hallo'], - user_location: [1, 'hello'], - }); - } else if (route === '/lunch/user_location_get') { - return $.when(1); - } else if (route === '/web/dataset/call_kw/ir.model.data/xmlid_to_res_id') { - return $.when(); - } else if (route.startsWith('data:image/png;base64,')) { - return $.when(); - } - return this._super(route, args); - }, - }); - - var select = lunchKanban.$('div.o_lunch_user_field div.o_field_widget.o_field_many2one'); - - assert.strictEqual(select.length, 1, 'There should be a user selection field'); - - lunchKanban.destroy(); - }); - - QUnit.module('LunchKanbanWidget'); - - QUnit.test('Rpc calls should be performed after the init()', function (assert) { - assert.expect(2); - - var parent = testUtils.createParent({}); - - var widget = new LunchKanbanWidget(parent, { - user_location: [1, 'hello'], - }); - - testUtils.mock.addMockEnvironment(widget, { - data: {}, - mockRPC: function (route, args) { - if (route === '/web/dataset/call_kw/ir.model.data/xmlid_to_res_id') { - assert.ok('should perform xmlid_to_res_id RPC call in the willStart()'); - assert.deepEqual(args, { - args: [], - kwargs: { - xmlid: "base.group_portal", - }, - method: "xmlid_to_res_id", - model: "ir.model.data", - }, 'should have the right parameters for xmlid_to_res_id RPC call'); - return $.when(); - } - return this._super.apply(this, arguments); - }, - }); - - widget.appendTo($("#qunit-fixture")); - - parent.destroy(); - }); -}); - -}); diff --git a/addons/lunch/views/lunch_templates.xml b/addons/lunch/views/lunch_templates.xml index a076bbb22619..40c57da0b79a 100644 --- a/addons/lunch/views/lunch_templates.xml +++ b/addons/lunch/views/lunch_templates.xml @@ -11,16 +11,25 @@ <script type="text/javascript" src="/lunch/static/src/js/lunch_kanban_renderer.js"></script> <script type="text/javascript" src="/lunch/static/src/js/lunch_kanban_record.js"></script> <script type="text/javascript" src="/lunch/static/src/js/lunch_kanban_model.js"></script> + <script type="text/javascript" src="/lunch/static/src/js/lunch_kanban_mobile.js"></script> </xpath> </template> <template id="qunit_suite" name="lunch tests" inherit_id="web.qunit_suite"> <xpath expr="//t[@t-set='head']" position="inside"> - <script type="text/javascript" src="/lunch/static/tests/test_lunch_kanban.js"></script> + <script type="text/javascript" src="/lunch/static/tests/lunch_test_utils.js"></script> + <script type="text/javascript" src="/lunch/static/tests/lunch_kanban_tests.js"></script> + </xpath> + </template> + + <template id="qunit_mobile_suite" name="lunch mobile tests" inherit_id="web.qunit_mobile_suite"> + <xpath expr="//t[@t-set='head']" position="inside"> + <script type="text/javascript" src="/lunch/static/tests/lunch_test_utils.js"></script> + <script type="text/javascript" src="/lunch/static/tests/lunch_kanban_mobile_tests.js"></script> </xpath> </template> <template id="lunch_payment_dialog" name="Lunch Payment Dialog"> - To add some money to your wallet, please contact your lunch manager. + To add some money to your wallet, please contact your lunch manager. </template> </odoo> diff --git a/addons/web/static/src/scss/search_view_mobile.scss b/addons/web/static/src/scss/search_view_mobile.scss index c3c264649ea8..dd72aaae83ba 100644 --- a/addons/web/static/src/scss/search_view_mobile.scss +++ b/addons/web/static/src/scss/search_view_mobile.scss @@ -7,7 +7,7 @@ padding: 0; width: 100%; background-color: white; - z-index: 1000; + z-index: $zindex-modal; overflow: auto; .o_mobile_search_header { height: 46px; @@ -89,7 +89,6 @@ right: 0; padding: 15px; font-size: 17px; - z-index: 1000; } } } -- GitLab