[IMP] web: Owl compatibility: add ComponentAdapter
This commit adds the ComponentAdapter, an Owl component meant to be used as universal adapter for Owl components that embed Odoo legacy widgets. This component will be a precious tool during the transition phase of converting our JS codebase from the legacy (widget) framework to Owl.
This commit is contained in:
@@ -1,15 +1,15 @@
|
||||
odoo.define("web.env", function (require) {
|
||||
"use strict";
|
||||
|
||||
const { _lt, _t, bus } = require("web.core");
|
||||
const { blockUI, unblockUI } = require("web.framework");
|
||||
const { device, isDebug } = require("web.config");
|
||||
const { jsonRpc } = require('web.ajax');
|
||||
const { device, isDebug } = require("web.config");
|
||||
const { _lt, _t, bus, serviceRegistry } = require("web.core");
|
||||
const dataManager = require('web.data_manager');
|
||||
const { blockUI, unblockUI } = require("web.framework");
|
||||
const rpc = require("web.rpc");
|
||||
const session = require("web.session");
|
||||
const utils = require("web.utils");
|
||||
|
||||
|
||||
const qweb = new owl.QWeb({ translateFn: _t });
|
||||
|
||||
function ajaxJsonRPC() {
|
||||
@@ -59,6 +59,64 @@ odoo.define("web.env", function (require) {
|
||||
utils.set_cookie(...arguments);
|
||||
}
|
||||
|
||||
// ServiceProvider
|
||||
const services = {}; // dict containing deployed service instances
|
||||
const UndeployedServices = {}; // dict containing classes of undeployed services
|
||||
function _deployServices() {
|
||||
let done = false;
|
||||
while (!done) {
|
||||
const serviceName = _.findKey(UndeployedServices, Service => {
|
||||
// no missing dependency
|
||||
return !_.some(Service.prototype.dependencies, depName => {
|
||||
return !services[depName];
|
||||
});
|
||||
});
|
||||
if (serviceName) {
|
||||
const Service = UndeployedServices[serviceName];
|
||||
// we created a patched version of the Service in which the 'trigger_up'
|
||||
// function directly calls the requested service, instead of triggering
|
||||
// a 'call_service' event up, which wouldn't work as services have
|
||||
// no parent
|
||||
const PatchedService = Service.extend({
|
||||
_trigger_up: function (ev) {
|
||||
this._super(...arguments);
|
||||
if (!ev.is_stopped() && ev.name === 'call_service') {
|
||||
const payload = ev.data;
|
||||
let args = payload.args || [];
|
||||
if (payload.service === 'ajax' && payload.method === 'rpc') {
|
||||
// ajax service uses an extra 'target' argument for rpc
|
||||
args = args.concat(ev.target);
|
||||
}
|
||||
const service = services[payload.service];
|
||||
const result = service[payload.method].apply(service, args);
|
||||
payload.callback(result);
|
||||
}
|
||||
},
|
||||
});
|
||||
const service = new PatchedService();
|
||||
services[serviceName] = service;
|
||||
delete UndeployedServices[serviceName];
|
||||
service.start();
|
||||
} else {
|
||||
done = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
_.each(serviceRegistry.map, (Service, serviceName) => {
|
||||
if (serviceName in UndeployedServices) {
|
||||
throw new Error(`Service ${serviceName} is already loaded.`);
|
||||
}
|
||||
UndeployedServices[serviceName] = Service;
|
||||
});
|
||||
serviceRegistry.onAdd((serviceName, Service) => {
|
||||
if (serviceName in services || serviceName in UndeployedServices) {
|
||||
throw new Error(`Service ${serviceName} is already loaded.`);
|
||||
}
|
||||
UndeployedServices[serviceName] = Service;
|
||||
_deployServices();
|
||||
});
|
||||
_deployServices();
|
||||
|
||||
// There should be as much dependencies as possible in the env object.
|
||||
// This will allow an easier testing of components.
|
||||
// See https://github.com/odoo/owl/blob/master/doc/reference/environment.md#content-of-an-environment
|
||||
@@ -67,10 +125,11 @@ odoo.define("web.env", function (require) {
|
||||
_lt,
|
||||
_t,
|
||||
bus,
|
||||
dataManager,
|
||||
device,
|
||||
isDebug,
|
||||
qweb,
|
||||
services: {
|
||||
services: Object.assign(services, {
|
||||
ajaxJsonRPC,
|
||||
blockUI,
|
||||
getCookie,
|
||||
@@ -80,7 +139,7 @@ odoo.define("web.env", function (require) {
|
||||
rpc: performRPC,
|
||||
setCookie,
|
||||
unblockUI,
|
||||
},
|
||||
}),
|
||||
session,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -0,0 +1,258 @@
|
||||
odoo.define('web.OwlCompatibility', function () {
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* This file defines the necessary tools for the transition phase where Odoo
|
||||
* legacy widgets and Owl components will coexist. There are two possible
|
||||
* scenarios:
|
||||
* 1) An Owl component has to instantiate legacy widgets
|
||||
* 2) A legacy widget has to instantiate Owl components
|
||||
*/
|
||||
|
||||
const { Component, tags } = owl;
|
||||
|
||||
/**
|
||||
* Case 1) An Owl component has to instantiate legacy widgets
|
||||
* ----------------------------------------------------------
|
||||
*
|
||||
* The ComponentAdapter is an Owl component meant to be used as universal
|
||||
* adapter for Owl components that embed Odoo legacy widgets (or dynamically
|
||||
* both Owl components and Odoo legacy widgets), e.g.:
|
||||
*
|
||||
* Owl Component
|
||||
* |
|
||||
* ComponentAdapter (Owl component)
|
||||
* |
|
||||
* Legacy Widget(s) (or Owl component(s))
|
||||
*
|
||||
*
|
||||
* The adapter takes the component/widget class as 'Component' prop, and the
|
||||
* arguments (except first arg 'parent') to initialize it as props.
|
||||
* For instance:
|
||||
* <ComponentAdapter Component="LegacyWidget" params="params"/>
|
||||
* will be translated to:
|
||||
* const LegacyWidget = this.props.Component;
|
||||
* const legacyWidget = new LegacyWidget(this, this.props.params);
|
||||
*
|
||||
* If more than one argument (in addition to 'parent') is given to initialize
|
||||
* the legacy widget, the arguments order (to initialize the sub widget) has
|
||||
* to be somehow specified. There are two alternatives. One can either (1)
|
||||
* specify the prop 'widgetArgs', corresponding to the array of arguments,
|
||||
* otherwise (2) a subclass of ComponentAdapter has to be defined. This
|
||||
* subclass must override the 'widgetArgs' getter to translate arguments
|
||||
* received as props to an array of arguments for the call to init.
|
||||
* For instance:
|
||||
* (1) <ComponentAdapter Component="LegacyWidget" firstArg="a" secondArg="b" widgetsArgs="[a, b]"/>
|
||||
* (2) class SpecificAdapter extends ComponentAdapter {
|
||||
* get widgetArgs() {
|
||||
* return [this.props.firstArg, this.props.secondArg];
|
||||
* }
|
||||
* }
|
||||
* <SpecificAdapter Component="LegacyWidget" firstArg="a" secondArg="b"/>
|
||||
*
|
||||
* If the legacy widget has to be updated when props change, one must define
|
||||
* a subclass of ComponentAdapter to override 'update' and 'render'. The
|
||||
* 'update' function takes the nextProps as argument, and should update the
|
||||
* internal state of the widget (might be async, and return a Promise).
|
||||
* However, to ensure that the DOM is updated all at once, it shouldn't do
|
||||
* a re-rendering. This is the role of function 'render', which will be
|
||||
* called just before patching the DOM, and which thus must be synchronous.
|
||||
* For instance:
|
||||
* class SpecificAdapter extends ComponentAdapter {
|
||||
* update(nextProps) {
|
||||
* return this.widget.updateState(nextProps);
|
||||
* }
|
||||
* render() {
|
||||
* return this.widget.render();
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
class ComponentAdapter extends Component {
|
||||
/**
|
||||
* Creates the template on-the-fly, depending on the type of Component
|
||||
* (legacy widget or Owl component).
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
constructor(parent, props) {
|
||||
if (!props.Component) {
|
||||
throw Error(`ComponentAdapter: 'Component' prop is missing.`);
|
||||
}
|
||||
let template;
|
||||
if (!(props.Component.prototype instanceof Component)) {
|
||||
template = tags.xml`<div/>`;
|
||||
} else {
|
||||
let propsStr = '';
|
||||
for (let p in props) {
|
||||
if (p !== 'Component') {
|
||||
propsStr += ` ${p}="props.${p}"`;
|
||||
}
|
||||
}
|
||||
template = tags.xml`<t t-component="props.Component"${propsStr}/>`;
|
||||
}
|
||||
ComponentAdapter.template = template;
|
||||
super(...arguments);
|
||||
this.template = template;
|
||||
ComponentAdapter.template = null;
|
||||
|
||||
this.widget = null; // widget instance, if Component is a legacy widget
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the legacy widget (not in the DOM yet)
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
willStart() {
|
||||
if (!(this.props.Component.prototype instanceof Component)) {
|
||||
this.widget = new this.props.Component(this, ...this.widgetArgs);
|
||||
return this.widget._widgetRenderAndInsert(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the internal state of the legacy widget (but doesn't re-render
|
||||
* it yet).
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
willUpdateProps(nextProps) {
|
||||
if (this.widget) {
|
||||
return this.update(nextProps);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hooks just before the actual patch to replace the fake div in the
|
||||
* vnode by the actual node of the legacy widget. If the widget has to
|
||||
* be re-render (because it has previously been updated), re-render it.
|
||||
* This must be synchronous.
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
__patch(vnode) {
|
||||
if (this.widget) {
|
||||
if (this.__owl__.vnode) { // not at first rendering
|
||||
this.render();
|
||||
}
|
||||
vnode.elm = this.widget.el;
|
||||
}
|
||||
return super.__patch(...arguments);
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
mounted() {
|
||||
if (this.widget && this.widget.on_attach_callback) {
|
||||
this.widget.on_attach_callback();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
willUnmount() {
|
||||
if (this.widget && this.widget.on_detach_callback) {
|
||||
this.widget.on_detach_callback();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
__destroy() {
|
||||
super.__destroy(...arguments);
|
||||
if (this.widget) {
|
||||
this.widget.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter that translates the props (except 'Component') into the array
|
||||
* of arguments used to initialize the legacy widget.
|
||||
*
|
||||
* Must be overriden if at least two props (other that Component) are
|
||||
* given.
|
||||
*
|
||||
* @returns {Array}
|
||||
*/
|
||||
get widgetArgs() {
|
||||
if (this.props.widgetArgs) {
|
||||
return this.props.widgetArgs;
|
||||
}
|
||||
const args = Object.keys(this.props);
|
||||
args.splice(args.indexOf('Component'), 1);
|
||||
if (args.length > 1) {
|
||||
throw new Error(`ComponentAdapter has more than 1 argument, 'widgetArgs' must be overriden.`);
|
||||
}
|
||||
return args.map(a => this.props[a]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Can be overriden to update the internal state of the widget when props
|
||||
* change. To ensure that the DOM is updated at once, this function should
|
||||
* not do a re-rendering (which should be done by 'render' instead).
|
||||
*
|
||||
* @param {Object} nextProps
|
||||
* @returns {Promise}
|
||||
*/
|
||||
update(/*nextProps*/) {
|
||||
console.warn(`ComponentAdapter: Widget could not be updated, maybe override 'update' function?`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Can be overriden to re-render the widget after an update. This
|
||||
* function will be called just before patchin the DOM, s.t. the DOM is
|
||||
* updated at once. It must be synchronous
|
||||
*/
|
||||
render() {
|
||||
console.warn(`ComponentAdapter: Widget could not be re-rendered, maybe override 'render' function?`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mocks _trigger_up to redirect Odoo legacy events to OWL events.
|
||||
*
|
||||
* @private
|
||||
* @param {OdooEvent} ev
|
||||
*/
|
||||
_trigger_up(ev) {
|
||||
const evType = ev.name;
|
||||
const payload = ev.data;
|
||||
if (evType === 'call_service') {
|
||||
let args = payload.args || [];
|
||||
if (payload.service === 'ajax' && payload.method === 'rpc') {
|
||||
// ajax service uses an extra 'target' argument for rpc
|
||||
args = args.concat(ev.target);
|
||||
}
|
||||
const service = this.env.services[payload.service];
|
||||
const result = service[payload.method].apply(service, args);
|
||||
payload.callback(result);
|
||||
} else if (evType === 'get_session') {
|
||||
if (payload.callback) {
|
||||
payload.callback(this.env.session);
|
||||
}
|
||||
} else if (evType === 'load_views') {
|
||||
const params = {
|
||||
model: payload.modelName,
|
||||
context: payload.context,
|
||||
views_descr: payload.views,
|
||||
};
|
||||
this.env.dataManager
|
||||
.load_views(params, payload.options || {})
|
||||
.then(payload.on_success);
|
||||
} else if (evType === 'load_filters') {
|
||||
return this.env.dataManager
|
||||
.load_filters(payload)
|
||||
.then(payload.on_success);
|
||||
} else {
|
||||
payload.__targetWidget = ev.target;
|
||||
this.trigger(evType.replace(/_/g, '-'), payload);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
ComponentAdapter,
|
||||
};
|
||||
});
|
||||
@@ -1,8 +1,10 @@
|
||||
odoo.define('web.test_env', async function (require) {
|
||||
"use strict";
|
||||
|
||||
const { buildQuery } = require("web.rpc");
|
||||
const AbstractStorageService = require('web.AbstractStorageService');
|
||||
const Bus = require("web.Bus");
|
||||
const RamStorage = require('web.RamStorage');
|
||||
const { buildQuery } = require("web.rpc");
|
||||
const session = require('web.session');
|
||||
|
||||
/**
|
||||
@@ -45,6 +47,10 @@ odoo.define('web.test_env', async function (require) {
|
||||
*/
|
||||
function makeTestEnvironment(env = {}, providedRPC = null) {
|
||||
const proxiedEnv = _proxify(env, 'env');
|
||||
const RamStorageService = AbstractStorageService.extend({
|
||||
storage: new RamStorage(),
|
||||
});
|
||||
let testEnv = {};
|
||||
const defaultEnv = {
|
||||
_t: env._t || (s => s),
|
||||
_lt: env._lt || (s => s),
|
||||
@@ -54,11 +60,22 @@ odoo.define('web.test_env', async function (require) {
|
||||
}, env.device),
|
||||
qweb: new owl.QWeb({ templates: session.owlTemplates }),
|
||||
services: Object.assign({
|
||||
ajax: { // for legacy subwidgets
|
||||
rpc() {
|
||||
const prom = testEnv.session.rpc(...arguments);
|
||||
prom.abort = function () {
|
||||
throw new Error("Can't abort this request");
|
||||
};
|
||||
return prom;
|
||||
},
|
||||
},
|
||||
getCookie() { },
|
||||
rpc(params, options) {
|
||||
const query = buildQuery(params);
|
||||
return testEnv.session.rpc(query.route, query.params, options);
|
||||
},
|
||||
local_storage: new RamStorageService(),
|
||||
session_storage: new RamStorageService(),
|
||||
}, env.services),
|
||||
session: Object.assign({
|
||||
rpc(route, params, options) {
|
||||
@@ -69,7 +86,7 @@ odoo.define('web.test_env', async function (require) {
|
||||
},
|
||||
}, env.session),
|
||||
};
|
||||
const testEnv = Object.assign(proxiedEnv, defaultEnv);
|
||||
testEnv = Object.assign(proxiedEnv, defaultEnv);
|
||||
return testEnv;
|
||||
}
|
||||
|
||||
|
||||
@@ -121,6 +121,7 @@ return Promise.all([
|
||||
return {
|
||||
mock: {
|
||||
addMockEnvironment: testUtilsMock.addMockEnvironment,
|
||||
getMockedOwlEnv: testUtilsMock.getMockedOwlEnv,
|
||||
intercept: testUtilsMock.intercept,
|
||||
patch: testUtilsMock.patch,
|
||||
patchDate: testUtilsMock.patchDate,
|
||||
|
||||
@@ -10,14 +10,15 @@ odoo.define('web.test_utils_mock', function (require) {
|
||||
* testUtils file.
|
||||
*/
|
||||
|
||||
var basic_fields = require('web.basic_fields');
|
||||
var config = require('web.config');
|
||||
var core = require('web.core');
|
||||
var dom = require('web.dom');
|
||||
var MockServer = require('web.MockServer');
|
||||
var session = require('web.session');
|
||||
const basic_fields = require('web.basic_fields');
|
||||
const config = require('web.config');
|
||||
const core = require('web.core');
|
||||
const dom = require('web.dom');
|
||||
const makeTestEnvironment = require('web.test_env');
|
||||
const MockServer = require('web.MockServer');
|
||||
const session = require('web.session');
|
||||
|
||||
var DebouncedField = basic_fields.DebouncedField;
|
||||
const DebouncedField = basic_fields.DebouncedField;
|
||||
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
@@ -359,6 +360,66 @@ function fieldsViewGet(server, params) {
|
||||
return fieldsView;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a mocked environment to be used by OWL components in tests.
|
||||
*
|
||||
* @param {Object} [params]
|
||||
* @param {Object} [params.actions] the actions given to the mock server
|
||||
* @param {Object} [params.archs] this archs given to the mock server
|
||||
* @param {Object} [params.data] the business data given to the mock server
|
||||
* @param {boolean} [params.debug]
|
||||
* @param {function} [params.mockRPC]
|
||||
* @returns {Object}
|
||||
*/
|
||||
function getMockedOwlEnv(params) {
|
||||
params = params || {};
|
||||
let Server = MockServer;
|
||||
if (params.mockRPC) {
|
||||
Server = MockServer.extend({ _performRpc: params.mockRPC });
|
||||
}
|
||||
const server = new Server(params.data, {
|
||||
actions: params.actions,
|
||||
archs: params.archs,
|
||||
debug: params.debug,
|
||||
});
|
||||
const env = {
|
||||
dataManager: {
|
||||
load_action: (actionID, context) => {
|
||||
return server.performRpc('/web/action/load', {
|
||||
kwargs: {
|
||||
action_id: actionID,
|
||||
additional_context: context,
|
||||
},
|
||||
});
|
||||
},
|
||||
load_views: (params, options) => {
|
||||
return server.performRpc('/web/dataset/call_kw/' + params.model, {
|
||||
args: [],
|
||||
kwargs: {
|
||||
context: params.context,
|
||||
options: options,
|
||||
views: params.views_descr,
|
||||
},
|
||||
method: 'load_views',
|
||||
model: params.model,
|
||||
}).then(function (views) {
|
||||
return _.mapObject(views, viewParams => {
|
||||
return fieldsViewGet(server, viewParams);
|
||||
});
|
||||
});
|
||||
},
|
||||
load_filters: params => {
|
||||
if (params.debug) {
|
||||
console.log('[mock] load_filters', params);
|
||||
}
|
||||
return Promise.resolve([]);
|
||||
},
|
||||
},
|
||||
session: params.session || {},
|
||||
};
|
||||
return makeTestEnvironment(env, server.performRpc.bind(server));
|
||||
}
|
||||
|
||||
/**
|
||||
* intercepts an event bubbling up the widget hierarchy. The event intercepted
|
||||
* must be a "custom event", i.e. an event generated by the method 'trigger_up'.
|
||||
@@ -573,6 +634,7 @@ function patchSetTimeout() {
|
||||
return {
|
||||
addMockEnvironment: addMockEnvironment,
|
||||
fieldsViewGet: fieldsViewGet,
|
||||
getMockedOwlEnv: getMockedOwlEnv,
|
||||
intercept: intercept,
|
||||
patchDate: patchDate,
|
||||
patch: patch,
|
||||
|
||||
@@ -0,0 +1,674 @@
|
||||
odoo.define('web.OwlCompatibilityTests', function (require) {
|
||||
"use strict";
|
||||
|
||||
const { ComponentAdapter } = require('web.OwlCompatibility');
|
||||
const testUtils = require('web.test_utils');
|
||||
const Widget = require('web.Widget');
|
||||
|
||||
const getMockedOwlEnv = testUtils.mock.getMockedOwlEnv;
|
||||
const makeTestPromise = testUtils.makeTestPromise;
|
||||
const nextTick = testUtils.nextTick;
|
||||
|
||||
const { Component, tags, useState } = owl;
|
||||
const { xml } = tags;
|
||||
|
||||
QUnit.module("Owl Compatibility", function () {
|
||||
QUnit.module("ComponentAdapter");
|
||||
|
||||
QUnit.test("sub widget with no argument", async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
const MyWidget = Widget.extend({
|
||||
start: function () {
|
||||
this.$el.text('Hello World!');
|
||||
}
|
||||
});
|
||||
class Parent extends Component {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.MyWidget = MyWidget;
|
||||
}
|
||||
}
|
||||
Parent.template = xml`
|
||||
<div>
|
||||
<ComponentAdapter Component="MyWidget"/>
|
||||
</div>`;
|
||||
Parent.components = { ComponentAdapter };
|
||||
|
||||
const target = testUtils.prepareTarget();
|
||||
const parent = new Parent();
|
||||
await parent.mount(target);
|
||||
|
||||
assert.strictEqual(parent.el.innerHTML, '<div>Hello World!</div>');
|
||||
|
||||
parent.destroy();
|
||||
});
|
||||
|
||||
QUnit.test("sub widget with one argument", async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
const MyWidget = Widget.extend({
|
||||
init: function (parent, name) {
|
||||
this._super.apply(this, arguments);
|
||||
this.name = name;
|
||||
},
|
||||
start: function () {
|
||||
this.$el.text(`Hello ${this.name}!`);
|
||||
}
|
||||
});
|
||||
class Parent extends Component {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.MyWidget = MyWidget;
|
||||
}
|
||||
}
|
||||
Parent.template = xml`
|
||||
<div>
|
||||
<ComponentAdapter Component="MyWidget" name="'World'"/>
|
||||
</div>`;
|
||||
Parent.components = { ComponentAdapter };
|
||||
|
||||
const target = testUtils.prepareTarget();
|
||||
const parent = new Parent();
|
||||
await parent.mount(target);
|
||||
|
||||
assert.strictEqual(parent.el.innerHTML, '<div>Hello World!</div>');
|
||||
|
||||
parent.destroy();
|
||||
});
|
||||
|
||||
QUnit.test("sub widget with several arguments (common Adapter)", async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
const MyWidget = Widget.extend({
|
||||
init: function (parent, a1, a2) {
|
||||
this._super.apply(this, arguments);
|
||||
this.a1 = a1;
|
||||
this.a2 = a2;
|
||||
},
|
||||
start: function () {
|
||||
this.$el.text(`${this.a1} ${this.a2}!`);
|
||||
}
|
||||
});
|
||||
class Parent extends Component {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.MyWidget = MyWidget;
|
||||
}
|
||||
}
|
||||
Parent.template = xml`
|
||||
<div>
|
||||
<ComponentAdapter Component="MyWidget" a1="'Hello'" a2="'World'"/>
|
||||
</div>`;
|
||||
Parent.components = { ComponentAdapter };
|
||||
|
||||
const target = testUtils.prepareTarget();
|
||||
const parent = new Parent();
|
||||
try {
|
||||
await parent.mount(target);
|
||||
} catch (e) {
|
||||
assert.strictEqual(e.toString(),
|
||||
`Error: ComponentAdapter has more than 1 argument, 'widgetArgs' must be overriden.`);
|
||||
}
|
||||
|
||||
parent.destroy();
|
||||
});
|
||||
|
||||
QUnit.test("sub widget with several arguments (specific Adapter)", async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
const MyWidget = Widget.extend({
|
||||
init: function (parent, a1, a2) {
|
||||
this._super.apply(this, arguments);
|
||||
this.a1 = a1;
|
||||
this.a2 = a2;
|
||||
},
|
||||
start: function () {
|
||||
this.$el.text(`${this.a1} ${this.a2}!`);
|
||||
}
|
||||
});
|
||||
class MyWidgetAdapter extends ComponentAdapter {
|
||||
get widgetArgs() {
|
||||
return [this.props.a1, this.props.a2];
|
||||
}
|
||||
}
|
||||
class Parent extends Component {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.MyWidget = MyWidget;
|
||||
}
|
||||
}
|
||||
Parent.template = xml`
|
||||
<div>
|
||||
<MyWidgetAdapter Component="MyWidget" a1="'Hello'" a2="'World'"/>
|
||||
</div>`;
|
||||
Parent.components = { MyWidgetAdapter };
|
||||
|
||||
const target = testUtils.prepareTarget();
|
||||
const parent = new Parent();
|
||||
await parent.mount(target);
|
||||
|
||||
assert.strictEqual(parent.el.innerHTML, '<div>Hello World!</div>');
|
||||
|
||||
parent.destroy();
|
||||
});
|
||||
|
||||
QUnit.test("sub widget and widgetArgs props", async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
const MyWidget = Widget.extend({
|
||||
init: function (parent, a1, a2) {
|
||||
this._super.apply(this, arguments);
|
||||
this.a1 = a1;
|
||||
this.a2 = a2;
|
||||
},
|
||||
start: function () {
|
||||
this.$el.text(`${this.a1} ${this.a2}!`);
|
||||
}
|
||||
});
|
||||
class Parent extends Component {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.MyWidget = MyWidget;
|
||||
}
|
||||
}
|
||||
Parent.template = xml`
|
||||
<div>
|
||||
<ComponentAdapter Component="MyWidget" a1="'Hello'" a2="'World'" widgetArgs="['Hello', 'World']"/>
|
||||
</div>`;
|
||||
Parent.components = { ComponentAdapter };
|
||||
|
||||
const target = testUtils.prepareTarget();
|
||||
const parent = new Parent();
|
||||
await parent.mount(target);
|
||||
|
||||
assert.strictEqual(parent.el.innerHTML, '<div>Hello World!</div>');
|
||||
|
||||
parent.destroy();
|
||||
});
|
||||
|
||||
QUnit.test("sub widget is updated when props change", async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
const MyWidget = Widget.extend({
|
||||
init: function (parent, name) {
|
||||
this._super.apply(this, arguments);
|
||||
this.name = name;
|
||||
},
|
||||
start: function () {
|
||||
this.render();
|
||||
},
|
||||
render: function () {
|
||||
this.$el.text(`Hello ${this.name}!`);
|
||||
},
|
||||
update: function (name) {
|
||||
this.name = name;
|
||||
},
|
||||
});
|
||||
class MyWidgetAdapter extends ComponentAdapter {
|
||||
update(nextProps) {
|
||||
return this.widget.update(nextProps.name);
|
||||
}
|
||||
render() {
|
||||
this.widget.render();
|
||||
}
|
||||
}
|
||||
class Parent extends Component {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.MyWidget = MyWidget;
|
||||
this.state = useState({
|
||||
name: "World",
|
||||
});
|
||||
}
|
||||
}
|
||||
Parent.template = xml`
|
||||
<div>
|
||||
<MyWidgetAdapter Component="MyWidget" name="state.name"/>
|
||||
</div>`;
|
||||
Parent.components = { MyWidgetAdapter };
|
||||
|
||||
const target = testUtils.prepareTarget();
|
||||
const parent = new Parent();
|
||||
await parent.mount(target);
|
||||
|
||||
assert.strictEqual(parent.el.innerHTML, '<div>Hello World!</div>');
|
||||
|
||||
parent.state.name = "GED";
|
||||
await nextTick();
|
||||
|
||||
assert.strictEqual(parent.el.innerHTML, '<div>Hello GED!</div>');
|
||||
|
||||
parent.destroy();
|
||||
});
|
||||
|
||||
QUnit.test("sub widget is updated when props change (async)", async function (assert) {
|
||||
assert.expect(7);
|
||||
|
||||
const prom = makeTestPromise();
|
||||
const MyWidget = Widget.extend({
|
||||
init: function (parent, name) {
|
||||
this._super.apply(this, arguments);
|
||||
this.name = name;
|
||||
},
|
||||
start: function () {
|
||||
this.render();
|
||||
},
|
||||
render: function () {
|
||||
this.$el.text(`Hello ${this.name}!`);
|
||||
assert.step('render');
|
||||
},
|
||||
update: function (name) {
|
||||
assert.step('update');
|
||||
this.name = name;
|
||||
},
|
||||
});
|
||||
class MyWidgetAdapter extends ComponentAdapter {
|
||||
update(nextProps) {
|
||||
return this.widget.update(nextProps.name);
|
||||
}
|
||||
render() {
|
||||
this.widget.render();
|
||||
}
|
||||
}
|
||||
class AsyncComponent extends Component {
|
||||
willUpdateProps() {
|
||||
return prom;
|
||||
}
|
||||
}
|
||||
AsyncComponent.template = xml`<div>Hi <t t-esc="props.name"/>!</div>`;
|
||||
class Parent extends Component {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.MyWidget = MyWidget;
|
||||
this.state = useState({
|
||||
name: "World",
|
||||
});
|
||||
}
|
||||
}
|
||||
Parent.template = xml`
|
||||
<div>
|
||||
<AsyncComponent name="state.name"/>
|
||||
<MyWidgetAdapter Component="MyWidget" name="state.name"/>
|
||||
</div>`;
|
||||
Parent.components = { AsyncComponent, MyWidgetAdapter };
|
||||
|
||||
const target = testUtils.prepareTarget();
|
||||
const parent = new Parent();
|
||||
await parent.mount(target);
|
||||
|
||||
assert.strictEqual(parent.el.innerHTML, '<div>Hi World!</div><div>Hello World!</div>');
|
||||
|
||||
parent.state.name = "GED";
|
||||
await nextTick();
|
||||
|
||||
assert.strictEqual(parent.el.innerHTML, '<div>Hi World!</div><div>Hello World!</div>');
|
||||
|
||||
prom.resolve();
|
||||
await nextTick();
|
||||
|
||||
assert.strictEqual(parent.el.innerHTML, '<div>Hi GED!</div><div>Hello GED!</div>');
|
||||
|
||||
assert.verifySteps(['render', 'update', 'render']);
|
||||
|
||||
parent.destroy();
|
||||
});
|
||||
|
||||
QUnit.test("sub widget methods are correctly called", async function (assert) {
|
||||
assert.expect(8);
|
||||
|
||||
const MyWidget = Widget.extend({
|
||||
on_attach_callback: function () {
|
||||
assert.step('on_attach_callback');
|
||||
},
|
||||
on_detach_callback: function () {
|
||||
assert.step('on_detach_callback');
|
||||
},
|
||||
destroy: function () {
|
||||
assert.step('destroy');
|
||||
this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
class Parent extends Component {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.MyWidget = MyWidget;
|
||||
}
|
||||
}
|
||||
Parent.template = xml`
|
||||
<div>
|
||||
<ComponentAdapter Component="MyWidget"/>
|
||||
</div>`;
|
||||
Parent.components = { ComponentAdapter };
|
||||
|
||||
const target = testUtils.prepareTarget();
|
||||
const parent = new Parent();
|
||||
await parent.mount(target);
|
||||
|
||||
assert.verifySteps(['on_attach_callback']);
|
||||
|
||||
parent.unmount();
|
||||
await parent.mount(target);
|
||||
|
||||
assert.verifySteps(['on_detach_callback', 'on_attach_callback']);
|
||||
|
||||
parent.destroy();
|
||||
|
||||
assert.verifySteps(['on_detach_callback', 'destroy']);
|
||||
});
|
||||
|
||||
QUnit.test("dynamic sub widget/component", async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
const MyWidget = Widget.extend({
|
||||
start: function () {
|
||||
this.$el.text('widget');
|
||||
},
|
||||
});
|
||||
class MyComponent extends Component {}
|
||||
MyComponent.template = xml`<div>component</div>`;
|
||||
class Parent extends Component {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.Children = [MyWidget, MyComponent];
|
||||
}
|
||||
}
|
||||
Parent.template = xml`
|
||||
<div>
|
||||
<ComponentAdapter t-foreach="Children" t-as="Child" Component="Child"/>
|
||||
</div>`;
|
||||
Parent.components = { ComponentAdapter };
|
||||
|
||||
const target = testUtils.prepareTarget();
|
||||
const parent = new Parent();
|
||||
await parent.mount(target);
|
||||
|
||||
assert.strictEqual(parent.el.innerHTML, '<div>widget</div><div>component</div>');
|
||||
|
||||
parent.destroy();
|
||||
});
|
||||
|
||||
QUnit.test("sub widget that triggers events", async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
let widget;
|
||||
const MyWidget = Widget.extend({
|
||||
init: function () {
|
||||
this._super.apply(this, arguments);
|
||||
widget = this;
|
||||
},
|
||||
});
|
||||
class Parent extends Component {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.MyWidget = MyWidget;
|
||||
}
|
||||
onSomeEvent(ev) {
|
||||
assert.step(ev.detail.value);
|
||||
assert.ok(ev.detail.__targetWidget instanceof MyWidget);
|
||||
}
|
||||
}
|
||||
Parent.template = xml`
|
||||
<div t-on-some-event="onSomeEvent">
|
||||
<ComponentAdapter Component="MyWidget"/>
|
||||
</div>`;
|
||||
Parent.components = { ComponentAdapter };
|
||||
|
||||
const target = testUtils.prepareTarget();
|
||||
const parent = new Parent();
|
||||
await parent.mount(target);
|
||||
|
||||
widget.trigger_up('some-event', { value: 'a' });
|
||||
widget.trigger_up('some_event', { value: 'b' }); // _ are converted to -
|
||||
|
||||
assert.verifySteps(['a', 'b']);
|
||||
|
||||
parent.destroy();
|
||||
});
|
||||
|
||||
QUnit.test("sub widget that calls _rpc", async function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
const MyWidget = Widget.extend({
|
||||
willStart: function () {
|
||||
return this._rpc({ route: 'some/route', params: { val: 2 } });
|
||||
},
|
||||
});
|
||||
class Parent extends Component {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.MyWidget = MyWidget;
|
||||
}
|
||||
}
|
||||
Parent.env = getMockedOwlEnv({
|
||||
mockRPC: function (route, args) {
|
||||
assert.step(`${route} ${args.val}`);
|
||||
return Promise.resolve();
|
||||
},
|
||||
});
|
||||
Parent.template = xml`
|
||||
<div>
|
||||
<ComponentAdapter Component="MyWidget"/>
|
||||
</div>`;
|
||||
Parent.components = { ComponentAdapter };
|
||||
|
||||
const target = testUtils.prepareTarget();
|
||||
const parent = new Parent();
|
||||
await parent.mount(target);
|
||||
|
||||
assert.strictEqual(parent.el.innerHTML, '<div></div>');
|
||||
assert.verifySteps(['some/route 2']);
|
||||
|
||||
parent.destroy();
|
||||
});
|
||||
|
||||
QUnit.test("sub widget that calls a service", async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
const MyWidget = Widget.extend({
|
||||
start: function () {
|
||||
let result;
|
||||
this.trigger_up('call_service', {
|
||||
service: 'math',
|
||||
method: 'sqrt',
|
||||
args: [9],
|
||||
callback: r => {
|
||||
result = r;
|
||||
},
|
||||
});
|
||||
assert.strictEqual(result, 3);
|
||||
},
|
||||
});
|
||||
class Parent extends Component {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.MyWidget = MyWidget;
|
||||
}
|
||||
}
|
||||
const env = getMockedOwlEnv();
|
||||
env.services.math = {
|
||||
sqrt: v => Math.sqrt(v),
|
||||
};
|
||||
Parent.env = env;
|
||||
Parent.template = xml`
|
||||
<div>
|
||||
<ComponentAdapter Component="MyWidget"/>
|
||||
</div>`;
|
||||
Parent.components = { ComponentAdapter };
|
||||
|
||||
const target = testUtils.prepareTarget();
|
||||
const parent = new Parent();
|
||||
await parent.mount(target);
|
||||
|
||||
parent.destroy();
|
||||
});
|
||||
|
||||
QUnit.test("sub widget that requests the session", async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
const MyWidget = Widget.extend({
|
||||
start: function () {
|
||||
assert.strictEqual(this.getSession().key, 'value');
|
||||
},
|
||||
});
|
||||
class Parent extends Component {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.MyWidget = MyWidget;
|
||||
}
|
||||
}
|
||||
Parent.env = getMockedOwlEnv({
|
||||
session: { key: 'value' },
|
||||
});
|
||||
Parent.template = xml`
|
||||
<div>
|
||||
<ComponentAdapter Component="MyWidget"/>
|
||||
</div>`;
|
||||
Parent.components = { ComponentAdapter };
|
||||
|
||||
const target = testUtils.prepareTarget();
|
||||
const parent = new Parent();
|
||||
await parent.mount(target);
|
||||
|
||||
parent.destroy();
|
||||
});
|
||||
|
||||
QUnit.test("sub widget that calls load_views", async function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
const MyWidget = Widget.extend({
|
||||
willStart: function () {
|
||||
return this.loadViews('some_model', { x: 2 }, [[false, 'list']]);
|
||||
},
|
||||
});
|
||||
class Parent extends Component {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.MyWidget = MyWidget;
|
||||
}
|
||||
}
|
||||
Parent.env = getMockedOwlEnv({
|
||||
mockRPC: function (route, args) {
|
||||
assert.strictEqual(route, '/web/dataset/call_kw/some_model');
|
||||
assert.deepEqual(args.kwargs.context, { x: 2 });
|
||||
assert.deepEqual(args.kwargs.views, [[false, 'list']]);
|
||||
return Promise.resolve();
|
||||
},
|
||||
});
|
||||
Parent.template = xml`
|
||||
<div>
|
||||
<ComponentAdapter Component="MyWidget"/>
|
||||
</div>`;
|
||||
Parent.components = { ComponentAdapter };
|
||||
|
||||
const target = testUtils.prepareTarget();
|
||||
const parent = new Parent();
|
||||
await parent.mount(target);
|
||||
|
||||
assert.strictEqual(parent.el.innerHTML, '<div></div>');
|
||||
|
||||
parent.destroy();
|
||||
});
|
||||
|
||||
QUnit.test("sub widgets in a t-if/t-else", async function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
const MyWidget1 = Widget.extend({
|
||||
start: function () {
|
||||
this.$el.text('Hi');
|
||||
},
|
||||
});
|
||||
const MyWidget2 = Widget.extend({
|
||||
start: function () {
|
||||
this.$el.text('Hello');
|
||||
},
|
||||
});
|
||||
class Parent extends Component {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.MyWidget1 = MyWidget1;
|
||||
this.MyWidget2 = MyWidget2;
|
||||
this.state = useState({
|
||||
flag: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
Parent.template = xml`
|
||||
<div>
|
||||
<ComponentAdapter t-if="state.flag" Component="MyWidget1"/>
|
||||
<ComponentAdapter t-else="" Component="MyWidget2"/>
|
||||
</div>`;
|
||||
Parent.components = { ComponentAdapter };
|
||||
|
||||
const target = testUtils.prepareTarget();
|
||||
const parent = new Parent();
|
||||
await parent.mount(target);
|
||||
|
||||
assert.strictEqual(parent.el.innerHTML, '<div>Hi</div>');
|
||||
|
||||
parent.state.flag = false;
|
||||
await nextTick();
|
||||
|
||||
assert.strictEqual(parent.el.innerHTML, '<div>Hello</div>');
|
||||
|
||||
parent.state.flag = true;
|
||||
await nextTick();
|
||||
|
||||
assert.strictEqual(parent.el.innerHTML, '<div>Hi</div>');
|
||||
|
||||
parent.destroy();
|
||||
});
|
||||
|
||||
QUnit.test("sub widget in a t-if, and events", async function (assert) {
|
||||
assert.expect(6);
|
||||
|
||||
let myWidget;
|
||||
const MyWidget = Widget.extend({
|
||||
start: function () {
|
||||
myWidget = this;
|
||||
this.$el.text('Hi');
|
||||
},
|
||||
});
|
||||
class Parent extends Component {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.MyWidget = MyWidget;
|
||||
this.state = useState({
|
||||
flag: true,
|
||||
});
|
||||
}
|
||||
onSomeEvent(ev) {
|
||||
assert.step(ev.detail.value);
|
||||
}
|
||||
}
|
||||
Parent.template = xml`
|
||||
<div t-on-some-event="onSomeEvent">
|
||||
<ComponentAdapter t-if="state.flag" Component="MyWidget"/>
|
||||
</div>`;
|
||||
Parent.components = { ComponentAdapter };
|
||||
|
||||
const target = testUtils.prepareTarget();
|
||||
const parent = new Parent();
|
||||
await parent.mount(target);
|
||||
|
||||
assert.strictEqual(parent.el.innerHTML, '<div>Hi</div>');
|
||||
myWidget.trigger_up('some-event', { value: 'a' });
|
||||
|
||||
parent.state.flag = false;
|
||||
await nextTick();
|
||||
|
||||
assert.strictEqual(parent.el.innerHTML, '');
|
||||
myWidget.trigger_up('some-event', { value: 'b' });
|
||||
|
||||
parent.state.flag = true;
|
||||
await nextTick();
|
||||
|
||||
assert.strictEqual(parent.el.innerHTML, '<div>Hi</div>');
|
||||
myWidget.trigger_up('some-event', { value: 'c' });
|
||||
|
||||
assert.verifySteps(['a', 'c']);
|
||||
|
||||
parent.destroy();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -380,6 +380,8 @@
|
||||
<script type="text/javascript" src="/web/static/src/js/widgets/attach_document.js"></script>
|
||||
<script type="text/javascript" src="/web/static/src/js/fields/signature.js"></script>
|
||||
|
||||
<script type="text/javascript" src="/web/static/src/js/owl_compatibility.js"></script>
|
||||
|
||||
<script type="text/javascript" src="/web/static/src/js/report/utils.js"/>
|
||||
<script type="text/javascript" src="/web/static/src/js/report/client_action.js"/>
|
||||
<link rel="stylesheet" type="text/scss" href="/web/static/src/scss/report_backend.scss"/>
|
||||
@@ -726,6 +728,8 @@
|
||||
<script type="text/javascript" src="/web/static/tests/tools/debug_manager_tests.js"/>
|
||||
|
||||
<script type="text/javascript" src="/web/static/tests/helpers/test_utils_tests.js"></script>
|
||||
|
||||
<script type="text/javascript" src="/web/static/tests/owl_compatibility_tests.js"></script>
|
||||
</t>
|
||||
|
||||
<div id="qunit"/>
|
||||
|
||||
Reference in New Issue
Block a user