[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:
Aaron Bohy
2020-01-21 07:17:31 +00:00
parent 45246a3b96
commit 671a5471cd
7 changed files with 1090 additions and 15 deletions
+65 -6
View File
@@ -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,
};
});
+19 -2
View File
@@ -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();
});
});
});
+4
View File
@@ -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"/>