[REF] *: adapt code to new owl webclient

This commit adapts the community codebase to the rewriting of the
/web application in owl.

Co-authored-by: Aaron Bohy <aab@odoo.com>
Co-authored-by: Bruno Boi <boi@odoo.com>
Co-authored-by: Géry Debongnie <ged@odoo.com>
Co-authored-by: Samuel Degueldre <sad@odoo.com>
Co-authored-by: Mathieu Duckerts-Antoine <dam@odoo.com>
Co-authored-by: Simon Genin (ges) <ges@odoo.com>
Co-authored-by: Francois (fge) <fge@odoo.com>
Co-authored-by: Michael Mattiello (mcm) <mcm@odoo.com>
Co-authored-by: Julien Mougenot <jum@odoo.com>
Co-authored-by: Lucas Perais (lpe) <lpe@odoo.com>
Co-authored-by: Jorge Pinna Puissant <jpp@odoo.com>
This commit is contained in:
Aaron Bohy
2021-06-18 21:31:27 +02:00
co-authored by Bruno Boi Géry Debongnie Samuel Degueldre Mathieu Duckerts-Antoine Simon Genin Francois Michael Mattiello Julien Mougenot Lucas Perais Jorge Pinna Puissant
parent 2587a16835
commit 14bffd983e
55 changed files with 1592 additions and 1112 deletions
+2 -2
View File
@@ -115,8 +115,8 @@ You could use this simplified accounting in case you work with an (external) acc
'account/static/tests/tours/**/*',
],
'web.qunit_suite_tests': [
('after', 'web/static/tests/views/kanban_tests.js', 'account/static/tests/account_payment_field_tests.js'),
('after', 'web/static/tests/views/kanban_tests.js', 'account/static/tests/section_and_note_tests.js'),
('after', 'web/static/tests/legacy/views/kanban_tests.js', 'account/static/tests/account_payment_field_tests.js'),
('after', 'web/static/tests/legacy/views/kanban_tests.js', 'account/static/tests/section_and_note_tests.js'),
],
'web.assets_qweb': [
'account/static/src/xml/**/*',
@@ -6,8 +6,6 @@ var config = require('web.config');
var core = require('web.core');
var session = require('web.session');
var time = require('web.time');
var AbstractWebClient = require('web.AbstractWebClient');
var Loading = require('web.Loading');
var QWeb = core.qweb;
var _t = core._t;
@@ -800,9 +798,6 @@ var DataImport = AbstractAction.extend({
{tracking_disable: tracking_disable}
);
var self = this;
this.trigger_up('with_client', {callback: function () {
this.loading.ignore_events = true;
}});
$.blockUI({message: QWeb.render('Throbber')});
$(document.body).addClass('o_ui_blocked');
var opts = this.import_options();
@@ -851,9 +846,6 @@ var DataImport = AbstractAction.extend({
}).finally(function () {
$(document.body).removeClass('o_ui_blocked');
$.unblockUI();
self.trigger_up('with_client', {callback: function () {
delete this.loading.ignore_events;
}});
});
}, /**
*
@@ -877,6 +869,8 @@ var DataImport = AbstractAction.extend({
method: 'execute_import',
args: args.concat([opts]),
kwargs: kwargs
}, {
shadow: true,
}).then(function (results) {
_.each(results.messages, offset_by(opts.skip));
if (!kwargs.dryrun && !results.ids) {
@@ -1172,18 +1166,6 @@ StateMachine.create({
],
});
Loading.include({
on_rpc_event: function () {
if (this.ignore_events) {
return
}
this._super.apply(this, arguments);
}
});
AbstractWebClient.prototype.custom_events['with_client'] = function (ev) {
ev.data.callback.call(this);
};
function offset_by(by) {
return function offset_message(msg) {
if (msg.rows) {
+1 -4
View File
@@ -19,10 +19,7 @@
'bus/static/src/js/services/bus_service.js',
],
'web.qunit_suite_tests': [
'bus/static/tests/bus_tests.js',
],
'web.assets_tests': [
'bus/static/tests/bus_tests_tour.js',
'bus/static/tests/*.js',
],
}
}
@@ -0,0 +1,92 @@
/** @odoo-module **/
import { browser } from "@web/core/browser/browser";
import { registry } from "@web/core/registry";
export const assetsWatchdogService = {
dependencies: ["notification"],
start(env, { notification }) {
const assets = {};
let assetsChangedNotificationId = null;
let bundleNotifTimerID = null;
env.bus.on("WEB_CLIENT_READY", null, async () => {
const legacyEnv = owl.Component.env;
document.querySelectorAll("*[data-asset-bundle]").forEach((el) => {
assets[el.getAttribute("data-asset-bundle")] = el.getAttribute(
"data-asset-version"
);
});
legacyEnv.services.bus_service.onNotification(this, onNotification);
legacyEnv.services.bus_service.addChannel("bundle_changed");
});
/**
* Displays one notification on user's screen when assets have changed
*/
function displayBundleChangedNotification() {
if (!assetsChangedNotificationId) {
// Wrap the notification inside a delay.
// The server may be overwhelmed with recomputing assets
// We wait until things settle down
browser.clearTimeout(bundleNotifTimerID);
bundleNotifTimerID = browser.setTimeout(() => {
assetsChangedNotificationId = notification.create(
env._t("The page appears to be out of date."),
{
title: env._t("Refresh"),
type: "warning",
sticky: true,
buttons: [
{
name: env._t("Refresh"),
primary: true,
onClick: () => {
browser.location.reload();
},
},
],
onClose: () => {
assetsChangedNotificationId = null;
},
}
);
}, getBundleNotificationDelay());
}
}
/**
* Computes a random delay to avoid hammering the server
* when bundles change with all the users reloading
* at the same time
*
* @return {number} delay in milliseconds
*/
function getBundleNotificationDelay() {
return 10000 + Math.floor(Math.random() * 50) * 1000;
}
/**
* Reacts to bus's notification
*
* @param {Array} notifications: list of received notifications
*/
function onNotification(notifications) {
for (const notif of notifications) {
if (notif[0][1] === "bundle_changed") {
const bundleXmlId = notif[1][0];
const bundleVersion = notif[1][1];
if (bundleXmlId in assets && bundleVersion !== assets[bundleXmlId]) {
displayBundleChangedNotification();
break;
}
}
}
}
},
};
registry.category("services").add("assetsWatchdog", assetsWatchdogService);
-109
View File
@@ -1,109 +0,0 @@
odoo.define('bus.WebClient', function (require) {
"use strict";
const core = require('web.core');
const WebClient = require('web.WebClient');
const _t = core._t;
WebClient.include({
//----------------------------------------------------------------------
// Public
//----------------------------------------------------------------------
/**
* Detects the presence of assets in DOM's HEAD
*
* @override
*/
async start() {
this._assetsChangedNotificationId = null;
this._assets = {};
await this._super(...arguments);
},
/**
* Assigns handler to bus notification
*
* @override
*/
show_application() {
const shown = this._super(...arguments);
document.querySelectorAll('*[data-asset-bundle]').forEach(el => {
this._assets[el.getAttribute('data-asset-bundle')] = el.getAttribute('data-asset-version');
});
this.call('bus_service', 'onNotification', this, this._onNotification);
this.call('bus_service', 'addChannel', 'bundle_changed');
return shown;
},
//----------------------------------------------------------------------
// Private
//----------------------------------------------------------------------
/**
* Displays one notification on user's screen when assets have changed
*
* @private
*/
_displayBundleChangedNotification() {
if (!this._assetsChangedNotificationId) {
// Wrap the notification inside a delay.
// The server may be overwhelmed with recomputing assets
// We wait until things settle down
clearTimeout(this._bundleNotifTimerID);
this._bundleNotifTimerID = setTimeout(() => {
this._assetsChangedNotificationId = this.call('notification', 'notify', {
title: _t('Refresh'),
message: _t('The page appears to be out of date.'),
sticky: true,
onClose: () => {
this._assetsChangedNotificationId = null;
},
buttons: [{
text: _t('Refresh'),
primary: true,
click: () => {
window.location.reload(true);
}
}],
});
}, this._getBundleNotificationDelay());
}
},
/**
* Computes a random delay to avoid hammering the server
* when bundles change with all the users reloading
* at the same time
*
* @private
* @return {number} delay in milliseconds
*/
_getBundleNotificationDelay() {
return 10000 + Math.floor(Math.random()*50) * 1000;
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* Reacts to bus's notification
*
* @private
* @param {Array} notifications: list of received notifications
*/
_onNotification(notifications) {
for (const notif of notifications) {
if (notif[0][1] === 'bundle_changed') {
const bundleXmlId = notif[1][0];
const bundleVersion = notif[1][1];
if (bundleXmlId in this._assets && bundleVersion !== this._assets[bundleXmlId]) {
this._displayBundleChangedNotification();
break;
}
}
}
}
});
});
@@ -0,0 +1,86 @@
/** @odoo-module */
import * as legacyRegistry from "web.Registry";
import * as BusService from "bus.BusService";
import * as RamStorage from "web.RamStorage";
import * as AbstractStorageService from "web.AbstractStorageService";
import {
createWebClient,
getActionManagerTestConfig,
} from "@web/../tests/webclient/actions/helpers";
import { assetsWatchdogService } from "@bus/js/services/assets_watchdog_service";
import { click, nextTick, patchWithCleanup } from "@web/../tests/helpers/utils";
import { NotificationContainer } from "@web/core/notifications/notification_container";
import { browser } from "@web/core/browser/browser";
import { registry } from "@web/core/registry";
import { clearRegistryWithCleanup } from "@web/../tests/helpers/mock_env";
const LocalStorageService = AbstractStorageService.extend({
storage: new RamStorage(),
});
const mainComponentRegistry = registry.category("main_components");
const serviceRegistry = registry.category("services");
QUnit.module("Bus Assets WatchDog", (hooks) => {
let legacyServicesRegistry;
let testConfig;
hooks.beforeEach((assert) => {
legacyServicesRegistry = new legacyRegistry();
legacyServicesRegistry.add("bus_service", BusService);
legacyServicesRegistry.add("local_storage", LocalStorageService);
testConfig = getActionManagerTestConfig();
serviceRegistry.add("assetsWatchdog", assetsWatchdogService);
clearRegistryWithCleanup(mainComponentRegistry);
mainComponentRegistry.add("NotificationContainer", NotificationContainer);
patchWithCleanup(browser, {
setTimeout: (fn) => fn(),
clearTimeout: () => {},
location: {
reload: () => assert.step("reloadPage"),
},
});
});
QUnit.test("can listen on bus and displays notifications in DOM", async (assert) => {
assert.expect(5);
let pollNumber = 0;
const mockRPC = (route, args) => {
if (route === "/longpolling/poll") {
if (pollNumber > 0) {
return new Promise(() => {}); // let it hang to avoid further calls
}
pollNumber++;
assert.deepEqual(args.channels, ["bundle_changed"]);
return Promise.resolve([
{
id: "prout",
channel: ["db_name", "bundle_changed"],
message: ["web.assets_backend", "newHash"],
},
]);
}
};
const webClient = await createWebClient({
testConfig,
legacyParams: { serviceRegistry: legacyServicesRegistry },
mockRPC,
});
await nextTick();
assert.containsOnce(webClient.el, ".o_notification_body");
assert.strictEqual(
webClient.el.querySelector(".o_notification_body .o_notification_content").textContent,
"The page appears to be out of date."
);
// reload by clicking on the reload button
await click(webClient.el, ".o_notification_buttons .btn-primary");
assert.verifySteps(["reloadPage"]);
});
});
-25
View File
@@ -1,25 +0,0 @@
odoo.define("bus.tour", function (require) {
"use strict";
const tour = require("web_tour.tour");
tour.register("bundle_changed_notification", {
test: true,
url: '/web',
}, [{
trigger: '.o_web_client',
run() {
const webClient = odoo.__DEBUG__.services['web.web_client'];
const _delayFn = webClient._getBundleNotificationDelay;
webClient._getBundleNotificationDelay = () => 0;
this.call('bus_service', 'trigger',
'notification',
[[['db_name', 'bundle_changed'], ['web.assets_backend', 'hash']]]
);
webClient._getBundleNotificationDelay = _delayFn;
}
}, {
trigger: '.o_notification_title:contains(Refresh)',
}]
);
});
+2 -1
View File
@@ -36,7 +36,8 @@ class BusWebTests(odoo.tests.HttpCase):
self.patch(type(self.env['bus.bus']), 'sendone', patched_sendone)
self.start_tour('/web', "bundle_changed_notification", login='admin', timeout=180)
self.authenticate('admin', 'admin')
self.url_open('/web')
# One sendone for each asset bundle and for each CSS / JS
self.assertEqual(
+1
View File
@@ -47,6 +47,7 @@ If you need to manage your meetings, you should install the CRM module.
'calendar/static/src/js/calendar_view.js',
'calendar/static/src/js/mail_activity.js',
'calendar/static/src/js/systray_activity_menu.js',
'calendar/static/src/js/services/calendar_notification_service.js',
],
'web.qunit_suite_tests': [
'calendar/static/tests/**/*',
@@ -3,116 +3,10 @@ odoo.define('base_calendar.base_calendar', function (require) {
var BasicModel = require('web.BasicModel');
var fieldRegistry = require('web.field_registry');
var Notification = require('web.Notification');
var relationalFields = require('web.relational_fields');
var session = require('web.session');
var WebClient = require('web.WebClient');
const FieldMany2ManyTagsAvatar = relationalFields.FieldMany2ManyTagsAvatar;
var CalendarNotification = Notification.extend({
template: "CalendarNotification",
xmlDependencies: (Notification.prototype.xmlDependencies || [])
.concat(['/calendar/static/src/xml/notification_calendar.xml']),
init: function(parent, params) {
this._super(parent, params);
this.eid = params.eventID;
this.sticky = true;
this.events = _.extend(this.events || {}, {
'click .link2event': function() {
var self = this;
this._rpc({
route: '/web/action/load',
params: {
action_id: 'calendar.action_calendar_event_notify',
},
})
.then(function(r) {
r.res_id = self.eid;
return self.do_action(r);
});
},
'click .link2recall': function() {
this.close();
},
'click .link2showed': function() {
this._rpc({route: '/calendar/notify_ack'})
.then(this.close.bind(this, false), this.close.bind(this, false));
},
});
},
});
WebClient.include({
display_calendar_notif: function(notifications) {
var self = this;
var last_notif_timer = 0;
// Clear previously set timeouts and destroy currently displayed calendar notifications
clearTimeout(this.get_next_calendar_notif_timeout);
_.each(this.calendar_notif_timeouts, clearTimeout);
this.calendar_notif_timeouts = {};
// For each notification, set a timeout to display it
_.each(notifications, function(notif) {
var key = notif.event_id + ',' + notif.alarm_id;
if (key in self.calendar_notif) {
return;
}
self.calendar_notif_timeouts[key] = setTimeout(function () {
var notificationID = self.call('notification', 'notify', {
Notification: CalendarNotification,
title: notif.title,
message: notif.message,
eventID: notif.event_id,
onClose: function () {
delete self.calendar_notif[key];
},
});
self.calendar_notif[key] = notificationID;
}, notif.timer * 1000);
last_notif_timer = Math.max(last_notif_timer, notif.timer);
});
// Set a timeout to get the next notifications when the last one has been displayed
if (last_notif_timer > 0) {
this.get_next_calendar_notif_timeout = setTimeout(this.get_next_calendar_notif.bind(this), last_notif_timer * 1000);
}
},
get_next_calendar_notif: function() {
session.rpc("/calendar/notify", {}, {shadow: true})
.then(this.display_calendar_notif.bind(this))
.guardedCatch(function(reason) { //
var err = reason.message;
var ev = reason.event;
if(err.code === -32098) {
// Prevent the CrashManager to display an error
// in case of an xhr error not due to a server error
ev.preventDefault();
}
});
},
show_application: function() {
// An event is triggered on the bus each time a calendar event with alarm
// in which the current user is involved is created, edited or deleted
this.calendar_notif_timeouts = {};
this.calendar_notif = {};
this.call('bus_service', 'onNotification', this, function (notifications) {
_.each(notifications, (function (notification) {
if (notification[0][1] === 'calendar.alarm') {
this.display_calendar_notif(notification[1]);
}
}).bind(this));
});
return this._super.apply(this, arguments).then(this.get_next_calendar_notif.bind(this));
},
});
BasicModel.include({
/**
* @private
@@ -0,0 +1,108 @@
/** @odoo-module **/
import { browser } from "@web/core/browser/browser";
import { ConnectionLostError } from "@web/core/network/rpc_service";
import { registry } from "@web/core/registry";
export const calendarNotificationService = {
dependencies: ["action", "notification", "rpc"],
start(env, { action, notification, rpc }) {
let calendarNotifTimeouts = {};
let nextCalendarNotifTimeout = null;
const calendarNotif = {};
env.bus.on("WEB_CLIENT_READY", null, async () => {
const legacyEnv = owl.Component.env;
legacyEnv.services.bus_service.onNotification(this, (notifications) => {
for (const notif of notifications) {
if (notif[0][1] === "calendar.alarm") {
displayCalendarNotification(notif[1]);
}
}
});
legacyEnv.services.bus_service.startPolling();
});
/**
* Displays the Calendar notification on user's screen
*/
function displayCalendarNotification(notifications) {
let lastNotifTimer = 0;
// Clear previously set timeouts and destroy currently displayed calendar notifications
browser.clearTimeout(nextCalendarNotifTimeout);
Object.values(calendarNotifTimeouts).forEach((notif) => browser.clearTimeout(notif));
calendarNotifTimeouts = {};
// For each notification, set a timeout to display it
notifications.forEach(function (notif) {
const key = notif.event_id + "," + notif.alarm_id;
if (key in calendarNotif) {
return;
}
calendarNotifTimeouts[key] = browser.setTimeout(function () {
const notificationID = notification.create(notif.message, {
title: notif.title,
type: "warning",
sticky: true,
onClose: () => {
delete calendarNotif[key];
},
buttons: [
{
name: env._t("OK"),
primary: true,
onClick: async () => {
await rpc("/calendar/notify_ack");
notification.close(calendarNotif[key]);
},
},
{
name: env._t("Details"),
onClick: async () => {
await action.doAction(
"calendar.action_calendar_event_notify",
{
resId: notif.event_id,
}
);
notification.close(calendarNotif[key]);
},
},
{
name: env._t("Snooze"),
onClick: () => {
notification.close(calendarNotif[key]);
},
},
],
});
calendarNotif[key] = notificationID;
}, notif.timer * 1000);
lastNotifTimer = Math.max(lastNotifTimer, notif.timer);
});
// Set a timeout to get the next notifications when the last one has been displayed
if (lastNotifTimer > 0) {
nextCalendarNotifTimeout = browser.setTimeout(
getNextCalendarNotif,
lastNotifTimer * 1000
);
}
}
async function getNextCalendarNotif() {
try {
const result = await rpc("/calendar/notify", {}, { silent: true });
displayCalendarNotification(result);
} catch (error) {
if (!(error instanceof ConnectionLostError)) {
throw error;
}
}
}
},
};
registry.category("services").add("calendarNotification", calendarNotificationService);
@@ -1,19 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<template>
<t t-name="CalendarNotification" t-extend="Notification">
<t t-jquery=".o_notification_title > t" t-operation="replace">
<span t-attf-class="link2event eid_{{widget.eid}}">
<t t-esc="widget.title"/>
</span>
</t>
<t t-jquery=".o_notification_content" t-operation="after">
<div class="mt-2">
<button type="button" class="btn btn-primary link2showed oe_highlight oe_form oe_button"><span>OK</span></button>
<button type="button" class="btn btn-link link2event">Details</button>
<button type="button" class="btn btn-link link2recall">Snooze</button>
</div>
</t>
</t>
</template>
@@ -0,0 +1,233 @@
/** @odoo-module */
import * as legacyRegistry from "web.Registry";
import * as BusService from "bus.BusService";
import * as RamStorage from "web.RamStorage";
import * as AbstractStorageService from "web.AbstractStorageService";
import {
createWebClient,
getActionManagerTestConfig,
} from "@web/../tests/webclient/actions/helpers";
import { calendarNotificationService } from "@calendar/js/services/calendar_notification_service";
import { click, nextTick, patchWithCleanup } from "@web/../tests/helpers/utils";
import { NotificationContainer } from "@web/core/notifications/notification_container";
import { browser } from "@web/core/browser/browser";
import { clearRegistryWithCleanup } from "@web/../tests/helpers/mock_env";
import { registry } from "@web/core/registry";
const LocalStorageService = AbstractStorageService.extend({
storage: new RamStorage(),
});
const mainComponentRegistry = registry.category("main_components");
const serviceRegistry = registry.category("services");
QUnit.module("Calendar Notification", (hooks) => {
let legacyServicesRegistry;
let testConfig;
hooks.beforeEach(() => {
legacyServicesRegistry = new legacyRegistry();
legacyServicesRegistry.add("bus_service", BusService);
legacyServicesRegistry.add("local_storage", LocalStorageService);
testConfig = getActionManagerTestConfig();
serviceRegistry.add("calendarNotification", calendarNotificationService);
clearRegistryWithCleanup(mainComponentRegistry);
mainComponentRegistry.add("NotificationContainer", NotificationContainer);
patchWithCleanup(browser, {
setTimeout: (fn) => fn(),
clearTimeout: () => {},
});
});
QUnit.test(
"can listen on bus and display notifications in DOM and click OK",
async (assert) => {
assert.expect(5);
let pollNumber = 0;
const mockRPC = (route, args) => {
if (route === "/longpolling/poll") {
if (pollNumber > 0) {
return new Promise(() => {}); // let it hang to avoid further calls
}
pollNumber++;
return Promise.resolve([
{
id: "prout",
channel: ["db_name", "calendar.alarm", 7],
message: [
{
alarm_id: 1,
event_id: 2,
title: "Meeting",
message: "Very old meeting message",
timer: 20 * 60,
notify_at: "1978-04-14 12:45:00",
},
],
},
]);
}
if (route === "/calendar/notify") {
return Promise.resolve([]);
}
if (route === "/calendar/notify_ack") {
assert.step("notifyAck");
return Promise.resolve(true);
}
};
const webClient = await createWebClient({
testConfig,
legacyParams: { serviceRegistry: legacyServicesRegistry },
mockRPC,
});
await nextTick();
assert.containsOnce(webClient.el, ".o_notification_body");
assert.strictEqual(
webClient.el.querySelector(".o_notification_body .o_notification_content")
.textContent,
"Very old meeting message"
);
await click(webClient.el.querySelector(".o_notification_buttons .btn"));
assert.verifySteps(["notifyAck"]);
assert.containsNone(webClient.el, ".o_notification");
}
);
QUnit.test(
"can listen on bus and display notifications in DOM and click Detail",
async (assert) => {
assert.expect(5);
let pollNumber = 0;
const mockRPC = (route, args) => {
if (route === "/longpolling/poll") {
if (pollNumber > 0) {
return new Promise(() => {}); // let it hang to avoid further calls
}
pollNumber++;
return Promise.resolve([
{
id: "prout",
channel: ["db_name", "calendar.alarm", 7],
message: [
{
alarm_id: 1,
event_id: 2,
title: "Meeting",
message: "Very old meeting message",
timer: 20 * 60,
notify_at: "1978-04-14 12:45:00",
},
],
},
]);
}
if (route === "/calendar/notify") {
return Promise.resolve([]);
}
};
const fakeActionService = {
name: "action",
start() {
return {
doAction(actionId) {
assert.step(actionId);
return Promise.resolve(true);
},
loadState(state, options) {
return Promise.resolve(true);
},
};
},
};
serviceRegistry.add("action", fakeActionService, { force: true });
const webClient = await createWebClient({
testConfig,
legacyParams: { serviceRegistry: legacyServicesRegistry },
mockRPC,
});
await nextTick();
assert.containsOnce(webClient.el, ".o_notification_body");
assert.strictEqual(
webClient.el.querySelector(".o_notification_body .o_notification_content")
.textContent,
"Very old meeting message"
);
await click(webClient.el.querySelectorAll(".o_notification_buttons .btn")[1]);
assert.verifySteps(["calendar.action_calendar_event_notify"]);
assert.containsNone(webClient.el, ".o_notification");
}
);
QUnit.test(
"can listen on bus and display notifications in DOM and click Snooze",
async (assert) => {
assert.expect(4);
let pollNumber = 0;
const mockRPC = (route, args) => {
if (route === "/longpolling/poll") {
if (pollNumber > 0) {
return new Promise(() => {}); // let it hang to avoid further calls
}
pollNumber++;
return Promise.resolve([
{
id: "prout",
channel: ["db_name", "calendar.alarm", 7],
message: [
{
alarm_id: 1,
event_id: 2,
title: "Meeting",
message: "Very old meeting message",
timer: 20 * 60,
notify_at: "1978-04-14 12:45:00",
},
],
},
]);
}
if (route === "/calendar/notify") {
return Promise.resolve([]);
}
if (route === "/calendar/notify_ack") {
assert.step("notifyAck");
return Promise.resolve(true);
}
};
const webClient = await createWebClient({
testConfig,
legacyParams: { serviceRegistry: legacyServicesRegistry },
mockRPC,
});
await nextTick();
assert.containsOnce(webClient.el, ".o_notification_body");
assert.strictEqual(
webClient.el.querySelector(".o_notification_body .o_notification_content")
.textContent,
"Very old meeting message"
);
await click(webClient.el.querySelectorAll(".o_notification_buttons .btn")[2]);
assert.verifySteps([], "should only close the notification withtout calling a rpc");
assert.containsNone(webClient.el, ".o_notification");
}
);
});
+1 -1
View File
@@ -42,7 +42,7 @@ actions(Check in/Check out) performed by them.
'hr_attendance/static/src/scss/hr_attendance.scss',
],
'web.qunit_suite_tests': [
('after', 'web/static/tests/views/kanban_tests.js', 'hr_attendance/static/tests/hr_attendance_tests.js'),
('after', 'web/static/tests/legacy/views/kanban_tests.js', 'hr_attendance/static/tests/hr_attendance_tests.js'),
],
'web.assets_qweb': [
'hr_attendance/static/src/xml/**/*',
@@ -3530,7 +3530,7 @@ odoo.define('im_livechat.legacy.mail.DocumentViewer', function (require) {
var core = require('web.core');
var Widget = require('web.Widget');
var { hidePDFJSButtons } = require('@web/js/libs/pdfjs');
var { hidePDFJSButtons } = require('@web/legacy/js/libs/pdfjs');
var QWeb = core.qweb;
@@ -5,7 +5,7 @@ import useShouldUpdateBasedOnProps from '@mail/component_hooks/use_should_update
import useStore from '@mail/component_hooks/use_store/use_store';
import { link } from '@mail/model/model_field_command';
import { hidePDFJSButtons } from '@web/js/libs/pdfjs';
import { hidePDFJSButtons } from '@web/legacy/js/libs/pdfjs';
const { Component, QWeb } = owl;
const { useRef } = owl.hooks;
+1 -1
View File
@@ -2,7 +2,7 @@
import core from 'web.core';
import Widget from 'web.Widget';
import { hidePDFJSButtons } from '@web/js/libs/pdfjs';
import { hidePDFJSButtons } from '@web/legacy/js/libs/pdfjs';
var QWeb = core.qweb;
@@ -1,30 +1,37 @@
/** @odoo-module **/
import core from 'web.core';
import DebugManager from 'web.DebugManager.Backend';
import { registry } from "@web/core/registry";
var _t = core._t;
/**
* adds a new method available for the debug manager, called by the "Manage Messages" button.
*
*/
DebugManager.include({
getMailMessages: function () {
var selectedIDs = this._controller.getSelectedIds();
if (!selectedIDs.length) {
console.warn(_t("No message available"));
return;
}
this.do_action({
res_model: 'mail.message',
name: _t('Manage Messages'),
views: [[false, 'list'], [false, 'form']],
type: 'ir.actions.act_window',
domain: [['res_id', '=', selectedIDs[0]], ['model', '=', this._controller.modelName]],
context: {
default_res_model: this._controller.modelName,
default_res_id: selectedIDs[0],
},
});
},
});
export function manageMessages({ action, component, env }) {
const selectedIds = component.widget.getSelectedIds();
if (!selectedIds.length) {
return null; // No record
}
const description = env._t("Manage Messages");
return {
type: "item",
description,
callback: () => {
env.services.action.doAction({
res_model: "mail.message",
name: description,
views: [
[false, "list"],
[false, "form"],
],
type: "ir.actions.act_window",
domain: [
["res_id", "=", selectedIds[0]],
["model", "=", action.res_model],
],
context: {
default_res_model: action.res_model,
default_res_id: selectedIds[0],
},
});
},
sequence: 325,
};
}
registry.category("debug").category("form").add("mail.manageMessages", manageMessages);
@@ -191,7 +191,7 @@ function factory(dependencies) {
options: {
active_id: this.threadToActiveId(this),
clear_breadcrumbs: false,
on_reverse_breadcrumb: () => this.close(),
on_reverse_breadcrumb: () => this.close(), // this is useless, close is called by destroy anyway
},
});
}
+36 -2
View File
@@ -18,12 +18,17 @@ import AbstractStorageService from 'web.AbstractStorageService';
import NotificationService from 'web.NotificationService';
import RamStorage from 'web.RamStorage';
import {
createActionManager,
createView,
makeTestPromise,
mock,
} from 'web.test_utils';
import Widget from 'web.Widget';
import {
createWebClient,
getActionManagerTestConfig,
} from "@web/../tests/webclient/actions/helpers";
import { ComponentAdapter } from "web.OwlCompatibility";
import LegacyMockServer from "web.MockServer";
const {
addMockEnvironment,
@@ -546,7 +551,36 @@ async function start(param0 = {}) {
}
});
} else if (hasActionManager) {
widget = await createActionManager(kwargs);
let testConfig;
if (!kwargs.testConfig) {
testConfig = getActionManagerTestConfig();
} else {
testConfig = kwargs.testConfig;
delete kwargs.testConfig;
}
if (kwargs.actions) {
const actions = {};
kwargs.actions.forEach((act) => {
actions[act.xml_id || act.id] = act;
});
Object.assign(testConfig.serverData.actions, actions);
delete kwargs.actions;
}
Object.assign(testConfig.serverData.views, kwargs.archs);
delete kwargs.archs;
Object.assign(testConfig.serverData.models, kwargs.data);
delete kwargs.data;
const mockRPC = kwargs.mockRPC;
delete kwargs.mockRPC;
const legacyParams = kwargs;
legacyParams.withLegacyMockServer = true;
const widget = await createWebClient({ testConfig, mockRPC, legacyParams });
legacyPatch(widget, {
destroy() {
destroyCallbacks.forEach(callback => callback({ widget }));
-8
View File
@@ -1,14 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<!--
extends the debug mode menu to allow access to the attachment list view of the current record.
-->
<t t-extend="WebClient.DebugManager.View">
<t t-jquery="a[data-action='get_metadata']" t-operation="after">
<a role="menuitem" href="#" data-action="getMailMessages" class="dropdown-item">Manage Messages</a>
</t>
</t>
<!--
@param {mail.DocumentViewer} widget
-->
+110 -92
View File
@@ -2,8 +2,14 @@
import ActivityView from '@mail/js/views/activity/activity_view';
import testUtils from 'web.test_utils';
import { createWebClient } from "@web/../tests/webclient/actions/helpers";
var createActionManager = testUtils.createActionManager;
import { legacyExtraNextTick } from "@web/../tests/helpers/utils";
import { doAction, getActionManagerTestConfig } from "@web/../tests/webclient/actions/helpers";
import { registry } from "@web/core/registry";
import { makeFakeUserService } from "@web/../tests/helpers/mock_services";
let testConfig;
var createView = testUtils.createView;
@@ -108,6 +114,8 @@ QUnit.module('activity view', {
],
},
};
testConfig = getActionManagerTestConfig();
Object.assign(testConfig.serverData.models, this.data);
}
});
@@ -350,49 +358,68 @@ QUnit.test('activity view: activity widget', async function (assert) {
activity.destroy();
});
QUnit.test('activity view: no group_by_menu and no comparison_menu', async function (assert) {
QUnit.test("activity view: no group_by_menu and no comparison_menu", async function (assert) {
assert.expect(4);
var actionManager = await createActionManager({
actions: [{
id: 1,
name: 'Task Action',
res_model: 'task',
type: 'ir.actions.act_window',
views: [[false, 'activity']],
}],
archs: {
'task,false,activity': '<activity string="Task">' +
'<templates>' +
'<div t-name="activity-box">' +
'<field name="foo"/>' +
'</div>' +
'</templates>' +
'</activity>',
'task,false,search': '<search></search>',
},
data: this.data,
session: {
user_context: {lang: 'zz_ZZ'},
},
mockRPC: function(route, args) {
if (args.method === 'get_activity_data') {
assert.deepEqual(args.kwargs.context, {lang: 'zz_ZZ'},
'The context should have been passed');
}
return this._super.apply(this, arguments);
},
});
testConfig.serverData.actions[1] = {
id: 1,
name: "Task Action",
res_model: "task",
type: "ir.actions.act_window",
views: [[false, "activity"]],
};
await actionManager.doAction(1);
testConfig.serverData.views = {
"task,false,activity":
'<activity string="Task">' +
"<templates>" +
'<div t-name="activity-box">' +
'<field name="foo"/>' +
"</div>" +
"</templates>" +
"</activity>",
"task,false,search": "<search></search>",
};
assert.containsN(actionManager, '.o_search_options .o_dropdown button:visible', 2,
"only two elements should be available in view search");
assert.isVisible(actionManager.$('.o_search_options .o_dropdown.o_filter_menu > button'),
"filter should be available in view search");
assert.isVisible(actionManager.$('.o_search_options .o_dropdown.o_favorite_menu > button'),
"favorites should be available in view search");
actionManager.destroy();
const mockRPC = (route, args) => {
if (args.method === "get_activity_data") {
assert.strictEqual(
args.kwargs.context.lang,
"zz_ZZ",
"The context should have been passed"
);
}
};
registry.category("services").add(
"user",
makeFakeUserService({
session_info: {
user_context: { lang: "zz_ZZ" },
},
}),
{ force: true }
);
const webClient = await createWebClient({ testConfig, mockRPC , legacyParams: {withLegacyMockServer: true}});
await doAction(webClient, 1);
assert.containsN(
webClient,
".o_search_options .o_dropdown button:visible",
2,
"only two elements should be available in view search"
);
assert.isVisible(
$(webClient.el).find(".o_search_options .o_dropdown.o_filter_menu > button"),
"filter should be available in view search"
);
assert.isVisible(
$(webClient.el).find(".o_search_options .o_dropdown.o_favorite_menu > button"),
"favorites should be available in view search"
);
});
QUnit.test('activity view: search more to schedule an activity for a record of a respecting model', async function (assert) {
@@ -458,63 +485,54 @@ QUnit.test('activity view: search more to schedule an activity for a record of a
activity.destroy();
});
QUnit.test('Activity view: discard an activity creation dialog', async function (assert) {
QUnit.test("Activity view: discard an activity creation dialog", async function (assert) {
assert.expect(2);
var actionManager = await createActionManager({
actions: [{
id: 1,
name: 'Task Action',
res_model: 'task',
type: 'ir.actions.act_window',
views: [[false, 'activity']],
}],
archs: {
'task,false,activity': `
<activity string="Task">
<templates>
<div t-name="activity-box">
<field name="foo"/>
</div>
</templates>
</activity>`,
'task,false,search': '<search></search>',
'mail.activity,false,form': `
<form>
<field name="display_name"/>
<footer>
<button string="Discard" class="btn-secondary" special="cancel"/>
</footer>
</form>`
},
data: this.data,
intercepts: {
do_action(ev) {
actionManager.doAction(ev.data.action, ev.data.options);
}
},
async mockRPC(route, args) {
if (args.method === 'check_access_rights') {
return true;
}
return this._super(...arguments);
},
});
await actionManager.doAction(1);
testConfig.serverData.actions[1] = {
id: 1,
name: "Task Action",
res_model: "task",
type: "ir.actions.act_window",
views: [[false, "activity"]],
};
await testUtils.dom.click(actionManager.$('.o_activity_view .o_data_row .o_activity_empty_cell')[0]);
assert.containsOnce(
$,
'.modal.o_technical_modal.show',
"Activity Modal should be opened");
testConfig.serverData.views = {
"task,false,activity": `
<activity string="Task">
<templates>
<div t-name="activity-box">
<field name="foo"/>
</div>
</templates>
</activity>`,
"task,false,search": "<search></search>",
"mail.activity,false,form": `
<form>
<field name="display_name"/>
<footer>
<button string="Discard" class="btn-secondary" special="cancel"/>
</footer>
</form>`,
};
await testUtils.dom.click($('.modal.o_technical_modal.show button[special="cancel"]'));
assert.containsNone(
$,
'.modal.o_technical_modal.show',
"Activity Modal should be closed");
const mockRPC = (route, args) => {
if (args.method === "check_access_rights") {
return true;
}
};
actionManager.destroy();
const webClient = await createWebClient({ testConfig, mockRPC, legacyParams: {withLegacyMockServer: true} });
await doAction(webClient, 1);
await testUtils.dom.click(
$(webClient.el).find(".o_activity_view .o_data_row .o_activity_empty_cell")[0]
);
await legacyExtraNextTick();
assert.containsOnce($, ".modal.o_technical_modal", "Activity Modal should be opened");
await testUtils.dom.click($('.modal.o_technical_modal button[special="cancel"]'));
await legacyExtraNextTick();
assert.containsNone($, ".modal.o_technical_modal", "Activity Modal should be closed");
});
});
@@ -1,62 +1,70 @@
/** @odoo-module **/
import testUtils from 'web.test_utils';
import { manageMessages } from "@mail/js/tools/debug_manager";
import { click, legacyExtraNextTick } from "@web/../tests/helpers/utils";
import {
createWebClient,
doAction,
getActionManagerTestConfig,
} from "@web/../tests/webclient/actions/helpers";
import { debugService } from "@web/core/debug/debug_service";
import { registry } from "@web/core/registry";
var createDebugManager = testUtils.createDebugManager;
QUnit.module("DebugMenu");
QUnit.module('Mail DebugManager', {}, function () {
QUnit.test("Manage Messages", async function (assert) {
assert.expect(6);
QUnit.test("Manage Messages", async function (assert) {
assert.expect(3);
var debugManager = await createDebugManager({
intercepts: {
do_action: function (event) {
assert.deepEqual(event.data.action, {
context: {
default_res_model: "testModel",
default_res_id: 5,
},
res_model: 'mail.message',
name: "Manage Messages",
views: [[false, 'list'], [false, 'form']],
type: 'ir.actions.act_window',
domain: [['res_id', '=', 5], ['model', '=', 'testModel']],
});
},
},
});
await debugManager.appendTo($('#qunit-fixture'));
// Simulate update debug manager from web client
var action = {
views: [{
displayName: "Form",
fieldsView: {
view_id: 1,
},
type: "form",
}],
};
var view = {
viewType: "form",
getSelectedIds: function () {
return [5];
},
modelName: 'testModel',
};
await testUtils.nextTick();
await debugManager.update('action', action, view);
var $messageMenu = debugManager.$('a[data-action=getMailMessages]');
assert.strictEqual($messageMenu.length, 1, "should have Manage Message menu item");
assert.strictEqual($messageMenu.text().trim(), "Manage Messages",
"should have correct menu item text");
await testUtils.dom.click(debugManager.$('> a')); // open dropdown
await testUtils.dom.click($messageMenu);
debugManager.destroy();
const testConfig = Object.assign(getActionManagerTestConfig(), {
debug: "1",
});
// Add fake "mail.message" model and arch
testConfig.serverData.models["mail.message"] = {
fields: { name: { string: "Name", type: "char" } },
records: [],
};
Object.assign(testConfig.serverData.views, {
"mail.message,false,list": `<tree/>`,
"mail.message,false,form": `<form/>`,
"mail.message,false,search": `<search/>`,
});
// Activate debug service with "Manage Message" item
registry.category("services").add("debug", debugService);
registry.category("debug").category("form").add("manageMessages", manageMessages);
async function mockRPC(route, args) {
if (args.method === "check_access_rights") {
return true;
}
if (route === "/web/dataset/search_read" && args.model === "mail.message") {
assert.strictEqual(args.context.default_res_id, 5);
assert.strictEqual(args.context.default_res_model, "partner");
assert.deepEqual(args.domain, ["&", ["res_id", "=", 5], ["model", "=", "partner"]]);
}
}
const wc = await createWebClient({ testConfig, mockRPC });
await doAction(wc, 3, { viewType: "form", resId: 5 });
await legacyExtraNextTick();
await click(wc.el, ".o_debug_manager .o_dropdown_toggler");
const dropdownItems = wc.el.querySelectorAll(
".o_debug_manager .o_dropdown_menu .o_dropdown_item"
);
assert.strictEqual(dropdownItems.length, 1);
assert.strictEqual(
dropdownItems[0].innerText.trim(),
"Manage Messages",
"should have correct menu item text"
);
await click(dropdownItems[0]);
await legacyExtraNextTick();
assert.strictEqual(
wc.el.querySelector(".breadcrumb-item.active").innerText.trim(),
"Manage Messages"
);
});
@@ -14,7 +14,7 @@ odoo.define('mass_mailing.unsubscribe', function (require) {
var token = (location.search.split('token' + '=')[1] || '').split('&')[0];
if (!$('.o_unsubscribe_form').length) {
return Promise.reject("DOM doesn't contain '.o_unsubscribe_form'");
return;
}
session.load_translations().then(function () {
if (email != '' && email != undefined){
@@ -1,26 +1,18 @@
odoo.define('web.company_autocomplete', function (require) {
"use strict";
/** @odoo-module **/
const AbstractWebClient = require('web.AbstractWebClient');
const session = require('web.session');
import { registry } from "@web/core/registry";
return AbstractWebClient.include({
export const companyAutocompleteService = {
dependencies: ["orm", "user"],
start: function () {
if (session.iap_company_enrich) {
const current_company_id = session.user_companies.current_company;
this._rpc({
model: 'res.company',
method: 'iap_enrich_auto',
args: [current_company_id],
}, {
shadow: true,
});
start(env, { orm, user }) {
if (odoo.session_info.iap_company_enrich) {
const currentCompanyId = user.current_company.id;
orm.silent.call("res.company", "iap_enrich_auto", [currentCompanyId], {});
}
return this._super.apply(this, arguments);
},
};
});
});
registry
.category("services")
.add("partner_autocomplete.companyAutocomplete", companyAutocompleteService);
+37
View File
@@ -164,6 +164,43 @@
],
'point_of_sale.pos_assets_backend': [
('include', 'web.assets_backend'),
('remove', 'web/static/src/webclient/menu_service.js'),
('remove', 'web/static/src/core/errors/error_handlers.js'),
('remove', 'web/static/src/legacy/legacy_rpc_error_handler.js'),
],
'point_of_sale.tests_assets': [
'web/static/lib/daterangepicker/daterangepicker.css',
'web/static/lib/qunit/qunit-2.9.1.css',
'web/static/lib/qunit/qunit-2.9.1.js',
'web/static/tests/legacy/helpers/**/*',
('remove', 'web/static/tests/legacy/helpers/test_utils_tests.js'),
'web/static/tests/legacy/legacy_setup.js',
'web/static/tests/helpers/**/*.js',
'web/static/tests/qunit.js',
'web/static/tests/main.js',
'web/static/tests/setup.js',
# These 2 lines below are taken from web.assets_frontend
# They're required for the web.frontend_legacy to work properly
# It is expected to add other lines coming from the web.assets_frontend
# if we need to add more and more legacy stuff that would require other scss or js.
('include', 'web._assets_helpers'),
'web/static/lib/bootstrap/scss/_variables.scss',
('include', 'web.frontend_legacy'),
],
'point_of_sale.qunit_suite_tests': [
'web/static/tests/legacy/component_extension_tests.js',
'point_of_sale/static/tests/unit/**/*',
('remove', 'point_of_sale/static/tests/unit/helpers/test_main.js')
],
'point_of_sale.assets_backend_prod_only': [
'point_of_sale/static/src/js/chrome_adapter.js',
'point_of_sale/static/src/js/main.js',
'web/static/src/start.js',
'web/static/src/legacy/legacy_setup.js',
],
'web.assets_qweb': [
'point_of_sale/static/src/xml/**/*',
@@ -0,0 +1,48 @@
/** @odoo-module */
import { useService } from "@web/core/service_hook";
import Chrome from "point_of_sale.Chrome";
import Registries from "point_of_sale.Registries";
import { configureGui } from "point_of_sale.Gui";
import { useBus } from "@web/core/bus_hook";
import PosComponent from "point_of_sale.PosComponent";
import PopupControllerMixin from "point_of_sale.PopupControllerMixin";
import { useErrorHandlers } from "point_of_sale.custom_hooks";
import { ConnectionLostError } from "@web/core/network/rpc_service";
function setupResponsivePlugin(env) {
const isMobile = () => window.innerWidth <= 768;
env.isMobile = isMobile();
const updateEnv = owl.utils.debounce(() => {
if (env.isMobile !== isMobile()) {
env.isMobile = !env.isMobile;
env.qweb.forceUpdate();
}
}, 15);
window.addEventListener("resize", updateEnv);
}
export class ChromeAdapter extends PopupControllerMixin(PosComponent) {
setup() {
this.PosChrome = Registries.Component.get(Chrome);
this.legacyActionManager = useService("legacy_action_manager");
useErrorHandlers();
useBus(this.env.bus, "ERROR_DISPATCHED", (error) => {
if (error instanceof ConnectionLostError) {
error = { code: -9999 }; // just a negative number
}
this._handlePushOrderError(error);
});
this.env = owl.Component.env;
useBus(this.env.qweb, "update", () => this.render());
const chrome = owl.hooks.useRef("chrome");
owl.hooks.onMounted(async () => {
await chrome.comp.start();
configureGui({ component: chrome.comp });
setupResponsivePlugin(this.env);
});
}
}
ChromeAdapter.template = owl.tags.xml`<PosChrome t-ref="chrome" webClient="legacyActionManager"/>`;
@@ -1,20 +1,21 @@
odoo.define('point_of_sale.DebugManager.Backend', function(require) {
'use strict';
/** @odoo-module */
const { _t } = require('web.core');
const DebugManager = require('web.DebugManager.Backend');
import { registry } from "@web/core/registry";
DebugManager.include({
/**
* Runs the JS (desktop) tests
*/
perform_pos_js_tests() {
this.do_action({
name: _t('JS Tests'),
target: 'new',
type: 'ir.actions.act_url',
url: '/pos/ui/tests?mod=*',
function runPoSJSTests({ env }) {
return {
type: "item",
description: env._t("Run Point of Sale JS Tests"),
callback: () => {
env.services.action.doAction({
name: env._t("JS Tests"),
target: "new",
type: "ir.actions.act_url",
url: "/pos/ui/tests?mod=*",
});
},
});
});
sequence: 35,
};
}
registry.category("debug").add("point_of_sale.runPoSJSTests", runPoSJSTests);
+29 -44
View File
@@ -1,49 +1,34 @@
odoo.define('web.web_client', function (require) {
'use strict';
/** @odoo-module */
const AbstractService = require('web.AbstractService');
const env = require('web.env');
const WebClient = require('web.AbstractWebClient');
const Chrome = require('point_of_sale.Chrome');
const Registries = require('point_of_sale.Registries');
const { configureGui } = require('point_of_sale.Gui');
import { startWebClient } from "@web/start";
owl.config.mode = env.isDebug() ? 'dev' : 'prod';
owl.Component.env = env;
import { ChromeAdapter } from "@point_of_sale/js/chrome_adapter";
import Registries from "point_of_sale.Registries";
import { registry } from "@web/core/registry";
// For consistency's sake, we should trigger"WEB_CLIENT_READY" on the bus when PosApp is mounted
// But we can't since mail and some other poll react on that cue, and we don't want those services started
class PosApp extends owl.Component {
setup() {
this.Components = registry.category("main_components").getEntries();
}
}
PosApp.template = owl.tags.xml`
<body>
<ChromeAdapter />
<div>
<t t-foreach="Components" t-as="Component" t-key="Component[0]">
<t t-component="Component[1]"/>
</t>
</div>
</body>
`;
PosApp.components = { ChromeAdapter };
function startPosApp() {
Registries.Component.add(owl.misc.Portal);
Registries.Component.freeze();
startWebClient(PosApp);
}
function setupResponsivePlugin(env) {
const isMobile = () => window.innerWidth <= 768;
env.isMobile = isMobile();
const updateEnv = owl.utils.debounce(() => {
if (env.isMobile !== isMobile()) {
env.isMobile = !env.isMobile;
env.qweb.forceUpdate();
}
}, 15);
window.addEventListener("resize", updateEnv);
}
setupResponsivePlugin(owl.Component.env);
async function startPosApp(webClient) {
Registries.Component.freeze();
await env.session.is_bound;
env.qweb.addTemplates(env.session.owlTemplates);
env.bus = new owl.core.EventBus();
await owl.utils.whenReady();
await webClient.setElement(document.body);
await webClient.start();
webClient.isStarted = true;
const chrome = new (Registries.Component.get(Chrome))(null, { webClient });
await chrome.mount(document.querySelector('.o_action_manager'));
await chrome.start();
configureGui({ component: chrome });
}
AbstractService.prototype.deployServices(env);
const webClient = new WebClient();
startPosApp(webClient);
return webClient;
});
startPosApp();
@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-extend="WebClient.DebugManager.Backend">
<t t-jquery="a[data-action='perform_click_everywhere_test']" t-operation="after">
<a role="menuitem" href="#" data-action="perform_pos_js_tests" class="dropdown-item">Run Point of Sale JS Tests</a>
</t>
</t>
</templates>
@@ -50,7 +50,7 @@ odoo.define('point_of_sale.tour.pricelist', function (require) {
var steps = [{ // Leave category displayed by default
content: 'waiting for loading to finish',
extra_trigger: 'body .pos:not(:has(.loader))', // Pos has finished loading
trigger: 'body:not(.oe_wait)', // WebClient has finished Loading
trigger: '.o_loading_indicator:not(.o_loading)', // WebClient has finished Loading
run: function () {
var product_wall_shelf = posmodel.db.search_product_in_category(0, 'Wall Shelf Unit')[0];
var product_small_shelf = posmodel.db.search_product_in_category(0, 'Small Shelf')[0];
@@ -32,11 +32,12 @@
'login_number': login_number,
'debug': debug,
})"/>;
odoo.loadTemplatesPromise = fetch(`/web/webclient/qweb/${odoo.session_info.cache_hashes.qweb}?bundle=web.assets_qweb`).then(doc => doc.text());
</script>
<t t-call="point_of_sale.assets_common"/>
<t t-call="web.conditional_assets_tests"/>
<script type="text/javascript" src="/point_of_sale/static/src/js/main.js"></script>
<t t-call-assets="point_of_sale.assets_backend_prod_only" />
</head>
<body>
</body>
@@ -3,12 +3,8 @@
<template id="point_of_sale.qunit_suite_assets">
<t t-call-assets="point_of_sale.pos_assets_backend" t-js="false"/>
<link type="text/css" rel="stylesheet" href="/web/static/lib/qunit/qunit-2.9.1.css"/>
<script type="text/javascript" src="/web/static/lib/qunit/qunit-2.9.1.js"></script>
<script type="text/javascript" src="/web/static/tests/helpers/qunit_config.js"></script>
<script type="text/javascript" src="/web/static/tests/helpers/qunit_asserts.js"></script>
<t t-call-assets="point_of_sale.tests_assets" t-js="false"/>
<t t-call-assets="point_of_sale.tests_assets" t-css="false"/>
<style>
body {
position: relative;
@@ -25,36 +21,6 @@
display: inherit;
}
</style>
<script type="text/javascript" src="/web/static/tests/helpers/test_utils_create.js"></script>
<script type="text/javascript" src="/web/static/tests/helpers/test_utils_control_panel.js"></script>
<script type="text/javascript" src="/web/static/tests/helpers/test_utils_dom.js"></script>
<script type="text/javascript" src="/web/static/tests/helpers/test_utils_fields.js"></script>
<script type="text/javascript" src="/web/static/tests/helpers/test_utils_file.js"></script>
<script type="text/javascript" src="/web/static/tests/helpers/test_utils_form.js"></script>
<script type="text/javascript" src="/web/static/tests/helpers/test_utils_graph.js"></script>
<script type="text/javascript" src="/web/static/tests/helpers/test_utils_kanban.js"></script>
<script type="text/javascript" src="/web/static/tests/helpers/test_utils_mock.js"></script>
<script type="text/javascript" src="/web/static/tests/helpers/test_utils_modal.js"></script>
<script type="text/javascript" src="/web/static/tests/helpers/test_utils_pivot.js"></script>
<script type="text/javascript" src="/web/static/tests/helpers/test_utils.js"></script>
<script type="text/javascript" src="/web/static/tests/helpers/mock_server.js"></script>
<script type="text/javascript" src="/web/static/tests/helpers/test_env.js"></script>
<script type="text/javascript" src="/web/static/tests/component_extension_tests.js"></script>
<script type="text/javascript" src="/web/static/tests/boot_tests.js"></script>
<script type="text/javascript" src="/point_of_sale/static/tests/unit/helpers/test_env.js"></script>
<script type="text/javascript" src="/point_of_sale/static/tests/unit/helpers/test_main.js"></script>
</template>
<template id="point_of_sale.qunit_suite_tests">
<script type="text/javascript" src="/point_of_sale/static/tests/unit/test_ComponentRegistry.js"></script>
<script type="text/javascript" src="/point_of_sale/static/tests/unit/test_NumberBuffer.js"></script>
<script type="text/javascript" src="/point_of_sale/static/tests/unit/test_ChromeWidgets.js"></script>
<script type="text/javascript" src="/point_of_sale/static/tests/unit/test_ProductScreen.js"></script>
<script type="text/javascript" src="/point_of_sale/static/tests/unit/test_PaymentScreen.js"></script>
<script type="text/javascript" src="/point_of_sale/static/tests/unit/test_popups.js"></script>
</template>
<template id="point_of_sale.qunit_suite">
@@ -73,7 +39,7 @@
<t t-call="point_of_sale.assets_common" />
<t t-call="point_of_sale.qunit_suite_assets" />
<t t-call="point_of_sale.qunit_suite_tests" />
<t t-call-assets="point_of_sale.qunit_suite_tests" />
</t>
<div id="qunit"/>
+3 -1
View File
@@ -23,7 +23,6 @@ This module adds several features to the Point of Sale that are specific to rest
'views/pos_order_views.xml',
'views/pos_restaurant_views.xml',
'views/pos_config_views.xml',
'views/pos_restaurant_templates.xml',
],
'demo': [
'data/pos_restaurant_demo.xml',
@@ -69,6 +68,9 @@ This module adds several features to the Point of Sale that are specific to rest
'web.assets_tests': [
'pos_restaurant/static/tests/tours/**/*',
],
'point_of_sale.qunit_suite_tests': [
'pos_restaurant/static/tests/unit/**/*',
],
'web.assets_qweb': [
'pos_restaurant/static/src/xml/**/*',
],
@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="pos_restaurant.qunit_suite_tests" inherit_id="point_of_sale.qunit_suite_tests">
<xpath expr="." position="inside">
<script type="text/javascript" src="/pos_restaurant/static/tests/unit/test_FloorScreen.js"></script>
</xpath>
</template>
</odoo>
@@ -1,11 +1,10 @@
odoo.define('product.pricelist.report.tests', function (require) {
"use strict";
const core = require('web.core');
const GeneratePriceList = require('product.generate_pricelist').GeneratePriceList;
const NotificationService = require('web.NotificationService');
const testUtils = require('web.test_utils');
const createActionManager = testUtils.createActionManager;
const testUtilsMock = require('web.test_utils_mock');
const { createWebClient, getActionManagerTestConfig, doAction } = require('@web/../tests/webclient/actions/helpers');
let testConfig;
QUnit.module('Product Pricelist', {
beforeEach: function () {
@@ -32,6 +31,8 @@ QUnit.module('Product Pricelist', {
}]
}
};
testConfig = getActionManagerTestConfig();
Object.assign(testConfig.serverData, {models: this.data});
},
}, function () {
QUnit.test('Pricelist Client Action', async function (assert) {
@@ -50,21 +51,14 @@ QUnit.module('Product Pricelist', {
return this._super.apply(this, arguments);
},
});
const mockRPC = (route, args) => {
if (route === '/web/dataset/call_kw/report.product.report_pricelist/get_html') {
return Promise.resolve("");
}
};
const actionManager = await createActionManager({
data: this.data,
mockRPC: function(route, args) {
if (route === '/web/dataset/call_kw/report.product.report_pricelist/get_html') {
return Promise.resolve("");
}
return this._super(route, args);
},
services: {
notification: NotificationService,
},
});
await actionManager.doAction({
const webClient = await createWebClient({ testConfig, mockRPC });
await doAction(webClient, {
id: 1,
name: 'Generate Pricelist',
tag: 'generate_pricelist',
@@ -78,7 +72,7 @@ QUnit.module('Product Pricelist', {
});
// checking default pricelist
assert.strictEqual(actionManager.$('.o_field_many2one input').val(), "Public Pricelist",
assert.strictEqual($(webClient.el).find('.o_field_many2one input').val(), "Public Pricelist",
"should have default pricelist");
// changing pricelist
@@ -86,28 +80,28 @@ QUnit.module('Product Pricelist', {
await testUtils.fields.many2one.clickItem("pricelist_id", "Test");
// check wherther pricelist value has been updated or not. along with that check default quantities should be there.
assert.strictEqual(actionManager.$('.o_field_many2one input').val(), "Test",
assert.strictEqual($(webClient.el).find('.o_field_many2one input').val(), "Test",
"After pricelist change, the pricelist_id field should be updated");
assert.strictEqual(actionManager.$('.o_badges > .badge').length, 3,
assert.strictEqual($(webClient.el).find('.o_badges > .badge').length, 3,
"There should be 3 default Quantities");
// existing quantity can not be added.
await testUtils.dom.click(actionManager.$('.o_add_qty'));
await testUtils.dom.click($(webClient.el).find('.o_add_qty'));
let notificationElement = document.body.querySelector('.o_notification_manager .o_notification.bg-info');
assert.strictEqual(notificationElement.querySelector('.o_notification_content').textContent,
"Quantity already present (1).", "Existing Quantity can not be added");
// adding few more quantities to check.
actionManager.$('.o_product_qty').val(2);
$(webClient.el).find('.o_product_qty').val(2);
Qty.push(2);
await testUtils.dom.click(actionManager.$('.o_add_qty'));
actionManager.$('.o_product_qty').val(3);
await testUtils.dom.click($(webClient.el).find('.o_add_qty'));
$(webClient.el).find('.o_product_qty').val(3);
Qty.push(3);
await testUtils.dom.click(actionManager.$('.o_add_qty'));
await testUtils.dom.click($(webClient.el).find('.o_add_qty'));
// should not be added more then 5 quantities.
actionManager.$('.o_product_qty').val(4);
await testUtils.dom.click(actionManager.$('.o_add_qty'));
$(webClient.el).find('.o_product_qty').val(4);
await testUtils.dom.click($(webClient.el).find('.o_add_qty'));
notificationElement = document.body.querySelector('.o_notification_manager .o_notification.bg-warning');
assert.strictEqual(notificationElement.querySelector('.o_notification_content').textContent,
@@ -116,15 +110,15 @@ QUnit.module('Product Pricelist', {
// removing all the quantities should work
Qty.pop(10);
await testUtils.dom.click(actionManager.$('.o_badges .badge:contains("10") .o_remove_qty'));
await testUtils.dom.click($(webClient.el).find('.o_badges .badge:contains("10") .o_remove_qty'));
Qty.pop(5);
await testUtils.dom.click(actionManager.$('.o_badges .badge:contains("5") .o_remove_qty'));
await testUtils.dom.click($(webClient.el).find('.o_badges .badge:contains("5") .o_remove_qty'));
Qty.pop(3);
await testUtils.dom.click(actionManager.$('.o_badges .badge:contains("3") .o_remove_qty'));
await testUtils.dom.click($(webClient.el).find('.o_badges .badge:contains("3") .o_remove_qty'));
Qty.pop(2);
await testUtils.dom.click(actionManager.$('.o_badges .badge:contains("2") .o_remove_qty'));
await testUtils.dom.click($(webClient.el).find('.o_badges .badge:contains("2") .o_remove_qty'));
Qty.pop(1);
await testUtils.dom.click(actionManager.$('.o_badges .badge:contains("1") .o_remove_qty'));
await testUtils.dom.click($(webClient.el).find('.o_badges .badge:contains("1") .o_remove_qty'));
assert.verifySteps([
'field_changed',
@@ -138,7 +132,6 @@ QUnit.module('Product Pricelist', {
]);
testUtils.mock.unpatch(GeneratePriceList);
actionManager.destroy();
});
}
@@ -33,7 +33,7 @@ tour.register('sale_timesheet_tour', {
actions.text('Service Product (Prepaid Hours)', this.$anchor.find('input'));
},
}, {
trigger: 'ul.ui-menu.ui-widget.ui-autocomplete > li:first-child > a',
trigger: 'ul.ui-menu.ui-widget.ui-autocomplete > li:first-child > a:contains(Service Product (Prepaid Hours))',
content: 'Select the prepaid service product in the autocomplete dropdown',
run: 'click',
}, {
@@ -155,11 +155,11 @@ tour.register('sale_timesheet_tour', {
console.error('The quantity delivered on this Sales Order Item should be equal to 1.00 hour. qtyDelivered = ' + qtyDelivered);
},
}, {
trigger: 'a[data-menu-xmlid="project.menu_project_config"]',
trigger: 'button[data-menu-xmlid="project.menu_project_config"]',
content: 'Click on the Configuration menu.',
run: 'click',
}, {
trigger: 'a[role="menuitem"][data-menu-xmlid="project.menu_projects_config"]',
trigger: 'li[data-menu-xmlid="project.menu_projects_config"]',
content: 'Select Configuration > Projects.',
run: 'click',
}, {
@@ -241,7 +241,7 @@ tour.register('sale_timesheet_tour', {
trigger: '.o_form_button_save',
content: 'Save Project',
}, {
trigger: 'a[role="menuitem"][data-menu-xmlid="project.menu_projects"]',
trigger: 'li[data-menu-xmlid="project.menu_projects"]',
content: 'Select Projects'
}, {
trigger: 'div.o_kanban_primary_left :contains("Project for Freeman")',
@@ -7,7 +7,9 @@ odoo.define('stock.stock_traceability_report_backend_tests', function (require)
const testUtils = require('web.test_utils');
const { patch, unpatch } = require('web.utils');
const { createActionManager, dom: domUtils } = testUtils;
const { dom: domUtils } = testUtils;
const { legacyExtraNextTick } = require("@web/../tests/helpers/utils");
const { createWebClient, getActionManagerTestConfig, doAction } = require('@web/../tests/webclient/actions/helpers');
/**
* Helper function to instantiate a stock report action.
@@ -91,49 +93,48 @@ odoo.define('stock.stock_traceability_report_backend_tests', function (require)
this.__superMounted(...arguments);
},
});
const models = {
partner: {
fields: {
display_name: { string: "Displayed name", type: "char" },
},
records: [
{id: 1, display_name: "Genda Swami"},
],
},
};
let testConfig = getActionManagerTestConfig();
Object.assign(testConfig.serverData, { models });
const actionManager = await createActionManager({
actions: [
{
id: 42,
name: "Stock report",
tag: 'stock_report_generic',
type: 'ir.actions.client',
context: {},
params: {},
},
],
archs: {
'partner,false,form': '<form><field name="display_name"/></form>',
'partner,false,search': '<search></search>',
},
data: {
partner: {
fields: {
display_name: { string: "Displayed name", type: "char" },
},
records: [
{id: 1, display_name: "Genda Swami"},
],
},
},
testConfig.serverData.views = {
'partner,false,form': '<form><field name="display_name"/></form>',
'partner,false,search': '<search></search>',
};
testConfig.serverData.actions[42] = {
id: 42,
name: "Stock report",
tag: 'stock_report_generic',
type: 'ir.actions.client',
context: {},
params: {},
};
const webClient = await createWebClient({
testConfig,
mockRPC: function (route) {
if (route === '/web/dataset/call_kw/stock.traceability.report/get_html') {
return Promise.resolve({
html: '<a class="o_stock_reports_web_action" href="#" data-active-id="1" data-res-model="partner">Go to form view</a>',
});
}
return this._super.apply(this, arguments);
},
intercepts: {
do_action: ev => actionManager.doAction(ev.data.action, ev.data.options),
},
});
await actionManager.doAction(42);
await domUtils.click(actionManager.$('.o_stock_reports_web_action'));
await domUtils.click(actionManager.$('.breadcrumb-item:first'));
actionManager.destroy();
await doAction(webClient, 42);
await domUtils.click($(webClient.el).find('.o_stock_reports_web_action'));
await legacyExtraNextTick();
await domUtils.click($(webClient.el).find('.breadcrumb-item:first'));
await legacyExtraNextTick();
webClient.destroy();
assert.verifySteps([
'mounted 1',
+4 -2
View File
@@ -18,7 +18,7 @@ Odoo Web tours.
],
'assets': {
'web.assets_qweb': [
'web_tour/static/src/xml/debug_manager.xml',
'web_tour/static/src/debug/tour_dialog_component.xml',
],
'web.assets_common': [
'web_tour/static/src/scss/**/*',
@@ -30,7 +30,9 @@ Odoo Web tours.
'web_tour/static/src/js/tour_utils.js',
],
'web.assets_backend': [
'web_tour/static/src/js/debug_manager.js',
'web_tour/static/src/debug/debug_manager.js',
'web_tour/static/src/debug/tour_dialog_component.js',
'web_tour/static/src/services/*.js',
],
'web.assets_frontend': [
'web_tour/static/src/js/public/**/*',
@@ -0,0 +1,47 @@
/** @odoo-module **/
import { browser } from "@web/core/browser/browser";
import { registry } from "@web/core/registry";
import ToursDialog from "@web_tour/debug/tour_dialog_component";
import utils from "web_tour.utils";
function debugDisableTourItem(env) {
if (!env.services.user.isSystem) {
return {};
}
const activeTours = env.services.tour.getActiveTours();
if (activeTours.length === 0) {
return {};
}
return {
type: "item",
description: env._t("Disable Tours"),
callback: async () => {
await env.services.orm.call("web_tour.tour", "consume", [activeTours]);
for (const tourName of activeTours) {
browser.localStorage.removeItem(utils.get_debugging_key(tourName));
}
browser.location.reload();
},
sequence: 33,
};
}
function debugStartTourItem(env) {
if (!env.services.user.isSystem) {
return {};
}
return {
type: "item",
description: env._t("Start Tour"),
callback: async () => {
env.services.dialog.open(ToursDialog);
},
sequence: 32,
};
}
registry
.category("debug")
.add("web_tour.start_tour", debugStartTourItem)
.add("web_tour.disable_tour", debugDisableTourItem);
@@ -0,0 +1,43 @@
/** @odoo-module **/
import { useService } from "@web/core/service_hook";
export default class ToursDialog extends owl.Component {
setup() {
this.tourService = useService("tour");
this.onboardingTours = this.tourService.getOnboardingTours();
this.testingTours = this.tourService.getTestingTours();
}
//--------------------------------------------------------------------------
// Getters
//--------------------------------------------------------------------------
get dialogTitle() {
return this.env._t("Tours");
}
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* Resets the given tour to its initial step, in onboarding mode.
*
* @private
* @param {MouseEvent} ev
*/
_onStartTour(ev) {
this.tourService.reset(ev.target.dataset.name);
}
/**
* Starts the given tour in test mode.
*
* @private
* @param {MouseEvent} ev
*/
_onTestTour(ev) {
this.tourService.run(ev.target.dataset.name);
}
}
ToursDialog.template = "web_tour.ToursDialog";
@@ -0,0 +1,61 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="web_tour.ToursDialog" owl="1">
<Dialog title="dialogTitle">
<div>
<t t-call="web_tour.ToursDialog.Table">
<t t-set="caption">Onboarding tours</t>
<t t-set="tours" t-value="onboardingTours"/>
</t>
<t t-if="testingTours.length" t-call="web_tour.ToursDialog.Table">
<t t-set="caption">Testing tours</t>
<t t-set="tours" t-value="testingTours"/>
</t>
</div>
</Dialog>
</t>
<t t-name="web_tour.ToursDialog.Table" owl="1">
<div class="table-responsive">
<table class="table table-sm table-striped">
<caption style="caption-side: top; font-size: 14px">
<t t-esc="caption"/>
</caption>
<thead>
<tr>
<th>Sequence</th>
<th width="50%">Name</th>
<th width="50%">Path</th>
<th>Start</th>
<th>Test</th>
</tr>
</thead>
<tbody>
<tr t-foreach="tours" t-as="tour">
<td><t t-esc="tour.sequence"/></td>
<td><t t-esc="tour.name"/></td>
<td><t t-esc="tour.url"/></td>
<td>
<button type="button"
class="btn btn-primary fa fa-play o_start_tour"
t-on-click.prevent="_onStartTour"
t-att-data-name="tour.name"
aria-label="Start tour"
title="Start tour"/>
</td>
<td>
<button type="button"
class="btn btn-primary fa fa-cogs o_test_tour"
t-on-click.prevent="_onTestTour"
t-att-data-name="tour.name"
aria-label="Test tour"
title="Test tour"/>
</td>
</tr>
</tbody>
</table>
</div>
</t>
</templates>
@@ -1,81 +0,0 @@
odoo.define('web_tour.DebugManager.Backend', function (require) {
"use strict";
var core = require("web.core");
var DebugManager = require('web.DebugManager.Backend');
var Dialog = require("web.Dialog");
var local_storage = require('web.local_storage');
var tour = require('web_tour.tour');
var utils = require('web_tour.utils');
var get_debugging_key = utils.get_debugging_key;
function get_active_tours () {
return _.difference(_.keys(tour.tours), tour.consumed_tours);
}
DebugManager.include({
start: function () {
this.consume_tours_enabled = get_active_tours().length > 0;
return this._super.apply(this, arguments);
},
consume_tours: function () {
var active_tours = get_active_tours();
if (active_tours.length > 0) { // tours might have been consumed meanwhile
this._rpc({
model: 'web_tour.tour',
method: 'consume',
args: [active_tours],
})
.then(function () {
for (const tourName of active_tours) {
local_storage.removeItem(get_debugging_key(tourName));
}
window.location.reload();
});
}
},
start_tour: async function () {
const tours = Object.values(tour.tours).sort((t1, t2) => {
return (t1.sequence - t2.sequence) || (t1.name < t2.name ? -1 : 1);
});
const dialog = new Dialog(this, {
title: 'Tours',
$content: core.qweb.render('web_tour.ToursDialog', {
onboardingTours: tours.filter(t => !t.test),
testingTours: tours.filter(t => t.test),
}),
});
await dialog.open().opened();
dialog.$('.o_start_tour').on('click', this._onStartTour.bind(this));
dialog.$('.o_test_tour').on('click', this._onTestTour.bind(this));
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* Resets the given tour to its initial step, in onboarding mode.
*
* @private
* @param {MouseEvent}
*/
_onStartTour(ev) {
ev.preventDefault();
tour.reset($(ev.target).data('name'));
},
/**
* Starts the given tour in test mode.
*
* @private
* @param {MouseEvent}
*/
_onTestTour(ev) {
ev.preventDefault();
tour.run($(ev.target).data('name'));
},
});
});
@@ -7,7 +7,6 @@ var local_storage = require('web.local_storage');
var mixins = require('web.mixins');
var utils = require('web_tour.utils');
var TourStepUtils = require('web_tour.TourStepUtils');
var RainbowMan = require('web.RainbowMan');
var RunningTourActionHelper = require('web_tour.RunningTourActionHelper');
var ServicesMixin = require('web.ServicesMixin');
var session = require('web.session');
@@ -433,7 +432,7 @@ return core.Class.extend(mixins.EventDispatcherMixin, ServicesMixin, {
message = _t('<strong><b>Good job!</b> You went through all steps of this tour.</strong>');
}
const fadeout = this.tours[tour_name].fadeout;
new RainbowMan({message, fadeout}).appendTo(this.$body);
core.bus.trigger('show-effect', {type: "rainbowman", message, fadeout});
}
this.tours[tour_name].current_step = 0;
local_storage.removeItem(get_step_key(tour_name));
@@ -29,7 +29,7 @@ return core.Class.extend({
showAppsMenuItem() {
return {
edition: 'community',
trigger: '.o_menu_apps a',
trigger: '.o_navbar_apps_menu button',
auto: true,
position: 'bottom',
};
@@ -0,0 +1,93 @@
/** @odoo-module **/
import { registry } from "@web/core/registry";
import tourManager from "web_tour.tour";
export const tourService = {
start() {
/**
* @private
* @returns {Object} All the tours as a map
*/
function _getAllTourMap() {
return tourManager.tours;
}
/**
* @private
* @returns {Object} All the active tours as a map
*/
function _getActiveTourMap() {
return Object.fromEntries(
Object.entries(_getAllTourMap()).filter(
([key, value]) => !tourManager.consumed_tours.includes(key)
)
);
}
/**
* @private
* @returns {Array} Takes an Object (map) of tours and returns all the values
*/
function _fromTourMapToArray(tourMap) {
return Object.values(tourMap).sort((t1, t2) => {
return t1.sequence - t2.sequence || (t1.name < t2.name ? -1 : 1);
});
}
/**
* @returns {Array} All the tours
*/
function getAllTours() {
return _fromTourMapToArray(_getAllTourMap());
}
/**
* @returns {Array} All the active tours
*/
function getActiveTours() {
return _fromTourMapToArray(_getActiveTourMap());
}
/**
* @returns {Array} The onboarding tours
*/
function getOnboardingTours() {
return getAllTours().filter((t) => !t.test);
}
/**
* @returns {Array} The testing tours
*/
function getTestingTours() {
return getAllTours().filter((t) => t.test);
}
/**
* @param {string} tourName
* Run a tour
*/
function run(tourName) {
return tourManager.run(tourName);
}
/**
* @param {string} tourName
* Reset a tour
*/
function reset(tourName) {
return tourManager.reset(tourName);
}
return {
getAllTours,
getActiveTours,
getOnboardingTours,
getTestingTours,
run,
reset,
};
},
};
registry.category("services").add("tour", tourService);
@@ -1,54 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-extend="WebClient.DebugManager.Backend">
<t t-jquery="a[data-action='select_view']" t-operation="after">
<t t-if="manager._is_admin">
<a t-if="manager.consume_tours_enabled" href="#" class="dropdown-item" data-action="consume_tours">Disable Tours</a>
<a href="#" class="dropdown-item" data-action="start_tour">Start Tour</a>
</t>
</t>
</t>
<t t-name="web_tour.ToursDialog">
<div>
<t t-call="web_tour.ToursDialog.Table">
<t t-set="caption" t-value="'Onboarding tours'"/>
<t t-set="tours" t-value="onboardingTours"/>
</t>
<t t-if="testingTours.length" t-call="web_tour.ToursDialog.Table">
<t t-set="caption" t-value="'Testing tours'"/>
<t t-set="tours" t-value="testingTours"/>
</t>
</div>
</t>
<t t-name="web_tour.ToursDialog.Table">
<div class="table-responsive">
<table class="table table-sm table-striped">
<caption style="caption-side: top; font-size: 14px">
<t t-esc="caption"/>
</caption>
<thead>
<tr>
<th>Sequence</th>
<th width="50%">Name</th>
<th width="50%">Path</th>
<th>Start</th>
<th>Test</th>
</tr>
</thead>
<tbody>
<tr t-foreach="tours" t-as="tour">
<td><t t-esc="tour.sequence"/></td>
<td><t t-esc="tour.name"/></td>
<td><t t-esc="tour.url"/></td>
<td><button type="button" class="btn btn-primary fa fa-play o_start_tour" t-att-data-name="tour.name" aria-label="Start tour" title="Start tour"/></td>
<td><button type="button" class="btn btn-primary fa fa-cogs o_test_tour" t-att-data-name="tour.name" aria-label="Test tour" title="Test tour"/></td>
</tr>
</tbody>
</table>
</div>
</t>
</templates>
+1 -1
View File
@@ -185,7 +185,7 @@
'website.assets_editor': [
('include', 'web._assets_helpers'),
'web/static/lib/bootstrap/scss/_variables.scss',
'web/static/src/scss/ace.scss',
'web/static/src/legacy/scss/ace.scss',
'website/static/src/scss/website.editor.ui.scss',
'website/static/src/scss/website.theme_install.scss',
'website/static/src/js/menu/content.js',
@@ -1,22 +1,31 @@
odoo.define("website.tour.backend_dashboard", function (require) {
"use strict";
"use strict";
var tour = require("web_tour.tour");
var tour = require("web_tour.tour");
tour.register("backend_dashboard", {
test: true,
url: "/web",
}, [tour.stepUtils.showAppsMenuItem(),
tour.register(
"backend_dashboard",
{
trigger: 'a[data-menu-xmlid="website.menu_website_configuration"]',
}, {
trigger: '.dropdown-toggle[data-menu-xmlid="website.menu_dashboard"]',
}, {
trigger: '.dropdown-item[data-menu-xmlid="website.menu_website_google_analytics"]',
}, {
// Visits section should always be present even when empty / not hooked to anything
trigger: 'h2:contains("Visits")',
content: "Check if dashboard loads",
run: function () {}
}]);
test: true,
url: "/web",
},
[
tour.stepUtils.showAppsMenuItem(),
{
trigger: '.o_app[data-menu-xmlid="website.menu_website_configuration"]',
},
{
trigger: 'button[data-menu-xmlid="website.menu_dashboard"]',
},
{
trigger: 'li[data-menu-xmlid="website.menu_website_google_analytics"]',
},
{
// Visits section should always be present even when empty / not hooked to anything
trigger: 'h2:contains("Visits")',
content: "Check if dashboard loads",
run: function () {},
},
]
);
});
+1 -1
View File
@@ -7,7 +7,7 @@ const options = require('web_editor.snippets.options');
require('website.editor.snippets.options');
if (!$('.website_blog').length) {
return Promise.reject("DOM doesn't contain '.website_blog'");
return;
}
const NEW_TAG_PREFIX = 'new-blog-tag-';
@@ -6,9 +6,15 @@ var view_registry = require('web.view_registry');
var createView = testUtils.createView;
var BaseSettingsView = view_registry.get('base_settings');
var createActionManager = testUtils.createActionManager;
const { legacyExtraNextTick } = require("@web/../tests/helpers/utils");
const {
createWebClient,
doAction,
getActionManagerTestConfig,
} = require("@web/../tests/webclient/actions/helpers");
let testConfig;
QUnit.module('base_settings_tests', {
beforeEach: function () {
this.data = {
@@ -23,6 +29,9 @@ QUnit.module('base_settings_tests', {
fields: {}
}
};
testConfig = getActionManagerTestConfig();
const models = Object.assign(testConfig.serverData.models, this.data);
Object.assign(testConfig.serverData, { models });
}
}, function () {
@@ -91,160 +100,189 @@ QUnit.module('base_settings_tests', {
form.destroy();
});
QUnit.test('settings views does not read existing id when coming back in breadcrumbs', async function (assert) {
assert.expect(8);
QUnit.test(
"settings views does not read existing id when coming back in breadcrumbs",
async function (assert) {
assert.expect(8);
var actions = [{
id: 1,
name: 'Settings view',
res_model: 'res.config.settings',
type: 'ir.actions.act_window',
views: [[1, 'form']],
}, {
id: 4,
name: 'Other action',
res_model: 'task',
type: 'ir.actions.act_window',
views: [[2, 'list']],
}];
var archs = {
'res.config.settings,1,form': '<form string="Settings" js_class="base_settings">' +
'<div class="app_settings_block" string="CRM" data-key="crm">' +
'<button name="4" string="Execute action" type="action"/>' +
'</div>' +
'</form>',
'task,2,list': '<tree><field name="display_name"/></tree>',
'res.config.settings,false,search': '<search></search>',
'task,false,search': '<search></search>',
};
testConfig.serverData.actions = {
1: {
id: 1,
name: "Settings view",
res_model: "res.config.settings",
type: "ir.actions.act_window",
views: [[1, "form"]],
},
4: {
id: 4,
name: "Other action",
res_model: "task",
type: "ir.actions.act_window",
views: [[2, "list"]],
},
};
var actionManager = await createActionManager({
actions: actions,
archs: archs,
data: this.data,
mockRPC: function (route, args) {
testConfig.serverData.views = {
"res.config.settings,1,form":
`<form string="Settings" js_class="base_settings">
<div class="app_settings_block" string="CRM" data-key="crm">
<button name="4" string="Execute action" type="action"/>
</div>
</form>`,
"task,2,list": '<tree><field name="display_name"/></tree>',
"res.config.settings,false,search": "<search></search>",
"task,false,search": "<search></search>",
};
const mockRPC = (route, args) => {
if (args.method) {
assert.step(args.method);
}
return this._super.apply(this, arguments);
},
});
};
await actionManager.doAction(1);
await testUtils.nextTick();
await testUtils.dom.click(actionManager.$('button[name="4"]'));
await testUtils.dom.click($('.o_control_panel .breadcrumb-item a'));
assert.hasClass(actionManager.$('.o_form_view'), 'o_form_editable');
assert.verifySteps([
'load_views', // initial setting action
'onchange', // this is a setting view => create new record
'create', // when we click on action button => save
'read', // with save, we have a reload... (not necessary actually)
'load_views', // for other action in breadcrumb,
// with a searchread (not shown here since it is a route)
'onchange', // when we come back, we want to restart from scratch
]);
const webClient = await createWebClient({ testConfig, mockRPC });
actionManager.destroy();
});
await doAction(webClient, 1);
await testUtils.dom.click($(webClient.el).find('button[name="4"]'));
await legacyExtraNextTick();
await testUtils.dom.click($(".o_control_panel .breadcrumb-item a"));
await legacyExtraNextTick();
assert.hasClass($(webClient.el).find(".o_form_view"), "o_form_editable");
assert.verifySteps([
"load_views", // initial setting action
"onchange", // this is a setting view => create new record
"create", // when we click on action button => save
"read", // with save, we have a reload... (not necessary actually)
"load_views", // for other action in breadcrumb,
// with a searchread (not shown here since it is a route)
"onchange", // when we come back, we want to restart from scratch
]);
}
);
QUnit.test('clicking on any button in setting should show discard warning if setting form is dirty', async function (assert) {
assert.expect(11);
QUnit.test(
"clicking on any button in setting should show discard warning if setting form is dirty",
async function (assert) {
assert.expect(11);
var actions = [{
id: 1,
name: 'Settings view',
res_model: 'res.config.settings',
type: 'ir.actions.act_window',
views: [[1, 'form']],
}, {
id: 4,
name: 'Other action',
res_model: 'task',
type: 'ir.actions.act_window',
views: [[2, 'list']],
}];
var archs = {
'res.config.settings,1,form': '<form string="Settings" js_class="base_settings">' +
'<header>' +
'<button string="Save" type="object" name="execute" class="oe_highlight" />' +
'<button string="Cancel" type="object" name="cancel" class="oe_link" />' +
'</header>' +
'<div class="app_settings_block" string="CRM" data-key="crm">' +
'<div class="row mt16 o_settings_container">'+
'<div class="col-12 col-lg-6 o_setting_box">'+
'<div class="o_setting_left_pane">' +
'<field name="foo"/>'+
'</div>'+
'<div class="o_setting_right_pane">'+
'<span class="o_form_label">Foo</span>'+
'<div class="text-muted">'+
'this is foo'+
'</div>'+
'</div>' +
'</div>'+
'</div>'+
'<button name="4" string="Execute action" type="action"/>' +
'</div>' +
'</form>',
'task,2,list': '<tree><field name="display_name"/></tree>',
'res.config.settings,false,search': '<search></search>',
'task,false,search': '<search></search>',
};
testConfig.serverData.actions = {
1: {
id: 1,
name: "Settings view",
res_model: "res.config.settings",
type: "ir.actions.act_window",
views: [[1, "form"]],
},
4: {
id: 4,
name: "Other action",
res_model: "task",
type: "ir.actions.act_window",
views: [[2, "list"]],
},
};
var actionManager = await createActionManager({
actions: actions,
archs: archs,
data: this.data,
mockRPC: function (route, args) {
if (route === '/web/dataset/call_button') {
testConfig.serverData.views = {
"res.config.settings,1,form":
`<form string="Settings" js_class="base_settings">
<header>
<button string="Save" type="object" name="execute" class="oe_highlight" />
<button string="Cancel" type="object" name="cancel" class="oe_link" />
</header>
<div class="app_settings_block" string="CRM" data-key="crm">
<div class="row mt16 o_settings_container">
<div class="col-12 col-lg-6 o_setting_box">
<div class="o_setting_left_pane">
<field name="foo"/>
</div>
<div class="o_setting_right_pane">
<span class="o_form_label">Foo</span>
<div class="text-muted">
this is foo
</div>
</div>
</div>
</div>
<button name="4" string="Execute action" type="action"/>
</div>
</form>`,
"task,2,list": '<tree><field name="display_name"/></tree>',
"res.config.settings,false,search": "<search></search>",
"task,false,search": "<search></search>",
};
const mockRPC = (route, args) => {
if (route === "/web/dataset/call_button") {
if (args.method === "execute") {
assert.ok("execute method called");
return Promise.resolve(true);
return true;
}
if (args.method === "cancel") {
assert.ok("cancel method called");
return Promise.resolve(true);
return true;
}
}
return this._super.apply(this, arguments);
},
});
};
await actionManager.doAction(1);
assert.containsNone(actionManager, '.o_field_boolean input:checked',
"checkbox should not be checked");
const webClient = await createWebClient({ testConfig, mockRPC });
await testUtils.dom.click(actionManager.$("input[type='checkbox']"));
assert.containsOnce(actionManager, '.o_field_boolean input:checked',
"checkbox should be checked");
await doAction(webClient, 1);
assert.containsNone(
webClient,
".o_field_boolean input:checked",
"checkbox should not be checked"
);
await testUtils.dom.click(actionManager.$('button[name="4"]'));
assert.containsOnce(document.body, '.modal', "should open a warning dialog");
await testUtils.dom.click($(webClient.el).find("input[type='checkbox']"));
assert.containsOnce(
webClient,
".o_field_boolean input:checked",
"checkbox should be checked"
);
await testUtils.dom.click($('.modal button:contains(Discard)'));
assert.containsOnce(actionManager, '.o_list_view', "should be open list view");
await testUtils.dom.click($(webClient.el).find('button[name="4"]'));
await legacyExtraNextTick();
assert.containsOnce(document.body, ".modal", "should open a warning dialog");
await testUtils.dom.click($('.o_control_panel .breadcrumb-item a'));
assert.containsNone(actionManager, '.o_field_boolean input:checked',
"checkbox should not be checked");
await testUtils.dom.click($(".modal button:contains(Discard)"));
await legacyExtraNextTick();
assert.containsOnce(webClient, ".o_list_view", "should be open list view");
await testUtils.dom.click(actionManager.$("input[type='checkbox']"));
await testUtils.dom.click(actionManager.$('button[name="4"]'));
assert.containsOnce(document.body, '.modal', "should open a warning dialog");
await testUtils.dom.click($(".o_control_panel .breadcrumb-item a"));
await legacyExtraNextTick();
assert.containsNone(
webClient,
".o_field_boolean input:checked",
"checkbox should not be checked"
);
await testUtils.dom.click($('.modal button:contains(Stay Here)'));
assert.containsOnce(actionManager, '.o_form_view' ,"should be remain on form view");
await testUtils.dom.click($(webClient.el).find("input[type='checkbox']"));
await testUtils.dom.click($(webClient.el).find('button[name="4"]'));
await legacyExtraNextTick();
assert.containsOnce(document.body, ".modal", "should open a warning dialog");
await testUtils.dom.click(actionManager.$("button[name='execute']"));
assert.containsNone(document.body, '.modal', "should not open a warning dialog");
await testUtils.dom.click($(".modal button:contains(Stay Here)"));
await legacyExtraNextTick();
assert.containsOnce(webClient, ".o_form_view", "should be remain on form view");
await testUtils.dom.click(actionManager.$("input[type='checkbox']"));
await testUtils.dom.click(actionManager.$("button[name='cancel']"));
assert.containsNone(document.body, '.modal', "should not open a warning dialog");
await testUtils.dom.click($(webClient.el).find("button[name='execute']"));
await legacyExtraNextTick();
assert.containsNone(
document.body,
".modal",
"should not open a warning dialog"
);
actionManager.destroy();
});
await testUtils.dom.click($(webClient.el).find("input[type='checkbox']"));
await testUtils.dom.click($(webClient.el).find("button[name='cancel']"));
await legacyExtraNextTick();
assert.containsNone(
document.body,
".modal",
"should not open a warning dialog"
);
}
);
QUnit.test('settings view does not display other settings after reload', async function (assert) {
assert.expect(2);
@@ -365,72 +403,83 @@ QUnit.module('base_settings_tests', {
form.destroy();
});
QUnit.test('execute action from settings view with several actions in the breadcrumb', async function (assert) {
// This commit fixes a race condition, that's why we artificially slow down a read rpc
assert.expect(4);
QUnit.test(
"execute action from settings view with several actions in the breadcrumb",
async function (assert) {
// This commit fixes a race condition, that's why we artificially slow down a read rpc
assert.expect(4);
const actions = [{
id: 1,
name: 'First action',
res_model: 'task',
type: 'ir.actions.act_window',
views: [[1, 'list']],
}, {
id: 2,
name: 'Settings view',
res_model: 'res.config.settings',
type: 'ir.actions.act_window',
views: [[2, 'form']],
}, {
id: 3,
name: 'Other action',
res_model: 'task',
type: 'ir.actions.act_window',
views: [[3, 'list']],
}];
const archs = {
'task,1,list': '<tree><field name="display_name"/></tree>',
'res.config.settings,2,form': `
<form string="Settings" js_class="base_settings">
<div class="app_settings_block" string="CRM" data-key="crm">
<button name="3" string="Execute action" type="action"/>
</div>
</form>`,
'task,3,list': '<tree><field name="display_name"/></tree>',
'res.config.settings,false,search': '<search></search>',
'task,false,search': '<search></search>',
};
testConfig.serverData.actions = {
1: {
id: 1,
name: "First action",
res_model: "task",
type: "ir.actions.act_window",
views: [[1, "list"]],
},
2: {
id: 2,
name: "Settings view",
res_model: "res.config.settings",
type: "ir.actions.act_window",
views: [[2, "form"]],
},
3: {
id: 3,
name: "Other action",
res_model: "task",
type: "ir.actions.act_window",
views: [[3, "list"]],
},
};
let loadViewsDef;
const actionManager = await createActionManager({
actions: actions,
archs: archs,
data: this.data,
async mockRPC(route, args) {
const _super = this._super.bind(this);
if (args.method === 'read') {
await Promise.resolve(loadViewsDef); // slow down reload of settings view
testConfig.serverData.views = {
"task,1,list": '<tree><field name="display_name"/></tree>',
"res.config.settings,2,form": `
<form string="Settings" js_class="base_settings">
<div class="app_settings_block" string="CRM" data-key="crm">
<button name="3" string="Execute action" type="action"/>
</div>
</form>`,
"task,3,list": '<tree><field name="display_name"/></tree>',
"res.config.settings,false,search": "<search></search>",
"task,false,search": "<search></search>",
};
let loadViewsDef;
const mockRPC = async (route, args) => {
if (args.method === "read") {
await loadViewsDef; // slow down reload of settings view
}
return _super(route, args);
},
});
};
await actionManager.doAction(1);
assert.strictEqual(actionManager.$('.breadcrumb').text(), 'First action');
const webClient = await createWebClient({ testConfig, mockRPC });
await doAction(webClient, 1);
assert.strictEqual($(webClient.el).find(".breadcrumb").text(), "First action");
await actionManager.doAction(2);
assert.strictEqual(actionManager.$('.breadcrumb').text(), 'First actionNew');
await doAction(webClient, 2);
assert.strictEqual(
$(webClient.el).find(".breadcrumb").text(),
"First actionNew"
);
loadViewsDef = testUtils.makeTestPromise();
await testUtils.dom.click(actionManager.$('button[name="3"]'));
assert.strictEqual(actionManager.$('.breadcrumb').text(), 'First actionNew');
loadViewsDef = testUtils.makeTestPromise();
await testUtils.dom.click($(webClient.el).find('button[name="3"]'));
await legacyExtraNextTick();
assert.strictEqual(
$(webClient.el).find(".breadcrumb").text(),
"First actionNew"
);
loadViewsDef.resolve();
await testUtils.nextTick();
assert.strictEqual(actionManager.$('.breadcrumb').text(), 'First actionNewOther action');
actionManager.destroy();
});
loadViewsDef.resolve();
await testUtils.nextTick();
await legacyExtraNextTick();
assert.strictEqual(
$(webClient.el).find(".breadcrumb").text(),
"First actionNewOther action"
);
}
);
QUnit.test('settings can contain one2many fields', async function (assert) {
assert.expect(2);
@@ -477,75 +526,79 @@ QUnit.module('base_settings_tests', {
form.destroy();
});
QUnit.test('call "call_button/execute" when clicking on a button in dirty settings', async function (assert) {
assert.expect(7);
QUnit.test(
'call "call_button/execute" when clicking on a button in dirty settings',
async function (assert) {
assert.expect(7);
const actions = [{
id: 1,
name: 'Settings view',
res_model: 'res.config.settings',
type: 'ir.actions.act_window',
views: [[1, 'form']],
}];
const archs = {
'res.config.settings,1,form': `
<form string="Settings" js_class="base_settings">
<div class="app_settings_block" string="CRM" data-key="crm">
<div class="row mt16 o_settings_container">
<div class="col-12 col-lg-6 o_setting_box">
<div class="o_setting_left_pane">
<field name="foo"/>
</div>
<div class="o_setting_right_pane">
<span class="o_form_label">Foo</span>
<div class="text-muted">
this is foo
testConfig.serverData.actions[1] = {
id: 1,
name: "Settings view",
res_model: "res.config.settings",
type: "ir.actions.act_window",
views: [[1, "form"]],
};
testConfig.serverData.views = Object.assign(testConfig.serverData.views, {
"res.config.settings,1,form": `
<form string="Settings" js_class="base_settings">
<div class="app_settings_block" string="CRM" data-key="crm">
<div class="row mt16 o_settings_container">
<div class="col-12 col-lg-6 o_setting_box">
<div class="o_setting_left_pane">
<field name="foo"/>
</div>
<div class="o_setting_right_pane">
<span class="o_form_label">Foo</span>
<div class="text-muted">
this is foo
</div>
</div>
</div>
</div>
<button name="4" string="Execute action" type="action"/>
</div>
<button name="4" string="Execute action" type="action"/>
</div>
</form>
</form>
`,
'res.config.settings,false,search': '<search></search>',
};
"res.config.settings,false,search": "<search></search>",
});
const actionManager = await createActionManager({
actions: actions,
archs: archs,
data: this.data,
mockRPC(route, args) {
if (route === '/web/dataset/call_button' && args.method === 'execute') {
assert.step('execute');
return Promise.resolve(true);
} else if (args.method === 'create') {
assert.step('create');
const mockRPC = (route, args) => {
if (route === "/web/dataset/call_button" && args.method === "execute") {
assert.step("execute");
return true;
} else if (args.method === "create") {
assert.step("create");
}
return this._super.apply(this, arguments);
},
});
};
await actionManager.doAction(1);
assert.containsNone(actionManager, '.o_field_boolean input:checked',
'checkbox should not be checked');
const webClient = await createWebClient({ testConfig, mockRPC });
await testUtils.dom.click(actionManager.$('input[type="checkbox"]'));
assert.containsOnce(actionManager, '.o_field_boolean input:checked',
'checkbox should be checked');
await doAction(webClient, 1);
assert.containsNone(
webClient,
".o_field_boolean input:checked",
"checkbox should not be checked"
);
await testUtils.dom.click(actionManager.$('button[name="4"]'));
assert.containsOnce(document.body, '.modal', 'should open a warning dialog');
await testUtils.dom.click($(webClient.el).find('input[type="checkbox"]'));
assert.containsOnce(
webClient,
".o_field_boolean input:checked",
"checkbox should be checked"
);
await testUtils.dom.click($('.modal-footer .btn-primary'));
assert.verifySteps([
'create', // saveRecord from modal
'execute', // execute_action
'create' // saveRecord from FormController._onButtonClicked
]);
await testUtils.dom.click($(webClient.el).find('button[name="4"]'));
assert.containsOnce(document.body, ".modal", "should open a warning dialog");
actionManager.destroy();
});
await testUtils.dom.click($(".modal-footer .btn-primary"));
assert.verifySteps([
"create", // saveRecord from modal
"execute", // execute_action
"create", // saveRecord from FormController._onButtonClicked
]);
}
);
});
});
@@ -27,6 +27,7 @@ GLOBAL = ','.join([
'Popover',
'StackTrace',
'QUnit',
'luxon',
'moment',
'py',
'ClipboardJS',
@@ -31,6 +31,8 @@ class TestConflictMarkers(lint_case.LintCase):
for p in paths:
for dp, _, file_names in os.walk(p):
if 'node_modules' in dp:
continue
for fn in file_names:
if fn.endswith(EXTENSIONS):
self.check_file(os.path.join(dp, fn))
@@ -12,12 +12,12 @@ tour.register('main_flow_tour', {
tour.stepUtils.openBuggerMenu("li.breadcrumb-item.active:contains('Quotations')"),
{
// Add Stockable product
trigger: ".o_menu_sections a:contains('Products')",
trigger: ".o_menu_sections .o_dropdown_toggler span:contains('Products')",
extra_trigger: '.o_main_navbar',
content: _t("Let's create products."),
position: "bottom",
}, {
trigger: ".o_menu_sections a:has(span:contains('Products'))",
trigger: ".o_menu_sections .o_dropdown_item span:contains('Products')",
content: _t("Let's create products."),
position: "bottom"
}, {
@@ -654,29 +654,29 @@ tour.stepUtils.autoExpandMoreButtons('.o_form_readonly'),
tour.stepUtils.openBuggerMenu("li.breadcrumb-item.active:contains('Inventory Overview')"),
{
mobile: false,
trigger: ".o_menu_sections a[data-menu-xmlid='stock.menu_stock_config_settings']",
trigger: ".o_menu_sections button[data-menu-xmlid='stock.menu_stock_config_settings']",
extra_trigger: '.o_main_navbar',
content: _t("Go to Configuration"),
position: "bottom"
}, {
mobile: false,
trigger: ".o_menu_sections a[data-menu-xmlid='stock.menu_reordering_rules_config']",
trigger: ".o_menu_sections li[data-menu-xmlid='stock.menu_reordering_rules_config']",
content: _t("Reordering Rules"),
position: "bottom"
}, {
mobile: true,
trigger: ".o_menu_sections a:contains('Configuration')",
trigger: ".o_menu_sections button:contains('Configuration')",
extra_trigger: '.o_main_navbar',
content: _t("Go to Configuration"),
position: "bottom"
}, {
mobile: true,
trigger: ".o_menu_sections a[data-menu-xmlid='stock.menu_product_in_config_stock']",
trigger: ".o_menu_sections button[data-menu-xmlid='stock.menu_product_in_config_stock']",
content: _t("Click on Product Section"),
position: "bottom"
}, {
mobile: true,
trigger: ".o_menu_sections a:contains('Reordering Rules')",
trigger: ".o_menu_sections li:contains('Reordering Rules')",
content: _t("Click on Product Section"),
position: "bottom"
}, {
@@ -730,11 +730,11 @@ tour.stepUtils.openBuggerMenu("li.breadcrumb-item.active:contains('Inventory Ove
tour.stepUtils.openBuggerMenu("li.breadcrumb-item.active:contains('OP/')"),
{
// Run the schedulers
trigger: ".o_menu_sections a:contains('Operations')",
trigger: ".o_menu_sections button:contains('Operations')",
content: _t("Go to Run Schedulers"),
position: "bottom"
}, {
trigger: ".o_menu_sections a[data-menu-xmlid='stock.menu_procurement_compute']",
trigger: ".o_menu_sections li[data-menu-xmlid='stock.menu_procurement_compute']",
content: _t("Click on schedulers"),
position: "bottom"
}, {
@@ -778,7 +778,7 @@ tour.stepUtils.openBuggerMenu("li.breadcrumb-item.active:contains('OP/')"),
{
trigger: '.o_form_button_edit',
content: _t('Edit the vendor bill'),
extra_trigger: 'body.o_web_client:not(.oe_wait)',
extra_trigger: ".o_form_label .o_field_widget:contains('Vendor Bill')",
}, {
trigger:".o_field_widget[name=invoice_date] input",
content: _t('Set the invoice date'),
@@ -800,12 +800,12 @@ tour.stepUtils.openBuggerMenu("li.breadcrumb-item.active:contains('OP/')"),
tour.stepUtils.openBuggerMenu("li.breadcrumb-item.active:contains('Manufacturing Orders'), li.breadcrumb-item.active:contains('Work Centers Overview')"),
{
mobile: false,
trigger: ".o_menu_sections a[data-menu-xmlid='mrp.menu_mrp_manufacturing']",
trigger: ".o_menu_sections button[data-menu-xmlid='mrp.menu_mrp_manufacturing']",
content: _t('Click on Operations menuitem'),
position: 'bottom',
}, {
mobile: false,
trigger: ".o_menu_sections a[data-menu-xmlid='mrp.menu_mrp_production_action']",
trigger: ".o_menu_sections li[data-menu-xmlid='mrp.menu_mrp_production_action']",
content: _t('Open manufacturing orders'),
position: 'bottom',
}, {
@@ -815,13 +815,13 @@ tour.stepUtils.openBuggerMenu("li.breadcrumb-item.active:contains('Manufacturing
position: 'bottom',
}, {
mobile: true,
trigger: ".o_menu_sections a:contains('Operations')",
trigger: ".o_menu_sections button:contains('Operations')",
extra_trigger: '.o_main_navbar',
content: _t("Let's go to manufacturing order."),
position: "bottom",
}, {
mobile: true,
trigger: ".o_menu_sections a:has(span:contains('Manufacturing Orders'))",
trigger: ".o_menu_sections li:contains('Manufacturing Orders')",
content: _t("Let's go to manufacturing order."),
position: "bottom"
}, {
@@ -833,9 +833,8 @@ tour.stepUtils.openBuggerMenu("li.breadcrumb-item.active:contains('Manufacturing
},
...tour.stepUtils.statusbarButtonsSteps('Check availability', _t("Check availability")),
{
trigger: '.o_form_button_edit',
trigger: ".o_form_button_edit:not(:disabled)",
content: _t('Edit the production order'),
extra_trigger: 'body.o_web_client:not(.oe_wait)',
}, {
trigger: "input[name=qty_producing]",
position: 'left',
@@ -852,11 +851,11 @@ tour.stepUtils.openBuggerMenu("li.breadcrumb-item.active:contains('Manufacturing
...tour.stepUtils.goToAppSteps('sale.sale_menu_root', _t('Organize your sales activities with the <b>Sales app</b>.')),
tour.stepUtils.openBuggerMenu("li.breadcrumb-item.active:contains('Quotations')"),
{
trigger: ".o_menu_sections a[data-menu-xmlid='sale.sale_order_menu']",
trigger: ".o_menu_sections button[data-menu-xmlid='sale.sale_order_menu']",
content: _t("Go to Sales menu"),
position: "bottom"
}, {
trigger: ".o_menu_sections a[data-menu-xmlid='sale.menu_sale_order']",
trigger: ".o_menu_sections li[data-menu-xmlid='sale.menu_sale_order']",
content: _t("Go to the sales orders"),
position: "bottom"
}, {
@@ -336,11 +336,11 @@ odoo.define('web.test.x2many', function (require) {
{
content: "switch to the second form view to test one2many with editable list (toggle menu dropdown)",
trigger: 'nav .o_menu_sections .dropdown-toggle:containsExact(Discussions), li.o_extra_menu_items a i.fa-plus',
trigger: 'button[data-menu-xmlid="test_new_api.menu_main"], li.o_extra_menu_items a i.fa-plus',
extra_trigger: '.tab-pane:eq(0) .o_field_widget tbody .o_data_row:eq(2)',
}, {
content: "switch to the second form view to test one2many with editable list (open submenu)",
trigger: 'nav .o_menu_sections .dropdown-item:contains(Discussions 2)',
trigger: 'li[data-menu-xmlid="test_new_api.menu_discussions_2"]',
}, {
content: "select previous created record",
trigger: 'td:contains(test_trigger):last',
@@ -453,11 +453,11 @@ odoo.define('web.test.x2many', function (require) {
{
content: "switch to the third form view to test onchange changing one2many (toggle menu dropdown)",
trigger: 'nav .o_menu_sections .dropdown-toggle:containsExact(Discussions), li.o_extra_menu_items a i.fa-plus',
trigger: 'button[data-menu-xmlid="test_new_api.menu_main"], li.o_extra_menu_items a i.fa-plus',
extra_trigger: '.tab-pane:eq(0) .o_field_widget tbody .o_data_row:eq(2)',
}, {
content: "switch to the thied form view to test onchange changing one2many (open submenu)",
trigger: 'nav .o_menu_sections .dropdown-item:contains(Discussions 3)',
trigger: 'li[data-menu-xmlid="test_new_api.menu_discussions_3"]',
}, {
content: "wait web client",
trigger: '.breadcrumb:contains(Discussions 3)',