19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:32:43 +01:00
parent 4607ccbd2e
commit 825ff6514e
487 changed files with 184979 additions and 195262 deletions

View file

@ -0,0 +1,77 @@
import { defineMailModels } from "@mail/../tests/mail_test_helpers";
import { expect, test } from "@odoo/hoot";
import { click } from "@odoo/hoot-dom";
import { animationFrame } from "@odoo/hoot-mock";
import { defineModels, fields, models, mountView, onRpc } from "@web/../tests/web_test_helpers";
class LunchProduct extends models.Model {
_name = "lunch.product";
name = fields.Char();
is_favorite = fields.Boolean();
_records = [
{
id: 1,
name: "Product A",
},
];
_views = {
kanban: `
<kanban class="o_kanban_test" edit="0">
<template>
<t t-name="card">
<field name="is_favorite" widget="lunch_is_favorite" nolabel="1"/>
<field name="name"/>
</t>
</template>
</kanban>
`,
};
}
defineMailModels();
defineModels([LunchProduct]);
test("Check is_favorite field is still editable even if the record/view is in readonly.", async () => {
onRpc("lunch.product", "web_save", ({ args }) => {
const [ids, vals] = args;
expect(ids).toEqual([1]);
expect(vals).toEqual({ is_favorite: true });
expect.step("web_save");
});
await mountView({
resModel: "lunch.product",
type: "kanban",
});
expect("div[name=is_favorite] .o_favorite").toHaveCount(1);
expect.verifySteps([]);
await click("div[name=is_favorite] .o_favorite");
await animationFrame();
expect.verifySteps(["web_save"]);
});
test("Check is_favorite field is readonly if the field is readonly", async () => {
onRpc("lunch.product", "web_save", () => {
expect.step("web_save");
});
LunchProduct._views["kanban"] = LunchProduct._views["kanban"].replace(
'widget="lunch_is_favorite"',
'widget="lunch_is_favorite" readonly="1"'
);
await mountView({
resModel: "lunch.product",
type: "kanban",
});
expect("div[name=is_favorite] .o_favorite").toHaveCount(1);
expect.verifySteps([]);
await click("div[name=is_favorite] .o_favorite");
await animationFrame();
expect.verifySteps([]);
});

View file

@ -0,0 +1,368 @@
import { LunchKanbanRenderer } from "@lunch/views/kanban";
import { defineMailModels, mailModels } from "@mail/../tests/mail_test_helpers";
import { describe, expect, test } from "@odoo/hoot";
import {
contains,
defineModels,
fields,
models,
mountView,
onRpc,
patchWithCleanup,
} from "@web/../tests/web_test_helpers";
const lunchInfos = {
username: "Johnny Hache",
wallet: 12.05,
wallet_with_config: 12.05,
is_manager: false,
currency: {
symbol: "€",
position: "after",
},
user_location: [1, "Old Office"],
alerts: [],
lines: [],
};
async function mountLunchView() {
return mountView({
type: "kanban",
resModel: "lunch.product",
arch: `
<kanban js_class="lunch_kanban">
<templates>
<t t-name="card">
<field name="name"/>
<field name="price"/>
</t>
</templates>
</kanban>`,
});
}
class Product extends models.Model {
_name = "lunch.product";
name = fields.Char();
is_available_at = fields.Integer({ string: "Available" });
price = fields.Float();
_records = [
{ id: 1, name: "Big Plate", is_available_at: 1, price: 4.95 },
{ id: 2, name: "Small Plate", is_available_at: 2, price: 6.99 },
{ id: 3, name: "Just One Plate", is_available_at: 2, price: 5.87 },
];
}
class Location extends models.Model {
_name = "lunch.location";
name = fields.Char();
_records = [
{ id: 1, name: "Old Office" },
{ id: 2, name: "New Office" },
];
}
class Order extends models.Model {
_name = "lunch.order";
product_id = fields.Many2one({
string: "Product",
relation: "lunch.product",
});
_views = {
form: `<form>
<sheet>
<field name="product_id" readonly="1"/>
</sheet>
<footer>
<button name="add_to_cart" type="object" string="Add to cart" />
<button string="Discard" special="cancel"/>
</footer>
</form>`,
};
}
defineMailModels();
defineModels([Product, Location, Order]);
describe.current.tags("desktop");
onRpc("/lunch/user_location_get", function () {
return this.env["lunch.location"][0].id;
});
onRpc("/lunch/infos", () => lunchInfos);
test("Basic rendering", async () => {
await mountLunchView();
expect(".o_lunch_banner").toHaveCount(1);
expect(".o_lunch_content .alert").toHaveCount(0);
expect(".o_kanban_record:not(.o_kanban_ghost)").toHaveCount(1);
expect(".o_lunch_banner .lunch_user span").toHaveText("Johnny Hache");
});
test("Open product", async () => {
expect.assertions(2);
await mountLunchView();
patchWithCleanup(LunchKanbanRenderer.prototype, {
openOrderLine(productId, orderId) {
expect(productId).toBe(1);
},
});
expect(".o_kanban_record:not(.o_kanban_ghost)").toHaveCount(1);
await contains(".o_kanban_record:not(.o_kanban_ghost)").click();
});
test("Basic rendering with alerts", async () => {
expect.assertions(2);
const userInfos = {
...lunchInfos,
alerts: [
{
id: 1,
message: "<b>free boudin compote for everyone</b>",
},
],
};
onRpc("/lunch/user_location_get", () => userInfos.user_location[0]);
onRpc("/lunch/infos", () => userInfos);
await mountLunchView();
expect(".o_lunch_content .alert").toHaveCount(1);
expect(".o_lunch_content .alert").toHaveText("free boudin compote for everyone");
});
test("Location change", async () => {
expect.assertions(3);
const userInfos = { ...lunchInfos };
onRpc("/lunch/user_location_get", () => userInfos.user_location[0]);
onRpc("/lunch/user_location_set", async (request) => {
const { params } = await request.json();
expect(params.location_id).toBe(2);
userInfos.user_location = [2, "New Office"];
return true;
});
await mountLunchView();
await contains(".lunch_location input").click();
expect(".lunch_location .dropdown-item:contains(New Office)").toHaveCount(1);
await contains(
".lunch_location li:not(.o_m2o_dropdown_option) .dropdown-item:not(.ui-state-active)"
).click();
expect("article.o_kanban_record").toHaveCount(2);
});
test("Manager: user change", async () => {
expect.assertions(8);
mailModels.ResUsers._records.push(
{ id: 1, name: "Johnny Hache" },
{ id: 2, name: "David Elora" }
);
let userInfos = { ...lunchInfos, is_manager: true };
let expectedUserId = false; // false as we are requesting for the current user
onRpc("/lunch/user_location_get", () => userInfos.user_location[0]);
onRpc("/lunch/infos", async (request) => {
const { params } = await request.json();
expect(expectedUserId).toBe(params.user_id);
if (expectedUserId === 2) {
userInfos = {
...userInfos,
username: "David Elora",
wallet: -10000,
};
}
return userInfos;
});
onRpc("/lunch/user_location_set", async (request) => {
const { params } = await request.json();
expect(params.location_id).toBe(2);
userInfos.user_location = [2, "New Office"];
return true;
});
await mountLunchView();
expect(".lunch_user input").toHaveCount(1);
await contains(".lunch_user input").click();
expect(".lunch_user .dropdown-item:contains(David Elora)").toHaveCount(1);
expectedUserId = 2;
await contains(
".lunch_user li:not(.o_m2o_dropdown_option) .dropdown-item:contains('David Elora')"
).click();
expect(".o_lunch_banner span[name='o_lunch_balance']").toHaveText("Available Balance\n-10000.00€", {
message: "David Elora is poor",
});
await contains(".lunch_location input").click();
await contains(".lunch_location li:not(.o_m2o_dropdown_option) .dropdown-item:eq(1)").click();
expect(".lunch_user input").toHaveValue("David Elora", {
message: "changing location should not reset user",
});
});
test("Trash existing order", async () => {
expect.assertions(5);
let userInfos = {
...lunchInfos,
lines: [
{
id: 1,
product: [1, "Big Plate", "4.95", 4.95],
toppings: [],
quantity: 1,
price: 4.95,
raw_state: "new",
state: "To Order",
note: false,
},
],
raw_state: "new",
total: "4.95",
paid_subtotal: "0",
unpaid_subtotal: "4.95",
};
onRpc("/lunch/user_location_get", () => userInfos.user_location[0]);
onRpc("/lunch/infos", () => userInfos);
onRpc("/lunch/trash", () => {
userInfos = {
...userInfos,
lines: [],
raw_state: false,
total: 0,
};
return true;
});
await mountLunchView();
expect("div.o_lunch_banner > div > div").toHaveCount(3);
expect("div.o_lunch_banner div[name='o_lunch_order_buttons'] > button:contains(Clear Order)").toHaveCount(1, {
message: "should have clear order button",
});
expect("div.o_lunch_banner li[name='o_lunch_order_line']").toHaveCount(1, {
message: "should have one order line",
});
expect("div.o_lunch_banner div[name='o_lunch_order_buttons'] > button:contains(Order Now)").toHaveCount(
1
);
await contains("div.o_lunch_banner > div button:contains(Clear Order)").click();
expect("div.o_lunch_banner li[name='o_lunch_order_line']").toHaveCount(0);
});
test("Change existing order", async () => {
expect.assertions(1);
let userInfos = {
...lunchInfos,
lines: [
{
id: 1,
product: [1, "Big Plate", "4.95", 4.95],
toppings: [],
quantity: 1,
price: 4.95,
raw_state: "new",
state: "To Order",
note: false,
},
],
raw_state: "new",
total: "4.95",
paid_subtotal: "0",
unpaid_subtotal: "4.95",
};
onRpc("/lunch/user_location_get", () => userInfos.user_location[0]);
onRpc("/lunch/infos", () => userInfos);
onRpc("lunch.order", "update_quantity", ({ args }) => {
expect(args[1]).toBe(1, { message: "should increment order quantity by 1" });
userInfos = {
...userInfos,
lines: [
{
...userInfos.lines[0],
product: [1, "Big Plate", "9.9", 4.95],
quantity: 2,
price: 4.95 * 2,
},
],
total: 4.95 * 2,
unpaid_subtotal: 4.95 * 2,
};
return true;
});
await mountLunchView();
await contains("div.o_lunch_banner li[name='o_lunch_order_line']:contains(Big Plate) i.oi-plus").click();
});
test("Confirm existing order", async () => {
expect.assertions(3);
let userInfos = {
...lunchInfos,
lines: [
{
id: 1,
product: [1, "Big Plate", "4.95", 4.95],
toppings: [],
quantity: 1,
price: 4.95,
raw_state: "new",
state: "To Order",
note: false,
},
],
raw_state: "new",
total: "4.95",
paid_subtotal: "0",
unpaid_subtotal: "4.95",
};
onRpc("/lunch/user_location_get", () => userInfos.user_location[0]);
onRpc("/lunch/infos", () => userInfos);
onRpc("/lunch/pay", async (request) => {
const { params } = await request.json();
expect(params.user_id).toBe(false); // Should confirm order of current user
userInfos = {
...userInfos,
lines: [
{
...userInfos.lines[0],
raw_state: "ordered",
state: "Ordered,",
},
],
raw_state: "ordered",
wallet: userInfos.wallet - 4.95,
};
return true;
});
await mountLunchView();
expect("div.o_lunch_banner span[name='o_lunch_balance'] span:nth-child(2)").toHaveText("12.05€");
await contains("div.o_lunch_banner div[name='o_lunch_order_buttons'] > button:contains(Order Now)").click();
expect("div.o_lunch_banner span[name='o_lunch_balance'] span:nth-child(2)").toHaveText("7.10€", {
message: "Wallet should update",
});
});

View file

@ -1,204 +0,0 @@
odoo.define('lunch.lunchKanbanMobileTests', function (require) {
"use strict";
const LunchKanbanView = require('lunch.LunchKanbanView');
const testUtils = require('web.test_utils');
const {createLunchView, mockLunchRPC} = require('lunch.test_utils');
QUnit.module('Views');
QUnit.module('LunchKanbanView Mobile', {
beforeEach() {
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 Promise.resolve();
},
},
'lunch.product.category': {
fields: {},
records: [],
},
'lunch.supplier': {
fields: {},
records: [],
},
'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', async function (assert) {
assert.expect(7);
const kanban = await createLunchView({
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_legacy_kanban_view .o_kanban_record:not(.o_kanban_ghost)',
"should have 1 records in the renderer");
// check view layout
assert.containsOnce(kanban, '.o_content > .o_lunch_content',
"should have a 'kanban lunch wrapper' column");
assert.containsOnce(kanban, '.o_lunch_content > .o_legacy_kanban_view',
"should have a 'classical kanban view' column");
assert.hasClass(kanban.$('.o_legacy_kanban_view'), 'o_lunch_kanban_view',
"should have classname 'o_lunch_kanban_view'");
assert.containsOnce($('.o_lunch_content'), '> details',
"should have a 'lunch kanban' details/summary discolure panel");
assert.hasClass($('.o_lunch_content > details'), 'fixed-bottom',
"should have classname 'fixed-bottom'");
assert.isNotVisible($('.o_lunch_content > details .o_lunch_banner'),
"shouldn't have a visible 'lunch kanban' banner");
kanban.destroy();
});
QUnit.module('LunchWidget', function () {
QUnit.test('toggle', async function (assert) {
assert.expect(6);
const kanban = await createLunchView({
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_content > details');
assert.isNotVisible($details.find('.o_lunch_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");
await testUtils.dom.click($details.find('> summary'));
assert.isVisible($details.find('.o_lunch_banner'),
"should have a visible 'lunch kanban' banner");
await testUtils.dom.click($details.find('> summary'));
assert.isNotVisible($details.find('.o_lunch_banner'),
"shouldn't have a visible 'lunch kanban' banner");
kanban.destroy();
});
QUnit.test('keep open when adding quantities', async function (assert) {
assert.expect(6);
const kanban = await createLunchView({
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_content > details');
assert.isNotVisible($details.find('.o_lunch_banner'),
"shouldn't have a visible 'lunch kanban' banner");
assert.isVisible($details.find('> summary'),
"should hava a visible cart toggle button");
await testUtils.dom.click($details.find('> summary'));
assert.isVisible($details.find('.o_lunch_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');
await testUtils.dom.click($firstLine.find('button.o_add_product'));
assert.isVisible($('.o_lunch_content > details .o_lunch_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');
await testUtils.dom.click($firstLine.find('button.o_remove_product'));
assert.isVisible($('.o_lunch_content > details .o_lunch_banner'),
"remove quantity should keep 'lunch kanban' banner open");
kanban.destroy();
});
});
});
});

View file

@ -1,415 +0,0 @@
/** @odoo-module */
import { click, getFixture, nextTick, patchWithCleanup } from '@web/../tests/helpers/utils';
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
import { LunchKanbanRenderer } from '@lunch/views/kanban';
let target;
let serverData;
let lunchInfos;
async function makeLunchView(extraArgs = {}) {
return await makeView(
Object.assign({
serverData,
type: "kanban",
resModel: "lunch.product",
arch: `
<kanban js_class="lunch_kanban">
<templates>
<t t-name="kanban-box">
<div>
<field name="name"/>
<field name="price"/>
</div>
</t>
</templates>
</kanban>`,
mockRPC: (route, args) => {
if (route == '/lunch/user_location_get') {
return Promise.resolve(serverData.models['lunch.location'].records[0].id);
} else if (route == '/lunch/infos') {
return Promise.resolve(lunchInfos);
}
}
}, extraArgs
));
}
QUnit.module('Lunch', {}, function() {
QUnit.module('LunchKanban', (hooks) => {
hooks.beforeEach(() => {
target = getFixture();
serverData = {
models: {
'lunch.product': {
fields: {
id: { string: "ID", type: "integer" },
name: { string: 'Name', type: 'char' },
is_available_at: { string: 'Available', type: 'integer' },
price: { string: 'Price', type: 'float', },
},
records: [
{ id: 1, name: "Big Plate", is_available_at: 1, price: 4.95, },
{ id: 2, name: "Small Plate", is_available_at: 2, price: 6.99, },
{ id: 3, name: "Just One Plate", is_available_at: 2, price: 5.87, },
]
},
'lunch.location': {
fields: {
name: { string: 'Name', type: 'char' },
},
records: [
{ id: 1, name: "Old Office" },
{ id: 2, name: "New Office" },
]
},
'lunch.order': {
fields: {
product_id: { string: 'Product', type: 'many2one', relation: 'lunch.product', },
}
},
'res.users': {
fields: {
share: { type: 'boolean', },
},
records: [
{ id: 1, name: 'Johnny Hache', share: false, },
{ id: 2, name: 'David Elora', share: false, }
]
}
},
views: {
'lunch.order,false,form': `<form>
<sheet>
<field name="product_id" readonly="1"/>
</sheet>
<footer>
<button name="add_to_cart" type="object" string="Add to cart" />
<button string="Discard" special="cancel"/>
</footer>
</form>`
}
};
lunchInfos = {
username: "Johnny Hache",
wallet: 12.05,
is_manager: false,
currency: {
symbol: "€",
position: "after",
},
user_location: [1, "Old Office"],
alerts: [],
lines: [],
};
setupViewRegistries();
});
QUnit.test("Basic rendering", async function (assert) {
assert.expect(4);
await makeLunchView();
assert.containsOnce(target, '.o_lunch_banner');
assert.containsNone(target, '.o_lunch_content .alert');
assert.containsOnce(target, '.o_kanban_record:not(.o_kanban_ghost)', 1);
const lunchDashboard = target.querySelector('.o_lunch_banner');
const lunchUser = lunchDashboard.querySelector('.lunch_user span');
assert.equal(lunchUser.innerText, 'Johnny Hache');
});
QUnit.test("Open product", async function (assert) {
assert.expect(2);
await makeLunchView();
patchWithCleanup(LunchKanbanRenderer.prototype, {
openOrderLine(productId, orderId) {
assert.equal(productId, 1);
}
});
assert.containsOnce(target, '.o_kanban_record:not(.o_kanban_ghost)');
click(target, '.o_kanban_record:not(.o_kanban_ghost)');
});
QUnit.test("Basic rendering with alerts", async function (assert) {
assert.expect(2);
let userInfos = {
...lunchInfos,
alerts: [
{
id: 1,
message: '<b>free boudin compote for everyone</b>',
}
]
};
await makeLunchView({
mockRPC: (route, args) => {
if (route == '/lunch/user_location_get') {
return Promise.resolve(userInfos.user_location[0]);
} else if (route == '/lunch/infos') {
return Promise.resolve(userInfos);
}
}
});
assert.containsOnce(target, '.o_lunch_content .alert');
assert.equal(target.querySelector('.o_lunch_content .alert').innerText, 'free boudin compote for everyone');
});
QUnit.test("Open product", async function (assert) {
assert.expect(2);
await makeLunchView();
patchWithCleanup(LunchKanbanRenderer.prototype, {
openOrderLine(productId, orderId) {
assert.equal(productId, 1);
}
});
assert.containsOnce(target, '.o_kanban_record:not(.o_kanban_ghost)');
click(target, '.o_kanban_record:not(.o_kanban_ghost)');
});
QUnit.test("Location change", async function (assert) {
assert.expect(3);
let userInfos = { ...lunchInfos };
await makeLunchView({
mockRPC: (route, args) => {
if (route == '/lunch/user_location_get') {
return Promise.resolve(userInfos.user_location[0]);
} else if (route == '/lunch/infos') {
return Promise.resolve(userInfos);
} else if (route == '/lunch/user_location_set') {
assert.equal(args.location_id, 2);
userInfos.user_location = [2, "New Office"];
return Promise.resolve(true);
}
}
});
click(target, '.lunch_location input');
await nextTick();
assert.containsOnce(target, '.lunch_location .dropdown-item:contains(New Office)');
click(target, '.lunch_location .dropdown-item:not(.ui-state-active)');
await nextTick();
assert.containsN(target, 'div[role=article].o_kanban_record', 2);
});
QUnit.test("Manager: user change", async function (assert) {
assert.expect(8);
let userInfos = { ...lunchInfos, is_manager: true };
let expectedUserId = false; // false as we are requesting for the current user
await makeLunchView({
mockRPC: (route, args) => {
if (route == '/lunch/user_location_get') {
return Promise.resolve(userInfos.user_location[0]);
} else if (route == '/lunch/infos') {
assert.equal(expectedUserId, args.user_id);
if (expectedUserId === 2) {
userInfos = {
...userInfos,
username: 'David Elora',
wallet: -10000,
};
}
return Promise.resolve(userInfos);
} else if (route == '/lunch/user_location_set') {
assert.equal(args.location_id, 2);
userInfos.user_location = [2, "New Office"];
return Promise.resolve(true);
}
}
});
assert.containsOnce(target, '.lunch_user input');
click(target, '.lunch_user input');
await nextTick();
assert.containsOnce(target, '.lunch_user .dropdown-item:contains(David Elora)');
expectedUserId = 2;
click(target, '.lunch_user .dropdown-item:not(.ui-state-active)');
await nextTick();
const wallet = target.querySelector('.o_lunch_banner .col-9 > .d-flex > span:nth-child(2)');
assert.equal(wallet.innerText, '-10000.00€', 'David Elora is poor')
click(target, '.lunch_location input');
await nextTick();
click(target, '.lunch_location .dropdown-item:not(.ui-state-active)');
await nextTick();
const user = target.querySelector('.lunch_user input');
assert.equal(user.value, 'David Elora', 'changing location should not reset user');
});
QUnit.test("Trash existing order", async function (assert) {
assert.expect(5);
let userInfos = {
...lunchInfos,
lines: [
{
id: 1,
product: [1, "Big Plate", "4.95"],
toppings: [],
quantity: 1,
price: 4.95,
raw_state: "new",
state: "To Order",
note: false
}
],
raw_state: "new",
total: "4.95",
};
await makeLunchView({
mockRPC: (route, args) => {
if (route == '/lunch/user_location_get') {
return Promise.resolve(userInfos.user_location[0]);
} else if (route == '/lunch/infos') {
return Promise.resolve(userInfos);
} else if (route == '/lunch/trash') {
userInfos = {
...userInfos,
lines: [],
raw_state: false,
total: 0,
};
return Promise.resolve(true);
}
}
});
assert.containsN(target, 'div.o_lunch_banner > .row > div', 3);
assert.containsOnce(target, 'div.o_lunch_banner > .row > div:nth-child(2) button.fa-trash', 'should have trash icon');
assert.containsOnce(target, 'div.o_lunch_banner > .row > div:nth-child(2) ul > li', 'should have one order line');
assert.containsOnce(target, 'div.o_lunch_banner > .row > div:nth-child(3) button:contains(Order Now)');
click(target, 'div.o_lunch_banner > .row > div:nth-child(2) button.fa-trash');
await nextTick();
assert.containsN(target, 'div.o_lunch_banner > .row > div', 1);
});
QUnit.test("Change existing order", async function (assert) {
assert.expect(1);
let userInfos = {
...lunchInfos,
lines: [
{
id: 1,
product: [1, "Big Plate", "4.95"],
toppings: [],
quantity: 1,
price: 4.95,
raw_state: "new",
state: "To Order",
note: false
}
],
raw_state: "new",
total: "4.95",
};
await makeLunchView({
mockRPC: (route, args) => {
if (route == '/lunch/user_location_get') {
return Promise.resolve(userInfos.user_location[0]);
} else if (route == '/lunch/infos') {
return Promise.resolve(userInfos);
} else if (route == '/web/dataset/call_kw/lunch.order/update_quantity') {
assert.equal(args.args[1], 1, 'should increment order quantity by 1');
userInfos = {
...userInfos,
lines: [
{
...userInfos.lines[0],
product: [1, "Big Plate", "9.9"],
quantity: 2,
price: 4.95 * 2,
}
],
total: 4.95 * 2,
};
return Promise.resolve(true);
}
}
});
click(target, 'div.o_lunch_banner > .row > div:nth-child(2) button.fa-plus-circle');
});
QUnit.test("Confirm existing order", async function (assert) {
assert.expect(3);
let userInfos = {
...lunchInfos,
lines: [
{
id: 1,
product: [1, "Big Plate", "4.95"],
toppings: [],
quantity: 1,
price: 4.95,
raw_state: "new",
state: "To Order",
note: false
}
],
raw_state: "new",
total: "4.95",
};
await makeLunchView({
mockRPC: (route, args) => {
if (route == '/lunch/user_location_get') {
return Promise.resolve(userInfos.user_location[0]);
} else if (route == '/lunch/infos') {
return Promise.resolve(userInfos);
} else if (route == '/lunch/pay') {
assert.equal(args.user_id, false); // Should confirm order of current user
userInfos = {
...userInfos,
lines: [
{
...userInfos.lines[0],
raw_state: 'ordered',
state: 'Ordered,'
}
],
raw_state: 'ordered',
wallet: userInfos.wallet - 4.95,
};
return Promise.resolve(true);
}
}
});
const wallet = target.querySelector('.o_lunch_banner .col-9 > .d-flex > span:nth-child(2)');
assert.equal(wallet.innerText, '12.05€');
click(target, 'div.o_lunch_banner > .row > div:nth-child(3) button');
await nextTick();
assert.equal(wallet.innerText, '7.10€', 'Wallet should update');
});
});
});

View file

@ -1,273 +0,0 @@
odoo.define('lunch.lunchListTests', function (require) {
"use strict";
const LunchListView = require('lunch.LunchListView');
const testUtils = require('web.test_utils');
const {createLunchView, mockLunchRPC} = require('lunch.test_utils');
QUnit.module('Views');
QUnit.module('LunchListView', {
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 Promise.resolve();
},
},
'lunch.product.category': {
fields: {},
records: [],
},
'lunch.supplier': {
fields: {},
records: [],
},
'lunch.location': {
fields: {
name: {string: 'Name', type: 'char'},
company_id: {string: 'Company', type: 'many2one', relation: 'res.company'},
},
records: [
{id: 1, name: "Office 1", company_id: false},
{id: 2, name: "Office 2", company_id: false},
],
},
'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]},
],
},
'res.company': {
fields: {
name: {string: 'Name', type: 'char'},
}, records: [
{id: 1, name: "Dunder Trade Company"},
]
}
};
this.regularInfos = {
username: "Marc Demo",
wallet: 36.5,
is_manager: false,
group_portal_id: PORTAL_GROUP_ID,
currency: {
symbol: "\u20ac",
position: "after"
},
user_location: [2, "Office 2"],
alerts: [{id: 42, message: '<b>Warning! Neurotoxin pressure has reached dangerously unlethal levels.</b>'}]
};
},
}, function () {
QUnit.test('basic rendering', async function (assert) {
assert.expect(9);
const list = await createLunchView({
View: LunchListView,
model: 'product',
data: this.data,
arch: `
<tree>
<field name="name"/>
</tree>
`,
mockRPC: mockLunchRPC({
infos: this.regularInfos,
userLocation: this.data['lunch.location'].records[0].id,
}),
});
// check view layout
assert.containsN(list, '.o_content > div', 2,
"should have 2 columns");
assert.containsOnce(list, '.o_content > div.o_search_panel',
"should have a 'lunch filters' column");
assert.containsOnce(list, '.o_content > .o_lunch_content',
"should have a 'lunch wrapper' column");
assert.containsOnce(list, '.o_lunch_content > .o_legacy_list_view',
"should have a 'classical list view' column");
assert.hasClass(list.$('.o_legacy_list_view'), 'o_lunch_list_view',
"should have classname 'o_lunch_list_view'");
assert.containsOnce(list, '.o_lunch_content > .o_lunch_banner',
"should have a 'lunch' banner");
const $alertMessage = list.$('.alert > *');
assert.equal($alertMessage.length, 1);
assert.equal($alertMessage.prop('tagName'), 'B');
assert.equal($alertMessage.text(), "Warning! Neurotoxin pressure has reached dangerously unlethal levels.")
list.destroy();
});
QUnit.module('LunchWidget', function () {
QUnit.test('search panel domain location', async function (assert) {
assert.expect(18);
let expectedLocation = 1;
let locationId = this.data['lunch.location'].records[0].id;
const regularInfos = _.extend({}, this.regularInfos);
const list = await createLunchView({
View: LunchListView,
model: 'product',
data: this.data,
arch: `
<tree>
<field name="name"/>
</tree>
`,
mockRPC: function (route, args) {
assert.step(route);
if (route.startsWith('/lunch')) {
if (route === '/lunch/user_location_set') {
locationId = args.location_id;
return Promise.resolve(true);
}
return mockLunchRPC({
infos: regularInfos,
userLocation: locationId,
}).apply(this, arguments);
}
if (args.method === 'search_panel_select_multi_range') {
assert.deepEqual(args.kwargs.search_domain, [["is_available_at", "in", [expectedLocation]]],
'The initial domain of the search panel must contain the user location');
}
if (route === '/web/dataset/search_read') {
assert.deepEqual(args.domain, [["is_available_at", "in", [expectedLocation]]],
'The domain for fetching actual data should be correct');
}
return this._super.apply(this, arguments);
},
});
expectedLocation = 2;
await testUtils.fields.many2one.clickOpenDropdown('locations');
await testUtils.fields.many2one.clickItem('locations', "Office 2");
assert.verifySteps([
// Initial state
'/lunch/user_location_get',
'/web/dataset/call_kw/product/search_panel_select_multi_range',
'/web/dataset/call_kw/product/search_panel_select_multi_range',
'/web/dataset/search_read',
'/lunch/infos',
// Click m2o
'/web/dataset/call_kw/lunch.location/name_search',
// Click new location
'/lunch/user_location_set',
'/web/dataset/call_kw/product/search_panel_select_multi_range',
'/web/dataset/call_kw/product/search_panel_select_multi_range',
'/web/dataset/search_read',
'/lunch/infos',
]);
list.destroy();
});
QUnit.test('search panel domain location false: fetch products in all locations', async function (assert) {
assert.expect(9);
const regularInfos = _.extend({}, this.regularInfos);
const list = await createLunchView({
View: LunchListView,
model: 'product',
data: this.data,
arch: `
<tree>
<field name="name"/>
</tree>
`,
mockRPC: function (route, args) {
assert.step(route);
if (route.startsWith('/lunch')) {
return mockLunchRPC({
infos: regularInfos,
userLocation: false,
}).apply(this, arguments);
}
if (args.method === 'search_panel_select_multi_range') {
assert.deepEqual(args.kwargs.search_domain, [],
'The domain should not exist since the location is false.');
}
if (route === '/web/dataset/search_read') {
assert.deepEqual(args.domain, [],
'The domain for fetching actual data should be correct');
}
return this._super.apply(this, arguments);
}
});
assert.verifySteps([
'/lunch/user_location_get',
'/web/dataset/call_kw/product/search_panel_select_multi_range',
'/web/dataset/call_kw/product/search_panel_select_multi_range',
'/web/dataset/search_read',
'/lunch/infos',
])
list.destroy();
});
QUnit.test('add a product', async function (assert) {
assert.expect(1);
const list = await createLunchView({
View: LunchListView,
model: 'product',
data: this.data,
arch: `
<tree>
<field name="name"/>
</tree>
`,
mockRPC: mockLunchRPC({
infos: this.regularInfos,
userLocation: this.data['lunch.location'].records[0].id,
}),
intercepts: {
do_action: function (ev) {
assert.deepEqual(ev.data.action, {
name: "Configure Your Order",
res_model: 'lunch.order',
type: 'ir.actions.act_window',
views: [[false, 'form']],
target: 'new',
context: {
default_product_id: 1,
},
},
"should open the wizard");
},
},
});
await testUtils.dom.click(list.$('.o_data_row:first'));
list.destroy();
});
});
});
});

View file

@ -1,53 +1,53 @@
/** @odoo-module **/
import { _t } from "@web/core/l10n/translation";
import { registry } from "@web/core/registry";
import { stepUtils } from "@web_tour/tour_utils";
import { _t } from 'web.core';
import tour from 'web_tour.tour';
tour.register('order_lunch_tour', {
url: "/web",
test: true,
}, [
tour.stepUtils.showAppsMenuItem(),
registry.category("web_tour.tours").add('order_lunch_tour', {
url: "/odoo",
steps: () => [
stepUtils.showAppsMenuItem(),
{
trigger: '.o_app[data-menu-xmlid="lunch.menu_lunch"]',
content: _t("Start by accessing the lunch app."),
position: 'bottom',
tooltipPosition: 'bottom',
run: "click",
},
{
content:"click on location",
trigger: ".lunch_location .o_input_dropdown input",
run: 'click',
run: 'click'
},
{
content: "Pick 'Farm 1' option",
trigger: '.o_input_dropdown li:contains(Farm 1)',
trigger: '.dropdown-item:contains("Farm 1")',
run: "click",
},
{
trigger: '.lunch_location input:propValueContains(Farm 1)',
run: () => {}, // wait for article to be correctly loaded
trigger: '.lunch_location input:value("Farm 1")',
},
{
trigger: "div[role='article']",
trigger: ".o_kanban_record",
content: _t("Click on a product you want to order and is available."),
run: 'click'
},
{
trigger: 'textarea[id="note"]',
trigger: 'textarea[id="note_0"]',
content: _t("Add additionnal information about your order."),
position: 'bottom',
run: 'text allergy to peanuts',
tooltipPosition: 'bottom',
run: "edit allergy to peanuts",
},
{
trigger: 'button[name="add_to_cart"]',
content: _t("Add your order to the cart."),
position: 'bottom',
}, {
tooltipPosition: 'bottom',
run: "click",
},
{
trigger: 'button:contains("Order Now")',
content: _t("Validate your order"),
position: 'left',
tooltipPosition: 'left',
run: 'click',
}, {
trigger: '.o_lunch_widget_lines .badge:contains("Ordered")',
trigger: ".o_lunch_widget_line li[name='o_lunch_order_line'] .badge:contains('Ordered')",
content: 'Check that order is ordered',
run: () => {}
}]);
}]});