合并企业版代码(未测试,先提交到测试分支)
This commit is contained in:
BIN
web_mobile/static/src/img/app_store.png
Normal file
BIN
web_mobile/static/src/img/app_store.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
BIN
web_mobile/static/src/img/google_play.png
Normal file
BIN
web_mobile/static/src/img/google_play.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
48
web_mobile/static/src/js/core/control_panel.js
Normal file
48
web_mobile/static/src/js/core/control_panel.js
Normal file
@@ -0,0 +1,48 @@
|
||||
/** @odoo-module */
|
||||
|
||||
import { device } from "web.config";
|
||||
import * as LegacyControlPanel from "web.ControlPanel";
|
||||
import { useBackButton } from "web_mobile.hooks";
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { ControlPanel } from "@web/search/control_panel/control_panel";
|
||||
|
||||
|
||||
if (device.isMobile) {
|
||||
patch(LegacyControlPanel.prototype, "web_mobile", {
|
||||
setup() {
|
||||
this._super(...arguments);
|
||||
useBackButton(this._onBackButton.bind(this), () => this.state.showMobileSearch);
|
||||
},
|
||||
|
||||
//---------------------------------------------------------------------
|
||||
// Handlers
|
||||
//---------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* close mobile search on back-button
|
||||
* @private
|
||||
*/
|
||||
_onBackButton() {
|
||||
this._resetSearchState();
|
||||
},
|
||||
});
|
||||
|
||||
patch(ControlPanel.prototype, "web_mobile", {
|
||||
setup() {
|
||||
this._super(...arguments);
|
||||
useBackButton(this._onBackButton.bind(this), () => this.state.showMobileSearch);
|
||||
},
|
||||
|
||||
//---------------------------------------------------------------------
|
||||
// Handlers
|
||||
//---------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* close mobile search on back-button
|
||||
* @private
|
||||
*/
|
||||
_onBackButton() {
|
||||
this.resetSearchState();
|
||||
},
|
||||
});
|
||||
}
|
||||
79
web_mobile/static/src/js/core/dialog.js
Normal file
79
web_mobile/static/src/js/core/dialog.js
Normal file
@@ -0,0 +1,79 @@
|
||||
odoo.define("web_mobile.Dialog", function (require) {
|
||||
"use strict";
|
||||
|
||||
var Dialog = require("web.Dialog");
|
||||
var mobileMixins = require("web_mobile.mixins");
|
||||
|
||||
Dialog.include(
|
||||
_.extend({}, mobileMixins.BackButtonEventMixin, {
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Close the current dialog on 'backbutton' event.
|
||||
*
|
||||
* @private
|
||||
* @override
|
||||
* @param {Event} ev
|
||||
*/
|
||||
_onBackButton: function () {
|
||||
this.close();
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
odoo.define("web_mobile.OwlDialog", function (require) {
|
||||
"use strict";
|
||||
|
||||
const OwlDialog = require("web.OwlDialog");
|
||||
const { useBackButton } = require("web_mobile.hooks");
|
||||
const { patch } = require("web.utils");
|
||||
|
||||
patch(OwlDialog.prototype, "web_mobile", {
|
||||
setup() {
|
||||
this._super(...arguments);
|
||||
useBackButton(this._onBackButton.bind(this));
|
||||
},
|
||||
|
||||
//---------------------------------------------------------------------
|
||||
// Handlers
|
||||
//---------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Close dialog on back-button
|
||||
* @private
|
||||
*/
|
||||
_onBackButton() {
|
||||
this._close();
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
odoo.define("web_mobile.Popover", function (require) {
|
||||
"use strict";
|
||||
|
||||
const Popover = require("web.Popover");
|
||||
const { useBackButton } = require("web_mobile.hooks");
|
||||
const { patch } = require("web.utils");
|
||||
|
||||
patch(Popover.prototype, "web_mobile", {
|
||||
setup() {
|
||||
this._super(...arguments);
|
||||
useBackButton(this._onBackButton.bind(this), () => this.state.displayed);
|
||||
},
|
||||
|
||||
//---------------------------------------------------------------------
|
||||
// Handlers
|
||||
//---------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Close popover on back-button
|
||||
* @private
|
||||
*/
|
||||
_onBackButton() {
|
||||
this._close();
|
||||
},
|
||||
});
|
||||
});
|
||||
145
web_mobile/static/src/js/core/mixins.js
Normal file
145
web_mobile/static/src/js/core/mixins.js
Normal file
@@ -0,0 +1,145 @@
|
||||
odoo.define('web_mobile.mixins', function (require) {
|
||||
"use strict";
|
||||
|
||||
const session = require('web.session');
|
||||
const mobile = require('web_mobile.core');
|
||||
|
||||
/**
|
||||
* Mixin to setup lifecycle methods and allow to use 'backbutton' events sent
|
||||
* from the native application.
|
||||
*
|
||||
* @mixin
|
||||
* @name BackButtonEventMixin
|
||||
*
|
||||
*/
|
||||
var BackButtonEventMixin = {
|
||||
/**
|
||||
* Register event listener for 'backbutton' event when attached to the DOM
|
||||
*/
|
||||
on_attach_callback: function () {
|
||||
mobile.backButtonManager.addListener(this, this._onBackButton);
|
||||
},
|
||||
/**
|
||||
* Unregister event listener for 'backbutton' event when detached from the DOM
|
||||
*/
|
||||
on_detach_callback: function () {
|
||||
mobile.backButtonManager.removeListener(this, this._onBackButton);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {Event} ev 'backbutton' type event
|
||||
*/
|
||||
_onBackButton: function () {},
|
||||
};
|
||||
|
||||
/**
|
||||
* Mixin to hook into the controller record's saving method and
|
||||
* trigger the update of the user's account details on the mobile app.
|
||||
*
|
||||
* @mixin
|
||||
* @name UpdateDeviceAccountControllerMixin
|
||||
*
|
||||
*/
|
||||
const UpdateDeviceAccountControllerMixin = {
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
async save() {
|
||||
const changedFields = await this._super(...arguments);
|
||||
await session.updateAccountOnMobileDevice();
|
||||
return changedFields;
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Trigger the update of the user's account details on the mobile app as soon as
|
||||
* the session is correctly initialized.
|
||||
*/
|
||||
session.is_bound.then(() => session.updateAccountOnMobileDevice());
|
||||
|
||||
return {
|
||||
BackButtonEventMixin: BackButtonEventMixin,
|
||||
UpdateDeviceAccountControllerMixin,
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
odoo.define('web_mobile.hooks', function (require) {
|
||||
"use strict";
|
||||
|
||||
const { backButtonManager } = require('web_mobile.core');
|
||||
|
||||
const { onMounted, onPatched, onWillUnmount, useComponent } = owl;
|
||||
|
||||
/**
|
||||
* This hook provides support for executing code when the back button is pressed
|
||||
* on the mobile application of Odoo. This actually replaces the default back
|
||||
* button behavior so this feature should only be enabled when it is actually
|
||||
* useful.
|
||||
*
|
||||
* The feature is either enabled on mount or, using the `shouldEnable` function
|
||||
* argument as condition, when the component is patched. In both cases,
|
||||
* the feature is automatically disabled on unmount.
|
||||
*
|
||||
* @param {function} func the function to execute when the back button is
|
||||
* pressed. The function is called with the custom event as param.
|
||||
* @param {function} [shouldEnable] the function to execute when the DOM is
|
||||
* patched to check if the backbutton should be enabled or disabled ;
|
||||
* if undefined will be enabled on mount and disabled on unmount.
|
||||
*/
|
||||
function useBackButton(func, shouldEnable) {
|
||||
const component = useComponent();
|
||||
let isEnabled = false;
|
||||
|
||||
/**
|
||||
* Enables the func listener, overriding default back button behavior.
|
||||
*/
|
||||
function enable() {
|
||||
backButtonManager.addListener(component, func);
|
||||
isEnabled = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables the func listener, restoring the default back button behavior if
|
||||
* no other listeners are present.
|
||||
*/
|
||||
function disable() {
|
||||
backButtonManager.removeListener(component);
|
||||
isEnabled = false;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (shouldEnable && !shouldEnable()) {
|
||||
return;
|
||||
}
|
||||
enable();
|
||||
});
|
||||
|
||||
onPatched(() => {
|
||||
if (!shouldEnable) {
|
||||
return;
|
||||
}
|
||||
const shouldBeEnabled = shouldEnable();
|
||||
if (shouldBeEnabled && !isEnabled) {
|
||||
enable();
|
||||
} else if (!shouldBeEnabled && isEnabled) {
|
||||
disable();
|
||||
}
|
||||
});
|
||||
|
||||
onWillUnmount(() => {
|
||||
if (isEnabled) {
|
||||
disable();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
useBackButton,
|
||||
};
|
||||
});
|
||||
18
web_mobile/static/src/js/core/network/download.js
Normal file
18
web_mobile/static/src/js/core/network/download.js
Normal file
@@ -0,0 +1,18 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import mobile from "web_mobile.core";
|
||||
import { download } from "@web/core/network/download";
|
||||
|
||||
const _download = download._download;
|
||||
|
||||
download._download = async function (options) {
|
||||
if (mobile.methods.downloadFile) {
|
||||
if (odoo.csrf_token) {
|
||||
options.csrf_token = odoo.csrf_token;
|
||||
}
|
||||
mobile.methods.downloadFile(options);
|
||||
return Promise.resolve();
|
||||
} else {
|
||||
return _download.apply(this, arguments);
|
||||
}
|
||||
};
|
||||
89
web_mobile/static/src/js/core/session.js
Normal file
89
web_mobile/static/src/js/core/session.js
Normal file
@@ -0,0 +1,89 @@
|
||||
odoo.define('web_mobile.Session', function (require) {
|
||||
"use strict";
|
||||
|
||||
const core = require('web.core');
|
||||
const Session = require('web.Session');
|
||||
|
||||
const mobile = require('web_mobile.core');
|
||||
|
||||
const DEFAULT_AVATAR_SIZE = 128;
|
||||
|
||||
/*
|
||||
Android webview not supporting post download and odoo is using post method to download
|
||||
so here override get_file of session and passed all data to native mobile downloader
|
||||
ISSUE: https://code.google.com/p/android/issues/detail?id=1780
|
||||
*/
|
||||
|
||||
Session.include({
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
get_file: function (options) {
|
||||
if (mobile.methods.downloadFile) {
|
||||
if (core.csrf_token) {
|
||||
options.csrf_token = core.csrf_token;
|
||||
}
|
||||
mobile.methods.downloadFile(options);
|
||||
// There is no need to wait downloadFile because we delegate this to
|
||||
// Download Manager Service where error handling will be handled correclty.
|
||||
// On our side, we do not want to block the UI and consider the request
|
||||
// as success.
|
||||
if (options.success) { options.success(); }
|
||||
if (options.complete) { options.complete(); }
|
||||
return true;
|
||||
} else {
|
||||
return this._super.apply(this, arguments);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Update the user's account details on the mobile app
|
||||
*
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async updateAccountOnMobileDevice() {
|
||||
if (!mobile.methods.updateAccount) {
|
||||
return;
|
||||
}
|
||||
const base64Avatar = await this.fetchAvatar();
|
||||
return mobile.methods.updateAccount({
|
||||
avatar: base64Avatar.substring(base64Avatar.indexOf(',') + 1),
|
||||
name: this.name,
|
||||
username: this.username,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetch current user's avatar as PNG image
|
||||
*
|
||||
* @returns {Promise} resolved with the dataURL, or rejected if the file is
|
||||
* empty or if an error occurs.
|
||||
*/
|
||||
fetchAvatar() {
|
||||
const url = this.url('/web/image', {
|
||||
model: 'res.users',
|
||||
field: 'image_medium',
|
||||
id: this.uid,
|
||||
});
|
||||
return new Promise((resolve, reject) => {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = DEFAULT_AVATAR_SIZE;
|
||||
canvas.height = DEFAULT_AVATAR_SIZE;
|
||||
const context = canvas.getContext('2d');
|
||||
const image = new Image();
|
||||
image.addEventListener('load', () => {
|
||||
context.drawImage(image, 0, 0, DEFAULT_AVATAR_SIZE, DEFAULT_AVATAR_SIZE);
|
||||
resolve(canvas.toDataURL('image/png'));
|
||||
});
|
||||
image.addEventListener('error', reject);
|
||||
image.src = url;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
});
|
||||
14
web_mobile/static/src/js/crash_manager.js
Normal file
14
web_mobile/static/src/js/crash_manager.js
Normal file
@@ -0,0 +1,14 @@
|
||||
/** @odoo-module */
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import mobile from "web_mobile.core";
|
||||
|
||||
function mobileErrorHandler(env, error, originalError) {
|
||||
if (mobile.methods.crashManager) {
|
||||
error.originalError = originalError;
|
||||
mobile.methods.crashManager(error);
|
||||
}
|
||||
}
|
||||
registry
|
||||
.category("error_handlers")
|
||||
.add("web_mobile.errorHandler", mobileErrorHandler, { sequence: 3 });
|
||||
26
web_mobile/static/src/js/mobile_service.js
Normal file
26
web_mobile/static/src/js/mobile_service.js
Normal file
@@ -0,0 +1,26 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
import mobile from "web_mobile.core";
|
||||
import { shortcutItem, switchAccountItem } from "./user_menu_items";
|
||||
|
||||
const serviceRegistry = registry.category("services");
|
||||
const userMenuRegistry = registry.category("user_menuitems");
|
||||
|
||||
const mobileService = {
|
||||
start() {
|
||||
if (mobile.methods.addHomeShortcut) {
|
||||
userMenuRegistry.add("web_mobile.shortcut", shortcutItem);
|
||||
}
|
||||
|
||||
if (mobile.methods.switchAccount) {
|
||||
// remove "Log Out" and "My Odoo.com Account"
|
||||
userMenuRegistry.remove('log_out');
|
||||
userMenuRegistry.remove('odoo_account');
|
||||
|
||||
userMenuRegistry.add("web_mobile.switch", switchAccountItem);
|
||||
}
|
||||
},
|
||||
};
|
||||
serviceRegistry.add("mobile", mobileService);
|
||||
158
web_mobile/static/src/js/services/core.js
Normal file
158
web_mobile/static/src/js/services/core.js
Normal file
@@ -0,0 +1,158 @@
|
||||
/* global OdooDeviceUtility */
|
||||
odoo.define('web_mobile.core', function () {
|
||||
"use strict";
|
||||
|
||||
var available = typeof OdooDeviceUtility !== 'undefined';
|
||||
var DeviceUtility;
|
||||
var deferreds = {};
|
||||
var methods = {};
|
||||
|
||||
if (available){
|
||||
DeviceUtility = OdooDeviceUtility;
|
||||
delete window.OdooDeviceUtility;
|
||||
}
|
||||
|
||||
/**
|
||||
* Responsible for invoking native methods which called from JavaScript
|
||||
*
|
||||
* @param {String} name name of action want to perform in mobile
|
||||
* @param {Object} args extra arguments for mobile
|
||||
*
|
||||
* @returns Promise Object
|
||||
*/
|
||||
function native_invoke(name, args) {
|
||||
if(_.isUndefined(args)){
|
||||
args = {};
|
||||
}
|
||||
var id = _.uniqueId();
|
||||
args = JSON.stringify(args);
|
||||
DeviceUtility.execute(name, args, id);
|
||||
return new Promise(function (resolve, reject) {
|
||||
deferreds[id] = {
|
||||
successCallback: resolve,
|
||||
errorCallback: reject
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Manages deferred callback from initiate from native mobile
|
||||
*
|
||||
* @param {String} id callback id
|
||||
* @param {Object} result
|
||||
*/
|
||||
window.odoo.native_notify = function (id, result) {
|
||||
if (deferreds.hasOwnProperty(id)) {
|
||||
if (result.success) {
|
||||
deferreds[id].successCallback(result);
|
||||
} else {
|
||||
deferreds[id].errorCallback(result);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var plugins = available ? JSON.parse(DeviceUtility.list_plugins()) : [];
|
||||
_.each(plugins, function (plugin) {
|
||||
methods[plugin.name] = function (args) {
|
||||
return native_invoke(plugin.action, args);
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Use to notify an uri hash change on native devices (ios / android)
|
||||
*/
|
||||
if (methods.hashChange) {
|
||||
var currentHash;
|
||||
$(window).bind('hashchange', function (event) {
|
||||
var hash = event.getState();
|
||||
if (!_.isEqual(currentHash, hash)) {
|
||||
methods.hashChange(hash);
|
||||
}
|
||||
currentHash = hash;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Error related to the registration of a listener to the backbutton event
|
||||
*/
|
||||
class BackButtonListenerError extends Error {}
|
||||
|
||||
/**
|
||||
* By using the back button feature the default back button behavior from the
|
||||
* app is actually overridden so it is important to keep count to restore the
|
||||
* default when no custom listener are remaining.
|
||||
*/
|
||||
class BackButtonManager {
|
||||
|
||||
constructor() {
|
||||
this._listeners = new Map();
|
||||
this._onGlobalBackButton = this._onGlobalBackButton.bind(this);
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Enables the func listener, overriding default back button behavior.
|
||||
*
|
||||
* @param {Widget|Component} listener
|
||||
* @param {function} func
|
||||
* @throws {BackButtonListenerError} if the listener has already been registered
|
||||
*/
|
||||
addListener(listener, func) {
|
||||
if (!methods.overrideBackButton) {
|
||||
return;
|
||||
}
|
||||
if (this._listeners.has(listener)) {
|
||||
throw new BackButtonListenerError("This listener was already registered.");
|
||||
}
|
||||
this._listeners.set(listener, func);
|
||||
if (this._listeners.size === 1) {
|
||||
document.addEventListener('backbutton', this._onGlobalBackButton);
|
||||
methods.overrideBackButton({ enabled: true });
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Disables the func listener, restoring the default back button behavior if
|
||||
* no other listeners are present.
|
||||
*
|
||||
* @param {Widget|Component} listener
|
||||
* @throws {BackButtonListenerError} if the listener has already been unregistered
|
||||
*/
|
||||
removeListener(listener) {
|
||||
if (!methods.overrideBackButton) {
|
||||
return;
|
||||
}
|
||||
if (!this._listeners.has(listener)) {
|
||||
throw new BackButtonListenerError("This listener has already been unregistered.");
|
||||
}
|
||||
this._listeners.delete(listener);
|
||||
if (this._listeners.size === 0) {
|
||||
document.removeEventListener('backbutton', this._onGlobalBackButton);
|
||||
methods.overrideBackButton({ enabled: false });
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
_onGlobalBackButton() {
|
||||
const [listener, func] = [...this._listeners].pop();
|
||||
if (listener) {
|
||||
func.apply(listener, arguments);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const backButtonManager = new BackButtonManager();
|
||||
|
||||
return {
|
||||
BackButtonManager,
|
||||
BackButtonListenerError,
|
||||
backButtonManager,
|
||||
methods,
|
||||
};
|
||||
|
||||
});
|
||||
38
web_mobile/static/src/js/user_menu_items.js
Normal file
38
web_mobile/static/src/js/user_menu_items.js
Normal file
@@ -0,0 +1,38 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import mobile from "web_mobile.core";
|
||||
|
||||
export function shortcutItem(env) {
|
||||
return {
|
||||
type: "item",
|
||||
id: "web_mobile.shortcut",
|
||||
description: env._t("Add to Home Screen"),
|
||||
callback: () => {
|
||||
const { hash } = env.services.router.current;
|
||||
if (hash.menu_id) {
|
||||
const menu = env.services.menu.getMenu(hash.menu_id);
|
||||
const base64Icon = menu && menu.webIconData;
|
||||
mobile.methods.addHomeShortcut({
|
||||
title: document.title,
|
||||
shortcut_url: document.URL,
|
||||
web_icon: base64Icon.substring(base64Icon.indexOf(',') + 1),
|
||||
});
|
||||
} else {
|
||||
env.services.notification.add(env._t("No shortcut for Home Menu"));
|
||||
}
|
||||
},
|
||||
sequence: 100,
|
||||
};
|
||||
}
|
||||
|
||||
export function switchAccountItem(env) {
|
||||
return {
|
||||
type: "item",
|
||||
id: "web_mobile.switch",
|
||||
description: env._t("Switch/Add Account"),
|
||||
callback: () => {
|
||||
mobile.methods.switchAccount();
|
||||
},
|
||||
sequence: 100,
|
||||
};
|
||||
}
|
||||
23
web_mobile/static/src/views/user_preferences_form_view.js
Normal file
23
web_mobile/static/src/views/user_preferences_form_view.js
Normal file
@@ -0,0 +1,23 @@
|
||||
/** @odoo-module */
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
|
||||
import { formView } from "@web/views/form/form_view";
|
||||
import { UpdateDeviceAccountControllerMixin } from "web_mobile.mixins";
|
||||
import { Record, RelationalModel } from "@web/views/basic_relational_model";
|
||||
|
||||
export class ResUsersPreferenceRecord extends Record {}
|
||||
export class ResUsersPreferenceModel extends RelationalModel {}
|
||||
ResUsersPreferenceModel.Record = ResUsersPreferenceRecord;
|
||||
|
||||
patch(
|
||||
ResUsersPreferenceRecord.prototype,
|
||||
"res_users_controller_mobile_mixin",
|
||||
UpdateDeviceAccountControllerMixin
|
||||
);
|
||||
|
||||
registry.category("views").add("res_users_preferences_form", {
|
||||
...formView,
|
||||
Model: ResUsersPreferenceModel,
|
||||
});
|
||||
31
web_mobile/static/tests/helpers/test_utils.js
Normal file
31
web_mobile/static/tests/helpers/test_utils.js
Normal file
@@ -0,0 +1,31 @@
|
||||
odoo.define('web_mobile.testUtils', function () {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Transforms base64 encoded data to a Blob object
|
||||
*
|
||||
* @param {string} b64Data
|
||||
* @param {string} contentType
|
||||
* @param {int} sliceSize
|
||||
* @returns {Blob}
|
||||
*/
|
||||
function base64ToBlob(b64Data, contentType, sliceSize) {
|
||||
contentType = contentType || '';
|
||||
sliceSize = sliceSize || 512;
|
||||
|
||||
const byteCharacters = atob(b64Data);
|
||||
let byteArrays = [];
|
||||
|
||||
for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) {
|
||||
const slice = byteCharacters.slice(offset, offset + sliceSize);
|
||||
const byteNumbers = Array.from(slice).map((char) => char.charCodeAt(0));
|
||||
byteArrays.push(new Uint8Array(byteNumbers));
|
||||
}
|
||||
|
||||
return new Blob(byteArrays, { type: contentType });
|
||||
}
|
||||
|
||||
return {
|
||||
base64ToBlob,
|
||||
};
|
||||
});
|
||||
121
web_mobile/static/tests/user_menu_mobile_tests.js
Normal file
121
web_mobile/static/tests/user_menu_mobile_tests.js
Normal file
@@ -0,0 +1,121 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { hotkeyService } from "@web/core/hotkeys/hotkey_service";
|
||||
import { UserMenu } from "@web/webclient/user_menu/user_menu";
|
||||
import { shortcutItem, switchAccountItem } from "../src/js/user_menu_items";
|
||||
import { makeTestEnv } from "@web/../tests/helpers/mock_env";
|
||||
import { makeFakeNotificationService } from "@web/../tests/helpers/mock_services";
|
||||
import { click as _click, getFixture, mount, patchWithCleanup } from "@web/../tests/helpers/utils";
|
||||
import { menuService } from "@web/webclient/menus/menu_service";
|
||||
import { actionService } from "@web/webclient/actions/action_service";
|
||||
|
||||
import mobile from "web_mobile.core";
|
||||
import { viewService } from "@web/views/view_service";
|
||||
import { ormService } from "@web/core/orm_service";
|
||||
|
||||
const serviceRegistry = registry.category("services");
|
||||
const userMenuRegistry = registry.category("user_menuitems");
|
||||
const MY_IMAGE =
|
||||
"iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==";
|
||||
let target;
|
||||
|
||||
|
||||
// The UserMenu has a d-none class which is overriden, purely by bootstrap
|
||||
// by d-x-block classes when the screen has a minimum size.
|
||||
// To avoid problems, we always skip the visibility check by default when clicking
|
||||
const click = (el, selector, skipVisibility) => {
|
||||
if (skipVisibility === undefined) {
|
||||
skipVisibility = true;
|
||||
}
|
||||
return _click(el, selector, skipVisibility);
|
||||
};
|
||||
QUnit.module("UserMenu", {
|
||||
async beforeEach() {
|
||||
serviceRegistry.add("hotkey", hotkeyService);
|
||||
serviceRegistry.add("action", actionService)
|
||||
.add("view", viewService) // #action-serv-leg-compat-js-class
|
||||
.add("orm", ormService); // #action-serv-leg-compat-js-class
|
||||
serviceRegistry.add("menu", menuService);
|
||||
target = getFixture();
|
||||
},
|
||||
});
|
||||
|
||||
QUnit.test("can execute the callback of addHomeShortcut on an App", async (assert) => {
|
||||
assert.expect(8)
|
||||
patchWithCleanup(mobile.methods, {
|
||||
addHomeShortcut({ title, shortcut_url, web_icon }) {
|
||||
assert.step("should call addHomeShortcut");
|
||||
assert.strictEqual(title, document.title);
|
||||
assert.strictEqual(shortcut_url, document.URL);
|
||||
assert.strictEqual(web_icon, MY_IMAGE);
|
||||
}
|
||||
});
|
||||
const menus = {
|
||||
root: { id: "root", children: [1], name: "root", appID: "root" },
|
||||
1: { id: 1, children: [], name: "App0", appID: 1, webIconData: `data:image/png;base64,${MY_IMAGE}` },
|
||||
};
|
||||
const baseConfig = { serverData: { menus } };
|
||||
const env = await makeTestEnv(baseConfig);
|
||||
|
||||
userMenuRegistry.add("web_mobile.shortcut", shortcutItem);
|
||||
// Set App1 menu and mount
|
||||
env.services.menu.setCurrentMenu(1);
|
||||
await mount(UserMenu, target, { env });
|
||||
assert.hasClass(target.querySelector(".o_user_menu"), "d-none");
|
||||
// remove the "d-none" class to make the menu visible before interacting with it
|
||||
target.querySelector(".o_user_menu").classList.remove("d-none");
|
||||
await click(target.querySelector("button.dropdown-toggle"));
|
||||
assert.containsOnce(target, ".dropdown-menu .dropdown-item");
|
||||
const item = target.querySelector(".dropdown-menu .dropdown-item");
|
||||
assert.strictEqual(item.textContent, "Add to Home Screen");
|
||||
await click(item);
|
||||
assert.verifySteps(['should call addHomeShortcut']);
|
||||
});
|
||||
|
||||
QUnit.test("can execute the callback of addHomeShortcut on the HomeMenu", async (assert) => {
|
||||
assert.expect(4)
|
||||
patchWithCleanup(mobile.methods, {
|
||||
addHomeShortcut() {
|
||||
assert.step("shouldn't call addHomeShortcut");
|
||||
}
|
||||
});
|
||||
const mockNotification = (message) => {
|
||||
assert.step(`notification (${message})`);
|
||||
return () => {};
|
||||
}
|
||||
serviceRegistry.add("notification", makeFakeNotificationService(mockNotification));
|
||||
const env = await makeTestEnv();
|
||||
|
||||
userMenuRegistry.add("web_mobile.shortcut", shortcutItem);
|
||||
await mount(UserMenu, target, { env });
|
||||
// remove the "d-none" class to make the menu visible before interacting with it
|
||||
target.querySelector(".o_user_menu").classList.remove("d-none");
|
||||
await click(target.querySelector("button.dropdown-toggle"));
|
||||
assert.containsOnce(target, ".dropdown-menu .dropdown-item");
|
||||
const item = target.querySelector(".dropdown-menu .dropdown-item");
|
||||
assert.strictEqual(item.textContent, "Add to Home Screen");
|
||||
await click(item);
|
||||
assert.verifySteps(["notification (No shortcut for Home Menu)"]);
|
||||
});
|
||||
|
||||
QUnit.test("can execute the callback of switchAccount", async (assert) => {
|
||||
assert.expect(4)
|
||||
patchWithCleanup(mobile.methods, {
|
||||
switchAccount() {
|
||||
assert.step("should call switchAccount");
|
||||
}
|
||||
});
|
||||
const env = await makeTestEnv();
|
||||
|
||||
userMenuRegistry.add("web_mobile.switch", switchAccountItem);
|
||||
await mount(UserMenu, target, { env });
|
||||
// remove the "d-none" class to make the menu visible before interacting with it
|
||||
target.querySelector(".o_user_menu").classList.remove("d-none");
|
||||
await click(target.querySelector("button.dropdown-toggle"));
|
||||
assert.containsOnce(target, ".dropdown-menu .dropdown-item");
|
||||
const item = target.querySelector(".dropdown-menu .dropdown-item");
|
||||
assert.strictEqual(item.textContent, "Switch/Add Account");
|
||||
await click(item);
|
||||
assert.verifySteps(["should call switchAccount"]);
|
||||
});
|
||||
752
web_mobile/static/tests/web_mobile_tests.js
Normal file
752
web_mobile/static/tests/web_mobile_tests.js
Normal file
@@ -0,0 +1,752 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import Dialog from "web.Dialog";
|
||||
import dom from "web.dom";
|
||||
import OwlDialog from "web.OwlDialog";
|
||||
import Popover from "web.Popover";
|
||||
import session from "web.session";
|
||||
import makeTestEnvironment from "web.test_env";
|
||||
import testUtils from "web.test_utils";
|
||||
import Widget from "web.Widget";
|
||||
|
||||
import { useBackButton } from "web_mobile.hooks";
|
||||
import { BackButtonEventMixin } from "web_mobile.mixins";
|
||||
import mobile from "web_mobile.core";
|
||||
/*import UserPreferencesFormView from "web_mobile.UserPreferencesFormView";*/
|
||||
|
||||
import { createWebClient, doAction } from '@web/../tests/webclient/helpers';
|
||||
import { makeTestEnv } from "@web/../tests/helpers/mock_env";
|
||||
import { mount, getFixture, destroy, patchWithCleanup, clickSave} from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
|
||||
const { Component, useState, xml } = owl;
|
||||
|
||||
const { createParent } = testUtils;
|
||||
|
||||
const MY_IMAGE =
|
||||
"iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==";
|
||||
const BASE64_SVG_IMAGE =
|
||||
"PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHdpZHRoPScxNzUnIGhlaWdodD0nMTAwJyBmaWxsPScjMDAwJz48cG9seWdvbiBwb2ludHM9JzAsMCAxMDAsMCA1MCw1MCcvPjwvc3ZnPg==";
|
||||
const BASE64_PNG_HEADER = "iVBORw0KGg";
|
||||
|
||||
let target;
|
||||
let serverData;
|
||||
|
||||
QUnit.module("web_mobile", {
|
||||
async beforeEach() {
|
||||
target = getFixture();
|
||||
serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: {
|
||||
name: { string: "name", type: "char" },
|
||||
avatar_1920: {},
|
||||
parent_id: { string: "Parent", type: "many2one", relation: "partner" },
|
||||
sibling_ids: {
|
||||
string: "Sibling",
|
||||
type: "many2many",
|
||||
relation: "partner",
|
||||
},
|
||||
phone: {},
|
||||
mobile: {},
|
||||
email: {},
|
||||
street: {},
|
||||
street2: {},
|
||||
city: {},
|
||||
state_id: {},
|
||||
zip: {},
|
||||
country_id: {},
|
||||
website: {},
|
||||
function: {},
|
||||
title: {},
|
||||
date: { string: "A date", type: "date" },
|
||||
datetime: { string: "A datetime", type: "datetime" },
|
||||
},
|
||||
records: [
|
||||
{
|
||||
id: 1,
|
||||
name: "coucou1",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "coucou2",
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
name: "coucou3",
|
||||
avatar_1920: "image",
|
||||
parent_id: 1,
|
||||
phone: "phone",
|
||||
mobile: "mobile",
|
||||
email: "email",
|
||||
street: "street",
|
||||
street2: "street2",
|
||||
city: "city",
|
||||
state_id: "state_id",
|
||||
zip: "zip",
|
||||
country_id: "country_id",
|
||||
website: "website",
|
||||
function: "function",
|
||||
title: "title",
|
||||
},
|
||||
],
|
||||
},
|
||||
users: {
|
||||
fields: {
|
||||
name: { string: "name", type: "char" },
|
||||
},
|
||||
records: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
setupViewRegistries();
|
||||
},
|
||||
}, function () {
|
||||
QUnit.module("core", function () {
|
||||
QUnit.test("BackButtonManager", async function (assert) {
|
||||
assert.expect(13);
|
||||
|
||||
patchWithCleanup(mobile.methods, {
|
||||
overrideBackButton({ enabled }) {
|
||||
assert.step(`overrideBackButton: ${enabled}`);
|
||||
},
|
||||
});
|
||||
|
||||
const { BackButtonManager, BackButtonListenerError } = mobile;
|
||||
const manager = new BackButtonManager();
|
||||
const DummyWidget = Widget.extend({
|
||||
_onBackButton(ev) {
|
||||
assert.step(`${ev.type} event`);
|
||||
},
|
||||
});
|
||||
const dummy = new DummyWidget();
|
||||
|
||||
manager.addListener(dummy, dummy._onBackButton);
|
||||
assert.verifySteps(["overrideBackButton: true"]);
|
||||
|
||||
// simulate 'backbutton' event triggered by the app
|
||||
await testUtils.dom.triggerEvent(document, "backbutton");
|
||||
assert.verifySteps(["backbutton event"]);
|
||||
|
||||
manager.removeListener(dummy);
|
||||
assert.verifySteps(["overrideBackButton: false"]);
|
||||
await testUtils.dom.triggerEvent(document, "backbutton");
|
||||
assert.verifySteps([], "shouldn't trigger any handler");
|
||||
|
||||
manager.addListener(dummy, dummy._onBackButton);
|
||||
assert.throws(
|
||||
() => {
|
||||
manager.addListener(dummy, dummy._onBackButton);
|
||||
},
|
||||
BackButtonListenerError,
|
||||
"should raise an error if adding a listener twice"
|
||||
);
|
||||
assert.verifySteps(["overrideBackButton: true"]);
|
||||
|
||||
manager.removeListener(dummy);
|
||||
assert.throws(
|
||||
() => {
|
||||
manager.removeListener(dummy);
|
||||
},
|
||||
BackButtonListenerError,
|
||||
"should raise an error if removing a non-registered listener"
|
||||
);
|
||||
assert.verifySteps(["overrideBackButton: false"]);
|
||||
|
||||
dummy.destroy();
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.module("BackButtonEventMixin");
|
||||
|
||||
QUnit.test("widget should receive a backbutton event", async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
patchWithCleanup(mobile.methods, {
|
||||
overrideBackButton({ enabled }) {
|
||||
assert.step(`overrideBackButton: ${enabled}`);
|
||||
},
|
||||
});
|
||||
|
||||
const DummyWidget = Widget.extend(BackButtonEventMixin, {
|
||||
_onBackButton(ev) {
|
||||
assert.step(`${ev.type} event`);
|
||||
},
|
||||
});
|
||||
const backButtonEvent = new Event("backbutton");
|
||||
const dummy = new DummyWidget();
|
||||
dummy.appendTo($("<div>"));
|
||||
|
||||
// simulate 'backbutton' event triggered by the app
|
||||
document.dispatchEvent(backButtonEvent);
|
||||
// waiting nextTick to match testUtils.dom.triggerEvents() behavior
|
||||
await testUtils.nextTick();
|
||||
|
||||
assert.verifySteps([], "shouldn't have register handle before attached to the DOM");
|
||||
|
||||
dom.append($("qunit-fixture"), dummy.$el, {
|
||||
in_DOM: true,
|
||||
callbacks: [{ widget: dummy }],
|
||||
});
|
||||
|
||||
// simulate 'backbutton' event triggered by the app
|
||||
document.dispatchEvent(backButtonEvent);
|
||||
await testUtils.nextTick();
|
||||
|
||||
dom.detach([{ widget: dummy }]);
|
||||
|
||||
assert.verifySteps(
|
||||
["overrideBackButton: true", "backbutton event", "overrideBackButton: false"],
|
||||
"should have enabled/disabled the back-button override"
|
||||
);
|
||||
|
||||
dummy.destroy();
|
||||
});
|
||||
|
||||
QUnit.test("multiple widgets should receive backbutton events in the right order", async function (assert) {
|
||||
assert.expect(6);
|
||||
|
||||
patchWithCleanup(mobile.methods, {
|
||||
overrideBackButton({ enabled }) {
|
||||
assert.step(`overrideBackButton: ${enabled}`);
|
||||
},
|
||||
});
|
||||
|
||||
const DummyWidget = Widget.extend(BackButtonEventMixin, {
|
||||
init(parent, { name }) {
|
||||
this._super.apply(this, arguments);
|
||||
this.name = name;
|
||||
},
|
||||
_onBackButton(ev) {
|
||||
assert.step(`${this.name}: ${ev.type} event`);
|
||||
dom.detach([{ widget: this }]);
|
||||
},
|
||||
});
|
||||
const backButtonEvent = new Event("backbutton");
|
||||
const dummy1 = new DummyWidget(null, { name: "dummy1" });
|
||||
dom.append($("qunit-fixture"), dummy1.$el, {
|
||||
in_DOM: true,
|
||||
callbacks: [{ widget: dummy1 }],
|
||||
});
|
||||
|
||||
const dummy2 = new DummyWidget(null, { name: "dummy2" });
|
||||
dom.append($("qunit-fixture"), dummy2.$el, {
|
||||
in_DOM: true,
|
||||
callbacks: [{ widget: dummy2 }],
|
||||
});
|
||||
|
||||
const dummy3 = new DummyWidget(null, { name: "dummy3" });
|
||||
dom.append($("qunit-fixture"), dummy3.$el, {
|
||||
in_DOM: true,
|
||||
callbacks: [{ widget: dummy3 }],
|
||||
});
|
||||
|
||||
// simulate 'backbutton' events triggered by the app
|
||||
document.dispatchEvent(backButtonEvent);
|
||||
// waiting nextTick to match testUtils.dom.triggerEvents() behavior
|
||||
await testUtils.nextTick();
|
||||
document.dispatchEvent(backButtonEvent);
|
||||
await testUtils.nextTick();
|
||||
document.dispatchEvent(backButtonEvent);
|
||||
await testUtils.nextTick();
|
||||
|
||||
assert.verifySteps([
|
||||
"overrideBackButton: true",
|
||||
"dummy3: backbutton event",
|
||||
"dummy2: backbutton event",
|
||||
"dummy1: backbutton event",
|
||||
"overrideBackButton: false",
|
||||
]);
|
||||
|
||||
dummy1.destroy();
|
||||
dummy2.destroy();
|
||||
dummy3.destroy();
|
||||
});
|
||||
|
||||
QUnit.module("useBackButton");
|
||||
|
||||
QUnit.test("component should receive a backbutton event", async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
patchWithCleanup(mobile.methods, {
|
||||
overrideBackButton({ enabled }) {
|
||||
assert.step(`overrideBackButton: ${enabled}`);
|
||||
},
|
||||
});
|
||||
|
||||
class DummyComponent extends Component {
|
||||
setup() {
|
||||
this._backButtonHandler = useBackButton(this._onBackButton);
|
||||
}
|
||||
|
||||
_onBackButton(ev) {
|
||||
assert.step(`${ev.type} event`);
|
||||
}
|
||||
}
|
||||
DummyComponent.template = xml`<div/>`;
|
||||
|
||||
const target = getFixture();
|
||||
const env = makeTestEnv();
|
||||
|
||||
const dummy = await mount(DummyComponent, target, { env });
|
||||
|
||||
// simulate 'backbutton' event triggered by the app
|
||||
await testUtils.dom.triggerEvent(document, "backbutton");
|
||||
assert.verifySteps(
|
||||
["overrideBackButton: true", "backbutton event"],
|
||||
"should have enabled/disabled the back-button override"
|
||||
);
|
||||
|
||||
destroy(dummy);
|
||||
assert.verifySteps(["overrideBackButton: false"]);
|
||||
});
|
||||
|
||||
QUnit.test("multiple components should receive backbutton events in the right order", async function (assert) {
|
||||
assert.expect(6);
|
||||
|
||||
patchWithCleanup(mobile.methods, {
|
||||
overrideBackButton({ enabled }) {
|
||||
assert.step(`overrideBackButton: ${enabled}`);
|
||||
},
|
||||
});
|
||||
|
||||
class DummyComponent extends Component {
|
||||
setup() {
|
||||
this._backButtonHandler = useBackButton(this._onBackButton);
|
||||
}
|
||||
|
||||
_onBackButton(ev) {
|
||||
assert.step(`${this.props.name}: ${ev.type} event`);
|
||||
// unmounting is not supported anymore
|
||||
// A real business case equivalent to this is to have a Parent component
|
||||
// doing a foreach on some reactive object which contains the list of dummy components
|
||||
// and calling a callback props.onBackButton right here that removes the element from the list
|
||||
destroy(this);
|
||||
}
|
||||
}
|
||||
DummyComponent.template = xml`<div/>`;
|
||||
|
||||
const props1 = { name: "dummy1" };
|
||||
const props2 = { name: "dummy2" };
|
||||
const props3 = { name: "dummy3" };
|
||||
const target = getFixture();
|
||||
const env = makeTestEnv();
|
||||
|
||||
await mount(DummyComponent, target, { props: props1, env });
|
||||
await mount(DummyComponent, target, { props: props2, env });
|
||||
await mount(DummyComponent, target, { props: props3, env });
|
||||
|
||||
// simulate 'backbutton' events triggered by the app
|
||||
await testUtils.dom.triggerEvent(document, "backbutton");
|
||||
await testUtils.dom.triggerEvent(document, "backbutton");
|
||||
await testUtils.dom.triggerEvent(document, "backbutton");
|
||||
|
||||
assert.verifySteps([
|
||||
"overrideBackButton: true",
|
||||
"dummy3: backbutton event",
|
||||
"dummy2: backbutton event",
|
||||
"dummy1: backbutton event",
|
||||
"overrideBackButton: false",
|
||||
]);
|
||||
|
||||
});
|
||||
|
||||
QUnit.test("component should receive a backbutton event: custom activation", async function (assert) {
|
||||
assert.expect(10);
|
||||
|
||||
patchWithCleanup(mobile.methods, {
|
||||
overrideBackButton({ enabled }) {
|
||||
assert.step(`overrideBackButton: ${enabled}`);
|
||||
},
|
||||
});
|
||||
|
||||
class DummyComponent extends Component {
|
||||
setup() {
|
||||
this._backButtonHandler = useBackButton(
|
||||
this._onBackButton,
|
||||
this.shouldActivateBackButton.bind(this)
|
||||
);
|
||||
this.state = useState({
|
||||
show: this.props.show,
|
||||
});
|
||||
}
|
||||
|
||||
toggle() {
|
||||
this.state.show = !this.state.show;
|
||||
}
|
||||
|
||||
shouldActivateBackButton() {
|
||||
return this.state.show;
|
||||
}
|
||||
|
||||
_onBackButton(ev) {
|
||||
assert.step(`${ev.type} event`);
|
||||
}
|
||||
}
|
||||
DummyComponent.template = xml`<button class="dummy" t-esc="state.show" t-on-click="toggle"/>`;
|
||||
|
||||
const target = getFixture();
|
||||
const env = makeTestEnv();
|
||||
|
||||
const dummy = await mount(DummyComponent, target, { props: { show: false }, env });
|
||||
|
||||
assert.verifySteps([], "shouldn't have enabled backbutton mount");
|
||||
await testUtils.dom.click(target.querySelector(".dummy"));
|
||||
// simulate 'backbutton' event triggered by the app
|
||||
await testUtils.dom.triggerEvent(document, "backbutton");
|
||||
await testUtils.dom.click(target.querySelector(".dummy"));
|
||||
assert.verifySteps(
|
||||
[
|
||||
"overrideBackButton: true",
|
||||
"backbutton event",
|
||||
"overrideBackButton: false",
|
||||
],
|
||||
"should have enabled/disabled the back-button override"
|
||||
);
|
||||
destroy(dummy);
|
||||
|
||||
// enabled at mount
|
||||
const dummy2 = await mount(DummyComponent, target, { props: { show: true }, env });
|
||||
assert.verifySteps(
|
||||
["overrideBackButton: true"],
|
||||
"shouldn have enabled backbutton at mount"
|
||||
);
|
||||
// simulate 'backbutton' event triggered by the app
|
||||
await testUtils.dom.triggerEvent(document, "backbutton");
|
||||
destroy(dummy2);
|
||||
|
||||
assert.verifySteps(
|
||||
["backbutton event", "overrideBackButton: false"],
|
||||
"should have disabled the back-button override during unmount"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.module("Dialog");
|
||||
|
||||
QUnit.test("dialog is closable with backbutton event", async function (assert) {
|
||||
assert.expect(7);
|
||||
|
||||
patchWithCleanup(mobile.methods, {
|
||||
overrideBackButton({ enabled }) {
|
||||
assert.step(`overrideBackButton: ${enabled}`);
|
||||
},
|
||||
});
|
||||
|
||||
testUtils.mock.patch(Dialog, {
|
||||
close: function () {
|
||||
assert.step("close");
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
|
||||
const parent = await createParent({
|
||||
data: this.data,
|
||||
archs: {
|
||||
"partner,false,form": `
|
||||
<form>
|
||||
<sheet>
|
||||
<field name="name"/>
|
||||
</sheet>
|
||||
</form>
|
||||
`,
|
||||
},
|
||||
});
|
||||
|
||||
const backButtonEvent = new Event("backbutton");
|
||||
const dialog = new Dialog(parent, {
|
||||
res_model: "partner",
|
||||
res_id: 1,
|
||||
}).open();
|
||||
await dialog.opened().then(() => {
|
||||
assert.step("opened");
|
||||
});
|
||||
assert.containsOnce(document.body, ".modal", "should have a modal");
|
||||
|
||||
// simulate 'backbutton' event triggered by the app waiting
|
||||
document.dispatchEvent(backButtonEvent);
|
||||
// nextTick to match testUtils.dom.triggerEvents() behavior
|
||||
await testUtils.nextTick();
|
||||
|
||||
// The goal of this assert is to check that our event called the
|
||||
// opened/close methods on Dialog.
|
||||
assert.verifySteps(
|
||||
["overrideBackButton: true", "opened", "close", "overrideBackButton: false"],
|
||||
"should have open/close dialog"
|
||||
);
|
||||
assert.containsNone(document.body, ".modal", "modal should be closed");
|
||||
|
||||
parent.destroy();
|
||||
});
|
||||
|
||||
QUnit.module("OwlDialog");
|
||||
|
||||
QUnit.test("dialog is closable with backbutton event", async function (assert) {
|
||||
assert.expect(7);
|
||||
|
||||
patchWithCleanup(mobile.methods, {
|
||||
overrideBackButton({ enabled }) {
|
||||
assert.step(`overrideBackButton: ${enabled}`);
|
||||
},
|
||||
});
|
||||
|
||||
class Parent extends Component {
|
||||
setup() {
|
||||
this.state = useState({ display: true });
|
||||
}
|
||||
_onDialogClosed() {
|
||||
this.state.display = false;
|
||||
assert.step("dialog_closed");
|
||||
}
|
||||
}
|
||||
|
||||
Parent.components = { OwlDialog };
|
||||
Parent.template = xml`
|
||||
<div>
|
||||
<OwlDialog
|
||||
t-if="state.display"
|
||||
onClosed="() => this._onDialogClosed()">
|
||||
Some content
|
||||
</OwlDialog>
|
||||
</div>`;
|
||||
|
||||
const target = getFixture();
|
||||
const env = await makeTestEnvironment();
|
||||
|
||||
await mount(Parent, target, { env });
|
||||
|
||||
assert.containsOnce(document.body, ".o_dialog");
|
||||
assert.verifySteps(["overrideBackButton: true"]);
|
||||
// simulate 'backbutton' event triggered by the app
|
||||
await testUtils.dom.triggerEvent(document, "backbutton");
|
||||
assert.verifySteps(["dialog_closed", "overrideBackButton: false"]);
|
||||
assert.containsNone(document.body, ".o_dialog", "should have been closed");
|
||||
});
|
||||
|
||||
QUnit.module("Popover");
|
||||
|
||||
QUnit.test("popover is closable with backbutton event", async function (assert) {
|
||||
assert.expect(7);
|
||||
|
||||
patchWithCleanup(mobile.methods, {
|
||||
overrideBackButton({ enabled }) {
|
||||
assert.step(`overrideBackButton: ${enabled}`);
|
||||
},
|
||||
});
|
||||
|
||||
class Parent extends Component {}
|
||||
|
||||
Parent.components = { Popover };
|
||||
Parent.template = xml`
|
||||
<div>
|
||||
<Popover>
|
||||
<t t-set="opened">
|
||||
Some content
|
||||
</t>
|
||||
<button id="target">
|
||||
Show me
|
||||
</button>
|
||||
</Popover>
|
||||
</div>`;
|
||||
|
||||
const target = getFixture();
|
||||
const env = makeTestEnv();
|
||||
|
||||
await mount(Parent, target, { env });
|
||||
|
||||
assert.containsNone(document.body, ".o_popover");
|
||||
await testUtils.dom.click(document.querySelector("#target"));
|
||||
assert.containsOnce(document.body, ".o_popover");
|
||||
assert.verifySteps(["overrideBackButton: true"]);
|
||||
// simulate 'backbutton' event triggered by the app
|
||||
await testUtils.dom.triggerEvent(document, "backbutton");
|
||||
assert.verifySteps(["overrideBackButton: false"]);
|
||||
assert.containsNone(document.body, ".o_popover", "should have been closed");
|
||||
|
||||
});
|
||||
|
||||
QUnit.module("ControlPanel");
|
||||
|
||||
QUnit.test("mobile search: close with backbutton event", async function (assert) {
|
||||
assert.expect(7);
|
||||
|
||||
patchWithCleanup(mobile.methods, {
|
||||
overrideBackButton({ enabled }) {
|
||||
assert.step(`overrideBackButton: ${enabled}`);
|
||||
},
|
||||
});
|
||||
|
||||
const actions = {
|
||||
1: {
|
||||
id: 1,
|
||||
name: "Yes",
|
||||
res_model: "partner",
|
||||
type: "ir.actions.act_window",
|
||||
views: [[false, "list"]],
|
||||
},
|
||||
};
|
||||
|
||||
const views = {
|
||||
"partner,false,list": '<tree><field name="foo"/></tree>',
|
||||
"partner,false,search": `
|
||||
<search>
|
||||
<filter string="Active" name="my_projects" domain="[('boolean_field', '=', True)]"/>
|
||||
<field name="foo" string="Foo"/>
|
||||
</search>`,
|
||||
};
|
||||
|
||||
const models = {
|
||||
partner: {
|
||||
fields: {
|
||||
foo: { string: "Foo", type: "char" },
|
||||
boolean_field: { string: "I am a boolean", type: "boolean" },
|
||||
},
|
||||
records: [{ id: 1, display_name: "First record", foo: "yop" }],
|
||||
},
|
||||
};
|
||||
const serverData = {actions, models, views};
|
||||
|
||||
const webClient = await createWebClient({ serverData });
|
||||
|
||||
await doAction(webClient, 1);
|
||||
|
||||
// the mobile search is portaled in body, not in the fixture
|
||||
assert.containsNone(document.body, ".o_mobile_search");
|
||||
|
||||
// open the search view
|
||||
await testUtils.dom.click(
|
||||
target.querySelector("button.o_enable_searchview")
|
||||
);
|
||||
// open it in full screen
|
||||
await testUtils.dom.click(
|
||||
target.querySelector(".o_toggle_searchview_full")
|
||||
);
|
||||
|
||||
assert.containsOnce(document.body, ".o_mobile_search");
|
||||
assert.verifySteps(["overrideBackButton: true"]);
|
||||
|
||||
// simulate 'backbutton' event triggered by the app
|
||||
await testUtils.dom.triggerEvent(target, "backbutton");
|
||||
assert.containsNone(target, ".o_mobile_search");
|
||||
assert.verifySteps(["overrideBackButton: false"]);
|
||||
});
|
||||
|
||||
QUnit.module("UpdateDeviceAccountControllerMixin");
|
||||
|
||||
QUnit.test("controller should call native updateAccount method when saving record", async function (assert) {
|
||||
assert.expect(4);
|
||||
patchWithCleanup(mobile.methods, {
|
||||
updateAccount( options ) {
|
||||
const { avatar, name, username } = options;
|
||||
assert.ok("should call updateAccount");
|
||||
assert.ok(avatar.startsWith(BASE64_PNG_HEADER), "should have a PNG base64 encoded avatar");
|
||||
assert.strictEqual(name, "Marc Demo");
|
||||
assert.strictEqual(username, "demo");
|
||||
return Promise.resolve();
|
||||
}
|
||||
});
|
||||
|
||||
patchWithCleanup(session, {
|
||||
url(path) {
|
||||
if (path === '/web/image') {
|
||||
return `data:image/png;base64,${MY_IMAGE}`;
|
||||
}
|
||||
return this._super(...arguments);
|
||||
},
|
||||
username: "demo",
|
||||
name: "Marc Demo",
|
||||
});
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData: serverData,
|
||||
arch: `
|
||||
<form js_class="res_users_preferences_form">
|
||||
<sheet>
|
||||
<field name="name"/>
|
||||
</sheet>
|
||||
</form>`,
|
||||
});
|
||||
await clickSave(target);
|
||||
});
|
||||
|
||||
QUnit.test("controller should call native updateAccount method with SVG avatar when saving record", async function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
patchWithCleanup(mobile.methods, {
|
||||
updateAccount( options ) {
|
||||
const { avatar, name, username } = options;
|
||||
assert.ok("should call updateAccount");
|
||||
assert.ok(avatar.startsWith(BASE64_PNG_HEADER), "should have a PNG base64 encoded avatar");
|
||||
assert.strictEqual(name, "Marc Demo");
|
||||
assert.strictEqual(username, "demo");
|
||||
return Promise.resolve();
|
||||
}
|
||||
});
|
||||
|
||||
patchWithCleanup(session, {
|
||||
url(path) {
|
||||
if (path === '/web/image') {
|
||||
return `data:image/svg+xml;base64,${BASE64_SVG_IMAGE}`;
|
||||
}
|
||||
return this._super(...arguments);
|
||||
},
|
||||
username: "demo",
|
||||
name: "Marc Demo",
|
||||
});
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData: serverData,
|
||||
arch: `
|
||||
<form js_class="res_users_preferences_form">
|
||||
<sheet>
|
||||
<field name="name"/>
|
||||
</sheet>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
await clickSave(target);
|
||||
});
|
||||
|
||||
QUnit.test("UserPreferencesFormView should call native updateAccount method when saving record", async function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
patchWithCleanup(mobile.methods, {
|
||||
updateAccount( options ) {
|
||||
const { avatar, name, username } = options;
|
||||
assert.ok("should call updateAccount");
|
||||
assert.ok(avatar.startsWith(BASE64_PNG_HEADER), "should have a PNG base64 encoded avatar");
|
||||
assert.strictEqual(name, "Marc Demo");
|
||||
assert.strictEqual(username, "demo");
|
||||
return Promise.resolve();
|
||||
}
|
||||
});
|
||||
|
||||
patchWithCleanup(session, {
|
||||
url(path) {
|
||||
if (path === '/web/image') {
|
||||
return `data:image/png;base64,${MY_IMAGE}`;
|
||||
}
|
||||
return this._super(...arguments);
|
||||
},
|
||||
username: "demo",
|
||||
name: "Marc Demo",
|
||||
});
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "users",
|
||||
serverData: serverData,
|
||||
arch: `
|
||||
<form js_class="res_users_preferences_form">
|
||||
<sheet>
|
||||
<field name="name"/>
|
||||
</sheet>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
await clickSave(target);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user