diff --git a/addons/web/static/tests/owl_compatibility_tests.js b/addons/web/static/tests/owl_compatibility_tests.js index b82eeb04857026acc00f381870431a5eeec32d3f..5aca69326542e5b93236f02bc1202841b5eda6ae 100644 --- a/addons/web/static/tests/owl_compatibility_tests.js +++ b/addons/web/static/tests/owl_compatibility_tests.js @@ -1,7 +1,7 @@ odoo.define('web.OwlCompatibilityTests', function (require) { "use strict"; - const { ComponentAdapter } = require('web.OwlCompatibility'); + const { ComponentAdapter, ComponentWrapper, WidgetAdapterMixin } = require('web.OwlCompatibility'); const testUtils = require('web.test_utils'); const Widget = require('web.Widget'); @@ -12,6 +12,14 @@ odoo.define('web.OwlCompatibilityTests', function (require) { const { Component, tags, useState } = owl; const { xml } = tags; + + const WidgetAdapter = Widget.extend(WidgetAdapterMixin, { + destroy() { + this._super(...arguments); + WidgetAdapterMixin.destroy.call(this, ...arguments); + }, + }); + QUnit.module("Owl Compatibility", function () { QUnit.module("ComponentAdapter"); @@ -670,5 +678,394 @@ odoo.define('web.OwlCompatibilityTests', function (require) { parent.destroy(); }); + + QUnit.module('WidgetAdapterMixin and ComponentWrapper'); + + QUnit.test("widget with sub component", async function (assert) { + assert.expect(1); + + class MyComponent extends Component {} + MyComponent.template = xml`<div>Component</div>`; + const MyWidget = WidgetAdapter.extend({ + start() { + const component = new ComponentWrapper(this, MyComponent, {}); + return component.mount(this.el); + } + }); + + const target = testUtils.prepareTarget(); + const widget = new MyWidget(); + await widget.appendTo(target); + + assert.strictEqual(widget.el.innerHTML, '<div>Component</div>'); + + widget.destroy(); + }); + + QUnit.test("sub component hooks are correctly called", async function (assert) { + assert.expect(14); + + let component; + class MyComponent extends Component { + constructor(parent) { + super(parent); + assert.step("init"); + } + async willStart() { + assert.step("willStart"); + } + mounted() { + assert.step("mounted"); + } + willUnmount() { + assert.step("willUnmount"); + } + __destroy() { + super.__destroy(); + assert.step("__destroy"); + } + } + MyComponent.template = xml`<div>Component</div>`; + const MyWidget = WidgetAdapter.extend({ + start() { + component = new ComponentWrapper(this, MyComponent, {}); + return component.mount(this.el); + } + }); + + const target = testUtils.prepareTarget(); + const widget = new MyWidget(); + await widget.appendTo(target); + + assert.verifySteps(['init', 'willStart', 'mounted']); + assert.ok(component.__owl__.isMounted); + + widget.$el.detach(); + widget.on_detach_callback(); + + assert.verifySteps(['willUnmount']); + assert.ok(!component.__owl__.isMounted); + + widget.$el.appendTo(target); + widget.on_attach_callback(); + + assert.verifySteps(['mounted']); + assert.ok(component.__owl__.isMounted); + + widget.destroy(); + + assert.verifySteps(['willUnmount', '__destroy']); + }); + + QUnit.test("isMounted with several sub components", async function (assert) { + assert.expect(11); + + let c1; + let c2; + class MyComponent extends Component {} + MyComponent.template = xml`<div>Component <t t-esc="props.id"/></div>`; + const MyWidget = WidgetAdapter.extend({ + start() { + c1 = new ComponentWrapper(this, MyComponent, {id: 1}); + c2 = new ComponentWrapper(this, MyComponent, {id: 2}); + return Promise.all([c1.mount(this.el), c2.mount(this.el)]); + } + }); + + const target = testUtils.prepareTarget(); + const widget = new MyWidget(); + await widget.appendTo(target); + + assert.strictEqual(widget.el.innerHTML, '<div>Component 1</div><div>Component 2</div>'); + assert.ok(c1.__owl__.isMounted); + assert.ok(c2.__owl__.isMounted); + + widget.$el.detach(); + widget.on_detach_callback(); + + assert.ok(!c1.__owl__.isMounted); + assert.ok(!c2.__owl__.isMounted); + + widget.$el.appendTo(target); + widget.on_attach_callback(); + + assert.ok(c1.__owl__.isMounted); + assert.ok(c2.__owl__.isMounted); + + widget.destroy(); + + assert.ok(!c1.__owl__.isMounted); + assert.ok(!c2.__owl__.isMounted); + assert.ok(c1.__owl__.isDestroyed); + assert.ok(c2.__owl__.isDestroyed); + }); + + QUnit.test("isMounted with several levels of sub components", async function (assert) { + assert.expect(6); + + let child; + class MyChildComponent extends Component { + constructor() { + super(...arguments); + child = this; + } + } + MyChildComponent.template = xml`<div>child</div>`; + class MyComponent extends Component {} + MyComponent.template = xml`<div><MyChildComponent/></div>`; + MyComponent.components = { MyChildComponent }; + const MyWidget = WidgetAdapter.extend({ + start() { + let component = new ComponentWrapper(this, MyComponent, {}); + return component.mount(this.el); + } + }); + + const target = testUtils.prepareTarget(); + const widget = new MyWidget(); + await widget.appendTo(target); + + assert.strictEqual(widget.el.innerHTML, '<div><div>child</div></div>'); + assert.ok(child.__owl__.isMounted); + + widget.$el.detach(); + widget.on_detach_callback(); + + assert.ok(!child.__owl__.isMounted); + + widget.$el.appendTo(target); + widget.on_attach_callback(); + + assert.ok(child.__owl__.isMounted); + + widget.destroy(); + + assert.ok(!child.__owl__.isMounted); + assert.ok(child.__owl__.isDestroyed); + }); + + QUnit.test("sub component can be updated (in DOM)", async function (assert) { + assert.expect(2); + + class MyComponent extends Component {} + MyComponent.template = xml`<div>Component <t t-esc="props.val"/></div>`; + const MyWidget = WidgetAdapter.extend({ + start() { + this.component = new ComponentWrapper(this, MyComponent, {val: 1}); + return this.component.mount(this.el); + }, + update() { + return this.component.update({val: 2}); + }, + }); + + const target = testUtils.prepareTarget(); + const widget = new MyWidget(); + await widget.appendTo(target); + + assert.strictEqual(widget.el.innerHTML, '<div>Component 1</div>'); + + await widget.update(); + + assert.strictEqual(widget.el.innerHTML, '<div>Component 2</div>'); + + widget.destroy(); + }); + + QUnit.test("sub component can be updated (not in DOM)", async function (assert) { + assert.expect(4); + + class MyComponent extends Component {} + MyComponent.template = xml`<div>Component <t t-esc="props.val"/></div>`; + const MyWidget = WidgetAdapter.extend({ + start() { + this.component = new ComponentWrapper(this, MyComponent, {val: 1}); + return this.component.mount(this.el); + }, + update() { + return this.component.update({val: 2}); + }, + }); + + const target = testUtils.prepareTarget(); + const widget = new MyWidget(); + await widget.appendTo(target); + + assert.strictEqual(widget.el.innerHTML, '<div>Component 1</div>'); + + widget.$el.detach(); + widget.on_detach_callback(); + + assert.ok(!widget.component.__owl__.isMounted); + + await widget.update(); + + widget.$el.appendTo(target); + widget.on_attach_callback(); + + assert.ok(widget.component.__owl__.isMounted); + assert.strictEqual(widget.el.innerHTML, '<div>Component 2</div>'); + + widget.destroy(); + }); + + QUnit.test("update a destroyed sub component", async function (assert) { + assert.expect(1); + + class MyComponent extends Component {} + MyComponent.template = xml`<div>Component <t t-esc="props.val"/></div>`; + const MyWidget = WidgetAdapter.extend({ + start() { + this.component = new ComponentWrapper(this, MyComponent, {val: 1}); + return this.component.mount(this.el); + }, + update() { + this.component.update({val: 2}); + }, + }); + + const target = testUtils.prepareTarget(); + const widget = new MyWidget(); + await widget.appendTo(target); + + assert.strictEqual(widget.el.innerHTML, '<div>Component 1</div>'); + + widget.destroy(); + + widget.update(); // should not crash + }); + + QUnit.module('Several layers of legacy widgets and Owl components'); + + QUnit.test("Owl over legacy over Owl", async function (assert) { + assert.expect(7); + + let leafComponent; + class MyComponent extends Component {} + MyComponent.template = xml`<span>Component</span>`; + const MyWidget = WidgetAdapter.extend({ + custom_events: { + widget_event: function (ev) { + assert.step(`[widget] widget-event ${ev.data.value}`); + }, + both_event: function (ev) { + assert.step(`[widget] both-event ${ev.data.value}`); + if (ev.data.value === 4) { + ev.stopPropagation(); + } + } + }, + start() { + leafComponent = new ComponentWrapper(this, MyComponent, {}); + return leafComponent.mount(this.el); + }, + }); + class Parent extends Component { + constructor() { + super(...arguments); + this.MyWidget = MyWidget; + } + onRootEvent(ev) { + assert.step(`[root] root-event ${ev.detail.value}`); + } + onBothEvent(ev) { + assert.step(`[root] both-event ${ev.detail.value}`); + } + } + Parent.template = xml` + <div t-on-root-event="onRootEvent" t-on-both-event="onBothEvent"> + <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><span>Component</span></div>'); + + leafComponent.trigger('root-event', { value: 1 }); + leafComponent.trigger('widget-event', { value: 2 }); + leafComponent.trigger('both-event', { value: 3 }); + leafComponent.trigger('both-event', { value: 4 }); // will be stopped by widget + + assert.verifySteps([ + '[root] root-event 1', + '[widget] widget-event 2', + '[widget] both-event 3', + '[root] both-event 3', + '[widget] both-event 4', + ]); + + parent.destroy(); + }); + + QUnit.test("Legacy over Owl over legacy", async function (assert) { + assert.expect(7); + + let leafWidget; + const MyWidget = Widget.extend({ + start: function () { + leafWidget = this; + this.$el.text('Widget'); + } + }); + class MyComponent extends Component { + constructor() { + super(...arguments); + this.MyWidget = MyWidget; + } + onComponentEvent(ev) { + assert.step(`[component] component-event ${ev.detail.value}`); + } + onBothEvent(ev) { + assert.step(`[component] both-event ${ev.detail.value}`); + if (ev.detail.value === 4) { + ev.stopPropagation(); + } + } + } + MyComponent.template = xml` + <span t-on-component-event="onComponentEvent" t-on-both-event="onBothEvent"> + <ComponentAdapter Component="MyWidget"/> + </span>`; + MyComponent.components = { ComponentAdapter }; + const Parent = WidgetAdapter.extend({ + custom_events: { + root_event: function (ev) { + assert.step(`[root] root-event ${ev.data.value}`); + }, + both_event: function (ev) { + assert.step(`[root] both-event ${ev.data.value}`); + }, + }, + start() { + const component = new ComponentWrapper(this, MyComponent, {}); + return component.mount(this.el); + } + }); + + const target = testUtils.prepareTarget(); + const parent = new Parent(); + await parent.appendTo(target); + + assert.strictEqual(parent.el.innerHTML, '<span><div>Widget</div></span>'); + + leafWidget.trigger_up('root-event', { value: 1 }); + leafWidget.trigger_up('component-event', { value: 2 }); + leafWidget.trigger_up('both-event', { value: 3 }); + leafWidget.trigger_up('both-event', { value: 4 }); // will be stopped by component + + assert.verifySteps([ + '[root] root-event 1', + '[component] component-event 2', + '[component] both-event 3', + '[root] both-event 3', + '[component] both-event 4', + ]); + + parent.destroy(); + }); }); });