diff options
Diffstat (limited to 'addons/web/static/tests')
| -rw-r--r-- | addons/web/static/tests/helpers/test_env.js | 21 | ||||
| -rw-r--r-- | addons/web/static/tests/helpers/test_utils.js | 1 | ||||
| -rw-r--r-- | addons/web/static/tests/helpers/test_utils_mock.js | 76 | ||||
| -rw-r--r-- | addons/web/static/tests/owl_compatibility_tests.js | 674 |
4 files changed, 763 insertions, 9 deletions
diff --git a/addons/web/static/tests/helpers/test_env.js b/addons/web/static/tests/helpers/test_env.js index 417350ef65b..fabe52ff444 100644 --- a/addons/web/static/tests/helpers/test_env.js +++ b/addons/web/static/tests/helpers/test_env.js @@ -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; } diff --git a/addons/web/static/tests/helpers/test_utils.js b/addons/web/static/tests/helpers/test_utils.js index 447c39240a7..a9c07973566 100644 --- a/addons/web/static/tests/helpers/test_utils.js +++ b/addons/web/static/tests/helpers/test_utils.js @@ -121,6 +121,7 @@ return Promise.all([ return { mock: { addMockEnvironment: testUtilsMock.addMockEnvironment, + getMockedOwlEnv: testUtilsMock.getMockedOwlEnv, intercept: testUtilsMock.intercept, patch: testUtilsMock.patch, patchDate: testUtilsMock.patchDate, diff --git a/addons/web/static/tests/helpers/test_utils_mock.js b/addons/web/static/tests/helpers/test_utils_mock.js index b4c722b7de1..fe45bbb0e8e 100644 --- a/addons/web/static/tests/helpers/test_utils_mock.js +++ b/addons/web/static/tests/helpers/test_utils_mock.js @@ -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; //------------------------------------------------------------------------------ @@ -360,6 +361,66 @@ function fieldsViewGet(server, params) { } /** + * 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, diff --git a/addons/web/static/tests/owl_compatibility_tests.js b/addons/web/static/tests/owl_compatibility_tests.js new file mode 100644 index 00000000000..b82eeb04857 --- /dev/null +++ b/addons/web/static/tests/owl_compatibility_tests.js @@ -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(); + }); + }); +}); |
