mirror of
https://github.com/bringout/oca-ocb-core.git
synced 2026-04-20 10:12:04 +02:00
vanilla 19.0
This commit is contained in:
parent
991d2234ca
commit
d1963a3c3a
3066 changed files with 1651266 additions and 922560 deletions
|
|
@ -1,240 +0,0 @@
|
|||
odoo.define('web.component_extension_tests', function (require) {
|
||||
"use strict";
|
||||
|
||||
const makeTestEnvironment = require("web.test_env");
|
||||
const testUtils = require("web.test_utils");
|
||||
const { destroy, getFixture, mount } = require("@web/../tests/helpers/utils");
|
||||
const { LegacyComponent } = require("@web/legacy/legacy_component");
|
||||
|
||||
const { xml } = owl;
|
||||
const { useListener } = require("@web/core/utils/hooks");
|
||||
|
||||
let target;
|
||||
QUnit.module("web", { beforeEach() { target = getFixture(); }}, function () {
|
||||
QUnit.module("Component Extension");
|
||||
|
||||
QUnit.test("Component destroyed while performing successful RPC", async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
class Parent extends LegacyComponent {}
|
||||
Parent.template = xml`<div/>`;
|
||||
|
||||
const env = makeTestEnvironment({}, () => Promise.resolve());
|
||||
|
||||
const parent = await mount(Parent, target, { env });
|
||||
|
||||
parent.rpc({}).then(() => { throw new Error(); });
|
||||
destroy(parent);
|
||||
|
||||
await testUtils.nextTick();
|
||||
|
||||
assert.ok(true, "Promise should still be pending");
|
||||
});
|
||||
|
||||
QUnit.test("Component destroyed while performing failed RPC", async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
class Parent extends LegacyComponent {}
|
||||
Parent.template = xml`<div/>`;
|
||||
|
||||
const env = makeTestEnvironment({}, () => Promise.reject());
|
||||
const parent = await mount(Parent, target, { env });
|
||||
|
||||
parent.rpc({}).catch(() => { throw new Error(); });
|
||||
destroy(parent);
|
||||
|
||||
await testUtils.nextTick();
|
||||
|
||||
assert.ok(true, "Promise should still be pending");
|
||||
});
|
||||
|
||||
QUnit.module("Custom Hooks");
|
||||
|
||||
QUnit.test("useListener handler type", async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
class Parent extends LegacyComponent {
|
||||
setup() {
|
||||
useListener('custom1', '_onCustom1');
|
||||
}
|
||||
}
|
||||
Parent.template = xml`<div/>`;
|
||||
const env = makeTestEnvironment({}, () => Promise.reject());
|
||||
|
||||
try {
|
||||
await mount(Parent, target, { env })
|
||||
} catch (e) {
|
||||
assert.strictEqual(e.message, 'The handler must be a function');
|
||||
}
|
||||
});
|
||||
|
||||
QUnit.test("useListener in inheritance setting", async function (assert) {
|
||||
assert.expect(12);
|
||||
const env = makeTestEnvironment({}, () => Promise.reject());
|
||||
|
||||
class Parent extends LegacyComponent {
|
||||
setup() {
|
||||
useListener('custom1', this._onCustom1);
|
||||
useListener('custom2', this._onCustom2);
|
||||
}
|
||||
_onCustom1() {
|
||||
assert.step(`${this.constructor.name} custom1`);
|
||||
}
|
||||
_onCustom2() {
|
||||
assert.step('parent custom2');
|
||||
}
|
||||
}
|
||||
Parent.template = xml`<div/>`;
|
||||
|
||||
class Child extends Parent {
|
||||
setup() {
|
||||
super.setup();
|
||||
useListener('custom2', this._onCustom2);
|
||||
useListener('custom3', this._onCustom3);
|
||||
}
|
||||
_onCustom2() {
|
||||
assert.step('overriden custom2');
|
||||
}
|
||||
_onCustom3() {
|
||||
assert.step('child custom3');
|
||||
}
|
||||
}
|
||||
|
||||
const parent = await mount(Parent, target, { env });
|
||||
const child = await mount(Child, target, { env });
|
||||
|
||||
parent.trigger('custom1');
|
||||
assert.verifySteps(['Parent custom1']);
|
||||
parent.trigger('custom2');
|
||||
assert.verifySteps(['parent custom2']);
|
||||
parent.trigger('custom3');
|
||||
assert.verifySteps([]);
|
||||
|
||||
child.trigger('custom1');
|
||||
assert.verifySteps(['Child custom1']);
|
||||
// There are two handlers for that one (Parent and Child)
|
||||
// Although the handler is overriden in Child
|
||||
child.trigger('custom2');
|
||||
assert.verifySteps(['overriden custom2', 'overriden custom2']);
|
||||
child.trigger('custom3');
|
||||
assert.verifySteps(['child custom3']);
|
||||
});
|
||||
|
||||
QUnit.test("useListener with native JS selector", async function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
class Parent extends LegacyComponent {
|
||||
setup() {
|
||||
useListener('custom1', 'div .custom-class', this._onCustom1);
|
||||
}
|
||||
_onCustom1() {
|
||||
assert.step(`custom1`);
|
||||
}
|
||||
}
|
||||
Parent.template = xml`
|
||||
<div>
|
||||
<p>no trigger</p>
|
||||
<h1 class="custom-class">triggers</h1>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const env = makeTestEnvironment({}, () => Promise.reject());
|
||||
const parent = await mount(Parent, target, { env });
|
||||
|
||||
parent.el.querySelector('p').dispatchEvent(new Event('custom1', {bubbles: true}));
|
||||
assert.verifySteps([]);
|
||||
parent.el.querySelector('h1').dispatchEvent(new Event('custom1', {bubbles: true}));
|
||||
assert.verifySteps(['custom1']);
|
||||
});
|
||||
|
||||
QUnit.test("useListener with native JS selector delegation", async function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
class Parent extends LegacyComponent {
|
||||
setup() {
|
||||
useListener('custom1', '.custom-class', this._onCustom1);
|
||||
}
|
||||
_onCustom1() {
|
||||
assert.step(`custom1`);
|
||||
}
|
||||
}
|
||||
Parent.template = xml`
|
||||
<div>
|
||||
<p>no trigger</p>
|
||||
<h1 class="custom-class"><h2>triggers</h2></h1>
|
||||
</div>`;
|
||||
|
||||
target.classList.add('custom-class');
|
||||
const env = makeTestEnvironment({}, () => Promise.reject());
|
||||
|
||||
const parent = await mount(Parent, target, { env });
|
||||
|
||||
parent.el.querySelector('p').dispatchEvent(new Event('custom1', {bubbles: true}));
|
||||
assert.verifySteps([]);
|
||||
parent.el.querySelector('h2').dispatchEvent(new Event('custom1', {bubbles: true}));
|
||||
assert.verifySteps(['custom1']);
|
||||
target.classList.remove('custom-class');
|
||||
});
|
||||
|
||||
QUnit.test("useListener with capture option", async function (assert) {
|
||||
assert.expect(7);
|
||||
|
||||
class Leaf extends LegacyComponent {
|
||||
setup() {
|
||||
useListener('custom1', this._onCustom1);
|
||||
}
|
||||
_onCustom1() {
|
||||
assert.step(`${this.constructor.name} custom1`);
|
||||
}
|
||||
}
|
||||
Leaf.template = xml`<div class="leaf"/>`;
|
||||
|
||||
class Root extends LegacyComponent {
|
||||
setup() {
|
||||
useListener('custom1', this._onCustom1, { capture: true });
|
||||
}
|
||||
_onCustom1(event) {
|
||||
assert.step(`${this.constructor.name} custom1`);
|
||||
const detail = event.detail;
|
||||
if (detail && detail.stopMe) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
}
|
||||
}
|
||||
Root.template = xml`<div class="root"><Leaf/></div>`;
|
||||
Root.components = { Leaf };
|
||||
|
||||
await mount(Root, target);
|
||||
|
||||
const rootNode = document.body.querySelector('.root');
|
||||
const leafNode = document.body.querySelector('.leaf');
|
||||
rootNode.dispatchEvent(new CustomEvent('custom1', {
|
||||
bubbles: true,
|
||||
cancelable: true
|
||||
}));
|
||||
assert.verifySteps(['Root custom1']);
|
||||
|
||||
// Dispatch custom1 on the leaf element.
|
||||
// Since we listen in the capture phase, Root is first triggered.
|
||||
// The event is stopped there.
|
||||
leafNode.dispatchEvent(new CustomEvent('custom1', {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
detail: {
|
||||
stopMe: true
|
||||
},
|
||||
}));
|
||||
assert.verifySteps(['Root custom1']);
|
||||
|
||||
// Same as before, except this time we don't stop the event
|
||||
leafNode.dispatchEvent(new CustomEvent('custom1', {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
detail: {
|
||||
stopMe: false
|
||||
}
|
||||
}));
|
||||
assert.verifySteps(['Root custom1', 'Leaf custom1']);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,264 +0,0 @@
|
|||
odoo.define('web.action_menus_tests', function (require) {
|
||||
"use strict";
|
||||
|
||||
const ActionMenus = require('web.ActionMenus');
|
||||
const Registry = require('web.Registry');
|
||||
const testUtils = require('web.test_utils');
|
||||
|
||||
const { Component } = owl;
|
||||
const { createComponent } = testUtils;
|
||||
|
||||
QUnit.module('Components', {
|
||||
beforeEach() {
|
||||
this.action = {
|
||||
res_model: 'hobbit',
|
||||
};
|
||||
this.view = {
|
||||
// needed by google_drive module, makes sense to give a view anyway.
|
||||
type: 'form',
|
||||
};
|
||||
this.props = {
|
||||
activeIds: [23],
|
||||
context: {},
|
||||
items: {
|
||||
action: [
|
||||
{ action: { id: 1 }, name: "What's taters, precious ?", id: 1 },
|
||||
],
|
||||
print: [
|
||||
{ action: { id: 2 }, name: "Po-ta-toes", id: 2 },
|
||||
],
|
||||
other: [
|
||||
{ description: "Boil'em", callback() { } },
|
||||
{ description: "Mash'em", callback() { } },
|
||||
{ description: "Stick'em in a stew", url: '#stew' },
|
||||
],
|
||||
},
|
||||
};
|
||||
// Patch the registry of the action menus
|
||||
this.actionMenusRegistry = ActionMenus.registry;
|
||||
ActionMenus.registry = new Registry();
|
||||
},
|
||||
afterEach() {
|
||||
ActionMenus.registry = this.actionMenusRegistry;
|
||||
},
|
||||
}, function () {
|
||||
|
||||
QUnit.module('ActionMenus');
|
||||
|
||||
QUnit.test('basic interactions', async function (assert) {
|
||||
assert.expect(10);
|
||||
|
||||
const actionMenus = await createComponent(ActionMenus, {
|
||||
env: {
|
||||
action: this.action,
|
||||
view: this.view,
|
||||
},
|
||||
props: this.props,
|
||||
});
|
||||
|
||||
const dropdowns = actionMenus.el.getElementsByClassName('dropdown');
|
||||
assert.strictEqual(dropdowns.length, 2, "ActionMenus should contain 2 menus");
|
||||
assert.strictEqual(dropdowns[0].querySelector('.o_dropdown_title').innerText.trim(), "Print");
|
||||
assert.strictEqual(dropdowns[1].querySelector('.o_dropdown_title').innerText.trim(), "Action");
|
||||
assert.containsNone(actionMenus, '.o-dropdown-menu');
|
||||
|
||||
await testUtils.controlPanel.toggleActionMenu(actionMenus, "Action");
|
||||
|
||||
assert.containsOnce(actionMenus, '.o-dropdown-menu');
|
||||
assert.containsN(actionMenus, '.o-dropdown-menu .o_menu_item', 4);
|
||||
const actionsTexts = [...dropdowns[1].querySelectorAll('.o_menu_item')].map(el => el.innerText.trim());
|
||||
assert.deepEqual(actionsTexts, [
|
||||
"Boil'em",
|
||||
"Mash'em",
|
||||
"Stick'em in a stew",
|
||||
"What's taters, precious ?",
|
||||
], "callbacks should appear before actions");
|
||||
|
||||
await testUtils.controlPanel.toggleActionMenu(actionMenus, "Print");
|
||||
|
||||
assert.containsOnce(actionMenus, '.o-dropdown-menu');
|
||||
assert.containsN(actionMenus, '.o-dropdown-menu .o_menu_item', 1);
|
||||
|
||||
await testUtils.controlPanel.toggleActionMenu(actionMenus, "Print");
|
||||
|
||||
assert.containsNone(actionMenus, '.o-dropdown-menu');
|
||||
});
|
||||
|
||||
QUnit.test("empty action menus", async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
ActionMenus.registry.add("test", { Component, getProps: () => false });
|
||||
this.props.items = {};
|
||||
|
||||
const actionMenus = await createComponent(ActionMenus, {
|
||||
env: {
|
||||
action: this.action,
|
||||
view: this.view,
|
||||
},
|
||||
props: this.props,
|
||||
});
|
||||
|
||||
assert.containsNone(actionMenus, ".o_cp_action_menus > *");
|
||||
});
|
||||
|
||||
QUnit.test('execute action', async function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
const actionMenus = await createComponent(ActionMenus, {
|
||||
env: {
|
||||
action: this.action,
|
||||
view: this.view,
|
||||
},
|
||||
props: this.props,
|
||||
intercepts: {
|
||||
'do-action': ev => assert.step('do-action'),
|
||||
},
|
||||
async mockRPC(route, args) {
|
||||
switch (route) {
|
||||
case '/web/action/load':
|
||||
const expectedContext = {
|
||||
active_id: 23,
|
||||
active_ids: [23],
|
||||
active_model: 'hobbit',
|
||||
};
|
||||
assert.deepEqual(args.context, expectedContext);
|
||||
assert.step('load-action');
|
||||
return { context: {}, flags: {} };
|
||||
default:
|
||||
return this._super(...arguments);
|
||||
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
await testUtils.controlPanel.toggleActionMenu(actionMenus, "Action");
|
||||
await testUtils.controlPanel.toggleMenuItem(actionMenus, "What's taters, precious ?");
|
||||
|
||||
assert.verifySteps(['load-action', 'do-action']);
|
||||
});
|
||||
|
||||
QUnit.test('execute callback action', async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
const callbackPromise = testUtils.makeTestPromise();
|
||||
this.props.items.other[0].callback = function (items) {
|
||||
assert.strictEqual(items.length, 1);
|
||||
assert.strictEqual(items[0].description, "Boil'em");
|
||||
callbackPromise.resolve();
|
||||
};
|
||||
|
||||
const actionMenus = await createComponent(ActionMenus, {
|
||||
env: {
|
||||
action: this.action,
|
||||
view: this.view,
|
||||
},
|
||||
props: this.props,
|
||||
async mockRPC(route, args) {
|
||||
switch (route) {
|
||||
case '/web/action/load':
|
||||
throw new Error("No action should be loaded.");
|
||||
default:
|
||||
return this._super(...arguments);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
await testUtils.controlPanel.toggleActionMenu(actionMenus, "Action");
|
||||
await testUtils.controlPanel.toggleMenuItem(actionMenus, "Boil'em");
|
||||
|
||||
await callbackPromise;
|
||||
});
|
||||
|
||||
QUnit.test('execute print action', async function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
const actionMenus = await createComponent(ActionMenus, {
|
||||
env: {
|
||||
action: this.action,
|
||||
view: this.view,
|
||||
},
|
||||
intercepts: {
|
||||
'do-action': ev => assert.step('do-action'),
|
||||
},
|
||||
props: this.props,
|
||||
async mockRPC(route, args) {
|
||||
switch (route) {
|
||||
case '/web/action/load':
|
||||
const expectedContext = {
|
||||
active_id: 23,
|
||||
active_ids: [23],
|
||||
active_model: 'hobbit',
|
||||
};
|
||||
assert.deepEqual(args.context, expectedContext);
|
||||
assert.step('load-action');
|
||||
return { context: {}, flags: {} };
|
||||
default:
|
||||
return this._super(...arguments);
|
||||
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
await testUtils.controlPanel.toggleActionMenu(actionMenus, "Print");
|
||||
await testUtils.controlPanel.toggleMenuItem(actionMenus, "Po-ta-toes");
|
||||
|
||||
assert.verifySteps(['load-action', 'do-action']);
|
||||
});
|
||||
|
||||
QUnit.test('execute url action', async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
const actionMenus = await createComponent(ActionMenus, {
|
||||
env: {
|
||||
action: this.action,
|
||||
services: {
|
||||
navigate(url) {
|
||||
assert.step(url);
|
||||
},
|
||||
},
|
||||
view: this.view,
|
||||
},
|
||||
props: this.props,
|
||||
async mockRPC(route, args) {
|
||||
switch (route) {
|
||||
case '/web/action/load':
|
||||
throw new Error("No action should be loaded.");
|
||||
default:
|
||||
return this._super(...arguments);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
await testUtils.controlPanel.toggleActionMenu(actionMenus, "Action");
|
||||
await testUtils.controlPanel.toggleMenuItem(actionMenus, "Stick'em in a stew");
|
||||
|
||||
assert.verifySteps(['#stew']);
|
||||
});
|
||||
|
||||
QUnit.test('execute action with context', async function (assert) {
|
||||
assert.expect(1);
|
||||
const actionMenus = await createComponent(ActionMenus, {
|
||||
env: {
|
||||
action: this.action,
|
||||
view: this.view,
|
||||
},
|
||||
props: {
|
||||
...this.props,
|
||||
isDomainSelected: true,
|
||||
context: {
|
||||
allowed_company_ids: [112],
|
||||
},
|
||||
},
|
||||
async mockRPC(route, args) {
|
||||
if (route === "/web/dataset/call_kw/hobbit/search"){
|
||||
assert.deepEqual(args.kwargs.context, { allowed_company_ids: [112] }, "The kwargs should contains the right context");
|
||||
}
|
||||
return this._super(...arguments);
|
||||
},
|
||||
});
|
||||
|
||||
await testUtils.controlPanel.toggleActionMenu(actionMenus, "Action");
|
||||
await testUtils.controlPanel.toggleMenuItem(actionMenus, "What's taters, precious ?");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
odoo.define('web.custom_checkbox_tests', function (require) {
|
||||
"use strict";
|
||||
|
||||
const CustomCheckbox = require('web.CustomCheckbox');
|
||||
const testUtils = require('web.test_utils');
|
||||
|
||||
const { createComponent, dom: testUtilsDom } = testUtils;
|
||||
|
||||
QUnit.module('Components', {}, function () {
|
||||
|
||||
QUnit.module('CustomCheckbox');
|
||||
|
||||
QUnit.test('test checkbox: default values', async function(assert) {
|
||||
assert.expect(6);
|
||||
|
||||
const checkbox = await createComponent(CustomCheckbox, {});
|
||||
|
||||
assert.containsOnce(checkbox.el, 'input');
|
||||
assert.containsNone(checkbox.el, 'input:disabled');
|
||||
assert.containsOnce(checkbox.el, 'label');
|
||||
|
||||
const input = checkbox.el.querySelector('input');
|
||||
assert.notOk(input.checked, 'checkbox should be unchecked');
|
||||
assert.ok(input.id.startsWith('checkbox-comp-'));
|
||||
|
||||
await testUtilsDom.click(checkbox.el.querySelector('label'));
|
||||
assert.ok(input.checked, 'checkbox should be checked');
|
||||
});
|
||||
|
||||
QUnit.test('test checkbox: custom values', async function(assert) {
|
||||
assert.expect(6);
|
||||
|
||||
const checkbox = await createComponent(CustomCheckbox, {
|
||||
props: {
|
||||
id: 'my-form-check',
|
||||
disabled: true,
|
||||
value: true,
|
||||
text: 'checkbox',
|
||||
}
|
||||
});
|
||||
|
||||
assert.containsOnce(checkbox.el, 'input');
|
||||
assert.containsOnce(checkbox.el, 'input:disabled');
|
||||
assert.containsOnce(checkbox.el, 'label');
|
||||
|
||||
const input = checkbox.el.querySelector('input');
|
||||
assert.ok(input.checked, 'checkbox should be checked');
|
||||
assert.strictEqual(input.id, 'my-form-check');
|
||||
assert.ok(input.checked, 'checkbox should be checked');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,86 +0,0 @@
|
|||
odoo.define('web.custom_file_input_tests', function (require) {
|
||||
"use strict";
|
||||
|
||||
const CustomFileInput = require('web.CustomFileInput');
|
||||
const testUtils = require('web.test_utils');
|
||||
|
||||
const { createComponent } = testUtils;
|
||||
|
||||
QUnit.module('Components', {}, function () {
|
||||
|
||||
// This module cannot be tested as thoroughly as we want it to be:
|
||||
// browsers do not let scripts programmatically assign values to inputs
|
||||
// of type file
|
||||
QUnit.module('CustomFileInput');
|
||||
|
||||
QUnit.test("Upload a file: default props", async function (assert) {
|
||||
assert.expect(6);
|
||||
|
||||
const customFileInput = await createComponent(CustomFileInput, {
|
||||
env: {
|
||||
services: {
|
||||
async httpRequest(route, params) {
|
||||
assert.deepEqual(params, {
|
||||
csrf_token: odoo.csrf_token,
|
||||
ufile: [],
|
||||
});
|
||||
assert.step(route);
|
||||
return '[]';
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const input = customFileInput.el.querySelector('input');
|
||||
|
||||
assert.strictEqual(customFileInput.el.innerText.trim().toUpperCase(), "CHOOSE FILE",
|
||||
"File input total text should match its given inner element's text");
|
||||
assert.strictEqual(input.accept, '*',
|
||||
"Input should accept all files by default");
|
||||
|
||||
await testUtils.dom.triggerEvent(input, 'change');
|
||||
|
||||
assert.notOk(input.multiple, "'multiple' attribute should not be set");
|
||||
assert.verifySteps(['/web/binary/upload']);
|
||||
});
|
||||
|
||||
QUnit.test("Upload a file: custom attachment", async function (assert) {
|
||||
assert.expect(6);
|
||||
|
||||
const customFileInput = await createComponent(CustomFileInput, {
|
||||
env: {
|
||||
services: {
|
||||
async httpRequest(route, params) {
|
||||
assert.deepEqual(params, {
|
||||
id: 5,
|
||||
model: 'res.model',
|
||||
csrf_token: odoo.csrf_token,
|
||||
ufile: [],
|
||||
});
|
||||
assert.step(route);
|
||||
return '[]';
|
||||
},
|
||||
},
|
||||
},
|
||||
props: {
|
||||
accepted_file_extensions: '.png',
|
||||
action: '/web/binary/upload_attachment',
|
||||
id: 5,
|
||||
model: 'res.model',
|
||||
multi_upload: true,
|
||||
},
|
||||
intercepts: {
|
||||
'uploaded': ev => assert.strictEqual(ev.detail.files.length, 0,
|
||||
"'files' property should be an empty array"),
|
||||
},
|
||||
});
|
||||
const input = customFileInput.el.querySelector('input');
|
||||
|
||||
assert.strictEqual(input.accept, '.png', "Input should now only accept pngs");
|
||||
|
||||
await testUtils.dom.triggerEvent(input, 'change');
|
||||
|
||||
assert.ok(input.multiple, "'multiple' attribute should be set");
|
||||
assert.verifySteps(['/web/binary/upload_attachment']);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,333 +0,0 @@
|
|||
odoo.define('web.datepicker_tests', function (require) {
|
||||
"use strict";
|
||||
|
||||
const { DatePicker, DateTimePicker } = require('web.DatePickerOwl');
|
||||
const testUtils = require('web.test_utils');
|
||||
const time = require('web.time');
|
||||
|
||||
const { createComponent } = testUtils;
|
||||
|
||||
QUnit.module('Components', {}, function () {
|
||||
|
||||
QUnit.module('DatePicker (legacy)');
|
||||
|
||||
QUnit.test("basic rendering", async function (assert) {
|
||||
assert.expect(8);
|
||||
|
||||
const picker = await createComponent(DatePicker, {
|
||||
props: { date: moment('1997-01-09'), onDateTimeChanged: () => {} },
|
||||
});
|
||||
|
||||
|
||||
assert.containsOnce(picker, 'input.o_input.o_datepicker_input');
|
||||
assert.containsOnce(picker, 'span.o_datepicker_button');
|
||||
assert.containsNone(document.body, 'div.bootstrap-datetimepicker-widget');
|
||||
|
||||
const input = picker.el.querySelector('input.o_input.o_datepicker_input');
|
||||
assert.strictEqual(input.value, '01/09/1997',
|
||||
"Value should be the one given")
|
||||
;
|
||||
assert.strictEqual(input.dataset.target, `#${picker.el.id}`,
|
||||
"DatePicker id should match its input target");
|
||||
|
||||
await testUtils.dom.click(input);
|
||||
|
||||
assert.containsOnce(document.body, 'div.bootstrap-datetimepicker-widget .datepicker');
|
||||
assert.containsNone(document.body, 'div.bootstrap-datetimepicker-widget .timepicker');
|
||||
assert.strictEqual(
|
||||
document.querySelector('.datepicker .day.active').dataset.day,
|
||||
'01/09/1997',
|
||||
"Datepicker should have set the correct day"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("pick a date", async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
const picker = await createComponent(DatePicker, {
|
||||
props: {
|
||||
date: moment('1997-01-09'),
|
||||
onDateTimeChanged: date => {
|
||||
assert.step('datetime-changed');
|
||||
assert.strictEqual(date.format('MM/DD/YYYY'), '02/08/1997',
|
||||
"Event should transmit the correct date");
|
||||
},
|
||||
}
|
||||
});
|
||||
const input = picker.el.querySelector('.o_datepicker_input');
|
||||
|
||||
await testUtils.dom.click(input);
|
||||
await testUtils.dom.click(document.querySelector('.datepicker th.next')); // next month
|
||||
|
||||
assert.verifySteps([]);
|
||||
|
||||
await testUtils.dom.click(document.querySelectorAll('.datepicker table td')[15]); // previous day
|
||||
|
||||
assert.strictEqual(input.value, '02/08/1997');
|
||||
assert.verifySteps(['datetime-changed']);
|
||||
});
|
||||
|
||||
QUnit.test("pick a date with locale", async function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
// weird shit of moment https://github.com/moment/moment/issues/5600
|
||||
// When month regex returns undefined, january is taken (first month of the default "nameless" locale)
|
||||
const originalLocale = moment.locale();
|
||||
// Those parameters will make Moment's internal compute stuff that are relevant to the bug
|
||||
const months = 'janvier_février_mars_avril_mai_juin_juillet_août_septembre_octobre_novembre_décembre'.split('_');
|
||||
const monthsShort = 'janv._févr._mars_avr._mai_juin_juil._août_custSept._oct._nov._déc.'.split('_');
|
||||
moment.defineLocale('frenchForTests', { months, monthsShort, code: 'frTest' , monthsParseExact: true});
|
||||
|
||||
const hasChanged = testUtils.makeTestPromise();
|
||||
const picker = await createComponent(DatePicker, {
|
||||
translateParameters: {
|
||||
date_format: "%d %b, %Y", // Those are important too
|
||||
time_format: "%H:%M:%S",
|
||||
},
|
||||
props: {
|
||||
date: moment('09/01/1997', 'MM/DD/YYYY'),
|
||||
onDateTimeChanged: date => {
|
||||
assert.step('datetime-changed');
|
||||
assert.strictEqual(date.format('MM/DD/YYYY'), '09/02/1997',
|
||||
"Event should transmit the correct date");
|
||||
hasChanged.resolve();
|
||||
},
|
||||
}
|
||||
});
|
||||
const input = picker.el.querySelector('.o_datepicker_input');
|
||||
await testUtils.dom.click(input);
|
||||
|
||||
await testUtils.dom.click(document.querySelectorAll('.datepicker table td')[3]); // next day
|
||||
|
||||
assert.strictEqual(input.value, '02 custSept., 1997');
|
||||
assert.verifySteps(['datetime-changed']);
|
||||
|
||||
moment.locale(originalLocale);
|
||||
moment.updateLocale('frenchForTests', null);
|
||||
});
|
||||
|
||||
QUnit.test("enter a date value", async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
const picker = await createComponent(DatePicker, {
|
||||
props: {
|
||||
date: moment('1997-01-09'),
|
||||
onDateTimeChanged: date => {
|
||||
assert.step('datetime-changed');
|
||||
assert.strictEqual(date.format('MM/DD/YYYY'), '02/08/1997',
|
||||
"Event should transmit the correct date");
|
||||
},
|
||||
}
|
||||
});
|
||||
const input = picker.el.querySelector('.o_datepicker_input');
|
||||
|
||||
assert.verifySteps([]);
|
||||
|
||||
await testUtils.fields.editAndTrigger(input, '02/08/1997', ['change']);
|
||||
|
||||
assert.verifySteps(['datetime-changed']);
|
||||
|
||||
await testUtils.dom.click(input);
|
||||
|
||||
assert.strictEqual(
|
||||
document.querySelector('.datepicker .day.active').dataset.day,
|
||||
'02/08/1997',
|
||||
"Datepicker should have set the correct day"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("Date format is correctly set", async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
testUtils.mock.patch(time, { getLangDateFormat: () => "YYYY/MM/DD" });
|
||||
const picker = await createComponent(DatePicker, {
|
||||
props: { date: moment('1997-01-09'), onDateTimeChanged: () => {} },
|
||||
});
|
||||
const input = picker.el.querySelector('.o_datepicker_input');
|
||||
|
||||
assert.strictEqual(input.value, '1997/01/09');
|
||||
|
||||
// Forces an update to assert that the registered format is the correct one
|
||||
await testUtils.dom.click(input);
|
||||
|
||||
assert.strictEqual(input.value, '1997/01/09');
|
||||
|
||||
testUtils.mock.unpatch(time);
|
||||
});
|
||||
|
||||
QUnit.module('DateTimePicker (legacy)');
|
||||
|
||||
QUnit.test("basic rendering", async function (assert) {
|
||||
assert.expect(11);
|
||||
|
||||
const picker = await createComponent(DateTimePicker, {
|
||||
props: { date: moment('1997-01-09 12:30:01'), onDateTimeChanged: () => {} },
|
||||
});
|
||||
|
||||
assert.containsOnce(picker, 'input.o_input.o_datepicker_input');
|
||||
assert.containsOnce(picker, 'span.o_datepicker_button');
|
||||
assert.containsNone(document.body, 'div.bootstrap-datetimepicker-widget');
|
||||
|
||||
const input = picker.el.querySelector('input.o_input.o_datepicker_input');
|
||||
assert.strictEqual(input.value, '01/09/1997 12:30:01', "Value should be the one given");
|
||||
assert.strictEqual(input.dataset.target, `#${picker.el.id}`,
|
||||
"DateTimePicker id should match its input target");
|
||||
|
||||
await testUtils.dom.click(input);
|
||||
|
||||
assert.containsOnce(document.body, 'div.bootstrap-datetimepicker-widget .datepicker');
|
||||
assert.containsOnce(document.body, 'div.bootstrap-datetimepicker-widget .timepicker');
|
||||
assert.strictEqual(
|
||||
document.querySelector('.datepicker .day.active').dataset.day,
|
||||
'01/09/1997',
|
||||
"Datepicker should have set the correct day");
|
||||
|
||||
assert.strictEqual(document.querySelector('.timepicker .timepicker-hour').innerText.trim(), '12',
|
||||
"Datepicker should have set the correct hour");
|
||||
assert.strictEqual(document.querySelector('.timepicker .timepicker-minute').innerText.trim(), '30',
|
||||
"Datepicker should have set the correct minute");
|
||||
assert.strictEqual(document.querySelector('.timepicker .timepicker-second').innerText.trim(), '01',
|
||||
"Datepicker should have set the correct second");
|
||||
});
|
||||
|
||||
QUnit.test("pick a date and time", async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
const picker = await createComponent(DateTimePicker, {
|
||||
props: {
|
||||
date: moment('1997-01-09 12:30:01'),
|
||||
onDateTimeChanged: date => {
|
||||
assert.step('datetime-changed');
|
||||
assert.strictEqual(date.format('MM/DD/YYYY HH:mm:ss'), '02/08/1997 15:45:05',
|
||||
"Event should transmit the correct date");
|
||||
},
|
||||
}
|
||||
});
|
||||
const input = picker.el.querySelector('input.o_input.o_datepicker_input');
|
||||
|
||||
await testUtils.dom.click(input);
|
||||
await testUtils.dom.click(document.querySelector('.datepicker th.next')); // February
|
||||
await testUtils.dom.click(document.querySelectorAll('.datepicker table td')[15]); // 08
|
||||
await testUtils.dom.click(document.querySelector('a[title="Select Time"]'));
|
||||
await testUtils.dom.click(document.querySelector('.timepicker .timepicker-hour'));
|
||||
await testUtils.dom.click(document.querySelectorAll('.timepicker .hour')[15]); // 15h
|
||||
await testUtils.dom.click(document.querySelector('.timepicker .timepicker-minute'));
|
||||
await testUtils.dom.click(document.querySelectorAll('.timepicker .minute')[9]); // 45m
|
||||
await testUtils.dom.click(document.querySelector('.timepicker .timepicker-second'));
|
||||
|
||||
assert.verifySteps([]);
|
||||
|
||||
await testUtils.dom.click(document.querySelectorAll('.timepicker .second')[1]); // 05s
|
||||
|
||||
assert.strictEqual(input.value, '02/08/1997 15:45:05');
|
||||
assert.verifySteps(['datetime-changed']);
|
||||
});
|
||||
|
||||
QUnit.test("pick a date and time with locale", async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
// weird shit of moment https://github.com/moment/moment/issues/5600
|
||||
// When month regex returns undefined, january is taken (first month of the default "nameless" locale)
|
||||
const originalLocale = moment.locale();
|
||||
// Those parameters will make Moment's internal compute stuff that are relevant to the bug
|
||||
const months = 'janvier_février_mars_avril_mai_juin_juillet_août_septembre_octobre_novembre_décembre'.split('_');
|
||||
const monthsShort = 'janv._févr._mars_avr._mai_juin_juil._août_custSept._oct._nov._déc.'.split('_');
|
||||
moment.defineLocale('frenchForTests', { months, monthsShort, code: 'frTest' , monthsParseExact: true});
|
||||
|
||||
const hasChanged = testUtils.makeTestPromise();
|
||||
const picker = await createComponent(DateTimePicker, {
|
||||
translateParameters: {
|
||||
date_format: "%d %b, %Y", // Those are important too
|
||||
time_format: "%H:%M:%S",
|
||||
},
|
||||
props: {
|
||||
date: moment('09/01/1997 12:30:01', 'MM/DD/YYYY HH:mm:ss'),
|
||||
onDateTimeChanged: date => {
|
||||
assert.step('datetime-changed');
|
||||
assert.strictEqual(date.format('MM/DD/YYYY HH:mm:ss'), '09/02/1997 15:45:05',
|
||||
"Event should transmit the correct date");
|
||||
hasChanged.resolve();
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
const input = picker.el.querySelector('input.o_input.o_datepicker_input');
|
||||
|
||||
await testUtils.dom.click(input);
|
||||
await testUtils.dom.click(document.querySelectorAll('.datepicker table td')[3]); // next day
|
||||
await testUtils.dom.click(document.querySelector('a[title="Select Time"]'));
|
||||
await testUtils.dom.click(document.querySelector('.timepicker .timepicker-hour'));
|
||||
await testUtils.dom.click(document.querySelectorAll('.timepicker .hour')[15]); // 15h
|
||||
await testUtils.dom.click(document.querySelector('.timepicker .timepicker-minute'));
|
||||
await testUtils.dom.click(document.querySelectorAll('.timepicker .minute')[9]); // 45m
|
||||
await testUtils.dom.click(document.querySelector('.timepicker .timepicker-second'));
|
||||
|
||||
assert.verifySteps([]);
|
||||
await testUtils.dom.click(document.querySelectorAll('.timepicker .second')[1]); // 05s
|
||||
|
||||
assert.strictEqual(input.value, '02 custSept., 1997 15:45:05');
|
||||
assert.verifySteps(['datetime-changed']);
|
||||
|
||||
await hasChanged;
|
||||
|
||||
moment.locale(originalLocale);
|
||||
moment.updateLocale('frenchForTests', null);
|
||||
});
|
||||
|
||||
QUnit.test("enter a datetime value", async function (assert) {
|
||||
assert.expect(9);
|
||||
|
||||
const picker = await createComponent(DateTimePicker, {
|
||||
props: {
|
||||
date: moment('1997-01-09 12:30:01'),
|
||||
onDateTimeChanged: date => {
|
||||
assert.step('datetime-changed');
|
||||
assert.strictEqual(date.format('MM/DD/YYYY HH:mm:ss'), '02/08/1997 15:45:05',
|
||||
"Event should transmit the correct date");
|
||||
},
|
||||
}
|
||||
});
|
||||
const input = picker.el.querySelector('.o_datepicker_input');
|
||||
|
||||
assert.verifySteps([]);
|
||||
|
||||
await testUtils.fields.editAndTrigger(input, '02/08/1997 15:45:05', ['change']);
|
||||
|
||||
assert.verifySteps(['datetime-changed']);
|
||||
|
||||
await testUtils.dom.click(input);
|
||||
|
||||
assert.strictEqual(input.value, '02/08/1997 15:45:05');
|
||||
assert.strictEqual(
|
||||
document.querySelector('.datepicker .day.active').dataset.day,
|
||||
'02/08/1997',
|
||||
"Datepicker should have set the correct day"
|
||||
);
|
||||
assert.strictEqual(document.querySelector('.timepicker .timepicker-hour').innerText.trim(), '15',
|
||||
"Datepicker should have set the correct hour");
|
||||
assert.strictEqual(document.querySelector('.timepicker .timepicker-minute').innerText.trim(), '45',
|
||||
"Datepicker should have set the correct minute");
|
||||
assert.strictEqual(document.querySelector('.timepicker .timepicker-second').innerText.trim(), '05',
|
||||
"Datepicker should have set the correct second");
|
||||
});
|
||||
|
||||
QUnit.test("Date time format is correctly set", async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
testUtils.mock.patch(time, { getLangDatetimeFormat: () => "hh:mm:ss YYYY/MM/DD" });
|
||||
const picker = await createComponent(DateTimePicker, {
|
||||
props: { date: moment('1997-01-09 12:30:01'), onDateTimeChanged: () => {} },
|
||||
});
|
||||
const input = picker.el.querySelector('.o_datepicker_input');
|
||||
|
||||
assert.strictEqual(input.value, '12:30:01 1997/01/09');
|
||||
|
||||
// Forces an update to assert that the registered format is the correct one
|
||||
await testUtils.dom.click(input);
|
||||
|
||||
assert.strictEqual(input.value, '12:30:01 1997/01/09');
|
||||
|
||||
testUtils.mock.unpatch(time);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,68 +0,0 @@
|
|||
odoo.define('web.dropdown_menu_mobile_tests', function (require) {
|
||||
"use strict";
|
||||
|
||||
const DropdownMenu = require('web.DropdownMenu');
|
||||
const testUtils = require('web.test_utils');
|
||||
|
||||
const { createComponent } = testUtils;
|
||||
|
||||
QUnit.module('Components', {
|
||||
before: function () {
|
||||
this.items = [
|
||||
{
|
||||
isActive: false,
|
||||
description: 'Some Item',
|
||||
id: 1,
|
||||
groupId: 1,
|
||||
groupNumber: 1,
|
||||
options: [
|
||||
{ description: "First Option", groupNumber: 1, id: 1 },
|
||||
{ description: "Second Option", groupNumber: 2, id: 2 },
|
||||
],
|
||||
}, {
|
||||
isActive: true,
|
||||
description: 'Some other Item',
|
||||
id: 2,
|
||||
groupId: 2,
|
||||
groupNumber: 2,
|
||||
},
|
||||
];
|
||||
},
|
||||
}, function () {
|
||||
QUnit.module('DropdownMenu');
|
||||
|
||||
QUnit.test('display dropdown at the right position', async function (assert) {
|
||||
assert.expect(2);
|
||||
const viewPort = testUtils.prepareTarget();
|
||||
viewPort.style.position = 'initial';
|
||||
|
||||
const dropdown = await createComponent(DropdownMenu, {
|
||||
env: {
|
||||
device: {
|
||||
isMobile: true
|
||||
},
|
||||
},
|
||||
props: {
|
||||
items: this.items,
|
||||
title: "Dropdown",
|
||||
},
|
||||
});
|
||||
|
||||
await testUtils.dom.click(dropdown.el.querySelector('button'));
|
||||
assert.containsOnce(dropdown.el, '.dropdown-menu-start',
|
||||
"should display the dropdown menu at the right screen");
|
||||
await testUtils.dom.click(dropdown.el.querySelector('button'));
|
||||
|
||||
// position the dropdown to the right
|
||||
dropdown.el.parentNode.classList.add('clearfix');
|
||||
dropdown.el.classList.add('float-end');
|
||||
await testUtils.dom.click(dropdown.el.querySelector('button'));
|
||||
assert.containsOnce(dropdown.el, '.dropdown-menu-end',
|
||||
"should display the dropdown menu at the left screen");
|
||||
|
||||
dropdown.el.parentNode.classList.remove('clearfix');
|
||||
viewPort.style.position = '';
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,430 +0,0 @@
|
|||
odoo.define('web.dropdown_menu_tests', function (require) {
|
||||
"use strict";
|
||||
|
||||
const DropdownMenu = require('web.DropdownMenu');
|
||||
const testUtils = require('web.test_utils');
|
||||
const makeTestEnvironment = require("web.test_env");
|
||||
const { mount } = require("@web/../tests/helpers/utils");
|
||||
const { LegacyComponent } = require("@web/legacy/legacy_component");
|
||||
|
||||
const { useState, xml } = owl;
|
||||
const { createComponent } = testUtils;
|
||||
|
||||
QUnit.module('Components', {
|
||||
beforeEach: function () {
|
||||
this.items = [
|
||||
{
|
||||
isActive: false,
|
||||
description: 'Some Item',
|
||||
id: 1,
|
||||
groupId: 1,
|
||||
groupNumber: 1,
|
||||
options: [
|
||||
{ description: "First Option", groupNumber: 1, id: 1 },
|
||||
{ description: "Second Option", groupNumber: 2, id: 2 },
|
||||
],
|
||||
}, {
|
||||
isActive: true,
|
||||
description: 'Some other Item',
|
||||
id: 2,
|
||||
groupId: 2,
|
||||
groupNumber: 2,
|
||||
},
|
||||
];
|
||||
},
|
||||
}, function () {
|
||||
QUnit.module('DropdownMenu');
|
||||
|
||||
QUnit.test('simple rendering and basic interactions', async function (assert) {
|
||||
assert.expect(8);
|
||||
|
||||
const dropdown = await createComponent(DropdownMenu, {
|
||||
props: {
|
||||
items: this.items,
|
||||
title: "Dropdown",
|
||||
},
|
||||
});
|
||||
|
||||
assert.strictEqual(dropdown.el.querySelector('button').innerText.trim(), "Dropdown");
|
||||
assert.containsNone(dropdown, '.o-dropdown-menu');
|
||||
|
||||
await testUtils.dom.click(dropdown.el.querySelector('button'));
|
||||
|
||||
assert.containsN(dropdown, '.dropdown-divider, .dropdown-item', 3,
|
||||
'should have 3 elements counting the divider');
|
||||
const itemEls = dropdown.el.querySelectorAll('.o_menu_item > .dropdown-item');
|
||||
assert.strictEqual(itemEls[0].innerText.trim(), 'Some Item');
|
||||
assert.doesNotHaveClass(itemEls[0], 'selected');
|
||||
assert.hasClass(itemEls[1], 'selected');
|
||||
|
||||
const dropdownElements = dropdown.el.querySelectorAll('.o_menu_item *');
|
||||
for (const dropdownEl of dropdownElements) {
|
||||
await testUtils.dom.click(dropdownEl);
|
||||
}
|
||||
assert.containsOnce(dropdown, '.o-dropdown-menu',
|
||||
"Clicking on any item of the dropdown should not close it");
|
||||
|
||||
await testUtils.dom.click(document.body);
|
||||
|
||||
assert.containsNone(dropdown, '.o-dropdown-menu',
|
||||
"Clicking outside of the dropdown should close it");
|
||||
});
|
||||
|
||||
QUnit.test('only one dropdown rendering at same time (owl vs bootstrap dropdown)', async function (assert) {
|
||||
assert.expect(12);
|
||||
|
||||
const bsDropdown = document.createElement('div');
|
||||
bsDropdown.innerHTML = `<div class="dropdown">
|
||||
<button class="btn dropdown-toggle" type="button"
|
||||
data-bs-toggle="dropdown" aria-expanded="false">
|
||||
BS Dropdown button
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
<a class="dropdown-item" href="#">BS Action</a>
|
||||
</div>
|
||||
</div>`;
|
||||
document.body.append(bsDropdown);
|
||||
|
||||
const dropdown = await createComponent(DropdownMenu, {
|
||||
props: {
|
||||
items: this.items,
|
||||
title: "Dropdown",
|
||||
},
|
||||
});
|
||||
|
||||
await testUtils.dom.click(dropdown.el.querySelector('button'));
|
||||
|
||||
assert.hasClass(dropdown.el.querySelector('.o-dropdown-menu'), 'show');
|
||||
assert.doesNotHaveClass(bsDropdown.querySelector('.dropdown-menu'), 'show');
|
||||
|
||||
assert.isVisible(dropdown.el.querySelector('.o-dropdown-menu'),
|
||||
"owl dropdown menu should be visible");
|
||||
assert.isNotVisible(bsDropdown.querySelector('.dropdown-menu'),
|
||||
"bs dropdown menu should not be visible");
|
||||
|
||||
await testUtils.dom.click(bsDropdown.querySelector('.btn.dropdown-toggle'));
|
||||
|
||||
assert.doesNotHaveClass(dropdown.el, 'show');
|
||||
assert.containsNone(dropdown.el, '.o-dropdown-menu',
|
||||
"owl dropdown menu should not be set inside the dom");
|
||||
|
||||
assert.hasClass(bsDropdown.querySelector('.dropdown-menu'), 'show');
|
||||
assert.isVisible(bsDropdown.querySelector('.dropdown-menu'),
|
||||
"bs dropdown menu should be visible");
|
||||
|
||||
await testUtils.dom.click(document.body);
|
||||
|
||||
assert.doesNotHaveClass(dropdown.el, 'show');
|
||||
assert.containsNone(dropdown.el, '.o-dropdown-menu',
|
||||
"owl dropdown menu should not be set inside the dom");
|
||||
|
||||
assert.doesNotHaveClass(bsDropdown.querySelector('.dropdown-menu'), 'show');
|
||||
assert.isNotVisible(bsDropdown.querySelector('.dropdown-menu'),
|
||||
"bs dropdown menu should not be visible");
|
||||
|
||||
bsDropdown.remove();
|
||||
});
|
||||
|
||||
QUnit.test('click on an item without options should toggle it', async function (assert) {
|
||||
assert.expect(7);
|
||||
|
||||
delete this.items[0].options;
|
||||
|
||||
const dropdown = await createComponent(DropdownMenu, {
|
||||
props: { items: this.items },
|
||||
intercepts: {
|
||||
'item-selected': function (ev) {
|
||||
assert.strictEqual(ev.detail.item.id, 1);
|
||||
this.state.items[0].isActive = !this.state.items[0].isActive;
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
await testUtils.dom.click(dropdown.el.querySelector('button'));
|
||||
|
||||
const firstItemEl = dropdown.el.querySelector('.o_menu_item > a');
|
||||
assert.doesNotHaveClass(firstItemEl, 'selected');
|
||||
await testUtils.dom.click(firstItemEl);
|
||||
assert.hasClass(firstItemEl, 'selected');
|
||||
assert.isVisible(firstItemEl);
|
||||
await testUtils.dom.click(firstItemEl);
|
||||
assert.doesNotHaveClass(firstItemEl, 'selected');
|
||||
assert.isVisible(firstItemEl);
|
||||
});
|
||||
|
||||
QUnit.test('click on an item should not change url', async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
delete this.items[0].options;
|
||||
|
||||
const initialHref = window.location.href;
|
||||
const dropdown = await createComponent(DropdownMenu, {
|
||||
props: { items: this.items },
|
||||
});
|
||||
|
||||
await testUtils.dom.click(dropdown.el.querySelector('button'));
|
||||
await testUtils.dom.click(dropdown.el.querySelector('.o_menu_item > a'));
|
||||
assert.strictEqual(window.location.href, initialHref,
|
||||
"the url should not have changed after a click on an item");
|
||||
});
|
||||
|
||||
QUnit.test('options rendering', async function (assert) {
|
||||
assert.expect(6);
|
||||
|
||||
const dropdown = await createComponent(DropdownMenu, {
|
||||
props: { items: this.items },
|
||||
});
|
||||
|
||||
await testUtils.dom.click(dropdown.el.querySelector('button'));
|
||||
assert.containsN(dropdown, '.dropdown-divider, .dropdown-item', 3);
|
||||
|
||||
const firstItemEl = dropdown.el.querySelector('.o_menu_item > a');
|
||||
assert.hasClass(firstItemEl.querySelector('i'), 'o_icon_right fa fa-caret-right');
|
||||
// open options menu
|
||||
await testUtils.dom.click(firstItemEl);
|
||||
assert.hasClass(firstItemEl.querySelector('i'), 'o_icon_right fa fa-caret-down');
|
||||
assert.containsN(dropdown, '.dropdown-divider, .dropdown-item', 6);
|
||||
|
||||
// close options menu
|
||||
await testUtils.dom.click(firstItemEl);
|
||||
assert.hasClass(firstItemEl.querySelector('i'), 'o_icon_right fa fa-caret-right');
|
||||
assert.containsN(dropdown, '.dropdown-divider, .dropdown-item', 3);
|
||||
});
|
||||
|
||||
QUnit.test('close menu closes also submenus', async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
const dropdown = await createComponent(DropdownMenu, {
|
||||
props: { items: this.items },
|
||||
});
|
||||
|
||||
// open dropdown menu
|
||||
await testUtils.dom.click(dropdown.el.querySelector('button'));
|
||||
// open options menu of first item
|
||||
await testUtils.dom.click(dropdown.el.querySelector('.o_menu_item a'));
|
||||
|
||||
assert.containsN(dropdown, '.dropdown-divider, .dropdown-item', 6);
|
||||
await testUtils.dom.click(dropdown.el.querySelector('button'));
|
||||
|
||||
await testUtils.dom.click(dropdown.el.querySelector('button'));
|
||||
assert.containsN(dropdown, '.dropdown-divider, .dropdown-item', 3);
|
||||
});
|
||||
|
||||
QUnit.test('click on an option should trigger the event "item_option_clicked" with appropriate data', async function (assert) {
|
||||
assert.expect(18);
|
||||
|
||||
let eventNumber = 0;
|
||||
const dropdown = await createComponent(DropdownMenu, {
|
||||
props: { items: this.items },
|
||||
intercepts: {
|
||||
'item-selected': function (ev) {
|
||||
eventNumber++;
|
||||
const { option } = ev.detail;
|
||||
assert.strictEqual(ev.detail.item.id, 1);
|
||||
if (eventNumber === 1) {
|
||||
assert.strictEqual(option.id, 1);
|
||||
this.state.items[0].isActive = true;
|
||||
this.state.items[0].options[0].isActive = true;
|
||||
}
|
||||
if (eventNumber === 2) {
|
||||
assert.strictEqual(option.id, 2);
|
||||
this.state.items[0].options[1].isActive = true;
|
||||
}
|
||||
if (eventNumber === 3) {
|
||||
assert.strictEqual(option.id, 1);
|
||||
this.state.items[0].options[0].isActive = false;
|
||||
}
|
||||
if (eventNumber === 4) {
|
||||
assert.strictEqual(option.id, 2);
|
||||
this.state.items[0].isActive = false;
|
||||
this.state.items[0].options[1].isActive = false;
|
||||
}
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
// open dropdown menu
|
||||
await testUtils.dom.click(dropdown.el.querySelector('button'));
|
||||
assert.containsN(dropdown, '.dropdown-divider, .o_menu_item', 3);
|
||||
|
||||
// open menu options of first item
|
||||
await testUtils.dom.click(dropdown.el.querySelector('.o_menu_item > a'));
|
||||
let optionELs = dropdown.el.querySelectorAll('.o_menu_item .o_item_option > a');
|
||||
|
||||
// click on first option
|
||||
await testUtils.dom.click(optionELs[0]);
|
||||
assert.hasClass(dropdown.el.querySelector('.o_menu_item > a'), 'selected');
|
||||
optionELs = dropdown.el.querySelectorAll('.o_menu_item .o_item_option > a');
|
||||
assert.hasClass(optionELs[0], 'selected');
|
||||
assert.doesNotHaveClass(optionELs[1], 'selected');
|
||||
|
||||
// click on second option
|
||||
await testUtils.dom.click(optionELs[1]);
|
||||
assert.hasClass(dropdown.el.querySelector('.o_menu_item > a'), 'selected');
|
||||
optionELs = dropdown.el.querySelectorAll('.o_menu_item .o_item_option > a');
|
||||
assert.hasClass(optionELs[0], 'selected');
|
||||
assert.hasClass(optionELs[1], 'selected');
|
||||
|
||||
// click again on first option
|
||||
await testUtils.dom.click(optionELs[0]);
|
||||
// click again on second option
|
||||
await testUtils.dom.click(optionELs[1]);
|
||||
assert.doesNotHaveClass(dropdown.el.querySelector('.o_menu_item > a'), 'selected');
|
||||
optionELs = dropdown.el.querySelectorAll('.o_menu_item .o_item_option > a');
|
||||
assert.doesNotHaveClass(optionELs[0], 'selected');
|
||||
assert.doesNotHaveClass(optionELs[1], 'selected');
|
||||
});
|
||||
|
||||
QUnit.test('keyboard navigation', async function (assert) {
|
||||
assert.expect(12);
|
||||
|
||||
// Shorthand method to trigger a specific keydown.
|
||||
// Note that BootStrap handles some of the navigation moves (up and down)
|
||||
// so we need to give the event the proper "which" property. We also give
|
||||
// it when it's not required to check if it has been correctly prevented.
|
||||
async function navigate(key, global) {
|
||||
const which = {
|
||||
Enter: 13,
|
||||
Escape: 27,
|
||||
ArrowLeft: 37,
|
||||
ArrowUp: 38,
|
||||
ArrowRight: 39,
|
||||
ArrowDown: 40,
|
||||
}[key];
|
||||
const target = global ? document.body : document.activeElement;
|
||||
await testUtils.dom.triggerEvent(target, 'keydown', { key, which });
|
||||
if (key === 'Enter') {
|
||||
// Pressing "Enter" on a focused element triggers a click (HTML5 specs)
|
||||
await testUtils.dom.click(target);
|
||||
}
|
||||
}
|
||||
|
||||
const dropdown = await createComponent(DropdownMenu, {
|
||||
props: { items: this.items },
|
||||
});
|
||||
|
||||
// Initialize active element (start at toggle button)
|
||||
dropdown.el.querySelector('button').focus();
|
||||
await testUtils.dom.click(dropdown.el.querySelector('button'));
|
||||
|
||||
await navigate('ArrowDown'); // Go to next item
|
||||
|
||||
assert.strictEqual(document.activeElement, dropdown.el.querySelector('.o_menu_item a'));
|
||||
assert.containsNone(dropdown, '.o_item_option');
|
||||
|
||||
await navigate('ArrowRight'); // Unfold first item's options (w/ Right)
|
||||
|
||||
assert.strictEqual(document.activeElement, dropdown.el.querySelector('.o_menu_item a'));
|
||||
assert.containsN(dropdown, '.o_item_option', 2);
|
||||
|
||||
await navigate('ArrowDown'); // Go to next option
|
||||
|
||||
assert.strictEqual(document.activeElement, dropdown.el.querySelector('.o_item_option a'));
|
||||
|
||||
await navigate('ArrowLeft'); // Fold first item's options (w/ Left)
|
||||
|
||||
assert.strictEqual(document.activeElement, dropdown.el.querySelector('.o_menu_item a'));
|
||||
assert.containsNone(dropdown, '.o_item_option');
|
||||
|
||||
await navigate('Enter'); // Unfold first item's options (w/ Enter)
|
||||
|
||||
assert.strictEqual(document.activeElement, dropdown.el.querySelector('.o_menu_item a'));
|
||||
assert.containsN(dropdown, '.o_item_option', 2);
|
||||
|
||||
await navigate('ArrowDown'); // Go to next option
|
||||
await navigate('Escape'); // Fold first item's options (w/ Escape)
|
||||
await testUtils.nextTick();
|
||||
|
||||
assert.strictEqual(dropdown.el.querySelector('.o_menu_item a'), document.activeElement);
|
||||
assert.containsNone(dropdown, '.o_item_option');
|
||||
|
||||
await navigate('Escape', true); // Close the dropdown
|
||||
|
||||
assert.containsNone(dropdown, '.o-dropdown-menu', "Dropdown should be folded");
|
||||
});
|
||||
|
||||
QUnit.test('interactions between multiple dropdowns', async function (assert) {
|
||||
assert.expect(7);
|
||||
|
||||
const items = this.items;
|
||||
class Parent extends LegacyComponent {
|
||||
setup() {
|
||||
this.state = useState({ items });
|
||||
}
|
||||
}
|
||||
Parent.components = { DropdownMenu };
|
||||
Parent.template = xml`
|
||||
<div>
|
||||
<DropdownMenu title="'First'" items="state.items"/>
|
||||
<DropdownMenu title="'Second'" items="state.items"/>
|
||||
</div>`;
|
||||
const env = makeTestEnvironment();
|
||||
const target = testUtils.prepareTarget();
|
||||
|
||||
const parent = await mount(Parent, target, { env });
|
||||
|
||||
const [menu1, menu2] = parent.el.querySelectorAll('.dropdown');
|
||||
|
||||
assert.containsNone(parent, '.o-dropdown-menu');
|
||||
|
||||
await testUtils.dom.click(menu1.querySelector('button'));
|
||||
|
||||
assert.containsOnce(parent, '.o-dropdown-menu');
|
||||
const [first, second] = parent.el.querySelectorAll(".dropdown");
|
||||
assert.containsOnce(first, '.o-dropdown-menu');
|
||||
|
||||
await testUtils.dom.click(menu2.querySelector('button'));
|
||||
|
||||
assert.containsOnce(parent, '.o-dropdown-menu');
|
||||
assert.containsOnce(second, '.o-dropdown-menu');
|
||||
|
||||
await testUtils.dom.click(menu2.querySelector('.o_menu_item a'));
|
||||
await testUtils.dom.click(menu1.querySelector('button'));
|
||||
|
||||
assert.containsOnce(parent, '.o-dropdown-menu');
|
||||
assert.containsOnce(first, '.o-dropdown-menu');
|
||||
});
|
||||
|
||||
QUnit.test("dropdown doesn't get close on mousedown inside and mouseup outside dropdown", async function (assert) {
|
||||
// In this test, we simulate a case where the user clicks inside a dropdown menu item
|
||||
// (e.g. in the input of the 'Save current search' item in the Favorites menu), keeps
|
||||
// the click pressed, moves the cursor outside the dropdown and releases the click
|
||||
// (i.e. mousedown and focus inside the item, mouseup and click outside the dropdown).
|
||||
// In this case, we want to keep the dropdown menu open.
|
||||
assert.expect(5);
|
||||
|
||||
const items = this.items;
|
||||
class Parent extends LegacyComponent {
|
||||
setup() {
|
||||
this.items = items;
|
||||
}
|
||||
}
|
||||
Parent.components = { DropdownMenu };
|
||||
Parent.template = xml`
|
||||
<div>
|
||||
<DropdownMenu title="'First'" items="items"/>
|
||||
</div>`;
|
||||
const env = makeTestEnvironment();
|
||||
const target = testUtils.prepareTarget();
|
||||
|
||||
const parent = await mount(Parent, target, { env });
|
||||
|
||||
const menu = parent.el.querySelector(".dropdown");
|
||||
assert.doesNotHaveClass(menu, "show", "dropdown should not be open");
|
||||
|
||||
await testUtils.dom.click(menu.querySelector("button"));
|
||||
assert.hasClass(menu, "show", "dropdown should be open");
|
||||
|
||||
const firstItemEl = menu.querySelector(".o_menu_item > a");
|
||||
// open options menu
|
||||
await testUtils.dom.click(firstItemEl);
|
||||
assert.hasClass(firstItemEl.querySelector("i"), "o_icon_right fa fa-caret-down");
|
||||
|
||||
// force the focus inside the dropdown item and click outside
|
||||
firstItemEl.parentElement.querySelector(".o_menu_item_options .o_item_option a").focus();
|
||||
await testUtils.dom.triggerEvents(parent.el, "click");
|
||||
assert.hasClass(menu, "show", "dropdown should still be open");
|
||||
assert.hasClass(firstItemEl.querySelector("i"), "o_icon_right fa fa-caret-down");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,188 +0,0 @@
|
|||
odoo.define('web.pager_tests', function (require) {
|
||||
"use strict";
|
||||
|
||||
const Pager = require('web.Pager');
|
||||
const testUtils = require('web.test_utils');
|
||||
const { LegacyComponent } = require("@web/legacy/legacy_component");
|
||||
|
||||
const { createComponent } = testUtils;
|
||||
|
||||
const { xml, useState } = owl;
|
||||
|
||||
class PagerController extends LegacyComponent {
|
||||
setup() {
|
||||
this.state = useState({ ...this.props });
|
||||
}
|
||||
async updateProps(nextProps) {
|
||||
Object.assign(this.state, nextProps);
|
||||
await testUtils.nextTick();
|
||||
}
|
||||
}
|
||||
PagerController.template = xml`<Pager t-props="state" />`;
|
||||
PagerController.components = { Pager };
|
||||
|
||||
QUnit.module('Components', {}, function () {
|
||||
|
||||
QUnit.module('Legacy Pager');
|
||||
|
||||
QUnit.test('basic interactions', async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
const pager = await createComponent(PagerController, {
|
||||
props: {
|
||||
currentMinimum: 1,
|
||||
limit: 4,
|
||||
size: 10,
|
||||
onPagerChanged: function (detail) {
|
||||
pager.updateProps(detail);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
assert.strictEqual(testUtils.controlPanel.getPagerValue(pager), "1-4",
|
||||
"currentMinimum should be set to 1");
|
||||
|
||||
await testUtils.controlPanel.pagerNext(pager);
|
||||
|
||||
assert.strictEqual(testUtils.controlPanel.getPagerValue(pager), "5-8",
|
||||
"currentMinimum should now be 5");
|
||||
});
|
||||
|
||||
QUnit.test('edit the pager', async function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
const pager = await createComponent(PagerController, {
|
||||
props: {
|
||||
currentMinimum: 1,
|
||||
limit: 4,
|
||||
size: 10,
|
||||
onPagerChanged: function (detail) {
|
||||
pager.updateProps(detail);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await testUtils.dom.click(pager.el.querySelector('.o_pager_value'));
|
||||
|
||||
assert.containsOnce(pager, 'input',
|
||||
"the pager should contain an input");
|
||||
assert.strictEqual(testUtils.controlPanel.getPagerValue(pager), "1-4",
|
||||
"the input should have correct value");
|
||||
|
||||
// change the limit
|
||||
await testUtils.controlPanel.setPagerValue(pager, "1-6");
|
||||
|
||||
assert.containsNone(pager, 'input',
|
||||
"the pager should not contain an input anymore");
|
||||
assert.strictEqual(testUtils.controlPanel.getPagerValue(pager), "1-6",
|
||||
"the limit should have been updated");
|
||||
});
|
||||
|
||||
QUnit.test("keydown on pager with same value", async function (assert) {
|
||||
assert.expect(7);
|
||||
|
||||
const pager = await createComponent(PagerController, {
|
||||
props: {
|
||||
currentMinimum: 1,
|
||||
limit: 4,
|
||||
size: 10,
|
||||
onPagerChanged: () => assert.step("pager-changed"),
|
||||
},
|
||||
});
|
||||
|
||||
// Enter edit mode
|
||||
await testUtils.dom.click(pager.el.querySelector('.o_pager_value'));
|
||||
|
||||
assert.containsOnce(pager, "input");
|
||||
assert.strictEqual(testUtils.controlPanel.getPagerValue(pager), "1-4");
|
||||
assert.verifySteps([]);
|
||||
|
||||
// Exit edit mode
|
||||
await testUtils.dom.triggerEvent(pager.el.querySelector('input'), "keydown", { key: "Enter" });
|
||||
|
||||
assert.containsNone(pager, "input");
|
||||
assert.strictEqual(testUtils.controlPanel.getPagerValue(pager), "1-4");
|
||||
assert.verifySteps(["pager-changed"]);
|
||||
});
|
||||
|
||||
QUnit.test('pager value formatting', async function (assert) {
|
||||
assert.expect(8);
|
||||
|
||||
const pager = await createComponent(PagerController, {
|
||||
props: {
|
||||
currentMinimum: 1,
|
||||
limit: 4,
|
||||
size: 10,
|
||||
onPagerChanged: (detail) => {
|
||||
pager.updateProps(detail);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
assert.strictEqual(testUtils.controlPanel.getPagerValue(pager), "1-4", "Initial value should be correct");
|
||||
|
||||
async function inputAndAssert(input, expected, reason) {
|
||||
await testUtils.controlPanel.setPagerValue(pager, input);
|
||||
assert.strictEqual(testUtils.controlPanel.getPagerValue(pager), expected,
|
||||
`Pager value should be "${expected}" when given "${input}": ${reason}`);
|
||||
}
|
||||
|
||||
await inputAndAssert("4-4", "4", "values are squashed when minimum = maximum");
|
||||
await inputAndAssert("1-11", "1-10", "maximum is floored to size when out of range");
|
||||
await inputAndAssert("20-15", "10", "combination of the 2 assertions above");
|
||||
await inputAndAssert("6-5", "10", "fallback to previous value when minimum > maximum");
|
||||
await inputAndAssert("definitelyValidNumber", "10", "fallback to previous value if not a number");
|
||||
await inputAndAssert(" 1 , 2 ", "1-2", "value is normalized and accepts several separators");
|
||||
await inputAndAssert("3 8", "3-8", "value accepts whitespace(s) as a separator");
|
||||
});
|
||||
|
||||
QUnit.test('pager disabling', async function (assert) {
|
||||
assert.expect(10);
|
||||
|
||||
const reloadPromise = testUtils.makeTestPromise();
|
||||
const pager = await createComponent(PagerController, {
|
||||
props: {
|
||||
currentMinimum: 1,
|
||||
limit: 4,
|
||||
size: 10,
|
||||
// The goal here is to test the reactivity of the pager; in a
|
||||
// typical views, we disable the pager after switching page
|
||||
// to avoid switching twice with the same action (double click).
|
||||
onPagerChanged: async function (detail) {
|
||||
// 1. Simulate a (long) server action
|
||||
await reloadPromise;
|
||||
// 2. Update the view with loaded data
|
||||
pager.updateProps(detail);
|
||||
},
|
||||
},
|
||||
});
|
||||
const pagerButtons = pager.el.querySelectorAll('button');
|
||||
|
||||
// Click and check button is disabled
|
||||
await testUtils.controlPanel.pagerNext(pager);
|
||||
assert.ok(pager.el.querySelector('button.o_pager_next').disabled);
|
||||
// Try to edit the pager value
|
||||
await testUtils.dom.click(pager.el.querySelector('.o_pager_value'));
|
||||
|
||||
assert.strictEqual(pagerButtons.length, 2, "the two buttons should be displayed");
|
||||
assert.ok(pagerButtons[0].disabled, "'previous' is disabled");
|
||||
assert.ok(pagerButtons[1].disabled, "'next' is disabled");
|
||||
assert.strictEqual(pager.el.querySelector('.o_pager_value').tagName, 'SPAN',
|
||||
"pager edition is prevented");
|
||||
|
||||
// Server action is done
|
||||
reloadPromise.resolve();
|
||||
await testUtils.nextTick();
|
||||
|
||||
assert.strictEqual(pagerButtons.length, 2, "the two buttons should be displayed");
|
||||
assert.notOk(pagerButtons[0].disabled, "'previous' is enabled");
|
||||
assert.notOk(pagerButtons[1].disabled, "'next' is enabled");
|
||||
assert.strictEqual(testUtils.controlPanel.getPagerValue(pager), "5-8", "value has been updated");
|
||||
|
||||
await testUtils.dom.click(pager.el.querySelector('.o_pager_value'));
|
||||
|
||||
assert.strictEqual(pager.el.querySelector('.o_pager_value').tagName, 'INPUT',
|
||||
"pager edition is re-enabled");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,169 +0,0 @@
|
|||
odoo.define('web.comparison_menu_tests', function (require) {
|
||||
"use strict";
|
||||
|
||||
const { browser } = require('@web/core/browser/browser');
|
||||
const { patchWithCleanup } = require('@web/../tests/helpers/utils');
|
||||
const cpHelpers = require('@web/../tests/search/helpers');
|
||||
const {
|
||||
createControlPanel,
|
||||
mock,
|
||||
} = require('web.test_utils');
|
||||
|
||||
const { patchDate } = mock;
|
||||
const searchMenuTypes = ['filter', 'comparison'];
|
||||
|
||||
QUnit.module('Components', {
|
||||
beforeEach() {
|
||||
this.fields = {
|
||||
birthday: { string: "Birthday", type: "date", store: true, sortable: true },
|
||||
date_field: { string: "Date", type: "date", store: true, sortable: true },
|
||||
float_field: { string: "Float", type: "float", group_operator: 'sum' },
|
||||
foo: { string: "Foo", type: "char", store: true, sortable: true },
|
||||
};
|
||||
this.cpModelConfig = {
|
||||
arch: `
|
||||
<search>
|
||||
<filter name="birthday" date="birthday"/>
|
||||
<filter name="date_field" date="date_field"/>
|
||||
</search>`,
|
||||
fields: this.fields,
|
||||
searchMenuTypes,
|
||||
};
|
||||
patchWithCleanup(browser, {
|
||||
setTimeout: (fn) => fn(),
|
||||
});
|
||||
},
|
||||
}, function () {
|
||||
|
||||
QUnit.module('ComparisonMenu (legacy)');
|
||||
|
||||
QUnit.test('simple rendering', async function (assert) {
|
||||
const unpatchDate = patchDate(1997, 0, 9, 12, 0, 0);
|
||||
const params = {
|
||||
cpModelConfig: this.cpModelConfig,
|
||||
cpProps: { fields: this.fields, searchMenuTypes },
|
||||
};
|
||||
const controlPanel = await createControlPanel(params);
|
||||
|
||||
assert.containsOnce(controlPanel, ".dropdown.o_filter_menu");
|
||||
assert.containsNone(controlPanel, ".dropdown.o_comparison_menu");
|
||||
|
||||
await cpHelpers.toggleFilterMenu(controlPanel);
|
||||
await cpHelpers.toggleMenuItem(controlPanel, "Birthday");
|
||||
await cpHelpers.toggleMenuItemOption(controlPanel, "Birthday", "January");
|
||||
|
||||
assert.containsOnce(controlPanel, 'div.o_comparison_menu > button i.fa.fa-adjust');
|
||||
assert.strictEqual(controlPanel.el.querySelector('div.o_comparison_menu > button span').innerText.trim(), "Comparison");
|
||||
|
||||
await cpHelpers.toggleComparisonMenu(controlPanel);
|
||||
assert.containsN(controlPanel.el, ".o_comparison_menu .dropdown-item", 2);
|
||||
assert.containsN(
|
||||
controlPanel.el,
|
||||
".o_comparison_menu .dropdown-item[role=menuitemcheckbox]",
|
||||
2
|
||||
);
|
||||
|
||||
const comparisonOptions = [...controlPanel.el.querySelectorAll(
|
||||
'.o_comparison_menu .o_menu_item'
|
||||
)];
|
||||
assert.strictEqual(comparisonOptions.length, 2);
|
||||
assert.deepEqual(
|
||||
comparisonOptions.map(e => e.innerText),
|
||||
["Birthday: Previous Period", "Birthday: Previous Year"]
|
||||
);
|
||||
assert.deepEqual(
|
||||
comparisonOptions.map((e) => e.ariaChecked),
|
||||
["false", "false"]
|
||||
);
|
||||
|
||||
unpatchDate();
|
||||
});
|
||||
|
||||
QUnit.test('activate a comparison works', async function (assert) {
|
||||
const unpatchDate = patchDate(1997, 0, 9, 12, 0, 0);
|
||||
const params = {
|
||||
cpModelConfig: this.cpModelConfig,
|
||||
cpProps: { fields: this.fields, searchMenuTypes },
|
||||
};
|
||||
const controlPanel = await createControlPanel(params);
|
||||
|
||||
await cpHelpers.toggleFilterMenu(controlPanel);
|
||||
await cpHelpers.toggleMenuItem(controlPanel, "Birthday");
|
||||
await cpHelpers.toggleMenuItemOption(controlPanel, "Birthday", "January");
|
||||
await cpHelpers.toggleComparisonMenu(controlPanel);
|
||||
await cpHelpers.toggleMenuItem(controlPanel, "Birthday: Previous Period");
|
||||
|
||||
assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), [
|
||||
"Birthday: January 1997",
|
||||
"Birthday: Previous Period",
|
||||
]);
|
||||
|
||||
await cpHelpers.toggleFilterMenu(controlPanel);
|
||||
await cpHelpers.toggleMenuItem(controlPanel, "Date");
|
||||
await cpHelpers.toggleMenuItemOption(controlPanel, "Date", "December");
|
||||
await cpHelpers.toggleComparisonMenu(controlPanel);
|
||||
await cpHelpers.toggleMenuItem(controlPanel, "Date: Previous Year");
|
||||
|
||||
assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), [
|
||||
["Birthday: January 1997", "Date: December 1996"].join("or"),
|
||||
"Date: Previous Year",
|
||||
]);
|
||||
|
||||
await cpHelpers.toggleFilterMenu(controlPanel);
|
||||
await cpHelpers.toggleMenuItem(controlPanel, "Date");
|
||||
await cpHelpers.toggleMenuItemOption(controlPanel, "Date", "1996");
|
||||
|
||||
assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), [
|
||||
"Birthday: January 1997",
|
||||
]);
|
||||
|
||||
await cpHelpers.toggleComparisonMenu(controlPanel);
|
||||
await cpHelpers.toggleMenuItem(controlPanel, "Birthday: Previous Year");
|
||||
assert.containsN(controlPanel.el, ".o_comparison_menu .dropdown-item", 2);
|
||||
assert.containsN(
|
||||
controlPanel.el,
|
||||
".o_comparison_menu .dropdown-item[role=menuitemcheckbox]",
|
||||
2
|
||||
);
|
||||
const comparisonOptions = [
|
||||
...controlPanel.el.querySelectorAll(".o_comparison_menu .dropdown-item"),
|
||||
];
|
||||
assert.deepEqual(
|
||||
comparisonOptions.map((e) => e.innerText.trim()),
|
||||
["Birthday: Previous Period", "Birthday: Previous Year"]
|
||||
);
|
||||
assert.deepEqual(
|
||||
comparisonOptions.map((e) => e.ariaChecked),
|
||||
["false", "true"]
|
||||
);
|
||||
|
||||
assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), [
|
||||
"Birthday: January 1997",
|
||||
"Birthday: Previous Year",
|
||||
]);
|
||||
|
||||
await cpHelpers.removeFacet(controlPanel);
|
||||
|
||||
assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), []);
|
||||
|
||||
unpatchDate();
|
||||
});
|
||||
|
||||
QUnit.test('no timeRanges key in search query if "comparison" not in searchMenuTypes', async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
this.cpModelConfig.searchMenuTypes = ['filter'];
|
||||
const params = {
|
||||
cpModelConfig: this.cpModelConfig,
|
||||
cpProps: { fields: this.fields, searchMenuTypes: ['filter'] },
|
||||
};
|
||||
const controlPanel = await createControlPanel(params);
|
||||
|
||||
await cpHelpers.toggleFilterMenu(controlPanel);
|
||||
await cpHelpers.toggleMenuItem(controlPanel, "Birthday");
|
||||
await cpHelpers.toggleMenuItemOption(controlPanel, "Birthday", 0);
|
||||
|
||||
assert.notOk("timeRanges" in controlPanel.getQuery());
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,458 +0,0 @@
|
|||
odoo.define("web/static/tests/control_panel/control_panel_model_extension_tests.js", function (require) {
|
||||
"use strict";
|
||||
|
||||
const ActionModel = require("web.ActionModel");
|
||||
const makeTestEnvironment = require('web.test_env');
|
||||
|
||||
function createModel(params = {}) {
|
||||
const archs = (params.arch && { search: params.arch, }) || {};
|
||||
const { ControlPanel: controlPanelInfo, } = ActionModel.extractArchInfo(archs);
|
||||
const extensions = {
|
||||
ControlPanel: {
|
||||
context: params.context,
|
||||
archNodes: controlPanelInfo.children,
|
||||
dynamicFilters: params.dynamicFilters,
|
||||
favoriteFilters: params.favoriteFilters,
|
||||
env: makeTestEnvironment(),
|
||||
fields: params.fields,
|
||||
},
|
||||
};
|
||||
const model = new ActionModel(extensions);
|
||||
return model;
|
||||
}
|
||||
function sanitizeFilters(model) {
|
||||
const cpme = model.extensions[0].find(
|
||||
(ext) => ext.constructor.name === "ControlPanelModelExtension"
|
||||
);
|
||||
const filters = Object.values(cpme.state.filters);
|
||||
return filters.map(filter => {
|
||||
const copy = Object.assign({}, filter);
|
||||
delete copy.groupId;
|
||||
delete copy.groupNumber;
|
||||
delete copy.id;
|
||||
return copy;
|
||||
});
|
||||
}
|
||||
|
||||
QUnit.module('ControlPanelModelExtension', {
|
||||
beforeEach() {
|
||||
this.fields = {
|
||||
display_name: { string: "Displayed name", type: 'char' },
|
||||
foo: { string: "Foo", type: "char", default: "My little Foo Value", store: true, sortable: true },
|
||||
date_field: { string: "Date", type: "date", store: true, sortable: true },
|
||||
float_field: { string: "Float", type: "float" },
|
||||
bar: { string: "Bar", type: "many2one", relation: 'partner' },
|
||||
};
|
||||
}
|
||||
}, function () {
|
||||
QUnit.module('Arch parsing');
|
||||
|
||||
QUnit.test('empty arch', async function (assert) {
|
||||
assert.expect(1);
|
||||
const model = createModel();
|
||||
assert.deepEqual(sanitizeFilters(model), []);
|
||||
});
|
||||
|
||||
QUnit.test('one field tag', async function (assert) {
|
||||
assert.expect(1);
|
||||
const arch = `
|
||||
<search>
|
||||
<field name="bar"/>
|
||||
</search>`;
|
||||
const fields = this.fields;
|
||||
const model = createModel({ arch, fields, });
|
||||
assert.deepEqual(sanitizeFilters(model), [
|
||||
{
|
||||
description: "Bar",
|
||||
fieldName: "bar",
|
||||
fieldType: "many2one",
|
||||
type: "field"
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
QUnit.test('one separator tag', async function (assert) {
|
||||
assert.expect(1);
|
||||
const arch = `
|
||||
<search>
|
||||
<separator/>
|
||||
</search>`;
|
||||
const fields = this.fields;
|
||||
const model = createModel({ arch, fields, });
|
||||
assert.deepEqual(sanitizeFilters(model), []);
|
||||
});
|
||||
|
||||
QUnit.test('one separator tag and one field tag', async function (assert) {
|
||||
assert.expect(1);
|
||||
const arch = `
|
||||
<search>
|
||||
<separator/>
|
||||
<field name="bar"/>
|
||||
</search>`;
|
||||
const fields = this.fields;
|
||||
const model = createModel({ arch, fields, });
|
||||
assert.deepEqual(sanitizeFilters(model), [
|
||||
{
|
||||
description: "Bar",
|
||||
fieldName: "bar",
|
||||
fieldType: "many2one",
|
||||
type: "field"
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
QUnit.test('one filter tag', async function (assert) {
|
||||
assert.expect(1);
|
||||
const arch = `
|
||||
<search>
|
||||
<filter name="filter" string="Hello" domain="[]"/>
|
||||
</search>`;
|
||||
const fields = this.fields;
|
||||
const model = createModel({ arch, fields, });
|
||||
assert.deepEqual(sanitizeFilters(model), [
|
||||
{
|
||||
description: "Hello",
|
||||
domain: "[]",
|
||||
name: "filter",
|
||||
type: "filter",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
QUnit.test('one filter tag with date attribute', async function (assert) {
|
||||
assert.expect(1);
|
||||
const arch = `
|
||||
<search>
|
||||
<filter name="date_filter" string="Date" date="date_field"/>
|
||||
</search>`;
|
||||
const fields = this.fields;
|
||||
const model = createModel({ arch, fields, });
|
||||
const dateFilterId = Object.values(model.get('filters'))[0].id;
|
||||
assert.deepEqual(sanitizeFilters(model), [
|
||||
{
|
||||
defaultOptionId: "this_month",
|
||||
description: "Date",
|
||||
fieldName: "date_field",
|
||||
fieldType: "date",
|
||||
isDateFilter: true,
|
||||
hasOptions: true,
|
||||
name: "date_filter",
|
||||
type: "filter"
|
||||
},
|
||||
{
|
||||
comparisonOptionId: "previous_period",
|
||||
dateFilterId,
|
||||
description: "Date: Previous Period",
|
||||
type: "comparison"
|
||||
},
|
||||
{
|
||||
comparisonOptionId: "previous_year",
|
||||
dateFilterId,
|
||||
description: "Date: Previous Year",
|
||||
type: "comparison"
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
QUnit.test('one groupBy tag', async function (assert) {
|
||||
assert.expect(1);
|
||||
const arch = `
|
||||
<search>
|
||||
<filter name="groupby" string="Hi" context="{ 'group_by': 'date_field:day'}"/>
|
||||
</search>`;
|
||||
const fields = this.fields;
|
||||
const model = createModel({ arch, fields, });
|
||||
assert.deepEqual(sanitizeFilters(model), [
|
||||
{
|
||||
defaultOptionId: "day",
|
||||
description: "Hi",
|
||||
fieldName: "date_field",
|
||||
fieldType: "date",
|
||||
hasOptions: true,
|
||||
name: "groupby",
|
||||
type: "groupBy",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
QUnit.test('two filter tags', async function (assert) {
|
||||
assert.expect(1);
|
||||
const arch = `
|
||||
<search>
|
||||
<filter name="filter_1" string="Hello One" domain="[]"/>
|
||||
<filter name="filter_2" string="Hello Two" domain="[('bar', '=', 3)]"/>
|
||||
</search>`;
|
||||
const fields = this.fields;
|
||||
const model = createModel({ arch, fields, });
|
||||
assert.deepEqual(sanitizeFilters(model), [
|
||||
{
|
||||
description: "Hello One",
|
||||
domain: "[]",
|
||||
name: "filter_1",
|
||||
type: "filter",
|
||||
},
|
||||
{
|
||||
description: "Hello Two",
|
||||
domain: "[('bar', '=', 3)]",
|
||||
name: "filter_2",
|
||||
type: "filter",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
QUnit.test('two filter tags separated by a separator', async function (assert) {
|
||||
assert.expect(1);
|
||||
const arch = `
|
||||
<search>
|
||||
<filter name="filter_1" string="Hello One" domain="[]"/>
|
||||
<separator/>
|
||||
<filter name="filter_2" string="Hello Two" domain="[('bar', '=', 3)]"/>
|
||||
</search>`;
|
||||
const fields = this.fields;
|
||||
const model = createModel({ arch, fields, });
|
||||
assert.deepEqual(sanitizeFilters(model), [
|
||||
{
|
||||
description: "Hello One",
|
||||
domain: "[]",
|
||||
name: "filter_1",
|
||||
type: "filter",
|
||||
},
|
||||
{
|
||||
description: "Hello Two",
|
||||
domain: "[('bar', '=', 3)]",
|
||||
name: "filter_2",
|
||||
type: "filter",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
QUnit.test('one filter tag and one field', async function (assert) {
|
||||
assert.expect(1);
|
||||
const arch = `
|
||||
<search>
|
||||
<filter name="filter" string="Hello" domain="[]"/>
|
||||
<field name="bar"/>
|
||||
</search>`;
|
||||
const fields = this.fields;
|
||||
const model = createModel({ arch, fields, });
|
||||
assert.deepEqual(sanitizeFilters(model), [
|
||||
{
|
||||
description: "Hello",
|
||||
domain: "[]",
|
||||
name: "filter",
|
||||
type: "filter",
|
||||
},
|
||||
{
|
||||
description: "Bar",
|
||||
fieldName: "bar",
|
||||
fieldType: "many2one",
|
||||
type: "field",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
QUnit.test('two field tags', async function (assert) {
|
||||
assert.expect(1);
|
||||
const arch = `
|
||||
<search>
|
||||
<field name="foo"/>
|
||||
<field name="bar"/>
|
||||
</search>`;
|
||||
const fields = this.fields;
|
||||
const model = createModel({ arch, fields, });
|
||||
assert.deepEqual(sanitizeFilters(model), [
|
||||
{
|
||||
description: "Foo",
|
||||
fieldName: "foo",
|
||||
fieldType: "char",
|
||||
type: "field"
|
||||
},
|
||||
{
|
||||
description: "Bar",
|
||||
fieldName: "bar",
|
||||
fieldType: "many2one",
|
||||
type: "field"
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
QUnit.module('Preparing initial state');
|
||||
|
||||
QUnit.test('process favorite filters', async function (assert) {
|
||||
assert.expect(1);
|
||||
const favoriteFilters = [{
|
||||
user_id: [2, "Mitchell Admin"],
|
||||
name: 'Sorted filter',
|
||||
id: 5,
|
||||
context: {
|
||||
group_by: ['foo', 'bar']
|
||||
},
|
||||
sort: '["foo", "-bar"]',
|
||||
domain: "[('user_id', '=', uid)]",
|
||||
}];
|
||||
|
||||
const model = createModel({ favoriteFilters });
|
||||
assert.deepEqual(sanitizeFilters(model), [
|
||||
{
|
||||
context: {},
|
||||
description: "Sorted filter",
|
||||
domain: "[('user_id', '=', uid)]",
|
||||
groupBys: ['foo', 'bar'],
|
||||
orderedBy: [
|
||||
{
|
||||
asc: true,
|
||||
name: "foo"
|
||||
},
|
||||
{
|
||||
asc: false,
|
||||
name: "bar"
|
||||
}
|
||||
],
|
||||
removable: true,
|
||||
serverSideId: 5,
|
||||
type: "favorite",
|
||||
userId: 2
|
||||
},
|
||||
]);
|
||||
|
||||
});
|
||||
|
||||
QUnit.test('process dynamic filters', async function (assert) {
|
||||
assert.expect(1);
|
||||
const dynamicFilters = [{
|
||||
description: 'Quick search',
|
||||
domain: [['id', 'in', [1, 3, 4]]]
|
||||
}];
|
||||
|
||||
const model = createModel({ dynamicFilters });
|
||||
assert.deepEqual(sanitizeFilters(model), [
|
||||
{
|
||||
description: 'Quick search',
|
||||
domain: "[[\"id\",\"in\",[1,3,4]]]",
|
||||
isDefault: true,
|
||||
type: 'filter'
|
||||
},
|
||||
]);
|
||||
|
||||
});
|
||||
|
||||
QUnit.test('ir.filter values', async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
const context = {
|
||||
search_default_filter: true,
|
||||
search_default_bar: 0,
|
||||
search_default_groupby: 2,
|
||||
};
|
||||
const arch = `
|
||||
<search>
|
||||
<filter name="filter" string="Hello" domain="[['foo', '=', 'hello']]"/>
|
||||
<filter name="groupby" string="Goodbye" context="{'group_by': 'foo'}"/>
|
||||
<field name="bar"/>
|
||||
</search>`;
|
||||
const fields = this.fields;
|
||||
const model = createModel({ arch, fields, context });
|
||||
assert.deepEqual(model.get("irFilterValues"), {
|
||||
action_id: undefined,
|
||||
context: {
|
||||
group_by: ["foo"],
|
||||
},
|
||||
domain: '[["foo", "=", "hello"]]',
|
||||
model_id: undefined,
|
||||
sort: "[]",
|
||||
user_id: undefined
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test('falsy search defaults are not activated', async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
const context = {
|
||||
search_default_filter: false,
|
||||
search_default_bar: 0,
|
||||
search_default_groupby: 2,
|
||||
};
|
||||
const arch = `
|
||||
<search>
|
||||
<filter name="filter" string="Hello" domain="[]"/>
|
||||
<filter name="groupby" string="Goodbye" context="{'group_by': 'foo'}"/>
|
||||
<field name="bar"/>
|
||||
</search>`;
|
||||
const fields = this.fields;
|
||||
const model = createModel({ arch, fields, context });
|
||||
// only the truthy filter 'groupby' has isDefault true
|
||||
assert.deepEqual(sanitizeFilters(model), [
|
||||
{
|
||||
description: 'Hello',
|
||||
domain: "[]",
|
||||
name: "filter",
|
||||
type: 'filter',
|
||||
},
|
||||
{
|
||||
description: 'Bar',
|
||||
fieldName: 'bar',
|
||||
fieldType: 'many2one',
|
||||
type: 'field',
|
||||
},
|
||||
{
|
||||
defaultRank: 2,
|
||||
description: 'Goodbye',
|
||||
fieldName: 'foo',
|
||||
fieldType: 'char',
|
||||
isDefault: true,
|
||||
name: "groupby",
|
||||
type: 'groupBy',
|
||||
},
|
||||
]);
|
||||
|
||||
});
|
||||
|
||||
QUnit.test('search defaults on X2M fields', async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
const context = {
|
||||
search_default_otom: [1, 2],
|
||||
search_default_mtom: [1, 2]
|
||||
};
|
||||
const fields = this.fields;
|
||||
fields.otom = { string: "O2M", type: "one2many", relation: 'partner' };
|
||||
fields.mtom = { string: "M2M", type: "many2many", relation: 'partner' };
|
||||
const arch = `
|
||||
<search>
|
||||
<field name="otom"/>
|
||||
<field name="mtom"/>
|
||||
</search>`;
|
||||
const model = createModel({ arch, fields, context });
|
||||
assert.deepEqual(sanitizeFilters(model), [
|
||||
{
|
||||
"defaultAutocompleteValue": {
|
||||
"label": [1, 2],
|
||||
"operator": "ilike",
|
||||
"value": [1, 2]
|
||||
},
|
||||
"defaultRank": -10,
|
||||
"description": "O2M",
|
||||
"fieldName": "otom",
|
||||
"fieldType": "one2many",
|
||||
"isDefault": true,
|
||||
"type": "field"
|
||||
},
|
||||
{
|
||||
"defaultAutocompleteValue": {
|
||||
"label": [1, 2],
|
||||
"operator": "ilike",
|
||||
"value": [1, 2]
|
||||
},
|
||||
"defaultRank": -10,
|
||||
"description": "M2M",
|
||||
"fieldName": "mtom",
|
||||
"fieldType": "many2many",
|
||||
"isDefault": true,
|
||||
"type": "field"
|
||||
}
|
||||
]);
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
|
@ -1,307 +0,0 @@
|
|||
odoo.define('web.control_panel_tests', function (require) {
|
||||
"use strict";
|
||||
|
||||
const testUtils = require('web.test_utils');
|
||||
|
||||
const cpHelpers = require('@web/../tests/search/helpers');
|
||||
const { createControlPanel } = testUtils;
|
||||
|
||||
QUnit.module('ControlPanel', {
|
||||
beforeEach() {
|
||||
this.fields = {
|
||||
display_name: { string: "Displayed name", type: 'char', searchable: true },
|
||||
foo: { string: "Foo", type: "char", default: "My little Foo Value", store: true, sortable: true, searchable: true },
|
||||
date_field: { string: "Date", type: "date", store: true, sortable: true, searchable: true },
|
||||
float_field: { string: "Float", type: "float", searchable: true },
|
||||
bar: { string: "Bar", type: "many2one", relation: 'partner', searchable: true },
|
||||
};
|
||||
}
|
||||
}, function () {
|
||||
|
||||
QUnit.test('default field operator', async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
const fields = {
|
||||
foo_op: { string: "Foo Op", type: "char", store: true, sortable: true, searchable: true },
|
||||
foo: { string: "Foo", type: "char", store: true, sortable: true, searchable: true },
|
||||
bar_op: { string: "Bar Op", type: "many2one", relation: 'partner', searchable: true },
|
||||
bar: { string: "Bar", type: "many2one", relation: 'partner', searchable: true },
|
||||
selec: { string: "Selec", type: "selection", selection: [['red', "Red"], ['black', "Black"]] },
|
||||
};
|
||||
const arch = `
|
||||
<search>
|
||||
<field name="bar"/>
|
||||
<field name="bar_op" operator="child_of"/>
|
||||
<field name="foo"/>
|
||||
<field name="foo_op" operator="="/>
|
||||
<field name="selec"/>
|
||||
</search>`;
|
||||
const searchMenuTypes = [];
|
||||
const params = {
|
||||
cpModelConfig: {
|
||||
arch,
|
||||
fields,
|
||||
context: {
|
||||
show_filterC: true,
|
||||
search_default_bar: 10,
|
||||
search_default_bar_op: 10,
|
||||
search_default_foo: "foo",
|
||||
search_default_foo_op: "foo_op",
|
||||
search_default_selec: 'red',
|
||||
},
|
||||
searchMenuTypes,
|
||||
},
|
||||
cpProps: { fields, searchMenuTypes },
|
||||
env: {
|
||||
session: {
|
||||
async rpc() {
|
||||
return [[10, "Deco Addict"]];
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const controlPanel = await createControlPanel(params);
|
||||
|
||||
assert.deepEqual(
|
||||
cpHelpers.getFacetTexts(controlPanel).map(t => t.replace(/\s/g, "")),
|
||||
[
|
||||
"BarDecoAddict",
|
||||
"BarOpDecoAddict",
|
||||
"Foofoo",
|
||||
"FooOpfoo_op",
|
||||
"SelecRed"
|
||||
]
|
||||
);
|
||||
assert.deepEqual(
|
||||
controlPanel.getQuery().domain,
|
||||
[
|
||||
"&", "&", "&", "&",
|
||||
["bar", "=", 10],
|
||||
["bar_op", "child_of", 10],
|
||||
["foo", "ilike", "foo"],
|
||||
["foo_op", "=", "foo_op"],
|
||||
["selec", "=", "red"],
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.module('Keyboard navigation');
|
||||
|
||||
QUnit.test('remove a facet with backspace', async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
const params = {
|
||||
cpModelConfig: {
|
||||
arch: `<search> <field name="foo"/></search>`,
|
||||
fields: this.fields,
|
||||
context: { search_default_foo: "a" },
|
||||
searchMenuTypes: ['filter'],
|
||||
},
|
||||
cpProps: { fields: this.fields },
|
||||
};
|
||||
|
||||
const controlPanel = await createControlPanel(params);
|
||||
|
||||
assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), ['Foo\na']);
|
||||
|
||||
// delete a facet
|
||||
const searchInput = controlPanel.el.querySelector('input.o_searchview_input');
|
||||
await testUtils.dom.triggerEvent(searchInput, 'keydown', { key: 'Backspace' });
|
||||
|
||||
assert.containsNone(controlPanel, 'div.o_searchview div.o_searchview_facet');
|
||||
|
||||
// delete nothing (should not crash)
|
||||
await testUtils.dom.triggerEvent(searchInput, 'keydown', { key: 'Backspace' });
|
||||
});
|
||||
|
||||
QUnit.test('fields and filters with groups/invisible attribute', async function (assert) {
|
||||
// navigation and automatic menu closure don't work here (i don't know why yet) -->
|
||||
// should be tested separatly
|
||||
assert.expect(16);
|
||||
|
||||
const arch = `
|
||||
<search>
|
||||
<field name="display_name" string="Foo B" invisible="1"/>
|
||||
<field name="foo" string="Foo A"/>
|
||||
<filter name="filterA" string="FA" domain="[]"/>
|
||||
<filter name="filterB" string="FB" invisible="1" domain="[]"/>
|
||||
<filter name="filterC" string="FC" invisible="not context.get('show_filterC')" domain="[]"/>
|
||||
<filter name="groupByA" string="GA" context="{ 'group_by': 'date_field:day' }"/>
|
||||
<filter name="groupByB" string="GB" context="{ 'group_by': 'date_field:day' }" invisible="1"/>
|
||||
</search>`;
|
||||
const searchMenuTypes = ['filter', 'groupBy'];
|
||||
const params = {
|
||||
cpModelConfig: {
|
||||
arch,
|
||||
fields: this.fields,
|
||||
context: {
|
||||
show_filterC: true,
|
||||
search_default_display_name: 'value',
|
||||
search_default_filterB: true,
|
||||
search_default_groupByB: true
|
||||
},
|
||||
searchMenuTypes
|
||||
},
|
||||
cpProps: { fields: this.fields, searchMenuTypes },
|
||||
};
|
||||
const controlPanel = await createControlPanel(params);
|
||||
|
||||
function selectorContainsValue(selector, value, shouldContain) {
|
||||
const elements = [...controlPanel.el.querySelectorAll(selector)];
|
||||
const regExp = new RegExp(value);
|
||||
const matches = elements.filter(el => regExp.test(el.innerText.replace(/\s/g, "")));
|
||||
assert.strictEqual(matches.length, shouldContain ? 1 : 0,
|
||||
`${selector} in the control panel should${shouldContain ? '' : ' not'} contain "${value}".`
|
||||
);
|
||||
}
|
||||
|
||||
// default filters/fields should be activated even if invisible
|
||||
assert.containsN(controlPanel, 'div.o_searchview_facet', 3);
|
||||
selectorContainsValue('.o_searchview_facet', "FooBvalue", true);
|
||||
selectorContainsValue('.o_searchview_facet .o_facet_values', "FB", true);
|
||||
selectorContainsValue('.o_searchview_facet .o_facet_values', "GB", true);
|
||||
|
||||
await cpHelpers.toggleFilterMenu(controlPanel);
|
||||
|
||||
selectorContainsValue('.o_menu_item', "FA", true);
|
||||
selectorContainsValue('.o_menu_item', "FB", false);
|
||||
selectorContainsValue('.o_menu_item', "FC", true);
|
||||
|
||||
await cpHelpers.toggleGroupByMenu(controlPanel);
|
||||
|
||||
selectorContainsValue('.o_menu_item', "GA", true);
|
||||
selectorContainsValue('.o_menu_item', "GB", false);
|
||||
|
||||
// 'a' to filter nothing on bar
|
||||
await cpHelpers.editSearch(controlPanel, 'a');
|
||||
|
||||
// the only item in autocomplete menu should be FooA: a
|
||||
selectorContainsValue('.o_searchview_autocomplete', "SearchFooAfor:a", true);
|
||||
await cpHelpers.validateSearch(controlPanel);
|
||||
selectorContainsValue('.o_searchview_facet', "FooAa", true);
|
||||
|
||||
// The items in the Filters menu and the Group By menu should be the same as before
|
||||
await cpHelpers.toggleFilterMenu(controlPanel);
|
||||
|
||||
selectorContainsValue('.o_menu_item', "FA", true);
|
||||
selectorContainsValue('.o_menu_item', "FB", false);
|
||||
selectorContainsValue('.o_menu_item', "FC", true);
|
||||
|
||||
await cpHelpers.toggleGroupByMenu(controlPanel);
|
||||
|
||||
selectorContainsValue('.o_menu_item', "GA", true);
|
||||
selectorContainsValue('.o_menu_item', "GB", false);
|
||||
});
|
||||
|
||||
QUnit.test('invisible fields and filters with unknown related fields should not be rendered', async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
// This test case considers that the current user is not a member of
|
||||
// the "base.group_system" group and both "bar" and "date_field" fields
|
||||
// have field-level access control that limit access to them only from
|
||||
// that group.
|
||||
//
|
||||
// As MockServer currently does not support "groups" access control, we:
|
||||
//
|
||||
// - emulate field-level access control of fields_get() by removing
|
||||
// "bar" and "date_field" from the model fields
|
||||
// - set filters with groups="base.group_system" as `invisible=1` in
|
||||
// view to emulate the behavior of fields_view_get()
|
||||
// [see ir.ui.view `_apply_group()`]
|
||||
|
||||
delete this.fields.bar;
|
||||
delete this.fields.date_field;
|
||||
|
||||
const searchMenuTypes = [];
|
||||
const params = {
|
||||
cpProps: { fields: this.fields, searchMenuTypes },
|
||||
};
|
||||
const controlPanel = await createControlPanel(params);
|
||||
|
||||
assert.containsNone(controlPanel.el, 'div.o_search_options div.o_filter_menu',
|
||||
"there should not be filter dropdown");
|
||||
assert.containsNone(controlPanel.el, 'div.o_search_options div.o_group_by_menu',
|
||||
"there should not be groupby dropdown");
|
||||
});
|
||||
|
||||
QUnit.test('groupby menu is not rendered if searchMenuTypes does not have groupBy', async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
const arch = `<search/>`;
|
||||
const searchMenuTypes = ['filter'];
|
||||
const params = {
|
||||
cpModelConfig: {
|
||||
arch,
|
||||
fields: this.fields,
|
||||
searchMenuTypes,
|
||||
},
|
||||
cpProps: { fields: this.fields, searchMenuTypes },
|
||||
};
|
||||
const controlPanel = await createControlPanel(params);
|
||||
|
||||
assert.containsOnce(controlPanel.el, 'div.o_search_options div.o_filter_menu');
|
||||
assert.containsNone(controlPanel.el, 'div.o_search_options div.o_group_by_menu');
|
||||
});
|
||||
|
||||
QUnit.test('search field should be autofocused', async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
const controlPanel = await createControlPanel({
|
||||
model: 'partner',
|
||||
arch: '<search/>',
|
||||
data: this.data,
|
||||
env: {
|
||||
device: {
|
||||
isMobileDevice: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
assert.containsOnce(controlPanel, '.o_searchview_input', "has a search field");
|
||||
assert.containsOnce(controlPanel, '.o_searchview_input:focus-within',
|
||||
"has autofocused search field");
|
||||
});
|
||||
|
||||
QUnit.test("search field's autofocus should be disabled on mobile device", async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
const controlPanel = await createControlPanel({
|
||||
model: 'partner',
|
||||
arch: '<search/>',
|
||||
data: this.data,
|
||||
env: {
|
||||
device: {
|
||||
isMobileDevice: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
assert.containsOnce(controlPanel, '.o_searchview_input', "has a search field");
|
||||
assert.containsNone(controlPanel, '.o_searchview_input:focus-within',
|
||||
"hasn't autofocused search field");
|
||||
});
|
||||
|
||||
QUnit.test("dynamic domains evaluation using global context", async function (assert) {
|
||||
const arch = `
|
||||
<search>
|
||||
<filter name="filter" domain="[('date_deadline', '<', context.get('my_date'))]"/>
|
||||
</search>
|
||||
`;
|
||||
const context = {
|
||||
search_default_filter: true,
|
||||
my_date: "2021-09-17",
|
||||
};
|
||||
const fields = this.fields;
|
||||
const searchMenuTypes = ['filter'];
|
||||
const controlPanel = await createControlPanel({
|
||||
cpModelConfig: { arch, fields, searchMenuTypes, context },
|
||||
cpProps: { fields, searchMenuTypes },
|
||||
});
|
||||
assert.deepEqual(
|
||||
controlPanel.getQuery().domain,
|
||||
[['date_deadline', '<', "2021-09-17"]]
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
|
@ -1,624 +0,0 @@
|
|||
odoo.define('web.filter_menu_generator_tests', function (require) {
|
||||
"use strict";
|
||||
|
||||
const Domain = require('web.Domain');
|
||||
const CustomFilterItem = require('web.CustomFilterItem');
|
||||
const ActionModel = require('web.ActionModel');
|
||||
const pyUtils = require('web.py_utils');
|
||||
const testUtils = require('web.test_utils');
|
||||
|
||||
const { getFixture } = require('@web/../tests/helpers/utils');
|
||||
const cpHelpers = require('@web/../tests/search/helpers');
|
||||
const { createComponent } = testUtils;
|
||||
|
||||
const toggleAddCustomFilterStandalone = async (el) => {
|
||||
await cpHelpers.toggleMenu(el, "Add Custom Filter");
|
||||
};
|
||||
|
||||
let target;
|
||||
|
||||
QUnit.module('Components', {
|
||||
beforeEach: function () {
|
||||
this.fields = {
|
||||
date_field: { name: 'date_field', string: "A date", type: 'date', searchable: true },
|
||||
date_time_field: { name: 'date_time_field', string: "DateTime", type: 'datetime', searchable: true },
|
||||
boolean_field: { name: 'boolean_field', string: "Boolean Field", type: 'boolean', default: true, searchable: true },
|
||||
binary_field: { name: 'binary_field', string: "Binary Field", type: 'binary', searchable: true },
|
||||
char_field: { name: 'char_field', string: "Char Field", type: 'char', default: "foo", trim: true, searchable: true },
|
||||
float_field: { name: 'float_field', string: "Floaty McFloatface", type: 'float', searchable: true },
|
||||
color: { name: 'color', string: "Color", type: 'selection', selection: [['black', "Black"], ['white', "White"]], searchable: true },
|
||||
};
|
||||
target = getFixture();
|
||||
},
|
||||
}, function () {
|
||||
|
||||
QUnit.module('CustomFilterItem (legacy)');
|
||||
|
||||
QUnit.test('basic rendering', async function (assert) {
|
||||
assert.expect(16);
|
||||
|
||||
await createComponent(CustomFilterItem, {
|
||||
props: {
|
||||
fields: this.fields,
|
||||
},
|
||||
env: {
|
||||
searchModel: new ActionModel(),
|
||||
},
|
||||
});
|
||||
|
||||
assert.strictEqual(target.querySelector(".dropdown").innerText.trim(), "Add Custom Filter");
|
||||
assert.hasClass(target.querySelector(".dropdown"), "o_add_custom_filter_menu");
|
||||
assert.strictEqual(target.querySelector(".dropdown").children.length, 1);
|
||||
|
||||
await toggleAddCustomFilterStandalone(target);
|
||||
|
||||
// Single condition
|
||||
assert.containsOnce(target, '.o_filter_condition');
|
||||
assert.containsOnce(target, '.o_filter_condition select.o_generator_menu_field');
|
||||
assert.containsOnce(target, '.o_filter_condition select.o_generator_menu_operator');
|
||||
assert.containsOnce(target, '.o_filter_condition span.o_generator_menu_value');
|
||||
assert.containsNone(target, '.o_filter_condition .o_or_filter');
|
||||
assert.containsNone(target, '.o_filter_condition .o_generator_menu_delete');
|
||||
|
||||
// no deletion allowed on single condition
|
||||
assert.containsNone(target, '.o_filter_condition > i.o_generator_menu_delete');
|
||||
|
||||
// Buttons
|
||||
assert.containsOnce(target, 'button.o_apply_filter');
|
||||
assert.containsOnce(target, 'button.o_add_condition');
|
||||
|
||||
assert.containsOnce(target, '.o_filter_condition');
|
||||
|
||||
await cpHelpers.addCondition(target);
|
||||
|
||||
assert.containsN(target, '.o_filter_condition', 2);
|
||||
assert.containsOnce(target, '.o_filter_condition .o_or_filter');
|
||||
assert.containsN(target, '.o_filter_condition .o_generator_menu_delete', 2);
|
||||
});
|
||||
|
||||
QUnit.test('custom OR filter presets new condition from preceding', async function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
const searchModel = new ActionModel();
|
||||
await createComponent(CustomFilterItem, {
|
||||
props: {
|
||||
fields: this.fields,
|
||||
},
|
||||
env: { searchModel },
|
||||
});
|
||||
|
||||
// Open custom filter form
|
||||
await toggleAddCustomFilterStandalone(target);
|
||||
|
||||
// Retrieve second selectable values for field and operator dropdowns
|
||||
const fieldSecondValue = target.querySelector('.o_generator_menu_field option:nth-of-type(2)').value;
|
||||
const operatorSecondValue = target.querySelector('.o_generator_menu_operator option:nth-of-type(2)').value;
|
||||
|
||||
// Check if they really exist…
|
||||
assert.ok(!!fieldSecondValue);
|
||||
assert.ok(!!operatorSecondValue);
|
||||
|
||||
// Add first filter condition
|
||||
await testUtils.fields.editSelect(target.querySelector('.o_generator_menu_field'), fieldSecondValue);
|
||||
await cpHelpers.editConditionOperator(target, 0, operatorSecondValue);
|
||||
|
||||
// Add a second conditon on the filter being created
|
||||
await cpHelpers.addCondition(target);
|
||||
|
||||
// Check the defaults for field and operator dropdowns
|
||||
assert.strictEqual(
|
||||
target.querySelector('.o_filter_condition:nth-of-type(2) .o_generator_menu_field').value,
|
||||
fieldSecondValue
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector('.o_filter_condition:nth-of-type(2) .o_generator_menu_operator').value,
|
||||
operatorSecondValue
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test('binary field: basic search', async function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
let expectedFilters;
|
||||
class MockedSearchModel extends ActionModel {
|
||||
dispatch(method, ...args) {
|
||||
assert.strictEqual(method, 'createNewFilters');
|
||||
const preFilters = args[0];
|
||||
assert.deepEqual(preFilters, expectedFilters);
|
||||
}
|
||||
}
|
||||
const searchModel = new MockedSearchModel();
|
||||
await createComponent(CustomFilterItem, {
|
||||
props: {
|
||||
fields: this.fields,
|
||||
},
|
||||
env: { searchModel },
|
||||
});
|
||||
|
||||
// Default value
|
||||
expectedFilters = [{
|
||||
description: 'Binary Field is set',
|
||||
domain: '[["binary_field","!=",False]]',
|
||||
type: 'filter',
|
||||
}];
|
||||
await toggleAddCustomFilterStandalone(target);
|
||||
await testUtils.fields.editSelect(target.querySelector('.o_generator_menu_field'), 'binary_field');
|
||||
await cpHelpers.applyFilter(target);
|
||||
|
||||
// Updated value
|
||||
expectedFilters = [{
|
||||
description: 'Binary Field is not set',
|
||||
domain: '[["binary_field","=",False]]',
|
||||
type: 'filter',
|
||||
}];
|
||||
await testUtils.fields.editSelect(target.querySelector('.o_generator_menu_field'), 'binary_field');
|
||||
await cpHelpers.editConditionOperator(target, 0, '=');
|
||||
await cpHelpers.applyFilter(target);
|
||||
});
|
||||
|
||||
QUnit.test('selection field: default and updated value', async function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
let expectedFilters;
|
||||
class MockedSearchModel extends ActionModel {
|
||||
dispatch(method, ...args) {
|
||||
assert.strictEqual(method, 'createNewFilters');
|
||||
const preFilters = args[0];
|
||||
assert.deepEqual(preFilters, expectedFilters);
|
||||
}
|
||||
}
|
||||
const searchModel = new MockedSearchModel();
|
||||
await createComponent(CustomFilterItem, {
|
||||
props: {
|
||||
fields: this.fields,
|
||||
},
|
||||
env: { searchModel },
|
||||
});
|
||||
|
||||
// Default value
|
||||
expectedFilters = [{
|
||||
description: 'Color is "Black"',
|
||||
domain: '[["color","=","black"]]',
|
||||
type: 'filter',
|
||||
}];
|
||||
await toggleAddCustomFilterStandalone(target);
|
||||
await testUtils.fields.editSelect(target.querySelector('.o_generator_menu_field'), 'color');
|
||||
await cpHelpers.applyFilter(target);
|
||||
|
||||
// Updated value
|
||||
expectedFilters = [{
|
||||
description: 'Color is "White"',
|
||||
domain: '[["color","=","white"]]',
|
||||
type: 'filter',
|
||||
}];
|
||||
await testUtils.fields.editSelect(target.querySelector('.o_generator_menu_field'), 'color');
|
||||
await testUtils.fields.editSelect(target.querySelector('.o_generator_menu_value select'), 'white');
|
||||
await cpHelpers.applyFilter(target);
|
||||
});
|
||||
QUnit.test('selection field: no value', async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
this.fields.color.selection = [];
|
||||
let expectedFilters;
|
||||
class MockedSearchModel extends ActionModel {
|
||||
dispatch(method, ...args) {
|
||||
assert.strictEqual(method, 'createNewFilters');
|
||||
const preFilters = args[0];
|
||||
assert.deepEqual(preFilters, expectedFilters);
|
||||
}
|
||||
}
|
||||
const searchModel = new MockedSearchModel();
|
||||
const cfi = await createComponent(CustomFilterItem, {
|
||||
props: {
|
||||
fields: this.fields,
|
||||
},
|
||||
env: { searchModel },
|
||||
});
|
||||
|
||||
// Default value
|
||||
expectedFilters = [{
|
||||
description: 'Color is ""',
|
||||
domain: '[["color","=",""]]',
|
||||
type: 'filter',
|
||||
}];
|
||||
await toggleAddCustomFilterStandalone(cfi);
|
||||
await testUtils.fields.editSelect(cfi.el.querySelector('.o_generator_menu_field'), 'color');
|
||||
await cpHelpers.applyFilter(cfi);
|
||||
})
|
||||
|
||||
QUnit.test('adding a simple filter works', async function (assert) {
|
||||
assert.expect(7);
|
||||
|
||||
delete this.fields.date_field;
|
||||
|
||||
class MockedSearchModel extends ActionModel {
|
||||
dispatch(method, ...args) {
|
||||
assert.strictEqual(method, 'createNewFilters');
|
||||
const preFilters = args[0];
|
||||
const preFilter = preFilters[0];
|
||||
assert.strictEqual(preFilter.type, 'filter');
|
||||
assert.strictEqual(preFilter.description, 'Boolean Field is Yes');
|
||||
assert.strictEqual(preFilter.domain, '[["boolean_field","=",True]]');
|
||||
}
|
||||
}
|
||||
const searchModel = new MockedSearchModel();
|
||||
await createComponent(CustomFilterItem, {
|
||||
props: {
|
||||
fields: this.fields,
|
||||
},
|
||||
env: { searchModel },
|
||||
});
|
||||
|
||||
await toggleAddCustomFilterStandalone(target);
|
||||
await testUtils.fields.editSelect(target.querySelector('.o_generator_menu_field'), 'boolean_field');
|
||||
await cpHelpers.applyFilter(target);
|
||||
|
||||
// The only things visible should be the button 'Add Custom Filter' and the menu;
|
||||
assert.strictEqual(target.querySelector(".dropdown").children.length, 2);
|
||||
assert.containsOnce(target, 'button.dropdown-toggle');
|
||||
assert.containsOnce(target, '.dropdown-menu');
|
||||
});
|
||||
|
||||
QUnit.test('filtering by ID interval works', async function (assert) {
|
||||
assert.expect(4);
|
||||
this.fields.id_field = { name: 'id_field', string: "ID", type: "id", searchable: true };
|
||||
|
||||
const expectedDomains = [
|
||||
[['id_field','>', 10]],
|
||||
[['id_field','<=', 20]],
|
||||
];
|
||||
|
||||
class MockedSearchModel extends ActionModel {
|
||||
dispatch(method, ...args) {
|
||||
assert.strictEqual(method, 'createNewFilters');
|
||||
const preFilters = args[0];
|
||||
const preFilter = preFilters[0];
|
||||
// this step combine a tokenization/parsing followed by a string formatting
|
||||
let domain = pyUtils.assembleDomains([preFilter.domain]);
|
||||
domain = Domain.prototype.stringToArray(domain);
|
||||
assert.deepEqual(domain, expectedDomains.shift());
|
||||
}
|
||||
}
|
||||
const searchModel = new MockedSearchModel();
|
||||
await createComponent(CustomFilterItem, {
|
||||
props: {
|
||||
fields: this.fields,
|
||||
},
|
||||
env: { searchModel },
|
||||
});
|
||||
|
||||
async function testValue(operator, value) {
|
||||
// open filter menu generator, select ID field, switch operator, type value, then click apply
|
||||
await cpHelpers.editConditionField(target, 0, 'id_field');
|
||||
await cpHelpers.editConditionOperator(target, 0, operator);
|
||||
await cpHelpers.editConditionValue(target, 0,
|
||||
value
|
||||
);
|
||||
await cpHelpers.applyFilter(target);
|
||||
}
|
||||
|
||||
await toggleAddCustomFilterStandalone(target);
|
||||
for (const domain of [...expectedDomains]) {
|
||||
await testValue(domain[0][1], domain[0][2]);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
QUnit.test('commit search with an extended proposition with field char does not cause a crash', async function (assert) {
|
||||
assert.expect(12);
|
||||
|
||||
this.fields.many2one_field = { name: 'many2one_field', string: "Trululu", type: "many2one", searchable: true };
|
||||
const expectedDomains = [
|
||||
[['many2one_field', 'ilike', `a`]],
|
||||
[['many2one_field', 'ilike', `"a"`]],
|
||||
[['many2one_field', 'ilike', `'a'`]],
|
||||
[['many2one_field', 'ilike', `'`]],
|
||||
[['many2one_field', 'ilike', `"`]],
|
||||
[['many2one_field', 'ilike', `\\`]],
|
||||
];
|
||||
const testedValues = [`a`, `"a"`, `'a'`, `'`, `"`, `\\`];
|
||||
|
||||
class MockedSearchModel extends ActionModel {
|
||||
dispatch(method, ...args) {
|
||||
assert.strictEqual(method, 'createNewFilters');
|
||||
const preFilters = args[0];
|
||||
const preFilter = preFilters[0];
|
||||
// this step combine a tokenization/parsing followed by a string formatting
|
||||
let domain = pyUtils.assembleDomains([preFilter.domain]);
|
||||
domain = Domain.prototype.stringToArray(domain);
|
||||
assert.deepEqual(domain, expectedDomains.shift());
|
||||
}
|
||||
}
|
||||
const searchModel = new MockedSearchModel();
|
||||
await createComponent(CustomFilterItem, {
|
||||
props: {
|
||||
fields: this.fields,
|
||||
},
|
||||
env: { searchModel },
|
||||
});
|
||||
|
||||
async function testValue(value) {
|
||||
// open filter menu generator, select trululu field and enter string `a`, then click apply
|
||||
await cpHelpers.editConditionField(target, 0, 'many2one_field');
|
||||
await cpHelpers.editConditionValue(target, 0,
|
||||
value
|
||||
);
|
||||
await cpHelpers.applyFilter(target);
|
||||
}
|
||||
|
||||
await toggleAddCustomFilterStandalone(target);
|
||||
for (const value of testedValues) {
|
||||
await testValue(value);
|
||||
}
|
||||
|
||||
delete ActionModel.registry.map.testExtension;
|
||||
});
|
||||
|
||||
QUnit.test('custom filter datetime with equal operator', async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
class MockedSearchModel extends ActionModel {
|
||||
dispatch(method, ...args) {
|
||||
assert.strictEqual(method, 'createNewFilters');
|
||||
const preFilters = args[0];
|
||||
const preFilter = preFilters[0];
|
||||
assert.strictEqual(preFilter.description,
|
||||
'DateTime is equal to "02/22/2017 11:00:00"',
|
||||
"description should be in localized format");
|
||||
assert.deepEqual(preFilter.domain,
|
||||
'[["date_time_field","=","2017-02-22 15:00:00"]]',
|
||||
"domain should be in UTC format");
|
||||
}
|
||||
}
|
||||
const searchModel = new MockedSearchModel();
|
||||
await createComponent(CustomFilterItem, {
|
||||
props: {
|
||||
fields: this.fields,
|
||||
},
|
||||
session: {
|
||||
getTZOffset() {
|
||||
return -240;
|
||||
},
|
||||
},
|
||||
env: { searchModel },
|
||||
});
|
||||
|
||||
await toggleAddCustomFilterStandalone(target);
|
||||
await testUtils.fields.editSelect(target.querySelector('.o_generator_menu_field'), 'date_time_field');
|
||||
|
||||
assert.strictEqual(target.querySelector('.o_generator_menu_field').value, 'date_time_field');
|
||||
assert.strictEqual(target.querySelector('.o_generator_menu_operator').value, 'between');
|
||||
|
||||
await cpHelpers.editConditionOperator(target, 0, '=');
|
||||
await testUtils.fields.editSelect(target.querySelector('.o_filter_condition span.o_generator_menu_value input'), '02/22/2017 11:00:00'); // in TZ
|
||||
await cpHelpers.applyFilter(target);
|
||||
});
|
||||
|
||||
QUnit.test('custom filter datetime between operator', async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
class MockedSearchModel extends ActionModel {
|
||||
dispatch(method, ...args) {
|
||||
assert.strictEqual(method, 'createNewFilters');
|
||||
const preFilters = args[0];
|
||||
const preFilter = preFilters[0];
|
||||
assert.strictEqual(preFilter.description,
|
||||
'DateTime is between "02/22/2017 11:00:00 and 02/22/2017 17:00:00"',
|
||||
"description should be in localized format");
|
||||
assert.deepEqual(preFilter.domain,
|
||||
'[["date_time_field",">=","2017-02-22 15:00:00"]' +
|
||||
',["date_time_field","<=","2017-02-22 21:00:00"]]',
|
||||
"domain should be in UTC format");
|
||||
}
|
||||
}
|
||||
const searchModel = new MockedSearchModel();
|
||||
await createComponent(CustomFilterItem, {
|
||||
props: {
|
||||
fields: this.fields,
|
||||
},
|
||||
session: {
|
||||
getTZOffset() {
|
||||
return -240;
|
||||
},
|
||||
},
|
||||
env: { searchModel },
|
||||
});
|
||||
|
||||
await toggleAddCustomFilterStandalone(target);
|
||||
await testUtils.fields.editSelect(target.querySelector('.o_generator_menu_field'), 'date_time_field');
|
||||
|
||||
assert.strictEqual(target.querySelector('.o_generator_menu_field').value, 'date_time_field');
|
||||
assert.strictEqual(target.querySelector('.o_generator_menu_operator').value, 'between');
|
||||
|
||||
const valueInputs = target.querySelectorAll('.o_generator_menu_value .o_input');
|
||||
await testUtils.fields.editSelect(valueInputs[0], '02/22/2017 11:00:00'); // in TZ
|
||||
await testUtils.fields.editSelect(valueInputs[1], '02-22-2017 17:00:00'); // in TZ
|
||||
await cpHelpers.applyFilter(target);
|
||||
});
|
||||
|
||||
QUnit.test('default custom filter datetime', async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
class MockedSearchModel extends ActionModel {
|
||||
dispatch(method, ...args) {
|
||||
assert.strictEqual(method, 'createNewFilters');
|
||||
const domain = JSON.parse(args[0][0].domain);
|
||||
assert.strictEqual(domain[0][2].split(' ')[1],
|
||||
'04:00:00',
|
||||
"domain should be in UTC format");
|
||||
assert.strictEqual(domain[1][2].split(' ')[1],
|
||||
'03:59:59',
|
||||
"domain should be in UTC format");
|
||||
}
|
||||
}
|
||||
const searchModel = new MockedSearchModel();
|
||||
await createComponent(CustomFilterItem, {
|
||||
props: {
|
||||
fields: this.fields,
|
||||
},
|
||||
session: {
|
||||
getTZOffset() {
|
||||
return -240;
|
||||
},
|
||||
},
|
||||
env: { searchModel },
|
||||
});
|
||||
|
||||
await toggleAddCustomFilterStandalone(target);
|
||||
await testUtils.fields.editSelect(target.querySelector('.o_generator_menu_field'), 'date_time_field');
|
||||
|
||||
assert.strictEqual(target.querySelector('.o_generator_menu_field').value, 'date_time_field');
|
||||
assert.strictEqual(target.querySelector('.o_generator_menu_operator').value, 'between');
|
||||
|
||||
await cpHelpers.applyFilter(target);
|
||||
});
|
||||
|
||||
QUnit.test('input value parsing', async function (assert) {
|
||||
assert.expect(7);
|
||||
|
||||
await createComponent(CustomFilterItem, {
|
||||
props: {
|
||||
fields: this.fields,
|
||||
},
|
||||
env: {
|
||||
searchModel: new ActionModel(),
|
||||
},
|
||||
});
|
||||
|
||||
await toggleAddCustomFilterStandalone(target);
|
||||
await cpHelpers.addCondition(target);
|
||||
|
||||
await cpHelpers.editConditionField(target, 0, "float_field");
|
||||
await cpHelpers.editConditionField(target, 1, "id");
|
||||
|
||||
const [floatInput, idInput] = target.querySelectorAll('.o_generator_menu_value .o_input');
|
||||
|
||||
// Default values
|
||||
assert.strictEqual(floatInput.value, "0.0");
|
||||
assert.strictEqual(idInput.value, "0");
|
||||
|
||||
// Float parsing
|
||||
await cpHelpers.editConditionValue(target, 0, "4.2");
|
||||
assert.strictEqual(floatInput.value, "4.2");
|
||||
await cpHelpers.editConditionValue(target, 0, "DefinitelyValidFloat");
|
||||
// String input in a number input gives "", which is parsed as 0
|
||||
assert.strictEqual(floatInput.value, "0.0");
|
||||
|
||||
// Number parsing
|
||||
await cpHelpers.editConditionValue(target, 1, "4");
|
||||
assert.strictEqual(idInput.value, "4");
|
||||
await cpHelpers.editConditionValue(target, 1, "4.2");
|
||||
assert.strictEqual(idInput.value, "4");
|
||||
await cpHelpers.editConditionValue(target, 1, "DefinitelyValidID");
|
||||
// String input in a number input gives "", which is parsed as 0
|
||||
assert.strictEqual(idInput.value, "0");
|
||||
});
|
||||
|
||||
QUnit.test('input value parsing with language', async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
await createComponent(CustomFilterItem, {
|
||||
props: {
|
||||
fields: this.fields,
|
||||
},
|
||||
env: {
|
||||
searchModel: new ActionModel(),
|
||||
_t: Object.assign(s => s, { database: { parameters: { decimal_point: "," } }}),
|
||||
},
|
||||
translateParameters: {
|
||||
decimal_point: ",",
|
||||
thousands_sep: "",
|
||||
grouping: [3, 0],
|
||||
},
|
||||
});
|
||||
|
||||
await toggleAddCustomFilterStandalone(target);
|
||||
await cpHelpers.addCondition(target);
|
||||
|
||||
await cpHelpers.editConditionField(target, 0, "float_field");
|
||||
|
||||
const [floatInput] = target.querySelectorAll('.o_generator_menu_value .o_input');
|
||||
|
||||
// Default values
|
||||
assert.strictEqual(floatInput.value, "0,0");
|
||||
|
||||
// Float parsing
|
||||
await cpHelpers.editConditionValue(target, 0, '4,');
|
||||
assert.strictEqual(floatInput.value, "4,");
|
||||
await cpHelpers.editConditionValue(target, 0, '4,2');
|
||||
assert.strictEqual(floatInput.value, "4,2");
|
||||
await cpHelpers.editConditionValue(target, 0, '4,2,');
|
||||
assert.strictEqual(floatInput.value, "4,2");
|
||||
await cpHelpers.editConditionValue(target, 0, "DefinitelyValidFloat");
|
||||
// The input here is a string, resulting in a parsing error instead of 0
|
||||
assert.strictEqual(floatInput.value, "4,2");
|
||||
});
|
||||
|
||||
QUnit.test('add custom filter with multiple values', async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
class MockedSearchModel extends ActionModel {
|
||||
dispatch(method, ...args) {
|
||||
assert.strictEqual(method, 'createNewFilters');
|
||||
const preFilters = args[0];
|
||||
const expected = [
|
||||
{
|
||||
description: 'A date is equal to "01/09/1997"',
|
||||
domain: '[["date_field","=","1997-01-09"]]',
|
||||
type: "filter",
|
||||
},
|
||||
{
|
||||
description: 'Boolean Field is No',
|
||||
domain: '[["boolean_field","!=",True]]',
|
||||
type: "filter",
|
||||
},
|
||||
{
|
||||
description: 'Floaty McFloatface is equal to "7.2"',
|
||||
domain: '[["float_field","=",7.2]]',
|
||||
type: "filter",
|
||||
},
|
||||
{
|
||||
description: 'ID is "9"',
|
||||
domain: '[["id","=",9]]',
|
||||
type: "filter",
|
||||
},
|
||||
];
|
||||
assert.deepEqual(preFilters, expected,
|
||||
"Conditions should be in the correct order witht the right values.");
|
||||
}
|
||||
}
|
||||
const searchModel = new MockedSearchModel();
|
||||
await createComponent(CustomFilterItem, {
|
||||
props: {
|
||||
fields: this.fields,
|
||||
},
|
||||
env: { searchModel },
|
||||
});
|
||||
|
||||
await toggleAddCustomFilterStandalone(target);
|
||||
await cpHelpers.addCondition(target);
|
||||
await cpHelpers.addCondition(target);
|
||||
await cpHelpers.addCondition(target);
|
||||
await cpHelpers.addCondition(target);
|
||||
|
||||
await cpHelpers.editConditionField(target, 0, 'date_field');
|
||||
await cpHelpers.editConditionValue(target, 0, '01/09/1997');
|
||||
|
||||
await cpHelpers.editConditionField(target, 1, 'boolean_field');
|
||||
await cpHelpers.editConditionOperator(target, 1, '!=');
|
||||
|
||||
await cpHelpers.editConditionField(target, 2, 'char_field');
|
||||
await cpHelpers.editConditionValue(target, 2, "I will be deleted anyway");
|
||||
|
||||
await cpHelpers.editConditionField(target, 3, 'float_field');
|
||||
await cpHelpers.editConditionValue(target, 3, 7.2);
|
||||
|
||||
await cpHelpers.editConditionField(target, 4, 'id');
|
||||
await cpHelpers.editConditionValue(target, 4, 9);
|
||||
|
||||
await cpHelpers.removeCondition(target, 2);
|
||||
|
||||
await cpHelpers.applyFilter(target);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,626 +0,0 @@
|
|||
odoo.define('web.favorite_menu_tests', function (require) {
|
||||
"use strict";
|
||||
|
||||
const { browser } = require('@web/core/browser/browser');
|
||||
const { patchWithCleanup } = require('@web/../tests/helpers/utils');
|
||||
|
||||
const FormView = require('web.FormView');
|
||||
const testUtils = require('web.test_utils');
|
||||
|
||||
const cpHelpers = require('@web/../tests/search/helpers');
|
||||
const { makeLegacyDialogMappingTestEnv } = require('@web/../tests/helpers/legacy_env_utils');
|
||||
const { createControlPanel, createView, mock } = testUtils;
|
||||
const { patchDate } = mock;
|
||||
|
||||
const searchMenuTypes = ['favorite'];
|
||||
|
||||
QUnit.module('Components', {
|
||||
beforeEach: function () {
|
||||
this.fields = {
|
||||
bar: { string: "Bar", type: "many2one", relation: 'partner' },
|
||||
birthday: { string: "Birthday", type: "date", store: true, sortable: true },
|
||||
date_field: { string: "Date", type: "date", store: true, sortable: true },
|
||||
float_field: { string: "Float", type: "float", group_operator: 'sum' },
|
||||
foo: { string: "Foo", type: "char", store: true, sortable: true },
|
||||
};
|
||||
patchWithCleanup(browser, {
|
||||
setTimeout: (fn) => fn(),
|
||||
});
|
||||
},
|
||||
}, function () {
|
||||
|
||||
QUnit.module('FavoriteMenu (legacy)');
|
||||
|
||||
QUnit.test('simple rendering with no favorite', async function (assert) {
|
||||
assert.expect(8);
|
||||
|
||||
const params = {
|
||||
cpModelConfig: { searchMenuTypes },
|
||||
cpProps: { fields: this.fields, searchMenuTypes, action: { name: "Action Name" } },
|
||||
};
|
||||
const controlPanel = await createControlPanel(params);
|
||||
|
||||
assert.containsOnce(controlPanel, 'div.o_favorite_menu > button i.fa.fa-star');
|
||||
assert.strictEqual(controlPanel.el.querySelector('div.o_favorite_menu > button span').innerText.trim(), "Favorites");
|
||||
|
||||
await cpHelpers.toggleFavoriteMenu(controlPanel);
|
||||
assert.containsNone(controlPanel, '.dropdown-divider');
|
||||
assert.containsOnce(controlPanel, '.o_add_favorite');
|
||||
assert.strictEqual(controlPanel.el.querySelector('.o_add_favorite > button').innerText.trim(),
|
||||
"Save current search");
|
||||
|
||||
await cpHelpers.toggleSaveFavorite(controlPanel);
|
||||
assert.strictEqual(
|
||||
controlPanel.el.querySelector('.o_add_favorite input[type="text"]').value,
|
||||
'Action Name'
|
||||
);
|
||||
assert.containsN(controlPanel, '.o_add_favorite .form-check input[type="checkbox"]', 2);
|
||||
const labelEls = controlPanel.el.querySelectorAll('.o_add_favorite .form-check label');
|
||||
assert.deepEqual(
|
||||
[...labelEls].map(e => e.innerText.trim()),
|
||||
["Use by default", "Share with all users"]
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test('favorites use by default and share are exclusive', async function (assert) {
|
||||
assert.expect(11);
|
||||
|
||||
const params = {
|
||||
cpModelConfig: {
|
||||
viewInfo: { fields: this.fields },
|
||||
searchMenuTypes
|
||||
},
|
||||
cpProps: {
|
||||
fields: this.fields,
|
||||
searchMenuTypes,
|
||||
action: {},
|
||||
},
|
||||
};
|
||||
const controlPanel = await createControlPanel(params);
|
||||
|
||||
await cpHelpers.toggleFavoriteMenu(controlPanel);
|
||||
await cpHelpers.toggleSaveFavorite(controlPanel);
|
||||
const checkboxes = controlPanel.el.querySelectorAll('input[type="checkbox"]');
|
||||
|
||||
assert.strictEqual(checkboxes.length, 2, '2 checkboxes are present');
|
||||
|
||||
assert.notOk(checkboxes[0].checked, 'Start: None of the checkboxes are checked (1)');
|
||||
assert.notOk(checkboxes[1].checked, 'Start: None of the checkboxes are checked (2)');
|
||||
|
||||
await testUtils.dom.click(checkboxes[0]);
|
||||
assert.ok(checkboxes[0].checked, 'The first checkbox is checked');
|
||||
assert.notOk(checkboxes[1].checked, 'The second checkbox is not checked');
|
||||
|
||||
await testUtils.dom.click(checkboxes[1]);
|
||||
assert.notOk(checkboxes[0].checked,
|
||||
'Clicking on the second checkbox checks it, and unchecks the first (1)');
|
||||
assert.ok(checkboxes[1].checked,
|
||||
'Clicking on the second checkbox checks it, and unchecks the first (2)');
|
||||
|
||||
await testUtils.dom.click(checkboxes[0]);
|
||||
assert.ok(checkboxes[0].checked,
|
||||
'Clicking on the first checkbox checks it, and unchecks the second (1)');
|
||||
assert.notOk(checkboxes[1].checked,
|
||||
'Clicking on the first checkbox checks it, and unchecks the second (2)');
|
||||
|
||||
await testUtils.dom.click(checkboxes[0]);
|
||||
assert.notOk(checkboxes[0].checked, 'End: None of the checkboxes are checked (1)');
|
||||
assert.notOk(checkboxes[1].checked, 'End: None of the checkboxes are checked (2)');
|
||||
});
|
||||
|
||||
QUnit.test('save filter', async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
const params = {
|
||||
cpModelConfig: {
|
||||
fields: this.fields,
|
||||
searchMenuTypes
|
||||
},
|
||||
cpProps: {
|
||||
fields: this.fields,
|
||||
searchMenuTypes,
|
||||
action: {},
|
||||
},
|
||||
'get-controller-query-params': function (callback) {
|
||||
callback({
|
||||
orderedBy: [
|
||||
{ asc: true, name: 'foo' },
|
||||
{ asc: false, name: 'bar' }
|
||||
]
|
||||
});
|
||||
},
|
||||
env: {
|
||||
dataManager: {
|
||||
create_filter: async function (filter) {
|
||||
assert.strictEqual(filter.sort, '["foo","bar desc"]',
|
||||
'The right format for the string "sort" should be sent to the server'
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const controlPanel = await createControlPanel(params);
|
||||
|
||||
await cpHelpers.toggleFavoriteMenu(controlPanel);
|
||||
await cpHelpers.toggleSaveFavorite(controlPanel);
|
||||
await cpHelpers.editFavoriteName(controlPanel, "aaa");
|
||||
await cpHelpers.saveFavorite(controlPanel);
|
||||
});
|
||||
|
||||
QUnit.test('dynamic filters are saved dynamic', async function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
const arch = `
|
||||
<search>
|
||||
<filter string="Float" name="positive" domain="[('date_field', '>=', (context_today() + relativedelta()).strftime('%Y-%m-%d'))]"/>
|
||||
</search>
|
||||
`;
|
||||
const params = {
|
||||
cpModelConfig: {
|
||||
fields: {},
|
||||
arch ,
|
||||
searchMenuTypes,
|
||||
context: {
|
||||
search_default_positive: true,
|
||||
}
|
||||
},
|
||||
cpProps: {
|
||||
fields: {},
|
||||
searchMenuTypes,
|
||||
action: {},
|
||||
},
|
||||
'get-controller-query-params': function (callback) {
|
||||
callback();
|
||||
},
|
||||
env: {
|
||||
dataManager: {
|
||||
create_filter: async function (filter) {
|
||||
assert.strictEqual(
|
||||
filter.domain,
|
||||
"[(\"date_field\", \">=\", (context_today() + relativedelta()).strftime(\"%Y-%m-%d\"))]"
|
||||
);
|
||||
return 1; // serverSideId
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
const controlPanel = await createControlPanel(params);
|
||||
|
||||
assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), ['Float']);
|
||||
|
||||
await cpHelpers.toggleFavoriteMenu(controlPanel);
|
||||
await cpHelpers.toggleSaveFavorite(controlPanel);
|
||||
await cpHelpers.editFavoriteName(controlPanel, "My favorite");
|
||||
await cpHelpers.saveFavorite(controlPanel);
|
||||
|
||||
assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), ["My favorite"]);
|
||||
});
|
||||
|
||||
QUnit.test('save filters created via autocompletion works', async function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
const arch = `<search><field name="foo"/></search>`;
|
||||
const params = {
|
||||
cpModelConfig: {
|
||||
fields: this.fields,
|
||||
arch ,
|
||||
searchMenuTypes,
|
||||
},
|
||||
cpProps: {
|
||||
fields: this.fields,
|
||||
searchMenuTypes,
|
||||
action: {},
|
||||
},
|
||||
'get-controller-query-params': function (callback) {
|
||||
callback();
|
||||
},
|
||||
env: {
|
||||
dataManager: {
|
||||
create_filter: async function (filter) {
|
||||
assert.strictEqual(
|
||||
filter.domain,
|
||||
`[["foo", "ilike", "a"]]`
|
||||
);
|
||||
return 1; // serverSideId
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
const controlPanel = await createControlPanel(params);
|
||||
|
||||
assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), []);
|
||||
|
||||
await cpHelpers.editSearch(controlPanel, "a");
|
||||
await cpHelpers.validateSearch(controlPanel);
|
||||
|
||||
assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), ["Foo\na"]);
|
||||
|
||||
await cpHelpers.toggleFavoriteMenu(controlPanel);
|
||||
await cpHelpers.toggleSaveFavorite(controlPanel);
|
||||
await cpHelpers.editFavoriteName(controlPanel, "My favorite");
|
||||
await cpHelpers.saveFavorite(controlPanel);
|
||||
|
||||
assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), ["My favorite"]);
|
||||
});
|
||||
|
||||
QUnit.test('delete an active favorite remove it both in list of favorite and in search bar', async function (assert) {
|
||||
assert.expect(9);
|
||||
|
||||
const favoriteFilters = [{
|
||||
context: "{}",
|
||||
domain: "[['foo', '=', 'qsdf']]",
|
||||
id: 7,
|
||||
is_default: true,
|
||||
name: "My favorite",
|
||||
sort: "[]",
|
||||
user_id: [2, "Mitchell Admin"],
|
||||
}];
|
||||
const { legacyEnv } = await makeLegacyDialogMappingTestEnv();
|
||||
const params = {
|
||||
cpModelConfig: { favoriteFilters, searchMenuTypes },
|
||||
cpProps: { searchMenuTypes, action: {} },
|
||||
search: function (searchQuery) {
|
||||
const { domain } = searchQuery;
|
||||
assert.deepEqual(domain, []);
|
||||
},
|
||||
env: {
|
||||
...legacyEnv,
|
||||
dataManager: {
|
||||
delete_filter: function () {
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
const controlPanel = await createControlPanel(params);
|
||||
|
||||
await cpHelpers.toggleFavoriteMenu(controlPanel);
|
||||
const favorite = controlPanel.el.querySelector(".o_favorite_menu .dropdown-item");
|
||||
assert.equal(favorite.innerText, "My favorite");
|
||||
assert.deepEqual(favorite.getAttribute("role"), "menuitemcheckbox");
|
||||
assert.deepEqual(favorite.ariaChecked, "true");
|
||||
|
||||
const { domain } = controlPanel.getQuery();
|
||||
assert.deepEqual(domain, [["foo", "=", "qsdf"]]);
|
||||
assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), ["My favorite"]);
|
||||
assert.hasClass(controlPanel.el.querySelector('.o_favorite_menu .o_menu_item'), 'selected');
|
||||
|
||||
await cpHelpers.deleteFavorite(controlPanel, 0);
|
||||
|
||||
// confirm deletion
|
||||
await testUtils.dom.click(document.querySelector('div.o_dialog footer button'));
|
||||
assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), []);
|
||||
const itemEls = controlPanel.el.querySelectorAll('.o_favorite_menu .dropdown-item');
|
||||
assert.deepEqual([...itemEls].map(e => e.innerText.trim()), ["Save current search"]);
|
||||
});
|
||||
|
||||
QUnit.test('default favorite is not activated if key search_disable_custom_filters is set to true', async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
const favoriteFilters = [{
|
||||
context: "{}",
|
||||
domain: "",
|
||||
id: 7,
|
||||
is_default: true,
|
||||
name: "My favorite",
|
||||
sort: "[]",
|
||||
user_id: [2, "Mitchell Admin"],
|
||||
}];
|
||||
const params = {
|
||||
cpModelConfig: {
|
||||
favoriteFilters,
|
||||
searchMenuTypes,
|
||||
context: { search_disable_custom_filters: true }
|
||||
},
|
||||
cpProps: { searchMenuTypes, action: {} },
|
||||
};
|
||||
const controlPanel = await createControlPanel(params);
|
||||
|
||||
await cpHelpers.toggleFavoriteMenu(controlPanel);
|
||||
|
||||
const { domain } = controlPanel.getQuery();
|
||||
assert.deepEqual(domain, []);
|
||||
assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), []);
|
||||
});
|
||||
|
||||
QUnit.test('toggle favorite correctly clears filter, groupbys, comparison and field "options"', async function (assert) {
|
||||
assert.expect(15);
|
||||
|
||||
const unpatchDate = patchDate(2019, 6, 31, 13, 43, 0);
|
||||
|
||||
const favoriteFilters = [{
|
||||
context: `
|
||||
{
|
||||
"group_by": ["foo"],
|
||||
"comparison": {
|
||||
"favorite comparison content": "bla bla..."
|
||||
},
|
||||
}
|
||||
`,
|
||||
domain: "['!', ['foo', '=', 'qsdf']]",
|
||||
id: 7,
|
||||
is_default: false,
|
||||
name: "My favorite",
|
||||
sort: "[]",
|
||||
user_id: [2, "Mitchell Admin"],
|
||||
}];
|
||||
let firstSearch = true;
|
||||
const arch = `
|
||||
<search>
|
||||
<field string="Foo" name="foo"/>
|
||||
<filter string="Date Field Filter" name="positive" date="date_field" default_period="this_year"/>
|
||||
<filter string="Date Field Groupby" name="coolName" context="{'group_by': 'date_field'}"/>
|
||||
</search>
|
||||
`;
|
||||
const searchMenuTypes = ['filter', 'groupBy', 'comparison', 'favorite'];
|
||||
const params = {
|
||||
cpModelConfig: {
|
||||
favoriteFilters,
|
||||
arch,
|
||||
fields: this.fields,
|
||||
searchMenuTypes,
|
||||
context: {
|
||||
search_default_positive: true,
|
||||
search_default_coolName: true,
|
||||
search_default_foo: "a",
|
||||
}
|
||||
},
|
||||
cpProps: { searchMenuTypes, action: {}, fields: this.fields },
|
||||
search: function (searchQuery) {
|
||||
const { domain, groupBy, timeRanges } = searchQuery;
|
||||
if (firstSearch) {
|
||||
assert.deepEqual(domain, [['foo', 'ilike', 'a']]);
|
||||
assert.deepEqual(groupBy, ['date_field:month']);
|
||||
assert.deepEqual(timeRanges, {
|
||||
comparisonId: "previous_period",
|
||||
comparisonRange: ["&", ["date_field", ">=", "2018-01-01"], ["date_field", "<=", "2018-12-31"]],
|
||||
comparisonRangeDescription: "2018",
|
||||
fieldDescription: "Date Field Filter",
|
||||
fieldName: "date_field",
|
||||
range: ["&", ["date_field", ">=", "2019-01-01"], ["date_field", "<=", "2019-12-31"]],
|
||||
rangeDescription: "2019",
|
||||
});
|
||||
firstSearch = false;
|
||||
} else {
|
||||
assert.deepEqual(domain, ['!', ['foo', '=', 'qsdf']]);
|
||||
assert.deepEqual(groupBy, ['foo']);
|
||||
assert.deepEqual(timeRanges, {
|
||||
"favorite comparison content": "bla bla...",
|
||||
range: undefined,
|
||||
comparisonRange: undefined,
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
const controlPanel = await createControlPanel(params);
|
||||
|
||||
const { domain, groupBy, timeRanges } = controlPanel.getQuery();
|
||||
assert.deepEqual(domain, [
|
||||
"&",
|
||||
["foo", "ilike", "a"],
|
||||
"&",
|
||||
["date_field", ">=", "2019-01-01"],
|
||||
["date_field", "<=", "2019-12-31"]
|
||||
]);
|
||||
assert.deepEqual(groupBy, ['date_field:month']);
|
||||
assert.deepEqual(timeRanges, {});
|
||||
|
||||
assert.deepEqual(
|
||||
cpHelpers.getFacetTexts(controlPanel),
|
||||
[
|
||||
'Foo\na',
|
||||
'Date Field Filter: 2019',
|
||||
'Date Field Groupby: Month',
|
||||
]
|
||||
);
|
||||
|
||||
// activate a comparison
|
||||
await cpHelpers.toggleComparisonMenu(controlPanel);
|
||||
await cpHelpers.toggleMenuItem(controlPanel, "Date Field Filter: Previous period");
|
||||
|
||||
// activate the unique existing favorite
|
||||
await cpHelpers.toggleFavoriteMenu(controlPanel);
|
||||
const favorite = controlPanel.el.querySelector(".o_favorite_menu .dropdown-item");
|
||||
assert.equal(favorite.innerText, "My favorite");
|
||||
assert.deepEqual(favorite.getAttribute("role"), "menuitemcheckbox");
|
||||
assert.deepEqual(favorite.ariaChecked, "false");
|
||||
await cpHelpers.toggleMenuItem(controlPanel, 0);
|
||||
assert.deepEqual(favorite.ariaChecked, "true");
|
||||
|
||||
assert.deepEqual(
|
||||
cpHelpers.getFacetTexts(controlPanel),
|
||||
["My favorite"]
|
||||
);
|
||||
|
||||
unpatchDate();
|
||||
});
|
||||
|
||||
QUnit.test('favorites have unique descriptions (the submenus of the favorite menu are correctly updated)', async function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
const favoriteFilters = [{
|
||||
context: "{}",
|
||||
domain: "[]",
|
||||
id: 1,
|
||||
is_default: false,
|
||||
name: "My favorite",
|
||||
sort: "[]",
|
||||
user_id: [2, "Mitchell Admin"],
|
||||
}];
|
||||
const params = {
|
||||
cpModelConfig: { favoriteFilters, searchMenuTypes },
|
||||
cpProps: { searchMenuTypes, action: {} },
|
||||
'get-controller-query-params': function (callback) {
|
||||
callback();
|
||||
},
|
||||
env: {
|
||||
session: { uid: 4 },
|
||||
services: {
|
||||
notification: {
|
||||
notify: function (params) {
|
||||
assert.deepEqual(params, {
|
||||
message: "Filter with same name already exists.",
|
||||
type: "danger"
|
||||
});
|
||||
},
|
||||
}
|
||||
},
|
||||
dataManager: {
|
||||
create_filter: async function (irFilter) {
|
||||
assert.deepEqual(irFilter, {
|
||||
"action_id": undefined,
|
||||
"context": { "group_by": [] },
|
||||
"domain": "[]",
|
||||
"is_default": false,
|
||||
"model_id": undefined,
|
||||
"name": "My favorite 2",
|
||||
"sort": "[]",
|
||||
"user_id": 4,
|
||||
});
|
||||
return 2; // serverSideId
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
const controlPanel = await createControlPanel(params);
|
||||
|
||||
await cpHelpers.toggleFavoriteMenu(controlPanel);
|
||||
await cpHelpers.toggleSaveFavorite(controlPanel);
|
||||
|
||||
// first try: should fail
|
||||
await cpHelpers.editFavoriteName(controlPanel, "My favorite");
|
||||
await cpHelpers.saveFavorite(controlPanel);
|
||||
|
||||
// second try: should succeed
|
||||
await cpHelpers.editFavoriteName(controlPanel, "My favorite 2");
|
||||
await cpHelpers.saveFavorite(controlPanel);
|
||||
|
||||
// third try: should fail
|
||||
await cpHelpers.editFavoriteName(controlPanel, "My favorite 2");
|
||||
await cpHelpers.saveFavorite(controlPanel);
|
||||
});
|
||||
|
||||
QUnit.test('save search filter in modal', async function (assert) {
|
||||
assert.expect(5);
|
||||
const data = {
|
||||
partner: {
|
||||
fields: {
|
||||
date_field: { string: "Date", type: "date", store: true, sortable: true, searchable: true },
|
||||
birthday: { string: "Birthday", type: "date", store: true, sortable: true },
|
||||
foo: { string: "Foo", type: "char", store: true, sortable: true },
|
||||
bar: { string: "Bar", type: "many2one", relation: 'partner' },
|
||||
float_field: { string: "Float", type: "float", group_operator: 'sum' },
|
||||
},
|
||||
records: [
|
||||
{ id: 1, display_name: "First record", foo: "yop", bar: 2, date_field: "2017-01-25", birthday: "1983-07-15", float_field: 1 },
|
||||
{ id: 2, display_name: "Second record", foo: "blip", bar: 1, date_field: "2017-01-24", birthday: "1982-06-04", float_field: 2 },
|
||||
{ id: 3, display_name: "Third record", foo: "gnap", bar: 1, date_field: "2017-01-13", birthday: "1985-09-13", float_field: 1.618 },
|
||||
{ id: 4, display_name: "Fourth record", foo: "plop", bar: 2, date_field: "2017-02-25", birthday: "1983-05-05", float_field: -1 },
|
||||
{ id: 5, display_name: "Fifth record", foo: "zoup", bar: 2, date_field: "2016-01-25", birthday: "1800-01-01", float_field: 13 },
|
||||
{ id: 7, display_name: "Partner 6", },
|
||||
{ id: 8, display_name: "Partner 7", },
|
||||
{ id: 9, display_name: "Partner 8", },
|
||||
{ id: 10, display_name: "Partner 9", }
|
||||
],
|
||||
},
|
||||
};
|
||||
const form = await createView({
|
||||
arch: `
|
||||
<form string="Partners">
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="bar"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
archs: {
|
||||
'partner,false,list': '<tree><field name="display_name"/></tree>',
|
||||
'partner,false,search': '<search><field name="date_field"/></search>',
|
||||
},
|
||||
data,
|
||||
model: 'partner',
|
||||
res_id: 1,
|
||||
View: FormView,
|
||||
env: {
|
||||
dataManager: {
|
||||
create_filter(filter) {
|
||||
assert.strictEqual(filter.name, "Awesome Test Customer Filter",
|
||||
"filter name should be correct");
|
||||
},
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
await testUtils.form.clickEdit(form);
|
||||
|
||||
await testUtils.fields.many2one.clickOpenDropdown('bar');
|
||||
await testUtils.fields.many2one.clickItem('bar', 'Search');
|
||||
|
||||
assert.containsN(document.body, 'tr.o_data_row', 9, "should display 9 records");
|
||||
|
||||
const modal = document.body.querySelector(".modal");
|
||||
await cpHelpers.toggleFilterMenu(modal);
|
||||
await cpHelpers.toggleAddCustomFilter(modal);
|
||||
assert.strictEqual(document.querySelector('.o_filter_condition select.o_generator_menu_field').value,
|
||||
'date_field',
|
||||
"date field should be selected");
|
||||
await cpHelpers.applyFilter(modal);
|
||||
|
||||
assert.containsNone(document.body, 'tr.o_data_row', "should display 0 records");
|
||||
|
||||
// Save this search
|
||||
await cpHelpers.toggleFavoriteMenu(modal);
|
||||
await cpHelpers.toggleSaveFavorite(modal);
|
||||
|
||||
const filterNameInput = document.querySelector('.o_add_favorite input[type="text"]');
|
||||
assert.isVisible(filterNameInput, "should display an input field for the filter name");
|
||||
|
||||
await testUtils.fields.editAndTrigger(filterNameInput, 'Awesome Test Customer Filter', ["input", "change"]);
|
||||
await testUtils.dom.click(document.querySelector('.o_add_favorite button.btn-primary'));
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('modal loads saved search filters', async function (assert) {
|
||||
assert.expect(1);
|
||||
const data = {
|
||||
partner: {
|
||||
fields: {
|
||||
bar: { string: "Bar", type: "many2one", relation: 'partner' },
|
||||
},
|
||||
// 10 records so that the Search button shows
|
||||
records: Array.apply(null, Array(10)).map(function(_, i) {
|
||||
return { id: i, display_name: "Record " + i, bar: 1 };
|
||||
})
|
||||
},
|
||||
};
|
||||
const form = await createView({
|
||||
arch: `
|
||||
<form string="Partners">
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="bar"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
data,
|
||||
model: 'partner',
|
||||
res_id: 1,
|
||||
View: FormView,
|
||||
interceptsPropagate: {
|
||||
load_views: function (ev) {
|
||||
assert.ok(ev.data.options.load_filters, "opening dialog should load the filters");
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await testUtils.form.clickEdit(form);
|
||||
|
||||
await testUtils.fields.many2one.clickOpenDropdown('bar');
|
||||
await testUtils.fields.many2one.clickItem('bar', 'Search');
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,487 +0,0 @@
|
|||
odoo.define('web.filter_menu_tests', function (require) {
|
||||
"use strict";
|
||||
|
||||
const { browser } = require('@web/core/browser/browser');
|
||||
const { patchWithCleanup } = require('@web/../tests/helpers/utils');
|
||||
const testUtils = require('web.test_utils');
|
||||
|
||||
const cpHelpers = require('@web/../tests/search/helpers');
|
||||
const { createControlPanel, mock } = testUtils;
|
||||
const { patchDate } = mock;
|
||||
|
||||
const searchMenuTypes = ['filter'];
|
||||
|
||||
QUnit.module('Components', {
|
||||
beforeEach: function () {
|
||||
this.fields = {
|
||||
date_field: { string: "Date", type: "date", store: true, sortable: true, searchable: true },
|
||||
foo: { string: "Foo", type: "char", store: true, sortable: true },
|
||||
};
|
||||
patchWithCleanup(browser, {
|
||||
setTimeout: (fn) => fn(),
|
||||
});
|
||||
},
|
||||
}, function () {
|
||||
|
||||
QUnit.module('FilterMenu (legacy)');
|
||||
|
||||
QUnit.test('simple rendering with no filter', async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
const params = {
|
||||
cpModelConfig: { searchMenuTypes },
|
||||
cpProps: { fields: this.fields, searchMenuTypes },
|
||||
};
|
||||
const controlPanel = await createControlPanel(params);
|
||||
|
||||
await cpHelpers.toggleFilterMenu(controlPanel);
|
||||
assert.containsNone(controlPanel, '.o_menu_item, .dropdown-divider');
|
||||
assert.containsOnce(controlPanel, '.o_add_custom_filter_menu');
|
||||
});
|
||||
|
||||
QUnit.test('simple rendering with a single filter', async function (assert) {
|
||||
const arch = `
|
||||
<search>
|
||||
<filter string="Foo" name="foo" domain="[]"/>
|
||||
</search>`;
|
||||
const params = {
|
||||
cpModelConfig: { arch, fields: this.fields, searchMenuTypes },
|
||||
cpProps: { fields: this.fields, searchMenuTypes },
|
||||
};
|
||||
const controlPanel = await createControlPanel(params);
|
||||
|
||||
await cpHelpers.toggleFilterMenu(controlPanel);
|
||||
assert.containsOnce(controlPanel, '.o_menu_item');
|
||||
assert.containsOnce(controlPanel, ".o_menu_item[role=menuitemcheckbox]");
|
||||
assert.deepEqual(controlPanel.el.querySelector(".o_menu_item").ariaChecked, "false");
|
||||
assert.containsOnce(controlPanel, '.dropdown-divider');
|
||||
assert.containsOnce(controlPanel, 'div.o_add_custom_filter_menu');
|
||||
});
|
||||
|
||||
QUnit.test('should have Date and ID field proposed in that order in "Add custom Filter" submenu', async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
const params = {
|
||||
cpModelConfig: { fields: this.fields, searchMenuTypes },
|
||||
cpProps: { fields: this.fields, searchMenuTypes },
|
||||
};
|
||||
const controlPanel = await createControlPanel(params);
|
||||
|
||||
await cpHelpers.toggleFilterMenu(controlPanel);
|
||||
await cpHelpers.toggleAddCustomFilter(controlPanel);
|
||||
const optionEls = controlPanel.el.querySelectorAll('.o_filter_condition select.o_generator_menu_field option');
|
||||
assert.strictEqual(optionEls[0].innerText.trim(), 'Date');
|
||||
assert.strictEqual(optionEls[1].innerText.trim(), 'ID');
|
||||
});
|
||||
|
||||
QUnit.test('toggle a "simple" filter in filter menu works', async function (assert) {
|
||||
assert.expect(12);
|
||||
|
||||
const domains = [
|
||||
[['foo', '=', 'qsdf']],
|
||||
[]
|
||||
];
|
||||
const arch = `
|
||||
<search>
|
||||
<filter string="Foo" name="foo" domain="[['foo', '=', 'qsdf']]"/>
|
||||
</search>`;
|
||||
const params = {
|
||||
cpModelConfig: { arch, searchMenuTypes },
|
||||
cpProps: { fields: {}, searchMenuTypes },
|
||||
search: function (searchQuery) {
|
||||
const { domain } = searchQuery;
|
||||
assert.deepEqual(domain, domains.shift());
|
||||
}
|
||||
};
|
||||
const controlPanel = await createControlPanel(params);
|
||||
|
||||
await cpHelpers.toggleFilterMenu(controlPanel);
|
||||
assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), []);
|
||||
|
||||
assert.notOk(cpHelpers.isItemSelected(controlPanel, 0));
|
||||
assert.containsOnce(controlPanel, ".o_menu_item[role=menuitemcheckbox]");
|
||||
assert.deepEqual(controlPanel.el.querySelector(".o_menu_item").ariaChecked, "false");
|
||||
await cpHelpers.toggleMenuItem(controlPanel, "Foo");
|
||||
assert.deepEqual(controlPanel.el.querySelector(".o_menu_item").ariaChecked, "true");
|
||||
|
||||
assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), ['Foo']);
|
||||
assert.containsOnce(controlPanel.el.querySelector('.o_searchview .o_searchview_facet'),
|
||||
'span.fa.fa-filter.o_searchview_facet_label');
|
||||
|
||||
assert.ok(cpHelpers.isItemSelected(controlPanel, "Foo"));
|
||||
|
||||
await cpHelpers.toggleMenuItem(controlPanel, "Foo");
|
||||
assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), []);
|
||||
assert.notOk(cpHelpers.isItemSelected(controlPanel, "Foo"));
|
||||
});
|
||||
|
||||
QUnit.test('add a custom filter works', async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
const params = {
|
||||
cpModelConfig: { fields: this.fields, searchMenuTypes },
|
||||
cpProps: { fields: this.fields, searchMenuTypes },
|
||||
};
|
||||
const controlPanel = await createControlPanel(params);
|
||||
|
||||
await cpHelpers.toggleFilterMenu(controlPanel);
|
||||
await cpHelpers.toggleAddCustomFilter(controlPanel);
|
||||
// choose ID field in 'Add Custome filter' menu and value 1
|
||||
await cpHelpers.editConditionField(controlPanel, 0, 'id');
|
||||
await cpHelpers.editConditionValue(controlPanel, 0, 1);
|
||||
await cpHelpers.applyFilter(controlPanel);
|
||||
|
||||
assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), ['ID is "1"']);
|
||||
});
|
||||
|
||||
QUnit.test('deactivate a new custom filter works', async function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
const unpatchDate = patchDate(2020, 1, 5, 12, 20, 0);
|
||||
|
||||
const params = {
|
||||
cpModelConfig: { fields: this.fields, searchMenuTypes },
|
||||
cpProps: { fields: this.fields, searchMenuTypes },
|
||||
};
|
||||
const controlPanel = await createControlPanel(params);
|
||||
|
||||
await cpHelpers.toggleFilterMenu(controlPanel);
|
||||
await cpHelpers.toggleAddCustomFilter(controlPanel);
|
||||
await cpHelpers.applyFilter(controlPanel);
|
||||
|
||||
assert.ok(cpHelpers.isItemSelected(controlPanel, 'Date is equal to "02/05/2020"'));
|
||||
assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), ['Date is equal to "02/05/2020"']);
|
||||
|
||||
await cpHelpers.toggleMenuItem(controlPanel, 'Date is equal to "02/05/2020"');
|
||||
|
||||
assert.notOk(cpHelpers.isItemSelected(controlPanel, 'Date is equal to "02/05/2020"'));
|
||||
assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), []);
|
||||
|
||||
unpatchDate();
|
||||
});
|
||||
|
||||
QUnit.test('filter by a date field using period works', async function (assert) {
|
||||
assert.expect(56);
|
||||
|
||||
const unpatchDate = patchDate(2017, 2, 22, 1, 0, 0);
|
||||
|
||||
const basicDomains = [
|
||||
["&", ["date_field", ">=", "2017-01-01"], ["date_field", "<=", "2017-12-31"]],
|
||||
["&", ["date_field", ">=", "2017-02-01"], ["date_field", "<=", "2017-02-28"]],
|
||||
["&", ["date_field", ">=", "2017-01-01"], ["date_field", "<=", "2017-12-31"]],
|
||||
["&", ["date_field", ">=", "2017-01-01"], ["date_field", "<=", "2017-01-31"]],
|
||||
["|",
|
||||
"&", ["date_field", ">=", "2017-01-01"], ["date_field", "<=", "2017-01-31"],
|
||||
"&", ["date_field", ">=", "2017-10-01"], ["date_field", "<=", "2017-12-31"]
|
||||
],
|
||||
["&", ["date_field", ">=", "2017-10-01"], ["date_field", "<=", "2017-12-31"]],
|
||||
["&", ["date_field", ">=", "2017-01-01"], ["date_field", "<=", "2017-12-31"]],
|
||||
["&", ["date_field", ">=", "2017-01-01"], ["date_field", "<=", "2017-03-31"]],
|
||||
["&", ["date_field", ">=", "2017-01-01"], ["date_field", "<=", "2017-12-31"]],
|
||||
["&", ["date_field", ">=", "2017-01-01"], ["date_field", "<=", "2017-12-31"]],
|
||||
["|",
|
||||
"&", ["date_field", ">=", "2016-01-01"], ["date_field", "<=", "2016-12-31"],
|
||||
"&", ["date_field", ">=", "2017-01-01"], ["date_field", "<=", "2017-12-31"]
|
||||
],
|
||||
["|",
|
||||
"|",
|
||||
"&", ["date_field", ">=", "2015-01-01"], ["date_field", "<=", "2015-12-31"],
|
||||
"&", ["date_field", ">=", "2016-01-01"], ["date_field", "<=", "2016-12-31"],
|
||||
"&", ["date_field", ">=", "2017-01-01"], ["date_field", "<=", "2017-12-31"]
|
||||
],
|
||||
["|",
|
||||
"|",
|
||||
"&", ["date_field", ">=", "2015-03-01"], ["date_field", "<=", "2015-03-31"],
|
||||
"&", ["date_field", ">=", "2016-03-01"], ["date_field", "<=", "2016-03-31"],
|
||||
"&", ["date_field", ">=", "2017-03-01"], ["date_field", "<=", "2017-03-31"]
|
||||
]
|
||||
];
|
||||
|
||||
const arch = `
|
||||
<search>
|
||||
<filter string="Date" name="date_field" date="date_field"/>
|
||||
</search>`;
|
||||
const params = {
|
||||
cpModelConfig: {
|
||||
arch,
|
||||
fields: this.fields,
|
||||
searchMenuTypes,
|
||||
context: { search_default_date_field: 1 },
|
||||
},
|
||||
cpProps: { fields: this.fields, searchMenuTypes },
|
||||
search: function (searchQuery) {
|
||||
// we inspect query domain
|
||||
const { domain } = searchQuery;
|
||||
if (domain.length) {
|
||||
assert.deepEqual(domain, basicDomains.shift());
|
||||
}
|
||||
},
|
||||
};
|
||||
const controlPanel = await createControlPanel(params);
|
||||
|
||||
await cpHelpers.toggleFilterMenu(controlPanel);
|
||||
await cpHelpers.toggleMenuItem(controlPanel, "Date");
|
||||
|
||||
const optionEls = controlPanel.el.querySelectorAll('span.o_item_option');
|
||||
|
||||
// default filter should be activated with the global default period 'this_month'
|
||||
const { domain } = controlPanel.getQuery();
|
||||
assert.deepEqual(
|
||||
domain,
|
||||
["&", ["date_field", ">=", "2017-03-01"], ["date_field", "<=", "2017-03-31"]]
|
||||
);
|
||||
assert.ok(cpHelpers.isItemSelected(controlPanel, "Date"));
|
||||
assert.ok(cpHelpers.isOptionSelected(controlPanel, "Date", 0));
|
||||
|
||||
// check option descriptions
|
||||
const optionDescriptions = [...optionEls].map(e => e.innerText.trim());
|
||||
const expectedDescriptions = [
|
||||
'March', 'February', 'January',
|
||||
'Q4', 'Q3', 'Q2', 'Q1',
|
||||
'2017', '2016', '2015'
|
||||
];
|
||||
assert.deepEqual(optionDescriptions, expectedDescriptions);
|
||||
|
||||
// check generated domains
|
||||
const steps = [
|
||||
{ description: 'March', facetContent: 'Date: 2017', selectedoptions: [7] },
|
||||
{ description: 'February', facetContent: 'Date: February 2017', selectedoptions: [1, 7] },
|
||||
{ description: 'February', facetContent: 'Date: 2017', selectedoptions: [7] },
|
||||
{ description: 'January', facetContent: 'Date: January 2017', selectedoptions: [2, 7] },
|
||||
{ description: 'Q4', facetContent: 'Date: January 2017/Q4 2017', selectedoptions: [2, 3, 7] },
|
||||
{ description: 'January', facetContent: 'Date: Q4 2017', selectedoptions: [3, 7] },
|
||||
{ description: 'Q4', facetContent: 'Date: 2017', selectedoptions: [7] },
|
||||
{ description: 'Q1', facetContent: 'Date: Q1 2017', selectedoptions: [6, 7] },
|
||||
{ description: 'Q1', facetContent: 'Date: 2017', selectedoptions: [7] },
|
||||
{ description: '2017', selectedoptions: [] },
|
||||
{ description: '2017', facetContent: 'Date: 2017', selectedoptions: [7] },
|
||||
{ description: '2016', facetContent: 'Date: 2016/2017', selectedoptions: [7, 8] },
|
||||
{ description: '2015', facetContent: 'Date: 2015/2016/2017', selectedoptions: [7, 8, 9] },
|
||||
{ description: 'March', facetContent: 'Date: March 2015/March 2016/March 2017', selectedoptions: [0, 7, 8, 9] }
|
||||
];
|
||||
for (const s of steps) {
|
||||
const index = expectedDescriptions.indexOf(s.description);
|
||||
await cpHelpers.toggleMenuItemOption(controlPanel, 0, index);
|
||||
if (s.facetContent) {
|
||||
assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), [s.facetContent]);
|
||||
} else {
|
||||
assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), []);
|
||||
}
|
||||
s.selectedoptions.forEach(index => {
|
||||
assert.ok(cpHelpers.isOptionSelected(controlPanel, 0, index),
|
||||
`at step ${steps.indexOf(s) + 1}, option ${expectedDescriptions[index]} should be selected`);
|
||||
});
|
||||
}
|
||||
|
||||
unpatchDate();
|
||||
});
|
||||
|
||||
QUnit.test('filter by a date field using period works even in January', async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
const unpatchDate = patchDate(2017, 0, 7, 3, 0, 0);
|
||||
|
||||
const arch = `
|
||||
<search>
|
||||
<filter string="Date" name="some_filter" date="date_field" default_period="last_month"/>
|
||||
</search>`;
|
||||
const params = {
|
||||
cpModelConfig: {
|
||||
arch,
|
||||
fields: this.fields,
|
||||
searchMenuTypes,
|
||||
context: { search_default_some_filter: 1 },
|
||||
},
|
||||
cpProps: { fields: this.fields, searchMenuTypes },
|
||||
};
|
||||
const controlPanel = await createControlPanel(params);
|
||||
|
||||
const { domain } = controlPanel.getQuery();
|
||||
assert.deepEqual(domain, [
|
||||
'&',
|
||||
["date_field", ">=", "2016-12-01"],
|
||||
["date_field", "<=", "2016-12-31"]
|
||||
]);
|
||||
|
||||
assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), ["Date: December 2016"]);
|
||||
|
||||
await cpHelpers.toggleFilterMenu(controlPanel);
|
||||
await cpHelpers.toggleMenuItem(controlPanel, "Date");
|
||||
|
||||
assert.ok(cpHelpers.isItemSelected(controlPanel, "Date"));
|
||||
assert.ok(cpHelpers.isOptionSelected(controlPanel, "Date", 'December'));
|
||||
assert.ok(cpHelpers.isOptionSelected(controlPanel, "Date", '2016'));
|
||||
|
||||
unpatchDate();
|
||||
});
|
||||
|
||||
QUnit.test('`context` key in <filter> is used', async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
const arch = `
|
||||
<search>
|
||||
<filter string="Filter" name="some_filter" domain="[]" context="{'coucou_1': 1}"/>
|
||||
</search>`;
|
||||
const params = {
|
||||
cpModelConfig: {
|
||||
arch,
|
||||
fields: this.fields,
|
||||
searchMenuTypes
|
||||
},
|
||||
cpProps: { fields: this.fields, searchMenuTypes },
|
||||
search: function (searchQuery) {
|
||||
// we inspect query context
|
||||
const { context } = searchQuery;
|
||||
assert.deepEqual(context, { coucou_1: 1 });
|
||||
},
|
||||
};
|
||||
const controlPanel = await createControlPanel(params);
|
||||
|
||||
await cpHelpers.toggleFilterMenu(controlPanel);
|
||||
await cpHelpers.toggleMenuItem(controlPanel, 0);
|
||||
});
|
||||
|
||||
QUnit.test('Filter with JSON-parsable domain works', async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
const originalDomain = [['foo', '=', 'Gently Weeps']];
|
||||
const xml_domain = JSON.stringify(originalDomain);
|
||||
|
||||
const arch =
|
||||
`<search>
|
||||
<filter string="Foo" name="gently_weeps" domain="${_.escape(xml_domain)}"/>
|
||||
</search>`;
|
||||
const params = {
|
||||
cpModelConfig: {
|
||||
arch,
|
||||
fields: this.fields,
|
||||
searchMenuTypes,
|
||||
},
|
||||
cpProps: { fields: this.fields, searchMenuTypes },
|
||||
search: function (searchQuery) {
|
||||
const { domain } = searchQuery;
|
||||
assert.deepEqual(domain, originalDomain,
|
||||
'A JSON parsable xml domain should be handled just like any other'
|
||||
);
|
||||
},
|
||||
};
|
||||
const controlPanel = await createControlPanel(params);
|
||||
|
||||
await cpHelpers.toggleFilterMenu(controlPanel);
|
||||
await cpHelpers.toggleMenuItem(controlPanel, 0);
|
||||
});
|
||||
|
||||
QUnit.test('filter with date attribute set as search_default', async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
const unpatchDate = patchDate(2019, 6, 31, 13, 43, 0);
|
||||
|
||||
const arch =
|
||||
`<search>
|
||||
<filter string="Date" name="date_field" date="date_field" default_period="last_month"/>
|
||||
</search>`,
|
||||
params = {
|
||||
cpModelConfig: {
|
||||
arch,
|
||||
fields: this.fields,
|
||||
searchMenuTypes,
|
||||
context: {
|
||||
search_default_date_field: true
|
||||
}
|
||||
},
|
||||
cpProps: { fields: this.fields, searchMenuTypes },
|
||||
};
|
||||
const controlPanel = await createControlPanel(params);
|
||||
|
||||
assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), ["Date: June 2019"]);
|
||||
|
||||
unpatchDate();
|
||||
});
|
||||
|
||||
QUnit.test('filter domains are correcly combined by OR and AND', async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
const arch =
|
||||
`<search>
|
||||
<filter string="Filter Group 1" name="f_1_g1" domain="[['foo', '=', 'f1_g1']]"/>
|
||||
<separator/>
|
||||
<filter string="Filter 1 Group 2" name="f1_g2" domain="[['foo', '=', 'f1_g2']]"/>
|
||||
<filter string="Filter 2 GROUP 2" name="f2_g2" domain="[['foo', '=', 'f2_g2']]"/>
|
||||
</search>`,
|
||||
params = {
|
||||
cpModelConfig: {
|
||||
arch,
|
||||
fields: this.fields,
|
||||
searchMenuTypes,
|
||||
context: {
|
||||
search_default_f_1_g1: true,
|
||||
search_default_f1_g2: true,
|
||||
search_default_f2_g2: true,
|
||||
}
|
||||
},
|
||||
cpProps: { fields: this.fields, searchMenuTypes },
|
||||
};
|
||||
const controlPanel = await createControlPanel(params);
|
||||
|
||||
const { domain } = controlPanel.getQuery();
|
||||
assert.deepEqual(domain, [
|
||||
'&',
|
||||
['foo', '=', 'f1_g1'],
|
||||
'|',
|
||||
['foo', '=', 'f1_g2'],
|
||||
['foo', '=', 'f2_g2']
|
||||
]);
|
||||
|
||||
assert.deepEqual(
|
||||
cpHelpers.getFacetTexts(controlPanel),
|
||||
["Filter Group 1", "Filter 1 Group 2orFilter 2 GROUP 2"]
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test('arch order of groups of filters preserved', async function (assert) {
|
||||
assert.expect(12);
|
||||
|
||||
const arch =
|
||||
`<search>
|
||||
<filter string="1" name="coolName1" date="date_field"/>
|
||||
<separator/>
|
||||
<filter string="2" name="coolName2" date="date_field"/>
|
||||
<separator/>
|
||||
<filter string="3" name="coolName3" domain="[]"/>
|
||||
<separator/>
|
||||
<filter string="4" name="coolName4" domain="[]"/>
|
||||
<separator/>
|
||||
<filter string="5" name="coolName5" domain="[]"/>
|
||||
<separator/>
|
||||
<filter string="6" name="coolName6" domain="[]"/>
|
||||
<separator/>
|
||||
<filter string="7" name="coolName7" domain="[]"/>
|
||||
<separator/>
|
||||
<filter string="8" name="coolName8" domain="[]"/>
|
||||
<separator/>
|
||||
<filter string="9" name="coolName9" domain="[]"/>
|
||||
<separator/>
|
||||
<filter string="10" name="coolName10" domain="[]"/>
|
||||
<separator/>
|
||||
<filter string="11" name="coolName11" domain="[]"/>
|
||||
</search>`,
|
||||
params = {
|
||||
cpModelConfig: {
|
||||
arch,
|
||||
fields: this.fields,
|
||||
searchMenuTypes,
|
||||
},
|
||||
cpProps: { fields: this.fields, searchMenuTypes },
|
||||
};
|
||||
const controlPanel = await createControlPanel(params);
|
||||
|
||||
await cpHelpers.toggleFilterMenu(controlPanel);
|
||||
assert.containsN(controlPanel, '.o_filter_menu .o_menu_item', 11);
|
||||
|
||||
const menuItemEls = controlPanel.el.querySelectorAll('.o_filter_menu .o_menu_item');
|
||||
[...menuItemEls].forEach((e, index) => {
|
||||
assert.strictEqual(e.innerText.trim(), String(index + 1));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,472 +0,0 @@
|
|||
odoo.define('web.groupby_menu_tests', function (require) {
|
||||
"use strict";
|
||||
|
||||
const { browser } = require('@web/core/browser/browser');
|
||||
const { patchWithCleanup } = require('@web/../tests/helpers/utils');
|
||||
const testUtils = require('web.test_utils');
|
||||
|
||||
const cpHelpers = require('@web/../tests/search/helpers');
|
||||
const { createControlPanel } = testUtils;
|
||||
|
||||
const searchMenuTypes = ['groupBy'];
|
||||
|
||||
QUnit.module('Components', {
|
||||
beforeEach: function () {
|
||||
this.fields = {
|
||||
bar: { string: "Bar", type: "many2one", relation: 'partner' },
|
||||
birthday: { string: "Birthday", type: "date", store: true, sortable: true },
|
||||
date_field: { string: "Date", type: "date", store: true, sortable: true },
|
||||
float_field: { string: "Float", type: "float", group_operator: 'sum' },
|
||||
foo: { string: "Foo", type: "char", store: true, sortable: true },
|
||||
m2m: { string: "Many2Many", type: "many2many", store: true},
|
||||
m2m_not_stored: { string: "Many2Many not stored", type: "many2many" },
|
||||
};
|
||||
patchWithCleanup(browser, {
|
||||
setTimeout: (fn) => fn(),
|
||||
});
|
||||
},
|
||||
}, function () {
|
||||
|
||||
QUnit.module('GroupByMenu (legacy)');
|
||||
|
||||
QUnit.test('simple rendering with neither groupbys nor groupable fields', async function (assert) {
|
||||
|
||||
assert.expect(1);
|
||||
const params = {
|
||||
cpModelConfig: { searchMenuTypes },
|
||||
cpProps: { fields: {}, searchMenuTypes },
|
||||
};
|
||||
const controlPanel = await createControlPanel(params);
|
||||
|
||||
assert.containsNone(controlPanel, '.o_menu_item, .dropdown-divider, .o_add_custom_group_menu');
|
||||
});
|
||||
|
||||
QUnit.test('simple rendering with no groupby', async function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
// Manually make m2m_not_stored to be sortable.
|
||||
// Even if it's sortable, it should not be included in the add custom groupby options.
|
||||
this.fields.m2m_not_stored.sortable = true;
|
||||
|
||||
const params = {
|
||||
cpModelConfig: { searchMenuTypes },
|
||||
cpProps: { fields: this.fields, searchMenuTypes },
|
||||
};
|
||||
const controlPanel = await createControlPanel(params);
|
||||
|
||||
await cpHelpers.toggleGroupByMenu(controlPanel);
|
||||
assert.containsNone(controlPanel, '.o_menu_item, .dropdown-divider');
|
||||
assert.containsOnce(controlPanel, '.o_add_custom_group_menu');
|
||||
|
||||
await cpHelpers.toggleAddCustomGroup(controlPanel);
|
||||
|
||||
const optionEls = controlPanel.el.querySelectorAll('.o_add_custom_group_menu select option');
|
||||
assert.deepEqual(
|
||||
[...optionEls].map((el) => el.innerText.trim()),
|
||||
['Birthday', 'Date', 'Foo', 'Many2Many']
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test('simple rendering with a single groupby', async function (assert) {
|
||||
const arch = `
|
||||
<search>
|
||||
<filter string="Groupby Foo" name="gb_foo" context="{'group_by': 'foo'}"/>
|
||||
</search>`;
|
||||
const params = {
|
||||
cpModelConfig: { arch, fields: this.fields, searchMenuTypes },
|
||||
cpProps: { fields: this.fields, searchMenuTypes },
|
||||
};
|
||||
const controlPanel = await createControlPanel(params);
|
||||
|
||||
await cpHelpers.toggleGroupByMenu(controlPanel);
|
||||
assert.containsOnce(controlPanel, '.o_menu_item');
|
||||
const menuItem = controlPanel.el.querySelector(".o_menu_item");
|
||||
assert.strictEqual(menuItem.innerText.trim(), "Groupby Foo");
|
||||
assert.strictEqual(menuItem.getAttribute("role"), "menuitemcheckbox");
|
||||
assert.strictEqual(menuItem.ariaChecked, "false");
|
||||
assert.containsOnce(controlPanel, '.dropdown-divider');
|
||||
assert.containsOnce(controlPanel, '.o_add_custom_group_menu');
|
||||
});
|
||||
|
||||
QUnit.test('toggle a "simple" groupby in groupby menu works', async function (assert) {
|
||||
assert.expect(13);
|
||||
|
||||
const groupBys = [['foo'], []];
|
||||
const arch = `
|
||||
<search>
|
||||
<filter string="Groupby Foo" name="gb_foo" context="{'group_by': 'foo'}"/>
|
||||
</search>`;
|
||||
const params = {
|
||||
cpModelConfig: {arch, fields: this.fields, searchMenuTypes },
|
||||
cpProps: { fields: this.fields, searchMenuTypes },
|
||||
search: function (searchQuery) {
|
||||
const { groupBy } = searchQuery;
|
||||
assert.deepEqual(groupBy, groupBys.shift());
|
||||
},
|
||||
};
|
||||
const controlPanel = await createControlPanel(params);
|
||||
|
||||
await cpHelpers.toggleGroupByMenu(controlPanel);
|
||||
assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), []);
|
||||
|
||||
assert.notOk(cpHelpers.isItemSelected(controlPanel, 0));
|
||||
const menuItem = controlPanel.el.querySelector(".o_menu_item");
|
||||
assert.strictEqual(menuItem.innerText.trim(), "Groupby Foo");
|
||||
assert.strictEqual(menuItem.getAttribute("role"), "menuitemcheckbox");
|
||||
assert.strictEqual(menuItem.ariaChecked, "false");
|
||||
|
||||
await cpHelpers.toggleMenuItem(controlPanel, 0);
|
||||
assert.strictEqual(menuItem.ariaChecked, "true");
|
||||
assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), ['Groupby Foo']);
|
||||
assert.containsOnce(controlPanel.el.querySelector('.o_searchview .o_searchview_facet'),
|
||||
'span.oi.oi-group.o_searchview_facet_label');
|
||||
assert.ok(cpHelpers.isItemSelected(controlPanel, 0));
|
||||
|
||||
await cpHelpers.toggleMenuItem(controlPanel, 0);
|
||||
assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), []);
|
||||
assert.notOk(cpHelpers.isItemSelected(controlPanel, 0));
|
||||
});
|
||||
|
||||
QUnit.test('toggle a "simple" groupby quickly does not crash', async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
const arch = `
|
||||
<search>
|
||||
<filter string="Groupby Foo" name="gb_foo" context="{'group_by': 'foo'}"/>
|
||||
</search>`;
|
||||
const params = {
|
||||
cpModelConfig: { arch, fields: this.fields, searchMenuTypes },
|
||||
cpProps: { fields: this.fields, searchMenuTypes },
|
||||
};
|
||||
const controlPanel = await createControlPanel(params);
|
||||
|
||||
await cpHelpers.toggleGroupByMenu(controlPanel);
|
||||
|
||||
cpHelpers.toggleMenuItem(controlPanel, 0);
|
||||
cpHelpers.toggleMenuItem(controlPanel, 0);
|
||||
|
||||
assert.ok(true);
|
||||
});
|
||||
|
||||
QUnit.test('remove a "Group By" facet properly unchecks groupbys in groupby menu', async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
const arch = `
|
||||
<search>
|
||||
<filter string="Groupby Foo" name="gb_foo" context="{'group_by': 'foo'}"/>
|
||||
</search>`;
|
||||
const params = {
|
||||
cpModelConfig: {
|
||||
arch,
|
||||
fields: this.fields,
|
||||
searchMenuTypes,
|
||||
context: { search_default_gb_foo: 1 }
|
||||
},
|
||||
cpProps: { fields: this.fields, searchMenuTypes },
|
||||
search: function (searchQuery) {
|
||||
const { groupBy } = searchQuery;
|
||||
assert.deepEqual(groupBy, []);
|
||||
},
|
||||
};
|
||||
const controlPanel = await createControlPanel(params);
|
||||
|
||||
await cpHelpers.toggleGroupByMenu(controlPanel);
|
||||
const facetEl = controlPanel.el.querySelector('.o_searchview .o_searchview_facet');
|
||||
assert.strictEqual(facetEl.innerText.trim(), "Groupby Foo");
|
||||
assert.ok(cpHelpers.isItemSelected(controlPanel, 0));
|
||||
|
||||
await testUtils.dom.click(facetEl.querySelector('i.o_facet_remove'));
|
||||
assert.containsNone(controlPanel, '.o_searchview .o_searchview_facet');
|
||||
await cpHelpers.toggleGroupByMenu(controlPanel);
|
||||
assert.notOk(cpHelpers.isItemSelected(controlPanel, 0));
|
||||
});
|
||||
|
||||
QUnit.test('group by a date field using interval works', async function (assert) {
|
||||
assert.expect(21);
|
||||
|
||||
const groupBys = [
|
||||
['date_field:year', 'date_field:week' ],
|
||||
['date_field:year', 'date_field:month', 'date_field:week'],
|
||||
['date_field:year', 'date_field:month'],
|
||||
['date_field:year'],
|
||||
[]
|
||||
];
|
||||
|
||||
const arch = `
|
||||
<search>
|
||||
<filter string="Date" name="date" context="{'group_by': 'date_field:week'}"/>
|
||||
</search>`;
|
||||
const params = {
|
||||
cpModelConfig: {
|
||||
arch,
|
||||
fields: this.fields,
|
||||
searchMenuTypes,
|
||||
context: { search_default_date: 1 }
|
||||
},
|
||||
cpProps: { fields: this.fields, searchMenuTypes },
|
||||
search: function (searchQuery) {
|
||||
const { groupBy } = searchQuery;
|
||||
assert.deepEqual(groupBy, groupBys.shift());
|
||||
},
|
||||
};
|
||||
|
||||
const controlPanel = await createControlPanel(params);
|
||||
|
||||
await cpHelpers.toggleGroupByMenu(controlPanel);
|
||||
await cpHelpers.toggleMenuItem(controlPanel, 0);
|
||||
|
||||
const optionEls = controlPanel.el.querySelectorAll('span.o_item_option');
|
||||
|
||||
// default groupby should be activated with the default inteval 'week'
|
||||
const { groupBy } = controlPanel.getQuery();
|
||||
assert.deepEqual(groupBy, ['date_field:week']);
|
||||
|
||||
assert.ok(cpHelpers.isOptionSelected(controlPanel, 0, 3));
|
||||
|
||||
// check option descriptions
|
||||
const optionDescriptions = [...optionEls].map(e => e.innerText.trim());
|
||||
const expectedDescriptions = ['Year', 'Quarter', 'Month', 'Week', 'Day'];
|
||||
assert.deepEqual(optionDescriptions, expectedDescriptions);
|
||||
|
||||
const steps = [
|
||||
{ description: 'Year', facetContent: 'Date: Year>Date: Week', selectedoptions: [0, 3] },
|
||||
{ description: 'Month', facetContent: 'Date: Year>Date: Month>Date: Week', selectedoptions: [0, 2, 3] },
|
||||
{ description: 'Week', facetContent: 'Date: Year>Date: Month', selectedoptions: [0, 2] },
|
||||
{ description: 'Month', facetContent: 'Date: Year', selectedoptions: [0] },
|
||||
{ description: 'Year', selectedoptions: [] },
|
||||
];
|
||||
for (const s of steps) {
|
||||
const index = expectedDescriptions.indexOf(s.description);
|
||||
await cpHelpers.toggleMenuItemOption(controlPanel, 0, index);
|
||||
if (s.facetContent) {
|
||||
assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), [s.facetContent]);
|
||||
} else {
|
||||
assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), []);
|
||||
}
|
||||
s.selectedoptions.forEach(index => {
|
||||
assert.ok(cpHelpers.isOptionSelected(controlPanel, 0, index));
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
QUnit.test('interval options are correctly grouped and ordered', async function (assert) {
|
||||
assert.expect(8);
|
||||
|
||||
const arch = `
|
||||
<search>
|
||||
<filter string="Bar" name="bar" context="{'group_by': 'bar'}"/>
|
||||
<filter string="Date" name="date" context="{'group_by': 'date_field'}"/>
|
||||
<filter string="Foo" name="foo" context="{'group_by': 'foo'}"/>
|
||||
</search>`;
|
||||
const params = {
|
||||
cpModelConfig: {
|
||||
arch,
|
||||
fields: this.fields,
|
||||
searchMenuTypes,
|
||||
context: { search_default_bar: 1 }
|
||||
},
|
||||
cpProps: { fields: this.fields, searchMenuTypes },
|
||||
};
|
||||
|
||||
const controlPanel = await createControlPanel(params);
|
||||
|
||||
assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), ['Bar']);
|
||||
|
||||
// open menu 'Group By'
|
||||
await cpHelpers.toggleGroupByMenu(controlPanel);
|
||||
|
||||
// Open the groupby 'Date'
|
||||
await cpHelpers.toggleMenuItem(controlPanel, 'Date');
|
||||
// select option 'week'
|
||||
await cpHelpers.toggleMenuItemOption(controlPanel, 'Date', 'Week');
|
||||
assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), ['Bar>Date: Week']);
|
||||
|
||||
// select option 'day'
|
||||
await cpHelpers.toggleMenuItemOption(controlPanel, 'Date', 'Day');
|
||||
assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), ['Bar>Date: Week>Date: Day']);
|
||||
|
||||
// select option 'year'
|
||||
await cpHelpers.toggleMenuItemOption(controlPanel, 'Date', 'Year');
|
||||
assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), ['Bar>Date: Year>Date: Week>Date: Day']);
|
||||
|
||||
// select 'Foo'
|
||||
await cpHelpers.toggleMenuItem(controlPanel, 'Foo');
|
||||
assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), ['Bar>Date: Year>Date: Week>Date: Day>Foo']);
|
||||
|
||||
// select option 'quarter'
|
||||
await cpHelpers.toggleMenuItem(controlPanel, 'Date');
|
||||
await cpHelpers.toggleMenuItemOption(controlPanel, 'Date', 'Quarter');
|
||||
assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), ['Bar>Date: Year>Date: Quarter>Date: Week>Date: Day>Foo']);
|
||||
|
||||
// unselect 'Bar'
|
||||
await cpHelpers.toggleMenuItem(controlPanel, 'Bar');
|
||||
assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), ['Date: Year>Date: Quarter>Date: Week>Date: Day>Foo']);
|
||||
|
||||
// unselect option 'week'
|
||||
await cpHelpers.toggleMenuItem(controlPanel, 'Date');
|
||||
await cpHelpers.toggleMenuItemOption(controlPanel, 'Date', 'Week');
|
||||
assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), ['Date: Year>Date: Quarter>Date: Day>Foo']);
|
||||
});
|
||||
|
||||
QUnit.test('the ID field should not be proposed in "Add Custom Group" menu', async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
const fields = {
|
||||
foo: { string: "Foo", type: "char", store: true, sortable: true },
|
||||
id: { sortable: true, string: 'ID', type: 'integer' }
|
||||
};
|
||||
const params = {
|
||||
cpModelConfig: { searchMenuTypes },
|
||||
cpProps: { fields, searchMenuTypes },
|
||||
};
|
||||
const controlPanel = await createControlPanel(params);
|
||||
|
||||
await cpHelpers.toggleGroupByMenu(controlPanel);
|
||||
await cpHelpers.toggleAddCustomGroup(controlPanel);
|
||||
|
||||
const optionEls = controlPanel.el.querySelectorAll('.o_add_custom_group_menu select option');
|
||||
assert.strictEqual(optionEls.length, 1);
|
||||
assert.strictEqual(optionEls[0].innerText.trim(), "Foo");
|
||||
});
|
||||
|
||||
QUnit.test('add a date field in "Add Custome Group" activate a groupby with global default option "month"', async function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
const fields = {
|
||||
date_field: { string: "Date", type: "date", store: true, sortable: true },
|
||||
id: { sortable: true, string: 'ID', type: 'integer' }
|
||||
};
|
||||
const params = {
|
||||
cpModelConfig: { fields, searchMenuTypes },
|
||||
cpProps: { fields, searchMenuTypes },
|
||||
search: function (searchQuery) {
|
||||
const { groupBy } = searchQuery;
|
||||
assert.deepEqual(groupBy, ['date_field:month']);
|
||||
}
|
||||
};
|
||||
const controlPanel = await createControlPanel(params);
|
||||
|
||||
await cpHelpers.toggleGroupByMenu(controlPanel);
|
||||
await cpHelpers.toggleAddCustomGroup(controlPanel);
|
||||
await cpHelpers.applyGroup(controlPanel);
|
||||
|
||||
assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), ['Date: Month']);
|
||||
|
||||
assert.ok(cpHelpers.isItemSelected(controlPanel, "Date"));
|
||||
await cpHelpers.toggleMenuItem(controlPanel, "Date");
|
||||
assert.ok(cpHelpers.isOptionSelected(controlPanel, "Date", "Month"));
|
||||
});
|
||||
|
||||
QUnit.test('default groupbys can be ordered', async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
const arch = `
|
||||
<search>
|
||||
<filter string="Birthday" name="birthday" context="{'group_by': 'birthday'}"/>
|
||||
<filter string="Date" name="date" context="{'group_by': 'date_field:week'}"/>
|
||||
</search>`;
|
||||
const params = {
|
||||
cpModelConfig: {
|
||||
arch,
|
||||
fields: this.fields,
|
||||
searchMenuTypes,
|
||||
context: { search_default_birthday: 2, search_default_date: 1 }
|
||||
},
|
||||
cpProps: { fields: this.fields, searchMenuTypes },
|
||||
};
|
||||
|
||||
const controlPanel = await createControlPanel(params);
|
||||
|
||||
// the defautl groupbys should be activated in the right order
|
||||
const { groupBy } = controlPanel.getQuery();
|
||||
assert.deepEqual(groupBy, ['date_field:week', 'birthday:month']);
|
||||
assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), ['Date: Week>Birthday: Month']);
|
||||
});
|
||||
|
||||
QUnit.test('a separator in groupbys does not cause problems', async function (assert) {
|
||||
assert.expect(23);
|
||||
|
||||
const arch = `
|
||||
<search>
|
||||
<filter string="Date" name="coolName" context="{'group_by': 'date_field'}"/>
|
||||
<separator/>
|
||||
<filter string="Bar" name="superName" context="{'group_by': 'bar'}"/>
|
||||
</search>`;
|
||||
const params = {
|
||||
cpModelConfig: {
|
||||
arch,
|
||||
fields: this.fields,
|
||||
searchMenuTypes,
|
||||
},
|
||||
cpProps: { fields: this.fields, searchMenuTypes },
|
||||
};
|
||||
|
||||
const controlPanel = await createControlPanel(params);
|
||||
|
||||
await cpHelpers.toggleGroupByMenu(controlPanel);
|
||||
await cpHelpers.toggleMenuItem(controlPanel, 0);
|
||||
await cpHelpers.toggleMenuItemOption(controlPanel, 0, 4);
|
||||
|
||||
assert.ok(cpHelpers.isItemSelected(controlPanel, 0));
|
||||
assert.notOk(cpHelpers.isItemSelected(controlPanel, 1));
|
||||
assert.ok(cpHelpers.isOptionSelected(controlPanel, 0, 4), 'selected');
|
||||
assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), ['Date: Day']);
|
||||
|
||||
await cpHelpers.toggleMenuItem(controlPanel, 1);
|
||||
await cpHelpers.toggleMenuItem(controlPanel, 0);
|
||||
assert.ok(cpHelpers.isItemSelected(controlPanel, 0));
|
||||
assert.ok(cpHelpers.isItemSelected(controlPanel, 1));
|
||||
assert.ok(cpHelpers.isOptionSelected(controlPanel, 0, 4), 'selected');
|
||||
assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), ['Date: Day>Bar']);
|
||||
|
||||
await cpHelpers.toggleMenuItemOption(controlPanel, 0, 1);
|
||||
assert.ok(cpHelpers.isItemSelected(controlPanel, 0));
|
||||
assert.ok(cpHelpers.isItemSelected(controlPanel, 1));
|
||||
assert.ok(cpHelpers.isOptionSelected(controlPanel, 0, 1), 'selected');
|
||||
assert.ok(cpHelpers.isOptionSelected(controlPanel, 0, 4), 'selected');
|
||||
assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), ['Date: Quarter>Date: Day>Bar']);
|
||||
|
||||
await cpHelpers.toggleMenuItem(controlPanel, 1);
|
||||
await cpHelpers.toggleMenuItem(controlPanel, 0);
|
||||
assert.ok(cpHelpers.isItemSelected(controlPanel, 0));
|
||||
assert.notOk(cpHelpers.isItemSelected(controlPanel, 1));
|
||||
assert.ok(cpHelpers.isOptionSelected(controlPanel, 0, 1), 'selected');
|
||||
assert.ok(cpHelpers.isOptionSelected(controlPanel, 0, 4), 'selected');
|
||||
assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), ['Date: Quarter>Date: Day']);
|
||||
|
||||
await cpHelpers.removeFacet(controlPanel);
|
||||
assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), []);
|
||||
|
||||
await cpHelpers.toggleGroupByMenu(controlPanel);
|
||||
await cpHelpers.toggleMenuItem(controlPanel, 0);
|
||||
assert.notOk(cpHelpers.isItemSelected(controlPanel, 0));
|
||||
assert.notOk(cpHelpers.isItemSelected(controlPanel, 1));
|
||||
assert.notOk(cpHelpers.isOptionSelected(controlPanel, 0, 1), 'selected');
|
||||
assert.notOk(cpHelpers.isOptionSelected(controlPanel, 0, 4), 'selected');
|
||||
});
|
||||
|
||||
QUnit.test('falsy search default groupbys are not activated', async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
const arch = `
|
||||
<search>
|
||||
<filter string="Birthday" name="birthday" context="{'group_by': 'birthday'}"/>
|
||||
<filter string="Date" name="date" context="{'group_by': 'foo'}"/>
|
||||
</search>`;
|
||||
const params = {
|
||||
cpModelConfig: {
|
||||
arch,
|
||||
fields: this.fields,
|
||||
searchMenuTypes,
|
||||
context: { search_default_birthday: false, search_default_foo: 0 }
|
||||
},
|
||||
cpProps: { fields: this.fields, searchMenuTypes },
|
||||
};
|
||||
|
||||
const controlPanel = await createControlPanel(params);
|
||||
const { groupBy } = controlPanel.getQuery();
|
||||
assert.deepEqual(groupBy, []);
|
||||
assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), []);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,678 +0,0 @@
|
|||
/** @odoo-module alias="web.search_bar_tests" **/
|
||||
|
||||
import testUtils from "web.test_utils";
|
||||
import { createWebClient, doAction } from "@web/../tests/webclient/helpers";
|
||||
import { makeFakeUserService } from "@web/../tests/helpers/mock_services";
|
||||
import { Model } from "web.Model";
|
||||
import Registry from "web.Registry";
|
||||
import FormView from 'web.FormView';
|
||||
import ListView from 'web.ListView';
|
||||
import KanbanView from 'web.KanbanView';
|
||||
import SearchBar from "web.SearchBar";
|
||||
import { registry } from "@web/core/registry";
|
||||
import * as cpHelpers from "@web/../tests/search/helpers";
|
||||
import { getFixture, patchWithCleanup } from "@web/../tests/helpers/utils";
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import legacyViewRegistry from 'web.view_registry';
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
QUnit.module("Search Bar (legacy)", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
registry.category("views").remove("list"); // remove new list from registry
|
||||
registry.category("views").remove("kanban"); // remove new kanban from registry
|
||||
registry.category("views").remove("form"); // remove new form from registry
|
||||
legacyViewRegistry.add("list", ListView); // add legacy list -> will be wrapped and added to new registry
|
||||
legacyViewRegistry.add("kanban", KanbanView); // add legacy kanban -> will be wrapped and added to new registry
|
||||
legacyViewRegistry.add("form", FormView); // add legacy form -> will be wrapped and added to new registry
|
||||
|
||||
serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: {
|
||||
bar: { string: "Bar", type: "many2one", relation: "partner" },
|
||||
birthday: { string: "Birthday", type: "date" },
|
||||
birth_datetime: { string: "Birth DateTime", type: "datetime" },
|
||||
foo: { string: "Foo", type: "char" },
|
||||
bool: { string: "Bool", type: "boolean" },
|
||||
status: { string: 'Status', type: 'selection', selection: [['draft', "New"], ['cancel', "Cancelled"]] },
|
||||
},
|
||||
records: [
|
||||
{ id: 1, display_name: "First record", foo: "yop", bar: 2, bool: true, birthday: '1983-07-15', birth_datetime: '1983-07-15 01:00:00' },
|
||||
{ id: 2, display_name: "Second record", foo: "blip", bar: 1, bool: false, birthday: '1982-06-04', birth_datetime: '1982-06-04 02:00:00' },
|
||||
{ id: 3, display_name: "Third record", foo: "gnap", bar: 1, bool: false, birthday: '1985-09-13', birth_datetime: '1985-09-13 03:00:00' },
|
||||
{ id: 4, display_name: "Fourth record", foo: "plop", bar: 2, bool: true, birthday: '1983-05-05', birth_datetime: '1983-05-05 04:00:00' },
|
||||
{ id: 5, display_name: "Fifth record", foo: "zoup", bar: 2, bool: true, birthday: '1800-01-01', birth_datetime: '1800-01-01 05:00:00' },
|
||||
],
|
||||
}
|
||||
},
|
||||
views: {
|
||||
"partner,false,list": `<tree><field name="foo"/></tree>`,
|
||||
"partner,false,search": `
|
||||
<search>
|
||||
<field name="foo"/>
|
||||
<field name="birthday"/>
|
||||
<field name="birth_datetime"/>
|
||||
<field name="bar" context="{'bar': self}"/>
|
||||
<filter string="Date Field Filter" name="positive" date="birthday"/>
|
||||
<filter string="Date Field Groupby" name="coolName" context="{'group_by': 'birthday:day'}"/>
|
||||
</search>
|
||||
`,
|
||||
},
|
||||
actions: {
|
||||
1: {
|
||||
id: 1,
|
||||
name: "Partners Action",
|
||||
res_model: "partner",
|
||||
search_view_id: [false, "search"],
|
||||
type: "ir.actions.act_window",
|
||||
views: [[false, "list"]],
|
||||
},
|
||||
},
|
||||
};
|
||||
target = getFixture();
|
||||
});
|
||||
|
||||
QUnit.test("basic rendering", async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
const webClient = await createWebClient({ serverData });
|
||||
await doAction(webClient, 1);
|
||||
|
||||
assert.strictEqual(
|
||||
document.activeElement,
|
||||
target.querySelector(".o_searchview input.o_searchview_input"),
|
||||
"searchview input should be focused"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("navigation with facets", async function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
patchWithCleanup(browser, { setTimeout: (fn) => fn() });
|
||||
const webClient = await createWebClient({ serverData });
|
||||
await doAction(webClient, 1);
|
||||
|
||||
// add a facet
|
||||
await cpHelpers.toggleGroupByMenu(target);
|
||||
await cpHelpers.toggleMenuItem(target, 0);
|
||||
await cpHelpers.toggleMenuItemOption(target, 0, 0);
|
||||
assert.containsOnce(target, '.o_searchview .o_searchview_facet',
|
||||
"there should be one facet");
|
||||
assert.strictEqual(document.activeElement,
|
||||
target.querySelector('.o_searchview input.o_searchview_input'));
|
||||
|
||||
// press left to focus the facet
|
||||
await testUtils.dom.triggerEvent(document.activeElement, 'keydown', { key: 'ArrowLeft' });
|
||||
assert.strictEqual(document.activeElement, target.querySelector('.o_searchview .o_searchview_facet'));
|
||||
|
||||
// press right to focus the input
|
||||
await testUtils.dom.triggerEvent(document.activeElement, 'keydown', { key: 'ArrowRight' });
|
||||
assert.strictEqual(document.activeElement, target.querySelector('.o_searchview input.o_searchview_input'));
|
||||
});
|
||||
|
||||
QUnit.test('search date and datetime fields. Support of timezones', async function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
let searchReadCount = 0;
|
||||
|
||||
const webClient = await createWebClient({
|
||||
serverData,
|
||||
legacyParams: {
|
||||
getTZOffset() {
|
||||
return 360;
|
||||
},
|
||||
},
|
||||
mockRPC: (route, args) => {
|
||||
if (route === '/web/dataset/search_read') {
|
||||
switch (searchReadCount) {
|
||||
case 0:
|
||||
// Done on loading
|
||||
break;
|
||||
case 1:
|
||||
assert.deepEqual(args.domain, [["birthday", "=", "1983-07-15"]],
|
||||
"A date should stay what the user has input, but transmitted in server's format");
|
||||
break;
|
||||
case 2:
|
||||
// Done on closing the first facet
|
||||
break;
|
||||
case 3:
|
||||
assert.deepEqual(args.domain, [["birth_datetime", "=", "1983-07-14 18:00:00"]],
|
||||
"A datetime should be transformed in UTC and transmitted in server's format");
|
||||
break;
|
||||
}
|
||||
searchReadCount++;
|
||||
}
|
||||
}
|
||||
});
|
||||
await doAction(webClient, 1);
|
||||
|
||||
// Date case
|
||||
let searchInput = target.querySelector('.o_searchview_input');
|
||||
await testUtils.fields.editInput(searchInput, '07/15/1983');
|
||||
await testUtils.dom.triggerEvent(searchInput, 'keydown', { key: 'ArrowDown' });
|
||||
await testUtils.dom.triggerEvent(searchInput, 'keydown', { key: 'Enter' });
|
||||
|
||||
assert.strictEqual(target.querySelector('.o_searchview_facet .o_facet_values').innerText.trim(),
|
||||
'07/15/1983',
|
||||
'The format of the date in the facet should be in locale');
|
||||
|
||||
// Close Facet
|
||||
await testUtils.dom.click($('.o_searchview_facet .o_facet_remove'));
|
||||
|
||||
// DateTime case
|
||||
searchInput = target.querySelector('.o_searchview_input');
|
||||
await testUtils.fields.editInput(searchInput, '07/15/1983 00:00:00');
|
||||
await testUtils.dom.triggerEvent(searchInput, 'keydown', { key: 'ArrowDown' });
|
||||
await testUtils.dom.triggerEvent(searchInput, 'keydown', { key: 'ArrowDown' });
|
||||
await testUtils.dom.triggerEvent(searchInput, 'keydown', { key: 'Enter' });
|
||||
|
||||
assert.strictEqual(target.querySelector('.o_searchview_facet .o_facet_values').innerText.trim(),
|
||||
'07/15/1983 00:00:00',
|
||||
'The format of the datetime in the facet should be in locale');
|
||||
});
|
||||
|
||||
QUnit.test("autocomplete menu clickout interactions", async function (assert) {
|
||||
assert.expect(9);
|
||||
|
||||
const data = serverData.models;
|
||||
const fields = serverData.models.partner.fields;
|
||||
|
||||
class TestModelExtension extends Model.Extension {
|
||||
get(property) {
|
||||
switch (property) {
|
||||
case 'facets':
|
||||
return [];
|
||||
case 'filters':
|
||||
return Object.keys(fields).map((fname, index) => Object.assign({
|
||||
description: fields[fname].string,
|
||||
fieldName: fname,
|
||||
fieldType: fields[fname].type,
|
||||
id: index,
|
||||
}, fields[fname]));
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
class MockedModel extends Model {}
|
||||
MockedModel.registry = new Registry({ Test: TestModelExtension, });
|
||||
const searchModel = new MockedModel({ Test: {} });
|
||||
|
||||
|
||||
const searchBar = await testUtils.createComponent(SearchBar, {
|
||||
data,
|
||||
env: { searchModel },
|
||||
props: { fields },
|
||||
});
|
||||
const input = searchBar.el.querySelector('.o_searchview_input');
|
||||
|
||||
assert.containsNone(searchBar, '.o_searchview_autocomplete');
|
||||
|
||||
await testUtils.controlPanel.editSearch(searchBar, "Hello there");
|
||||
|
||||
assert.strictEqual(input.value, "Hello there", "input value should be updated");
|
||||
assert.containsOnce(searchBar, '.o_searchview_autocomplete');
|
||||
|
||||
await testUtils.dom.triggerEvent(input, 'keydown', { key: 'Escape' });
|
||||
|
||||
assert.strictEqual(input.value, "", "input value should be empty");
|
||||
assert.containsNone(searchBar, '.o_searchview_autocomplete');
|
||||
|
||||
await testUtils.controlPanel.editSearch(searchBar, "General Kenobi");
|
||||
|
||||
assert.strictEqual(input.value, "General Kenobi", "input value should be updated");
|
||||
assert.containsOnce(searchBar, '.o_searchview_autocomplete');
|
||||
|
||||
await testUtils.dom.click(document.body);
|
||||
|
||||
assert.strictEqual(input.value, "", "input value should be empty");
|
||||
assert.containsNone(searchBar, '.o_searchview_autocomplete');
|
||||
});
|
||||
|
||||
QUnit.test('select an autocomplete field', async function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
let searchReadCount = 0;
|
||||
const webClient = await createWebClient({
|
||||
serverData,
|
||||
mockRPC: (route, args) => {
|
||||
if (route === '/web/dataset/search_read') {
|
||||
switch (searchReadCount) {
|
||||
case 0:
|
||||
// Done on loading
|
||||
break;
|
||||
case 1:
|
||||
assert.deepEqual(args.domain, [["foo", "ilike", "a"]]);
|
||||
break;
|
||||
}
|
||||
searchReadCount++;
|
||||
}
|
||||
}
|
||||
});
|
||||
await doAction(webClient, 1);
|
||||
|
||||
const searchInput = target.querySelector('.o_searchview_input');
|
||||
await testUtils.fields.editInput(searchInput, 'a');
|
||||
assert.containsN(target, '.o_searchview_autocomplete li', 2,
|
||||
"there should be 2 result for 'a' in search bar autocomplete");
|
||||
|
||||
await testUtils.dom.triggerEvent(searchInput, 'keydown', { key: 'Enter' });
|
||||
assert.strictEqual(target.querySelector('.o_searchview_input_container .o_facet_values').innerText.trim(),
|
||||
"a", "There should be a field facet with label 'a'");
|
||||
});
|
||||
|
||||
QUnit.test('autocomplete input is trimmed', async function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
let searchReadCount = 0;
|
||||
const webClient = await createWebClient({
|
||||
serverData,
|
||||
mockRPC: (route, args) => {
|
||||
if (route === '/web/dataset/search_read') {
|
||||
switch (searchReadCount) {
|
||||
case 0:
|
||||
// Done on loading
|
||||
break;
|
||||
case 1:
|
||||
assert.deepEqual(args.domain, [["foo", "ilike", "a"]]);
|
||||
break;
|
||||
}
|
||||
searchReadCount++;
|
||||
}
|
||||
}
|
||||
});
|
||||
await doAction(webClient, 1);
|
||||
|
||||
const searchInput = target.querySelector('.o_searchview_input');
|
||||
await testUtils.fields.editInput(searchInput, 'a ');
|
||||
assert.containsN(target, '.o_searchview_autocomplete li', 2,
|
||||
"there should be 2 result for 'a' in search bar autocomplete");
|
||||
|
||||
await testUtils.dom.triggerEvent(searchInput, 'keydown', {key: 'Enter'});
|
||||
assert.strictEqual(target.querySelector('.o_searchview_input_container .o_facet_values').innerText.trim(),
|
||||
"a", "There should be a field facet with label 'a'");
|
||||
});
|
||||
|
||||
QUnit.test('select an autocomplete field with `context` key', async function (assert) {
|
||||
assert.expect(9);
|
||||
|
||||
let searchReadCount = 0;
|
||||
const firstLoading = testUtils.makeTestPromise();
|
||||
const webClient = await createWebClient({
|
||||
serverData,
|
||||
mockRPC: (route, args) => {
|
||||
if (route === '/web/dataset/search_read') {
|
||||
switch (searchReadCount) {
|
||||
case 0:
|
||||
firstLoading.resolve();
|
||||
break;
|
||||
case 1:
|
||||
assert.deepEqual(args.domain, [["bar", "=", 1]]);
|
||||
assert.deepEqual(args.context.bar, [1]);
|
||||
break;
|
||||
case 2:
|
||||
assert.deepEqual(args.domain, ["|", ["bar", "=", 1], ["bar", "=", 2]]);
|
||||
assert.deepEqual(args.context.bar, [1, 2]);
|
||||
break;
|
||||
}
|
||||
searchReadCount++;
|
||||
}
|
||||
}
|
||||
});
|
||||
await doAction(webClient, 1);
|
||||
await firstLoading;
|
||||
assert.strictEqual(searchReadCount, 1, "there should be 1 search_read");
|
||||
const searchInput = target.querySelector('.o_searchview_input');
|
||||
|
||||
// 'r' key to filter on bar "First Record"
|
||||
await testUtils.fields.editInput(searchInput, 'record');
|
||||
await testUtils.dom.triggerEvent(searchInput, 'keydown', { key: 'ArrowDown' });
|
||||
await testUtils.dom.triggerEvent(searchInput, 'keydown', { key: 'ArrowRight' });
|
||||
await testUtils.dom.triggerEvent(searchInput, 'keydown', { key: 'ArrowDown' });
|
||||
await testUtils.dom.triggerEvent(searchInput, 'keydown', { key: 'Enter' });
|
||||
|
||||
assert.strictEqual(target.querySelector('.o_searchview_input_container .o_facet_values').innerText.trim(),
|
||||
"First record",
|
||||
"the autocompletion facet should be correct");
|
||||
assert.strictEqual(searchReadCount, 2, "there should be 2 search_read");
|
||||
|
||||
// 'r' key to filter on bar "Second Record"
|
||||
await testUtils.fields.editInput(searchInput, 'record');
|
||||
await testUtils.dom.triggerEvent(searchInput, 'keydown', { key: 'ArrowDown' });
|
||||
await testUtils.dom.triggerEvent(searchInput, 'keydown', { key: 'ArrowRight' });
|
||||
await testUtils.dom.triggerEvent(searchInput, 'keydown', { key: 'ArrowDown' });
|
||||
await testUtils.dom.triggerEvent(searchInput, 'keydown', { key: 'ArrowDown' });
|
||||
await testUtils.dom.triggerEvent(searchInput, 'keydown', { key: 'Enter' });
|
||||
|
||||
assert.strictEqual(target.querySelector('.o_searchview_input_container .o_facet_values').innerText.trim(),
|
||||
"First recordorSecond record",
|
||||
"the autocompletion facet should be correct");
|
||||
assert.strictEqual(searchReadCount, 3, "there should be 3 search_read");
|
||||
});
|
||||
|
||||
QUnit.test('no search text triggers a reload', async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
// Switch to pivot to ensure that the event comes from the control panel
|
||||
// (pivot does not have a handler on "reload" event).
|
||||
serverData.actions[1].views = [[false, "pivot"]];
|
||||
serverData.views['partner,false,pivot'] = `
|
||||
<pivot>
|
||||
<field name="foo" type="row"/>
|
||||
</pivot>
|
||||
`;
|
||||
|
||||
registry.category("services").add("user", makeFakeUserService());
|
||||
|
||||
let rpcs;
|
||||
const webClient = await createWebClient({
|
||||
serverData,
|
||||
mockRPC: () => { rpcs++; },
|
||||
});
|
||||
await doAction(webClient, 1);
|
||||
|
||||
const searchInput = target.querySelector('.o_searchview_input');
|
||||
rpcs = 0;
|
||||
await testUtils.dom.triggerEvent(searchInput, 'keydown', { key: 'Enter' });
|
||||
|
||||
assert.containsNone(target, '.o_searchview_facet_label');
|
||||
assert.strictEqual(rpcs, 2, "should have reloaded");
|
||||
});
|
||||
|
||||
QUnit.test('selecting (no result) triggers a re-render', async function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
const webClient = await createWebClient({ serverData });
|
||||
await doAction(webClient, 1);
|
||||
|
||||
const searchInput = target.querySelector('.o_searchview_input');
|
||||
|
||||
// 'a' key to filter nothing on bar
|
||||
await testUtils.fields.editInput(searchInput, 'hello there');
|
||||
await testUtils.dom.triggerEvent(searchInput, 'keydown', { key: 'ArrowDown' });
|
||||
await testUtils.dom.triggerEvent(searchInput, 'keydown', { key: 'ArrowRight' });
|
||||
await testUtils.dom.triggerEvent(searchInput, 'keydown', { key: 'ArrowDown' });
|
||||
|
||||
assert.strictEqual(target.querySelector('.o_searchview_autocomplete .focus').innerText.trim(), "(no result)",
|
||||
"there should be no result for 'a' in bar");
|
||||
|
||||
await testUtils.dom.triggerEvent(searchInput, 'keydown', { key: 'Enter' });
|
||||
|
||||
assert.containsNone(target, '.o_searchview_facet_label');
|
||||
assert.strictEqual(target.querySelector('.o_searchview_input').value, "",
|
||||
"the search input should be re-rendered");
|
||||
});
|
||||
|
||||
QUnit.test('update suggested filters in autocomplete menu with Japanese IME', async function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
// The goal here is to simulate as many events happening during an IME
|
||||
// assisted composition session as possible. Some of these events are
|
||||
// not handled but are triggered to ensure they do not interfere.
|
||||
const TEST = "TEST";
|
||||
const テスト = "テスト";
|
||||
const webClient = await createWebClient({ serverData });
|
||||
await doAction(webClient, 1);
|
||||
|
||||
const searchInput = target.querySelector('.o_searchview_input');
|
||||
|
||||
// Simulate typing "TEST" on search view.
|
||||
for (let i = 0; i < TEST.length; i++) {
|
||||
const key = TEST[i].toUpperCase();
|
||||
await testUtils.dom.triggerEvent(searchInput, 'keydown',
|
||||
{ key, isComposing: true });
|
||||
if (i === 0) {
|
||||
// Composition is initiated after the first keydown
|
||||
await testUtils.dom.triggerEvent(searchInput, 'compositionstart');
|
||||
}
|
||||
await testUtils.dom.triggerEvent(searchInput, 'keypress',
|
||||
{ key, isComposing: true });
|
||||
searchInput.value = TEST.slice(0, i + 1);
|
||||
await testUtils.dom.triggerEvent(searchInput, 'keyup',
|
||||
{ key, isComposing: true });
|
||||
await testUtils.dom.triggerEvent(searchInput, 'input',
|
||||
{ inputType: 'insertCompositionText', isComposing: true });
|
||||
}
|
||||
assert.containsOnce(target, '.o_searchview_autocomplete',
|
||||
"should display autocomplete dropdown menu on typing something in search view"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector('.o_searchview_autocomplete li').innerText.trim(),
|
||||
"Search Foo for: TEST",
|
||||
`1st filter suggestion should be based on typed word "TEST"`
|
||||
);
|
||||
|
||||
// Simulate soft-selection of another suggestion from IME through keyboard navigation.
|
||||
await testUtils.dom.triggerEvent(searchInput, 'keydown',
|
||||
{ key: 'ArrowDown', isComposing: true });
|
||||
await testUtils.dom.triggerEvent(searchInput, 'keypress',
|
||||
{ key: 'ArrowDown', isComposing: true });
|
||||
searchInput.value = テスト;
|
||||
await testUtils.dom.triggerEvent(searchInput, 'keyup',
|
||||
{ key: 'ArrowDown', isComposing: true });
|
||||
await testUtils.dom.triggerEvent(searchInput, 'input',
|
||||
{ inputType: 'insertCompositionText', isComposing: true });
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector('.o_searchview_autocomplete li').innerText.trim(),
|
||||
"Search Foo for: テスト",
|
||||
`1st filter suggestion should be updated with soft-selection typed word "テスト"`
|
||||
);
|
||||
|
||||
// Simulate selection on suggestion item "TEST" from IME.
|
||||
await testUtils.dom.triggerEvent(searchInput, 'keydown',
|
||||
{ key: 'Enter', isComposing: true });
|
||||
await testUtils.dom.triggerEvent(searchInput, 'keypress',
|
||||
{ key: 'Enter', isComposing: true });
|
||||
searchInput.value = TEST;
|
||||
await testUtils.dom.triggerEvent(searchInput, 'keyup',
|
||||
{ key: 'Enter', isComposing: true });
|
||||
await testUtils.dom.triggerEvent(searchInput, 'input',
|
||||
{ inputType: 'insertCompositionText', isComposing: true });
|
||||
|
||||
// End of the composition
|
||||
await testUtils.dom.triggerEvent(searchInput, 'compositionend');
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector('.o_searchview_autocomplete li').innerText.trim(),
|
||||
"Search Foo for: TEST",
|
||||
`1st filter suggestion should finally be updated with click selection on word "TEST" from IME`
|
||||
);
|
||||
|
||||
});
|
||||
|
||||
QUnit.test('open search view autocomplete on paste value using mouse', async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
const webClient = await createWebClient({ serverData });
|
||||
await doAction(webClient, 1);
|
||||
|
||||
// Simulate paste text through the mouse.
|
||||
const searchInput = target.querySelector('.o_searchview_input');
|
||||
searchInput.value = "ABC";
|
||||
await testUtils.dom.triggerEvent(searchInput, 'input',
|
||||
{ inputType: 'insertFromPaste' });
|
||||
await testUtils.nextTick();
|
||||
assert.containsOnce(target, '.o_searchview_autocomplete',
|
||||
"should display autocomplete dropdown menu on paste in search view");
|
||||
|
||||
});
|
||||
|
||||
QUnit.test('select autocompleted many2one', async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
serverData.views['partner,false,search'] = `
|
||||
<search>
|
||||
<field name="foo"/>
|
||||
<field name="birthday"/>
|
||||
<field name="birth_datetime"/>
|
||||
<field name="bar" operator="child_of"/>
|
||||
</search>
|
||||
`;
|
||||
|
||||
const webClient = await createWebClient({
|
||||
serverData,
|
||||
mockRPC: (route, { domain }) => {
|
||||
if (route === '/web/dataset/search_read') {
|
||||
assert.step(JSON.stringify(domain));
|
||||
}
|
||||
},
|
||||
});
|
||||
await doAction(webClient, 1);
|
||||
|
||||
await testUtils.controlPanel.editSearch(target, "rec");
|
||||
await testUtils.dom.click(target.querySelector('.o_searchview_autocomplete li:last-child'));
|
||||
|
||||
await cpHelpers.removeFacet(target, 0);
|
||||
|
||||
await testUtils.controlPanel.editSearch(target, "rec");
|
||||
await testUtils.dom.click(target.querySelector('.o_expand'));
|
||||
await testUtils.dom.click(target.querySelector('.o_searchview_autocomplete li.o_menu_item.o_indent'));
|
||||
|
||||
assert.verifySteps([
|
||||
'[]',
|
||||
'[["bar","child_of","rec"]]', // Incomplete string -> Name search
|
||||
'[]',
|
||||
'[["bar","child_of",1]]', // Suggestion select -> Specific ID
|
||||
]);
|
||||
|
||||
});
|
||||
|
||||
QUnit.test('"null" as autocomplete value', async function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
const webClient = await createWebClient({
|
||||
serverData,
|
||||
mockRPC: (route, { domain }) => {
|
||||
if (route === '/web/dataset/search_read') {
|
||||
assert.step(JSON.stringify(domain));
|
||||
}
|
||||
},
|
||||
});
|
||||
await doAction(webClient, 1);
|
||||
|
||||
await testUtils.controlPanel.editSearch(target, "null");
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector('.o_searchview_autocomplete .focus').innerText,
|
||||
"Search Foo for: null"
|
||||
);
|
||||
|
||||
await testUtils.dom.click(target.querySelector('.o_searchview_autocomplete li.focus a'));
|
||||
|
||||
assert.verifySteps([
|
||||
JSON.stringify([]), // initial search
|
||||
JSON.stringify([["foo", "ilike", "null"]]),
|
||||
]);
|
||||
|
||||
});
|
||||
|
||||
QUnit.test('autocompletion with a boolean field', async function (assert) {
|
||||
assert.expect(11);
|
||||
|
||||
serverData.views['partner,false,search'] = `
|
||||
<search><field name="bool"/></search>
|
||||
`;
|
||||
|
||||
const webClient = await createWebClient({
|
||||
serverData,
|
||||
mockRPC: (route, { domain }) => {
|
||||
if (route === '/web/dataset/search_read') {
|
||||
assert.step(JSON.stringify(domain));
|
||||
}
|
||||
},
|
||||
});
|
||||
await doAction(webClient, 1);
|
||||
|
||||
await testUtils.controlPanel.editSearch(target, "y");
|
||||
|
||||
assert.containsOnce(target, '.o_searchview_autocomplete li');
|
||||
assert.strictEqual(target.querySelector('.o_searchview_autocomplete li').innerText, "Search Bool: Yes");
|
||||
assert.doesNotHaveClass(target.querySelector('.o_searchview_autocomplete li'), 'o_indent');
|
||||
|
||||
// select "Yes"
|
||||
await testUtils.dom.click(target.querySelector('.o_searchview_autocomplete li'));
|
||||
|
||||
await cpHelpers.removeFacet(target, 0);
|
||||
|
||||
await testUtils.controlPanel.editSearch(target, "No");
|
||||
|
||||
assert.containsOnce(target, '.o_searchview_autocomplete li');
|
||||
assert.strictEqual(target.querySelector('.o_searchview_autocomplete li').innerText, "Search Bool: No");
|
||||
assert.doesNotHaveClass(target.querySelector('.o_searchview_autocomplete li'), 'o_indent');
|
||||
|
||||
// select "No"
|
||||
await testUtils.dom.click(target.querySelector('.o_searchview_autocomplete li'));
|
||||
|
||||
assert.verifySteps([
|
||||
JSON.stringify([]), // initial search
|
||||
JSON.stringify([["bool", "=", true]]),
|
||||
JSON.stringify([]),
|
||||
JSON.stringify([["bool", "=", false]]),
|
||||
]);
|
||||
});
|
||||
|
||||
QUnit.test('autocompletion with a selection field', async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
serverData.views['partner,false,search'] = `
|
||||
<search><field name="status"/></search>
|
||||
`;
|
||||
|
||||
const webClient = await createWebClient({ serverData });
|
||||
await doAction(webClient, 1);
|
||||
|
||||
await testUtils.controlPanel.editSearch(target, "n");
|
||||
|
||||
assert.containsN(target, '.o_searchview_autocomplete li', 2);
|
||||
assert.strictEqual(target.querySelector('.o_searchview_autocomplete li:first-child').innerText, "Search Status: New");
|
||||
assert.strictEqual(target.querySelector('.o_searchview_autocomplete li:last-child').innerText, "Search Status: Cancelled");
|
||||
assert.doesNotHaveClass(target.querySelector('.o_searchview_autocomplete li:first-child'), 'o_indent');
|
||||
assert.doesNotHaveClass(target.querySelector('.o_searchview_autocomplete li:last-child'), 'o_indent');
|
||||
});
|
||||
|
||||
QUnit.test("reference fields are supported in search view", async function (assert) {
|
||||
assert.expect(7);
|
||||
|
||||
const partnerModel = serverData.models.partner;
|
||||
|
||||
partnerModel.fields.ref = { type: 'reference', string: "Reference" };
|
||||
partnerModel.records.forEach((record, i) => {
|
||||
record.ref = `ref${String(i).padStart(3, "0")}`;
|
||||
});
|
||||
serverData.views["partner,false,search"] = `
|
||||
<search>
|
||||
<field name="ref"/>
|
||||
</search>
|
||||
`;
|
||||
|
||||
const webClient = await createWebClient({
|
||||
serverData,
|
||||
mockRPC: (route, { domain }) => {
|
||||
if (route === '/web/dataset/search_read') {
|
||||
assert.step(JSON.stringify(domain));
|
||||
}
|
||||
},
|
||||
});
|
||||
await doAction(webClient, 1);
|
||||
|
||||
await testUtils.controlPanel.editSearch(target, "ref");
|
||||
await cpHelpers.validateSearch(target);
|
||||
|
||||
assert.containsN(target, ".o_data_row", 5);
|
||||
|
||||
await cpHelpers.removeFacet(target, 0);
|
||||
await testUtils.controlPanel.editSearch(target, "ref002");
|
||||
await cpHelpers.validateSearch(target);
|
||||
|
||||
assert.containsOnce(target, ".o_data_row");
|
||||
|
||||
assert.verifySteps([
|
||||
'[]',
|
||||
'[["ref","ilike","ref"]]',
|
||||
'[]',
|
||||
'[["ref","ilike","ref002"]]',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,369 +0,0 @@
|
|||
odoo.define('web.search_utils_tests', function (require) {
|
||||
"use strict";
|
||||
|
||||
const { constructDateDomain } = require('web.searchUtils');
|
||||
const testUtils = require('web.test_utils');
|
||||
const { registry } = require("@web/core/registry");
|
||||
const { translatedTerms } = require("@web/core/l10n/translation");
|
||||
const { patchWithCleanup } = require("@web/../tests/helpers/utils");
|
||||
const { makeTestEnv } = require("@web/../tests/helpers/mock_env");
|
||||
const { makeFakeLocalizationService } = require("@web/../tests/helpers/mock_services");
|
||||
const { _t } = require('web.core');
|
||||
|
||||
const patchDate = testUtils.mock.patchDate;
|
||||
|
||||
QUnit.module('SearchUtils', function () {
|
||||
|
||||
QUnit.module('Construct domain');
|
||||
|
||||
QUnit.test('construct simple domain based on date field (no comparisonOptionId)', function (assert) {
|
||||
assert.expect(4);
|
||||
const unpatchDate = patchDate(2020, 5, 1, 13, 0, 0);
|
||||
const referenceMoment = moment().utc();
|
||||
assert.deepEqual(
|
||||
constructDateDomain(referenceMoment, 'date_field', 'date', []),
|
||||
{
|
||||
domain: "[]",
|
||||
description: "",
|
||||
}
|
||||
);
|
||||
assert.deepEqual(
|
||||
constructDateDomain(referenceMoment, 'date_field', 'date', ['this_month', 'this_year']),
|
||||
{
|
||||
domain: `["&", ["date_field", ">=", "2020-06-01"], ["date_field", "<=", "2020-06-30"]]`,
|
||||
description: "June 2020",
|
||||
}
|
||||
);
|
||||
assert.deepEqual(
|
||||
constructDateDomain(referenceMoment, 'date_field', 'date', ['second_quarter', 'this_year']),
|
||||
{
|
||||
domain: `["&", ["date_field", ">=", "2020-04-01"], ["date_field", "<=", "2020-06-30"]]`,
|
||||
description: "Q2 2020",
|
||||
}
|
||||
);
|
||||
assert.deepEqual(
|
||||
constructDateDomain(referenceMoment, 'date_field', 'date', ['this_year']),
|
||||
{
|
||||
domain: `["&", ["date_field", ">=", "2020-01-01"], ["date_field", "<=", "2020-12-31"]]`,
|
||||
description: "2020",
|
||||
}
|
||||
);
|
||||
unpatchDate();
|
||||
});
|
||||
|
||||
QUnit.test('construct simple domain based on datetime field (no comparisonOptionId)', function (assert) {
|
||||
assert.expect(3);
|
||||
const unpatchDate = patchDate(2020, 5, 1, 13, 0, 0);
|
||||
const referenceMoment = moment().utc();
|
||||
assert.deepEqual(
|
||||
constructDateDomain(referenceMoment, 'date_field', 'datetime', ['this_month', 'this_year']),
|
||||
{
|
||||
domain: `["&", ["date_field", ">=", "2020-06-01 00:00:00"], ["date_field", "<=", "2020-06-30 23:59:59"]]`,
|
||||
description: "June 2020",
|
||||
}
|
||||
);
|
||||
assert.deepEqual(
|
||||
constructDateDomain(referenceMoment, 'date_field', 'datetime', ['second_quarter', 'this_year']),
|
||||
{
|
||||
domain: `["&", ["date_field", ">=", "2020-04-01 00:00:00"], ["date_field", "<=", "2020-06-30 23:59:59"]]`,
|
||||
description: "Q2 2020",
|
||||
}
|
||||
);
|
||||
assert.deepEqual(
|
||||
constructDateDomain(referenceMoment, 'date_field', 'datetime', ['this_year']),
|
||||
{
|
||||
domain: `["&", ["date_field", ">=", "2020-01-01 00:00:00"], ["date_field", "<=", "2020-12-31 23:59:59"]]`,
|
||||
description: "2020",
|
||||
}
|
||||
);
|
||||
unpatchDate();
|
||||
});
|
||||
|
||||
QUnit.test('construct domain based on date field (no comparisonOptionId)', function (assert) {
|
||||
assert.expect(3);
|
||||
const unpatchDate = patchDate(2020, 0, 1, 12, 0, 0);
|
||||
const referenceMoment = moment().utc();
|
||||
assert.deepEqual(
|
||||
constructDateDomain(referenceMoment, 'date_field', 'date', ['this_month', 'first_quarter', 'this_year']),
|
||||
{
|
||||
domain: "[" +
|
||||
`"|", ` +
|
||||
`"&", ["date_field", ">=", "2020-01-01"], ["date_field", "<=", "2020-01-31"], ` +
|
||||
`"&", ["date_field", ">=", "2020-01-01"], ["date_field", "<=", "2020-03-31"]` +
|
||||
"]",
|
||||
description: "January 2020/Q1 2020",
|
||||
}
|
||||
);
|
||||
assert.deepEqual(
|
||||
constructDateDomain(referenceMoment, 'date_field', 'date', ['second_quarter', 'this_year', 'last_year']),
|
||||
{
|
||||
domain: "[" +
|
||||
`"|", ` +
|
||||
`"&", ["date_field", ">=", "2019-04-01"], ["date_field", "<=", "2019-06-30"], ` +
|
||||
`"&", ["date_field", ">=", "2020-04-01"], ["date_field", "<=", "2020-06-30"]` +
|
||||
"]",
|
||||
description: "Q2 2019/Q2 2020",
|
||||
}
|
||||
);
|
||||
assert.deepEqual(
|
||||
constructDateDomain(referenceMoment, 'date_field', 'date', ['this_year', 'this_month', 'antepenultimate_month']),
|
||||
{
|
||||
domain: "[" +
|
||||
`"|", ` +
|
||||
`"&", ["date_field", ">=", "2020-01-01"], ["date_field", "<=", "2020-01-31"], ` +
|
||||
`"&", ["date_field", ">=", "2020-11-01"], ["date_field", "<=", "2020-11-30"]` +
|
||||
"]",
|
||||
description: "January 2020/November 2020",
|
||||
}
|
||||
);
|
||||
unpatchDate();
|
||||
});
|
||||
|
||||
QUnit.test('construct domain based on datetime field (no comparisonOptionId)', function (assert) {
|
||||
assert.expect(3);
|
||||
const unpatchDate = patchDate(2020, 0, 1, 12, 0, 0);
|
||||
const referenceMoment = moment().utc();
|
||||
assert.deepEqual(
|
||||
constructDateDomain(referenceMoment, 'date_field', 'datetime', ['this_month', 'first_quarter', 'this_year']),
|
||||
{
|
||||
domain: "[" +
|
||||
`"|", ` +
|
||||
`"&", ["date_field", ">=", "2020-01-01 00:00:00"], ["date_field", "<=", "2020-01-31 23:59:59"], ` +
|
||||
`"&", ["date_field", ">=", "2020-01-01 00:00:00"], ["date_field", "<=", "2020-03-31 23:59:59"]` +
|
||||
"]",
|
||||
description: "January 2020/Q1 2020",
|
||||
}
|
||||
);
|
||||
assert.deepEqual(
|
||||
constructDateDomain(referenceMoment, 'date_field', 'datetime', ['second_quarter', 'this_year', 'last_year']),
|
||||
{
|
||||
domain: "[" +
|
||||
`"|", ` +
|
||||
`"&", ["date_field", ">=", "2019-04-01 00:00:00"], ["date_field", "<=", "2019-06-30 23:59:59"], ` +
|
||||
`"&", ["date_field", ">=", "2020-04-01 00:00:00"], ["date_field", "<=", "2020-06-30 23:59:59"]` +
|
||||
"]",
|
||||
description: "Q2 2019/Q2 2020",
|
||||
}
|
||||
);
|
||||
assert.deepEqual(
|
||||
constructDateDomain(referenceMoment, 'date_field', 'datetime', ['this_year', 'this_month', 'antepenultimate_month']),
|
||||
{
|
||||
domain: "[" +
|
||||
`"|", ` +
|
||||
`"&", ["date_field", ">=", "2020-01-01 00:00:00"], ["date_field", "<=", "2020-01-31 23:59:59"], ` +
|
||||
`"&", ["date_field", ">=", "2020-11-01 00:00:00"], ["date_field", "<=", "2020-11-30 23:59:59"]` +
|
||||
"]",
|
||||
description: "January 2020/November 2020",
|
||||
}
|
||||
);
|
||||
unpatchDate();
|
||||
});
|
||||
|
||||
QUnit.test('construct comparison domain based on date field and option "previous_period"', function (assert) {
|
||||
assert.expect(5);
|
||||
const unpatchDate = patchDate(2020, 0, 1, 12, 0, 0);
|
||||
const referenceMoment = moment().utc();
|
||||
assert.deepEqual(
|
||||
constructDateDomain(referenceMoment, 'date_field', 'date', ['this_month', 'first_quarter', 'this_year'], 'previous_period'),
|
||||
{
|
||||
domain: "[" +
|
||||
`"|", "|", ` +
|
||||
`"&", ["date_field", ">=", "2019-10-01"], ["date_field", "<=", "2019-10-31"], ` +
|
||||
`"&", ["date_field", ">=", "2019-11-01"], ["date_field", "<=", "2019-11-30"], ` +
|
||||
`"&", ["date_field", ">=", "2019-12-01"], ["date_field", "<=", "2019-12-31"]` +
|
||||
"]",
|
||||
description: "October 2019/November 2019/December 2019",
|
||||
}
|
||||
);
|
||||
assert.deepEqual(
|
||||
constructDateDomain(referenceMoment, 'date_field', 'date', ['second_quarter', 'this_year', 'last_year'], 'previous_period'),
|
||||
{
|
||||
domain: "[" +
|
||||
`"|", ` +
|
||||
`"&", ["date_field", ">=", "2018-01-01"], ["date_field", "<=", "2018-03-31"], ` +
|
||||
`"&", ["date_field", ">=", "2019-01-01"], ["date_field", "<=", "2019-03-31"]` +
|
||||
"]",
|
||||
description: "Q1 2018/Q1 2019",
|
||||
}
|
||||
);
|
||||
assert.deepEqual(
|
||||
constructDateDomain(referenceMoment, 'date_field', 'date', ['this_year', 'antepenultimate_year', 'this_month', 'antepenultimate_month'], 'previous_period'),
|
||||
{
|
||||
domain: "[" +
|
||||
`"|", "|", "|", ` +
|
||||
`"&", ["date_field", ">=", "2015-02-01"], ["date_field", "<=", "2015-02-28"], ` +
|
||||
`"&", ["date_field", ">=", "2015-12-01"], ["date_field", "<=", "2015-12-31"], ` +
|
||||
`"&", ["date_field", ">=", "2017-02-01"], ["date_field", "<=", "2017-02-28"], ` +
|
||||
`"&", ["date_field", ">=", "2017-12-01"], ["date_field", "<=", "2017-12-31"]` +
|
||||
"]",
|
||||
description: "February 2015/December 2015/February 2017/December 2017",
|
||||
}
|
||||
);
|
||||
assert.deepEqual(
|
||||
constructDateDomain(referenceMoment, 'date_field', 'date', ['this_year', 'last_year'], 'previous_period'),
|
||||
{
|
||||
domain: "[" +
|
||||
`"|", ` +
|
||||
`"&", ["date_field", ">=", "2017-01-01"], ["date_field", "<=", "2017-12-31"], ` +
|
||||
`"&", ["date_field", ">=", "2018-01-01"], ["date_field", "<=", "2018-12-31"]` +
|
||||
"]",
|
||||
description: "2017/2018",
|
||||
}
|
||||
);
|
||||
assert.deepEqual(
|
||||
constructDateDomain(referenceMoment, 'date_field', 'date', ['second_quarter', 'third_quarter', 'last_year'], 'previous_period'),
|
||||
{
|
||||
domain: "[" +
|
||||
`"|", ` +
|
||||
`"&", ["date_field", ">=", "2018-10-01"], ["date_field", "<=", "2018-12-31"], ` +
|
||||
`"&", ["date_field", ">=", "2019-01-01"], ["date_field", "<=", "2019-03-31"]` +
|
||||
"]",
|
||||
description: "Q4 2018/Q1 2019",
|
||||
}
|
||||
);
|
||||
unpatchDate();
|
||||
});
|
||||
|
||||
QUnit.test('construct comparison domain based on datetime field and option "previous_year"', function (assert) {
|
||||
assert.expect(3);
|
||||
const unpatchDate = patchDate(2020, 5, 1, 13, 0, 0);
|
||||
const referenceMoment = moment().utc();
|
||||
assert.deepEqual(
|
||||
constructDateDomain(referenceMoment, 'date_field', 'datetime', ['this_month', 'first_quarter', 'this_year'], 'previous_year'),
|
||||
{
|
||||
domain: "[" +
|
||||
`"|", ` +
|
||||
`"&", ["date_field", ">=", "2019-06-01 00:00:00"], ["date_field", "<=", "2019-06-30 23:59:59"], ` +
|
||||
`"&", ["date_field", ">=", "2019-01-01 00:00:00"], ["date_field", "<=", "2019-03-31 23:59:59"]` +
|
||||
"]",
|
||||
description: "June 2019/Q1 2019",
|
||||
}
|
||||
);
|
||||
assert.deepEqual(
|
||||
constructDateDomain(referenceMoment, 'date_field', 'datetime', ['second_quarter', 'this_year', 'last_year'], 'previous_year'),
|
||||
{
|
||||
domain: "[" +
|
||||
`"|", ` +
|
||||
`"&", ["date_field", ">=", "2018-04-01 00:00:00"], ["date_field", "<=", "2018-06-30 23:59:59"], ` +
|
||||
`"&", ["date_field", ">=", "2019-04-01 00:00:00"], ["date_field", "<=", "2019-06-30 23:59:59"]` +
|
||||
"]",
|
||||
description: "Q2 2018/Q2 2019",
|
||||
}
|
||||
);
|
||||
assert.deepEqual(
|
||||
constructDateDomain(referenceMoment, 'date_field', 'datetime', ['this_year', 'antepenultimate_year', 'this_month', 'antepenultimate_month'], 'previous_year'),
|
||||
{
|
||||
domain: "[" +
|
||||
`"|", "|", "|", ` +
|
||||
`"&", ["date_field", ">=", "2017-04-01 00:00:00"], ["date_field", "<=", "2017-04-30 23:59:59"], ` +
|
||||
`"&", ["date_field", ">=", "2017-06-01 00:00:00"], ["date_field", "<=", "2017-06-30 23:59:59"], ` +
|
||||
`"&", ["date_field", ">=", "2019-04-01 00:00:00"], ["date_field", "<=", "2019-04-30 23:59:59"], ` +
|
||||
`"&", ["date_field", ">=", "2019-06-01 00:00:00"], ["date_field", "<=", "2019-06-30 23:59:59"]` +
|
||||
"]",
|
||||
description: "April 2017/June 2017/April 2019/June 2019",
|
||||
}
|
||||
);
|
||||
unpatchDate();
|
||||
});
|
||||
|
||||
QUnit.module('Options translation');
|
||||
|
||||
QUnit.test("Quarter option: custom translation", async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
const unpatchDate = patchDate(2020, 5, 1, 13, 0, 0);
|
||||
const referenceMoment = moment().locale('en');
|
||||
registry.category("services").add("localization", makeFakeLocalizationService());
|
||||
await makeTestEnv();
|
||||
patchWithCleanup(translatedTerms, {
|
||||
"Q2": "Deuxième trimestre de l'an de grâce",
|
||||
});
|
||||
|
||||
assert.deepEqual(
|
||||
constructDateDomain(referenceMoment, 'date_field', 'date', ['second_quarter', 'this_year']),
|
||||
{
|
||||
domain: `["&", ["date_field", ">=", "2020-04-01"], ["date_field", "<=", "2020-06-30"]]`,
|
||||
description: "Deuxième trimestre de l'an de grâce 2020",
|
||||
},
|
||||
"Quarter term should be translated"
|
||||
);
|
||||
|
||||
unpatchDate();
|
||||
});
|
||||
|
||||
QUnit.test("Quarter option: right to left", async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
const unpatchDate = patchDate(2020, 5, 1, 13, 0, 0);
|
||||
const referenceMoment = moment().locale('en');
|
||||
testUtils.mock.patch(_t.database.parameters, {
|
||||
direction: "rtl",
|
||||
});
|
||||
|
||||
assert.deepEqual(
|
||||
constructDateDomain(referenceMoment, 'date_field', 'date', ['second_quarter', 'this_year']),
|
||||
{
|
||||
domain: `["&", ["date_field", ">=", "2020-04-01"], ["date_field", "<=", "2020-06-30"]]`,
|
||||
description: "2020 Q2",
|
||||
},
|
||||
"Notation should be right to left"
|
||||
);
|
||||
|
||||
unpatchDate();
|
||||
testUtils.mock.unpatch(_t.database.parameters);
|
||||
});
|
||||
|
||||
QUnit.test("Quarter option: custom translation and right to left", async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
const unpatchDate = patchDate(2020, 5, 1, 13, 0, 0);
|
||||
const referenceMoment = moment().locale('en');
|
||||
registry.category("services").add("localization", makeFakeLocalizationService());
|
||||
await makeTestEnv();
|
||||
patchWithCleanup(translatedTerms, {
|
||||
"Q2": "2e Trimestre",
|
||||
});
|
||||
testUtils.mock.patch(_t.database.parameters, {
|
||||
direction: "rtl",
|
||||
});
|
||||
|
||||
assert.deepEqual(
|
||||
constructDateDomain(referenceMoment, 'date_field', 'date', ['second_quarter', 'this_year']),
|
||||
{
|
||||
domain: `["&", ["date_field", ">=", "2020-04-01"], ["date_field", "<=", "2020-06-30"]]`,
|
||||
description: "2020 2e Trimestre",
|
||||
},
|
||||
"Quarter term should be translated and notation should be right to left"
|
||||
);
|
||||
|
||||
unpatchDate();
|
||||
testUtils.mock.unpatch(_t.database.parameters);
|
||||
});
|
||||
|
||||
QUnit.test("Moment.js localization does not affect formatted domain dates", async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
const unpatchDate = patchDate(2020, 5, 1, 13, 0, 0);
|
||||
const initialLocale = moment.locale();
|
||||
moment.defineLocale('addoneForTest', {
|
||||
postformat: function (string) {
|
||||
return string.replace(/\d/g, match => (1 + parseInt(match)) % 10);
|
||||
}
|
||||
});
|
||||
const referenceMoment = moment().locale('addoneForTest');
|
||||
|
||||
assert.deepEqual(
|
||||
constructDateDomain(referenceMoment, 'date_field', 'date', ['this_month', 'this_year']),
|
||||
{
|
||||
domain: `["&", ["date_field", ">=", "2020-06-01"], ["date_field", "<=", "2020-06-30"]]`,
|
||||
description: "June 3131",
|
||||
},
|
||||
"Numbers in domain should not use addoneForTest locale"
|
||||
);
|
||||
|
||||
moment.locale(initialLocale);
|
||||
moment.updateLocale("addoneForTest", null);
|
||||
unpatchDate();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,592 +0,0 @@
|
|||
odoo.define('web.concurrency_tests', function (require) {
|
||||
"use strict";
|
||||
|
||||
var concurrency = require('web.concurrency');
|
||||
var testUtils = require('web.test_utils');
|
||||
|
||||
var makeTestPromise = testUtils.makeTestPromise;
|
||||
var makeTestPromiseWithAssert = testUtils.makeTestPromiseWithAssert;
|
||||
|
||||
QUnit.module('core', {}, function () {
|
||||
|
||||
QUnit.module('concurrency');
|
||||
|
||||
QUnit.test('mutex: simple scheduling', async function (assert) {
|
||||
assert.expect(5);
|
||||
var mutex = new concurrency.Mutex();
|
||||
|
||||
var prom1 = makeTestPromiseWithAssert(assert, 'prom1');
|
||||
var prom2 = makeTestPromiseWithAssert(assert, 'prom2');
|
||||
|
||||
mutex.exec(function () { return prom1; });
|
||||
mutex.exec(function () { return prom2; });
|
||||
|
||||
assert.verifySteps([]);
|
||||
|
||||
await prom1.resolve();
|
||||
|
||||
assert.verifySteps(['ok prom1']);
|
||||
|
||||
await prom2.resolve();
|
||||
|
||||
assert.verifySteps(['ok prom2']);
|
||||
});
|
||||
|
||||
QUnit.test('mutex: simpleScheduling2', async function (assert) {
|
||||
assert.expect(5);
|
||||
var mutex = new concurrency.Mutex();
|
||||
|
||||
var prom1 = makeTestPromiseWithAssert(assert, 'prom1');
|
||||
var prom2 = makeTestPromiseWithAssert(assert, 'prom2');
|
||||
|
||||
mutex.exec(function () { return prom1; });
|
||||
mutex.exec(function () { return prom2; });
|
||||
|
||||
assert.verifySteps([]);
|
||||
|
||||
await prom2.resolve();
|
||||
|
||||
assert.verifySteps(['ok prom2']);
|
||||
|
||||
await prom1.resolve();
|
||||
|
||||
assert.verifySteps(['ok prom1']);
|
||||
});
|
||||
|
||||
QUnit.test('mutex: reject', async function (assert) {
|
||||
assert.expect(7);
|
||||
var mutex = new concurrency.Mutex();
|
||||
|
||||
var prom1 = makeTestPromiseWithAssert(assert, 'prom1');
|
||||
var prom2 = makeTestPromiseWithAssert(assert, 'prom2');
|
||||
var prom3 = makeTestPromiseWithAssert(assert, 'prom3');
|
||||
|
||||
mutex.exec(function () { return prom1; }).catch(function () {});
|
||||
mutex.exec(function () { return prom2; }).catch(function () {});
|
||||
mutex.exec(function () { return prom3; }).catch(function () {});
|
||||
|
||||
assert.verifySteps([]);
|
||||
|
||||
prom1.resolve();
|
||||
await testUtils.nextMicrotaskTick();
|
||||
|
||||
assert.verifySteps(['ok prom1']);
|
||||
|
||||
prom2.catch(function () {
|
||||
assert.verifySteps(['ko prom2']);
|
||||
});
|
||||
prom2.reject({name: "sdkjfmqsjdfmsjkdfkljsdq"});
|
||||
await testUtils.nextMicrotaskTick();
|
||||
|
||||
prom3.resolve();
|
||||
await testUtils.nextMicrotaskTick();
|
||||
|
||||
assert.verifySteps(['ok prom3']);
|
||||
});
|
||||
|
||||
QUnit.test('mutex: getUnlockedDef checks', async function (assert) {
|
||||
assert.expect(9);
|
||||
|
||||
var mutex = new concurrency.Mutex();
|
||||
|
||||
var prom1 = makeTestPromiseWithAssert(assert, 'prom1');
|
||||
var prom2 = makeTestPromiseWithAssert(assert, 'prom2');
|
||||
|
||||
mutex.getUnlockedDef().then(function () {
|
||||
assert.step('mutex unlocked (1)');
|
||||
});
|
||||
|
||||
await testUtils.nextMicrotaskTick();
|
||||
|
||||
assert.verifySteps(['mutex unlocked (1)']);
|
||||
|
||||
mutex.exec(function () { return prom1; });
|
||||
await testUtils.nextMicrotaskTick();
|
||||
|
||||
mutex.getUnlockedDef().then(function () {
|
||||
assert.step('mutex unlocked (2)');
|
||||
});
|
||||
|
||||
assert.verifySteps([]);
|
||||
|
||||
mutex.exec(function () { return prom2; });
|
||||
await testUtils.nextMicrotaskTick();
|
||||
|
||||
assert.verifySteps([]);
|
||||
|
||||
await prom1.resolve();
|
||||
|
||||
assert.verifySteps(['ok prom1']);
|
||||
|
||||
prom2.resolve();
|
||||
await testUtils.nextTick();
|
||||
|
||||
assert.verifySteps(['ok prom2', 'mutex unlocked (2)']);
|
||||
});
|
||||
|
||||
QUnit.test('mutex: error and getUnlockedDef', async function (assert) {
|
||||
const mutex = new concurrency.Mutex();
|
||||
mutex.exec(async () => {
|
||||
await Promise.resolve();
|
||||
throw new Error("boom");
|
||||
}).catch(() => assert.step("prom rejected"));
|
||||
await testUtils.nextTick();
|
||||
assert.verifySteps(['prom rejected']);
|
||||
|
||||
mutex.getUnlockedDef().then(function () {
|
||||
assert.step('mutex unlocked');
|
||||
});
|
||||
await testUtils.nextMicrotaskTick();
|
||||
assert.verifySteps(['mutex unlocked']);
|
||||
});
|
||||
|
||||
QUnit.test('DropPrevious: basic usecase', async function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
var dp = new concurrency.DropPrevious();
|
||||
|
||||
var prom1 = makeTestPromise(assert, 'prom1');
|
||||
var prom2 = makeTestPromise(assert, 'prom2');
|
||||
|
||||
dp.add(prom1).then(() => assert.step('should not go here'))
|
||||
.catch(()=> assert.step("rejected dp1"));
|
||||
dp.add(prom2).then(() => assert.step("ok dp2"));
|
||||
|
||||
await testUtils.nextMicrotaskTick();
|
||||
assert.verifySteps(['rejected dp1']);
|
||||
|
||||
prom2.resolve();
|
||||
await testUtils.nextMicrotaskTick();
|
||||
|
||||
assert.verifySteps(['ok dp2']);
|
||||
});
|
||||
|
||||
QUnit.test('DropPrevious: resolve first before last', async function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
var dp = new concurrency.DropPrevious();
|
||||
|
||||
var prom1 = makeTestPromise(assert, 'prom1');
|
||||
var prom2 = makeTestPromise(assert, 'prom2');
|
||||
|
||||
dp.add(prom1).then(() => assert.step('should not go here'))
|
||||
.catch(()=> assert.step("rejected dp1"));
|
||||
dp.add(prom2).then(() => assert.step("ok dp2"));
|
||||
|
||||
|
||||
await testUtils.nextMicrotaskTick();
|
||||
|
||||
assert.verifySteps(['rejected dp1']);
|
||||
|
||||
prom1.resolve();
|
||||
prom2.resolve();
|
||||
await testUtils.nextMicrotaskTick();
|
||||
|
||||
assert.verifySteps(['ok dp2']);
|
||||
});
|
||||
|
||||
QUnit.test('DropMisordered: resolve all correctly ordered, sync', async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
var dm = new concurrency.DropMisordered(),
|
||||
flag = false;
|
||||
|
||||
var d1 = makeTestPromise();
|
||||
var d2 = makeTestPromise();
|
||||
|
||||
var r1 = dm.add(d1),
|
||||
r2 = dm.add(d2);
|
||||
|
||||
Promise.all([r1, r2]).then(function () {
|
||||
flag = true;
|
||||
});
|
||||
|
||||
d1.resolve();
|
||||
d2.resolve();
|
||||
await testUtils.nextTick();
|
||||
|
||||
assert.ok(flag);
|
||||
});
|
||||
|
||||
QUnit.test("DropMisordered: don't resolve mis-ordered, sync", async function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
var dm = new concurrency.DropMisordered(),
|
||||
done1 = false,
|
||||
done2 = false,
|
||||
fail1 = false,
|
||||
fail2 = false;
|
||||
|
||||
var d1 = makeTestPromise();
|
||||
var d2 = makeTestPromise();
|
||||
|
||||
dm.add(d1).then(function () { done1 = true; })
|
||||
.catch(function () { fail1 = true; });
|
||||
dm.add(d2).then(function () { done2 = true; })
|
||||
.catch(function () { fail2 = true; });
|
||||
|
||||
d2.resolve();
|
||||
d1.resolve();
|
||||
await testUtils.nextMicrotaskTick();
|
||||
|
||||
// d1 is in limbo
|
||||
assert.ok(!done1);
|
||||
assert.ok(!fail1);
|
||||
|
||||
// d2 is fulfilled
|
||||
assert.ok(done2);
|
||||
assert.ok(!fail2);
|
||||
});
|
||||
|
||||
QUnit.test('DropMisordered: fail mis-ordered flag, sync', async function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
var dm = new concurrency.DropMisordered(true/* failMisordered */),
|
||||
done1 = false,
|
||||
done2 = false,
|
||||
fail1 = false,
|
||||
fail2 = false;
|
||||
|
||||
var d1 = makeTestPromise();
|
||||
var d2 = makeTestPromise();
|
||||
|
||||
dm.add(d1).then(function () { done1 = true; })
|
||||
.catch(function () { fail1 = true; });
|
||||
dm.add(d2).then(function () { done2 = true; })
|
||||
.catch(function () { fail2 = true; });
|
||||
|
||||
d2.resolve();
|
||||
d1.resolve();
|
||||
await testUtils.nextMicrotaskTick();
|
||||
|
||||
// d1 is in limbo
|
||||
assert.ok(!done1);
|
||||
assert.ok(fail1);
|
||||
|
||||
// d2 is resolved
|
||||
assert.ok(done2);
|
||||
assert.ok(!fail2);
|
||||
});
|
||||
|
||||
QUnit.test('DropMisordered: resolve all correctly ordered, async', function (assert) {
|
||||
var done = assert.async();
|
||||
assert.expect(1);
|
||||
|
||||
var dm = new concurrency.DropMisordered();
|
||||
|
||||
var d1 = makeTestPromise();
|
||||
var d2 = makeTestPromise();
|
||||
|
||||
var r1 = dm.add(d1),
|
||||
r2 = dm.add(d2);
|
||||
|
||||
setTimeout(function () { d1.resolve(); }, 10);
|
||||
setTimeout(function () { d2.resolve(); }, 20);
|
||||
|
||||
Promise.all([r1, r2]).then(function () {
|
||||
assert.ok(true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("DropMisordered: don't resolve mis-ordered, async", function (assert) {
|
||||
var done = assert.async();
|
||||
assert.expect(4);
|
||||
|
||||
var dm = new concurrency.DropMisordered(),
|
||||
done1 = false, done2 = false,
|
||||
fail1 = false, fail2 = false;
|
||||
|
||||
var d1 = makeTestPromise();
|
||||
var d2 = makeTestPromise();
|
||||
|
||||
dm.add(d1).then(function () { done1 = true; })
|
||||
.catch(function () { fail1 = true; });
|
||||
dm.add(d2).then(function () { done2 = true; })
|
||||
.catch(function () { fail2 = true; });
|
||||
|
||||
setTimeout(function () { d1.resolve(); }, 20);
|
||||
setTimeout(function () { d2.resolve(); }, 10);
|
||||
|
||||
setTimeout(function () {
|
||||
// d1 is in limbo
|
||||
assert.ok(!done1);
|
||||
assert.ok(!fail1);
|
||||
|
||||
// d2 is resolved
|
||||
assert.ok(done2);
|
||||
assert.ok(!fail2);
|
||||
done();
|
||||
}, 30);
|
||||
});
|
||||
|
||||
QUnit.test('DropMisordered: fail mis-ordered flag, async', function (assert) {
|
||||
var done = assert.async();
|
||||
assert.expect(4);
|
||||
|
||||
var dm = new concurrency.DropMisordered(true),
|
||||
done1 = false, done2 = false,
|
||||
fail1 = false, fail2 = false;
|
||||
|
||||
var d1 = makeTestPromise();
|
||||
var d2 = makeTestPromise();
|
||||
|
||||
dm.add(d1).then(function () { done1 = true; })
|
||||
.catch(function () { fail1 = true; });
|
||||
dm.add(d2).then(function () { done2 = true; })
|
||||
.catch(function () { fail2 = true; });
|
||||
|
||||
setTimeout(function () { d1.resolve(); }, 20);
|
||||
setTimeout(function () { d2.resolve(); }, 10);
|
||||
|
||||
setTimeout(function () {
|
||||
// d1 is failed
|
||||
assert.ok(!done1);
|
||||
assert.ok(fail1);
|
||||
|
||||
// d2 is resolved
|
||||
assert.ok(done2);
|
||||
assert.ok(!fail2);
|
||||
done();
|
||||
}, 30);
|
||||
});
|
||||
|
||||
QUnit.test('MutexedDropPrevious: simple', async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
var m = new concurrency.MutexedDropPrevious();
|
||||
var d1 = makeTestPromise();
|
||||
|
||||
d1.then(function () {
|
||||
assert.step("d1 resolved");
|
||||
});
|
||||
m.exec(function () { return d1; }).then(function (result) {
|
||||
assert.step("p1 done");
|
||||
assert.strictEqual(result, 'd1');
|
||||
});
|
||||
|
||||
assert.verifySteps([]);
|
||||
d1.resolve('d1');
|
||||
await testUtils.nextMicrotaskTick();
|
||||
|
||||
assert.verifySteps(["d1 resolved","p1 done"]);
|
||||
});
|
||||
|
||||
QUnit.test('MutexedDropPrevious: d2 arrives after d1 resolution', async function (assert) {
|
||||
assert.expect(8);
|
||||
|
||||
var m = new concurrency.MutexedDropPrevious();
|
||||
var d1 = makeTestPromiseWithAssert(assert, 'd1');
|
||||
|
||||
m.exec(function () { return d1; }).then(function () {
|
||||
assert.step("p1 resolved");
|
||||
});
|
||||
|
||||
assert.verifySteps([]);
|
||||
d1.resolve('d1');
|
||||
await testUtils.nextMicrotaskTick();
|
||||
|
||||
assert.verifySteps(['ok d1','p1 resolved']);
|
||||
|
||||
var d2 = makeTestPromiseWithAssert(assert, 'd2');
|
||||
m.exec(function () { return d2; }).then(function () {
|
||||
assert.step("p2 resolved");
|
||||
});
|
||||
|
||||
assert.verifySteps([]);
|
||||
d2.resolve('d2');
|
||||
await testUtils.nextMicrotaskTick();
|
||||
|
||||
assert.verifySteps(['ok d2','p2 resolved']);
|
||||
});
|
||||
|
||||
QUnit.test('MutexedDropPrevious: p1 does not return a deferred', async function (assert) {
|
||||
assert.expect(7);
|
||||
|
||||
var m = new concurrency.MutexedDropPrevious();
|
||||
|
||||
m.exec(function () { return 42; }).then(function () {
|
||||
assert.step("p1 resolved");
|
||||
});
|
||||
|
||||
assert.verifySteps([]);
|
||||
await testUtils.nextMicrotaskTick();
|
||||
|
||||
assert.verifySteps(['p1 resolved']);
|
||||
|
||||
var d2 = makeTestPromiseWithAssert(assert, 'd2');
|
||||
m.exec(function () { return d2; }).then(function () {
|
||||
assert.step("p2 resolved");
|
||||
});
|
||||
|
||||
assert.verifySteps([]);
|
||||
d2.resolve('d2');
|
||||
await testUtils.nextMicrotaskTick();
|
||||
assert.verifySteps(['ok d2','p2 resolved']);
|
||||
});
|
||||
|
||||
QUnit.test('MutexedDropPrevious: p2 arrives before p1 resolution', async function (assert) {
|
||||
assert.expect(8);
|
||||
|
||||
var m = new concurrency.MutexedDropPrevious();
|
||||
var d1 = makeTestPromiseWithAssert(assert, 'd1');
|
||||
|
||||
m.exec(function () { return d1; }).catch(function () {
|
||||
assert.step("p1 rejected");
|
||||
});
|
||||
assert.verifySteps([]);
|
||||
|
||||
var d2 = makeTestPromiseWithAssert(assert, 'd2');
|
||||
m.exec(function () { return d2; }).then(function () {
|
||||
assert.step("p2 resolved");
|
||||
});
|
||||
|
||||
assert.verifySteps([]);
|
||||
d1.resolve('d1');
|
||||
await testUtils.nextMicrotaskTick();
|
||||
assert.verifySteps(['p1 rejected', 'ok d1']);
|
||||
|
||||
d2.resolve('d2');
|
||||
await testUtils.nextMicrotaskTick();
|
||||
assert.verifySteps(['ok d2', 'p2 resolved']);
|
||||
});
|
||||
|
||||
QUnit.test('MutexedDropPrevious: 3 arrives before 2 initialization', async function (assert) {
|
||||
assert.expect(10);
|
||||
var m = new concurrency.MutexedDropPrevious();
|
||||
|
||||
var d1 = makeTestPromiseWithAssert(assert, 'd1');
|
||||
var d3 = makeTestPromiseWithAssert(assert, 'd3');
|
||||
|
||||
m.exec(function () { return d1; }).catch(function () {
|
||||
assert.step('p1 rejected');
|
||||
});
|
||||
|
||||
m.exec(function () {
|
||||
assert.ok(false, "should not execute this function");
|
||||
}).catch(function () {
|
||||
assert.step('p2 rejected');
|
||||
});
|
||||
|
||||
m.exec(function () { return d3; }).then(function (result) {
|
||||
assert.strictEqual(result, 'd3');
|
||||
assert.step('p3 resolved');
|
||||
});
|
||||
|
||||
assert.verifySteps([]);
|
||||
|
||||
await testUtils.nextMicrotaskTick();
|
||||
|
||||
assert.verifySteps(['p1 rejected', 'p2 rejected']);
|
||||
|
||||
d1.resolve('d1');
|
||||
await testUtils.nextMicrotaskTick();
|
||||
|
||||
assert.verifySteps(['ok d1']);
|
||||
|
||||
d3.resolve('d3');
|
||||
await testUtils.nextTick();
|
||||
|
||||
|
||||
assert.verifySteps(['ok d3','p3 resolved']);
|
||||
});
|
||||
|
||||
QUnit.test('MutexedDropPrevious: 3 arrives after 2 initialization', async function (assert) {
|
||||
assert.expect(15);
|
||||
var m = new concurrency.MutexedDropPrevious();
|
||||
|
||||
var d1 = makeTestPromiseWithAssert(assert, 'd1');
|
||||
var d2 = makeTestPromiseWithAssert(assert, 'd2');
|
||||
var d3 = makeTestPromiseWithAssert(assert, 'd3');
|
||||
|
||||
m.exec(function () {
|
||||
assert.step('execute d1');
|
||||
return d1;
|
||||
}).catch(function () {
|
||||
assert.step('p1 rejected');
|
||||
});
|
||||
|
||||
m.exec(function () {
|
||||
assert.step('execute d2');
|
||||
return d2;
|
||||
}).catch(function () {
|
||||
assert.step('p2 rejected');
|
||||
});
|
||||
|
||||
assert.verifySteps(['execute d1']);
|
||||
|
||||
await testUtils.nextMicrotaskTick();
|
||||
assert.verifySteps(['p1 rejected']);
|
||||
|
||||
d1.resolve('d1');
|
||||
await testUtils.nextMicrotaskTick();
|
||||
|
||||
assert.verifySteps(['ok d1', 'execute d2']);
|
||||
|
||||
m.exec(function () {
|
||||
assert.step('execute d3');
|
||||
return d3;
|
||||
}).then(function () {
|
||||
assert.step('p3 resolved');
|
||||
});
|
||||
await testUtils.nextMicrotaskTick();
|
||||
assert.verifySteps(['p2 rejected']);
|
||||
|
||||
d2.resolve();
|
||||
await testUtils.nextMicrotaskTick();
|
||||
assert.verifySteps(['ok d2', 'execute d3']);
|
||||
|
||||
d3.resolve();
|
||||
await testUtils.nextTick();
|
||||
assert.verifySteps(['ok d3', 'p3 resolved']);
|
||||
|
||||
});
|
||||
|
||||
QUnit.test('MutexedDropPrevious: 2 in then of 1 with 3', async function (assert) {
|
||||
assert.expect(9);
|
||||
|
||||
var m = new concurrency.MutexedDropPrevious();
|
||||
|
||||
var d1 = makeTestPromiseWithAssert(assert, 'd1');
|
||||
var d2 = makeTestPromiseWithAssert(assert, 'd2');
|
||||
var d3 = makeTestPromiseWithAssert(assert, 'd3');
|
||||
var p3;
|
||||
|
||||
m.exec(function () { return d1; })
|
||||
.catch(function () {
|
||||
assert.step('p1 rejected');
|
||||
p3 = m.exec(function () {
|
||||
return d3;
|
||||
}).then(function () {
|
||||
assert.step('p3 resolved');
|
||||
});
|
||||
return p3;
|
||||
});
|
||||
|
||||
await testUtils.nextTick();
|
||||
assert.verifySteps([]);
|
||||
|
||||
m.exec(function () {
|
||||
assert.ok(false, 'should not execute this function');
|
||||
return d2;
|
||||
}).catch(function () {
|
||||
assert.step('p2 rejected');
|
||||
});
|
||||
|
||||
await testUtils.nextTick();
|
||||
assert.verifySteps(['p1 rejected', 'p2 rejected']);
|
||||
|
||||
d1.resolve('d1');
|
||||
await testUtils.nextTick();
|
||||
|
||||
assert.verifySteps(['ok d1']);
|
||||
|
||||
d3.resolve('d3');
|
||||
await testUtils.nextTick();
|
||||
|
||||
assert.verifySteps(['ok d3', 'p3 resolved']);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
|
@ -0,0 +1,195 @@
|
|||
/** @odoo-module alias=@web/../tests/core/datetime/datetime_test_helpers default=false */
|
||||
|
||||
import { patchWithCleanup } from "@web/../tests/helpers/utils";
|
||||
import { localization } from "@web/core/l10n/localization";
|
||||
import { ensureArray } from "@web/core/utils/arrays";
|
||||
import { click, getFixture } from "../../helpers/utils";
|
||||
|
||||
/**
|
||||
* @typedef {import("@web/core/datetime/datetime_picker").DateTimePickerProps} DateTimePickerProps
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {false | {
|
||||
* title?: string | string[],
|
||||
* date?: {
|
||||
* cells: (number | string | [number] | [string])[][],
|
||||
* daysOfWeek?: string[],
|
||||
* weekNumbers?: number[],
|
||||
* }[],
|
||||
* time?: ([number, number] | [number, number, "AM" | "PM"])[],
|
||||
* }} expectedParams
|
||||
*/
|
||||
export function assertDateTimePicker(expectedParams) {
|
||||
const assert = QUnit.assert;
|
||||
const fixture = getFixture();
|
||||
|
||||
// Check for picker in DOM
|
||||
if (expectedParams) {
|
||||
assert.containsOnce(fixture, ".o_datetime_picker");
|
||||
} else {
|
||||
assert.containsNone(fixture, ".o_datetime_picker");
|
||||
return;
|
||||
}
|
||||
|
||||
const { title, date, time } = expectedParams;
|
||||
|
||||
// Title
|
||||
if (title) {
|
||||
const expectedTitle = ensureArray(title);
|
||||
assert.containsOnce(fixture, ".o_datetime_picker_header");
|
||||
assert.deepEqual(
|
||||
getTexts(".o_datetime_picker_header", "strong"),
|
||||
expectedTitle,
|
||||
`title should be "${expectedTitle.join(" - ")}"`
|
||||
);
|
||||
} else {
|
||||
assert.containsNone(fixture, ".o_datetime_picker_header");
|
||||
}
|
||||
|
||||
// Time picker
|
||||
if (time) {
|
||||
assert.containsN(fixture, ".o_time_picker", time.length);
|
||||
const timePickers = select(".o_time_picker");
|
||||
for (let i = 0; i < time.length; i++) {
|
||||
const expectedTime = time[i];
|
||||
const values = select(timePickers[i], ".o_time_picker_select").map((sel) => sel.value);
|
||||
const actual = [...values.slice(0, 2).map(Number), ...values.slice(2)];
|
||||
assert.deepEqual(actual, expectedTime, `time values should be [${expectedTime}]`);
|
||||
}
|
||||
} else {
|
||||
assert.containsNone(fixture, ".o_time_picker");
|
||||
}
|
||||
|
||||
// Date picker
|
||||
const datePickerEls = select(".o_date_picker");
|
||||
assert.containsN(fixture, ".o_date_picker", date.length);
|
||||
|
||||
let selectedCells = 0;
|
||||
let outOfRangeCells = 0;
|
||||
let todayCells = 0;
|
||||
for (let i = 0; i < date.length; i++) {
|
||||
const { cells, daysOfWeek, weekNumbers } = date[i];
|
||||
const datePickerEl = datePickerEls[i];
|
||||
const cellEls = select(datePickerEl, ".o_date_item_cell");
|
||||
|
||||
assert.strictEqual(
|
||||
cellEls.length,
|
||||
PICKER_ROWS * PICKER_COLS,
|
||||
`picker should have ${
|
||||
PICKER_ROWS * PICKER_COLS
|
||||
} cells (${PICKER_ROWS} rows and ${PICKER_COLS} columns)`
|
||||
);
|
||||
|
||||
if (daysOfWeek) {
|
||||
const actualDow = getTexts(datePickerEl, ".o_day_of_week_cell");
|
||||
assert.deepEqual(
|
||||
actualDow,
|
||||
daysOfWeek,
|
||||
`picker should display the days of week: ${daysOfWeek
|
||||
.map((dow) => `"${dow}"`)
|
||||
.join(", ")}`
|
||||
);
|
||||
}
|
||||
|
||||
if (weekNumbers) {
|
||||
assert.deepEqual(
|
||||
getTexts(datePickerEl, ".o_week_number_cell").map(Number),
|
||||
weekNumbers,
|
||||
`picker should display the week numbers (${weekNumbers.join(", ")})`
|
||||
);
|
||||
}
|
||||
|
||||
// Date cells
|
||||
const expectedCells = cells.flatMap((row, rowIndex) =>
|
||||
row.map((cell, colIndex) => {
|
||||
const cellEl = cellEls[rowIndex * PICKER_COLS + colIndex];
|
||||
|
||||
// Check flags
|
||||
let value = cell;
|
||||
const isSelected = Array.isArray(cell);
|
||||
if (isSelected) {
|
||||
value = value[0];
|
||||
}
|
||||
const isToday = typeof value === "string";
|
||||
if (isToday) {
|
||||
value = Number(value);
|
||||
}
|
||||
const isOutOfRange = value < 0;
|
||||
if (isOutOfRange) {
|
||||
value = Math.abs(value);
|
||||
}
|
||||
|
||||
// Assert based on flags
|
||||
if (isSelected) {
|
||||
selectedCells++;
|
||||
assert.hasClass(cellEl, "o_selected");
|
||||
}
|
||||
if (isOutOfRange) {
|
||||
outOfRangeCells++;
|
||||
assert.hasClass(cellEl, "o_out_of_range");
|
||||
}
|
||||
if (isToday) {
|
||||
todayCells++;
|
||||
assert.hasClass(cellEl, "o_today");
|
||||
}
|
||||
|
||||
return value;
|
||||
})
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
cellEls.map((cell) => Number(getTexts(cell)[0])),
|
||||
expectedCells,
|
||||
`cell content should match the expected values: [${expectedCells.join(", ")}]`
|
||||
);
|
||||
}
|
||||
|
||||
assert.containsN(fixture, ".o_selected", selectedCells);
|
||||
assert.containsN(fixture, ".o_out_of_range", outOfRangeCells);
|
||||
assert.containsN(fixture, ".o_today", todayCells);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {RegExp | string} expr
|
||||
*/
|
||||
export function getPickerCell(expr) {
|
||||
const regex = expr instanceof RegExp ? expr : new RegExp(`^${expr}$`, "i");
|
||||
const cells = select(".o_datetime_picker .o_date_item_cell").filter((cell) =>
|
||||
regex.test(getTexts(cell)[0])
|
||||
);
|
||||
return cells.length === 1 ? cells[0] : cells;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {...(string | HTMLElement)} selectors
|
||||
* @returns {string[]}
|
||||
*/
|
||||
export function getTexts(...selectors) {
|
||||
return select(...selectors).map((e) => e.innerText.trim().replace(/\s+/g, " "));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {...(string | HTMLElement)} selectors
|
||||
* @returns {HTMLElement[]}
|
||||
*/
|
||||
const select = (...selectors) => {
|
||||
const root = selectors[0] instanceof Element ? selectors.shift() : getFixture();
|
||||
return selectors.length ? [...root.querySelectorAll(selectors.join(" "))] : [root];
|
||||
};
|
||||
|
||||
export function useTwelveHourClockFormat() {
|
||||
const { dateFormat = "dd/MM/yyyy", timeFormat = "HH:mm:ss" } = localization;
|
||||
const twcTimeFormat = `${timeFormat.replace(/H/g, "h")} a`;
|
||||
patchWithCleanup(localization, {
|
||||
dateTimeFormat: `${dateFormat} ${twcTimeFormat}`,
|
||||
timeFormat: twcTimeFormat,
|
||||
});
|
||||
}
|
||||
|
||||
export function zoomOut() {
|
||||
return click(getFixture(), ".o_zoom_out");
|
||||
}
|
||||
|
||||
const PICKER_ROWS = 6;
|
||||
const PICKER_COLS = 7;
|
||||
|
|
@ -1,326 +0,0 @@
|
|||
odoo.define('web.dialog_tests', function (require) {
|
||||
"use strict";
|
||||
|
||||
var Dialog = require('web.Dialog');
|
||||
var testUtils = require('web.test_utils');
|
||||
var Widget = require('web.Widget');
|
||||
|
||||
var ESCAPE_KEY = $.Event("keyup", { which: 27 });
|
||||
|
||||
async function createEmptyParent(debug) {
|
||||
var widget = new Widget();
|
||||
|
||||
await testUtils.mock.addMockEnvironment(widget, {
|
||||
debug: debug || false,
|
||||
});
|
||||
return widget;
|
||||
}
|
||||
|
||||
QUnit.module('core', {}, function () {
|
||||
|
||||
QUnit.module('Dialog');
|
||||
|
||||
QUnit.test("Closing custom dialog using buttons calls standard callback", async function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
var testPromise = testUtils.makeTestPromiseWithAssert(assert, 'custom callback');
|
||||
var parent = await createEmptyParent();
|
||||
new Dialog(parent, {
|
||||
buttons: [
|
||||
{
|
||||
text: "Close",
|
||||
classes: 'btn-primary',
|
||||
close: true,
|
||||
click: testPromise.resolve,
|
||||
},
|
||||
],
|
||||
$content: $('<main/>'),
|
||||
onForceClose: testPromise.reject,
|
||||
}).open();
|
||||
|
||||
assert.verifySteps([]);
|
||||
|
||||
await testUtils.nextTick();
|
||||
await testUtils.dom.click($('.modal[role="dialog"] .btn-primary'));
|
||||
|
||||
testPromise.then(() => {
|
||||
assert.verifySteps(['ok custom callback']);
|
||||
});
|
||||
|
||||
parent.destroy();
|
||||
});
|
||||
|
||||
QUnit.test("Closing custom dialog without using buttons calls force close callback", async function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
var testPromise = testUtils.makeTestPromiseWithAssert(assert, 'custom callback');
|
||||
var parent = await createEmptyParent();
|
||||
new Dialog(parent, {
|
||||
buttons: [
|
||||
{
|
||||
text: "Close",
|
||||
classes: 'btn-primary',
|
||||
close: true,
|
||||
click: testPromise.reject,
|
||||
},
|
||||
],
|
||||
$content: $('<main/>'),
|
||||
onForceClose: testPromise.resolve,
|
||||
}).open();
|
||||
|
||||
assert.verifySteps([]);
|
||||
|
||||
await testUtils.nextTick();
|
||||
await testUtils.dom.triggerEvents($('.modal[role="dialog"]'), [ESCAPE_KEY]);
|
||||
|
||||
testPromise.then(() => {
|
||||
assert.verifySteps(['ok custom callback']);
|
||||
});
|
||||
|
||||
parent.destroy();
|
||||
});
|
||||
|
||||
QUnit.test("Closing confirm dialog without using buttons calls cancel callback", async function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
var testPromise = testUtils.makeTestPromiseWithAssert(assert, 'confirm callback');
|
||||
var parent = await createEmptyParent();
|
||||
var options = {
|
||||
confirm_callback: testPromise.reject,
|
||||
cancel_callback: testPromise.resolve,
|
||||
};
|
||||
Dialog.confirm(parent, "", options);
|
||||
|
||||
assert.verifySteps([]);
|
||||
|
||||
await testUtils.nextTick();
|
||||
await testUtils.dom.triggerEvents($('.modal[role="dialog"]'), [ESCAPE_KEY]);
|
||||
|
||||
testPromise.then(() => {
|
||||
assert.verifySteps(['ok confirm callback']);
|
||||
});
|
||||
|
||||
parent.destroy();
|
||||
});
|
||||
|
||||
QUnit.test("click twice on 'Ok' button of a confirm dialog", async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
var testPromise = testUtils.makeTestPromise();
|
||||
var parent = await createEmptyParent();
|
||||
var options = {
|
||||
confirm_callback: () => {
|
||||
assert.step("confirm");
|
||||
return testPromise;
|
||||
},
|
||||
};
|
||||
Dialog.confirm(parent, "", options);
|
||||
await testUtils.nextTick();
|
||||
|
||||
assert.verifySteps([]);
|
||||
|
||||
await testUtils.dom.click($('.modal[role="dialog"] .btn-primary'));
|
||||
await testUtils.dom.click($('.modal[role="dialog"] .btn-primary'));
|
||||
await testUtils.nextTick();
|
||||
assert.verifySteps(['confirm']);
|
||||
assert.ok($('.modal[role="dialog"]').hasClass('show'), "Should still be opened");
|
||||
testPromise.resolve();
|
||||
await testUtils.nextTick();
|
||||
assert.notOk($('.modal[role="dialog"]').hasClass('show'), "Should now be closed");
|
||||
|
||||
parent.destroy();
|
||||
});
|
||||
|
||||
QUnit.test("click on 'Cancel' and then 'Ok' in a confirm dialog", async function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
var parent = await createEmptyParent();
|
||||
var options = {
|
||||
confirm_callback: () => {
|
||||
throw new Error("should not be called");
|
||||
},
|
||||
cancel_callback: () => {
|
||||
assert.step("cancel");
|
||||
}
|
||||
};
|
||||
Dialog.confirm(parent, "", options);
|
||||
await testUtils.nextTick();
|
||||
|
||||
assert.verifySteps([]);
|
||||
|
||||
testUtils.dom.click($('.modal[role="dialog"] footer button:not(.btn-primary)'));
|
||||
testUtils.dom.click($('.modal[role="dialog"] footer .btn-primary'));
|
||||
assert.verifySteps(['cancel']);
|
||||
|
||||
parent.destroy();
|
||||
});
|
||||
|
||||
QUnit.test("click on 'Cancel' and then 'Ok' in a confirm dialog (no cancel callback)", async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
var parent = await createEmptyParent();
|
||||
var options = {
|
||||
confirm_callback: () => {
|
||||
throw new Error("should not be called");
|
||||
},
|
||||
// Cannot add a step in cancel_callback, that's the point of this
|
||||
// test, we'll rely on checking the Dialog is opened then closed
|
||||
// without a crash.
|
||||
};
|
||||
Dialog.confirm(parent, "", options);
|
||||
await testUtils.nextTick();
|
||||
|
||||
assert.ok($('.modal[role="dialog"]').hasClass('show'));
|
||||
testUtils.dom.click($('.modal[role="dialog"] footer button:not(.btn-primary)'));
|
||||
testUtils.dom.click($('.modal[role="dialog"] footer .btn-primary'));
|
||||
await testUtils.nextTick();
|
||||
assert.notOk($('.modal[role="dialog"]').hasClass('show'));
|
||||
|
||||
parent.destroy();
|
||||
});
|
||||
|
||||
QUnit.test("Confirm dialog callbacks properly handle rejections", async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
var parent = await createEmptyParent();
|
||||
var options = {
|
||||
confirm_callback: () => {
|
||||
assert.step("confirm");
|
||||
return Promise.reject();
|
||||
},
|
||||
cancel_callback: () => {
|
||||
assert.step("cancel");
|
||||
return $.Deferred().reject(); // Test jquery deferred too
|
||||
}
|
||||
};
|
||||
Dialog.confirm(parent, "", options);
|
||||
await testUtils.nextTick();
|
||||
|
||||
assert.verifySteps([]);
|
||||
testUtils.dom.click($('.modal[role="dialog"] footer button:not(.btn-primary)'));
|
||||
await testUtils.nextTick();
|
||||
testUtils.dom.click($('.modal[role="dialog"] footer .btn-primary'));
|
||||
await testUtils.nextTick();
|
||||
testUtils.dom.click($('.modal[role="dialog"] footer button:not(.btn-primary)'));
|
||||
assert.verifySteps(['cancel', 'confirm', 'cancel']);
|
||||
|
||||
parent.destroy();
|
||||
});
|
||||
|
||||
QUnit.test("Properly can rely on the this in confirm and cancel callbacks of confirm dialog", async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
let dialogInstance = null;
|
||||
var parent = await createEmptyParent();
|
||||
var options = {
|
||||
confirm_callback: function () {
|
||||
assert.equal(this, dialogInstance, "'this' is properly a reference to the dialog instance");
|
||||
return Promise.reject();
|
||||
},
|
||||
cancel_callback: function () {
|
||||
assert.equal(this, dialogInstance, "'this' is properly a reference to the dialog instance");
|
||||
return Promise.reject();
|
||||
}
|
||||
};
|
||||
dialogInstance = Dialog.confirm(parent, "", options);
|
||||
await testUtils.nextTick();
|
||||
|
||||
testUtils.dom.click($('.modal[role="dialog"] footer button:not(.btn-primary)'));
|
||||
await testUtils.nextTick();
|
||||
testUtils.dom.click($('.modal[role="dialog"] footer .btn-primary'));
|
||||
|
||||
parent.destroy();
|
||||
});
|
||||
|
||||
QUnit.test("Confirm dialog callbacks can return anything without crash", async function (assert) {
|
||||
assert.expect(3);
|
||||
// Note that this test could be removed in master if the related code
|
||||
// is reworked. This only prevents a stable fix to break this again by
|
||||
// relying on the fact what is returned by those callbacks are undefined
|
||||
// or promises.
|
||||
|
||||
var parent = await createEmptyParent();
|
||||
var options = {
|
||||
confirm_callback: () => {
|
||||
assert.step("confirm");
|
||||
return 5;
|
||||
},
|
||||
};
|
||||
Dialog.confirm(parent, "", options);
|
||||
await testUtils.nextTick();
|
||||
|
||||
assert.verifySteps([]);
|
||||
testUtils.dom.click($('.modal[role="dialog"] footer .btn-primary'));
|
||||
assert.verifySteps(['confirm']);
|
||||
|
||||
parent.destroy();
|
||||
});
|
||||
|
||||
QUnit.test("Closing alert dialog without using buttons calls confirm callback", async function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
var testPromise = testUtils.makeTestPromiseWithAssert(assert, 'alert callback');
|
||||
var parent = await createEmptyParent();
|
||||
var options = {
|
||||
confirm_callback: testPromise.resolve,
|
||||
};
|
||||
Dialog.alert(parent, "", options);
|
||||
|
||||
assert.verifySteps([]);
|
||||
|
||||
await testUtils.nextTick();
|
||||
await testUtils.dom.triggerEvents($('.modal[role="dialog"]'), [ESCAPE_KEY]);
|
||||
|
||||
testPromise.then(() => {
|
||||
assert.verifySteps(['ok alert callback']);
|
||||
});
|
||||
|
||||
parent.destroy();
|
||||
});
|
||||
|
||||
QUnit.test("Ensure on_attach_callback and on_detach_callback are properly called", async function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
const TestDialog = Dialog.extend({
|
||||
on_attach_callback() {
|
||||
assert.step('on_attach_callback');
|
||||
},
|
||||
on_detach_callback() {
|
||||
assert.step('on_detach_callback');
|
||||
},
|
||||
});
|
||||
|
||||
const parent = await createEmptyParent();
|
||||
const dialog = new TestDialog(parent, {
|
||||
buttons: [
|
||||
{
|
||||
text: "Close",
|
||||
classes: 'btn-primary',
|
||||
close: true,
|
||||
},
|
||||
],
|
||||
$content: $('<main/>'),
|
||||
}).open();
|
||||
|
||||
await dialog.opened();
|
||||
|
||||
assert.verifySteps(['on_attach_callback']);
|
||||
|
||||
await testUtils.dom.click($('.modal[role="dialog"] .btn-primary'));
|
||||
assert.verifySteps(['on_detach_callback']);
|
||||
|
||||
parent.destroy();
|
||||
});
|
||||
|
||||
QUnit.test("Should not be displayed if parent is destroyed while dialog is being opened", async function (assert) {
|
||||
assert.expect(1);
|
||||
const parent = await createEmptyParent();
|
||||
const dialog = new Dialog(parent);
|
||||
dialog.open();
|
||||
parent.destroy();
|
||||
await testUtils.nextTick();
|
||||
assert.containsNone(document.body, ".modal[role='dialog']");
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
|
@ -1,133 +0,0 @@
|
|||
odoo.define('web.dom_tests', function (require) {
|
||||
"use strict";
|
||||
|
||||
var dom = require('web.dom');
|
||||
var testUtils = require('web.test_utils');
|
||||
|
||||
/**
|
||||
* Create an autoresize text area with 'border-box' as box sizing rule.
|
||||
* The minimum height of this autoresize text are is 1px.
|
||||
*
|
||||
* @param {Object} [options={}]
|
||||
* @param {integer} [options.borderBottomWidth=0]
|
||||
* @param {integer} [options.borderTopWidth=0]
|
||||
* @param {integer} [options.padding=0]
|
||||
*/
|
||||
function prepareAutoresizeTextArea(options) {
|
||||
options = options || {};
|
||||
var $textarea = $('<textarea>');
|
||||
$textarea.css('box-sizing', 'border-box');
|
||||
$textarea.css({
|
||||
padding: options.padding || 0,
|
||||
borderTopWidth: options.borderTopWidth || 0,
|
||||
borderBottomWidth: options.borderBottomWidth || 0,
|
||||
});
|
||||
$textarea.appendTo($('#qunit-fixture'));
|
||||
dom.autoresize($textarea, { min_height: 1 });
|
||||
return $textarea;
|
||||
}
|
||||
|
||||
QUnit.module('core', {}, function () {
|
||||
QUnit.module('dom', {}, function () {
|
||||
|
||||
QUnit.module('autoresize', {
|
||||
afterEach: function () {
|
||||
$('#qunit-fixture').find('textarea').remove();
|
||||
},
|
||||
});
|
||||
|
||||
QUnit.test('autoresize (border-box): no padding + no border', function (assert) {
|
||||
assert.expect(3);
|
||||
var $textarea = prepareAutoresizeTextArea();
|
||||
assert.strictEqual($('textarea').length, 2,
|
||||
"there should be two textareas in the DOM");
|
||||
|
||||
$textarea = $('textarea:eq(0)');
|
||||
var $fixedTextarea = $('textarea:eq(1)');
|
||||
assert.strictEqual($textarea.css('height'),
|
||||
$fixedTextarea[0].scrollHeight + 'px',
|
||||
"autoresized textarea should have height of fixed textarea + padding (0 line)");
|
||||
|
||||
testUtils.fields.editInput($textarea, 'a\nb\nc\nd');
|
||||
assert.strictEqual($textarea.css('height'),
|
||||
$fixedTextarea[0].scrollHeight + 'px',
|
||||
"autoresized textarea should have height of fixed textarea + padding (4 lines)");
|
||||
});
|
||||
|
||||
QUnit.test('autoresize (border-box): padding + no border', function (assert) {
|
||||
assert.expect(3);
|
||||
var $textarea = prepareAutoresizeTextArea({ padding: 10 });
|
||||
assert.strictEqual($('textarea').length, 2,
|
||||
"there should be two textareas in the DOM");
|
||||
|
||||
$textarea = $('textarea:eq(0)');
|
||||
var $fixedTextarea = $('textarea:eq(1)');
|
||||
// twice the padding of 10px
|
||||
var expectedTextAreaHeight = $fixedTextarea[0].scrollHeight + 2*10;
|
||||
assert.strictEqual($textarea.css('height'),
|
||||
expectedTextAreaHeight + 'px',
|
||||
"autoresized textarea should have height of fixed textarea + padding (0 line)");
|
||||
|
||||
testUtils.fields.editInput($textarea, 'a\nb\nc\nd');
|
||||
// twice the padding of 10px
|
||||
expectedTextAreaHeight = $fixedTextarea[0].scrollHeight + 2*10;
|
||||
assert.strictEqual($textarea.css('height'),
|
||||
expectedTextAreaHeight + 'px',
|
||||
"autoresized textarea should have height of fixed textarea + padding (4 lines)");
|
||||
});
|
||||
|
||||
QUnit.test('autoresize (border-box): no padding + border', function (assert) {
|
||||
assert.expect(3);
|
||||
var $textarea = prepareAutoresizeTextArea({
|
||||
borderTopWidth: 2,
|
||||
borderBottomWidth: 3,
|
||||
});
|
||||
assert.strictEqual($('textarea').length, 2,
|
||||
"there should be two textareas in the DOM");
|
||||
|
||||
$textarea = $('textarea:eq(0)');
|
||||
var $fixedTextarea = $('textarea:eq(1)');
|
||||
// top (2px) + bottom (3px) borders
|
||||
var expectedTextAreaHeight = $fixedTextarea[0].scrollHeight + (2 + 3);
|
||||
assert.strictEqual($textarea.css('height'),
|
||||
expectedTextAreaHeight + 'px',
|
||||
"autoresized textarea should have height of fixed textarea + border (0 line)");
|
||||
|
||||
testUtils.fields.editInput($textarea, 'a\nb\nc\nd');
|
||||
// top (2px) + bottom (3px) borders
|
||||
expectedTextAreaHeight = $fixedTextarea[0].scrollHeight + (2 + 3);
|
||||
assert.strictEqual($textarea.css('height'),
|
||||
expectedTextAreaHeight + 'px',
|
||||
"autoresized textarea should have height of fixed textarea + border (4 lines)");
|
||||
});
|
||||
|
||||
QUnit.test('autoresize (border-box): padding + border', function (assert) {
|
||||
assert.expect(3);
|
||||
var $textarea = prepareAutoresizeTextArea({
|
||||
padding: 10,
|
||||
borderTopWidth: 2,
|
||||
borderBottomWidth: 3,
|
||||
});
|
||||
assert.strictEqual($('textarea').length, 2,
|
||||
"there should be two textareas in the DOM");
|
||||
|
||||
$textarea = $('textarea:eq(0)');
|
||||
var $fixedTextarea = $('textarea:eq(1)');
|
||||
// twice padding (10px) + top (2px) + bottom (3px) borders
|
||||
var expectedTextAreaHeight = $fixedTextarea[0].scrollHeight + (2*10 + 2 + 3);
|
||||
assert.strictEqual($textarea.css('height'),
|
||||
expectedTextAreaHeight + 'px',
|
||||
"autoresized textarea should have height of fixed textarea + border (0 line)");
|
||||
|
||||
testUtils.fields.editInput($textarea, 'a\nb\nc\nd');
|
||||
// twice padding (10px) + top (2px) + bottom (3px) borders
|
||||
expectedTextAreaHeight = $fixedTextarea[0].scrollHeight + (2*10 + 2 + 3);
|
||||
assert.strictEqual($textarea.css('height'),
|
||||
expectedTextAreaHeight + 'px',
|
||||
"autoresized textarea should have height of fixed textarea + border (4 lines)");
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
|
@ -1,199 +0,0 @@
|
|||
odoo.define('web.domain_tests', function (require) {
|
||||
"use strict";
|
||||
|
||||
var Domain = require('web.Domain');
|
||||
|
||||
QUnit.module('core', {}, function () {
|
||||
|
||||
QUnit.module('domain');
|
||||
|
||||
QUnit.test("empty", function (assert) {
|
||||
assert.expect(1);
|
||||
assert.ok(new Domain([]).compute({}));
|
||||
});
|
||||
|
||||
QUnit.test("basic", function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
var fields = {
|
||||
a: 3,
|
||||
group_method: 'line',
|
||||
select1: 'day',
|
||||
rrule_type: 'monthly',
|
||||
};
|
||||
assert.ok(new Domain([['a', '=', 3]]).compute(fields));
|
||||
assert.ok(new Domain([['group_method','!=','count']]).compute(fields));
|
||||
assert.ok(new Domain([['select1','=','day'], ['rrule_type','=','monthly']]).compute(fields));
|
||||
});
|
||||
|
||||
QUnit.test("or", function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
var web = {
|
||||
section_id: null,
|
||||
user_id: null,
|
||||
member_ids: null,
|
||||
};
|
||||
var currentDomain = [
|
||||
'|',
|
||||
['section_id', '=', 42],
|
||||
'|',
|
||||
['user_id', '=', 3],
|
||||
['member_ids', 'in', [3]]
|
||||
];
|
||||
assert.ok(new Domain(currentDomain).compute(_.extend({}, web, {section_id: 42})));
|
||||
assert.ok(new Domain(currentDomain).compute(_.extend({}, web, {user_id: 3})));
|
||||
assert.ok(new Domain(currentDomain).compute(_.extend({}, web, {member_ids: 3})));
|
||||
});
|
||||
|
||||
QUnit.test("not", function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
var fields = {
|
||||
a: 5,
|
||||
group_method: 'line',
|
||||
};
|
||||
assert.ok(new Domain(['!', ['a', '=', 3]]).compute(fields));
|
||||
assert.ok(new Domain(['!', ['group_method','=','count']]).compute(fields));
|
||||
});
|
||||
|
||||
QUnit.test("domains initialized with a number", function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
assert.ok(new Domain(1).compute({}));
|
||||
assert.notOk(new Domain(0).compute({}));
|
||||
});
|
||||
|
||||
QUnit.test("invalid domains should not succeed", function (assert) {
|
||||
assert.expect(3);
|
||||
assert.throws(
|
||||
() => new Domain(['|', ['hr_presence_state', '=', 'absent']]),
|
||||
/invalid domain .* \(missing 1 segment/
|
||||
);
|
||||
assert.throws(
|
||||
() => new Domain(['|', '|', ['hr_presence_state', '=', 'absent'], ['attendance_state', '=', 'checked_in']]),
|
||||
/invalid domain .* \(missing 1 segment/
|
||||
);
|
||||
assert.throws(
|
||||
() => new Domain(['&', ['composition_mode', '!=', 'mass_post']]),
|
||||
/invalid domain .* \(missing 1 segment/
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("domain <=> condition", function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
var domain = [
|
||||
'|',
|
||||
'|',
|
||||
'|',
|
||||
'&', ['doc.amount', '>', 33], ['doc.toto', '!=', null],
|
||||
'&', ['doc.bidule.active', '=', true], ['truc', 'in', [2, 3]],
|
||||
['gogo', '=', 'gogo value'],
|
||||
['gogo', '!=', false]
|
||||
];
|
||||
var condition = '((doc.amount > 33 and doc.toto is not None or doc.bidule.active is True and truc in [2,3]) or gogo == "gogo value") or gogo';
|
||||
|
||||
assert.equal(Domain.prototype.domainToCondition(domain), condition);
|
||||
assert.deepEqual(Domain.prototype.conditionToDomain(condition), domain);
|
||||
assert.deepEqual(Domain.prototype.conditionToDomain(
|
||||
'doc and toto is None or not tata'),
|
||||
['|', '&', ['doc', '!=', false], ['toto', '=', null], ['tata', '=', false]]);
|
||||
assert.deepEqual(Domain.prototype.conditionToDomain(
|
||||
`field in ("foo", "bar") and display_name in ['boo','far']`),
|
||||
['&', ['field', 'in', ['foo', 'bar']], ['display_name', 'in', ['boo', 'far']]]);
|
||||
});
|
||||
|
||||
QUnit.test("condition 'a field is set' does not convert to a domain", function (assert) {
|
||||
assert.expect(1);
|
||||
var expected = [["doc.blabla","!=",false]];
|
||||
var condition = "doc.blabla";
|
||||
|
||||
var actual = Domain.prototype.conditionToDomain(condition);
|
||||
|
||||
assert.deepEqual(actual, expected);
|
||||
});
|
||||
|
||||
QUnit.test("condition with a function should fail", function (assert) {
|
||||
assert.expect(1);
|
||||
var condition = "doc.blabla()";
|
||||
|
||||
assert.throws(function() { Domain.prototype.conditionToDomain(condition); });
|
||||
});
|
||||
|
||||
QUnit.test("empty condition should not fail", function (assert) {
|
||||
assert.expect(2);
|
||||
var condition = "";
|
||||
var actual = Domain.prototype.conditionToDomain(condition);
|
||||
assert.strictEqual(typeof(actual),typeof([]));
|
||||
assert.strictEqual(actual.length, 0);
|
||||
});
|
||||
QUnit.test("undefined condition should not fail", function (assert) {
|
||||
assert.expect(2);
|
||||
var condition = undefined;
|
||||
var actual = Domain.prototype.conditionToDomain(condition);
|
||||
assert.strictEqual(typeof(actual),typeof([]));
|
||||
assert.strictEqual(actual.length, 0);
|
||||
});
|
||||
|
||||
QUnit.test("compute true domain", function (assert) {
|
||||
assert.expect(1);
|
||||
assert.ok(new Domain(Domain.TRUE_DOMAIN).compute({}));
|
||||
});
|
||||
|
||||
QUnit.test("compute false domain", function (assert) {
|
||||
assert.expect(1);
|
||||
assert.notOk(new Domain(Domain.FALSE_DOMAIN).compute({}));
|
||||
});
|
||||
|
||||
QUnit.test("arrayToString", function (assert) {
|
||||
assert.expect(14);
|
||||
|
||||
const arrayToString = Domain.prototype.arrayToString;
|
||||
|
||||
// domains containing null, false or true
|
||||
assert.strictEqual(arrayToString([['name', '=', null]]), '[["name","=",None]]');
|
||||
assert.strictEqual(arrayToString([['name', '=', false]]), '[["name","=",False]]');
|
||||
assert.strictEqual(arrayToString([['name', '=', true]]), '[["name","=",True]]');
|
||||
assert.strictEqual(arrayToString([['name', '=', 'null']]), '[["name","=","null"]]');
|
||||
assert.strictEqual(arrayToString([['name', '=', 'false']]), '[["name","=","false"]]');
|
||||
assert.strictEqual(arrayToString([['name', '=', 'true']]), '[["name","=","true"]]');
|
||||
assert.strictEqual(arrayToString([['name', 'in', [true, false]]]), '[["name","in",[True,False]]]');
|
||||
assert.strictEqual(arrayToString([['name', 'in', [null]]]), '[["name","in",[None]]]');
|
||||
|
||||
assert.strictEqual(arrayToString([['name', 'in', ["foo", "bar"]]]), '[["name","in",["foo","bar"]]]');
|
||||
assert.strictEqual(arrayToString([['name', 'in', [1, 2]]]), '[["name","in",[1,2]]]');
|
||||
assert.strictEqual(arrayToString(), '[]');
|
||||
|
||||
assert.strictEqual(arrayToString(['&', ['name', '=', 'foo'], ['type', '=', 'bar']]), '["&",["name","=","foo"],["type","=","bar"]]');
|
||||
assert.strictEqual(arrayToString(['|', ['name', '=', 'foo'], ['type', '=', 'bar']]), '["|",["name","=","foo"],["type","=","bar"]]');
|
||||
|
||||
// string domains are not processed
|
||||
assert.strictEqual(arrayToString('[["name", "ilike", "foo"]]'), '[["name", "ilike", "foo"]]');
|
||||
});
|
||||
|
||||
QUnit.test("like, =like, ilike and =ilike", function (assert) {
|
||||
assert.expect(16);
|
||||
|
||||
assert.ok(new Domain([['a', 'like', 'value']]).compute({ a: 'value' }));
|
||||
assert.ok(new Domain([['a', 'like', 'value']]).compute({ a: 'some value' }));
|
||||
assert.notOk(new Domain([['a', 'like', 'value']]).compute({ a: 'Some Value' }));
|
||||
assert.notOk(new Domain([['a', 'like', 'value']]).compute({ a: false }));
|
||||
|
||||
assert.ok(new Domain([['a', '=like', '%value']]).compute({ a: 'value' }));
|
||||
assert.ok(new Domain([['a', '=like', '%value']]).compute({ a: 'some value' }));
|
||||
assert.notOk(new Domain([['a', '=like', '%value']]).compute({ a: 'Some Value' }));
|
||||
assert.notOk(new Domain([['a', '=like', '%value']]).compute({ a: false }));
|
||||
|
||||
assert.ok(new Domain([['a', 'ilike', 'value']]).compute({ a: 'value' }));
|
||||
assert.ok(new Domain([['a', 'ilike', 'value']]).compute({ a: 'some value' }));
|
||||
assert.ok(new Domain([['a', 'ilike', 'value']]).compute({ a: 'Some Value' }));
|
||||
assert.notOk(new Domain([['a', 'ilike', 'value']]).compute({ a: false }));
|
||||
|
||||
assert.ok(new Domain([['a', '=ilike', '%value']]).compute({ a: 'value' }));
|
||||
assert.ok(new Domain([['a', '=ilike', '%value']]).compute({ a: 'some value' }));
|
||||
assert.ok(new Domain([['a', '=ilike', '%value']]).compute({ a: 'Some Value' }));
|
||||
assert.notOk(new Domain([['a', '=ilike', '%value']]).compute({ a: false }));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,78 +0,0 @@
|
|||
odoo.define('web.mixins_tests', function (require) {
|
||||
"use strict";
|
||||
|
||||
const AbstractAction = require("web.AbstractAction");
|
||||
const core = require("web.core");
|
||||
var testUtils = require('web.test_utils');
|
||||
var Widget = require('web.Widget');
|
||||
|
||||
const { dialogService } = require('@web/core/dialog/dialog_service');
|
||||
const { errorService } = require("@web/core/errors/error_service");
|
||||
const { registry } = require('@web/core/registry');
|
||||
const { getFixture, nextTick, patchWithCleanup } = require('@web/../tests/helpers/utils');
|
||||
const { createWebClient, doAction } = require('@web/../tests/webclient/helpers');
|
||||
|
||||
QUnit.module('core', {}, function () {
|
||||
|
||||
QUnit.module('mixins');
|
||||
|
||||
QUnit.test('perform a do_action properly', function (assert) {
|
||||
assert.expect(3);
|
||||
var done = assert.async();
|
||||
|
||||
var widget = new Widget();
|
||||
|
||||
testUtils.mock.intercept(widget, 'do_action', function (event) {
|
||||
assert.strictEqual(event.data.action, 'test.some_action_id',
|
||||
"should have sent proper action name");
|
||||
assert.deepEqual(event.data.options, {clear_breadcrumbs: true},
|
||||
"should have sent proper options");
|
||||
event.data.on_success();
|
||||
});
|
||||
|
||||
widget.do_action('test.some_action_id', {clear_breadcrumbs: true}).then(function () {
|
||||
assert.ok(true, 'deferred should have been resolved');
|
||||
widget.destroy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test('checks that the error generated by a do_action opens one dialog', async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
window.addEventListener("unhandledrejection", async (ev) => {
|
||||
ev.preventDefault();
|
||||
});
|
||||
patchWithCleanup(QUnit, {
|
||||
onUnhandledRejection: () => {},
|
||||
});
|
||||
|
||||
const serviceRegistry = registry.category("services");
|
||||
serviceRegistry.add("dialog", dialogService);
|
||||
serviceRegistry.add("error", errorService);
|
||||
|
||||
const TestAction = AbstractAction.extend({
|
||||
on_attach_callback() {
|
||||
this.do_action({
|
||||
id: 1,
|
||||
type: "ir.actions.server",
|
||||
})
|
||||
},
|
||||
});
|
||||
core.action_registry.add("TestAction", TestAction);
|
||||
|
||||
const mockRPC = (route) => {
|
||||
if (route === "/web/action/run") {
|
||||
throw new Error("This error should be throw only once");
|
||||
}
|
||||
};
|
||||
const target = getFixture();
|
||||
const webClient = await createWebClient({ mockRPC});
|
||||
await doAction(webClient, "TestAction");
|
||||
await nextTick();
|
||||
assert.containsOnce(target, ".o_dialog");
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
|
@ -1,416 +0,0 @@
|
|||
odoo.define('web.owl_dialog_tests', function (require) {
|
||||
"use strict";
|
||||
|
||||
const LegacyDialog = require('web.Dialog');
|
||||
const makeTestEnvironment = require('web.test_env');
|
||||
const Dialog = require('web.OwlDialog');
|
||||
const testUtils = require('web.test_utils');
|
||||
|
||||
const { makeLegacyDialogMappingTestEnv } = require('@web/../tests/helpers/legacy_env_utils');
|
||||
const { Dialog: WowlDialog } = require("@web/core/dialog/dialog");
|
||||
const { WithEnv } = require("@web/core/utils/components");
|
||||
const {
|
||||
getFixture,
|
||||
nextTick,
|
||||
mount,
|
||||
destroy,
|
||||
} = require("@web/../tests/helpers/utils");
|
||||
const { LegacyComponent } = require("@web/legacy/legacy_component");
|
||||
|
||||
const { Component, useState, xml } = owl;
|
||||
const EscapeKey = { key: 'Escape', keyCode: 27, which: 27 };
|
||||
|
||||
QUnit.module('core', {}, function () {
|
||||
QUnit.module('OwlDialog');
|
||||
|
||||
QUnit.test("Rendering of all props", async function (assert) {
|
||||
assert.expect(36);
|
||||
|
||||
class SubComponent extends LegacyComponent {
|
||||
// Handlers
|
||||
_onClick() {
|
||||
assert.step('subcomponent_clicked');
|
||||
}
|
||||
}
|
||||
SubComponent.template = xml`<div class="o_subcomponent" t-esc="props.text" t-on-click="_onClick"/>`;
|
||||
|
||||
class Parent extends LegacyComponent {
|
||||
setup() {
|
||||
this.state = useState({ textContent: "sup" });
|
||||
}
|
||||
// Handlers
|
||||
_onButtonClicked() {
|
||||
assert.step('button_clicked');
|
||||
}
|
||||
_onDialogClosed() {
|
||||
assert.step('dialog_closed');
|
||||
}
|
||||
}
|
||||
Parent.components = { Dialog, SubComponent };
|
||||
Parent.template = xml`
|
||||
<Dialog
|
||||
backdrop="state.backdrop"
|
||||
contentClass="state.contentClass"
|
||||
fullscreen="state.fullscreen"
|
||||
renderFooter="state.renderFooter"
|
||||
renderHeader="state.renderHeader"
|
||||
size="state.size"
|
||||
subtitle="state.subtitle"
|
||||
technical="state.technical"
|
||||
title="state.title"
|
||||
onClosed="_onDialogClosed"
|
||||
>
|
||||
<SubComponent text="state.textContent"/>
|
||||
<t t-set-slot="buttons">
|
||||
<button class="btn btn-primary" t-on-click="_onButtonClicked">The Button</button>
|
||||
</t>
|
||||
</Dialog>`;
|
||||
|
||||
const parent = await mount(Parent, getFixture(), {
|
||||
env: makeTestEnvironment(),
|
||||
});
|
||||
const dialog = document.querySelector('.o_dialog');
|
||||
|
||||
// Helper function
|
||||
async function changeProps(key, value) {
|
||||
parent.state[key] = value;
|
||||
await testUtils.nextTick();
|
||||
}
|
||||
|
||||
// Basic layout with default properties
|
||||
assert.containsOnce(dialog, '.modal.o_technical_modal');
|
||||
assert.hasClass(dialog.querySelector('.modal .modal-dialog'), 'modal-lg');
|
||||
assert.containsOnce(dialog, '.modal-header > button.btn-close');
|
||||
assert.containsOnce(dialog, '.modal-footer > button.btn.btn-primary');
|
||||
assert.strictEqual(dialog.querySelector('.modal-body').innerText.trim(), "sup",
|
||||
"Subcomponent should match with its given text");
|
||||
|
||||
// Backdrop (default: 'static')
|
||||
// Static backdrop click should focus first button
|
||||
// => we need to reset that property
|
||||
dialog.querySelector('.btn-primary').blur(); // Remove the focus explicitely
|
||||
assert.containsNone(document.body, '.modal-backdrop'); // No backdrop *element* for Odoo modal...
|
||||
assert.notEqual(window.getComputedStyle(dialog.querySelector('.modal')).backgroundColor, 'rgba(0, 0, 0, 0)'); // ... but a non transparent modal
|
||||
await testUtils.dom.click(dialog.querySelector('.modal'));
|
||||
assert.strictEqual(document.activeElement, dialog.querySelector('.btn-primary'),
|
||||
"Button should be focused when clicking on backdrop");
|
||||
assert.verifySteps([]); // Ensure not closed
|
||||
dialog.querySelector('.btn-primary').blur(); // Remove the focus explicitely
|
||||
|
||||
await changeProps('backdrop', false);
|
||||
assert.containsNone(document.body, '.modal-backdrop'); // No backdrop *element* for Odoo modal...
|
||||
assert.strictEqual(window.getComputedStyle(dialog.querySelector('.modal')).backgroundColor, 'rgba(0, 0, 0, 0)');
|
||||
await testUtils.dom.click(dialog.querySelector('.modal'));
|
||||
assert.notEqual(document.activeElement, dialog.querySelector('.btn-primary'),
|
||||
"Button should not be focused when clicking on backdrop 'false'");
|
||||
assert.verifySteps([]); // Ensure not closed
|
||||
|
||||
await changeProps('backdrop', true);
|
||||
assert.containsNone(document.body, '.modal-backdrop'); // No backdrop *element* for Odoo modal...
|
||||
assert.notEqual(window.getComputedStyle(dialog.querySelector('.modal')).backgroundColor, 'rgba(0, 0, 0, 0)'); // ... but a non transparent modal
|
||||
await testUtils.dom.click(dialog.querySelector('.modal'));
|
||||
assert.notEqual(document.activeElement, dialog.querySelector('.btn-primary'),
|
||||
"Button should not be focused when clicking on backdrop 'true'");
|
||||
assert.verifySteps(['dialog_closed']);
|
||||
|
||||
// Dialog class (default: '')
|
||||
await changeProps('contentClass', 'my_dialog_class');
|
||||
assert.hasClass(dialog.querySelector('.modal-content'), 'my_dialog_class');
|
||||
|
||||
// Full screen (default: false)
|
||||
assert.doesNotHaveClass(dialog.querySelector('.modal'), 'o_modal_full');
|
||||
await changeProps('fullscreen', true);
|
||||
assert.hasClass(dialog.querySelector('.modal'), 'o_modal_full');
|
||||
|
||||
// Size class (default: 'large')
|
||||
await changeProps('size', 'extra-large');
|
||||
assert.strictEqual(dialog.querySelector('.modal-dialog').className, 'modal-dialog modal-xl',
|
||||
"Modal should have taken the class modal-xl");
|
||||
await changeProps('size', 'medium');
|
||||
assert.strictEqual(dialog.querySelector('.modal-dialog').className, 'modal-dialog',
|
||||
"Modal should not have any additionnal class with 'medium'");
|
||||
await changeProps('size', 'small');
|
||||
assert.strictEqual(dialog.querySelector('.modal-dialog').className, 'modal-dialog modal-sm',
|
||||
"Modal should have taken the class modal-sm");
|
||||
|
||||
// Subtitle (default: '')
|
||||
await changeProps('subtitle', "The Subtitle");
|
||||
assert.strictEqual(dialog.querySelector('span.o_subtitle').innerText.trim(), "The Subtitle",
|
||||
"Subtitle should match with its given text");
|
||||
|
||||
// Technical (default: true)
|
||||
assert.hasClass(dialog.querySelector('.modal'), 'o_technical_modal');
|
||||
await changeProps('technical', false);
|
||||
assert.doesNotHaveClass(dialog.querySelector('.modal'), 'o_technical_modal');
|
||||
|
||||
// Title (default: 'Odoo')
|
||||
assert.strictEqual(dialog.querySelector('h4.modal-title').innerText.trim(), "Odoo" + "The Subtitle",
|
||||
"Title should match with its default text");
|
||||
await changeProps('title', "The Title");
|
||||
assert.strictEqual(dialog.querySelector('h4.modal-title').innerText.trim(), "The Title" + "The Subtitle",
|
||||
"Title should match with its given text");
|
||||
|
||||
// Reactivity of buttons
|
||||
await testUtils.dom.click(dialog.querySelector('.modal-footer .btn-primary'));
|
||||
assert.verifySteps(["button_clicked"]);
|
||||
|
||||
// Render footer (default: true)
|
||||
await changeProps('renderFooter', false);
|
||||
assert.containsNone(dialog, '.modal-footer');
|
||||
|
||||
// Render header (default: true)
|
||||
await changeProps('renderHeader', false);
|
||||
assert.containsNone(dialog, '.header');
|
||||
|
||||
// Reactivity of subcomponents
|
||||
await changeProps('textContent', "wassup");
|
||||
assert.strictEqual(dialog.querySelector('.o_subcomponent').innerText.trim(), "wassup",
|
||||
"Subcomponent should match with its given text");
|
||||
await testUtils.dom.click(dialog.querySelector('.o_subcomponent'));
|
||||
|
||||
assert.verifySteps(["subcomponent_clicked"]);
|
||||
});
|
||||
|
||||
QUnit.test("Interactions between multiple dialogs", async function (assert) {
|
||||
assert.expect(23);
|
||||
|
||||
const { legacyEnv } = await makeLegacyDialogMappingTestEnv();
|
||||
class Parent extends LegacyComponent {
|
||||
setup() {
|
||||
this.dialogIds = useState([]);
|
||||
}
|
||||
// Handlers
|
||||
_onDialogClosed(id) {
|
||||
assert.step(`dialog_${id}_closed`);
|
||||
this.dialogIds.splice(this.dialogIds.findIndex(d => d === id), 1);
|
||||
}
|
||||
}
|
||||
Parent.components = { Dialog };
|
||||
Parent.template = xml`
|
||||
<div>
|
||||
<Dialog t-foreach="dialogIds" t-as="dialogId" t-key="dialogId"
|
||||
contentClass="'dialog_' + dialogId" onClosed="() => this._onDialogClosed(dialogId)"
|
||||
/>
|
||||
</div>`;
|
||||
|
||||
const parent = await mount(Parent, getFixture(), {
|
||||
env: legacyEnv,
|
||||
});
|
||||
|
||||
// Dialog 1 : Owl
|
||||
parent.dialogIds.push(1);
|
||||
await testUtils.nextTick();
|
||||
// Dialog 2 : Legacy
|
||||
new LegacyDialog(null, {}).open();
|
||||
await testUtils.nextTick();
|
||||
// Dialog 3 : Legacy
|
||||
new LegacyDialog(null, {}).open();
|
||||
await testUtils.nextTick();
|
||||
// Dialog 4 : Owl
|
||||
parent.dialogIds.push(4);
|
||||
await testUtils.nextTick();
|
||||
// Dialog 5 : Owl
|
||||
parent.dialogIds.push(5);
|
||||
await testUtils.nextTick();
|
||||
// Dialog 6 : Legacy (unopened)
|
||||
const unopenedModal = new LegacyDialog(null, {});
|
||||
await testUtils.nextTick();
|
||||
|
||||
// Manually closes the last legacy dialog. Should not affect the other
|
||||
// existing dialogs (3 owl and 2 legacy).
|
||||
unopenedModal.close();
|
||||
assert.containsN(document.body, ".modal", 5);
|
||||
|
||||
let modals = document.querySelectorAll('.modal');
|
||||
assert.notOk(modals[modals.length - 1].classList.contains('o_inactive_modal'),
|
||||
"last dialog should have the active class");
|
||||
assert.notOk(modals[modals.length - 1].classList.contains('o_legacy_dialog'),
|
||||
"active dialog should not have the legacy class");
|
||||
assert.containsN(document.body, '.o_dialog', 3);
|
||||
assert.containsN(document.body, '.o_legacy_dialog', 2);
|
||||
|
||||
// Reactivity with owl dialogs
|
||||
await testUtils.dom.triggerEvent(modals[modals.length - 1], 'keydown', EscapeKey); // Press Escape
|
||||
|
||||
modals = document.querySelectorAll('.modal');
|
||||
assert.notOk(modals[modals.length - 1].classList.contains('o_inactive_modal'),
|
||||
"last dialog should have the active class");
|
||||
assert.notOk(modals[modals.length - 1].classList.contains('o_legacy_dialog'),
|
||||
"active dialog should not have the legacy class");
|
||||
assert.containsN(document.body, '.o_dialog', 2);
|
||||
assert.containsN(document.body, '.o_legacy_dialog', 2);
|
||||
|
||||
await testUtils.dom.click(modals[modals.length - 1].querySelector('.btn.btn-primary')); // Click on 'Ok' button
|
||||
|
||||
modals = document.querySelectorAll('.modal');
|
||||
assert.containsOnce(document.body, '.modal.o_legacy_dialog:not(.o_inactive_modal)',
|
||||
"active dialog should have the legacy class");
|
||||
assert.containsOnce(document.body, '.o_dialog');
|
||||
assert.containsN(document.body, '.o_legacy_dialog', 2);
|
||||
|
||||
// Reactivity with legacy dialogs
|
||||
await testUtils.dom.triggerEvent(modals[modals.length - 1], 'keydown', EscapeKey);
|
||||
|
||||
modals = document.querySelectorAll('.modal');
|
||||
assert.containsOnce(document.body, '.modal.o_legacy_dialog:not(.o_inactive_modal)',
|
||||
"active dialog should have the legacy class");
|
||||
assert.containsOnce(document.body, '.o_dialog');
|
||||
assert.containsOnce(document.body, '.o_legacy_dialog');
|
||||
|
||||
await testUtils.dom.click(modals[modals.length - 1].querySelector('.btn-close'));
|
||||
|
||||
modals = document.querySelectorAll('.modal');
|
||||
assert.notOk(modals[modals.length - 1].classList.contains('o_inactive_modal'),
|
||||
"last dialog should have the active class");
|
||||
assert.notOk(modals[modals.length - 1].classList.contains('o_legacy_dialog'),
|
||||
"active dialog should not have the legacy class");
|
||||
assert.containsOnce(document.body, '.o_dialog');
|
||||
assert.containsNone(document.body, '.o_legacy_dialog');
|
||||
|
||||
destroy(parent);
|
||||
|
||||
assert.containsNone(document.body, '.modal');
|
||||
// dialog 1 is closed through the removal of its parent => no callback
|
||||
assert.verifySteps(['dialog_5_closed', 'dialog_4_closed']);
|
||||
});
|
||||
|
||||
QUnit.test("Interactions between legacy owl dialogs and new owl dialogs", async function (assert) {
|
||||
assert.expect(9);
|
||||
const { legacyEnv, env } = await makeLegacyDialogMappingTestEnv();
|
||||
|
||||
class OwlDialogWrapper extends LegacyComponent {
|
||||
setup() {
|
||||
this.env = legacyEnv;
|
||||
this.__owl__.childEnv = legacyEnv;
|
||||
}
|
||||
}
|
||||
OwlDialogWrapper.template = xml`
|
||||
<Dialog
|
||||
onClosed="() => props.close()"
|
||||
/>
|
||||
`;
|
||||
OwlDialogWrapper.components = { Dialog };
|
||||
class WowlDialogWrapper extends Component {}
|
||||
WowlDialogWrapper.components = { WowlDialog };
|
||||
WowlDialogWrapper.template = xml`<WowlDialog contentClass="props.contentClass">content</WowlDialog>`;
|
||||
class Parent extends LegacyComponent {
|
||||
setup() {
|
||||
this.dialogs = useState([]);
|
||||
}
|
||||
// Handlers
|
||||
_onDialogClosed(id) {
|
||||
assert.step(`dialog_${id}_closed`);
|
||||
this.dialogs.splice(this.dialogs.findIndex(d => d.id === id), 1);
|
||||
}
|
||||
}
|
||||
Parent.template = xml`
|
||||
<div>
|
||||
<div class="o_dialog_container"/>
|
||||
<t t-foreach="dialogs" t-as="dialog" t-key="dialog.id">
|
||||
<WithEnv env="{ dialogData: { isActive: true, close: () => this._onDialogClosed(dialog.id) } }">
|
||||
<t t-component="dialog.class"
|
||||
contentClass="'dialog_' + dialog.id"
|
||||
close="() => this._onDialogClosed(dialog.id)"
|
||||
/>
|
||||
</WithEnv>
|
||||
</t>
|
||||
</div>`;
|
||||
Parent.components = { WithEnv };
|
||||
|
||||
const target = getFixture();
|
||||
const parent = await mount(Parent, target, { env });
|
||||
|
||||
parent.dialogs.push({ id: 1, class: WowlDialogWrapper });
|
||||
await nextTick();
|
||||
parent.dialogs.push({ id: 2, class: OwlDialogWrapper });
|
||||
await nextTick();
|
||||
parent.dialogs.push({ id: 3, class: WowlDialogWrapper });
|
||||
await nextTick();
|
||||
|
||||
assert.verifySteps([]);
|
||||
assert.containsN(document.body, ".modal", 3);
|
||||
await testUtils.dom.triggerEvent(document.activeElement, 'keydown', EscapeKey); // Press Escape
|
||||
assert.verifySteps(['dialog_3_closed']);
|
||||
await testUtils.dom.triggerEvent(document.activeElement, 'keydown', EscapeKey); // Press Escape
|
||||
assert.verifySteps(['dialog_2_closed']);
|
||||
await testUtils.dom.triggerEvent(document.activeElement, 'keydown', EscapeKey); // Press Escape
|
||||
assert.verifySteps(['dialog_1_closed']);
|
||||
await nextTick();
|
||||
assert.containsNone(document.body, ".modal");
|
||||
});
|
||||
|
||||
QUnit.test("Z-index toggling and interactions", async function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
function createCustomModal(className) {
|
||||
const $modal = $(
|
||||
`<div role="dialog" class="${className}" tabindex="-1">
|
||||
<div class="modal-dialog medium">
|
||||
<div class="modal-content">
|
||||
<main class="modal-body">The modal body</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>`
|
||||
).appendTo('body').modal('show');
|
||||
const modal = $modal[0];
|
||||
modal.destroy = function () {
|
||||
$modal.modal('hide');
|
||||
this.remove();
|
||||
};
|
||||
return modal;
|
||||
}
|
||||
|
||||
class Parent1 extends LegacyComponent {
|
||||
setup() {
|
||||
}
|
||||
}
|
||||
Parent1.components = { Dialog };
|
||||
Parent1.template = xml`
|
||||
<div>
|
||||
<Dialog/>
|
||||
</div>`;
|
||||
|
||||
const parent1 = await mount(Parent1, getFixture(), {
|
||||
env: makeTestEnvironment(),
|
||||
});
|
||||
|
||||
class Parent2 extends LegacyComponent {
|
||||
setup() {
|
||||
this.state = useState({ showSecondDialog: true });
|
||||
}
|
||||
}
|
||||
Parent2.components = { Dialog };
|
||||
Parent2.template = xml`
|
||||
<div>
|
||||
<Dialog t-if="state.showSecondDialog"/>
|
||||
</div>`;
|
||||
|
||||
const parent2 = await mount(Parent2, getFixture(), {
|
||||
env: makeTestEnvironment(),
|
||||
});
|
||||
|
||||
const frontEndModal = createCustomModal('modal');
|
||||
const backEndModal = createCustomModal('modal o_technical_modal');
|
||||
|
||||
// querySelector will target the first modal (the static one).
|
||||
const owlIndexBefore = getComputedStyle(document.querySelector('.o_dialog .modal')).zIndex;
|
||||
const feZIndexBefore = getComputedStyle(frontEndModal).zIndex;
|
||||
const beZIndexBefore = getComputedStyle(backEndModal).zIndex;
|
||||
|
||||
parent2.state.showSecondDialog = false;
|
||||
await testUtils.nextTick();
|
||||
|
||||
assert.ok(owlIndexBefore < getComputedStyle(document.querySelector('.o_dialog .modal')).zIndex,
|
||||
"z-index of the owl dialog should be incremented since the active modal was destroyed");
|
||||
assert.strictEqual(feZIndexBefore, getComputedStyle(frontEndModal).zIndex,
|
||||
"z-index of front-end modals should not be impacted by Owl Dialog activity system");
|
||||
assert.strictEqual(beZIndexBefore, getComputedStyle(backEndModal).zIndex,
|
||||
"z-index of custom back-end modals should not be impacted by Owl Dialog activity system");
|
||||
|
||||
frontEndModal.destroy();
|
||||
backEndModal.destroy();
|
||||
destroy(parent1);
|
||||
destroy(parent2);
|
||||
});
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,271 +0,0 @@
|
|||
odoo.define('web.popover_tests', function (require) {
|
||||
'use strict';
|
||||
|
||||
const makeTestEnvironment = require('web.test_env');
|
||||
const Popover = require('web.Popover');
|
||||
const testUtils = require('web.test_utils');
|
||||
const { click, mount } = require("@web/../tests/helpers/utils");
|
||||
const { LegacyComponent } = require("@web/legacy/legacy_component");
|
||||
|
||||
const { useState, xml } = owl;
|
||||
|
||||
QUnit.module('core', {}, function () {
|
||||
QUnit.module('Popover');
|
||||
|
||||
QUnit.test('Basic rendering & props', async function (assert) {
|
||||
assert.expect(11);
|
||||
|
||||
class SubComponent extends LegacyComponent {}
|
||||
SubComponent.template = xml`
|
||||
<div class="o_subcomponent" style="width: 280px;" t-esc="props.text"/>
|
||||
`;
|
||||
|
||||
class Parent extends LegacyComponent {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.state = useState({
|
||||
position: 'right',
|
||||
title: '👋',
|
||||
textContent: 'sup',
|
||||
});
|
||||
}
|
||||
}
|
||||
Parent.components = { Popover, SubComponent };
|
||||
Parent.template = xml`
|
||||
<div>
|
||||
<button id="passiveTarget">🚫</button>
|
||||
<Popover
|
||||
position="state.position"
|
||||
title="state.title"
|
||||
>
|
||||
<t t-set-slot="opened">
|
||||
<SubComponent text="state.textContent"/>
|
||||
</t>
|
||||
<button id="target">
|
||||
Notice me, senpai 👀
|
||||
</button>
|
||||
</Popover>
|
||||
</div>`;
|
||||
|
||||
const target = testUtils.prepareTarget();
|
||||
const env = makeTestEnvironment();
|
||||
|
||||
/**
|
||||
* The component being tested behaves differently based on its
|
||||
* visibility (or not) in the viewport. The qunit target has to be
|
||||
* in the view port for these tests to be meaningful.
|
||||
*/
|
||||
target.style.top = '300px';
|
||||
target.style.left = '150px';
|
||||
target.style.width = '300px';
|
||||
|
||||
// Helper functions
|
||||
async function changeProps(key, value) {
|
||||
parent.state[key] = value;
|
||||
await testUtils.nextTick();
|
||||
}
|
||||
function pointsTo(popover, element, position) {
|
||||
const hasCorrectClass = popover.classList.contains(
|
||||
`o_popover--${position}`
|
||||
);
|
||||
const expectedPosition = Popover.computePositioningData(
|
||||
popover,
|
||||
element
|
||||
)[position];
|
||||
const correctLeft =
|
||||
parseFloat(popover.style.left) ===
|
||||
Math.round(expectedPosition.left * 100) / 100;
|
||||
const correctTop =
|
||||
parseFloat(popover.style.top) ===
|
||||
Math.round(expectedPosition.top * 100) / 100;
|
||||
return hasCorrectClass && correctLeft && correctTop;
|
||||
}
|
||||
|
||||
const parent = await mount(Parent, target, { env });
|
||||
const body = document.querySelector('body');
|
||||
let popover, title;
|
||||
// Show/hide
|
||||
assert.containsNone(body, '.o_popover');
|
||||
await click(body, '#target');
|
||||
assert.containsOnce(body, '.o_popover');
|
||||
assert.containsOnce(body, '.o_subcomponent');
|
||||
assert.containsOnce(body, '.o_popover--right');
|
||||
await click(body, '#passiveTarget');
|
||||
assert.containsNone(body, '.o_popover');
|
||||
// Reactivity of title
|
||||
await click(body, '#target');
|
||||
popover = document.querySelector('.o_popover');
|
||||
title = popover.querySelector('.o_popover_header').innerText.trim();
|
||||
assert.strictEqual(title, '👋');
|
||||
await changeProps('title', '🤔');
|
||||
title = popover.querySelector('.o_popover_header').innerText.trim();
|
||||
assert.strictEqual(
|
||||
title,
|
||||
'🤔',
|
||||
'The title of the popover should have changed.'
|
||||
);
|
||||
// Position and target reactivity
|
||||
const element = document.getElementById("passiveTarget").nextSibling;
|
||||
assert.ok(
|
||||
pointsTo(
|
||||
document.querySelector('.o_popover'),
|
||||
element,
|
||||
parent.state.position
|
||||
),
|
||||
'Popover should be visually aligned with its target'
|
||||
);
|
||||
await changeProps('position', 'bottom');
|
||||
assert.ok(
|
||||
pointsTo(
|
||||
document.querySelector('.o_popover'),
|
||||
element,
|
||||
parent.state.position
|
||||
),
|
||||
'Popover should be bottomed positioned'
|
||||
);
|
||||
// Reactivity of subcomponents
|
||||
await changeProps('textContent', 'wassup');
|
||||
assert.strictEqual(
|
||||
popover.querySelector('.o_subcomponent').innerText.trim(),
|
||||
'wassup',
|
||||
'Subcomponent should match with its given text'
|
||||
);
|
||||
await click(body, '#passiveTarget');
|
||||
// Requested position not fitting
|
||||
await changeProps('position', 'left');
|
||||
await click(body, '#target');
|
||||
assert.ok(
|
||||
pointsTo(document.querySelector('.o_popover'), element, 'right'),
|
||||
"Popover should be right-positioned because it doesn't fit left"
|
||||
);
|
||||
await click(body, '#passiveTarget');
|
||||
});
|
||||
|
||||
QUnit.test('Multiple popovers', async function (assert) {
|
||||
assert.expect(9);
|
||||
|
||||
class Parent extends LegacyComponent {}
|
||||
Parent.components = { Popover };
|
||||
Parent.template = xml`
|
||||
<div>
|
||||
<Popover>
|
||||
<button id="firstTarget">👋</button>
|
||||
<t t-set-slot="opened">
|
||||
<p id="firstContent">first popover</p>
|
||||
</t>
|
||||
</Popover>
|
||||
<br/>
|
||||
<Popover>
|
||||
<button id="secondTarget">👏</button>
|
||||
<t t-set-slot="opened">
|
||||
<p id="secondContent">second popover</p>
|
||||
</t>
|
||||
</Popover>
|
||||
<br/>
|
||||
<span id="dismissPopovers">💀</span>
|
||||
</div>`;
|
||||
|
||||
const target = testUtils.prepareTarget();
|
||||
const env = makeTestEnvironment();
|
||||
|
||||
const body = document.querySelector('body');
|
||||
|
||||
await mount(Parent, target, { env });
|
||||
// Show first popover
|
||||
assert.containsNone(body, '.o_popover');
|
||||
await click(body, '#firstTarget');
|
||||
assert.containsOnce(body, '#firstContent');
|
||||
assert.containsNone(body, '#secondContent');
|
||||
await click(body, '#dismissPopovers');
|
||||
assert.containsNone(body, '.o_popover');
|
||||
// Show first then display second
|
||||
await click(body, '#firstTarget');
|
||||
assert.containsOnce(body, '#firstContent');
|
||||
assert.containsNone(body, '#secondContent');
|
||||
await click(body, '#secondTarget');
|
||||
assert.containsNone(body, '#firstContent');
|
||||
assert.containsOnce(body, '#secondContent');
|
||||
await click(body, '#dismissPopovers');
|
||||
assert.containsNone(body, '.o_popover');
|
||||
});
|
||||
|
||||
QUnit.test('toggle', async function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
class Parent extends LegacyComponent {}
|
||||
Parent.template = xml`
|
||||
<div>
|
||||
<Popover>
|
||||
<button id="open">Open</button>
|
||||
<t t-set-slot="opened">
|
||||
Opened!
|
||||
</t>
|
||||
</Popover>
|
||||
</div>
|
||||
`;
|
||||
Parent.components = { Popover };
|
||||
|
||||
const target = testUtils.prepareTarget();
|
||||
const env = makeTestEnvironment();
|
||||
|
||||
await mount(Parent, target, { env });
|
||||
|
||||
const body = document.querySelector('body');
|
||||
assert.containsOnce(body, '#open');
|
||||
assert.containsNone(body, '.o_popover');
|
||||
|
||||
await click(body, '#open');
|
||||
assert.containsOnce(body, '.o_popover');
|
||||
|
||||
await click(body, '#open');
|
||||
assert.containsNone(body, '.o_popover');
|
||||
});
|
||||
|
||||
QUnit.test('close event', async function (assert) {
|
||||
assert.expect(7);
|
||||
|
||||
// Needed to trigger the event from inside the Popover slot.
|
||||
class Content extends LegacyComponent {
|
||||
onClick() {
|
||||
this.trigger("o-popover-close");
|
||||
}
|
||||
}
|
||||
Content.template = xml`
|
||||
<button id="close" t-on-click="onClick">
|
||||
Close
|
||||
</button>
|
||||
`;
|
||||
|
||||
class Parent extends LegacyComponent {}
|
||||
Parent.components = { Content, Popover };
|
||||
Parent.template = xml`
|
||||
<div>
|
||||
<Popover>
|
||||
<button id="open">Open</button>
|
||||
<t t-set-slot="opened">
|
||||
<Content/>
|
||||
</t>
|
||||
</Popover>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const target = testUtils.prepareTarget();
|
||||
const env = makeTestEnvironment();
|
||||
|
||||
await mount(Parent, target, { env });
|
||||
|
||||
const body = document.querySelector('body');
|
||||
assert.containsOnce(body, '#open');
|
||||
assert.containsNone(body, '.o_popover');
|
||||
assert.containsNone(body, '#close');
|
||||
|
||||
await click(body, '#open');
|
||||
assert.containsOnce(body, '.o_popover');
|
||||
assert.containsOnce(body, '#close');
|
||||
|
||||
await click(body, '#close');
|
||||
assert.containsNone(body, '.o_popover');
|
||||
assert.containsNone(body, '#close');
|
||||
});
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,90 +0,0 @@
|
|||
odoo.define('web.registry_tests', function (require) {
|
||||
"use strict";
|
||||
|
||||
var Registry = require('web.Registry');
|
||||
|
||||
QUnit.module('core', {}, function () {
|
||||
|
||||
QUnit.module('Registry');
|
||||
|
||||
QUnit.test('key set', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
var registry = new Registry();
|
||||
var foo = {};
|
||||
|
||||
registry
|
||||
.add('foo', foo);
|
||||
|
||||
assert.strictEqual(registry.get('foo'), foo);
|
||||
});
|
||||
|
||||
QUnit.test('get initial keys', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
var registry = new Registry({ a: 1, });
|
||||
assert.deepEqual(
|
||||
registry.keys(),
|
||||
['a'],
|
||||
"keys on prototype should be returned"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test('get initial entries', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
var registry = new Registry({ a: 1, });
|
||||
assert.deepEqual(
|
||||
registry.entries(),
|
||||
{ a: 1, },
|
||||
"entries on prototype should be returned"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test('multiget', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
var foo = {};
|
||||
var bar = {};
|
||||
var registry = new Registry({
|
||||
foo: foo,
|
||||
bar: bar,
|
||||
});
|
||||
assert.strictEqual(
|
||||
registry.getAny(['qux', 'grault', 'bar', 'foo']),
|
||||
bar,
|
||||
"Registry getAny should find first defined key");
|
||||
});
|
||||
|
||||
QUnit.test('keys and values are properly ordered', function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
var registry = new Registry();
|
||||
|
||||
registry
|
||||
.add('fred', 'foo', 3)
|
||||
.add('george', 'bar', 2)
|
||||
.add('ronald', 'qux', 4);
|
||||
|
||||
assert.deepEqual(registry.keys(), ['george', 'fred', 'ronald']);
|
||||
assert.deepEqual(registry.values(), ['bar', 'foo', 'qux']);
|
||||
});
|
||||
|
||||
QUnit.test("predicate prevents invalid values", function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
const predicate = value => typeof value === "number";
|
||||
const registry = new Registry(null, predicate);
|
||||
registry.onAdd((key) => assert.step(key));
|
||||
|
||||
assert.ok(registry.add("age", 23));
|
||||
assert.throws(
|
||||
() => registry.add("name", "Fred"),
|
||||
new Error(`Value of key "name" does not pass the addition predicate.`)
|
||||
);
|
||||
assert.deepEqual(registry.entries(), { age: 23 });
|
||||
assert.verifySteps(["age"]);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
|
@ -1,316 +0,0 @@
|
|||
odoo.define('web.rpc_tests', function (require) {
|
||||
"use strict";
|
||||
|
||||
var rpc = require('web.rpc');
|
||||
|
||||
QUnit.module('core', {}, function () {
|
||||
|
||||
QUnit.module('RPC Builder');
|
||||
|
||||
QUnit.test('basic rpc (route)', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
var query = rpc.buildQuery({
|
||||
route: '/my/route',
|
||||
});
|
||||
assert.strictEqual(query.route, '/my/route', "should have the proper route");
|
||||
});
|
||||
|
||||
QUnit.test('rpc on route with parameters', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
var query = rpc.buildQuery({
|
||||
route: '/my/route',
|
||||
params: {hey: 'there', model: 'test'},
|
||||
});
|
||||
|
||||
assert.deepEqual(query.params, {hey: 'there', model: 'test'},
|
||||
"should transfer the proper parameters");
|
||||
});
|
||||
|
||||
QUnit.test('basic rpc, with no context', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
var query = rpc.buildQuery({
|
||||
model: 'partner',
|
||||
method: 'test',
|
||||
kwargs: {},
|
||||
});
|
||||
assert.notOk(query.params.kwargs.context,
|
||||
"does not automatically add a context");
|
||||
});
|
||||
|
||||
QUnit.test('basic rpc, with context', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
var query = rpc.buildQuery({
|
||||
model: 'partner',
|
||||
method: 'test',
|
||||
context: {a: 1},
|
||||
});
|
||||
|
||||
assert.deepEqual(query.params.kwargs.context, {a: 1},
|
||||
"properly transfer the context");
|
||||
});
|
||||
|
||||
QUnit.test('basic rpc, with context, part 2', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
var query = rpc.buildQuery({
|
||||
model: 'partner',
|
||||
method: 'test',
|
||||
kwargs: {context: {a: 1}},
|
||||
});
|
||||
|
||||
assert.deepEqual(query.params.kwargs.context, {a: 1},
|
||||
"properly transfer the context");
|
||||
|
||||
});
|
||||
|
||||
QUnit.test('basic rpc (method of model)', function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
var query = rpc.buildQuery({
|
||||
model: 'partner',
|
||||
method: 'test',
|
||||
kwargs: {context: {a: 1}},
|
||||
});
|
||||
|
||||
assert.strictEqual(query.route, '/web/dataset/call_kw/partner/test',
|
||||
"should call the proper route");
|
||||
assert.strictEqual(query.params.model, 'partner',
|
||||
"should correctly specify the model");
|
||||
assert.strictEqual(query.params.method, 'test',
|
||||
"should correctly specify the method");
|
||||
});
|
||||
|
||||
QUnit.test('rpc with args and kwargs', function (assert) {
|
||||
assert.expect(4);
|
||||
var query = rpc.buildQuery({
|
||||
model: 'partner',
|
||||
method: 'test',
|
||||
args: ['arg1', 2],
|
||||
kwargs: {k: 78},
|
||||
});
|
||||
|
||||
assert.strictEqual(query.route, '/web/dataset/call_kw/partner/test',
|
||||
"should call the proper route");
|
||||
assert.strictEqual(query.params.args[0], 'arg1',
|
||||
"should call with correct args");
|
||||
assert.strictEqual(query.params.args[1], 2,
|
||||
"should call with correct args");
|
||||
assert.strictEqual(query.params.kwargs.k, 78,
|
||||
"should call with correct kargs");
|
||||
});
|
||||
|
||||
QUnit.test('search_read controller', function (assert) {
|
||||
assert.expect(1);
|
||||
var query = rpc.buildQuery({
|
||||
route: '/web/dataset/search_read',
|
||||
model: 'partner',
|
||||
domain: ['a', '=', 1],
|
||||
fields: ['name'],
|
||||
limit: 32,
|
||||
offset: 2,
|
||||
orderBy: [{name: 'yop', asc: true}, {name: 'aa', asc: false}],
|
||||
});
|
||||
assert.deepEqual(query.params, {
|
||||
context: {},
|
||||
domain: ['a', '=', 1],
|
||||
fields: ['name'],
|
||||
limit: 32,
|
||||
offset: 2,
|
||||
model: 'partner',
|
||||
sort: 'yop ASC, aa DESC',
|
||||
}, "should have correct args");
|
||||
});
|
||||
|
||||
QUnit.test('search_read method', function (assert) {
|
||||
assert.expect(1);
|
||||
var query = rpc.buildQuery({
|
||||
model: 'partner',
|
||||
method: 'search_read',
|
||||
domain: ['a', '=', 1],
|
||||
fields: ['name'],
|
||||
limit: 32,
|
||||
offset: 2,
|
||||
orderBy: [{name: 'yop', asc: true}, {name: 'aa', asc: false}],
|
||||
});
|
||||
assert.deepEqual(query.params, {
|
||||
args: [],
|
||||
kwargs: {
|
||||
domain: ['a', '=', 1],
|
||||
fields: ['name'],
|
||||
offset: 2,
|
||||
limit: 32,
|
||||
order: 'yop ASC, aa DESC'
|
||||
},
|
||||
method: 'search_read',
|
||||
model: 'partner'
|
||||
}, "should have correct kwargs");
|
||||
});
|
||||
|
||||
QUnit.test('search_read with args', function (assert) {
|
||||
assert.expect(1);
|
||||
var query = rpc.buildQuery({
|
||||
model: 'partner',
|
||||
method: 'search_read',
|
||||
args: [
|
||||
['a', '=', 1],
|
||||
['name'],
|
||||
2,
|
||||
32,
|
||||
'yop ASC, aa DESC',
|
||||
]
|
||||
});
|
||||
assert.deepEqual(query.params, {
|
||||
args: [['a', '=', 1], ['name'], 2, 32, 'yop ASC, aa DESC'],
|
||||
kwargs: {},
|
||||
method: 'search_read',
|
||||
model: 'partner'
|
||||
}, "should have correct args");
|
||||
});
|
||||
|
||||
QUnit.test('read_group', function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
var query = rpc.buildQuery({
|
||||
model: 'partner',
|
||||
method: 'read_group',
|
||||
domain: ['a', '=', 1],
|
||||
fields: ['name'],
|
||||
groupBy: ['product_id'],
|
||||
context: {abc: 'def'},
|
||||
lazy: true,
|
||||
});
|
||||
|
||||
assert.deepEqual(query.params, {
|
||||
args: [],
|
||||
kwargs: {
|
||||
context: {abc: 'def'},
|
||||
domain: ['a', '=', 1],
|
||||
fields: ['name'],
|
||||
groupby: ['product_id'],
|
||||
lazy: true,
|
||||
},
|
||||
method: 'read_group',
|
||||
model: 'partner',
|
||||
}, "should have correct args");
|
||||
assert.equal(query.route, '/web/dataset/call_kw/partner/read_group',
|
||||
"should call correct route");
|
||||
});
|
||||
|
||||
QUnit.test('read_group with kwargs', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
var query = rpc.buildQuery({
|
||||
model: 'partner',
|
||||
method: 'read_group',
|
||||
domain: ['a', '=', 1],
|
||||
fields: ['name'],
|
||||
groupBy: ['product_id'],
|
||||
lazy: false,
|
||||
kwargs: {context: {abc: 'def'}}
|
||||
});
|
||||
|
||||
assert.deepEqual(query.params, {
|
||||
args: [],
|
||||
kwargs: {
|
||||
context: {abc: 'def'},
|
||||
domain: ['a', '=', 1],
|
||||
fields: ['name'],
|
||||
groupby: ['product_id'],
|
||||
lazy: false,
|
||||
},
|
||||
method: 'read_group',
|
||||
model: 'partner',
|
||||
}, "should have correct args");
|
||||
});
|
||||
|
||||
QUnit.test('read_group with no domain, nor fields', function (assert) {
|
||||
assert.expect(7);
|
||||
var query = rpc.buildQuery({
|
||||
model: 'partner',
|
||||
method: 'read_group',
|
||||
});
|
||||
|
||||
assert.deepEqual(query.params.kwargs.domain, [], "should have [] as default domain");
|
||||
assert.deepEqual(query.params.kwargs.fields, [], "should have false as default fields");
|
||||
assert.deepEqual(query.params.kwargs.groupby, [], "should have false as default groupby");
|
||||
assert.deepEqual(query.params.kwargs.offset, undefined, "should not enforce a default value for offst");
|
||||
assert.deepEqual(query.params.kwargs.limit, undefined, "should not enforce a default value for limit");
|
||||
assert.deepEqual(query.params.kwargs.orderby, undefined, "should not enforce a default value for orderby");
|
||||
assert.deepEqual(query.params.kwargs.lazy, undefined, "should not enforce a default value for lazy");
|
||||
});
|
||||
|
||||
QUnit.test('read_group with args and kwargs', function (assert) {
|
||||
assert.expect(9);
|
||||
var query = rpc.buildQuery({
|
||||
model: 'partner',
|
||||
method: 'read_group',
|
||||
kwargs: {
|
||||
domain: ['name', '=', 'saucisse'],
|
||||
fields: ['category_id'],
|
||||
groupby: ['country_id'],
|
||||
},
|
||||
});
|
||||
|
||||
assert.deepEqual(query.params.kwargs.domain, ['name', '=', 'saucisse'], "should have ['name', '=', 'saucisse'] category_id as default domain");
|
||||
assert.deepEqual(query.params.kwargs.fields, ['category_id'], "should have category_id as default fields");
|
||||
assert.deepEqual(query.params.kwargs.groupby, ['country_id'], "should have country_id as default groupby");
|
||||
|
||||
var query = rpc.buildQuery({
|
||||
model: 'partner',
|
||||
method: 'read_group',
|
||||
args: [['name', '=', 'saucisse']],
|
||||
kwargs: {
|
||||
fields: ['category_id'],
|
||||
groupby: ['country_id'],
|
||||
},
|
||||
});
|
||||
|
||||
assert.deepEqual(query.params.kwargs.domain, undefined, "should not enforce a default value for domain");
|
||||
assert.deepEqual(query.params.kwargs.fields, ['category_id'], "should have category_id as default fields");
|
||||
assert.deepEqual(query.params.kwargs.groupby, ['country_id'], "should have country_id as default groupby");
|
||||
|
||||
var query = rpc.buildQuery({
|
||||
model: 'partner',
|
||||
method: 'read_group',
|
||||
args: [['name', '=', 'saucisse'], ['category_id'], ['country_id']],
|
||||
});
|
||||
|
||||
assert.deepEqual(query.params.kwargs.domain, undefined, "should not enforce a default value for domain");
|
||||
assert.deepEqual(query.params.kwargs.fields, undefined, "should not enforce a default value for fields");
|
||||
assert.deepEqual(query.params.kwargs.groupby, undefined, "should not enforce a default value for groupby");
|
||||
});
|
||||
|
||||
QUnit.test('search_read with no domain, nor fields', function (assert) {
|
||||
assert.expect(5);
|
||||
var query = rpc.buildQuery({
|
||||
model: 'partner',
|
||||
method: 'search_read',
|
||||
});
|
||||
|
||||
assert.deepEqual(query.params.kwargs.domain, undefined, "should not enforce a default value for domain");
|
||||
assert.deepEqual(query.params.kwargs.fields, undefined, "should not enforce a default value for fields");
|
||||
assert.deepEqual(query.params.kwargs.offset, undefined, "should not enforce a default value for offset");
|
||||
assert.deepEqual(query.params.kwargs.limit, undefined, "should not enforce a default value for limit");
|
||||
assert.deepEqual(query.params.kwargs.order, undefined, "should not enforce a default value for orderby");
|
||||
});
|
||||
|
||||
QUnit.test('search_read controller with no domain, nor fields', function (assert) {
|
||||
assert.expect(5);
|
||||
var query = rpc.buildQuery({
|
||||
model: 'partner',
|
||||
route: '/web/dataset/search_read',
|
||||
});
|
||||
|
||||
assert.deepEqual(query.params.domain, undefined, "should not enforce a default value for domain");
|
||||
assert.deepEqual(query.params.fields, undefined, "should not enforce a default value for fields");
|
||||
assert.deepEqual(query.params.offset, undefined, "should not enforce a default value for groupby");
|
||||
assert.deepEqual(query.params.limit, undefined, "should not enforce a default value for limit");
|
||||
assert.deepEqual(query.params.sort, undefined, "should not enforce a default value for order");
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
|
@ -1,165 +0,0 @@
|
|||
odoo.define('web.time_tests', function (require) {
|
||||
"use strict";
|
||||
|
||||
const core = require('web.core');
|
||||
var time = require('web.time');
|
||||
|
||||
QUnit.module('core', {}, function () {
|
||||
|
||||
QUnit.module('Time utils');
|
||||
|
||||
QUnit.test('Parse server datetime', function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
var date = time.str_to_datetime("2009-05-04 12:34:23");
|
||||
assert.deepEqual(
|
||||
[date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(),
|
||||
date.getUTCHours(), date.getUTCMinutes(), date.getUTCSeconds()],
|
||||
[2009, 5 - 1, 4, 12, 34, 23]);
|
||||
assert.deepEqual(
|
||||
[date.getFullYear(), date.getMonth(), date.getDate(),
|
||||
date.getHours(), date.getMinutes(), date.getSeconds()],
|
||||
[2009, 5 - 1, 4, 12 - (date.getTimezoneOffset() / 60), 34, 23]);
|
||||
|
||||
var date2 = time.str_to_datetime('2011-12-10 00:00:00');
|
||||
assert.deepEqual(
|
||||
[date2.getUTCFullYear(), date2.getUTCMonth(), date2.getUTCDate(),
|
||||
date2.getUTCHours(), date2.getUTCMinutes(), date2.getUTCSeconds()],
|
||||
[2011, 12 - 1, 10, 0, 0, 0]);
|
||||
|
||||
var date3 = time.str_to_datetime("2009-05-04 12:34:23.84565");
|
||||
assert.deepEqual(
|
||||
[date3.getUTCFullYear(), date3.getUTCMonth(), date3.getUTCDate(),
|
||||
date3.getUTCHours(), date3.getUTCMinutes(), date3.getUTCSeconds(), date3.getUTCMilliseconds()],
|
||||
[2009, 5 - 1, 4, 12, 34, 23, 845]);
|
||||
});
|
||||
|
||||
QUnit.test('Parse server datetime on 31', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
var wDate = window.Date;
|
||||
|
||||
try {
|
||||
window.Date = function (v) {
|
||||
if (_.isUndefined(v)) {
|
||||
v = '2013-10-31 12:34:56';
|
||||
}
|
||||
return new wDate(v);
|
||||
};
|
||||
var date = time.str_to_datetime('2013-11-11 02:45:21');
|
||||
|
||||
assert.deepEqual(
|
||||
[date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(),
|
||||
date.getUTCHours(), date.getUTCMinutes(), date.getUTCSeconds()],
|
||||
[2013, 11 - 1, 11, 2, 45, 21]);
|
||||
}
|
||||
finally {
|
||||
window.Date = wDate;
|
||||
}
|
||||
});
|
||||
|
||||
QUnit.test('Parse server date', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
var date = time.str_to_date("2009-05-04");
|
||||
assert.deepEqual(
|
||||
[date.getFullYear(), date.getMonth(), date.getDate()],
|
||||
[2009, 5 - 1, 4]);
|
||||
});
|
||||
|
||||
QUnit.test('Parse server date on 31', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
var wDate = window.Date;
|
||||
|
||||
try {
|
||||
window.Date = function (v) {
|
||||
if (_.isUndefined(v)) {
|
||||
v = '2013-10-31 12:34:56';
|
||||
}
|
||||
return new wDate(v);
|
||||
};
|
||||
var date = time.str_to_date('2013-11-21');
|
||||
|
||||
assert.deepEqual(
|
||||
[date.getFullYear(), date.getMonth(), date.getDate()],
|
||||
[2013, 11 - 1, 21]);
|
||||
}
|
||||
finally {
|
||||
window.Date = wDate;
|
||||
}
|
||||
});
|
||||
|
||||
QUnit.test('Parse server time', function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
var date = time.str_to_time("12:34:23");
|
||||
assert.deepEqual(
|
||||
[date.getHours(), date.getMinutes(), date.getSeconds()],
|
||||
[12, 34, 23]);
|
||||
|
||||
date = time.str_to_time("12:34:23.5467");
|
||||
assert.deepEqual(
|
||||
[date.getHours(), date.getMinutes(), date.getSeconds(), date.getMilliseconds()],
|
||||
[12, 34, 23, 546]);
|
||||
});
|
||||
|
||||
QUnit.test('Format server datetime', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
var date = new Date();
|
||||
date.setUTCFullYear(2009);
|
||||
date.setUTCMonth(5 - 1);
|
||||
date.setUTCDate(4);
|
||||
date.setUTCHours(12);
|
||||
date.setUTCMinutes(34);
|
||||
date.setUTCSeconds(23);
|
||||
assert.strictEqual(time.datetime_to_str(date), "2009-05-04 12:34:23");
|
||||
});
|
||||
|
||||
QUnit.test('Format server date', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
var date = new Date();
|
||||
date.setUTCFullYear(2009);
|
||||
date.setUTCMonth(5 - 1);
|
||||
date.setUTCDate(4);
|
||||
date.setUTCHours(0);
|
||||
date.setUTCMinutes(0);
|
||||
date.setUTCSeconds(0);
|
||||
assert.strictEqual(time.date_to_str(date), "2009-05-04");
|
||||
});
|
||||
|
||||
QUnit.test('Format server time', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
var date = new Date();
|
||||
date.setUTCFullYear(1970);
|
||||
date.setUTCMonth(1 - 1);
|
||||
date.setUTCDate(1);
|
||||
date.setUTCHours(0);
|
||||
date.setUTCMinutes(0);
|
||||
date.setUTCSeconds(0);
|
||||
date.setHours(12);
|
||||
date.setMinutes(34);
|
||||
date.setSeconds(23);
|
||||
assert.strictEqual(time.time_to_str(date), "12:34:23");
|
||||
});
|
||||
|
||||
QUnit.test("Get lang datetime format", (assert) => {
|
||||
assert.expect(4);
|
||||
const originalParameters = Object.assign({}, core._t.database.parameters);
|
||||
Object.assign(core._t.database.parameters, {
|
||||
date_format: '%m/%d/%Y',
|
||||
time_format: '%H:%M:%S',
|
||||
});
|
||||
assert.strictEqual(time.getLangDateFormat(), "MM/DD/YYYY");
|
||||
assert.strictEqual(time.getLangDateFormatWoZero(), "M/D/YYYY");
|
||||
assert.strictEqual(time.getLangTimeFormat(), "HH:mm:ss");
|
||||
assert.strictEqual(time.getLangTimeFormatWoZero(), "H:m:s");
|
||||
Object.assign(core._t.database.parameters, originalParameters);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
|
@ -1,316 +0,0 @@
|
|||
odoo.define('web.util_tests', function (require) {
|
||||
"use strict";
|
||||
|
||||
var utils = require('web.utils');
|
||||
const { getDataURLFromFile } = require('@web/core/utils/urls')
|
||||
|
||||
QUnit.module('core', {}, function () {
|
||||
|
||||
QUnit.module('utils');
|
||||
|
||||
QUnit.test('findWhere', function (assert) {
|
||||
assert.expect(7);
|
||||
|
||||
const { findWhere } = utils;
|
||||
|
||||
const list = [
|
||||
undefined,
|
||||
{ a: 1, b: 2 },
|
||||
{ a: 2, b: 2 },
|
||||
{ a: 1, b: 3 },
|
||||
{ a: 1, b: 4 },
|
||||
{ a: 2, b: 4 },
|
||||
];
|
||||
|
||||
assert.deepEqual(findWhere(list, { a: 1 }), { a: 1, b: 2 });
|
||||
assert.deepEqual(findWhere(list, { a: 2 }), { a: 2, b: 2 });
|
||||
assert.deepEqual(findWhere(list, { b: 4 }), { a: 1, b: 4 });
|
||||
assert.deepEqual(findWhere(list, { b: 4, a: 2 }), { a: 2, b: 4 });
|
||||
assert.ok(findWhere([], { a: 1 }) === undefined);
|
||||
assert.ok(findWhere(list, { a: 1, b: 5 }) === undefined);
|
||||
assert.ok(findWhere(list, { c: 1 }) === undefined);
|
||||
});
|
||||
|
||||
QUnit.test('groupBy', function (assert) {
|
||||
assert.expect(7);
|
||||
|
||||
const { groupBy } = utils;
|
||||
|
||||
// Invalid
|
||||
assert.throws(
|
||||
() => groupBy({}),
|
||||
new TypeError(`list is not iterable`)
|
||||
);
|
||||
assert.throws(
|
||||
() => groupBy([], true),
|
||||
new Error(`Expected criterion of type 'string' or 'function' and got 'boolean'`)
|
||||
);
|
||||
assert.throws(
|
||||
() => groupBy([], 3),
|
||||
new Error(`Expected criterion of type 'string' or 'function' and got 'number'`)
|
||||
);
|
||||
assert.throws(
|
||||
() => groupBy([], {}),
|
||||
new Error(`Expected criterion of type 'string' or 'function' and got 'object'`)
|
||||
);
|
||||
|
||||
// criterion = default
|
||||
assert.deepEqual(
|
||||
groupBy(["a", "b", 1, true]),
|
||||
{
|
||||
1: [1],
|
||||
a: ["a"],
|
||||
b: ["b"],
|
||||
true: [true],
|
||||
}
|
||||
);
|
||||
// criterion = string
|
||||
assert.deepEqual(
|
||||
groupBy([{ x: "a" }, { x: "a" }, { x: "b" }], "x"),
|
||||
{
|
||||
a: [{ x: "a" }, { x: "a" }],
|
||||
b: [{ x: "b" }],
|
||||
}
|
||||
);
|
||||
// criterion = function
|
||||
assert.deepEqual(
|
||||
groupBy(["a", "b", 1, true], (x) => `el${x}`),
|
||||
{
|
||||
ela: ["a"],
|
||||
elb: ["b"],
|
||||
el1: [1],
|
||||
eltrue: [true],
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test('intersperse', function (assert) {
|
||||
assert.expect(27);
|
||||
|
||||
var intersperse = utils.intersperse;
|
||||
|
||||
assert.strictEqual(intersperse("", []), "");
|
||||
assert.strictEqual(intersperse("0", []), "0");
|
||||
assert.strictEqual(intersperse("012", []), "012");
|
||||
assert.strictEqual(intersperse("1", []), "1");
|
||||
assert.strictEqual(intersperse("12", []), "12");
|
||||
assert.strictEqual(intersperse("123", []), "123");
|
||||
assert.strictEqual(intersperse("1234", []), "1234");
|
||||
assert.strictEqual(intersperse("123456789", []), "123456789");
|
||||
assert.strictEqual(intersperse("&ab%#@1", []), "&ab%#@1");
|
||||
|
||||
assert.strictEqual(intersperse("0", []), "0");
|
||||
assert.strictEqual(intersperse("0", [1]), "0");
|
||||
assert.strictEqual(intersperse("0", [2]), "0");
|
||||
assert.strictEqual(intersperse("0", [200]), "0");
|
||||
|
||||
assert.strictEqual(intersperse("12345678", [0], '.'), '12345678');
|
||||
assert.strictEqual(intersperse("", [1], '.'), '');
|
||||
assert.strictEqual(intersperse("12345678", [1], '.'), '1234567.8');
|
||||
assert.strictEqual(intersperse("12345678", [1], '.'), '1234567.8');
|
||||
assert.strictEqual(intersperse("12345678", [2], '.'), '123456.78');
|
||||
assert.strictEqual(intersperse("12345678", [2, 1], '.'), '12345.6.78');
|
||||
assert.strictEqual(intersperse("12345678", [2, 0], '.'), '12.34.56.78');
|
||||
assert.strictEqual(intersperse("12345678", [-1, 2], '.'), '12345678');
|
||||
assert.strictEqual(intersperse("12345678", [2, -1], '.'), '123456.78');
|
||||
assert.strictEqual(intersperse("12345678", [2, 0, 1], '.'), '12.34.56.78');
|
||||
assert.strictEqual(intersperse("12345678", [2, 0, 0], '.'), '12.34.56.78');
|
||||
assert.strictEqual(intersperse("12345678", [2, 0, -1], '.'), '12.34.56.78');
|
||||
assert.strictEqual(intersperse("12345678", [3,3,3,3], '.'), '12.345.678');
|
||||
assert.strictEqual(intersperse("12345678", [3,0], '.'), '12.345.678');
|
||||
});
|
||||
|
||||
QUnit.test('is_bin_size', function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
var is_bin_size = utils.is_bin_size;
|
||||
|
||||
assert.strictEqual(is_bin_size('Cg=='), false);
|
||||
assert.strictEqual(is_bin_size('2.5 Mb'), true);
|
||||
// should also work for non-latin languages (e.g. russian)
|
||||
assert.strictEqual(is_bin_size('64.2 Кб'), true);
|
||||
});
|
||||
|
||||
QUnit.test('unaccent', function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
var singleCharacters = utils.unaccent("ⱮɀꝾƶⱵȥ");
|
||||
var doubledCharacters = utils.unaccent("DZDŽꝎꜩꝡƕ");
|
||||
var caseSensetiveCharacters = utils.unaccent("ⱮɀꝾƶⱵȥ", true);
|
||||
|
||||
assert.strictEqual("mzgzhz", singleCharacters);
|
||||
assert.strictEqual("dzdzootzvyhv", doubledCharacters);
|
||||
assert.strictEqual("MzGzHz", caseSensetiveCharacters);
|
||||
});
|
||||
|
||||
QUnit.test('human_number', function (assert) {
|
||||
assert.expect(26);
|
||||
|
||||
var human_number = utils.human_number;
|
||||
|
||||
assert.strictEqual(human_number(1020, 2, 1), '1.02k');
|
||||
assert.strictEqual(human_number(1020000, 2, 2), '1020k');
|
||||
assert.strictEqual(human_number(10200000, 2, 2), '10.2M');
|
||||
assert.strictEqual(human_number(1020, 2, 1), '1.02k');
|
||||
assert.strictEqual(human_number(1002, 2, 1), '1k');
|
||||
assert.strictEqual(human_number(101, 2, 1), '101');
|
||||
assert.strictEqual(human_number(64.2, 2, 1), '64');
|
||||
assert.strictEqual(human_number(1e+18), '1E');
|
||||
assert.strictEqual(human_number(1e+21, 2, 1), '1e+21');
|
||||
assert.strictEqual(human_number(1.0045e+22, 2, 1), '1e+22');
|
||||
assert.strictEqual(human_number(1.0045e+22, 3, 1), '1.005e+22');
|
||||
assert.strictEqual(human_number(1.012e+43, 2, 1), '1.01e+43');
|
||||
assert.strictEqual(human_number(1.012e+43, 2, 2), '1.01e+43');
|
||||
|
||||
assert.strictEqual(human_number(-1020, 2, 1), '-1.02k');
|
||||
assert.strictEqual(human_number(-1020000, 2, 2), '-1020k');
|
||||
assert.strictEqual(human_number(-10200000, 2, 2), '-10.2M');
|
||||
assert.strictEqual(human_number(-1020, 2, 1), '-1.02k');
|
||||
assert.strictEqual(human_number(-1002, 2, 1), '-1k');
|
||||
assert.strictEqual(human_number(-101, 2, 1), '-101');
|
||||
assert.strictEqual(human_number(-64.2, 2, 1), '-64');
|
||||
assert.strictEqual(human_number(-1e+18), '-1E');
|
||||
assert.strictEqual(human_number(-1e+21, 2, 1), '-1e+21');
|
||||
assert.strictEqual(human_number(-1.0045e+22, 2, 1), '-1e+22');
|
||||
assert.strictEqual(human_number(-1.0045e+22, 3, 1), '-1.004e+22');
|
||||
assert.strictEqual(human_number(-1.012e+43, 2, 1), '-1.01e+43');
|
||||
assert.strictEqual(human_number(-1.012e+43, 2, 2), '-1.01e+43');
|
||||
});
|
||||
|
||||
QUnit.test('round_decimals', function (assert) {
|
||||
assert.expect(21);
|
||||
|
||||
var round_di = utils.round_decimals;
|
||||
|
||||
assert.strictEqual(String(round_di(1.0, 0)), '1');
|
||||
assert.strictEqual(String(round_di(1.0, 1)), '1');
|
||||
assert.strictEqual(String(round_di(1.0, 2)), '1');
|
||||
assert.strictEqual(String(round_di(1.0, 3)), '1');
|
||||
assert.strictEqual(String(round_di(1.0, 4)), '1');
|
||||
assert.strictEqual(String(round_di(1.0, 5)), '1');
|
||||
assert.strictEqual(String(round_di(1.0, 6)), '1');
|
||||
assert.strictEqual(String(round_di(1.0, 7)), '1');
|
||||
assert.strictEqual(String(round_di(1.0, 8)), '1');
|
||||
assert.strictEqual(String(round_di(0.5, 0)), '1');
|
||||
assert.strictEqual(String(round_di(-0.5, 0)), '-1');
|
||||
assert.strictEqual(String(round_di(2.6745, 3)), '2.6750000000000003');
|
||||
assert.strictEqual(String(round_di(-2.6745, 3)), '-2.6750000000000003');
|
||||
assert.strictEqual(String(round_di(2.6744, 3)), '2.674');
|
||||
assert.strictEqual(String(round_di(-2.6744, 3)), '-2.674');
|
||||
assert.strictEqual(String(round_di(0.0004, 3)), '0');
|
||||
assert.strictEqual(String(round_di(-0.0004, 3)), '0');
|
||||
assert.strictEqual(String(round_di(357.4555, 3)), '357.456');
|
||||
assert.strictEqual(String(round_di(-357.4555, 3)), '-357.456');
|
||||
assert.strictEqual(String(round_di(457.4554, 3)), '457.455');
|
||||
assert.strictEqual(String(round_di(-457.4554, 3)), '-457.455');
|
||||
});
|
||||
|
||||
QUnit.test('round_precision', function (assert) {
|
||||
assert.expect(26);
|
||||
|
||||
var round_pr = utils.round_precision;
|
||||
|
||||
assert.strictEqual(String(round_pr(1.0, 1)), '1');
|
||||
assert.strictEqual(String(round_pr(1.0, 0.1)), '1');
|
||||
assert.strictEqual(String(round_pr(1.0, 0.01)), '1');
|
||||
assert.strictEqual(String(round_pr(1.0, 0.001)), '1');
|
||||
assert.strictEqual(String(round_pr(1.0, 0.0001)), '1');
|
||||
assert.strictEqual(String(round_pr(1.0, 0.00001)), '1');
|
||||
assert.strictEqual(String(round_pr(1.0, 0.000001)), '1');
|
||||
assert.strictEqual(String(round_pr(1.0, 0.0000001)), '1');
|
||||
assert.strictEqual(String(round_pr(1.0, 0.00000001)), '1');
|
||||
assert.strictEqual(String(round_pr(0.5, 1)), '1');
|
||||
assert.strictEqual(String(round_pr(-0.5, 1)), '-1');
|
||||
assert.strictEqual(String(round_pr(2.6745, 0.001)), '2.6750000000000003');
|
||||
assert.strictEqual(String(round_pr(-2.6745, 0.001)), '-2.6750000000000003');
|
||||
assert.strictEqual(String(round_pr(2.6744, 0.001)), '2.674');
|
||||
assert.strictEqual(String(round_pr(-2.6744, 0.001)), '-2.674');
|
||||
assert.strictEqual(String(round_pr(0.0004, 0.001)), '0');
|
||||
assert.strictEqual(String(round_pr(-0.0004, 0.001)), '0');
|
||||
assert.strictEqual(String(round_pr(357.4555, 0.001)), '357.456');
|
||||
assert.strictEqual(String(round_pr(-357.4555, 0.001)), '-357.456');
|
||||
assert.strictEqual(String(round_pr(457.4554, 0.001)), '457.455');
|
||||
assert.strictEqual(String(round_pr(-457.4554, 0.001)), '-457.455');
|
||||
assert.strictEqual(String(round_pr(-457.4554, 0.05)), '-457.45000000000005');
|
||||
assert.strictEqual(String(round_pr(457.444, 0.5)), '457.5');
|
||||
assert.strictEqual(String(round_pr(457.3, 5)), '455');
|
||||
assert.strictEqual(String(round_pr(457.5, 5)), '460');
|
||||
assert.strictEqual(String(round_pr(457.1, 3)), '456');
|
||||
});
|
||||
|
||||
QUnit.test('sortBy', function (assert) {
|
||||
assert.expect(29);
|
||||
const { sortBy } = utils;
|
||||
const bools = [true, false, true];
|
||||
const ints = [2, 1, 5];
|
||||
const strs = ['b', 'a', 'z'];
|
||||
const objbools = [{ x: true }, { x: false }, { x: true }];
|
||||
const objints = [{ x: 2 }, { x: 1 }, { x: 5 }];
|
||||
const objstrss = [{ x: 'b' }, { x: 'a' }, { x: 'z' }];
|
||||
|
||||
// Invalid
|
||||
assert.throws(
|
||||
() => sortBy({}),
|
||||
new TypeError(`array.slice is not a function`)
|
||||
);
|
||||
assert.throws(
|
||||
() => sortBy([Symbol('b'), Symbol('a')]),
|
||||
TypeError
|
||||
);
|
||||
assert.throws(
|
||||
() => sortBy(ints, true),
|
||||
new Error(`Expected criterion of type 'string' or 'function' and got 'boolean'`)
|
||||
);
|
||||
assert.throws(
|
||||
() => sortBy(ints, 3),
|
||||
new Error(`Expected criterion of type 'string' or 'function' and got 'number'`)
|
||||
);
|
||||
assert.throws(
|
||||
() => sortBy(ints, {}),
|
||||
new Error(`Expected criterion of type 'string' or 'function' and got 'object'`)
|
||||
);
|
||||
// Do not sort in place
|
||||
const toSort = [2, 3, 1];
|
||||
sortBy(toSort);
|
||||
assert.deepEqual(toSort, [2, 3, 1]);
|
||||
// Sort (no criterion)
|
||||
assert.deepEqual(sortBy([]), []);
|
||||
assert.deepEqual(sortBy(ints), [1, 2, 5]);
|
||||
assert.deepEqual(sortBy(bools), [false, true, true]);
|
||||
assert.deepEqual(sortBy(strs), ['a', 'b', 'z']);
|
||||
assert.deepEqual(sortBy(objbools), [{ x: true }, { x: false }, { x: true }]);
|
||||
assert.deepEqual(sortBy(objints), [{ x: 2 }, { x: 1 }, { x: 5 }]);
|
||||
assert.deepEqual(sortBy(objstrss), [{ x: 'b' }, { x: 'a' }, { x: 'z' }]);
|
||||
// Sort by property
|
||||
const prop = 'x';
|
||||
assert.deepEqual(sortBy([], prop), []);
|
||||
assert.deepEqual(sortBy(ints, prop), [2, 1, 5]);
|
||||
assert.deepEqual(sortBy(bools, prop), [true, false, true]);
|
||||
assert.deepEqual(sortBy(strs, prop), ['b', 'a', 'z']);
|
||||
assert.deepEqual(sortBy(objbools, prop), [{ x: false }, { x: true }, { x: true }]);
|
||||
assert.deepEqual(sortBy(objints, prop), [{ x: 1 }, { x: 2 }, { x: 5 }]);
|
||||
assert.deepEqual(sortBy(objstrss, prop), [{ x: 'a' }, { x: 'b' }, { x: 'z' }]);
|
||||
// Sort by getter
|
||||
const getter = obj => obj.x;
|
||||
assert.deepEqual(sortBy([], getter), []);
|
||||
assert.deepEqual(sortBy(ints, getter), [2, 1, 5]);
|
||||
assert.deepEqual(sortBy(bools, getter), [true, false, true]);
|
||||
assert.deepEqual(sortBy(strs, getter), ['b', 'a', 'z']);
|
||||
assert.deepEqual(sortBy(objbools, getter), [{ x: false }, { x: true }, { x: true }]);
|
||||
assert.deepEqual(sortBy(objints, getter), [{ x: 1 }, { x: 2 }, { x: 5 }]);
|
||||
assert.deepEqual(sortBy(objstrss, getter), [{ x: 'a' }, { x: 'b' }, { x: 'z' }]);
|
||||
// Descending order
|
||||
assert.deepEqual(sortBy(ints, null, 'desc'), [5, 2, 1]);
|
||||
assert.deepEqual(sortBy(objstrss, prop, 'desc'), [{ x: 'z' }, { x: 'b' }, { x: 'a' }]);
|
||||
});
|
||||
|
||||
QUnit.test('getDataURLFromFile handles empty file', async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
const emptyFile = new File([""], "empty.txt", { type: "text/plain" });
|
||||
const dataUrl = await getDataURLFromFile(emptyFile);
|
||||
assert.strictEqual(dataUrl, "data:text/plain;base64,", "dataURL for empty file is not proper");
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
/** @odoo-module alias=@web/../tests/core/utils/nested_sortable_tests default=false */
|
||||
|
||||
import { drag, getFixture } from "@web/../tests/helpers/utils";
|
||||
|
||||
/**
|
||||
* Dragging methods taking into account the fact that it's the top of the
|
||||
* dragged element that triggers the moves (not the position of the cursor),
|
||||
* and the fact that during the first move, the dragged element is replaced by
|
||||
* a placeholder that does not have the same height. The moves are done with
|
||||
* the same x position to prevent triggering horizontal moves.
|
||||
* @param {string} from
|
||||
*/
|
||||
export const sortableDrag = async (from) => {
|
||||
const fixture = getFixture();
|
||||
const fromEl = fixture.querySelector(from);
|
||||
const fromRect = fromEl.getBoundingClientRect();
|
||||
const { drop, moveTo } = await drag(from);
|
||||
let isFirstMove = true;
|
||||
|
||||
/**
|
||||
* @param {string} [targetSelector]
|
||||
*/
|
||||
const moveAbove = async (targetSelector) => {
|
||||
const el = fixture.querySelector(targetSelector);
|
||||
await moveTo(el, {
|
||||
x: fromRect.x - el.getBoundingClientRect().x + fromRect.width / 2,
|
||||
y: fromRect.height / 2 + 5,
|
||||
});
|
||||
isFirstMove = false;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} [targetSelector]
|
||||
*/
|
||||
const moveUnder = async (targetSelector) => {
|
||||
const el = fixture.querySelector(targetSelector);
|
||||
const elRect = el.getBoundingClientRect();
|
||||
let firstMoveBelow = false;
|
||||
if (isFirstMove && elRect.y > fromRect.y) {
|
||||
// Need to consider that the moved element will be replaced by a
|
||||
// placeholder with a height of 5px
|
||||
firstMoveBelow = true;
|
||||
}
|
||||
await moveTo(el, {
|
||||
x: fromRect.x - elRect.x + fromRect.width / 2,
|
||||
y:
|
||||
((firstMoveBelow ? -1 : 1) * fromRect.height) / 2 +
|
||||
elRect.height +
|
||||
(firstMoveBelow ? 4 : -1),
|
||||
});
|
||||
isFirstMove = false;
|
||||
};
|
||||
|
||||
return { moveAbove, moveUnder, drop };
|
||||
};
|
||||
|
|
@ -1,227 +0,0 @@
|
|||
odoo.define('web.basic_fields_mobile_tests', function (require) {
|
||||
"use strict";
|
||||
|
||||
var FormView = require('web.FormView');
|
||||
var ListView = require('web.ListView');
|
||||
var testUtils = require('web.test_utils');
|
||||
|
||||
var createView = testUtils.createView;
|
||||
|
||||
QUnit.module('fields', {}, function () {
|
||||
|
||||
QUnit.module('basic_fields', {
|
||||
beforeEach: function () {
|
||||
this.data = {
|
||||
partner: {
|
||||
fields: {
|
||||
date: {string: "A date", type: "date", searchable: true},
|
||||
datetime: {string: "A datetime", type: "datetime", searchable: true},
|
||||
display_name: {string: "Displayed name", type: "char", searchable: true},
|
||||
foo: {string: "Foo", type: "char", default: "My little Foo Value", searchable: true, trim: true},
|
||||
bar: {string: "Bar", type: "boolean", default: true, searchable: true},
|
||||
int_field: {string: "int_field", type: "integer", sortable: true, searchable: true},
|
||||
qux: {string: "Qux", type: "float", digits: [16,1], searchable: true},
|
||||
},
|
||||
records: [{
|
||||
id: 1,
|
||||
date: "2017-02-03",
|
||||
datetime: "2017-02-08 10:00:00",
|
||||
display_name: "first record",
|
||||
bar: true,
|
||||
foo: "yop",
|
||||
int_field: 10,
|
||||
qux: 0.44444,
|
||||
}, {
|
||||
id: 2,
|
||||
display_name: "second record",
|
||||
bar: true,
|
||||
foo: "blip",
|
||||
int_field: 0,
|
||||
qux: 0,
|
||||
}, {
|
||||
id: 4,
|
||||
display_name: "aaa",
|
||||
foo: "abc",
|
||||
int_field: false,
|
||||
qux: false,
|
||||
}],
|
||||
onchanges: {},
|
||||
},
|
||||
};
|
||||
}
|
||||
}, function () {
|
||||
|
||||
QUnit.module('PhoneWidget');
|
||||
|
||||
QUnit.test('phone field in form view on extra small screens', async function (assert) {
|
||||
assert.expect(7);
|
||||
|
||||
var form = await createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch:'<form string="Partners">' +
|
||||
'<sheet>' +
|
||||
'<group>' +
|
||||
'<field name="foo" widget="phone"/>' +
|
||||
'</group>' +
|
||||
'</sheet>' +
|
||||
'</form>',
|
||||
res_id: 1,
|
||||
});
|
||||
|
||||
var $phoneLink = form.$('div.o_form_uri.o_field_widget.o_field_phone > a');
|
||||
assert.strictEqual($phoneLink.length, 1,
|
||||
"should have a anchor with correct classes");
|
||||
assert.strictEqual($phoneLink.text(), 'yop',
|
||||
"value should be displayed properly");
|
||||
assert.hasAttrValue($phoneLink, 'href', 'tel:yop',
|
||||
"should have proper tel prefix");
|
||||
|
||||
// switch to edit mode and check the result
|
||||
await testUtils.form.clickEdit(form);
|
||||
assert.containsOnce(form, 'input[type="text"].o_field_widget',
|
||||
"should have an int for the phone field");
|
||||
assert.strictEqual(form.$('input[type="text"].o_field_widget').val(), 'yop',
|
||||
"input should contain field value in edit mode");
|
||||
|
||||
// change value in edit mode
|
||||
await testUtils.fields.editInput(form.$('input[type="text"].o_field_widget'), 'new');
|
||||
|
||||
// save
|
||||
await testUtils.form.clickSave(form);
|
||||
$phoneLink = form.$('div.o_form_uri.o_field_widget.o_field_phone > a');
|
||||
assert.strictEqual($phoneLink.text(), 'new',
|
||||
"new value should be displayed properly");
|
||||
assert.hasAttrValue($phoneLink, 'href', 'tel:new',
|
||||
"should still have proper tel prefix");
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('phone field in editable list view on extra small screens', async function (assert) {
|
||||
assert.expect(10);
|
||||
|
||||
var list = await createView({
|
||||
View: ListView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<tree editable="bottom"><field name="foo" widget="phone"/></tree>',
|
||||
});
|
||||
|
||||
assert.containsN(list, '.o_data_row', 3,
|
||||
"should have 3 record");
|
||||
assert.strictEqual(list.$('tbody td:not(.o_list_record_selector) a').first().text(), 'yop',
|
||||
"value should be displayed properly");
|
||||
|
||||
var $phoneLink = list.$('div.o_form_uri.o_field_widget.o_field_phone > a');
|
||||
assert.strictEqual($phoneLink.length, 3,
|
||||
"should have anchors with correct classes");
|
||||
assert.hasAttrValue($phoneLink.first(), 'href', 'tel:yop',
|
||||
"should have proper tel prefix");
|
||||
|
||||
// Edit a line and check the result
|
||||
var $cell = list.$('tbody td:not(.o_list_record_selector)').first();
|
||||
await testUtils.dom.click($cell);
|
||||
assert.hasClass($cell.parent(),'o_selected_row', 'should be set as edit mode');
|
||||
assert.strictEqual($cell.find('input').val(), 'yop',
|
||||
'should have the corect value in internal input');
|
||||
await testUtils.fields.editInput($cell.find('input'), 'new');
|
||||
|
||||
// save
|
||||
await testUtils.dom.click(list.$buttons.find('.o_list_button_save'));
|
||||
$cell = list.$('tbody td:not(.o_list_record_selector)').first();
|
||||
assert.doesNotHaveClass($cell.parent(), 'o_selected_row', 'should not be in edit mode anymore');
|
||||
assert.strictEqual(list.$('tbody td:not(.o_list_record_selector) a').first().text(), 'new',
|
||||
"value should be properly updated");
|
||||
$phoneLink = list.$('div.o_form_uri.o_field_widget.o_field_phone > a');
|
||||
assert.strictEqual($phoneLink.length, 3,
|
||||
"should still have anchors with correct classes");
|
||||
assert.hasAttrValue($phoneLink.first(), 'href', 'tel:new',
|
||||
"should still have proper tel prefix");
|
||||
|
||||
list.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('phone field does not allow html injections', async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
var form = await createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch:'<form string="Partners">' +
|
||||
'<sheet>' +
|
||||
'<group>' +
|
||||
'<field name="foo" widget="phone"/>' +
|
||||
'</group>' +
|
||||
'</sheet>' +
|
||||
'</form>',
|
||||
res_id: 1,
|
||||
viewOptions: {
|
||||
mode: 'edit',
|
||||
},
|
||||
});
|
||||
|
||||
var val = '<script>throw Error();</script><script>throw Error();</script>';
|
||||
await testUtils.fields.editInput(form.$('input.o_field_widget[name="foo"]'), val);
|
||||
|
||||
// save
|
||||
await testUtils.form.clickSave(form);
|
||||
assert.strictEqual(form.$('.o_field_widget').text(), val,
|
||||
"value should have been correctly escaped");
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.module('FieldDateRange');
|
||||
|
||||
QUnit.test('date field: toggle daterangepicker then scroll', async function (assert) {
|
||||
assert.expect(4);
|
||||
const scrollEvent = new UIEvent('scroll');
|
||||
|
||||
function scrollAtHeight(height) {
|
||||
window.scrollTo(0, height);
|
||||
document.dispatchEvent(scrollEvent);
|
||||
}
|
||||
this.data.partner.fields.date_end = {string: 'Date End', type: 'date'};
|
||||
|
||||
var form = await createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<form>' +
|
||||
'<field name="date" widget="daterange" options="{\'related_end_date\': \'date_end\'}"/>' +
|
||||
'<field name="date_end" widget="daterange" options="{\'related_start_date\': \'date\'}"/>' +
|
||||
'</form>',
|
||||
session: {
|
||||
getTZOffset: function () {
|
||||
return 330;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Check date range picker initialization
|
||||
assert.containsN(document.body, '.daterangepicker', 2,
|
||||
"should initialize 2 date range picker");
|
||||
|
||||
// Open date range picker
|
||||
await testUtils.dom.click("input[name=date]");
|
||||
assert.isVisible($('.daterangepicker:first'),
|
||||
"date range picker should be opened");
|
||||
|
||||
// Scroll
|
||||
scrollAtHeight(50);
|
||||
assert.isVisible($('.daterangepicker:first'),
|
||||
"date range picker should be opened");
|
||||
|
||||
// Close picker
|
||||
await testUtils.dom.click($('.daterangepicker:first .cancelBtn'));
|
||||
assert.isNotVisible($('.daterangepicker:first'),
|
||||
"date range picker should be closed");
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,451 +0,0 @@
|
|||
odoo.define('web.field_utils_tests', function (require) {
|
||||
"use strict";
|
||||
|
||||
var core = require('web.core');
|
||||
var session = require('web.session');
|
||||
var fieldUtils = require('web.field_utils');
|
||||
|
||||
QUnit.module('fields', {}, function () {
|
||||
|
||||
QUnit.module('field_utils');
|
||||
|
||||
QUnit.test('format integer', function(assert) {
|
||||
assert.expect(5);
|
||||
|
||||
var originalGrouping = core._t.database.parameters.grouping;
|
||||
|
||||
core._t.database.parameters.grouping = [3, 3, 3, 3];
|
||||
assert.strictEqual(fieldUtils.format.integer(1000000), '1,000,000');
|
||||
|
||||
core._t.database.parameters.grouping = [3, 2, -1];
|
||||
assert.strictEqual(fieldUtils.format.integer(106500), '1,06,500');
|
||||
|
||||
core._t.database.parameters.grouping = [1, 2, -1];
|
||||
assert.strictEqual(fieldUtils.format.integer(106500), '106,50,0');
|
||||
|
||||
assert.strictEqual(fieldUtils.format.integer(0), "0");
|
||||
assert.strictEqual(fieldUtils.format.integer(false), "");
|
||||
|
||||
core._t.database.parameters.grouping = originalGrouping;
|
||||
});
|
||||
|
||||
QUnit.test('format float', function(assert) {
|
||||
assert.expect(5);
|
||||
|
||||
var originalParameters = $.extend(true, {}, core._t.database.parameters);
|
||||
|
||||
core._t.database.parameters.grouping = [3, 3, 3, 3];
|
||||
assert.strictEqual(fieldUtils.format.float(1000000), '1,000,000.00');
|
||||
|
||||
core._t.database.parameters.grouping = [3, 2, -1];
|
||||
assert.strictEqual(fieldUtils.format.float(106500), '1,06,500.00');
|
||||
|
||||
core._t.database.parameters.grouping = [1, 2, -1];
|
||||
assert.strictEqual(fieldUtils.format.float(106500), '106,50,0.00');
|
||||
|
||||
_.extend(core._t.database.parameters, {
|
||||
grouping: [3, 0],
|
||||
decimal_point: ',',
|
||||
thousands_sep: '.'
|
||||
});
|
||||
assert.strictEqual(fieldUtils.format.float(6000), '6.000,00');
|
||||
assert.strictEqual(fieldUtils.format.float(false), '');
|
||||
|
||||
core._t.database.parameters = originalParameters;
|
||||
});
|
||||
|
||||
QUnit.test("format_datetime", function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
var date_string = "2009-05-04 12:34:23";
|
||||
var date = fieldUtils.parse.datetime(date_string, {}, {timezone: false});
|
||||
var str = fieldUtils.format.datetime(date, {}, {timezone: false});
|
||||
assert.strictEqual(str, moment(date).format("MM/DD/YYYY HH:mm:ss"));
|
||||
});
|
||||
|
||||
QUnit.test("format_datetime (with different timezone offset)", function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
// mock the date format to avoid issues due to localisation
|
||||
var dateFormat = core._t.database.parameters.date_format;
|
||||
core._t.database.parameters.date_format = '%m/%d/%Y';
|
||||
session.getTZOffset = function (date) {
|
||||
// simulate daylight saving time
|
||||
var startDate = new Date(2017, 2, 26);
|
||||
var endDate = new Date(2017, 9, 29);
|
||||
if (startDate < date && date < endDate) {
|
||||
return 120; // UTC+2
|
||||
} else {
|
||||
return 60; // UTC+1
|
||||
}
|
||||
};
|
||||
|
||||
var str = fieldUtils.format.datetime(moment.utc('2017-01-01T10:00:00Z'));
|
||||
assert.strictEqual(str, '01/01/2017 11:00:00');
|
||||
str = fieldUtils.format.datetime(moment.utc('2017-06-01T10:00:00Z'));
|
||||
assert.strictEqual(str, '06/01/2017 12:00:00');
|
||||
|
||||
core._t.database.parameters.date_format = dateFormat;
|
||||
});
|
||||
|
||||
QUnit.test("format_many2one", function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
assert.strictEqual('', fieldUtils.format.many2one(null));
|
||||
assert.strictEqual('A M2O value', fieldUtils.format.many2one({
|
||||
data: { display_name: 'A M2O value' },
|
||||
}));
|
||||
});
|
||||
|
||||
QUnit.test('format monetary', function(assert) {
|
||||
assert.expect(1);
|
||||
|
||||
assert.strictEqual(fieldUtils.format.monetary(false), '');
|
||||
});
|
||||
|
||||
QUnit.test('format char', function(assert) {
|
||||
assert.expect(1);
|
||||
|
||||
assert.strictEqual(fieldUtils.format.char(), '',
|
||||
"undefined char should be formatted as an empty string");
|
||||
});
|
||||
|
||||
QUnit.test('format many2many', function(assert) {
|
||||
assert.expect(3);
|
||||
|
||||
assert.strictEqual(fieldUtils.format.many2many({data: []}), 'No records');
|
||||
assert.strictEqual(fieldUtils.format.many2many({data: [1]}), '1 record');
|
||||
assert.strictEqual(fieldUtils.format.many2many({data: [1, 2]}), '2 records');
|
||||
});
|
||||
|
||||
QUnit.test('format one2many', function(assert) {
|
||||
assert.expect(3);
|
||||
|
||||
assert.strictEqual(fieldUtils.format.one2many({data: []}), 'No records');
|
||||
assert.strictEqual(fieldUtils.format.one2many({data: [1]}), '1 record');
|
||||
assert.strictEqual(fieldUtils.format.one2many({data: [1, 2]}), '2 records');
|
||||
});
|
||||
|
||||
QUnit.test('format binary', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
// base64 estimated size (bytes) = value.length / 1.37 (http://en.wikipedia.org/wiki/Base64#MIME)
|
||||
// Here: 4 / 1.37 = 2.91970800 => 2.92 (rounded 2 decimals by utils.human_size)
|
||||
assert.strictEqual(fieldUtils.format.binary('Cg=='), '2.92 Bytes');
|
||||
});
|
||||
|
||||
QUnit.test('format percentage', function (assert) {
|
||||
assert.expect(12);
|
||||
|
||||
var originalParameters = _.clone(core._t.database.parameters);
|
||||
|
||||
assert.strictEqual(fieldUtils.format.percentage(0), '0%');
|
||||
assert.strictEqual(fieldUtils.format.percentage(0.5), '50%');
|
||||
assert.strictEqual(fieldUtils.format.percentage(1), '100%');
|
||||
|
||||
assert.strictEqual(fieldUtils.format.percentage(-0.2), '-20%');
|
||||
assert.strictEqual(fieldUtils.format.percentage(2.5), '250%');
|
||||
|
||||
assert.strictEqual(fieldUtils.format.percentage(0.125), '12.5%');
|
||||
assert.strictEqual(fieldUtils.format.percentage(0.666666), '66.67%');
|
||||
|
||||
assert.strictEqual(fieldUtils.format.percentage(false), '0%');
|
||||
assert.strictEqual(fieldUtils.format.percentage(50, null,
|
||||
{humanReadable: function (val) {return true;}}), '5k%'
|
||||
);
|
||||
|
||||
_.extend(core._t.database.parameters, {
|
||||
grouping: [3, 0],
|
||||
decimal_point: ',',
|
||||
thousands_sep: '.'
|
||||
});
|
||||
assert.strictEqual(fieldUtils.format.percentage(0.125), '12,5%');
|
||||
assert.strictEqual(fieldUtils.format.percentage(0.666666), '66,67%');
|
||||
assert.strictEqual(fieldUtils.format.percentage(0.5, null, { noSymbol: true }), '50');
|
||||
|
||||
core._t.database.parameters = originalParameters;
|
||||
});
|
||||
|
||||
QUnit.test('format float time', function (assert) {
|
||||
assert.expect(7);
|
||||
|
||||
assert.strictEqual(fieldUtils.format.float_time(2), '02:00');
|
||||
assert.strictEqual(fieldUtils.format.float_time(3.5), '03:30');
|
||||
assert.strictEqual(fieldUtils.format.float_time(0.25), '00:15');
|
||||
|
||||
assert.strictEqual(fieldUtils.format.float_time(-0.5), '-00:30');
|
||||
|
||||
const options = {
|
||||
noLeadingZeroHour: true,
|
||||
};
|
||||
assert.strictEqual(fieldUtils.format.float_time(2, null, options), '2:00');
|
||||
assert.strictEqual(fieldUtils.format.float_time(3.5, null, options), '3:30');
|
||||
assert.strictEqual(fieldUtils.format.float_time(-0.5, null, options), '-0:30');
|
||||
});
|
||||
|
||||
QUnit.test('parse float', function(assert) {
|
||||
assert.expect(10);
|
||||
|
||||
var originalParameters = _.clone(core._t.database.parameters);
|
||||
|
||||
_.extend(core._t.database.parameters, {
|
||||
grouping: [3, 0],
|
||||
decimal_point: '.',
|
||||
thousands_sep: ','
|
||||
});
|
||||
|
||||
assert.strictEqual(fieldUtils.parse.float(""), 0);
|
||||
assert.strictEqual(fieldUtils.parse.float("0"), 0);
|
||||
assert.strictEqual(fieldUtils.parse.float("100.00"), 100);
|
||||
assert.strictEqual(fieldUtils.parse.float("-100.00"), -100);
|
||||
assert.strictEqual(fieldUtils.parse.float("1,000.00"), 1000);
|
||||
assert.strictEqual(fieldUtils.parse.float("1,000,000.00"), 1000000);
|
||||
assert.strictEqual(fieldUtils.parse.float('1,234.567'), 1234.567);
|
||||
assert.throws(function () {
|
||||
fieldUtils.parse.float("1.000.000");
|
||||
}, "Throw an exception if it's not a valid number");
|
||||
|
||||
_.extend(core._t.database.parameters, {
|
||||
grouping: [3, 0],
|
||||
decimal_point: ',',
|
||||
thousands_sep: '.'
|
||||
});
|
||||
|
||||
assert.strictEqual(fieldUtils.parse.float('1.234,567'), 1234.567);
|
||||
assert.throws(function () {
|
||||
fieldUtils.parse.float("1,000,000");
|
||||
}, "Throw an exception if it's not a valid number");
|
||||
|
||||
_.extend(core._t.database.parameters, originalParameters);
|
||||
});
|
||||
|
||||
QUnit.test('parse integer', function(assert) {
|
||||
assert.expect(11);
|
||||
|
||||
var originalParameters = _.clone(core._t.database.parameters);
|
||||
|
||||
_.extend(core._t.database.parameters, {
|
||||
grouping: [3, 0],
|
||||
decimal_point: '.',
|
||||
thousands_sep: ','
|
||||
});
|
||||
|
||||
assert.strictEqual(fieldUtils.parse.integer(""), 0);
|
||||
assert.strictEqual(fieldUtils.parse.integer("0"), 0);
|
||||
assert.strictEqual(fieldUtils.parse.integer("100"), 100);
|
||||
assert.strictEqual(fieldUtils.parse.integer("-100"), -100);
|
||||
assert.strictEqual(fieldUtils.parse.integer("1,000"), 1000);
|
||||
assert.strictEqual(fieldUtils.parse.integer("1,000,000"), 1000000);
|
||||
assert.throws(function () {
|
||||
fieldUtils.parse.integer("1.000.000");
|
||||
}, "Throw an exception if it's not a valid number");
|
||||
assert.throws(function () {
|
||||
fieldUtils.parse.integer("1,234.567");
|
||||
}, "Throw an exception if the number is a float");
|
||||
|
||||
_.extend(core._t.database.parameters, {
|
||||
grouping: [3, 0],
|
||||
decimal_point: ',',
|
||||
thousands_sep: '.'
|
||||
});
|
||||
|
||||
assert.strictEqual(fieldUtils.parse.integer("1.000.000"), 1000000);
|
||||
assert.throws(function () {
|
||||
fieldUtils.parse.integer("1,000,000");
|
||||
}, "Throw an exception if it's not a valid number");
|
||||
assert.throws(function () {
|
||||
fieldUtils.parse.integer("1.234,567");
|
||||
}, "Throw an exception if the number is a float");
|
||||
|
||||
_.extend(core._t.database.parameters, originalParameters);
|
||||
});
|
||||
|
||||
QUnit.test('parse monetary', function(assert) {
|
||||
assert.expect(15);
|
||||
var originalCurrencies = session.currencies;
|
||||
const originalParameters = _.clone(core._t.database.parameters);
|
||||
session.currencies = {
|
||||
1: {
|
||||
digits: [69, 2],
|
||||
position: "after",
|
||||
symbol: "€"
|
||||
},
|
||||
3: {
|
||||
digits: [69, 2],
|
||||
position: "before",
|
||||
symbol: "$"
|
||||
}
|
||||
};
|
||||
|
||||
assert.strictEqual(fieldUtils.parse.monetary(""), 0);
|
||||
assert.strictEqual(fieldUtils.parse.monetary("0"), 0);
|
||||
assert.strictEqual(fieldUtils.parse.monetary("100.00"), 100);
|
||||
assert.strictEqual(fieldUtils.parse.monetary("-100.00"), -100);
|
||||
assert.strictEqual(fieldUtils.parse.monetary("1,000.00"), 1000);
|
||||
assert.strictEqual(fieldUtils.parse.monetary("1,000,000.00"), 1000000);
|
||||
assert.strictEqual(fieldUtils.parse.monetary("$ 125.00", {}, {currency_id: 3}), 125);
|
||||
assert.strictEqual(fieldUtils.parse.monetary("1,000.00 €", {}, {currency_id: 1}), 1000);
|
||||
assert.throws(function() {fieldUtils.parse.monetary("$ 12.00", {}, {currency_id: 3})}, /is not a correct/);
|
||||
assert.throws(function() {fieldUtils.parse.monetary("$ 12.00", {}, {currency_id: 1})}, /is not a correct/);
|
||||
assert.throws(function() {fieldUtils.parse.monetary("$ 12.00 34", {}, {currency_id: 3})}, /is not a correct/);
|
||||
|
||||
// In some languages, the non-breaking space character is used as thousands separator.
|
||||
const nbsp = '\u00a0';
|
||||
_.extend(core._t.database.parameters, {
|
||||
grouping: [3, 0],
|
||||
decimal_point: '.',
|
||||
thousands_sep: nbsp,
|
||||
});
|
||||
assert.strictEqual(fieldUtils.parse.monetary(`1${nbsp}000.00${nbsp}€`, {}, {currency_id: 1}), 1000);
|
||||
assert.strictEqual(fieldUtils.parse.monetary(`$${nbsp}1${nbsp}000.00`, {}, {currency_id: 3}), 1000);
|
||||
assert.strictEqual(fieldUtils.parse.monetary(`1${nbsp}000.00`), 1000);
|
||||
assert.strictEqual(fieldUtils.parse.monetary(`1${nbsp}000${nbsp}000.00`), 1000000);
|
||||
|
||||
session.currencies = originalCurrencies;
|
||||
core._t.database.parameters = originalParameters;
|
||||
});
|
||||
|
||||
QUnit.test('parse percentage', function(assert) {
|
||||
assert.expect(7);
|
||||
|
||||
var originalParameters = _.clone(core._t.database.parameters);
|
||||
|
||||
assert.strictEqual(fieldUtils.parse.percentage(""), 0);
|
||||
assert.strictEqual(fieldUtils.parse.percentage("0"), 0);
|
||||
assert.strictEqual(fieldUtils.parse.percentage("0.5"), 0.005);
|
||||
assert.strictEqual(fieldUtils.parse.percentage("1"), 0.01);
|
||||
assert.strictEqual(fieldUtils.parse.percentage("100"), 1);
|
||||
|
||||
_.extend(core._t.database.parameters, {
|
||||
grouping: [3, 0],
|
||||
decimal_point: ',',
|
||||
thousands_sep: '.'
|
||||
});
|
||||
|
||||
assert.strictEqual(fieldUtils.parse.percentage("1.234,56"), 12.3456);
|
||||
assert.strictEqual(fieldUtils.parse.percentage("6,02"), 0.0602);
|
||||
|
||||
core._t.database.parameters = originalParameters;
|
||||
|
||||
});
|
||||
|
||||
QUnit.test('parse datetime', function (assert) {
|
||||
assert.expect(7);
|
||||
|
||||
var originalParameters = _.clone(core._t.database.parameters);
|
||||
var originalLocale = moment.locale();
|
||||
var dateStr, date1, date2;
|
||||
|
||||
moment.defineLocale('englishForTest', {
|
||||
dayOfMonthOrdinalParse: /\d{1,2}(st|nd|rd|th)/,
|
||||
ordinal: function (number) {
|
||||
var b = number % 10,
|
||||
output = (~~(number % 100 / 10) === 1) ? 'th' :
|
||||
(b === 1) ? 'st' :
|
||||
(b === 2) ? 'nd' :
|
||||
(b === 3) ? 'rd' : 'th';
|
||||
return number + output;
|
||||
},
|
||||
});
|
||||
|
||||
moment.defineLocale('norvegianForTest', {
|
||||
monthsShort: 'jan._feb._mars_april_mai_juni_juli_aug._sep._okt._nov._des.'.split('_'),
|
||||
monthsParseExact: true,
|
||||
dayOfMonthOrdinalParse: /\d{1,2}\./,
|
||||
ordinal: '%d.',
|
||||
});
|
||||
|
||||
moment.locale('englishForTest');
|
||||
_.extend(core._t.database.parameters, {date_format: '%m/%d/%Y', time_format: '%H:%M:%S'});
|
||||
assert.throws(function () {
|
||||
fieldUtils.parse.datetime("13/01/2019 12:00:00", {}, {});
|
||||
}, /is not a correct/, "Wrongly formated dates should be invalid");
|
||||
assert.throws(function () {
|
||||
fieldUtils.parse.datetime("10000-01-01 12:00:00", {}, {});
|
||||
}, /is not a correct/, "Dates after 9999 should be invalid");
|
||||
assert.throws(function () {
|
||||
fieldUtils.parse.datetime("999-01-01 12:00:00", {}, {});
|
||||
}, /is not a correct/, "Dates before 1000 should be invalid");
|
||||
|
||||
dateStr = '01/13/2019 10:05:45';
|
||||
date1 = fieldUtils.parse.datetime(dateStr);
|
||||
date2 = moment.utc(dateStr, ['MM/DD/YYYY HH:mm:ss'], true);
|
||||
assert.equal(date1.format(), date2.format(), "Date with leading 0");
|
||||
|
||||
dateStr = '1/14/2019 10:5:45';
|
||||
date1 = fieldUtils.parse.datetime(dateStr);
|
||||
date2 = moment.utc(dateStr, ['M/D/YYYY H:m:s'], true);
|
||||
assert.equal(date1.format(), date2.format(), "Date without leading 0");
|
||||
|
||||
dateStr = '01/01/1000 10:15:45';
|
||||
date1 = fieldUtils.parse.datetime(dateStr);
|
||||
date2 = moment.utc(dateStr, ['MM/DD/YYYY HH:mm:ss'], true);
|
||||
assert.equal(date1.format(), date2.format(), "can parse dates of year 1");
|
||||
|
||||
moment.locale('norvegianForTest');
|
||||
_.extend(core._t.database.parameters, {date_format: '%d. %b %Y', time_format: '%H:%M:%S'});
|
||||
dateStr = '16. jan. 2019 10:05:45';
|
||||
date1 = fieldUtils.parse.datetime(dateStr);
|
||||
date2 = moment.utc(dateStr, ['DD. MMM YYYY HH:mm:ss'], true);
|
||||
assert.equal(date1.format(), date2.format(), "Day/month inverted + month i18n");
|
||||
|
||||
moment.locale(originalLocale);
|
||||
moment.updateLocale("englishForTest", null);
|
||||
moment.updateLocale("norvegianForTest", null);
|
||||
core._t.database.parameters = originalParameters;
|
||||
});
|
||||
|
||||
QUnit.test('parse date without separator', function (assert) {
|
||||
assert.expect(8);
|
||||
|
||||
var originalParameters = _.clone(core._t.database.parameters);
|
||||
|
||||
_.extend(core._t.database.parameters, {date_format: '%d.%m/%Y'});
|
||||
var dateFormat = "DD.MM/YYYY";
|
||||
|
||||
assert.throws(function () {fieldUtils.parse.date("1197")}, /is not a correct/, "Wrongly formated dates should be invalid");
|
||||
assert.throws(function () {fieldUtils.parse.date("0131")}, /is not a correct/, "Wrongly formated dates should be invalid");
|
||||
assert.throws(function () {fieldUtils.parse.date("970131")}, /is not a correct/, "Wrongly formated dates should be invalid");
|
||||
assert.equal(fieldUtils.parse.date("3101").format(dateFormat), "31.01/" + moment.utc().year());
|
||||
assert.equal(fieldUtils.parse.date("31.01").format(dateFormat), "31.01/" + moment.utc().year());
|
||||
assert.equal(fieldUtils.parse.date("310197").format(dateFormat), "31.01/1997");
|
||||
assert.equal(fieldUtils.parse.date("310117").format(dateFormat), "31.01/2017");
|
||||
assert.equal(fieldUtils.parse.date("31011985").format(dateFormat), "31.01/1985");
|
||||
|
||||
core._t.database.parameters = originalParameters;
|
||||
});
|
||||
|
||||
QUnit.test('parse datetime without separator', function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
var originalParameters = _.clone(core._t.database.parameters);
|
||||
|
||||
_.extend(core._t.database.parameters, {date_format: '%d.%m/%Y', time_format: '%H:%M/%S'});
|
||||
var dateTimeFormat = "DD.MM/YYYY HH:mm/ss";
|
||||
|
||||
assert.equal(fieldUtils.parse.datetime("3101198508").format(dateTimeFormat), "31.01/1985 08:00/00");
|
||||
assert.equal(fieldUtils.parse.datetime("310119850833").format(dateTimeFormat), "31.01/1985 08:33/00");
|
||||
assert.equal(fieldUtils.parse.datetime("31/01/1985 08").format(dateTimeFormat), "31.01/1985 08:00/00");
|
||||
|
||||
core._t.database.parameters = originalParameters;
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test('parse smart date input', function (assert) {
|
||||
assert.expect(10);
|
||||
|
||||
const format = "DD MM YYYY";
|
||||
assert.strictEqual(fieldUtils.parse.date("+1d").format(format), moment().add(1, 'days').format(format));
|
||||
assert.strictEqual(fieldUtils.parse.datetime("+2w").format(format), moment().add(2, 'weeks').format(format));
|
||||
assert.strictEqual(fieldUtils.parse.date("+3m").format(format), moment().add(3, 'months').format(format));
|
||||
assert.strictEqual(fieldUtils.parse.datetime("+4y").format(format), moment().add(4, 'years').format(format));
|
||||
|
||||
assert.strictEqual(fieldUtils.parse.date("+5").format(format), moment().add(5, 'days').format(format));
|
||||
assert.strictEqual(fieldUtils.parse.datetime("-5").format(format), moment().subtract(5, 'days').format(format));
|
||||
|
||||
assert.strictEqual(fieldUtils.parse.date("-4y").format(format), moment().subtract(4, 'years').format(format));
|
||||
assert.strictEqual(fieldUtils.parse.datetime("-3m").format(format), moment().subtract(3, 'months').format(format));
|
||||
assert.strictEqual(fieldUtils.parse.date("-2w").format(format), moment().subtract(2, 'weeks').format(format));
|
||||
assert.strictEqual(fieldUtils.parse.datetime("-1d").format(format), moment().subtract(1, 'days').format(format));
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -1,66 +0,0 @@
|
|||
odoo.define("web.relational_fields_mobile_tests", function (require) {
|
||||
"use strict";
|
||||
|
||||
const FormView = require("web.FormView");
|
||||
const testUtils = require("web.test_utils");
|
||||
|
||||
QUnit.module("fields", {}, function () {
|
||||
QUnit.module("relational_fields", {
|
||||
beforeEach() {
|
||||
this.data = {
|
||||
partner: {
|
||||
fields: {
|
||||
display_name: { string: "Displayed name", type: "char" },
|
||||
p: {string: "one2many field", type: "one2many", relation: "partner", relation_field: "trululu"},
|
||||
trululu: {string: "Trululu", type: "many2one", relation: "partner"},
|
||||
},
|
||||
records: [{
|
||||
id: 1,
|
||||
display_name: "first record",
|
||||
p: [2, 4],
|
||||
trululu: 4,
|
||||
}, {
|
||||
id: 2,
|
||||
display_name: "second record",
|
||||
p: [],
|
||||
trululu: 1,
|
||||
}, {
|
||||
id: 4,
|
||||
display_name: "aaa",
|
||||
}],
|
||||
},
|
||||
};
|
||||
},
|
||||
}, function () {
|
||||
QUnit.module("FieldOne2Many");
|
||||
|
||||
QUnit.test("one2many on mobile: display list if present without kanban view", async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
const form = await testUtils.createView({
|
||||
View: FormView,
|
||||
model: "partner",
|
||||
data: this.data,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="p">
|
||||
<tree>
|
||||
<field name="display_name"/>
|
||||
</tree>
|
||||
</field>
|
||||
</form>
|
||||
`,
|
||||
res_id: 1,
|
||||
});
|
||||
|
||||
await testUtils.form.clickEdit(form);
|
||||
assert.containsOnce(form, ".o_field_x2many_list",
|
||||
"should display one2many's list");
|
||||
assert.containsN(form, ".o_field_x2many_list .o_data_row", 2,
|
||||
"should display 2 records in one2many's list");
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,208 +0,0 @@
|
|||
odoo.define('web.signature_field_tests', function (require) {
|
||||
"use strict";
|
||||
|
||||
var FormView = require('web.FormView');
|
||||
var testUtils = require('web.test_utils');
|
||||
|
||||
var createView = testUtils.createView;
|
||||
|
||||
QUnit.module('fields', {}, function () {
|
||||
|
||||
QUnit.module('signature legacy', {
|
||||
beforeEach: function () {
|
||||
this.data = {
|
||||
partner: {
|
||||
fields: {
|
||||
display_name: {string: "Name", type: "char" },
|
||||
product_id: {string: "Product Name", type: "many2one", relation: 'product'},
|
||||
sign: {string: "Signature", type: "binary"},
|
||||
},
|
||||
records: [{
|
||||
id: 1,
|
||||
display_name: "Pop's Chock'lit",
|
||||
product_id: 7,
|
||||
}],
|
||||
onchanges: {},
|
||||
},
|
||||
product: {
|
||||
fields: {
|
||||
name: {string: "Product Name", type: "char"}
|
||||
},
|
||||
records: [{
|
||||
id: 7,
|
||||
display_name: "Veggie Burger",
|
||||
}]
|
||||
},
|
||||
};
|
||||
}
|
||||
}, function () {
|
||||
QUnit.test('Set simple field in "full_name" node option', async function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
var form = await createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
res_id: 1,
|
||||
data: this.data,
|
||||
arch: '<form>' +
|
||||
'<field name="display_name"/>' +
|
||||
'<field name="sign" widget="signature" options="{\'full_name\': \'display_name\'}" />' +
|
||||
'</form>',
|
||||
mockRPC: function (route, args) {
|
||||
if (route === '/web/sign/get_fonts/') {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return this._super(route, args);
|
||||
},
|
||||
});
|
||||
|
||||
await testUtils.form.clickEdit(form);
|
||||
|
||||
assert.containsOnce(form, 'div[name=sign] div.o_signature svg',
|
||||
"should have a valid signature widget");
|
||||
// Click on the widget to open signature modal
|
||||
await testUtils.dom.click(form.$('div[name=sign] div.o_signature'));
|
||||
assert.strictEqual($('.modal .modal-body a.o_web_sign_auto_button').length, 1,
|
||||
'should open a modal with "Auto" button');
|
||||
assert.strictEqual($('.modal .modal-body .o_web_sign_name_input').val(), "Pop's Chock'lit",
|
||||
'Correct Value should be set in the input for auto drawing the signature');
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('Set m2o field in "full_name" node option', async function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
var form = await createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
res_id: 1,
|
||||
data: this.data,
|
||||
arch: '<form>' +
|
||||
'<field name="product_id"/>' +
|
||||
'<field name="sign" widget="signature" options="{\'full_name\': \'product_id\'}" />' +
|
||||
'</form>',
|
||||
mockRPC: function (route, args) {
|
||||
if (route === '/web/sign/get_fonts/') {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return this._super(route, args);
|
||||
},
|
||||
});
|
||||
|
||||
await testUtils.form.clickEdit(form);
|
||||
|
||||
assert.containsOnce(form, 'div[name=sign] div.o_signature svg',
|
||||
"should have a valid signature widget");
|
||||
// Click on the widget to open signature modal
|
||||
await testUtils.dom.click(form.$('div[name=sign] div.o_signature'));
|
||||
assert.strictEqual($('.modal .modal-body a.o_web_sign_auto_button').length, 1,
|
||||
'should open a modal with "Auto" button');
|
||||
assert.strictEqual($('.modal .modal-body .o_web_sign_name_input').val(), "Veggie Burger",
|
||||
'Correct Value should be set in the input for auto drawing the signature');
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.module('Signature Widget');
|
||||
|
||||
QUnit.test('Signature widget renders a Sign button', async function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
const form = await createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
res_id: 1,
|
||||
data: this.data,
|
||||
arch: '<form>' +
|
||||
'<header>' +
|
||||
'<widget name="signature" string="Sign"/>' +
|
||||
'</header>' +
|
||||
'</form>',
|
||||
mockRPC: function (route, args) {
|
||||
if (route === '/web/sign/get_fonts/') {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return this._super(route, args);
|
||||
},
|
||||
});
|
||||
|
||||
assert.containsOnce(form, 'button.o_sign_button.o_widget',
|
||||
"Should have a signature widget button");
|
||||
assert.strictEqual($('.modal-dialog').length, 0,
|
||||
"Should not have any modal");
|
||||
// Clicks on the sign button to open the sign modal.
|
||||
await testUtils.dom.click(form.$('span.o_sign_label'));
|
||||
assert.strictEqual($('.modal-dialog').length, 1,
|
||||
"Should have one modal opened");
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('Signature widget: full_name option', async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
const form = await createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
res_id: 1,
|
||||
data: this.data,
|
||||
arch: '<form>' +
|
||||
'<header>' +
|
||||
'<widget name="signature" string="Sign" full_name="display_name"/>' +
|
||||
'</header>' +
|
||||
'<field name="display_name"/>' +
|
||||
'</form>',
|
||||
mockRPC: function (route, args) {
|
||||
if (route === '/web/sign/get_fonts/') {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return this._super(route, args);
|
||||
},
|
||||
});
|
||||
|
||||
// Clicks on the sign button to open the sign modal.
|
||||
await testUtils.dom.click(form.$('span.o_sign_label'));
|
||||
assert.strictEqual($('.modal .modal-body a.o_web_sign_auto_button').length, 1,
|
||||
"Should open a modal with \"Auto\" button");
|
||||
assert.strictEqual($('.modal .modal-body .o_web_sign_name_input').val(), "Pop's Chock'lit",
|
||||
"Correct Value should be set in the input for auto drawing the signature");
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('Signature widget: highlight option', async function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
const form = await createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
res_id: 1,
|
||||
data: this.data,
|
||||
arch: '<form>' +
|
||||
'<header>' +
|
||||
'<widget name="signature" string="Sign" highlight="1"/>' +
|
||||
'</header>' +
|
||||
'</form>',
|
||||
mockRPC: function (route, args) {
|
||||
if (route === '/web/sign/get_fonts/') {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return this._super(route, args);
|
||||
},
|
||||
});
|
||||
|
||||
assert.hasClass(form.$('button.o_sign_button.o_widget'), 'btn-primary',
|
||||
"The button must have the 'btn-primary' class as \"highlight=1\"");
|
||||
// Clicks on the sign button to open the sign modal.
|
||||
await testUtils.dom.click(form.$('span.o_sign_label'));
|
||||
assert.isNotVisible($('.modal .modal-body a.o_web_sign_auto_button'),
|
||||
"\"Auto\" button must be invisible");
|
||||
assert.strictEqual($('.modal .modal-body .o_web_sign_name_input').val(), '',
|
||||
"No value should be set in the input for auto drawing the signature");
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,314 +0,0 @@
|
|||
odoo.define('web.special_fields_tests', function (require) {
|
||||
"use strict";
|
||||
|
||||
var FormView = require('web.FormView');
|
||||
var ListView = require('web.ListView');
|
||||
var testUtils = require('web.test_utils');
|
||||
|
||||
var createView = testUtils.createView;
|
||||
|
||||
QUnit.module('fields', {}, function () {
|
||||
|
||||
QUnit.module('special_fields', {
|
||||
beforeEach: function () {
|
||||
this.data = {
|
||||
partner: {
|
||||
fields: {
|
||||
display_name: { string: "Displayed name", type: "char" },
|
||||
foo: {string: "Foo", type: "char", default: "My little Foo Value"},
|
||||
bar: {string: "Bar", type: "boolean", default: true},
|
||||
int_field: {string: "int_field", type: "integer", sortable: true},
|
||||
qux: {string: "Qux", type: "float", digits: [16,1] },
|
||||
p: {string: "one2many field", type: "one2many", relation: 'partner', relation_field: 'trululu'},
|
||||
turtles: {string: "one2many turtle field", type: "one2many", relation: 'turtle'},
|
||||
trululu: {string: "Trululu", type: "many2one", relation: 'partner'},
|
||||
timmy: { string: "pokemon", type: "many2many", relation: 'partner_type'},
|
||||
product_id: {string: "Product", type: "many2one", relation: 'product'},
|
||||
color: {
|
||||
type: "selection",
|
||||
selection: [['red', "Red"], ['black', "Black"]],
|
||||
default: 'red',
|
||||
},
|
||||
date: {string: "Some Date", type: "date"},
|
||||
datetime: {string: "Datetime Field", type: 'datetime'},
|
||||
user_id: {string: "User", type: 'many2one', relation: 'user'},
|
||||
},
|
||||
records: [{
|
||||
id: 1,
|
||||
display_name: "first record",
|
||||
bar: true,
|
||||
foo: "yop",
|
||||
int_field: 10,
|
||||
qux: 0.44,
|
||||
p: [],
|
||||
turtles: [2],
|
||||
timmy: [],
|
||||
trululu: 4,
|
||||
user_id: 17,
|
||||
}, {
|
||||
id: 2,
|
||||
display_name: "second record",
|
||||
bar: true,
|
||||
foo: "blip",
|
||||
int_field: 9,
|
||||
qux: 13,
|
||||
p: [],
|
||||
timmy: [],
|
||||
trululu: 1,
|
||||
product_id: 37,
|
||||
date: "2017-01-25",
|
||||
datetime: "2016-12-12 10:55:05",
|
||||
user_id: 17,
|
||||
}, {
|
||||
id: 4,
|
||||
display_name: "aaa",
|
||||
bar: false,
|
||||
}],
|
||||
onchanges: {},
|
||||
},
|
||||
product: {
|
||||
fields: {
|
||||
name: {string: "Product Name", type: "char"}
|
||||
},
|
||||
records: [{
|
||||
id: 37,
|
||||
display_name: "xphone",
|
||||
}, {
|
||||
id: 41,
|
||||
display_name: "xpad",
|
||||
}]
|
||||
},
|
||||
partner_type: {
|
||||
fields: {
|
||||
name: {string: "Partner Type", type: "char"},
|
||||
color: {string: "Color index", type: "integer"},
|
||||
},
|
||||
records: [
|
||||
{id: 12, display_name: "gold", color: 2},
|
||||
{id: 14, display_name: "silver", color: 5},
|
||||
]
|
||||
},
|
||||
turtle: {
|
||||
fields: {
|
||||
display_name: { string: "Displayed name", type: "char" },
|
||||
turtle_foo: {string: "Foo", type: "char", default: "My little Foo Value"},
|
||||
turtle_bar: {string: "Bar", type: "boolean", default: true},
|
||||
turtle_int: {string: "int", type: "integer", sortable: true},
|
||||
turtle_qux: {string: "Qux", type: "float", digits: [16,1], required: true, default: 1.5},
|
||||
turtle_description: {string: "Description", type: "text"},
|
||||
turtle_trululu: {string: "Trululu", type: "many2one", relation: 'partner'},
|
||||
product_id: {string: "Product", type: "many2one", relation: 'product', required: true},
|
||||
partner_ids: {string: "Partner", type: "many2many", relation: 'partner'},
|
||||
},
|
||||
records: [{
|
||||
id: 1,
|
||||
display_name: "leonardo",
|
||||
turtle_bar: true,
|
||||
turtle_foo: "yop",
|
||||
partner_ids: [],
|
||||
}, {
|
||||
id: 2,
|
||||
display_name: "donatello",
|
||||
turtle_bar: true,
|
||||
turtle_foo: "blip",
|
||||
turtle_int: 9,
|
||||
partner_ids: [2,4],
|
||||
}, {
|
||||
id: 3,
|
||||
display_name: "raphael",
|
||||
turtle_bar: false,
|
||||
turtle_foo: "kawa",
|
||||
turtle_int: 21,
|
||||
turtle_qux: 9.8,
|
||||
partner_ids: [],
|
||||
}],
|
||||
},
|
||||
user: {
|
||||
fields: {
|
||||
name: {string: "Name", type: "char"}
|
||||
},
|
||||
records: [{
|
||||
id: 17,
|
||||
name: "Aline",
|
||||
}, {
|
||||
id: 19,
|
||||
name: "Christine",
|
||||
}]
|
||||
},
|
||||
};
|
||||
}
|
||||
}, function () {
|
||||
|
||||
QUnit.module('FieldTimezoneMismatch');
|
||||
|
||||
QUnit.test('widget timezone_mismatch in a list view', async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
this.data.partner.fields.tz_offset = {
|
||||
string: "tz_offset",
|
||||
type: "char"
|
||||
};
|
||||
this.data.partner.records.forEach(function (r) {
|
||||
r.color = 'red';
|
||||
r.tz_offset = 0;
|
||||
});
|
||||
this.data.partner.onchanges = {
|
||||
color: function (r) {
|
||||
r.tz_offset = '+4800'; // make sur we have a mismatch
|
||||
}
|
||||
};
|
||||
|
||||
var list = await createView({
|
||||
View: ListView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<tree string="Colors" editable="top">' +
|
||||
'<field name="tz_offset" invisible="True"/>' +
|
||||
'<field name="color" widget="timezone_mismatch"/>' +
|
||||
'</tree>',
|
||||
});
|
||||
|
||||
assert.strictEqual(list.$('td:contains(Red)').length, 3,
|
||||
"should have 3 rows with correct value");
|
||||
await testUtils.dom.click(list.$('td:contains(Red):first'));
|
||||
|
||||
var $td = list.$('tbody tr.o_selected_row td:not(.o_list_record_selector)');
|
||||
|
||||
assert.strictEqual($td.find('select').length, 1, "td should have a child 'select'");
|
||||
assert.strictEqual($td.contents().length, 1, "select tag should be only child of td");
|
||||
|
||||
await testUtils.fields.editSelect($td.find('select'), '"black"');
|
||||
|
||||
assert.strictEqual($td.find('.o_tz_warning').length, 1, "Should display icon alert");
|
||||
assert.ok($td.find('select option:selected').text().match(/Black\s+\([0-9]+\/[0-9]+\/[0-9]+ [0-9]+:[0-9]+:[0-9]+\)/), "Should display the datetime in the selected timezone");
|
||||
list.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('widget timezone_mismatch in a form view', async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
this.data.partner.fields.tz_offset = {
|
||||
string: "tz_offset",
|
||||
type: "char"
|
||||
};
|
||||
this.data.partner.fields.tz = {
|
||||
type: "selection",
|
||||
selection: [['Europe/Brussels', "Europe/Brussels"], ['America/Los_Angeles', "America/Los_Angeles"]],
|
||||
};
|
||||
this.data.partner.records[0].tz = false;
|
||||
this.data.partner.records[0].tz_offset = '+4800';
|
||||
|
||||
var form = await createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
res_id: 1,
|
||||
data: this.data,
|
||||
arch: '<form>' +
|
||||
'<field name="tz_offset" invisible="True"/>' +
|
||||
'<field name="tz" widget="timezone_mismatch"/>' +
|
||||
'</form>',
|
||||
});
|
||||
await testUtils.form.clickEdit(form);
|
||||
assert.containsOnce(form, 'select[name=tz]');
|
||||
|
||||
var $timezoneMismatch = form.$('.o_tz_warning');
|
||||
assert.strictEqual($timezoneMismatch.length, 1, "warning class should be there.");
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('widget timezone_mismatch in a form view edit mode with mismatch', async function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
this.data.partner.fields.tz_offset = {
|
||||
string: "tz_offset",
|
||||
type: "char"
|
||||
};
|
||||
this.data.partner.fields.tz = {
|
||||
type: "selection",
|
||||
selection: [['Europe/Brussels', "Europe/Brussels"], ['America/Los_Angeles', "America/Los_Angeles"]],
|
||||
};
|
||||
this.data.partner.records[0].tz = 'America/Los_Angeles';
|
||||
this.data.partner.records[0].tz_offset = '+4800';
|
||||
|
||||
var form = await createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
res_id: 1,
|
||||
data: this.data,
|
||||
arch: '<form>' +
|
||||
'<field name="tz_offset" invisible="True"/>' +
|
||||
'<field name="tz" widget="timezone_mismatch" options="{\'tz_offset_field\': \'tz_offset\'}"/>' +
|
||||
'</form>',
|
||||
viewOptions: {
|
||||
mode: 'edit',
|
||||
},
|
||||
});
|
||||
|
||||
var $timezoneEl = form.$('select[name="tz"]');
|
||||
assert.strictEqual($timezoneEl.children().length, 3,
|
||||
'The select element should have 3 children');
|
||||
|
||||
var $timezoneMismatch = form.$('.o_tz_warning');
|
||||
assert.strictEqual($timezoneMismatch.length, 1,
|
||||
'timezone mismatch is present');
|
||||
|
||||
assert.notOk($timezoneMismatch.children().length,
|
||||
'The mismatch element should not have children');
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.module('IframeWrapper');
|
||||
|
||||
QUnit.test('iframe_wrapper widget in form view', async function (assert) {
|
||||
|
||||
assert.expect(2);
|
||||
|
||||
this.data = {
|
||||
report: {
|
||||
fields: {
|
||||
report_content: {string: "Content of report", type: "html"}
|
||||
},
|
||||
records: [{
|
||||
id: 1,
|
||||
report_content:
|
||||
`<html>
|
||||
<head>
|
||||
<style>
|
||||
body { color : rgb(255, 0, 0); }
|
||||
</style>
|
||||
<head>
|
||||
<body>
|
||||
<div class="nice_div"><p>Some content</p></div>
|
||||
</body>
|
||||
</html>`
|
||||
}]
|
||||
}
|
||||
};
|
||||
|
||||
const form = await createView({
|
||||
View: FormView,
|
||||
model: 'report',
|
||||
data: this.data,
|
||||
arch: `<form><field name="report_content" widget="iframe_wrapper"/></form>`,
|
||||
res_id: 1,
|
||||
});
|
||||
|
||||
const $iframe = form.$('iframe');
|
||||
await $iframe.data('ready');
|
||||
const doc = $iframe.contents()[0];
|
||||
|
||||
assert.strictEqual($(doc).find('.nice_div').html(), '<p>Some content</p>',
|
||||
"should have rendered a div with correct content");
|
||||
|
||||
assert.strictEqual($(doc).find('.nice_div p').css('color'), 'rgb(255, 0, 0)',
|
||||
"head tag style should have been applied");
|
||||
|
||||
form.destroy();
|
||||
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,88 +0,0 @@
|
|||
odoo.define('web.keyboard_navigation_mixin_tests', function (require) {
|
||||
"use strict";
|
||||
|
||||
var KeyboardNavigationMixin = require('web.KeyboardNavigationMixin');
|
||||
var testUtils = require('web.test_utils');
|
||||
var Widget = require('web.Widget');
|
||||
|
||||
QUnit.module('KeyboardNavigationMixin', function () {
|
||||
QUnit.test('aria-keyshortcuts is added on elements with accesskey', async function (assert) {
|
||||
assert.expect(1);
|
||||
var $target = $('#qunit-fixture');
|
||||
var KeyboardWidget = Widget.extend(KeyboardNavigationMixin, {
|
||||
init: function () {
|
||||
this._super.apply(this, arguments);
|
||||
KeyboardNavigationMixin.init.call(this);
|
||||
},
|
||||
start: function () {
|
||||
KeyboardNavigationMixin.start.call(this);
|
||||
var $button = $('<button>').text('Click Me!').attr('accesskey', 'o');
|
||||
// we need to define the accesskey because it will not be assigned on invisible buttons
|
||||
this.$el.append($button);
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
destroy: function () {
|
||||
KeyboardNavigationMixin.destroy.call(this);
|
||||
return this._super(...arguments);
|
||||
},
|
||||
});
|
||||
var parent = await testUtils.createParent({});
|
||||
var w = new KeyboardWidget(parent);
|
||||
await w.appendTo($target);
|
||||
|
||||
// minimum set of attribute to generate a native event that works with the mixin
|
||||
var e = new Event("keydown");
|
||||
e.key = '';
|
||||
e.altKey = true;
|
||||
w.$el[0].dispatchEvent(e);
|
||||
|
||||
assert.ok(w.$el.find('button[aria-keyshortcuts]')[0], 'the aria-keyshortcuts is set on the button');
|
||||
|
||||
parent.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('keep CSS position absolute for parent of overlay', async function (assert) {
|
||||
// If we change the CSS position of an 'absolute' element to 'relative',
|
||||
// we may likely change its position on the document. Since the overlay
|
||||
// CSS position is 'absolute', it will match the size and cover the
|
||||
// parent with 'absolute' > 'absolute', without altering the position
|
||||
// of the parent on the document.
|
||||
assert.expect(1);
|
||||
var $target = $('#qunit-fixture');
|
||||
var $button;
|
||||
var KeyboardWidget = Widget.extend(KeyboardNavigationMixin, {
|
||||
init: function () {
|
||||
this._super.apply(this, arguments);
|
||||
KeyboardNavigationMixin.init.call(this);
|
||||
},
|
||||
start: function () {
|
||||
KeyboardNavigationMixin.start.call(this);
|
||||
$button = $('<button>').text('Click Me!').attr('accesskey', 'o');
|
||||
// we need to define the accesskey because it will not be assigned on invisible buttons
|
||||
this.$el.append($button);
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
destroy: function () {
|
||||
KeyboardNavigationMixin.destroy.call(this);
|
||||
return this._super(...arguments);
|
||||
},
|
||||
});
|
||||
var parent = await testUtils.createParent({});
|
||||
var w = new KeyboardWidget(parent);
|
||||
await w.appendTo($target);
|
||||
|
||||
$button.css('position', 'absolute');
|
||||
|
||||
// minimum set of attribute to generate a native event that works with the mixin
|
||||
var e = new Event("keydown");
|
||||
e.key = '';
|
||||
e.altKey = true;
|
||||
w.$el[0].dispatchEvent(e);
|
||||
|
||||
assert.strictEqual($button.css('position'), 'absolute',
|
||||
"should not have kept the CSS position of the button");
|
||||
|
||||
parent.destroy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,114 @@
|
|||
/** @odoo-module alias=@web/../tests/helpers/cleanup default=false */
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Cleanup
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
const cleanups = [];
|
||||
|
||||
/**
|
||||
* Register a cleanup callback that will be executed whenever the current test
|
||||
* is done.
|
||||
*
|
||||
* - the cleanups will be executed in reverse order
|
||||
* - they will be executed even if the test fails/crashes
|
||||
*
|
||||
* @param {Function} callback
|
||||
*/
|
||||
export function registerCleanup(callback) {
|
||||
cleanups.push(callback);
|
||||
}
|
||||
|
||||
if (window.QUnit) {
|
||||
QUnit.on("OdooAfterTestHook", (info) => {
|
||||
if (QUnit.config.debug) {
|
||||
return;
|
||||
}
|
||||
let cleanup;
|
||||
// note that this calls the cleanup callbacks in reverse order!
|
||||
while ((cleanup = cleanups.pop())) {
|
||||
try {
|
||||
cleanup(info);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Check leftovers
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* List of elements tolerated in the body after a test. The property "keep"
|
||||
* prevents the element from being removed (typically: qunit suite elements).
|
||||
*/
|
||||
const validElements = [
|
||||
// always in the body:
|
||||
{ tagName: "DIV", attr: "id", value: "qunit", keep: true },
|
||||
{ tagName: "DIV", attr: "id", value: "qunit-fixture", keep: true },
|
||||
// shouldn't be in the body after a test but are tolerated:
|
||||
{ tagName: "SCRIPT", attr: "id", value: "" },
|
||||
{ tagName: "DIV", attr: "class", value: "o_notification_manager" },
|
||||
{ tagName: "DIV", attr: "class", value: "tooltip fade bs-tooltip-auto" },
|
||||
{ tagName: "DIV", attr: "class", value: "tooltip fade bs-tooltip-auto show" },
|
||||
{ tagName: "DIV", attr: "class", value: "tooltip tooltip-field-info fade bs-tooltip-auto" },
|
||||
{
|
||||
tagName: "DIV",
|
||||
attr: "class",
|
||||
value: "tooltip tooltip-field-info fade bs-tooltip-auto show",
|
||||
},
|
||||
|
||||
// Due to a Document Kanban bug (already present in 12.0)
|
||||
{ tagName: "DIV", attr: "class", value: "ui-helper-hidden-accessible" },
|
||||
{
|
||||
tagName: "UL",
|
||||
attr: "class",
|
||||
value: "ui-menu ui-widget ui-widget-content ui-autocomplete ui-front",
|
||||
},
|
||||
{
|
||||
tagName: "UL",
|
||||
attr: "class",
|
||||
value: "ui-menu ui-widget ui-widget-content ui-autocomplete dropdown-menu ui-front", // many2ones
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* After each test, we check that there is no leftover in the DOM.
|
||||
*
|
||||
* Note: this event is not QUnit standard, we added it for this specific use case.
|
||||
* As a payload, an object with keys 'moduleName' and 'testName' is provided. It
|
||||
* is used to indicate the test that left elements in the DOM, when it happens.
|
||||
*/
|
||||
QUnit.on("OdooAfterTestHook", function (info) {
|
||||
if (QUnit.config.debug) {
|
||||
return;
|
||||
}
|
||||
const failed = info.testReport.getStatus() === "failed";
|
||||
const toRemove = [];
|
||||
// check for leftover elements in the body
|
||||
for (const bodyChild of document.body.children) {
|
||||
const tolerated = validElements.find(
|
||||
(e) => e.tagName === bodyChild.tagName && bodyChild.getAttribute(e.attr) === e.value
|
||||
);
|
||||
if (!failed && !tolerated) {
|
||||
QUnit.pushFailure(
|
||||
`Body still contains undesirable elements:\n${bodyChild.outerHTML}`
|
||||
);
|
||||
}
|
||||
if (!tolerated || !tolerated.keep) {
|
||||
toRemove.push(bodyChild);
|
||||
}
|
||||
}
|
||||
// cleanup leftovers in #qunit-fixture
|
||||
const qunitFixture = document.getElementById("qunit-fixture");
|
||||
if (qunitFixture.children.length) {
|
||||
toRemove.push(...qunitFixture.children);
|
||||
}
|
||||
// remove unwanted elements if not in debug
|
||||
for (const el of toRemove) {
|
||||
el.remove();
|
||||
}
|
||||
document.body.classList.remove("modal-open");
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,149 @@
|
|||
/** @odoo-module alias=@web/../tests/helpers/mock_env default=false */
|
||||
|
||||
import { SERVICES_METADATA } from "@web/core/utils/hooks";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { makeEnv, startServices } from "@web/env";
|
||||
import { registerCleanup } from "./cleanup";
|
||||
import { makeMockServer } from "./mock_server";
|
||||
import { mocks } from "./mock_services";
|
||||
import { patchWithCleanup } from "./utils";
|
||||
import { Component } from "@odoo/owl";
|
||||
import { startRouter } from "@web/core/browser/router";
|
||||
|
||||
function prepareRegistry(registry, keepContent = false) {
|
||||
const _addEventListener = registry.addEventListener.bind(registry);
|
||||
const _removeEventListener = registry.removeEventListener.bind(registry);
|
||||
const patch = {
|
||||
content: keepContent ? { ...registry.content } : {},
|
||||
elements: null,
|
||||
entries: null,
|
||||
subRegistries: {},
|
||||
addEventListener(type, callback) {
|
||||
_addEventListener(type, callback);
|
||||
registerCleanup(() => {
|
||||
_removeEventListener(type, callback);
|
||||
});
|
||||
},
|
||||
};
|
||||
patchWithCleanup(registry, patch);
|
||||
}
|
||||
|
||||
export function clearRegistryWithCleanup(registry) {
|
||||
prepareRegistry(registry);
|
||||
}
|
||||
|
||||
function cloneRegistryWithCleanup(registry) {
|
||||
prepareRegistry(registry, true);
|
||||
}
|
||||
|
||||
export function clearServicesMetadataWithCleanup() {
|
||||
const servicesMetadata = Object.assign({}, SERVICES_METADATA);
|
||||
for (const key of Object.keys(SERVICES_METADATA)) {
|
||||
delete SERVICES_METADATA[key];
|
||||
}
|
||||
registerCleanup(() => {
|
||||
for (const key of Object.keys(SERVICES_METADATA)) {
|
||||
delete SERVICES_METADATA[key];
|
||||
}
|
||||
Object.assign(SERVICES_METADATA, servicesMetadata);
|
||||
});
|
||||
}
|
||||
|
||||
export const registryNamesToCloneWithCleanup = [
|
||||
"actions",
|
||||
"command_provider",
|
||||
"command_setup",
|
||||
"error_handlers",
|
||||
"fields",
|
||||
"fields",
|
||||
"main_components",
|
||||
"view_widgets",
|
||||
"views",
|
||||
];
|
||||
|
||||
export const utils = {
|
||||
prepareRegistriesWithCleanup() {
|
||||
// Clone registries
|
||||
registryNamesToCloneWithCleanup.forEach((registryName) =>
|
||||
cloneRegistryWithCleanup(registry.category(registryName))
|
||||
);
|
||||
|
||||
// Clear registries
|
||||
clearRegistryWithCleanup(registry.category("command_categories"));
|
||||
clearRegistryWithCleanup(registry.category("debug"));
|
||||
clearRegistryWithCleanup(registry.category("error_dialogs"));
|
||||
clearRegistryWithCleanup(registry.category("favoriteMenu"));
|
||||
clearRegistryWithCleanup(registry.category("ir.actions.report handlers"));
|
||||
clearRegistryWithCleanup(registry.category("main_components"));
|
||||
|
||||
clearRegistryWithCleanup(registry.category("services"));
|
||||
clearServicesMetadataWithCleanup();
|
||||
|
||||
clearRegistryWithCleanup(registry.category("systray"));
|
||||
clearRegistryWithCleanup(registry.category("user_menuitems"));
|
||||
clearRegistryWithCleanup(registry.category("kanban_examples"));
|
||||
// fun fact: at least one registry is missing... this shows that we need a
|
||||
// better design for the way we clear these registries...
|
||||
},
|
||||
};
|
||||
|
||||
// This is exported in a utils object to allow for patching
|
||||
export function prepareRegistriesWithCleanup() {
|
||||
return utils.prepareRegistriesWithCleanup();
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {import("@web/env").OdooEnv} OdooEnv
|
||||
*/
|
||||
|
||||
/**
|
||||
* Create a test environment
|
||||
*
|
||||
* @param {*} config
|
||||
* @returns {Promise<OdooEnv>}
|
||||
*/
|
||||
export async function makeTestEnv(config = {}) {
|
||||
startRouter();
|
||||
// add all missing dependencies if necessary
|
||||
const serviceRegistry = registry.category("services");
|
||||
const servicesToProcess = serviceRegistry.getAll();
|
||||
while (servicesToProcess.length) {
|
||||
const service = servicesToProcess.pop();
|
||||
if (service.dependencies) {
|
||||
for (const depName of service.dependencies) {
|
||||
if (depName in mocks && !serviceRegistry.contains(depName)) {
|
||||
const dep = mocks[depName]();
|
||||
serviceRegistry.add(depName, dep);
|
||||
servicesToProcess.push(dep);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (config.serverData || config.mockRPC || config.activateMockServer) {
|
||||
await makeMockServer(config.serverData, config.mockRPC);
|
||||
}
|
||||
|
||||
let env = makeEnv();
|
||||
await startServices(env);
|
||||
Component.env = env;
|
||||
if ("config" in config) {
|
||||
env = Object.assign(Object.create(env), { config: config.config });
|
||||
}
|
||||
return env;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a test environment for dialog tests
|
||||
*
|
||||
* @param {*} config
|
||||
* @returns {Promise<OdooEnv>}
|
||||
*/
|
||||
export async function makeDialogTestEnv(config = {}) {
|
||||
const env = await makeTestEnv(config);
|
||||
env.dialogData = {
|
||||
isActive: true,
|
||||
close() {},
|
||||
};
|
||||
return env;
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,303 @@
|
|||
/** @odoo-module alias=@web/../tests/helpers/mock_services default=false */
|
||||
|
||||
import { effectService } from "@web/core/effects/effect_service";
|
||||
import { localization } from "@web/core/l10n/localization";
|
||||
import { ConnectionAbortedError, rpcBus, rpc } from "@web/core/network/rpc";
|
||||
import { ormService } from "@web/core/orm_service";
|
||||
import { overlayService } from "@web/core/overlay/overlay_service";
|
||||
import { uiService } from "@web/core/ui/ui_service";
|
||||
import { user } from "@web/core/user";
|
||||
import { patchWithCleanup } from "./utils";
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Mock Services
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
export const defaultLocalization = {
|
||||
dateFormat: "MM/dd/yyyy",
|
||||
timeFormat: "HH:mm:ss",
|
||||
dateTimeFormat: "MM/dd/yyyy HH:mm:ss",
|
||||
decimalPoint: ".",
|
||||
direction: "ltr",
|
||||
grouping: [],
|
||||
multiLang: false,
|
||||
thousandsSep: ",",
|
||||
weekStart: 7,
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Partial<typeof defaultLocalization>} [config]
|
||||
*/
|
||||
export function makeFakeLocalizationService(config = {}) {
|
||||
patchWithCleanup(localization, { ...defaultLocalization, ...config });
|
||||
patchWithCleanup(luxon.Settings, { defaultNumberingSystem: "latn" });
|
||||
|
||||
return {
|
||||
name: "localization",
|
||||
start: async (env) => {
|
||||
return localization;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function patchRPCWithCleanup(mockRPC = () => {}) {
|
||||
let nextId = 1;
|
||||
patchWithCleanup(rpc, {
|
||||
_rpc: function (route, params = {}, settings = {}) {
|
||||
let rejectFn;
|
||||
const data = {
|
||||
id: nextId++,
|
||||
jsonrpc: "2.0",
|
||||
method: "call",
|
||||
params: params,
|
||||
};
|
||||
rpcBus.trigger("RPC:REQUEST", { data, url: route, settings });
|
||||
const rpcProm = new Promise((resolve, reject) => {
|
||||
rejectFn = reject;
|
||||
Promise.resolve(mockRPC(...arguments))
|
||||
.then((result) => {
|
||||
rpcBus.trigger("RPC:RESPONSE", { data, settings, result });
|
||||
resolve(result);
|
||||
})
|
||||
.catch((error) => {
|
||||
rpcBus.trigger("RPC:RESPONSE", {
|
||||
data,
|
||||
settings,
|
||||
error,
|
||||
});
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
rpcProm.abort = (rejectError = true) => {
|
||||
if (rejectError) {
|
||||
rejectFn(new ConnectionAbortedError("XmlHttpRequestError abort"));
|
||||
}
|
||||
};
|
||||
return rpcProm;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function makeMockXHR(response, sendCb, def) {
|
||||
const MockXHR = function () {
|
||||
return {
|
||||
_loadListener: null,
|
||||
url: "",
|
||||
addEventListener(type, listener) {
|
||||
if (type === "load") {
|
||||
this._loadListener = listener;
|
||||
} else if (type === "error") {
|
||||
this._errorListener = listener;
|
||||
}
|
||||
},
|
||||
set onload(listener) {
|
||||
this._loadListener = listener;
|
||||
},
|
||||
set onerror(listener) {
|
||||
this._errorListener = listener;
|
||||
},
|
||||
open(method, url) {
|
||||
this.url = url;
|
||||
},
|
||||
getResponseHeader() {},
|
||||
setRequestHeader() {},
|
||||
async send(data) {
|
||||
let listener = this._loadListener;
|
||||
if (sendCb) {
|
||||
if (typeof data === "string") {
|
||||
try {
|
||||
data = JSON.parse(data);
|
||||
} catch {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
try {
|
||||
await sendCb.call(this, data);
|
||||
} catch {
|
||||
listener = this._errorListener;
|
||||
}
|
||||
}
|
||||
if (def) {
|
||||
await def;
|
||||
}
|
||||
listener.call(this);
|
||||
},
|
||||
response: JSON.stringify(response || ""),
|
||||
};
|
||||
};
|
||||
return MockXHR;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Low level API mocking
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
export function makeMockFetch(mockRPC) {
|
||||
return async (input, params) => {
|
||||
let route = typeof input === "string" ? input : input.url;
|
||||
if (route.includes("load_menus")) {
|
||||
route = route.split("?")[0];
|
||||
}
|
||||
let res;
|
||||
let status;
|
||||
try {
|
||||
res = await mockRPC(route, params);
|
||||
status = 200;
|
||||
} catch {
|
||||
status = 500;
|
||||
}
|
||||
const blob = new Blob([JSON.stringify(res || {})], { type: "application/json" });
|
||||
const response = new Response(blob, { status });
|
||||
// Mock some functions of the Response API to make them almost synchronous (micro-tick level)
|
||||
// as their native implementation is async (tick level), which can lead to undeterministic
|
||||
// errors as it breaks the hypothesis that calling nextTick after fetching data is enough
|
||||
// to see the result rendered in the DOM.
|
||||
response.json = () => Promise.resolve(JSON.parse(JSON.stringify(res || {})));
|
||||
response.text = () => Promise.resolve(String(res || {}));
|
||||
response.blob = () => Promise.resolve(blob);
|
||||
return response;
|
||||
};
|
||||
}
|
||||
|
||||
export const fakeCommandService = {
|
||||
start() {
|
||||
return {
|
||||
add() {
|
||||
return () => {};
|
||||
},
|
||||
getCommands() {
|
||||
return [];
|
||||
},
|
||||
openPalette() {},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export const fakeTitleService = {
|
||||
start() {
|
||||
let current = {};
|
||||
return {
|
||||
get current() {
|
||||
return JSON.stringify(current);
|
||||
},
|
||||
getParts() {
|
||||
return current;
|
||||
},
|
||||
setParts(parts) {
|
||||
current = Object.assign({}, current, parts);
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export function makeFakeNotificationService(mock) {
|
||||
return {
|
||||
start() {
|
||||
function add() {
|
||||
if (mock) {
|
||||
return mock(...arguments);
|
||||
}
|
||||
}
|
||||
return {
|
||||
add,
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function makeFakeDialogService(addDialog, closeAllDialog) {
|
||||
return {
|
||||
start() {
|
||||
return {
|
||||
add: addDialog || (() => () => {}),
|
||||
closeAll: closeAllDialog || (() => () => {}),
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function makeFakePwaService() {
|
||||
return {
|
||||
start() {
|
||||
return {
|
||||
canPromptToInstall: false,
|
||||
isAvailable: false,
|
||||
isScopedApp: false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function patchUserContextWithCleanup(patch) {
|
||||
const context = user.context;
|
||||
patchWithCleanup(user, {
|
||||
get context() {
|
||||
return Object.assign({}, context, patch);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function patchUserWithCleanup(patch) {
|
||||
patchWithCleanup(user, patch);
|
||||
}
|
||||
|
||||
export function makeFakeBarcodeService() {
|
||||
return {
|
||||
start() {
|
||||
return {
|
||||
bus: {
|
||||
async addEventListener() {},
|
||||
async removeEventListener() {},
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function makeFakeHTTPService(getResponse, postResponse) {
|
||||
getResponse =
|
||||
getResponse ||
|
||||
((route, readMethod) => {
|
||||
return readMethod === "json" ? {} : "";
|
||||
});
|
||||
postResponse =
|
||||
postResponse ||
|
||||
((route, params, readMethod) => {
|
||||
return readMethod === "json" ? {} : "";
|
||||
});
|
||||
return {
|
||||
start() {
|
||||
return {
|
||||
async get(...args) {
|
||||
return getResponse(...args);
|
||||
},
|
||||
async post(...args) {
|
||||
return postResponse(...args);
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function makeFakeActionService() {
|
||||
return {
|
||||
start() {
|
||||
return {
|
||||
doAction() {},
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const mocks = {
|
||||
command: () => fakeCommandService,
|
||||
effect: () => effectService, // BOI The real service ? Is this what we want ?
|
||||
localization: makeFakeLocalizationService,
|
||||
notification: makeFakeNotificationService,
|
||||
title: () => fakeTitleService,
|
||||
ui: () => uiService,
|
||||
dialog: makeFakeDialogService,
|
||||
orm: () => ormService,
|
||||
action: makeFakeActionService,
|
||||
overlay: () => overlayService,
|
||||
};
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
/** @odoo-module alias=@web/../tests/helpers/mount_in_fixture default=false**/
|
||||
|
||||
import { App, Component, xml } from "@odoo/owl";
|
||||
import { registerCleanup } from "@web/../tests/helpers/cleanup";
|
||||
import { makeTestEnv } from "@web/../tests/helpers/mock_env";
|
||||
import { mocks } from "@web/../tests/helpers/mock_services";
|
||||
import { MainComponentsContainer } from "@web/core/main_components_container";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { getTemplate } from "@web/core/templates";
|
||||
|
||||
class TestComponent extends Component {
|
||||
static props = {
|
||||
components: { type: Array },
|
||||
};
|
||||
|
||||
static template = xml`
|
||||
<t t-foreach="props.components" t-as="comp" t-key="comp.component.name">
|
||||
<t t-component="comp.component" t-props="comp.props"/>
|
||||
</t>
|
||||
`;
|
||||
|
||||
/**
|
||||
* Returns the instance of the first component.
|
||||
* @returns {Component}
|
||||
*/
|
||||
get defaultComponent() {
|
||||
return this.__owl__.bdom.children[0].child.component;
|
||||
}
|
||||
}
|
||||
|
||||
function getApp(env, props) {
|
||||
const appConfig = {
|
||||
env,
|
||||
getTemplate,
|
||||
test: true,
|
||||
props: props,
|
||||
};
|
||||
if (env.services && "localization" in env.services) {
|
||||
appConfig.translateFn = env._t;
|
||||
}
|
||||
const app = new App(TestComponent, appConfig);
|
||||
registerCleanup(() => app.destroy());
|
||||
return app;
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {Object} Config
|
||||
* @property {Object} env
|
||||
* @property {Object} props
|
||||
* @property {string[]} templates
|
||||
*/
|
||||
|
||||
/**
|
||||
* This functions will mount the given component and
|
||||
* will add a MainComponentsContainer if the overlay
|
||||
* service is loaded.
|
||||
*
|
||||
* @template T
|
||||
* @param {new (...args: any[]) => T} Comp
|
||||
* @param {HTMLElement} target
|
||||
* @param {Config} config
|
||||
* @returns {Promise<T>} Instance of Comp
|
||||
*/
|
||||
export async function mountInFixture(Comp, target, config = {}) {
|
||||
const serviceRegistry = registry.category("services");
|
||||
|
||||
let env = config.env || {};
|
||||
const isEnvInitialized = env && env.services;
|
||||
|
||||
function isServiceRegistered(serviceName) {
|
||||
return isEnvInitialized
|
||||
? serviceName in env.services
|
||||
: serviceRegistry.contains(serviceName);
|
||||
}
|
||||
|
||||
async function addService(serviceName, service) {
|
||||
if (isServiceRegistered(serviceName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
service = typeof service === "function" ? service() : service;
|
||||
if (isEnvInitialized) {
|
||||
env.services[serviceName] = await service.start(env);
|
||||
} else {
|
||||
serviceRegistry.add(serviceName, service);
|
||||
}
|
||||
}
|
||||
|
||||
const components = [{ component: Comp, props: config.props || {} }];
|
||||
if (isServiceRegistered("overlay")) {
|
||||
await addService("localization", mocks.localization);
|
||||
components.push({ component: MainComponentsContainer, props: {} });
|
||||
}
|
||||
|
||||
if (!isEnvInitialized) {
|
||||
env = await makeTestEnv(env);
|
||||
}
|
||||
|
||||
const app = getApp(env, { components });
|
||||
|
||||
if (config.templates) {
|
||||
app.addTemplates(config.templates);
|
||||
}
|
||||
|
||||
const testComp = await app.mount(target);
|
||||
return testComp.defaultComponent;
|
||||
}
|
||||
|
|
@ -1,88 +0,0 @@
|
|||
odoo.define('web.test_env', async function (require) {
|
||||
"use strict";
|
||||
|
||||
const Bus = require('web.Bus');
|
||||
const session = require('web.session');
|
||||
const { makeTestEnvServices } = require('@web/../tests/legacy/helpers/test_services');
|
||||
const { templates, setLoadXmlDefaultApp } = require("@web/core/assets");
|
||||
const { renderToString } = require('@web/core/utils/render');
|
||||
const { App, Component } = owl;
|
||||
|
||||
let app;
|
||||
|
||||
/**
|
||||
* Creates a test environment with the given environment object.
|
||||
* Any access to a key that has not been explicitly defined in the given environment object
|
||||
* will result in an error.
|
||||
*
|
||||
* @param {Object} [env={}]
|
||||
* @param {Function} [providedRPC=null]
|
||||
* @returns {Proxy}
|
||||
*/
|
||||
function makeTestEnvironment(env = {}, providedRPC = null) {
|
||||
if (!app) {
|
||||
app = new App(null, { templates, test: true });
|
||||
renderToString.app = app;
|
||||
setLoadXmlDefaultApp(app);
|
||||
}
|
||||
|
||||
const defaultTranslationParamters = {
|
||||
code: "en_US",
|
||||
date_format: '%m/%d/%Y',
|
||||
decimal_point: ".",
|
||||
direction: 'ltr',
|
||||
grouping: [],
|
||||
thousands_sep: ",",
|
||||
time_format: '%H:%M:%S',
|
||||
};
|
||||
|
||||
let _t;
|
||||
if ('_t' in env) {
|
||||
_t = Object.assign(env._t, {database: env._t.database || {}})
|
||||
} else {
|
||||
_t = Object.assign(((s) => s), { database: {} });
|
||||
}
|
||||
|
||||
_t.database.parameters = Object.assign(defaultTranslationParamters, _t.database.parameters);
|
||||
|
||||
const defaultEnv = {
|
||||
_t,
|
||||
browser: Object.assign({
|
||||
setTimeout: window.setTimeout.bind(window),
|
||||
clearTimeout: window.clearTimeout.bind(window),
|
||||
setInterval: window.setInterval.bind(window),
|
||||
clearInterval: window.clearInterval.bind(window),
|
||||
requestAnimationFrame: window.requestAnimationFrame.bind(window),
|
||||
Date: window.Date,
|
||||
fetch: (window.fetch || (() => { })).bind(window),
|
||||
}, env.browser),
|
||||
bus: env.bus || new Bus(),
|
||||
device: Object.assign({
|
||||
isMobile: false,
|
||||
SIZES: { XS: 0, VSM: 1, SM: 2, MD: 3, LG: 4, XL: 5, XXL: 6 },
|
||||
}, env.device),
|
||||
isDebug: env.isDebug || (() => false),
|
||||
services: makeTestEnvServices(env),
|
||||
session: Object.assign({
|
||||
rpc(route, params, options) {
|
||||
if (providedRPC) {
|
||||
return providedRPC(route, params, options);
|
||||
}
|
||||
throw new Error(`No method to perform RPC`);
|
||||
},
|
||||
url: session.url,
|
||||
getTZOffset: (() => 0),
|
||||
}, env.session),
|
||||
};
|
||||
return Object.assign(env, defaultEnv);
|
||||
}
|
||||
|
||||
/**
|
||||
* Before each test, we want Component.env to be a fresh test environment.
|
||||
*/
|
||||
QUnit.on('OdooBeforeTestHook', function () {
|
||||
Component.env = makeTestEnvironment();
|
||||
});
|
||||
|
||||
return makeTestEnvironment;
|
||||
});
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import { buildQuery } from 'web.rpc';
|
||||
|
||||
const testEnvServices = {
|
||||
getCookie() {},
|
||||
httpRequest(/* route, params = {}, readMethod = 'json' */) {
|
||||
return Promise.resolve('');
|
||||
},
|
||||
hotkey: { add: () => () => {} }, // fake service
|
||||
notification: { notify() {} },
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates services for the test environment. object
|
||||
*
|
||||
* @param {Object} [env]
|
||||
* @returns {Object}
|
||||
*/
|
||||
function makeTestEnvServices(env) {
|
||||
return Object.assign({}, testEnvServices, {
|
||||
ajax: {
|
||||
rpc() {
|
||||
return env.session.rpc(...arguments); // Compatibility Legacy Widgets
|
||||
},
|
||||
},
|
||||
rpc(params, options) {
|
||||
const query = buildQuery(params);
|
||||
return env.session.rpc(query.route, query.params, options);
|
||||
},
|
||||
ui: { activeElement: document }, // fake service
|
||||
}, env.services);
|
||||
}
|
||||
|
||||
export {
|
||||
makeTestEnvServices,
|
||||
testEnvServices,
|
||||
};
|
||||
|
|
@ -1,277 +0,0 @@
|
|||
odoo.define('web.test_utils', async function (require) {
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Test Utils
|
||||
*
|
||||
* In this module, we define various utility functions to help simulate a mock
|
||||
* environment as close as possible as a real environment. The main function is
|
||||
* certainly createView, which takes a bunch of parameters and give you back an
|
||||
* instance of a view, appended in the dom, ready to be tested.
|
||||
*/
|
||||
|
||||
const relationalFields = require('web.relational_fields');
|
||||
const session = require('web.session');
|
||||
const testUtilsCreate = require('web.test_utils_create');
|
||||
const testUtilsControlPanel = require('web.test_utils_control_panel');
|
||||
const testUtilsDom = require('web.test_utils_dom');
|
||||
const testUtilsFields = require('web.test_utils_fields');
|
||||
const testUtilsFile = require('web.test_utils_file');
|
||||
const testUtilsForm = require('web.test_utils_form');
|
||||
const testUtilsGraph = require('web.test_utils_graph');
|
||||
const testUtilsKanban = require('web.test_utils_kanban');
|
||||
const testUtilsMock = require('web.test_utils_mock');
|
||||
const testUtilsModal = require('web.test_utils_modal');
|
||||
const testUtilsPivot = require('web.test_utils_pivot');
|
||||
const tools = require('web.tools');
|
||||
|
||||
|
||||
function deprecated(fn, type) {
|
||||
const msg = `Helper 'testUtils.${fn.name}' is deprecated. ` +
|
||||
`Please use 'testUtils.${type}.${fn.name}' instead.`;
|
||||
return tools.deprecated(fn, msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function, make a promise with a public resolve function. Note that
|
||||
* this is not standard and should not be used outside of tests...
|
||||
*
|
||||
* @returns {Promise + resolve and reject function}
|
||||
*/
|
||||
function makeTestPromise() {
|
||||
let resolve;
|
||||
let reject;
|
||||
const promise = new Promise(function (_resolve, _reject) {
|
||||
resolve = _resolve;
|
||||
reject = _reject;
|
||||
});
|
||||
promise.resolve = function () {
|
||||
resolve.apply(null, arguments);
|
||||
return promise;
|
||||
};
|
||||
promise.reject = function () {
|
||||
reject.apply(null, arguments);
|
||||
return promise;
|
||||
};
|
||||
return promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a promise with public resolve and reject functions (see
|
||||
* @makeTestPromise). Perform an assert.step when the promise is
|
||||
* resolved/rejected.
|
||||
*
|
||||
* @param {Object} assert instance object with the assertion methods
|
||||
* @param {function} assert.step
|
||||
* @param {string} str message to pass to assert.step
|
||||
* @returns {Promise + resolve and reject function}
|
||||
*/
|
||||
function makeTestPromiseWithAssert(assert, str) {
|
||||
const prom = makeTestPromise();
|
||||
prom.then(() => assert.step('ok ' + str)).catch(function () { });
|
||||
prom.catch(() => assert.step('ko ' + str));
|
||||
return prom;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new promise that can be waited by the caller in order to execute
|
||||
* code after the next microtask tick and before the next jobqueue tick.
|
||||
*
|
||||
* @return {Promise} an already fulfilled promise
|
||||
*/
|
||||
async function nextMicrotaskTick() {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a promise that will be resolved after the tick after the
|
||||
* nextAnimationFrame
|
||||
*
|
||||
* This is usefull to guarantee that OWL has had the time to render
|
||||
*
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async function nextTick() {
|
||||
return testUtilsDom.returnAfterNextAnimationFrame();
|
||||
}
|
||||
|
||||
/**
|
||||
* Awaits for an additionnal rendering frame initiated by the Owl
|
||||
* compatibility layer processing.
|
||||
*
|
||||
* By default a simple "nextTick" will handle the rendering of any widget/
|
||||
* component stuctures having at most 1 switch between the type of
|
||||
* entities (Component > Widget or Widget > Component). However more time
|
||||
* must be spent rendering in case we have additionnal switches. In such
|
||||
* cases this function must be used (1 call for each additionnal switch)
|
||||
* since it will be removed along with the compatiblity layer once the
|
||||
* framework has been entirely converted, and using this helper will make
|
||||
* it easier to wipe it from the code base.
|
||||
*
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async function owlCompatibilityExtraNextTick() {
|
||||
return testUtilsDom.returnAfterNextAnimationFrame();
|
||||
}
|
||||
|
||||
// Loading static files cannot be properly simulated when their real content is
|
||||
// really needed. This is the case for static XML files so we load them here,
|
||||
// before starting the qunit test suite.
|
||||
// (session.js is in charge of loading the static xml bundle and we also have
|
||||
// to load xml files that are normally lazy loaded by specific widgets).
|
||||
// Assets can also contain static xml files. They are loaded when the session
|
||||
// is launched.
|
||||
await session.is_bound;
|
||||
setTimeout(function () {
|
||||
// jquery autocomplete refines the search in a setTimeout() parameterized
|
||||
// with a delay, so we force this delay to 0 s.t. the dropdown is filtered
|
||||
// directly on the next tick
|
||||
relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = 0;
|
||||
}, 0);
|
||||
return {
|
||||
mock: {
|
||||
addMockEnvironment: testUtilsMock.addMockEnvironment,
|
||||
addMockEnvironmentOwl: testUtilsMock.addMockEnvironmentOwl,
|
||||
intercept: testUtilsMock.intercept,
|
||||
patch: testUtilsMock.patch,
|
||||
patchDate: testUtilsMock.patchDate,
|
||||
unpatch: testUtilsMock.unpatch,
|
||||
getView: testUtilsMock.getView,
|
||||
patchSetTimeout: testUtilsMock.patchSetTimeout,
|
||||
},
|
||||
controlPanel: {
|
||||
// Generic interactions
|
||||
toggleMenu: testUtilsControlPanel.toggleMenu,
|
||||
toggleMenuItem: testUtilsControlPanel.toggleMenuItem,
|
||||
toggleMenuItemOption: testUtilsControlPanel.toggleMenuItemOption,
|
||||
isItemSelected: testUtilsControlPanel.isItemSelected,
|
||||
isOptionSelected: testUtilsControlPanel.isOptionSelected,
|
||||
getMenuItemTexts: testUtilsControlPanel.getMenuItemTexts,
|
||||
// Button interactions
|
||||
getButtons: testUtilsControlPanel.getButtons,
|
||||
// FilterMenu interactions
|
||||
toggleFilterMenu: testUtilsControlPanel.toggleFilterMenu,
|
||||
toggleAddCustomFilter: testUtilsControlPanel.toggleAddCustomFilter,
|
||||
applyFilter: testUtilsControlPanel.applyFilter,
|
||||
addCondition: testUtilsControlPanel.addCondition,
|
||||
// GroupByMenu interactions
|
||||
toggleGroupByMenu: testUtilsControlPanel.toggleGroupByMenu,
|
||||
toggleAddCustomGroup: testUtilsControlPanel.toggleAddCustomGroup,
|
||||
selectGroup: testUtilsControlPanel.selectGroup,
|
||||
applyGroup: testUtilsControlPanel.applyGroup,
|
||||
// FavoriteMenu interactions
|
||||
toggleFavoriteMenu: testUtilsControlPanel.toggleFavoriteMenu,
|
||||
toggleSaveFavorite: testUtilsControlPanel.toggleSaveFavorite,
|
||||
editFavoriteName: testUtilsControlPanel.editFavoriteName,
|
||||
saveFavorite: testUtilsControlPanel.saveFavorite,
|
||||
deleteFavorite: testUtilsControlPanel.deleteFavorite,
|
||||
// ComparisonMenu interactions
|
||||
toggleComparisonMenu: testUtilsControlPanel.toggleComparisonMenu,
|
||||
// SearchBar interactions
|
||||
getFacetTexts: testUtilsControlPanel.getFacetTexts,
|
||||
removeFacet: testUtilsControlPanel.removeFacet,
|
||||
editSearch: testUtilsControlPanel.editSearch,
|
||||
validateSearch: testUtilsControlPanel.validateSearch,
|
||||
// Action menus interactions
|
||||
toggleActionMenu: testUtilsControlPanel.toggleActionMenu,
|
||||
// Pager interactions
|
||||
pagerPrevious: testUtilsControlPanel.pagerPrevious,
|
||||
pagerNext: testUtilsControlPanel.pagerNext,
|
||||
getPagerValue: testUtilsControlPanel.getPagerValue,
|
||||
getPagerSize: testUtilsControlPanel.getPagerSize,
|
||||
setPagerValue: testUtilsControlPanel.setPagerValue,
|
||||
// View switcher
|
||||
switchView: testUtilsControlPanel.switchView,
|
||||
},
|
||||
dom: {
|
||||
triggerKeypressEvent: testUtilsDom.triggerKeypressEvent,
|
||||
triggerMouseEvent: testUtilsDom.triggerMouseEvent,
|
||||
triggerPositionalMouseEvent: testUtilsDom.triggerPositionalMouseEvent,
|
||||
triggerPositionalTapEvents: testUtilsDom.triggerPositionalTapEvents,
|
||||
dragAndDrop: testUtilsDom.dragAndDrop,
|
||||
find: testUtilsDom.findItem,
|
||||
getNode: testUtilsDom.getNode,
|
||||
openDatepicker: testUtilsDom.openDatepicker,
|
||||
click: testUtilsDom.click,
|
||||
clickFirst: testUtilsDom.clickFirst,
|
||||
clickLast: testUtilsDom.clickLast,
|
||||
triggerEvents: testUtilsDom.triggerEvents,
|
||||
triggerEvent: testUtilsDom.triggerEvent,
|
||||
},
|
||||
form: {
|
||||
clickEdit: testUtilsForm.clickEdit,
|
||||
clickSave: testUtilsForm.clickSave,
|
||||
clickCreate: testUtilsForm.clickCreate,
|
||||
clickDiscard: testUtilsForm.clickDiscard,
|
||||
reload: testUtilsForm.reload,
|
||||
},
|
||||
graph: {
|
||||
reload: testUtilsGraph.reload,
|
||||
},
|
||||
kanban: {
|
||||
reload: testUtilsKanban.reload,
|
||||
clickCreate: testUtilsKanban.clickCreate,
|
||||
quickCreate: testUtilsKanban.quickCreate,
|
||||
toggleGroupSettings: testUtilsKanban.toggleGroupSettings,
|
||||
toggleRecordDropdown: testUtilsKanban.toggleRecordDropdown,
|
||||
},
|
||||
modal: {
|
||||
clickButton: testUtilsModal.clickButton,
|
||||
},
|
||||
pivot: {
|
||||
clickMeasure: testUtilsPivot.clickMeasure,
|
||||
toggleMeasuresDropdown: testUtilsPivot.toggleMeasuresDropdown,
|
||||
reload: testUtilsPivot.reload,
|
||||
},
|
||||
fields: {
|
||||
many2one: {
|
||||
createAndEdit: testUtilsFields.clickM2OCreateAndEdit,
|
||||
clickOpenDropdown: testUtilsFields.clickOpenM2ODropdown,
|
||||
clickHighlightedItem: testUtilsFields.clickM2OHighlightedItem,
|
||||
clickItem: testUtilsFields.clickM2OItem,
|
||||
searchAndClickItem: testUtilsFields.searchAndClickM2OItem,
|
||||
},
|
||||
editInput: testUtilsFields.editInput,
|
||||
editSelect: testUtilsFields.editSelect,
|
||||
editAndTrigger: testUtilsFields.editAndTrigger,
|
||||
triggerKey: testUtilsFields.triggerKey,
|
||||
triggerKeydown: testUtilsFields.triggerKeydown,
|
||||
triggerKeyup: testUtilsFields.triggerKeyup,
|
||||
},
|
||||
file: {
|
||||
createFile: testUtilsFile.createFile,
|
||||
dragoverFile: testUtilsFile.dragoverFile,
|
||||
dropFile: testUtilsFile.dropFile,
|
||||
dropFiles: testUtilsFile.dropFiles,
|
||||
inputFiles: testUtilsFile.inputFiles,
|
||||
},
|
||||
|
||||
createComponent: testUtilsCreate.createComponent,
|
||||
createControlPanel: testUtilsCreate.createControlPanel,
|
||||
createAsyncView: testUtilsCreate.createView,
|
||||
createCalendarView: testUtilsCreate.createCalendarView,
|
||||
createView: testUtilsCreate.createView,
|
||||
createModel: testUtilsCreate.createModel,
|
||||
createParent: testUtilsCreate.createParent,
|
||||
makeTestPromise: makeTestPromise,
|
||||
makeTestPromiseWithAssert: makeTestPromiseWithAssert,
|
||||
nextMicrotaskTick: nextMicrotaskTick,
|
||||
nextTick: nextTick,
|
||||
owlCompatibilityExtraNextTick,
|
||||
prepareTarget: testUtilsCreate.prepareTarget,
|
||||
returnAfterNextAnimationFrame: testUtilsDom.returnAfterNextAnimationFrame,
|
||||
|
||||
// backward-compatibility
|
||||
addMockEnvironment: deprecated(testUtilsMock.addMockEnvironment, 'mock'),
|
||||
dragAndDrop: deprecated(testUtilsDom.dragAndDrop, 'dom'),
|
||||
getView: deprecated(testUtilsMock.getView, 'mock'),
|
||||
intercept: deprecated(testUtilsMock.intercept, 'mock'),
|
||||
openDatepicker: deprecated(testUtilsDom.openDatepicker, 'dom'),
|
||||
patch: deprecated(testUtilsMock.patch, 'mock'),
|
||||
patchDate: deprecated(testUtilsMock.patchDate, 'mock'),
|
||||
triggerKeypressEvent: deprecated(testUtilsDom.triggerKeypressEvent, 'dom'),
|
||||
triggerMouseEvent: deprecated(testUtilsDom.triggerMouseEvent, 'dom'),
|
||||
triggerPositionalMouseEvent: deprecated(testUtilsDom.triggerPositionalMouseEvent, 'dom'),
|
||||
unpatch: deprecated(testUtilsMock.unpatch, 'mock'),
|
||||
};
|
||||
});
|
||||
|
|
@ -1,360 +0,0 @@
|
|||
odoo.define('web.test_utils_control_panel', function (require) {
|
||||
"use strict";
|
||||
|
||||
const { click, findItem, getNode, triggerEvent } = require('web.test_utils_dom');
|
||||
const { editInput, editSelect, editAndTrigger } = require('web.test_utils_fields');
|
||||
|
||||
//-------------------------------------------------------------------------
|
||||
// Exported functions
|
||||
//-------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @param {EventTarget} el
|
||||
* @param {(number|string)} menuFinder
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async function toggleMenu(el, menuFinder) {
|
||||
const menu = findItem(el, `.dropdown > button`, menuFinder);
|
||||
await click(menu);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {EventTarget} el
|
||||
* @param {(number|string)} itemFinder
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async function toggleMenuItem(el, itemFinder) {
|
||||
const item = findItem(el, `.o_menu_item > a`, itemFinder);
|
||||
await click(item);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {EventTarget} el
|
||||
* @param {(number|string)} itemFinder
|
||||
* @param {(number|string)} optionFinder
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async function toggleMenuItemOption(el, itemFinder, optionFinder) {
|
||||
const item = findItem(el, `.o_menu_item > a`, itemFinder);
|
||||
const option = findItem(item.parentNode, '.o_item_option > a', optionFinder);
|
||||
await click(option);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {EventTarget} el
|
||||
* @param {(number|string)} itemFinder
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isItemSelected(el, itemFinder) {
|
||||
const item = findItem(el, `.o_menu_item > a`, itemFinder);
|
||||
return item.classList.contains('selected');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {EventTarget} el
|
||||
* @param {(number|string)} itemuFinder
|
||||
* @param {(number|string)} optionFinder
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isOptionSelected(el, itemFinder, optionFinder) {
|
||||
const item = findItem(el, `.o_menu_item > a`, itemFinder);
|
||||
const option = findItem(item.parentNode, '.o_item_option > a', optionFinder);
|
||||
return option.classList.contains('selected');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {EventTarget} el
|
||||
* @returns {string[]}
|
||||
*/
|
||||
function getMenuItemTexts(el) {
|
||||
return [...getNode(el).querySelectorAll(`.dropdown ul .o_menu_item`)].map(
|
||||
e => e.innerText.trim()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {EventTarget} el
|
||||
* @returns {HTMLButtonElement[]}
|
||||
*/
|
||||
function getButtons(el) {
|
||||
return [...getNode(el).querySelector((`div.o_cp_bottom div.o_cp_buttons`)).children];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {EventTarget} el
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async function toggleFilterMenu(el) {
|
||||
await click(getNode(el).querySelector(`.o_filter_menu button`));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {EventTarget} el
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async function toggleAddCustomFilter(el) {
|
||||
await click(getNode(el).querySelector(`button.o_add_custom_filter`));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {EventTarget} el
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async function applyFilter(el) {
|
||||
await click(getNode(el).querySelector(`div.o_add_filter_menu > button.o_apply_filter`));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {EventTarget} el
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async function addCondition(el) {
|
||||
await click(getNode(el).querySelector(`div.o_add_filter_menu > button.o_add_condition`));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {EventTarget} el
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async function toggleGroupByMenu(el) {
|
||||
await click(getNode(el).querySelector(`.o_group_by_menu button`));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {EventTarget} el
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async function toggleAddCustomGroup(el) {
|
||||
await click(getNode(el).querySelector(`span.o_add_custom_group_by`));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {EventTarget} el
|
||||
* @param {string} fieldName
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async function selectGroup(el, fieldName) {
|
||||
await editSelect(
|
||||
getNode(el).querySelector(`select.o_group_by_selector`),
|
||||
fieldName
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {EventTarget} el
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async function applyGroup(el) {
|
||||
await click(getNode(el).querySelector(`div.o_add_group_by_menu > button.o_apply_group_by`));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {EventTarget} el
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async function toggleFavoriteMenu(el) {
|
||||
await click(getNode(el).querySelector(`.o_favorite_menu button`));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {EventTarget} el
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async function toggleSaveFavorite(el) {
|
||||
await click(getNode(el).querySelector(`.o_favorite_menu .o_add_favorite .dropdown-item`));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {EventTarget} el
|
||||
* @param {string} name
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async function editFavoriteName(el, name) {
|
||||
await editInput(getNode(el).querySelector(`.o_favorite_menu .o_add_favorite input[type="text"]`), name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {EventTarget} el
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async function saveFavorite(el) {
|
||||
await click(getNode(el).querySelector(`.o_favorite_menu .o_add_favorite button.o_save_favorite`));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {EventTarget} el
|
||||
* @param {(string|number)} favoriteFinder
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async function deleteFavorite(el, favoriteFinder) {
|
||||
const favorite = findItem(el, `.o_favorite_menu .o_menu_item`, favoriteFinder);
|
||||
await click(favorite.querySelector('i.fa-trash'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {EventTarget} el
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async function toggleComparisonMenu(el) {
|
||||
await click(getNode(el).querySelector(`div.o_comparison_menu > button`));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {EventTarget} el
|
||||
* @returns {Promise}
|
||||
*/
|
||||
function getFacetTexts(el) {
|
||||
return [...getNode(el).querySelectorAll(`.o_searchview .o_searchview_facet`)].map(
|
||||
facet => facet.innerText.trim()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {EventTarget} el
|
||||
* @param {(string|number)} facetFinder
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async function removeFacet(el, facetFinder = 0) {
|
||||
const facet = findItem(el, `.o_searchview .o_searchview_facet`, facetFinder);
|
||||
await click(facet.querySelector('.o_facet_remove'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {EventTarget} el
|
||||
* @param {string} value
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async function editSearch(el, value) {
|
||||
await editInput(getNode(el).querySelector(`.o_searchview_input`), value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {EventTarget} el
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async function validateSearch(el) {
|
||||
await triggerEvent(
|
||||
getNode(el).querySelector(`.o_searchview_input`),
|
||||
'keydown', { key: 'Enter' }
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {EventTarget} el
|
||||
* @param {string} [menuFinder="Action"]
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async function toggleActionMenu(el, menuFinder = "Action") {
|
||||
const dropdown = findItem(el, `.o_cp_action_menus button`, menuFinder);
|
||||
await click(dropdown);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {EventTarget} el
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async function pagerPrevious(el) {
|
||||
await click(getNode(el).querySelector(`.o_pager button.o_pager_previous`));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {EventTarget} el
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async function pagerNext(el) {
|
||||
await click(getNode(el).querySelector(`.o_pager button.o_pager_next`));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {EventTarget} el
|
||||
* @returns {string}
|
||||
*/
|
||||
function getPagerValue(el) {
|
||||
const pagerValue = getNode(el).querySelector(`.o_pager_counter .o_pager_value`);
|
||||
switch (pagerValue.tagName) {
|
||||
case 'INPUT':
|
||||
return pagerValue.value;
|
||||
case 'SPAN':
|
||||
return pagerValue.innerText.trim();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {EventTarget} el
|
||||
* @returns {string}
|
||||
*/
|
||||
function getPagerSize(el) {
|
||||
return getNode(el).querySelector(`.o_pager_counter span.o_pager_limit`).innerText.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {EventTarget} el
|
||||
* @param {string} value
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async function setPagerValue(el, value) {
|
||||
let pagerValue = getNode(el).querySelector(`.o_pager_counter .o_pager_value`);
|
||||
if (pagerValue.tagName === 'SPAN') {
|
||||
await click(pagerValue);
|
||||
}
|
||||
pagerValue = getNode(el).querySelector(`.o_pager_counter input.o_pager_value`);
|
||||
if (!pagerValue) {
|
||||
throw new Error("Pager value is being edited and cannot be changed.");
|
||||
}
|
||||
await editAndTrigger(pagerValue, value, ['change', 'blur']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {EventTarget} el
|
||||
* @param {string} viewType
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async function switchView(el, viewType) {
|
||||
await click(getNode(el).querySelector(`button.o_switch_view.o_${viewType}`));
|
||||
}
|
||||
|
||||
return {
|
||||
// Generic interactions
|
||||
toggleMenu,
|
||||
toggleMenuItem,
|
||||
toggleMenuItemOption,
|
||||
isItemSelected,
|
||||
isOptionSelected,
|
||||
getMenuItemTexts,
|
||||
// Button interactions
|
||||
getButtons,
|
||||
// FilterMenu interactions
|
||||
toggleFilterMenu,
|
||||
toggleAddCustomFilter,
|
||||
applyFilter,
|
||||
addCondition,
|
||||
// GroupByMenu interactions
|
||||
toggleGroupByMenu,
|
||||
toggleAddCustomGroup,
|
||||
selectGroup,
|
||||
applyGroup,
|
||||
// FavoriteMenu interactions
|
||||
toggleFavoriteMenu,
|
||||
toggleSaveFavorite,
|
||||
editFavoriteName,
|
||||
saveFavorite,
|
||||
deleteFavorite,
|
||||
// ComparisonMenu interactions
|
||||
toggleComparisonMenu,
|
||||
// SearchBar interactions
|
||||
getFacetTexts,
|
||||
removeFacet,
|
||||
editSearch,
|
||||
validateSearch,
|
||||
// Action menus interactions
|
||||
toggleActionMenu,
|
||||
// Pager interactions
|
||||
pagerPrevious,
|
||||
pagerNext,
|
||||
getPagerValue,
|
||||
getPagerSize,
|
||||
setPagerValue,
|
||||
// View switcher
|
||||
switchView,
|
||||
};
|
||||
});
|
||||
|
|
@ -1,403 +0,0 @@
|
|||
odoo.define('web.test_utils_create', function (require) {
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Create Test Utils
|
||||
*
|
||||
* This module defines various utility functions to help creating mock widgets
|
||||
*
|
||||
* Note that all methods defined in this module are exported in the main
|
||||
* testUtils file.
|
||||
*/
|
||||
|
||||
const ActionMenus = require('web.ActionMenus');
|
||||
const concurrency = require('web.concurrency');
|
||||
const ControlPanel = require('web.ControlPanel');
|
||||
const { useListener } = require("@web/core/utils/hooks");
|
||||
const dom = require('web.dom');
|
||||
const makeTestEnvironment = require('web.test_env');
|
||||
const ActionModel = require('web.ActionModel');
|
||||
const Registry = require('web.Registry');
|
||||
const testUtilsMock = require('web.test_utils_mock');
|
||||
const Widget = require('web.Widget');
|
||||
const { destroy, getFixture, mount, useChild } = require('@web/../tests/helpers/utils');
|
||||
const { registerCleanup } = require("@web/../tests/helpers/cleanup");
|
||||
const { LegacyComponent } = require("@web/legacy/legacy_component");
|
||||
|
||||
const { Component, onMounted, onWillStart, useState, xml } = owl;
|
||||
|
||||
/**
|
||||
* Similar as createView, but specific for calendar views. Some calendar
|
||||
* tests need to trigger positional clicks on the DOM produced by fullcalendar.
|
||||
* Those tests must use this helper with option positionalClicks set to true.
|
||||
* This will move the rendered calendar to the body (required to do positional
|
||||
* clicks), and wait for a setTimeout(0) before returning, because fullcalendar
|
||||
* makes the calendar scroll to 6:00 in a setTimeout(0), which might have an
|
||||
* impact according to where we want to trigger positional clicks.
|
||||
*
|
||||
* @param {Object} params @see createView
|
||||
* @param {Object} [options]
|
||||
* @param {boolean} [options.positionalClicks=false]
|
||||
* @returns {Promise<CalendarController>}
|
||||
*/
|
||||
async function createCalendarView(params, options) {
|
||||
const calendar = await createView(params);
|
||||
if (!options || !options.positionalClicks) {
|
||||
return calendar;
|
||||
}
|
||||
const viewElements = [...document.getElementById('qunit-fixture').children];
|
||||
// prepend reset the scrollTop to zero so we restore it manually
|
||||
let fcScroller = document.querySelector('.fc-scroller');
|
||||
const scrollPosition = fcScroller.scrollTop;
|
||||
viewElements.forEach(el => document.body.prepend(el));
|
||||
fcScroller = document.querySelector('.fc-scroller');
|
||||
fcScroller.scrollTop = scrollPosition;
|
||||
|
||||
const destroy = calendar.destroy;
|
||||
calendar.destroy = () => {
|
||||
viewElements.forEach(el => el.remove());
|
||||
destroy();
|
||||
};
|
||||
await concurrency.delay(0);
|
||||
return calendar;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a simple component environment with a basic Parent component, an
|
||||
* extensible env and a mocked server. The returned value is the instance of
|
||||
* the given constructor.
|
||||
* @param {class} constructor Component class to instantiate
|
||||
* @param {Object} [params = {}]
|
||||
* @param {boolean} [params.debug]
|
||||
* @param {Object} [params.env]
|
||||
* @param {Object} [params.intercepts] object in which the keys represent the
|
||||
* intercepted event names and the values are their callbacks.
|
||||
* @param {Object} [params.props]
|
||||
* @returns {Promise<Component>} instance of `constructor`
|
||||
*/
|
||||
async function createComponent(constructor, params = {}) {
|
||||
if (!constructor) {
|
||||
throw new Error(`Missing argument "constructor".`);
|
||||
}
|
||||
if (!(constructor.prototype instanceof Component)) {
|
||||
throw new Error(`Argument "constructor" must be an Owl Component.`);
|
||||
}
|
||||
const cleanUp = await testUtilsMock.addMockEnvironmentOwl(Component, params);
|
||||
class Parent extends LegacyComponent {
|
||||
setup() {
|
||||
this.Component = constructor;
|
||||
this.state = useState(params.props || {});
|
||||
for (const eventName in params.intercepts || {}) {
|
||||
useListener(eventName, params.intercepts[eventName]);
|
||||
}
|
||||
useChild();
|
||||
}
|
||||
}
|
||||
Parent.template = xml`<t t-component="Component" t-props="state"/>`;
|
||||
|
||||
const target = getFixture();
|
||||
const env = Component.env;
|
||||
const parent = await mount(Parent, target, { env });
|
||||
registerCleanup(cleanUp);
|
||||
registerCleanup(() => {
|
||||
destroy(parent);
|
||||
});
|
||||
return parent.child;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Control Panel instance, with an extensible environment and
|
||||
* its related Control Panel Model. Event interception is done through
|
||||
* params['get-controller-query-params'] and params.search, for the two
|
||||
* available event handlers respectively.
|
||||
* @param {Object} [params={}]
|
||||
* @param {Object} [params.cpProps]
|
||||
* @param {Object} [params.cpModelConfig]
|
||||
* @param {boolean} [params.debug]
|
||||
* @param {Object} [params.env]
|
||||
* @returns {Object} useful control panel testing elements:
|
||||
* - controlPanel: the control panel instance
|
||||
* - el: the control panel HTML element
|
||||
* - helpers: a suite of bound helpers (see above functions for all
|
||||
* available helpers)
|
||||
*/
|
||||
async function createControlPanel(params = {}) {
|
||||
const env = makeTestEnvironment(params.env || {});
|
||||
const props = Object.assign({
|
||||
action: {},
|
||||
fields: {},
|
||||
}, params.cpProps);
|
||||
const globalConfig = Object.assign({
|
||||
context: {},
|
||||
domain: [],
|
||||
}, params.cpModelConfig);
|
||||
|
||||
if (globalConfig.arch && globalConfig.fields) {
|
||||
const model = "__mockmodel__";
|
||||
const serverParams = {
|
||||
model,
|
||||
data: { [model]: { fields: globalConfig.fields, records: [] } },
|
||||
};
|
||||
const mockServer = await testUtilsMock.addMockEnvironment(
|
||||
new Widget(),
|
||||
serverParams,
|
||||
);
|
||||
const { arch } = testUtilsMock.getView(mockServer, {
|
||||
arch: globalConfig.arch,
|
||||
fields: globalConfig.fields,
|
||||
model,
|
||||
viewOptions: { context: globalConfig.context },
|
||||
});
|
||||
Object.assign(globalConfig, { arch });
|
||||
}
|
||||
|
||||
globalConfig.env = env;
|
||||
const archs = (globalConfig.arch && { search: globalConfig.arch, }) || {};
|
||||
const { ControlPanel: controlPanelInfo, } = ActionModel.extractArchInfo(archs);
|
||||
const extensions = {
|
||||
ControlPanel: { archNodes: controlPanelInfo.children, },
|
||||
};
|
||||
|
||||
class Parent extends LegacyComponent {
|
||||
setup() {
|
||||
this.searchModel = new ActionModel(extensions, globalConfig);
|
||||
this.state = useState(props);
|
||||
useChild();
|
||||
onWillStart(async () => {
|
||||
await this.searchModel.load();
|
||||
});
|
||||
onMounted(() => {
|
||||
if (params['get-controller-query-params']) {
|
||||
this.searchModel.on('get-controller-query-params', this,
|
||||
params['get-controller-query-params']);
|
||||
}
|
||||
if (params.search) {
|
||||
this.searchModel.on('search', this, params.search);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
Parent.components = { ControlPanel };
|
||||
Parent.template = xml`
|
||||
<ControlPanel
|
||||
t-props="state"
|
||||
searchModel="searchModel"
|
||||
/>`;
|
||||
|
||||
const target = getFixture();
|
||||
const parent = await mount(Parent, target, { env });
|
||||
const controlPanel = parent.child;
|
||||
controlPanel.getQuery = () => parent.searchModel.get("query");
|
||||
registerCleanup(() => {
|
||||
destroy(parent);
|
||||
});
|
||||
return controlPanel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a model from given parameters.
|
||||
*
|
||||
* @param {Object} params This object will be given to addMockEnvironment, so
|
||||
* any parameters from that method applies
|
||||
* @param {Class} params.Model the model class to use
|
||||
* @returns {Model}
|
||||
*/
|
||||
async function createModel(params) {
|
||||
const widget = new Widget();
|
||||
|
||||
const model = new params.Model(widget, params);
|
||||
|
||||
await testUtilsMock.addMockEnvironment(widget, params);
|
||||
|
||||
// override the model's 'destroy' so that it calls 'destroy' on the widget
|
||||
// instead, as the widget is the parent of the model and the mockServer.
|
||||
model.destroy = function () {
|
||||
// remove the override to properly destroy the model when it will be
|
||||
// called the second time (by its parent)
|
||||
delete model.destroy;
|
||||
widget.destroy();
|
||||
};
|
||||
|
||||
return model;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a widget parent from given parameters.
|
||||
*
|
||||
* @param {Object} params This object will be given to addMockEnvironment, so
|
||||
* any parameters from that method applies
|
||||
* @returns {Promise<Widget>}
|
||||
*/
|
||||
async function createParent(params) {
|
||||
const widget = new Widget();
|
||||
await testUtilsMock.addMockEnvironment(widget, params);
|
||||
return widget;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a view from various parameters. Here, a view means a javascript
|
||||
* instance of an AbstractView class, such as a form view, a list view or a
|
||||
* kanban view.
|
||||
*
|
||||
* It returns the instance of the view, properly created, with all rpcs going
|
||||
* through a mock method using the data object as source, and already loaded/
|
||||
* started.
|
||||
*
|
||||
* @param {Object} params
|
||||
* @param {string} params.arch the xml (arch) of the view to be instantiated
|
||||
* @param {any[]} [params.domain] the initial domain for the view
|
||||
* @param {Object} [params.context] the initial context for the view
|
||||
* @param {string[]} [params.groupBy] the initial groupBy for the view
|
||||
* @param {Object[]} [params.favoriteFilters] the favorite filters one would like to have at initialization
|
||||
* @param {integer} [params.fieldDebounce=0] the debounce value to use for the
|
||||
* duration of the test.
|
||||
* @param {AbstractView} params.View the class that will be instantiated
|
||||
* @param {string} params.model a model name, will be given to the view
|
||||
* @param {Object} params.intercepts an object with event names as key, and
|
||||
* callback as value. Each key,value will be used to intercept the event.
|
||||
* Note that this is particularly useful if you want to intercept events going
|
||||
* up in the init process of the view, because there are no other way to do it
|
||||
* after this method returns
|
||||
* @param {Boolean} [params.doNotDisableAHref=false] will not preventDefault on the A elements of the view if true.
|
||||
* Default is false.
|
||||
* @param {Boolean} [params.touchScreen=false] will add the o_touch_device to the webclient (flag used to define a
|
||||
* device with a touch screen. Default value is false
|
||||
* @returns {Promise<AbstractController>} the instance of the view
|
||||
*/
|
||||
async function createView(params) {
|
||||
const target = prepareTarget(params.debug);
|
||||
const widget = new Widget();
|
||||
// reproduce the DOM environment of views
|
||||
const webClient = Object.assign(document.createElement('div'), {
|
||||
className: params.touchScreen ? 'o_web_client o_touch_device' : 'o_web_client',
|
||||
});
|
||||
const actionManager = Object.assign(document.createElement('div'), {
|
||||
className: 'o_action_manager',
|
||||
});
|
||||
const dialogContainer = Object.assign(document.createElement('div'), {
|
||||
className: 'o_dialog_container',
|
||||
});
|
||||
target.prepend(webClient);
|
||||
webClient.append(actionManager);
|
||||
webClient.append(dialogContainer);
|
||||
|
||||
// add mock environment: mock server, session, fieldviewget, ...
|
||||
const mockServer = await testUtilsMock.addMockEnvironment(widget, params);
|
||||
const viewInfo = testUtilsMock.getView(mockServer, params);
|
||||
|
||||
params.server = mockServer;
|
||||
|
||||
// create the view
|
||||
const View = params.View;
|
||||
const modelName = params.model || 'foo';
|
||||
const defaultAction = {
|
||||
res_model: modelName,
|
||||
context: {},
|
||||
type: 'ir.actions.act_window',
|
||||
};
|
||||
const viewOptions = Object.assign({
|
||||
action: Object.assign(defaultAction, params.action),
|
||||
view: { ...viewInfo, fields: mockServer.fieldsGet(params.model) },
|
||||
modelName: modelName,
|
||||
ids: 'res_id' in params ? [params.res_id] : undefined,
|
||||
currentId: 'res_id' in params ? params.res_id : undefined,
|
||||
domain: params.domain || [],
|
||||
context: params.context || {},
|
||||
hasActionMenus: false,
|
||||
}, params.viewOptions);
|
||||
// patch the View to handle the groupBy given in params, as we can't give it
|
||||
// in init (unlike the domain and context which can be set in the action)
|
||||
testUtilsMock.patch(View, {
|
||||
_updateMVCParams() {
|
||||
this._super(...arguments);
|
||||
this.loadParams.groupedBy = params.groupBy || viewOptions.groupBy || [];
|
||||
testUtilsMock.unpatch(View);
|
||||
},
|
||||
});
|
||||
if ('hasSelectors' in params) {
|
||||
viewOptions.hasSelectors = params.hasSelectors;
|
||||
}
|
||||
|
||||
let view;
|
||||
if (viewInfo.type === 'controlpanel' || viewInfo.type === 'search') {
|
||||
// TODO: probably needs to create an helper just for that
|
||||
view = new params.View({ viewInfo, modelName });
|
||||
} else {
|
||||
viewOptions.controlPanelFieldsView = Object.assign(testUtilsMock.getView(mockServer, {
|
||||
arch: params.archs && params.archs[params.model + ',false,search'] || '<search/>',
|
||||
fields: viewInfo.fields,
|
||||
model: params.model,
|
||||
}), { favoriteFilters: params.favoriteFilters });
|
||||
|
||||
view = new params.View(viewInfo, viewOptions);
|
||||
}
|
||||
|
||||
if (params.interceptsPropagate) {
|
||||
for (const name in params.interceptsPropagate) {
|
||||
testUtilsMock.intercept(widget, name, params.interceptsPropagate[name], true);
|
||||
}
|
||||
}
|
||||
|
||||
// Override the ActionMenus registry unless told otherwise.
|
||||
let actionMenusRegistry = ActionMenus.registry;
|
||||
if (params.actionMenusRegistry !== true) {
|
||||
ActionMenus.registry = new Registry();
|
||||
}
|
||||
|
||||
const viewController = await view.getController(widget);
|
||||
// override the view's 'destroy' so that it calls 'destroy' on the widget
|
||||
// instead, as the widget is the parent of the view and the mockServer.
|
||||
viewController.__destroy = viewController.destroy;
|
||||
viewController.destroy = function () {
|
||||
// remove the override to properly destroy the viewController and its children
|
||||
// when it will be called the second time (by its parent)
|
||||
delete viewController.destroy;
|
||||
widget.destroy();
|
||||
webClient.remove();
|
||||
if (params.actionMenusRegistry !== true) {
|
||||
ActionMenus.registry = actionMenusRegistry;
|
||||
}
|
||||
};
|
||||
|
||||
// render the viewController in a fragment as they must be able to render correctly
|
||||
// without being in the DOM
|
||||
const fragment = document.createDocumentFragment();
|
||||
await viewController.appendTo(fragment);
|
||||
dom.prepend(actionManager, fragment, {
|
||||
callbacks: [{ widget: viewController }],
|
||||
in_DOM: true,
|
||||
});
|
||||
|
||||
if (!params.doNotDisableAHref) {
|
||||
[...viewController.el.getElementsByTagName('A')].forEach(elem => {
|
||||
elem.addEventListener('click', ev => {
|
||||
ev.preventDefault();
|
||||
});
|
||||
});
|
||||
}
|
||||
return viewController;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the target (fixture or body) of the document and adds event listeners
|
||||
* to intercept custom or DOM events.
|
||||
*
|
||||
* @param {boolean} [debug=false] if true, the widget will be appended in
|
||||
* the DOM. Also, RPCs and uncaught OdooEvent will be logged
|
||||
* @returns {HTMLElement}
|
||||
*/
|
||||
function prepareTarget(debug = false) {
|
||||
document.body.classList.toggle('debug', debug);
|
||||
return debug ? document.body : document.getElementById('qunit-fixture');
|
||||
}
|
||||
|
||||
return {
|
||||
createCalendarView,
|
||||
createComponent,
|
||||
createControlPanel,
|
||||
createModel,
|
||||
createParent,
|
||||
createView,
|
||||
prepareTarget,
|
||||
};
|
||||
});
|
||||
|
|
@ -1,250 +0,0 @@
|
|||
odoo.define('web.test_utils_fields', function (require) {
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Field Test Utils
|
||||
*
|
||||
* This module defines various utility functions to help testing field widgets.
|
||||
*
|
||||
* Note that all methods defined in this module are exported in the main
|
||||
* testUtils file.
|
||||
*/
|
||||
|
||||
const testUtilsDom = require('web.test_utils_dom');
|
||||
|
||||
const ARROW_KEYS_MAPPING = {
|
||||
down: 'ArrowDown',
|
||||
left: 'ArrowLeft',
|
||||
right: 'ArrowRight',
|
||||
up: 'ArrowUp',
|
||||
};
|
||||
|
||||
//-------------------------------------------------------------------------
|
||||
// Public functions
|
||||
//-------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Autofills the input of a many2one field and clicks on the "Create and Edit" option.
|
||||
*
|
||||
* @param {string} fieldName
|
||||
* @param {string} text Used as default value for the record name
|
||||
* @see clickM2OItem
|
||||
*/
|
||||
async function clickM2OCreateAndEdit(fieldName, text = "ABC") {
|
||||
await clickOpenM2ODropdown(fieldName);
|
||||
const match = document.querySelector(`.o_field_many2one[name=${fieldName}] input`);
|
||||
await editInput(match, text);
|
||||
return clickM2OItem(fieldName, "Create and Edit");
|
||||
}
|
||||
|
||||
/**
|
||||
* Click on the active (highlighted) selection in a m2o dropdown.
|
||||
*
|
||||
* @param {string} fieldName
|
||||
* @param {[string]} selector if set, this will restrict the search for the m2o
|
||||
* input
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async function clickM2OHighlightedItem(fieldName, selector) {
|
||||
const m2oSelector = `${selector || ''} .o_field_many2one[name=${fieldName}] input`;
|
||||
const $dropdown = $(m2oSelector).autocomplete('widget');
|
||||
// clicking on an li (no matter which one), will select the focussed one
|
||||
return testUtilsDom.click($dropdown[0].querySelector('li'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Click on a menuitem in the m2o dropdown. This helper will target an element
|
||||
* which contains some specific text. Note that it assumes that the dropdown
|
||||
* is currently open.
|
||||
*
|
||||
* Example:
|
||||
* testUtils.fields.many2one.clickM2OItem('partner_id', 'George');
|
||||
*
|
||||
* @param {string} fieldName
|
||||
* @param {string} searchText
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async function clickM2OItem(fieldName, searchText) {
|
||||
const m2oSelector = `.o_field_many2one[name=${fieldName}] input`;
|
||||
const $dropdown = $(m2oSelector).autocomplete('widget');
|
||||
const $target = $dropdown.find(`li:contains(${searchText})`).first();
|
||||
if ($target.length !== 1 || !$target.is(':visible')) {
|
||||
throw new Error('Menu item should be visible');
|
||||
}
|
||||
$target.mouseenter(); // This is NOT a mouseenter event. See jquery.js:5516 for more headaches.
|
||||
return testUtilsDom.click($target);
|
||||
}
|
||||
|
||||
/**
|
||||
* Click to open the dropdown on a many2one
|
||||
*
|
||||
* @param {string} fieldName
|
||||
* @param {[string]} selector if set, this will restrict the search for the m2o
|
||||
* input
|
||||
* @returns {Promise<HTMLInputElement>} the main many2one input
|
||||
*/
|
||||
async function clickOpenM2ODropdown(fieldName, selector) {
|
||||
const m2oSelector = `${selector || ''} .o_field_many2one[name=${fieldName}] input`;
|
||||
const matches = document.querySelectorAll(m2oSelector);
|
||||
if (matches.length !== 1) {
|
||||
throw new Error(`cannot open m2o: selector ${selector} has been found ${matches.length} instead of 1`);
|
||||
}
|
||||
|
||||
await testUtilsDom.click(matches[0]);
|
||||
return matches[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the value of an element and then, trigger all specified events.
|
||||
* Note that this helper also checks the unicity of the target.
|
||||
*
|
||||
* Example:
|
||||
* testUtils.fields.editAndTrigger($('selector'), 'test', ['input', 'change']);
|
||||
*
|
||||
* @param {jQuery|EventTarget} el should target an input, textarea or select
|
||||
* @param {string|number} value
|
||||
* @param {string[]} events
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async function editAndTrigger(el, value, events) {
|
||||
if (el instanceof jQuery) {
|
||||
if (el.length !== 1) {
|
||||
throw new Error(`target ${el.selector} has length ${el.length} instead of 1`);
|
||||
}
|
||||
el.val(value);
|
||||
} else {
|
||||
el.value = value;
|
||||
}
|
||||
return testUtilsDom.triggerEvents(el, events);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the value of an input.
|
||||
*
|
||||
* Note that this helper also checks the unicity of the target.
|
||||
*
|
||||
* Example:
|
||||
* testUtils.fields.editInput($('selector'), 'somevalue');
|
||||
*
|
||||
* @param {jQuery|EventTarget} el should target an input, textarea or select
|
||||
* @param {string|number} value
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async function editInput(el, value) {
|
||||
return editAndTrigger(el, value, ['input']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the value of a select.
|
||||
*
|
||||
* Note that this helper also checks the unicity of the target.
|
||||
*
|
||||
* Example:
|
||||
* testUtils.fields.editSelect($('selector'), 'somevalue');
|
||||
*
|
||||
* @param {jQuery|EventTarget} el should target an input, textarea or select
|
||||
* @param {string|number} value
|
||||
* @returns {Promise}
|
||||
*/
|
||||
function editSelect(el, value) {
|
||||
return editAndTrigger(el, value, ['change']);
|
||||
}
|
||||
|
||||
/**
|
||||
* This helper is useful to test many2one fields. Here is what it does:
|
||||
* - click to open the dropdown
|
||||
* - enter a search string in the input
|
||||
* - wait for the selection
|
||||
* - click on the requested menuitem, or the active one by default
|
||||
*
|
||||
* Example:
|
||||
* testUtils.fields.many2one.searchAndClickM2OItem('partner_id', {search: 'George'});
|
||||
*
|
||||
* @param {string} fieldName
|
||||
* @param {[Object]} [options = {}]
|
||||
* @param {[string]} [options.selector]
|
||||
* @param {[string]} [options.search]
|
||||
* @param {[string]} [options.item]
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async function searchAndClickM2OItem(fieldName, options = {}) {
|
||||
const input = await clickOpenM2ODropdown(fieldName, options.selector);
|
||||
if (options.search) {
|
||||
await editInput(input, options.search);
|
||||
}
|
||||
if (options.item) {
|
||||
return clickM2OItem(fieldName, options.item);
|
||||
} else {
|
||||
return clickM2OHighlightedItem(fieldName, options.selector);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to trigger a key event on an element.
|
||||
*
|
||||
* @param {string} type type of key event ('press', 'up' or 'down')
|
||||
* @param {jQuery} $el
|
||||
* @param {number|string} keyCode used as number, but if string, it'll check if
|
||||
* the string corresponds to a key -otherwise it will keep only the first
|
||||
* char to get a letter key- and convert it into a keyCode.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
function triggerKey(type, $el, keyCode) {
|
||||
type = 'key' + type;
|
||||
const params = {};
|
||||
if (typeof keyCode === 'string') {
|
||||
// Key (new API)
|
||||
if (keyCode in ARROW_KEYS_MAPPING) {
|
||||
params.key = ARROW_KEYS_MAPPING[keyCode];
|
||||
} else {
|
||||
params.key = keyCode[0].toUpperCase() + keyCode.slice(1).toLowerCase();
|
||||
}
|
||||
// KeyCode/which (jQuery)
|
||||
if (keyCode.length > 1) {
|
||||
keyCode = keyCode.toUpperCase();
|
||||
keyCode = $.ui.keyCode[keyCode];
|
||||
} else {
|
||||
keyCode = keyCode.charCodeAt(0);
|
||||
}
|
||||
}
|
||||
params.keyCode = keyCode;
|
||||
params.which = keyCode;
|
||||
return testUtilsDom.triggerEvent($el, type, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to trigger a keydown event on an element.
|
||||
*
|
||||
* @param {jQuery} $el
|
||||
* @param {number|string} keyCode @see triggerKey
|
||||
* @returns {Promise}
|
||||
*/
|
||||
function triggerKeydown($el, keyCode) {
|
||||
return triggerKey('down', $el, keyCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to trigger a keyup event on an element.
|
||||
*
|
||||
* @param {jQuery} $el
|
||||
* @param {number|string} keyCode @see triggerKey
|
||||
* @returns {Promise}
|
||||
*/
|
||||
function triggerKeyup($el, keyCode) {
|
||||
return triggerKey('up', $el, keyCode);
|
||||
}
|
||||
|
||||
return {
|
||||
clickM2OCreateAndEdit,
|
||||
clickM2OHighlightedItem,
|
||||
clickM2OItem,
|
||||
clickOpenM2ODropdown,
|
||||
editAndTrigger,
|
||||
editInput,
|
||||
editSelect,
|
||||
searchAndClickM2OItem,
|
||||
triggerKey,
|
||||
triggerKeydown,
|
||||
triggerKeyup,
|
||||
};
|
||||
});
|
||||
|
|
@ -1,158 +0,0 @@
|
|||
odoo.define('web.test_utils_file', function () {
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* FILE Test Utils
|
||||
*
|
||||
* This module defines various utility functions to help simulate events with
|
||||
* files, such as drag-and-drop.
|
||||
*
|
||||
* Note that all methods defined in this module are exported in the main
|
||||
* testUtils file.
|
||||
*/
|
||||
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Private functions
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Create a fake object 'dataTransfer', linked to some files, which is passed to
|
||||
* drag and drop events.
|
||||
*
|
||||
* @param {Object[]} files
|
||||
* @returns {Object}
|
||||
*/
|
||||
function _createFakeDataTransfer(files) {
|
||||
return {
|
||||
dropEffect: 'all',
|
||||
effectAllowed: 'all',
|
||||
files,
|
||||
getData: function () {
|
||||
return files;
|
||||
},
|
||||
items: [],
|
||||
types: ['Files'],
|
||||
};
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Public functions
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Create a file object, which can be used for drag-and-drop.
|
||||
*
|
||||
* @param {Object} data
|
||||
* @param {string} data.name
|
||||
* @param {string} data.content
|
||||
* @param {string} data.contentType
|
||||
* @returns {Promise<Object>} resolved with file created
|
||||
*/
|
||||
function createFile(data) {
|
||||
// Note: this is only supported by Chrome, and does not work in Incognito mode
|
||||
return new Promise(function (resolve, reject) {
|
||||
var requestFileSystem = window.requestFileSystem || window.webkitRequestFileSystem;
|
||||
if (!requestFileSystem) {
|
||||
throw new Error('FileSystem API is not supported');
|
||||
}
|
||||
requestFileSystem(window.TEMPORARY, 1024 * 1024, function (fileSystem) {
|
||||
fileSystem.root.getFile(data.name, { create: true }, function (fileEntry) {
|
||||
fileEntry.createWriter(function (fileWriter) {
|
||||
fileWriter.onwriteend = function (e) {
|
||||
fileSystem.root.getFile(data.name, {}, function (fileEntry) {
|
||||
fileEntry.file(function (file) {
|
||||
resolve(file);
|
||||
});
|
||||
});
|
||||
};
|
||||
fileWriter.write(new Blob([ data.content ], { type: data.contentType }));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Drag a file over a DOM element
|
||||
*
|
||||
* @param {$.Element} $el
|
||||
* @param {Object} file must have been created beforehand (@see createFile)
|
||||
*/
|
||||
function dragoverFile($el, file) {
|
||||
var ev = new Event('dragover', { bubbles: true });
|
||||
Object.defineProperty(ev, 'dataTransfer', {
|
||||
value: _createFakeDataTransfer(file),
|
||||
});
|
||||
$el[0].dispatchEvent(ev);
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop a file on a DOM element.
|
||||
*
|
||||
* @param {$.Element} $el
|
||||
* @param {Object} file must have been created beforehand (@see createFile)
|
||||
*/
|
||||
function dropFile($el, file) {
|
||||
var ev = new Event('drop', { bubbles: true, });
|
||||
Object.defineProperty(ev, 'dataTransfer', {
|
||||
value: _createFakeDataTransfer([file]),
|
||||
});
|
||||
$el[0].dispatchEvent(ev);
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop some files on a DOM element.
|
||||
*
|
||||
* @param {$.Element} $el
|
||||
* @param {Object[]} files must have been created beforehand (@see createFile)
|
||||
*/
|
||||
function dropFiles($el, files) {
|
||||
var ev = new Event('drop', { bubbles: true, });
|
||||
Object.defineProperty(ev, 'dataTransfer', {
|
||||
value: _createFakeDataTransfer(files),
|
||||
});
|
||||
$el[0].dispatchEvent(ev);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set files in a file input
|
||||
*
|
||||
* @param {DOM.Element} el
|
||||
* @param {Object[]} files must have been created beforehand
|
||||
* @see testUtils.file.createFile
|
||||
*/
|
||||
function inputFiles(el, files) {
|
||||
// could not use _createFakeDataTransfer as el.files assignation will only
|
||||
// work with a real FileList object.
|
||||
const dataTransfer = new window.DataTransfer();
|
||||
for (const file of files) {
|
||||
dataTransfer.items.add(file);
|
||||
}
|
||||
el.files = dataTransfer.files;
|
||||
/**
|
||||
* Changing files programatically is not supposed to trigger the event but
|
||||
* it does in Chrome versions before 73 (which is on runbot), so in that
|
||||
* case there is no need to make a manual dispatch, because it would lead to
|
||||
* the files being added twice.
|
||||
*/
|
||||
const versionRaw = navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./);
|
||||
const chromeVersion = versionRaw ? parseInt(versionRaw[2], 10) : false;
|
||||
if (!chromeVersion || chromeVersion >= 73) {
|
||||
el.dispatchEvent(new Event('change'));
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Exposed API
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
return {
|
||||
createFile: createFile,
|
||||
dragoverFile: dragoverFile,
|
||||
dropFile: dropFile,
|
||||
dropFiles,
|
||||
inputFiles,
|
||||
};
|
||||
|
||||
});
|
||||
|
|
@ -1,74 +0,0 @@
|
|||
odoo.define('web.test_utils_form', function (require) {
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Form Test Utils
|
||||
*
|
||||
* This module defines various utility functions to help test form views.
|
||||
*
|
||||
* Note that all methods defined in this module are exported in the main
|
||||
* testUtils file.
|
||||
*/
|
||||
|
||||
var testUtilsDom = require('web.test_utils_dom');
|
||||
|
||||
/**
|
||||
* Clicks on the Edit button in a form view, to set it to edit mode. Note that
|
||||
* it checks that the button is visible, so calling this method in edit mode
|
||||
* will fail.
|
||||
*
|
||||
* @param {FormController} form
|
||||
*/
|
||||
function clickEdit(form) {
|
||||
return testUtilsDom.click(form.$buttons.find('.o_form_button_edit'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks on the Save button in a form view. Note that this method checks that
|
||||
* the Save button is visible.
|
||||
*
|
||||
* @param {FormController} form
|
||||
*/
|
||||
function clickSave(form) {
|
||||
return testUtilsDom.click(form.$buttons.find('.o_form_button_save'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks on the Create button in a form view. Note that this method checks that
|
||||
* the Create button is visible.
|
||||
*
|
||||
* @param {FormController} form
|
||||
*/
|
||||
function clickCreate(form) {
|
||||
return testUtilsDom.click(form.$buttons.find('.o_form_button_create'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks on the Discard button in a form view. Note that this method checks that
|
||||
* the Discard button is visible.
|
||||
*
|
||||
* @param {FormController} form
|
||||
*/
|
||||
function clickDiscard(form) {
|
||||
return testUtilsDom.click(form.$buttons.find('.o_form_button_cancel'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Reloads a form view.
|
||||
*
|
||||
* @param {FormController} form
|
||||
* @param {[Object]} params given to the controller reload method
|
||||
*/
|
||||
function reload(form, params) {
|
||||
return form.reload(params);
|
||||
}
|
||||
|
||||
return {
|
||||
clickEdit: clickEdit,
|
||||
clickSave: clickSave,
|
||||
clickCreate: clickCreate,
|
||||
clickDiscard: clickDiscard,
|
||||
reload: reload,
|
||||
};
|
||||
|
||||
});
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
odoo.define('web.test_utils_graph', function () {
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Graph Test Utils
|
||||
*
|
||||
* This module defines various utility functions to help test graph views.
|
||||
*
|
||||
* Note that all methods defined in this module are exported in the main
|
||||
* testUtils file.
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* Reloads a graph view.
|
||||
*
|
||||
* @param {GraphController} graph
|
||||
* @param {[Object]} params given to the controller reload method
|
||||
*/
|
||||
function reload(graph, params) {
|
||||
return graph.reload(params);
|
||||
}
|
||||
|
||||
return {
|
||||
reload: reload,
|
||||
};
|
||||
|
||||
});
|
||||
|
|
@ -1,102 +0,0 @@
|
|||
odoo.define('web.test_utils_kanban', function (require) {
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Kanban Test Utils
|
||||
*
|
||||
* This module defines various utility functions to help testing kanban views.
|
||||
*
|
||||
* Note that all methods defined in this module are exported in the main
|
||||
* testUtils file.
|
||||
*/
|
||||
|
||||
var testUtilsDom = require('web.test_utils_dom');
|
||||
var testUtilsFields = require('web.test_utils_fields');
|
||||
|
||||
/**
|
||||
* Clicks on the Create button in a kanban view. Note that this method checks that
|
||||
* the Create button is visible.
|
||||
*
|
||||
* @param {KanbanController} kanban
|
||||
* @returns {Promise}
|
||||
*/
|
||||
function clickCreate(kanban) {
|
||||
return testUtilsDom.click(kanban.$buttons.find('.o-kanban-button-new'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the settings menu for a column (in a grouped kanban view)
|
||||
*
|
||||
* @param {jQuery} $column
|
||||
* @returns {Promise}
|
||||
*/
|
||||
function toggleGroupSettings($column) {
|
||||
var $dropdownToggler = $column.find('.o_kanban_config > a.dropdown-toggle');
|
||||
if (!$dropdownToggler.is(':visible')) {
|
||||
$dropdownToggler.css('display', 'block');
|
||||
}
|
||||
return testUtilsDom.click($dropdownToggler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit a value in a quickcreate form view (this method assumes that the quick
|
||||
* create feature is active, and a sub form view is open)
|
||||
*
|
||||
* @param {kanbanController} kanban
|
||||
* @param {string|number} value
|
||||
* @param {[string]} fieldName
|
||||
* @returns {Promise}
|
||||
*/
|
||||
function quickCreate(kanban, value, fieldName) {
|
||||
var additionalSelector = fieldName ? ('[name=' + fieldName + ']'): '';
|
||||
var enterEvent = $.Event(
|
||||
'keydown',
|
||||
{
|
||||
which: $.ui.keyCode.ENTER,
|
||||
keyCode: $.ui.keyCode.ENTER,
|
||||
}
|
||||
);
|
||||
return testUtilsFields.editAndTrigger(
|
||||
kanban.$('.o_kanban_quick_create input' + additionalSelector),
|
||||
value,
|
||||
['input', enterEvent]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reloads a kanban view.
|
||||
*
|
||||
* @param {KanbanController} kanban
|
||||
* @param {[Object]} params given to the controller reload method
|
||||
* @returns {Promise}
|
||||
*/
|
||||
function reload(kanban, params) {
|
||||
return kanban.reload(params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the setting dropdown of a kanban record. Note that the template of a
|
||||
* kanban record is not standardized, so this method will fail if the template
|
||||
* does not comply with the usual dom structure.
|
||||
*
|
||||
* @param {jQuery} $record
|
||||
* @returns {Promise}
|
||||
*/
|
||||
function toggleRecordDropdown($record) {
|
||||
var $dropdownToggler = $record.find('.o_dropdown_kanban > a.dropdown-toggle');
|
||||
if (!$dropdownToggler.is(':visible')) {
|
||||
$dropdownToggler.css('display', 'block');
|
||||
}
|
||||
return testUtilsDom.click($dropdownToggler);
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
clickCreate: clickCreate,
|
||||
quickCreate: quickCreate,
|
||||
reload: reload,
|
||||
toggleGroupSettings: toggleGroupSettings,
|
||||
toggleRecordDropdown: toggleRecordDropdown,
|
||||
};
|
||||
|
||||
});
|
||||
|
|
@ -1,742 +0,0 @@
|
|||
odoo.define('web.test_utils_mock', function (require) {
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Mock Test Utils
|
||||
*
|
||||
* This module defines various utility functions to help mocking data.
|
||||
*
|
||||
* Note that all methods defined in this module are exported in the main
|
||||
* testUtils file.
|
||||
*/
|
||||
|
||||
const AbstractStorageService = require('web.AbstractStorageService');
|
||||
const AjaxService = require('web.AjaxService');
|
||||
const basic_fields = require('web.basic_fields');
|
||||
const Bus = require('web.Bus');
|
||||
const config = require('web.config');
|
||||
const core = require('web.core');
|
||||
const dom = require('web.dom');
|
||||
const FormController = require('web.FormController');
|
||||
const makeTestEnvironment = require('web.test_env');
|
||||
const MockServer = require('web.MockServer');
|
||||
const RamStorage = require('web.RamStorage');
|
||||
const session = require('web.session');
|
||||
const { patchWithCleanup, patchDate } = require("@web/../tests/helpers/utils");
|
||||
const { browser } = require("@web/core/browser/browser");
|
||||
const { assets } = require("@web/core/assets");
|
||||
const { processArch } = require("@web/legacy/legacy_load_views");
|
||||
|
||||
const { Component } = require("@odoo/owl");
|
||||
const DebouncedField = basic_fields.DebouncedField;
|
||||
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Private functions
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Returns a mocked environment to be used by OWL components in tests, with
|
||||
* requested services (+ ajax, local_storage and session_storage) deployed.
|
||||
*
|
||||
* @private
|
||||
* @param {Object} params
|
||||
* @param {Bus} [params.bus]
|
||||
* @param {boolean} [params.debug]
|
||||
* @param {Object} [params.env]
|
||||
* @param {Bus} [params.env.bus]
|
||||
* @param {Object} [params.env.dataManager]
|
||||
* @param {Object} [params.env.services]
|
||||
* @param {Object[]} [params.favoriteFilters]
|
||||
* @param {Object} [params.services]
|
||||
* @param {Object} [params.session]
|
||||
* @param {MockServer} [mockServer]
|
||||
* @returns {Promise<Object>} env
|
||||
*/
|
||||
async function _getMockedOwlEnv(params, mockServer) {
|
||||
params.env = params.env || {};
|
||||
|
||||
const database = {parameters: params.translateParameters || {}};
|
||||
|
||||
// build the env
|
||||
const favoriteFilters = params.favoriteFilters;
|
||||
const debug = params.debug;
|
||||
const services = {};
|
||||
const env = Object.assign({}, params.env, {
|
||||
_t: params.env && params.env._t || Object.assign((s => s), { database }),
|
||||
browser: Object.assign({
|
||||
fetch: (resource, init) => mockServer.performFetch(resource, init),
|
||||
}, params.env.browser),
|
||||
bus: params.bus || params.env.bus || new Bus(),
|
||||
dataManager: Object.assign({
|
||||
load_action: (actionID, context) => {
|
||||
return mockServer.performRpc('/web/action/load', {
|
||||
action_id: actionID,
|
||||
additional_context: context,
|
||||
});
|
||||
},
|
||||
load_views: (params, options) => {
|
||||
return mockServer.performRpc('/web/dataset/call_kw/' + params.model, {
|
||||
args: [],
|
||||
kwargs: {
|
||||
context: params.context,
|
||||
options: options,
|
||||
views: params.views_descr,
|
||||
},
|
||||
method: 'get_views',
|
||||
model: params.model,
|
||||
}).then(function (views) {
|
||||
views = _.mapObject(views, viewParams => {
|
||||
return getView(mockServer, viewParams);
|
||||
});
|
||||
if (favoriteFilters && 'search' in views) {
|
||||
views.search.favoriteFilters = favoriteFilters;
|
||||
}
|
||||
return views;
|
||||
});
|
||||
},
|
||||
load_filters: params => {
|
||||
if (debug) {
|
||||
console.log('[mock] load_filters', params);
|
||||
}
|
||||
return Promise.resolve([]);
|
||||
},
|
||||
}, params.env.dataManager),
|
||||
services: Object.assign(services, params.env.services),
|
||||
session: params.env.session || params.session || {},
|
||||
});
|
||||
|
||||
// deploy services into the env
|
||||
// determine services to instantiate (classes), and already register function services
|
||||
const servicesToDeploy = {};
|
||||
for (const name in params.services || {}) {
|
||||
const Service = params.services[name];
|
||||
if (Service.constructor.name === 'Class') {
|
||||
servicesToDeploy[name] = Service;
|
||||
} else {
|
||||
services[name] = Service;
|
||||
}
|
||||
}
|
||||
// always deploy ajax, local storage and session storage
|
||||
if (!servicesToDeploy.ajax) {
|
||||
const MockedAjaxService = AjaxService.extend({
|
||||
rpc: mockServer.performRpc.bind(mockServer),
|
||||
});
|
||||
services.ajax = new MockedAjaxService(env);
|
||||
}
|
||||
const RamStorageService = AbstractStorageService.extend({
|
||||
storage: new RamStorage(),
|
||||
});
|
||||
if (!servicesToDeploy.local_storage) {
|
||||
services.local_storage = new RamStorageService(env);
|
||||
}
|
||||
if (!servicesToDeploy.session_storage) {
|
||||
services.session_storage = new RamStorageService(env);
|
||||
}
|
||||
// deploy other requested services
|
||||
let done = false;
|
||||
while (!done) {
|
||||
const serviceName = Object.keys(servicesToDeploy).find(serviceName => {
|
||||
const Service = servicesToDeploy[serviceName];
|
||||
return Service.prototype.dependencies.every(depName => {
|
||||
return env.services[depName];
|
||||
});
|
||||
});
|
||||
if (serviceName) {
|
||||
const Service = servicesToDeploy[serviceName];
|
||||
services[serviceName] = new Service(env);
|
||||
delete servicesToDeploy[serviceName];
|
||||
services[serviceName].start();
|
||||
} else {
|
||||
const serviceNames = _.keys(servicesToDeploy);
|
||||
if (serviceNames.length) {
|
||||
console.warn("Non loaded services:", serviceNames);
|
||||
}
|
||||
done = true;
|
||||
}
|
||||
}
|
||||
// wait for asynchronous services to properly start
|
||||
await new Promise(setTimeout);
|
||||
|
||||
return env;
|
||||
}
|
||||
/**
|
||||
* This function is used to mock global objects (session, config...) in tests.
|
||||
* It is necessary for legacy widgets. It returns a cleanUp function to call at
|
||||
* the end of the test.
|
||||
*
|
||||
* The function could be removed as soon as we do not support legacy widgets
|
||||
* anymore.
|
||||
*
|
||||
* @private
|
||||
* @param {Object} params
|
||||
* @param {Object} [params.config] if given, it is used to extend the global
|
||||
* config,
|
||||
* @param {Object} [params.session] if given, it is used to extend the current,
|
||||
* real session.
|
||||
* @param {Object} [params.translateParameters] if given, it will be used to
|
||||
* extend the core._t.database.parameters object.
|
||||
* @returns {function} a cleanUp function to restore everything, to call at the
|
||||
* end of the test
|
||||
*/
|
||||
function _mockGlobalObjects(params) {
|
||||
// store initial session state (for restoration)
|
||||
const initialSession = Object.assign({}, session);
|
||||
const sessionPatch = Object.assign({
|
||||
getTZOffset() { return 0; },
|
||||
async user_has_group() { return false; },
|
||||
}, params.session);
|
||||
// patch session
|
||||
Object.assign(session, sessionPatch);
|
||||
|
||||
// patch config
|
||||
let initialConfig;
|
||||
if ('config' in params) {
|
||||
initialConfig = Object.assign({}, config);
|
||||
initialConfig.device = Object.assign({}, config.device);
|
||||
if ('device' in params.config) {
|
||||
Object.assign(config.device, params.config.device);
|
||||
}
|
||||
if ('debug' in params.config) {
|
||||
odoo.debug = params.config.debug;
|
||||
}
|
||||
}
|
||||
|
||||
// patch translate params
|
||||
let initialParameters;
|
||||
if ('translateParameters' in params) {
|
||||
initialParameters = Object.assign({}, core._t.database.parameters);
|
||||
Object.assign(core._t.database.parameters, params.translateParameters);
|
||||
}
|
||||
|
||||
// build the cleanUp function to restore everything at the end of the test
|
||||
function cleanUp() {
|
||||
let key;
|
||||
for (key in sessionPatch) {
|
||||
delete session[key];
|
||||
}
|
||||
Object.assign(session, initialSession);
|
||||
if ('config' in params) {
|
||||
for (key in config) {
|
||||
delete config[key];
|
||||
}
|
||||
_.extend(config, initialConfig);
|
||||
}
|
||||
if ('translateParameters' in params) {
|
||||
for (key in core._t.database.parameters) {
|
||||
delete core._t.database.parameters[key];
|
||||
}
|
||||
_.extend(core._t.database.parameters, initialParameters);
|
||||
}
|
||||
}
|
||||
|
||||
return cleanUp;
|
||||
}
|
||||
/**
|
||||
* logs all event going through the target widget.
|
||||
*
|
||||
* @param {Widget} widget
|
||||
*/
|
||||
function _observe(widget) {
|
||||
var _trigger_up = widget._trigger_up.bind(widget);
|
||||
widget._trigger_up = function (event) {
|
||||
console.log('%c[event] ' + event.name, 'color: blue; font-weight: bold;', event);
|
||||
_trigger_up(event);
|
||||
};
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Public functions
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* performs a get_view, and mocks the postprocessing done by the
|
||||
* data_manager to return an equivalent structure.
|
||||
*
|
||||
* @param {MockServer} server
|
||||
* @param {Object} params
|
||||
* @param {string} params.model
|
||||
* @returns {Object} an object with 3 keys: arch, fields and viewFields
|
||||
*/
|
||||
function getView(server, params) {
|
||||
var view = server.getView(params);
|
||||
const fields = server.fieldsGet(params.model);
|
||||
// mock the structure produced by the DataManager
|
||||
const models = { [params.model]: fields };
|
||||
for (const modelName of view.models) {
|
||||
models[modelName] = models[modelName] || server.fieldsGet(modelName);
|
||||
}
|
||||
const { arch, viewFields } = processArch(view.arch, view.type, params.model, models);
|
||||
return {
|
||||
arch,
|
||||
fields,
|
||||
model: view.model,
|
||||
toolbar: view.toolbar,
|
||||
type: view.type,
|
||||
viewFields,
|
||||
view_id: view.id,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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'.
|
||||
*
|
||||
* Note that this method really intercepts the event if @propagate is not set.
|
||||
* It will not be propagated further, and even the handlers on the target will
|
||||
* not fire.
|
||||
*
|
||||
* @param {Widget} widget the target widget (any Odoo widget)
|
||||
* @param {string} eventName description of the event
|
||||
* @param {function} fn callback executed when the even is intercepted
|
||||
* @param {boolean} [propagate=false]
|
||||
*/
|
||||
function intercept(widget, eventName, fn, propagate) {
|
||||
var _trigger_up = widget._trigger_up.bind(widget);
|
||||
widget._trigger_up = function (event) {
|
||||
if (event.name === eventName) {
|
||||
fn(event);
|
||||
if (!propagate) { return; }
|
||||
}
|
||||
_trigger_up(event);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a mock environment to test Owl Components. This function generates a test
|
||||
* env and sets it on the given Component. It also has several side effects,
|
||||
* like patching the global session or config objects. It returns a cleanup
|
||||
* function to call at the end of the test.
|
||||
*
|
||||
* @param {Component} Component
|
||||
* @param {Object} [params]
|
||||
* @param {Object} [params.actions]
|
||||
* @param {Object} [params.archs]
|
||||
* @param {string} [params.currentDate]
|
||||
* @param {Object} [params.data]
|
||||
* @param {boolean} [params.debug]
|
||||
* @param {function} [params.mockFetch]
|
||||
* @param {function} [params.mockRPC]
|
||||
* @param {number} [params.fieldDebounce=0] the value of the DEBOUNCE attribute
|
||||
* of fields
|
||||
* @param {boolean} [params.debounce=true] if false, patch _.debounce to remove
|
||||
* its behavior
|
||||
* @param {boolean} [params.throttle=false] by default, _.throttle is patched to
|
||||
* remove its behavior, except if this params is set to true
|
||||
* @param {boolean} [params.mockSRC=false] if true, redirect src GET requests to
|
||||
* the mockServer
|
||||
* @param {MockServer} [mockServer]
|
||||
* @returns {Promise<function>} the cleanup function
|
||||
*/
|
||||
async function addMockEnvironmentOwl(Component, params, mockServer) {
|
||||
params = params || {};
|
||||
|
||||
// instantiate a mockServer if not provided
|
||||
if (!mockServer) {
|
||||
let Server = MockServer;
|
||||
if (params.mockFetch) {
|
||||
Server = Server.extend({ _performFetch: params.mockFetch });
|
||||
}
|
||||
if (params.mockRPC) {
|
||||
Server = Server.extend({ _performRpc: params.mockRPC });
|
||||
}
|
||||
mockServer = new Server(params.data, {
|
||||
actions: params.actions,
|
||||
archs: params.archs,
|
||||
currentDate: params.currentDate,
|
||||
debug: params.debug,
|
||||
});
|
||||
}
|
||||
|
||||
patchWithCleanup(browser, {
|
||||
fetch: async (url, args) => {
|
||||
const result = await mockServer.performFetch(url, args || {});
|
||||
return {
|
||||
json: () => result,
|
||||
text: () => result,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
if (params.mockFetch) {
|
||||
const { loadJS, loadCSS } = assets;
|
||||
patchWithCleanup(assets, {
|
||||
loadJS: async function (ressource) {
|
||||
let res = await params.mockFetch(ressource, {});
|
||||
if (res === undefined) {
|
||||
res = await loadJS(ressource);
|
||||
} else {
|
||||
console.log("%c[assets] fetch (mock) JS ressource " + ressource, "color: #66e; font-weight: bold;");
|
||||
}
|
||||
return res;
|
||||
},
|
||||
loadCSS: async function (ressource) {
|
||||
let res = await params.mockFetch(ressource, {});
|
||||
if (res === undefined) {
|
||||
res = await loadCSS(ressource);
|
||||
} else {
|
||||
console.log("%c[assets] fetch (mock) CSS ressource " + ressource, "color: #66e; font-weight: bold;");
|
||||
}
|
||||
return res;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// remove the multi-click delay for the quick edit in form view
|
||||
const initialQuickEditDelay = FormController.prototype.multiClickTime;
|
||||
FormController.prototype.multiClickTime = params.formMultiClickTime || 0;
|
||||
|
||||
// make sure the debounce value for input fields is set to 0
|
||||
const initialDebounceValue = DebouncedField.prototype.DEBOUNCE;
|
||||
DebouncedField.prototype.DEBOUNCE = params.fieldDebounce || 0;
|
||||
const initialDOMDebounceValue = dom.DEBOUNCE;
|
||||
dom.DEBOUNCE = 0;
|
||||
|
||||
// patch underscore debounce/throttle functions
|
||||
const initialDebounce = _.debounce;
|
||||
if (params.debounce === false) {
|
||||
_.debounce = function (func) {
|
||||
return func;
|
||||
};
|
||||
}
|
||||
// fixme: throttle is inactive by default, should we make it explicit ?
|
||||
const initialThrottle = _.throttle;
|
||||
if (!('throttle' in params) || !params.throttle) {
|
||||
_.throttle = function (func) {
|
||||
return func;
|
||||
};
|
||||
}
|
||||
|
||||
// mock global objects for legacy widgets (session, config...)
|
||||
const restoreMockedGlobalObjects = _mockGlobalObjects(params);
|
||||
|
||||
// set the test env on owl Component
|
||||
const env = await _getMockedOwlEnv(params, mockServer);
|
||||
const originalEnv = Component.env;
|
||||
const __env = makeTestEnvironment(env, mockServer.performRpc.bind(mockServer));
|
||||
owl.Component.env = __env;
|
||||
|
||||
// while we have a mix between Owl and legacy stuff, some of them triggering
|
||||
// events on the env.bus (a new Bus instance especially created for the current
|
||||
// test), the others using core.bus, we have to ensure that events triggered
|
||||
// on env.bus are also triggered on core.bus (note that outside the testing
|
||||
// environment, both are the exact same instance of Bus)
|
||||
const envBusTrigger = env.bus.trigger;
|
||||
env.bus.trigger = function () {
|
||||
core.bus.trigger(...arguments);
|
||||
envBusTrigger.call(env.bus, ...arguments);
|
||||
};
|
||||
|
||||
// build the clean up function to call at the end of the test
|
||||
function cleanUp() {
|
||||
env.bus.destroy();
|
||||
Object.keys(env.services).forEach(function (s) {
|
||||
var service = env.services[s] || {};
|
||||
if (service.destroy && !service.isDestroyed()) {
|
||||
service.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
FormController.prototype.multiClickTime = initialQuickEditDelay;
|
||||
|
||||
DebouncedField.prototype.DEBOUNCE = initialDebounceValue;
|
||||
dom.DEBOUNCE = initialDOMDebounceValue;
|
||||
_.debounce = initialDebounce;
|
||||
_.throttle = initialThrottle;
|
||||
|
||||
// clear the caches (e.g. data_manager, ModelFieldSelector) at the end
|
||||
// of each test to avoid collisions
|
||||
core.bus.trigger('clear_cache');
|
||||
|
||||
$('body').off('DOMNodeInserted.removeSRC');
|
||||
$('.blockUI').remove(); // fixme: move to qunit_config in OdooAfterTestHook?
|
||||
|
||||
restoreMockedGlobalObjects();
|
||||
|
||||
Component.env = originalEnv;
|
||||
}
|
||||
|
||||
return cleanUp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a mock environment to a widget. This helper function can simulate
|
||||
* various kind of side effects, such as mocking RPCs, changing the session,
|
||||
* or the translation settings.
|
||||
*
|
||||
* The simulated environment lasts for the lifecycle of the widget, meaning it
|
||||
* disappears when the widget is destroyed. It is particularly relevant for the
|
||||
* session mocks, because the previous session is restored during the destroy
|
||||
* call. So, it means that you have to be careful and make sure that it is
|
||||
* properly destroyed before another test is run, otherwise you risk having
|
||||
* interferences between tests.
|
||||
*
|
||||
* @param {Widget} widget
|
||||
* @param {Object} params
|
||||
* @param {Object} [params.archs] a map of string [model,view_id,view_type] to
|
||||
* a arch object. It is used to mock answers to 'load_views' custom events.
|
||||
* This is useful when the widget instantiate a formview dialog that needs
|
||||
* to load a particular arch.
|
||||
* @param {string} [params.currentDate] a string representation of the current
|
||||
* date. It is given to the mock server.
|
||||
* @param {Object} params.data the data given to the created mock server. It is
|
||||
* used to generate mock answers for every kind of routes supported by odoo
|
||||
* @param {number} [params.debug] if set to true, logs RPCs and uncaught Odoo
|
||||
* events.
|
||||
* @param {Object} [params.bus] the instance of Bus that will be used (in the env)
|
||||
* @param {function} [params.mockFetch] a function that will be used to override
|
||||
* the _performFetch method from the mock server. It is really useful to add
|
||||
* some custom fetch mocks, or to check some assertions.
|
||||
* @param {function} [params.mockRPC] a function that will be used to override
|
||||
* the _performRpc method from the mock server. It is really useful to add
|
||||
* some custom rpc mocks, or to check some assertions.
|
||||
* @param {Object} [params.session] if it is given, it will be used as answer
|
||||
* for all calls to this.getSession() by the widget, of its children. Also,
|
||||
* it will be used to extend the current, real session. This side effect is
|
||||
* undone when the widget is destroyed.
|
||||
* @param {Object} [params.translateParameters] if given, it will be used to
|
||||
* extend the core._t.database.parameters object. After the widget
|
||||
* destruction, the original parameters will be restored.
|
||||
* @param {Object} [params.intercepts] an object with event names as key, and
|
||||
* callback as value. Each key,value will be used to intercept the event.
|
||||
* Note that this is particularly useful if you want to intercept events going
|
||||
* up in the init process of the view, because there are no other way to do it
|
||||
* after this method returns. Some events ('call_service', "load_views",
|
||||
* "get_session", "load_filters") have a special treatment beforehand.
|
||||
* @param {Object} [params.services={}] list of services to load in
|
||||
* addition to the ajax service. For instance, if a test needs the local
|
||||
* storage service in order to work, it can provide a mock version of it.
|
||||
* @param {boolean} [debounce=true] set to false to completely remove the
|
||||
* debouncing, forcing the handler to be called directly (not on the next
|
||||
* execution stack, like it does with delay=0).
|
||||
* @param {boolean} [throttle=false] set to true to keep the throttling, which
|
||||
* is completely removed by default.
|
||||
*
|
||||
* @returns {Promise<MockServer>} the instance of the mock server, created by this
|
||||
* function. It is necessary for createView so that method can call some
|
||||
* other methods on it.
|
||||
*/
|
||||
async function addMockEnvironment(widget, params) {
|
||||
// log events triggered up if debug flag is true
|
||||
if (params.debug) {
|
||||
_observe(widget);
|
||||
var separator = window.location.href.indexOf('?') !== -1 ? "&" : "?";
|
||||
var url = window.location.href + separator + 'testId=' + QUnit.config.current.testId;
|
||||
console.log('%c[debug] debug mode activated', 'color: blue; font-weight: bold;', url);
|
||||
}
|
||||
|
||||
// instantiate mock server
|
||||
var Server = MockServer;
|
||||
if (params.mockFetch) {
|
||||
Server = MockServer.extend({ _performFetch: params.mockFetch });
|
||||
}
|
||||
if (params.mockRPC) {
|
||||
Server = Server.extend({ _performRpc: params.mockRPC });
|
||||
}
|
||||
var mockServer = new Server(params.data, {
|
||||
actions: params.actions,
|
||||
archs: params.archs,
|
||||
currentDate: params.currentDate,
|
||||
debug: params.debug,
|
||||
widget: widget,
|
||||
});
|
||||
|
||||
// build and set the Owl env on Component
|
||||
if (!('mockSRC' in params)) { // redirect src rpcs to the mock server
|
||||
params.mockSRC = true;
|
||||
}
|
||||
const cleanUp = await addMockEnvironmentOwl(Component, params, mockServer);
|
||||
const env = Component.env;
|
||||
|
||||
// ensure to clean up everything when the widget will be destroyed
|
||||
const destroy = widget.destroy;
|
||||
widget.destroy = function () {
|
||||
cleanUp();
|
||||
destroy.call(this, ...arguments);
|
||||
};
|
||||
|
||||
// intercept service/data manager calls and redirect them to the env
|
||||
intercept(widget, 'call_service', function (ev) {
|
||||
if (env.services[ev.data.service]) {
|
||||
var service = env.services[ev.data.service];
|
||||
const result = service[ev.data.method].apply(service, ev.data.args || []);
|
||||
ev.data.callback(result);
|
||||
}
|
||||
});
|
||||
intercept(widget, 'load_action', async ev => {
|
||||
const action = await env.dataManager.load_action(ev.data.actionID, ev.data.context);
|
||||
ev.data.on_success(action);
|
||||
});
|
||||
intercept(widget, "load_views", async ev => {
|
||||
const params = {
|
||||
model: ev.data.modelName,
|
||||
context: ev.data.context,
|
||||
views_descr: ev.data.views,
|
||||
};
|
||||
const views = await env.dataManager.load_views(params, ev.data.options);
|
||||
if ('search' in views && params.favoriteFilters) {
|
||||
views.search.favoriteFilters = params.favoriteFilters;
|
||||
}
|
||||
ev.data.on_success(views);
|
||||
});
|
||||
intercept(widget, "get_session", ev => {
|
||||
ev.data.callback(session);
|
||||
});
|
||||
intercept(widget, "load_filters", async ev => {
|
||||
const filters = await env.dataManager.load_filters(ev.data);
|
||||
ev.data.on_success(filters);
|
||||
});
|
||||
|
||||
// make sure all other Odoo events bubbling up are intercepted
|
||||
Object.keys(params.intercepts || {}).forEach(function (name) {
|
||||
intercept(widget, name, params.intercepts[name]);
|
||||
});
|
||||
|
||||
return mockServer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Patch window.Date so that the time starts its flow from the provided Date.
|
||||
*
|
||||
* Usage:
|
||||
*
|
||||
* ```
|
||||
* testUtils.mock.patchDate(2018, 0, 10, 17, 59, 30)
|
||||
* new window.Date(); // "Wed Jan 10 2018 17:59:30 GMT+0100 (Central European Standard Time)"
|
||||
* ... // 5 hours delay
|
||||
* new window.Date(); // "Wed Jan 10 2018 22:59:30 GMT+0100 (Central European Standard Time)"
|
||||
* ```
|
||||
*
|
||||
* The returned function is there to preserve the former API. Before it was
|
||||
* necessary to call that function to unpatch the date. Now the unpatch is
|
||||
* done automatically via a call to registerCleanup.
|
||||
*
|
||||
* @param {integer} year
|
||||
* @param {integer} month index of the month, starting from zero.
|
||||
* @param {integer} day the day of the month.
|
||||
* @param {integer} hours the digits for hours (24h)
|
||||
* @param {integer} minutes
|
||||
* @param {integer} seconds
|
||||
* @returns {Function} callback function is now useless
|
||||
*/
|
||||
function legacyPatchDate(year, month, day, hours, minutes, seconds) {
|
||||
patchDate(year, month, day, hours, minutes, seconds);
|
||||
return function () {}; // all calls to that function are now useless
|
||||
}
|
||||
|
||||
var patches = {};
|
||||
/**
|
||||
* Patches a given Class or Object with the given properties.
|
||||
*
|
||||
* @param {Class|Object} target
|
||||
* @param {Object} props
|
||||
*/
|
||||
function patch(target, props) {
|
||||
var patchID = _.uniqueId('patch_');
|
||||
target.__patchID = patchID;
|
||||
patches[patchID] = {
|
||||
target: target,
|
||||
otherPatchedProps: [],
|
||||
ownPatchedProps: [],
|
||||
};
|
||||
if (target.prototype) {
|
||||
_.each(props, function (value, key) {
|
||||
if (target.prototype.hasOwnProperty(key)) {
|
||||
patches[patchID].ownPatchedProps.push({
|
||||
key: key,
|
||||
initialValue: target.prototype[key],
|
||||
});
|
||||
} else {
|
||||
patches[patchID].otherPatchedProps.push(key);
|
||||
}
|
||||
});
|
||||
target.include(props);
|
||||
} else {
|
||||
_.each(props, function (value, key) {
|
||||
if (key in target) {
|
||||
var oldValue = target[key];
|
||||
patches[patchID].ownPatchedProps.push({
|
||||
key: key,
|
||||
initialValue: oldValue,
|
||||
});
|
||||
if (typeof value === 'function') {
|
||||
target[key] = function () {
|
||||
var oldSuper = this._super;
|
||||
this._super = oldValue;
|
||||
var result = value.apply(this, arguments);
|
||||
if (oldSuper === undefined) {
|
||||
delete this._super;
|
||||
} else {
|
||||
this._super = oldSuper;
|
||||
}
|
||||
return result;
|
||||
};
|
||||
} else {
|
||||
target[key] = value;
|
||||
}
|
||||
} else {
|
||||
patches[patchID].otherPatchedProps.push(key);
|
||||
target[key] = value;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unpatches a given Class or Object.
|
||||
*
|
||||
* @param {Class|Object} target
|
||||
*/
|
||||
function unpatch(target) {
|
||||
var patchID = target.__patchID;
|
||||
var patch = patches[patchID];
|
||||
if (target.prototype) {
|
||||
_.each(patch.ownPatchedProps, function (p) {
|
||||
target.prototype[p.key] = p.initialValue;
|
||||
});
|
||||
_.each(patch.otherPatchedProps, function (key) {
|
||||
delete target.prototype[key];
|
||||
});
|
||||
} else {
|
||||
_.each(patch.ownPatchedProps, function (p) {
|
||||
target[p.key] = p.initialValue;
|
||||
});
|
||||
_.each(patch.otherPatchedProps, function (key) {
|
||||
delete target[key];
|
||||
});
|
||||
}
|
||||
delete patches[patchID];
|
||||
delete target.__patchID;
|
||||
}
|
||||
|
||||
window.originalSetTimeout = window.setTimeout;
|
||||
function patchSetTimeout() {
|
||||
var original = window.setTimeout;
|
||||
var self = this;
|
||||
window.setTimeout = function (handler, delay) {
|
||||
console.log("calling setTimeout on " + (handler.name || "some function") + "with delay of " + delay);
|
||||
console.trace();
|
||||
var handlerArguments = Array.prototype.slice.call(arguments, 1);
|
||||
return original(function () {
|
||||
handler.bind(self, handlerArguments)();
|
||||
console.log('after doing the action of the setTimeout');
|
||||
}, delay);
|
||||
};
|
||||
|
||||
return function () {
|
||||
window.setTimeout = original;
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
addMockEnvironment: addMockEnvironment,
|
||||
getView: getView,
|
||||
addMockEnvironmentOwl: addMockEnvironmentOwl,
|
||||
intercept: intercept,
|
||||
patchDate: legacyPatchDate,
|
||||
patch: patch,
|
||||
unpatch: unpatch,
|
||||
patchSetTimeout: patchSetTimeout,
|
||||
};
|
||||
|
||||
});
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
odoo.define('web.test_utils_modal', function (require) {
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Modal Test Utils
|
||||
*
|
||||
* This module defines various utility functions to help test pivot views.
|
||||
*
|
||||
* Note that all methods defined in this module are exported in the main
|
||||
* testUtils file.
|
||||
*/
|
||||
|
||||
const { _t } = require('web.core');
|
||||
const testUtilsDom = require('web.test_utils_dom');
|
||||
|
||||
/**
|
||||
* Click on a button in the footer of a modal (which contains a given string).
|
||||
*
|
||||
* @param {string} text (in english: this method will perform the translation)
|
||||
*/
|
||||
function clickButton(text) {
|
||||
return testUtilsDom.click($(`.modal-footer button:contains(${_t(text)})`));
|
||||
}
|
||||
|
||||
return { clickButton };
|
||||
});
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
odoo.define('web.test_utils_pivot', function (require) {
|
||||
"use strict";
|
||||
|
||||
var testUtilsDom = require('web.test_utils_dom');
|
||||
|
||||
/**
|
||||
* Pivot Test Utils
|
||||
*
|
||||
* This module defines various utility functions to help test pivot views.
|
||||
*
|
||||
* Note that all methods defined in this module are exported in the main
|
||||
* testUtils file.
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* Select a measure by clicking on the corresponding dropdown item (in the
|
||||
* control panel 'Measure' submenu).
|
||||
*
|
||||
* Note that this method assumes that the dropdown menu is open.
|
||||
* @see toggleMeasuresDropdown
|
||||
*
|
||||
* @param {PivotController} pivot
|
||||
* @param {string} measure
|
||||
*/
|
||||
function clickMeasure(pivot, measure) {
|
||||
return testUtilsDom.click(pivot.$buttons.find(`.dropdown-item[data-field=${measure}]`));
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the 'Measure' dropdown menu (in the control panel)
|
||||
*
|
||||
* @see clickMeasure
|
||||
*
|
||||
* @param {PivotController} pivot
|
||||
*/
|
||||
function toggleMeasuresDropdown(pivot) {
|
||||
return testUtilsDom.click(pivot.$buttons.filter('.btn-group:first').find('> button'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Reloads a graph view.
|
||||
*
|
||||
* @param {PivotController} pivot
|
||||
* @param {[Object]} params given to the controller reload method
|
||||
*/
|
||||
function reload(pivot, params) {
|
||||
return pivot.reload(params);
|
||||
}
|
||||
|
||||
return {
|
||||
clickMeasure: clickMeasure,
|
||||
reload: reload,
|
||||
toggleMeasuresDropdown: toggleMeasuresDropdown,
|
||||
};
|
||||
|
||||
});
|
||||
1137
odoo-bringout-oca-ocb-web/web/static/tests/legacy/helpers/utils.js
Normal file
1137
odoo-bringout-oca-ocb-web/web/static/tests/legacy/helpers/utils.js
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,6 @@
|
|||
// @odoo-module ignore
|
||||
|
||||
// This module has for sole purpose to mark all odoo modules defined between it
|
||||
// and ignore_missing_deps_stop as ignored for missing dependency errors.
|
||||
// see the template conditional_assets_tests to understand how it's used.
|
||||
window.__odooIgnoreMissingDependencies = true;
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
// @odoo-module ignore
|
||||
|
||||
window.__odooIgnoreMissingDependencies = false;
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
/** @odoo-module alias=web.legacySetup **/
|
||||
|
||||
// in tests, there's nothing to setup globally (we don't want to deploy services),
|
||||
// but this module must exist has it is required by other modules
|
||||
export const legacySetupProm = Promise.resolve();
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
odoo.define('web.class_tests', function (require) {
|
||||
"use strict";
|
||||
/** @odoo-module alias=@web/../tests/legacy_tests/core/class_tests default=false */
|
||||
|
||||
var Class = require('web.Class');
|
||||
import Class from "@web/legacy/js/core/class";
|
||||
|
||||
QUnit.module('core', {}, function () {
|
||||
|
||||
|
|
@ -164,5 +163,3 @@ QUnit.module('core', {}, function () {
|
|||
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
|
@ -0,0 +1,127 @@
|
|||
/** @odoo-module alias=@web/../tests/legacy_tests/helpers/test_utils default=false */
|
||||
|
||||
/**
|
||||
* Test Utils
|
||||
*
|
||||
* In this module, we define various utility functions to help simulate a mock
|
||||
* environment as close as possible as a real environment.
|
||||
*/
|
||||
|
||||
import testUtilsDom from "@web/../tests/legacy_tests/helpers/test_utils_dom";
|
||||
import testUtilsFields from "@web/../tests/legacy_tests/helpers/test_utils_fields";
|
||||
import testUtilsMock from "@web/../tests/legacy_tests/helpers/test_utils_mock";
|
||||
|
||||
function deprecated(fn, type) {
|
||||
return function () {
|
||||
const msg = `Helper 'testUtils.${fn.name}' is deprecated. ` +
|
||||
`Please use 'testUtils.${type}.${fn.name}' instead.`;
|
||||
console.warn(msg);
|
||||
return fn.apply(this, arguments);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function, make a promise with a public resolve function. Note that
|
||||
* this is not standard and should not be used outside of tests...
|
||||
*
|
||||
* @returns {Promise + resolve and reject function}
|
||||
*/
|
||||
function makeTestPromise() {
|
||||
let resolve;
|
||||
let reject;
|
||||
const promise = new Promise(function (_resolve, _reject) {
|
||||
resolve = _resolve;
|
||||
reject = _reject;
|
||||
});
|
||||
promise.resolve = function () {
|
||||
resolve.apply(null, arguments);
|
||||
return promise;
|
||||
};
|
||||
promise.reject = function () {
|
||||
reject.apply(null, arguments);
|
||||
return promise;
|
||||
};
|
||||
return promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a promise with public resolve and reject functions (see
|
||||
* @makeTestPromise). Perform an assert.step when the promise is
|
||||
* resolved/rejected.
|
||||
*
|
||||
* @param {Object} assert instance object with the assertion methods
|
||||
* @param {function} assert.step
|
||||
* @param {string} str message to pass to assert.step
|
||||
* @returns {Promise + resolve and reject function}
|
||||
*/
|
||||
function makeTestPromiseWithAssert(assert, str) {
|
||||
const prom = makeTestPromise();
|
||||
prom.then(() => assert.step('ok ' + str)).catch(function () { });
|
||||
prom.catch(() => assert.step('ko ' + str));
|
||||
return prom;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new promise that can be waited by the caller in order to execute
|
||||
* code after the next microtask tick and before the next jobqueue tick.
|
||||
*
|
||||
* @return {Promise} an already fulfilled promise
|
||||
*/
|
||||
async function nextMicrotaskTick() {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a promise that will be resolved after the tick after the
|
||||
* nextAnimationFrame
|
||||
*
|
||||
* This is usefull to guarantee that OWL has had the time to render
|
||||
*
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export async function nextTick() {
|
||||
return testUtilsDom.returnAfterNextAnimationFrame();
|
||||
}
|
||||
|
||||
export const mock = {
|
||||
intercept: testUtilsMock.intercept,
|
||||
patch: testUtilsMock.patch,
|
||||
patchDate: testUtilsMock.patchDate,
|
||||
unpatch: testUtilsMock.unpatch,
|
||||
getView: testUtilsMock.getView,
|
||||
};
|
||||
|
||||
export const dom = {
|
||||
dragAndDrop: testUtilsDom.dragAndDrop,
|
||||
find: testUtilsDom.findItem,
|
||||
click: testUtilsDom.click,
|
||||
clickFirst: testUtilsDom.clickFirst,
|
||||
triggerEvents: testUtilsDom.triggerEvents,
|
||||
triggerEvent: testUtilsDom.triggerEvent,
|
||||
};
|
||||
|
||||
export const fields = {
|
||||
editInput: testUtilsFields.editInput,
|
||||
editAndTrigger: testUtilsFields.editAndTrigger,
|
||||
triggerKeydown: testUtilsFields.triggerKeydown,
|
||||
};
|
||||
|
||||
export default {
|
||||
mock,
|
||||
dom,
|
||||
fields,
|
||||
|
||||
makeTestPromise: makeTestPromise,
|
||||
makeTestPromiseWithAssert: makeTestPromiseWithAssert,
|
||||
nextMicrotaskTick: nextMicrotaskTick,
|
||||
nextTick: nextTick,
|
||||
|
||||
// backward-compatibility
|
||||
dragAndDrop: deprecated(testUtilsDom.dragAndDrop, 'dom'),
|
||||
getView: deprecated(testUtilsMock.getView, 'mock'),
|
||||
intercept: deprecated(testUtilsMock.intercept, 'mock'),
|
||||
openDatepicker: deprecated(testUtilsDom.openDatepicker, 'dom'),
|
||||
patch: deprecated(testUtilsMock.patch, 'mock'),
|
||||
patchDate: deprecated(testUtilsMock.patchDate, 'mock'),
|
||||
unpatch: deprecated(testUtilsMock.unpatch, 'mock'),
|
||||
};
|
||||
|
|
@ -1,10 +1,6 @@
|
|||
odoo.define('web.test_utils_dom', function (require) {
|
||||
"use strict";
|
||||
/** @odoo-module alias=@web/../tests/legacy_tests/helpers/test_utils_dom default=false */
|
||||
|
||||
const concurrency = require('web.concurrency');
|
||||
const Widget = require('web.Widget');
|
||||
|
||||
const { Component } = owl;
|
||||
import { delay } from "@web/core/utils/concurrency";
|
||||
|
||||
/**
|
||||
* DOM Test Utils
|
||||
|
|
@ -20,7 +16,7 @@ odoo.define('web.test_utils_dom', function (require) {
|
|||
//-------------------------------------------------------------------------
|
||||
|
||||
// TriggerEvent helpers
|
||||
const keyboardEventBubble = args => Object.assign({}, args, { bubbles: true, keyCode: args.which });
|
||||
const keyboardEventBubble = args => Object.assign({}, args, { bubbles: true});
|
||||
const mouseEventMapping = args => Object.assign({}, args, {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
|
|
@ -132,7 +128,7 @@ odoo.define('web.test_utils_dom', function (require) {
|
|||
* @param {boolean} [options.last=false] if true, clicks on the last element
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async function click(el, options = {}) {
|
||||
export async function click(el, options = {}) {
|
||||
let matches, target;
|
||||
let selectorMsg = "";
|
||||
if (typeof el === 'string') {
|
||||
|
|
@ -196,21 +192,6 @@ odoo.define('web.test_utils_dom', function (require) {
|
|||
return click(el, Object.assign({}, options, { first: true }));
|
||||
}
|
||||
|
||||
/**
|
||||
* Click on the last element of a list of elements. Note that if the list has
|
||||
* only one visible element, we trigger an error. In that case, it is better to
|
||||
* use the click helper instead.
|
||||
*
|
||||
* @param {string|EventTarget|EventTarget[]} el (if string: it is a (jquery) selector)
|
||||
* @param {boolean} [options={}] click options
|
||||
* @param {boolean} [options.allowInvisible=false] if true, clicks on the
|
||||
* element event if it is invisible
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async function clickLast(el, options) {
|
||||
return click(el, Object.assign({}, options, { last: true }));
|
||||
}
|
||||
|
||||
/**
|
||||
* Simulate a drag and drop operation between 2 jquery nodes: $el and $to.
|
||||
* This is a crude simulation, with only the mousedown, mousemove and mouseup
|
||||
|
|
@ -318,94 +299,6 @@ odoo.define('web.test_utils_dom', function (require) {
|
|||
return returnAfterNextAnimationFrame();
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to retrieve a distinct item from a collection of elements defined
|
||||
* by the given "selector" string. It can either be the index of the item or its
|
||||
* inner text.
|
||||
* @param {Element} el
|
||||
* @param {string} selector
|
||||
* @param {number | string} [elFinder=0]
|
||||
* @returns {Element | null}
|
||||
*/
|
||||
function findItem(el, selector, elFinder = 0) {
|
||||
const elements = [...getNode(el).querySelectorAll(selector)];
|
||||
if (!elements.length) {
|
||||
throw new Error(`No element found with selector "${selector}".`);
|
||||
}
|
||||
switch (typeof elFinder) {
|
||||
case "number": {
|
||||
const match = elements[elFinder];
|
||||
if (!match) {
|
||||
throw new Error(
|
||||
`No element with selector "${selector}" at index ${elFinder}.`
|
||||
);
|
||||
}
|
||||
return match;
|
||||
}
|
||||
case "string": {
|
||||
const match = elements.find(
|
||||
(el) => el.innerText.trim().toLowerCase() === elFinder.toLowerCase()
|
||||
);
|
||||
if (!match) {
|
||||
throw new Error(
|
||||
`No element with selector "${selector}" containing "${elFinder}".
|
||||
`);
|
||||
}
|
||||
return match;
|
||||
}
|
||||
default: throw new Error(
|
||||
`Invalid provided element finder: must be a number|string|function.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function used to extract an HTML EventTarget element from a given
|
||||
* target. The extracted element will depend on the target type:
|
||||
* - Component|Widget -> el
|
||||
* - jQuery -> associated element (must have 1)
|
||||
* - HTMLCollection (or similar) -> first element (must have 1)
|
||||
* - string -> result of document.querySelector with string
|
||||
* - else -> as is
|
||||
* @private
|
||||
* @param {(Component|Widget|jQuery|HTMLCollection|HTMLElement|string)} target
|
||||
* @returns {EventTarget}
|
||||
*/
|
||||
function getNode(target) {
|
||||
let nodes;
|
||||
if (target instanceof Component || target instanceof Widget) {
|
||||
nodes = [target.el];
|
||||
} else if (typeof target === 'string') {
|
||||
nodes = document.querySelectorAll(target);
|
||||
} else if (target === jQuery) { // jQuery (or $)
|
||||
nodes = [document.body];
|
||||
} else if (target.length) { // jQuery instance, HTMLCollection or array
|
||||
nodes = target;
|
||||
} else {
|
||||
nodes = [target];
|
||||
}
|
||||
if (nodes.length !== 1) {
|
||||
throw new Error(`Found ${nodes.length} nodes instead of 1.`);
|
||||
}
|
||||
const node = nodes[0];
|
||||
if (!node) {
|
||||
throw new Error(`Expected a node and got ${node}.`);
|
||||
}
|
||||
if (!_isEventTarget(node)) {
|
||||
throw new Error(`Expected node to be an instance of EventTarget and got ${node.constructor.name}.`);
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the datepicker of a given element.
|
||||
*
|
||||
* @param {jQuery} $datepickerEl element to which a datepicker is attached
|
||||
*/
|
||||
async function openDatepicker($datepickerEl) {
|
||||
return click($datepickerEl.find('.o_datepicker_input'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a promise that will be resolved after the nextAnimationFrame after
|
||||
* the next tick
|
||||
|
|
@ -415,7 +308,7 @@ odoo.define('web.test_utils_dom', function (require) {
|
|||
* @returns {Promise}
|
||||
*/
|
||||
async function returnAfterNextAnimationFrame() {
|
||||
await concurrency.delay(0);
|
||||
await delay(0);
|
||||
await new Promise(resolve => {
|
||||
window.requestAnimationFrame(resolve);
|
||||
});
|
||||
|
|
@ -434,7 +327,7 @@ odoo.define('web.test_utils_dom', function (require) {
|
|||
* @param {Boolean} [fast=false] true if the trigger event have to wait for a single tick instead of waiting for the next animation frame
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async function triggerEvent(el, eventType, eventAttrs = {}, fast = false) {
|
||||
export async function triggerEvent(el, eventType, eventAttrs = {}, fast = false) {
|
||||
let matches;
|
||||
let selectorMsg = "";
|
||||
if (_isEventTarget(el)) {
|
||||
|
|
@ -488,123 +381,11 @@ odoo.define('web.test_utils_dom', function (require) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Simulate a keypress event for a given character
|
||||
*
|
||||
* @param {string} char the character, or 'ENTER'
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async function triggerKeypressEvent(char) {
|
||||
let keycode;
|
||||
if (char === 'Enter') {
|
||||
keycode = $.ui.keyCode.ENTER;
|
||||
} else if (char === "Tab") {
|
||||
keycode = $.ui.keyCode.TAB;
|
||||
} else {
|
||||
keycode = char.charCodeAt(0);
|
||||
}
|
||||
return triggerEvent(document.body, 'keypress', {
|
||||
key: char,
|
||||
keyCode: keycode,
|
||||
which: keycode,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* simulate a mouse event with a custom event who add the item position. This is
|
||||
* sometimes necessary because the basic way to trigger an event (such as
|
||||
* $el.trigger('mousemove')); ) is too crude for some uses.
|
||||
*
|
||||
* @param {jQuery|EventTarget} $el
|
||||
* @param {string} type a mouse event type, such as 'mousedown' or 'mousemove'
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async function triggerMouseEvent($el, type) {
|
||||
const el = $el instanceof jQuery ? $el[0] : $el;
|
||||
if (!el) {
|
||||
throw new Error(`no target found to trigger MouseEvent`);
|
||||
}
|
||||
const rect = el.getBoundingClientRect();
|
||||
// try to click around the center of the element, biased to the bottom
|
||||
// right as chrome messes up when clicking on the top-left corner...
|
||||
const left = rect.x + Math.ceil(rect.width / 2);
|
||||
const top = rect.y + Math.ceil(rect.height / 2);
|
||||
return triggerEvent(el, type, {which: 1, clientX: left, clientY: top});
|
||||
}
|
||||
|
||||
/**
|
||||
* simulate a mouse event with a custom event on a position x and y. This is
|
||||
* sometimes necessary because the basic way to trigger an event (such as
|
||||
* $el.trigger('mousemove')); ) is too crude for some uses.
|
||||
*
|
||||
* @param {integer} x
|
||||
* @param {integer} y
|
||||
* @param {string} type a mouse event type, such as 'mousedown' or 'mousemove'
|
||||
* @returns {HTMLElement}
|
||||
*/
|
||||
async function triggerPositionalMouseEvent(x, y, type) {
|
||||
const ev = document.createEvent("MouseEvent");
|
||||
const el = document.elementFromPoint(x, y);
|
||||
ev.initMouseEvent(
|
||||
type,
|
||||
true /* bubble */,
|
||||
true /* cancelable */,
|
||||
window, null,
|
||||
x, y, x, y, /* coordinates */
|
||||
false, false, false, false, /* modifier keys */
|
||||
0 /*left button*/, null
|
||||
);
|
||||
el.dispatchEvent(ev);
|
||||
return el;
|
||||
}
|
||||
|
||||
/**
|
||||
* Simulate a "TAP" (touch) event with a custom position x and y.
|
||||
*
|
||||
* @param {number} x
|
||||
* @param {number} y
|
||||
* @returns {HTMLElement}
|
||||
*/
|
||||
async function triggerPositionalTapEvents(x, y) {
|
||||
const element = document.elementFromPoint(x, y);
|
||||
const touch = new Touch({
|
||||
identifier: 0,
|
||||
target: element,
|
||||
clientX: x,
|
||||
clientY: y,
|
||||
pageX: x,
|
||||
pageY: y,
|
||||
});
|
||||
await triggerEvent(element, 'touchstart', {
|
||||
touches: [touch],
|
||||
targetTouches: [touch],
|
||||
changedTouches: [touch],
|
||||
});
|
||||
await triggerEvent(element, 'touchmove', {
|
||||
touches: [touch],
|
||||
targetTouches: [touch],
|
||||
changedTouches: [touch],
|
||||
});
|
||||
await triggerEvent(element, 'touchend', {
|
||||
changedTouches: [touch],
|
||||
});
|
||||
return element;
|
||||
}
|
||||
|
||||
return {
|
||||
export default {
|
||||
click,
|
||||
clickFirst,
|
||||
clickLast,
|
||||
dragAndDrop,
|
||||
findItem,
|
||||
getNode,
|
||||
openDatepicker,
|
||||
returnAfterNextAnimationFrame,
|
||||
triggerEvent,
|
||||
triggerEvents,
|
||||
triggerKeypressEvent,
|
||||
triggerMouseEvent,
|
||||
triggerPositionalMouseEvent,
|
||||
triggerPositionalTapEvents,
|
||||
};
|
||||
});
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
/** @odoo-module alias=@web/../tests/legacy_tests/helpers/test_utils_fields default=false */
|
||||
|
||||
/**
|
||||
* Field Test Utils
|
||||
*
|
||||
* This module defines various utility functions to help testing field widgets.
|
||||
*
|
||||
* Note that all methods defined in this module are exported in the main
|
||||
* testUtils file.
|
||||
*/
|
||||
|
||||
import testUtilsDom from "./test_utils_dom";
|
||||
|
||||
//-------------------------------------------------------------------------
|
||||
// Public functions
|
||||
//-------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Sets the value of an element and then, trigger all specified events.
|
||||
* Note that this helper also checks the unicity of the target.
|
||||
*
|
||||
* Example:
|
||||
* testUtils.fields.editAndTrigger($('selector'), 'test', ['input', 'change']);
|
||||
*
|
||||
* @param {jQuery|EventTarget} el should target an input, textarea or select
|
||||
* @param {string|number} value
|
||||
* @param {string[]} events
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export async function editAndTrigger(el, value, events) {
|
||||
if (el instanceof jQuery) {
|
||||
if (el.length !== 1) {
|
||||
throw new Error(`target ${el.selector} has length ${el.length} instead of 1`);
|
||||
}
|
||||
el.val(value);
|
||||
} else {
|
||||
el.value = value;
|
||||
}
|
||||
return testUtilsDom.triggerEvents(el, events);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the value of an input.
|
||||
*
|
||||
* Note that this helper also checks the unicity of the target.
|
||||
*
|
||||
* Example:
|
||||
* testUtils.fields.editInput($('selector'), 'somevalue');
|
||||
*
|
||||
* @param {jQuery|EventTarget} el should target an input, textarea or select
|
||||
* @param {string|number} value
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export async function editInput(el, value) {
|
||||
return editAndTrigger(el, value, ['input']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to trigger a key event on an element.
|
||||
*
|
||||
* @param {string} type type of key event ('press', 'up' or 'down')
|
||||
* @param {jQuery} $el
|
||||
* @param {string} key
|
||||
* @returns {Promise}
|
||||
*/
|
||||
function triggerKey(type, $el, key) {
|
||||
type = 'key' + type;
|
||||
const params = {};
|
||||
params.key = key;
|
||||
return testUtilsDom.triggerEvent($el, type, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to trigger a keydown event on an element.
|
||||
*
|
||||
* @param {jQuery} $el
|
||||
* @param {number|string} key @see triggerKey
|
||||
* @returns {Promise}
|
||||
*/
|
||||
function triggerKeydown($el, key) {
|
||||
return triggerKey('down', $el, key);
|
||||
}
|
||||
|
||||
export default {
|
||||
editAndTrigger,
|
||||
editInput,
|
||||
triggerKeydown,
|
||||
};
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
/** @odoo-module alias=@web/../tests/legacy_tests/helpers/test_utils_mock default=false */
|
||||
|
||||
/**
|
||||
* Mock Test Utils
|
||||
*
|
||||
* This module defines various utility functions to help mocking data.
|
||||
*
|
||||
* Note that all methods defined in this module are exported in the main
|
||||
* testUtils file.
|
||||
*/
|
||||
|
||||
import { patchDate } from "@web/../tests/helpers/utils";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Public functions
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* 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'.
|
||||
*
|
||||
* Note that this method really intercepts the event if @propagate is not set.
|
||||
* It will not be propagated further, and even the handlers on the target will
|
||||
* not fire.
|
||||
*
|
||||
* @param {Widget} widget the target widget (any Odoo widget)
|
||||
* @param {string} eventName description of the event
|
||||
* @param {function} fn callback executed when the even is intercepted
|
||||
* @param {boolean} [propagate=false]
|
||||
*/
|
||||
function intercept(widget, eventName, fn, propagate) {
|
||||
var _trigger_up = widget._trigger_up.bind(widget);
|
||||
widget._trigger_up = function (event) {
|
||||
if (event.name === eventName) {
|
||||
fn(event);
|
||||
if (!propagate) { return; }
|
||||
}
|
||||
_trigger_up(event);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Patch window.Date so that the time starts its flow from the provided Date.
|
||||
*
|
||||
* Usage:
|
||||
*
|
||||
* ```
|
||||
* testUtils.mock.patchDate(2018, 0, 10, 17, 59, 30)
|
||||
* new window.Date(); // "Wed Jan 10 2018 17:59:30 GMT+0100 (Central European Standard Time)"
|
||||
* ... // 5 hours delay
|
||||
* new window.Date(); // "Wed Jan 10 2018 22:59:30 GMT+0100 (Central European Standard Time)"
|
||||
* ```
|
||||
*
|
||||
* The returned function is there to preserve the former API. Before it was
|
||||
* necessary to call that function to unpatch the date. Now the unpatch is
|
||||
* done automatically via a call to registerCleanup.
|
||||
*
|
||||
* @param {integer} year
|
||||
* @param {integer} month index of the month, starting from zero.
|
||||
* @param {integer} day the day of the month.
|
||||
* @param {integer} hours the digits for hours (24h)
|
||||
* @param {integer} minutes
|
||||
* @param {integer} seconds
|
||||
* @returns {Function} callback function is now useless
|
||||
*/
|
||||
function legacyPatchDate(year, month, day, hours, minutes, seconds) {
|
||||
patchDate(year, month, day, hours, minutes, seconds);
|
||||
return function () {}; // all calls to that function are now useless
|
||||
}
|
||||
|
||||
export default {
|
||||
intercept: intercept,
|
||||
patchDate: legacyPatchDate,
|
||||
};
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
odoo.define('web.testUtilsTests', function (require) {
|
||||
"use strict";
|
||||
/** @odoo-module alias=@web/../tests/legacy_tests/helpers/test_utils_tests default=false */
|
||||
|
||||
var testUtils = require('web.test_utils');
|
||||
import testUtils from "./test_utils";
|
||||
const { DateTime } = luxon;
|
||||
|
||||
QUnit.module('web', {}, function () {
|
||||
QUnit.module('testUtils', {}, function () {
|
||||
|
|
@ -26,11 +26,9 @@ QUnit.test('new moment', function (assert) {
|
|||
assert.expect(1);
|
||||
const unpatchDate = testUtils.mock.patchDate(2018, 9, 23, 14, 50, 0);
|
||||
|
||||
const m = moment();
|
||||
assert.strictEqual(m.format('YYYY-MM-DD HH:mm'), '2018-10-23 14:50');
|
||||
assert.strictEqual(DateTime.now().toFormat("yyyy-MM-dd HH:mm"), '2018-10-23 14:50');
|
||||
unpatchDate();
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
/** @odoo-module alias=@web/../tests/legacy_tests/patch_localization default=false */
|
||||
|
||||
import { localization } from "@web/core/l10n/localization";
|
||||
|
||||
Object.assign(localization, {
|
||||
dateFormat: "MM/dd/yyyy",
|
||||
timeFormat: "HH:mm:ss",
|
||||
dateTimeFormat: "MM/dd/yyyy HH:mm:ss",
|
||||
decimalPoint: ".",
|
||||
direction: "ltr",
|
||||
grouping: [],
|
||||
multiLang: false,
|
||||
thousandsSep: ",",
|
||||
weekStart: 7,
|
||||
code: "en",
|
||||
});
|
||||
10
odoo-bringout-oca-ocb-web/web/static/tests/legacy/main.js
Normal file
10
odoo-bringout-oca-ocb-web/web/static/tests/legacy/main.js
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
/** @odoo-module alias=@web/../tests/main default=false */
|
||||
|
||||
import { setupQUnit } from "./qunit";
|
||||
import { setupTests } from "./setup";
|
||||
|
||||
(async () => {
|
||||
setupQUnit();
|
||||
await setupTests();
|
||||
QUnit.start();
|
||||
})();
|
||||
|
|
@ -1,137 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import MockServer from 'web.MockServer';
|
||||
|
||||
QUnit.module('web', {}, function () {
|
||||
QUnit.module('legacy', {}, function () {
|
||||
QUnit.module('mock_relational_fields_tests.js', {
|
||||
beforeEach() {
|
||||
this.data = {
|
||||
foo: {
|
||||
fields: {
|
||||
one2many_field: { type: 'one2many', relation: 'bar', inverse_fname_by_model_name: { bar: 'many2one_field' } },
|
||||
many2one_field: { type: 'many2one', relation: 'bar', inverse_fname_by_model_name: { bar: 'one2many_field' } },
|
||||
many2many_field: { type: 'many2many', relation: 'bar', inverse_fname_by_model_name: { bar: 'many2many_field' } },
|
||||
many2one_reference: { type: 'many2one_reference', model_name_ref_fname: 'res_model', inverse_fname_by_model_name: { bar: 'one2many_field' } },
|
||||
res_model: { type: 'char' },
|
||||
|
||||
},
|
||||
records: [],
|
||||
},
|
||||
bar: {
|
||||
fields: {
|
||||
many2one_field: { type: 'many2one', relation: 'foo' },
|
||||
one2many_field: { type: 'one2many', relation: 'foo', inverse_fname_by_model_name: { foo: 'many2one_field' } },
|
||||
many2many_field: { type: 'many2many', relation: 'foo', inverse_fname_by_model_name: { foo: 'many2many_field' } },
|
||||
},
|
||||
records: [],
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
QUnit.test('many2one_ref should auto fill inverse field', async function (assert) {
|
||||
this.data['bar'].records.push({ id: 1 });
|
||||
this.data['foo'].records.push({
|
||||
id: 2,
|
||||
res_model: 'bar',
|
||||
many2one_reference: 1,
|
||||
});
|
||||
const mockServer = new MockServer(this.data, {});
|
||||
assert.deepEqual([2], mockServer.data['bar'].records[0].one2many_field);
|
||||
|
||||
mockServer._mockUnlink('foo', [2]);
|
||||
assert.deepEqual([], mockServer.data['bar'].records[0].one2many_field);
|
||||
});
|
||||
|
||||
QUnit.test('many2one should auto fill inverse field', async function (assert) {
|
||||
this.data['bar'].records.push({ id: 1 });
|
||||
this.data['foo'].records.push({
|
||||
id: 2,
|
||||
many2one_field: 1,
|
||||
});
|
||||
const mockServer = new MockServer(this.data, {});
|
||||
assert.deepEqual([2], mockServer.data['bar'].records[0].one2many_field);
|
||||
|
||||
mockServer._mockUnlink('foo', [2]);
|
||||
assert.deepEqual([], mockServer.data['bar'].records[0].one2many_field);
|
||||
});
|
||||
|
||||
QUnit.test('one2many should auto fill inverse field', async function (assert) {
|
||||
this.data['bar'].records.push({ id: 1 });
|
||||
this.data['bar'].records.push({ id: 2 });
|
||||
this.data['foo'].records.push({
|
||||
id: 3,
|
||||
one2many_field: [1, 2],
|
||||
});
|
||||
const mockServer = new MockServer(this.data, {});
|
||||
assert.strictEqual(3, mockServer.data['bar'].records[0].many2one_field);
|
||||
assert.strictEqual(3, mockServer.data['bar'].records[1].many2one_field);
|
||||
|
||||
mockServer._mockUnlink('foo', [3]);
|
||||
assert.strictEqual(false, mockServer.data['bar'].records[0].many2one_field);
|
||||
assert.strictEqual(false, mockServer.data['bar'].records[1].many2one_field);
|
||||
});
|
||||
|
||||
QUnit.test('many2many should auto fill inverse field', async function (assert) {
|
||||
this.data['bar'].records.push({ id: 1 });
|
||||
this.data['foo'].records.push({
|
||||
id: 2,
|
||||
many2many_field: [1],
|
||||
});
|
||||
const mockServer = new MockServer(this.data, {});
|
||||
assert.deepEqual([2], mockServer.data['bar'].records[0].many2many_field);
|
||||
|
||||
mockServer._mockUnlink('foo', [2]);
|
||||
assert.deepEqual([], mockServer.data['bar'].records[0].many2many_field);
|
||||
});
|
||||
|
||||
QUnit.test('one2many update should update inverse field', async function (assert) {
|
||||
this.data['bar'].records.push({ id: 1 });
|
||||
this.data['bar'].records.push({ id: 2 });
|
||||
this.data['foo'].records.push({
|
||||
id: 3,
|
||||
one2many_field: [1, 2],
|
||||
});
|
||||
const mockServer = new MockServer(this.data, {});
|
||||
mockServer._mockWrite('foo', [[3], { one2many_field: [1] }]);
|
||||
assert.strictEqual(3, mockServer.data['bar'].records[0].many2one_field);
|
||||
assert.strictEqual(false, mockServer.data['bar'].records[1].many2one_field);
|
||||
});
|
||||
|
||||
QUnit.test('many2many update should update inverse field', async function (assert) {
|
||||
this.data['bar'].records.push({ id: 1 });
|
||||
this.data['foo'].records.push({
|
||||
id: 2,
|
||||
many2many_field: [1],
|
||||
});
|
||||
const mockServer = new MockServer(this.data, {});
|
||||
mockServer._mockWrite('foo', [[2], { many2many_field: [] }]);
|
||||
assert.deepEqual([], mockServer.data['bar'].records[0].many2many_field);
|
||||
});
|
||||
|
||||
QUnit.test('many2one update should update inverse field', async function (assert) {
|
||||
this.data['bar'].records.push({ id: 1 });
|
||||
this.data['foo'].records.push({
|
||||
id: 2,
|
||||
many2one_field: 1,
|
||||
});
|
||||
const mockServer = new MockServer(this.data, {});
|
||||
mockServer._mockWrite('foo', [[2], { many2one_field: false }]);
|
||||
assert.deepEqual([], mockServer.data['bar'].records[0].one2many_field);
|
||||
});
|
||||
|
||||
QUnit.test('many2one_ref update should update inverse field', async function (assert) {
|
||||
this.data['bar'].records.push({ id: 1 });
|
||||
this.data['foo'].records.push({
|
||||
id: 2,
|
||||
res_model: 'bar',
|
||||
many2one_reference: 1,
|
||||
});
|
||||
const mockServer = new MockServer(this.data, {});
|
||||
mockServer._mockWrite('foo', [[2], { many2one_reference: false }]);
|
||||
assert.deepEqual([], mockServer.data['bar'].records[0].one2many_field);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,565 +0,0 @@
|
|||
odoo.define('web.mockserver_tests', function (require) {
|
||||
"use strict";
|
||||
|
||||
const MockServer = require("web.MockServer");
|
||||
|
||||
QUnit.module("Legacy MockServer", {
|
||||
beforeEach() {
|
||||
this.data = {
|
||||
"res.partner": {
|
||||
fields: {
|
||||
name: {
|
||||
string: "Name",
|
||||
type: "string",
|
||||
},
|
||||
email: {
|
||||
string: "Email",
|
||||
type: "string",
|
||||
},
|
||||
active: {
|
||||
string: "Active",
|
||||
type: "bool",
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
records: [
|
||||
{ id: 1, name: "Jean-Michel", email: "jean.michel@example.com" },
|
||||
{ id: 2, name: "Raoul", email: "raoul@example.com", active: false },
|
||||
],
|
||||
},
|
||||
bar: {
|
||||
fields: {
|
||||
foo: {
|
||||
string: "Foo",
|
||||
type: "integer",
|
||||
searchable: true,
|
||||
group_operator: "sum",
|
||||
},
|
||||
date: { string: "Date", type: "date", store: true, sortable: true },
|
||||
datetime: {
|
||||
string: "DateTime",
|
||||
type: "datetime",
|
||||
store: true,
|
||||
sortable: true,
|
||||
},
|
||||
partners: { string: "Buddies", type: "many2many", relation: "res.partner" },
|
||||
},
|
||||
records: [
|
||||
{
|
||||
id: 1,
|
||||
foo: 12,
|
||||
date: "2016-12-14",
|
||||
datetime: "2016-12-14 12:34:56",
|
||||
partners: [1, 2],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
foo: 1,
|
||||
date: "2016-10-26",
|
||||
datetime: "2016-10-26 12:34:56",
|
||||
partners: [1],
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
foo: 17,
|
||||
date: "2016-12-15",
|
||||
datetime: "2016-12-15 12:34:56",
|
||||
partners: [2],
|
||||
},
|
||||
{ id: 4, foo: 2, date: "2016-04-11", datetime: "2016-04-11 12:34:56" },
|
||||
{ id: 5, foo: 0, date: "2016-12-15", datetime: "2016-12-15 12:34:56" },
|
||||
{ id: 6, foo: 42, date: "2019-12-30", datetime: "2019-12-30 12:34:56" },
|
||||
],
|
||||
},
|
||||
};
|
||||
},
|
||||
}, function () {
|
||||
QUnit.test("performRpc: search_read with an empty array of fields", async function (assert) {
|
||||
assert.expect(1);
|
||||
const server = new MockServer(this.data, {});
|
||||
const result = await server.performRpc("", {
|
||||
model: "res.partner",
|
||||
method: "search_read",
|
||||
args: [],
|
||||
kwargs: {
|
||||
fields: [],
|
||||
},
|
||||
});
|
||||
const expectedFields = ["id", "email", "name", "display_name"];
|
||||
assert.strictEqual(_.difference(expectedFields, Object.keys(result[0])).length, 0,
|
||||
"should contains all the fields");
|
||||
});
|
||||
|
||||
QUnit.test("performRpc: search_read without fields", async function (assert) {
|
||||
assert.expect(1);
|
||||
const server = new MockServer(this.data, {});
|
||||
const result = await server.performRpc("", {
|
||||
model: "res.partner",
|
||||
method: "search_read",
|
||||
args: [],
|
||||
kwargs: {},
|
||||
});
|
||||
const expectedFields = ["id", "email", "name", "display_name"];
|
||||
assert.strictEqual(_.difference(expectedFields, Object.keys(result[0])).length, 0,
|
||||
"should contains all the fields");
|
||||
});
|
||||
|
||||
QUnit.test("performRpc: name_get with no args", async function (assert) {
|
||||
assert.expect(2);
|
||||
const server = new MockServer(this.data, {});
|
||||
try {
|
||||
await server.performRpc("", {
|
||||
model: "res.partner",
|
||||
method: "name_get",
|
||||
args: [],
|
||||
kwargs: {},
|
||||
});
|
||||
} catch (_error) {
|
||||
assert.step("name_get failed")
|
||||
}
|
||||
assert.verifySteps(["name_get failed"])
|
||||
});
|
||||
|
||||
QUnit.test("performRpc: name_get with undefined arg", async function (assert) {
|
||||
assert.expect(1);
|
||||
const server = new MockServer(this.data, {});
|
||||
const result = await server.performRpc("", {
|
||||
model: "res.partner",
|
||||
method: "name_get",
|
||||
args: [undefined],
|
||||
kwargs: {},
|
||||
});
|
||||
assert.deepEqual(result, [])
|
||||
});
|
||||
|
||||
QUnit.test("performRpc: name_get with a single id", async function (assert) {
|
||||
assert.expect(1);
|
||||
const server = new MockServer(this.data, {});
|
||||
const result = await server.performRpc("", {
|
||||
model: "res.partner",
|
||||
method: "name_get",
|
||||
args: [1],
|
||||
kwargs: {},
|
||||
});
|
||||
assert.deepEqual(result, [[1, "Jean-Michel"]]);
|
||||
});
|
||||
|
||||
QUnit.test("performRpc: name_get with array of ids", async function (assert) {
|
||||
assert.expect(1);
|
||||
const server = new MockServer(this.data, {});
|
||||
const result = await server.performRpc("", {
|
||||
model: "res.partner",
|
||||
method: "name_get",
|
||||
args: [[1]],
|
||||
kwargs: {},
|
||||
});
|
||||
assert.deepEqual(result, [[1, "Jean-Michel"]]);
|
||||
});
|
||||
|
||||
QUnit.test("performRpc: name_get with invalid id", async function (assert) {
|
||||
assert.expect(2);
|
||||
const server = new MockServer(this.data, {});
|
||||
try {
|
||||
await server.performRpc("", {
|
||||
model: "res.partner",
|
||||
method: "name_get",
|
||||
args: [11111],
|
||||
kwargs: {},
|
||||
});
|
||||
} catch (_error) {
|
||||
assert.step("name_get failed")
|
||||
}
|
||||
assert.verifySteps(["name_get failed"])
|
||||
});
|
||||
|
||||
QUnit.test("performRpc: name_get with id and undefined id", async function (assert) {
|
||||
assert.expect(1);
|
||||
const server = new MockServer(this.data, {});
|
||||
const result = await server.performRpc("", {
|
||||
model: "res.partner",
|
||||
method: "name_get",
|
||||
args: [[undefined, 1]],
|
||||
kwargs: {},
|
||||
});
|
||||
assert.deepEqual(result, [[null, ""], [1, "Jean-Michel"]]);
|
||||
});
|
||||
|
||||
QUnit.test("performRpc: name_get with single id 0", async function (assert) {
|
||||
assert.expect(1);
|
||||
const server = new MockServer(this.data, {});
|
||||
const result = await server.performRpc("", {
|
||||
model: "res.partner",
|
||||
method: "name_get",
|
||||
args: [0],
|
||||
kwargs: {},
|
||||
});
|
||||
assert.deepEqual(result, []);
|
||||
});
|
||||
|
||||
QUnit.test("performRpc: name_get with array of id 0", async function (assert) {
|
||||
assert.expect(1);
|
||||
const server = new MockServer(this.data, {});
|
||||
const result = await server.performRpc("", {
|
||||
model: "res.partner",
|
||||
method: "name_get",
|
||||
args: [[0]],
|
||||
kwargs: {},
|
||||
});
|
||||
assert.deepEqual(result, [[null, ""]]);
|
||||
});
|
||||
|
||||
QUnit.test("performRpc: search with active_test=false", async function (assert) {
|
||||
assert.expect(1);
|
||||
const server = new MockServer(this.data, {});
|
||||
const result = await server.performRpc("", {
|
||||
model: "res.partner",
|
||||
method: "search",
|
||||
args: [[]],
|
||||
kwargs: {
|
||||
context: { active_test: false },
|
||||
},
|
||||
});
|
||||
assert.deepEqual(result, [1, 2]);
|
||||
});
|
||||
|
||||
QUnit.test("performRpc: search with active_test=true", async function (assert) {
|
||||
assert.expect(1);
|
||||
const server = new MockServer(this.data, {});
|
||||
const result = await server.performRpc("", {
|
||||
model: "res.partner",
|
||||
method: "search",
|
||||
args: [[]],
|
||||
kwargs: {
|
||||
context: { active_test: true },
|
||||
},
|
||||
});
|
||||
assert.deepEqual(result, [1]);
|
||||
});
|
||||
|
||||
QUnit.test("performRpc: search_read with active_test=false", async function (assert) {
|
||||
assert.expect(1);
|
||||
const server = new MockServer(this.data, {});
|
||||
const result = await server.performRpc("", {
|
||||
model: "res.partner",
|
||||
method: "search_read",
|
||||
args: [[]],
|
||||
kwargs: {
|
||||
fields: ["name"],
|
||||
context: { active_test: false },
|
||||
},
|
||||
});
|
||||
assert.deepEqual(result, [{id: 1, name: "Jean-Michel"}, {id: 2, name: "Raoul"}]);
|
||||
});
|
||||
|
||||
QUnit.test("performRpc: search_read with active_test=true", async function (assert) {
|
||||
assert.expect(1);
|
||||
const server = new MockServer(this.data, {});
|
||||
const result = await server.performRpc("", {
|
||||
model: "res.partner",
|
||||
method: "search_read",
|
||||
args: [[]],
|
||||
kwargs: {
|
||||
fields: ["name"],
|
||||
context: { active_test: true },
|
||||
},
|
||||
});
|
||||
assert.deepEqual(result, [{id: 1, name: "Jean-Michel"}]);
|
||||
});
|
||||
|
||||
QUnit.test("performRpc: read_group, group by date", async function (assert) {
|
||||
assert.expect(10);
|
||||
const server = new MockServer(this.data, {});
|
||||
let result = await server.performRpc("", {
|
||||
model: "bar",
|
||||
method: "read_group",
|
||||
args: [[]],
|
||||
kwargs: {
|
||||
fields: ["foo"],
|
||||
domain: [],
|
||||
groupby: ["date"], //Month by default
|
||||
},
|
||||
});
|
||||
assert.deepEqual(
|
||||
result.map((x) => x.date),
|
||||
["December 2016", "October 2016", "April 2016", "December 2019"]
|
||||
);
|
||||
assert.deepEqual(
|
||||
result.map((x) => x.date_count),
|
||||
[3, 1, 1, 1]
|
||||
);
|
||||
|
||||
result = await server.performRpc("", {
|
||||
model: "bar",
|
||||
method: "read_group",
|
||||
args: [[]],
|
||||
kwargs: {
|
||||
fields: ["foo"],
|
||||
domain: [],
|
||||
groupby: ["date:day"],
|
||||
},
|
||||
});
|
||||
assert.deepEqual(
|
||||
result.map((x) => x["date:day"]),
|
||||
["2016-12-14", "2016-10-26", "2016-12-15", "2016-04-11", "2019-12-30"]
|
||||
);
|
||||
assert.deepEqual(
|
||||
result.map((x) => x.date_count),
|
||||
[1, 1, 2, 1, 1]
|
||||
);
|
||||
|
||||
result = await server.performRpc("", {
|
||||
model: "bar",
|
||||
method: "read_group",
|
||||
args: [[]],
|
||||
kwargs: {
|
||||
fields: ["foo"],
|
||||
domain: [],
|
||||
groupby: ["date:week"],
|
||||
},
|
||||
});
|
||||
assert.deepEqual(
|
||||
result.map((x) => x["date:week"]),
|
||||
["W50 2016", "W43 2016", "W15 2016", "W01 2020"]
|
||||
);
|
||||
assert.deepEqual(
|
||||
result.map((x) => x.date_count),
|
||||
[3, 1, 1, 1]
|
||||
);
|
||||
|
||||
result = await server.performRpc("", {
|
||||
model: "bar",
|
||||
method: "read_group",
|
||||
args: [[]],
|
||||
kwargs: {
|
||||
fields: ["foo"],
|
||||
domain: [],
|
||||
groupby: ["date:quarter"],
|
||||
},
|
||||
});
|
||||
assert.deepEqual(
|
||||
result.map((x) => x["date:quarter"]),
|
||||
["Q4 2016", "Q2 2016", "Q4 2019"]
|
||||
);
|
||||
assert.deepEqual(
|
||||
result.map((x) => x.date_count),
|
||||
[4, 1, 1]
|
||||
);
|
||||
|
||||
result = await server.performRpc("", {
|
||||
model: "bar",
|
||||
method: "read_group",
|
||||
args: [[]],
|
||||
kwargs: {
|
||||
fields: ["foo"],
|
||||
domain: [],
|
||||
groupby: ["date:year"],
|
||||
},
|
||||
});
|
||||
assert.deepEqual(
|
||||
result.map((x) => x["date:year"]),
|
||||
["2016", "2019"]
|
||||
);
|
||||
assert.deepEqual(
|
||||
result.map((x) => x.date_count),
|
||||
[5, 1]
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("performRpc: read_group, group by datetime", async function (assert) {
|
||||
const server = new MockServer(this.data, {});
|
||||
let result = await server.performRpc("", {
|
||||
model: "bar",
|
||||
method: "read_group",
|
||||
args: [[]],
|
||||
kwargs: {
|
||||
fields: ["foo"],
|
||||
domain: [],
|
||||
groupby: ["datetime"], //Month by default
|
||||
},
|
||||
});
|
||||
assert.deepEqual(
|
||||
result.map((x) => x.datetime),
|
||||
["December 2016", "October 2016", "April 2016", "December 2019"]
|
||||
);
|
||||
assert.deepEqual(
|
||||
result.map((x) => x.datetime_count),
|
||||
[3, 1, 1, 1]
|
||||
);
|
||||
|
||||
result = await server.performRpc("", {
|
||||
model: "bar",
|
||||
method: "read_group",
|
||||
args: [[]],
|
||||
kwargs: {
|
||||
fields: ["foo"],
|
||||
domain: [],
|
||||
groupby: ["datetime:hour"],
|
||||
},
|
||||
});
|
||||
assert.deepEqual(
|
||||
result.map((x) => x["datetime:hour"]),
|
||||
["12:00 14 Dec", "12:00 26 Oct", "12:00 15 Dec", "12:00 11 Apr", "12:00 30 Dec"]
|
||||
);
|
||||
assert.deepEqual(
|
||||
result.map((x) => x.datetime_count),
|
||||
[1, 1, 2, 1, 1]
|
||||
);
|
||||
|
||||
result = await server.performRpc("", {
|
||||
model: "bar",
|
||||
method: "read_group",
|
||||
args: [[]],
|
||||
kwargs: {
|
||||
fields: ["foo"],
|
||||
domain: [],
|
||||
groupby: ["datetime:day"],
|
||||
},
|
||||
});
|
||||
assert.deepEqual(
|
||||
result.map((x) => x["datetime:day"]),
|
||||
["2016-12-14", "2016-10-26", "2016-12-15", "2016-04-11", "2019-12-30"]
|
||||
);
|
||||
assert.deepEqual(
|
||||
result.map((x) => x.datetime_count),
|
||||
[1, 1, 2, 1, 1]
|
||||
);
|
||||
|
||||
result = await server.performRpc("", {
|
||||
model: "bar",
|
||||
method: "read_group",
|
||||
args: [[]],
|
||||
kwargs: {
|
||||
fields: ["foo"],
|
||||
domain: [],
|
||||
groupby: ["datetime:week"],
|
||||
},
|
||||
});
|
||||
assert.deepEqual(
|
||||
result.map((x) => x["datetime:week"]),
|
||||
["W50 2016", "W43 2016", "W15 2016", "W01 2020"]
|
||||
);
|
||||
assert.deepEqual(
|
||||
result.map((x) => x.datetime_count),
|
||||
[3, 1, 1, 1]
|
||||
);
|
||||
|
||||
result = await server.performRpc("", {
|
||||
model: "bar",
|
||||
method: "read_group",
|
||||
args: [[]],
|
||||
kwargs: {
|
||||
fields: ["foo"],
|
||||
domain: [],
|
||||
groupby: ["datetime:quarter"],
|
||||
},
|
||||
});
|
||||
assert.deepEqual(
|
||||
result.map((x) => x["datetime:quarter"]),
|
||||
["Q4 2016", "Q2 2016", "Q4 2019"]
|
||||
);
|
||||
assert.deepEqual(
|
||||
result.map((x) => x.datetime_count),
|
||||
[4, 1, 1]
|
||||
);
|
||||
|
||||
result = await server.performRpc("", {
|
||||
model: "bar",
|
||||
method: "read_group",
|
||||
args: [[]],
|
||||
kwargs: {
|
||||
fields: ["foo"],
|
||||
domain: [],
|
||||
groupby: ["datetime:year"],
|
||||
},
|
||||
});
|
||||
assert.deepEqual(
|
||||
result.map((x) => x["datetime:year"]),
|
||||
["2016", "2019"]
|
||||
);
|
||||
assert.deepEqual(
|
||||
result.map((x) => x.datetime_count),
|
||||
[5, 1]
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("performRpc: read_group, group by m2m", async function (assert) {
|
||||
const server = new MockServer(this.data, {});
|
||||
const result = await server.performRpc("", {
|
||||
model: "bar",
|
||||
method: "read_group",
|
||||
args: [[]],
|
||||
kwargs: {
|
||||
fields: ["partners"],
|
||||
domain: [],
|
||||
groupby: ["partners"],
|
||||
},
|
||||
});
|
||||
assert.deepEqual(
|
||||
result,
|
||||
[
|
||||
{
|
||||
__domain: [["partners", "=", 1]],
|
||||
partners: [1, "Jean-Michel"],
|
||||
partners_count: 2,
|
||||
},
|
||||
{
|
||||
__domain: [["partners", "=", 2]],
|
||||
partners: [2, "Raoul"],
|
||||
partners_count: 2,
|
||||
},
|
||||
{
|
||||
__domain: [["partners", "=", false]],
|
||||
partners: false,
|
||||
partners_count: 3,
|
||||
},
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("performRpc: read_group, group by integer", async function (assert) {
|
||||
const server = new MockServer(this.data, {});
|
||||
const result = await server.performRpc("", {
|
||||
model: "bar",
|
||||
method: "read_group",
|
||||
args: [[]],
|
||||
kwargs: {
|
||||
fields: ["foo"],
|
||||
domain: [],
|
||||
groupby: ["foo"],
|
||||
},
|
||||
});
|
||||
assert.deepEqual(result, [
|
||||
{
|
||||
__domain: [["foo", "=", 12]],
|
||||
foo: 12,
|
||||
foo_count: 1,
|
||||
},
|
||||
{
|
||||
__domain: [["foo", "=", 1]],
|
||||
foo: 1,
|
||||
foo_count: 1,
|
||||
},
|
||||
{
|
||||
__domain: [["foo", "=", 17]],
|
||||
foo: 17,
|
||||
foo_count: 1,
|
||||
},
|
||||
{
|
||||
__domain: [["foo", "=", 2]],
|
||||
foo: 2,
|
||||
foo_count: 1,
|
||||
},
|
||||
{
|
||||
__domain: [["foo", "=", 0]],
|
||||
foo: 0,
|
||||
foo_count: 1,
|
||||
},
|
||||
{
|
||||
__domain: [["foo", "=", 42]],
|
||||
foo: 42,
|
||||
foo_count: 1,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,5 @@
|
|||
/** @odoo-module alias=@web/../tests/patch_translations default=false */
|
||||
|
||||
import { translatedTerms, translationLoaded } from "@web/core/l10n/translation";
|
||||
|
||||
translatedTerms[translationLoaded] = true;
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
/** @odoo-module **/
|
||||
import { registerCleanup } from "@web/../tests/helpers/cleanup";
|
||||
import { getFixture, nextTick } from "@web/../tests/helpers/utils";
|
||||
import { DEBOUNCE, makeAsyncHandler, makeButtonHandler } from '@web/legacy/js/public/minimal_dom';
|
||||
|
||||
QUnit.module('core', {}, function () {
|
||||
|
||||
QUnit.module('MinimalDom');
|
||||
|
||||
QUnit.test('MakeButtonHandler does not retrigger the same error', async function (assert) {
|
||||
assert.expect(1);
|
||||
assert.expectErrors();
|
||||
|
||||
// create a target for the button handler
|
||||
const fixture = getFixture();
|
||||
const button = document.createElement("button");
|
||||
fixture.appendChild(button);
|
||||
registerCleanup(() => { button.remove(); });
|
||||
|
||||
// get a way to reject the promise later
|
||||
let rejectPromise;
|
||||
const buttonHandler = makeButtonHandler(() => new Promise((resolve, reject) => {
|
||||
rejectPromise = reject;
|
||||
}));
|
||||
|
||||
// trigger the handler
|
||||
buttonHandler({ target: button });
|
||||
|
||||
// wait for the button effect has been applied before rejecting the promise
|
||||
await new Promise(res => setTimeout(res, DEBOUNCE + 1));
|
||||
rejectPromise(new Error("reject"));
|
||||
|
||||
// check that there was only one unhandledrejection error
|
||||
await nextTick();
|
||||
assert.verifyErrors(["reject"]);
|
||||
});
|
||||
|
||||
QUnit.test('MakeAsyncHandler does not retrigger the same error', async function (assert) {
|
||||
assert.expect(1);
|
||||
assert.expectErrors();
|
||||
|
||||
// get a way to reject the promise later
|
||||
let rejectPromise;
|
||||
const asyncHandler = makeAsyncHandler(() => new Promise((resolve, reject) => {
|
||||
rejectPromise = reject;
|
||||
}));
|
||||
|
||||
// trigger the handler
|
||||
asyncHandler();
|
||||
|
||||
rejectPromise(new Error("reject"));
|
||||
|
||||
// check that there was only one unhandledrejection error
|
||||
await nextTick();
|
||||
assert.verifyErrors(["reject"]);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,12 +1,8 @@
|
|||
odoo.define('web.widget_tests', function (require) {
|
||||
"use strict";
|
||||
import publicWidget from "@web/legacy/js/public/public_widget";
|
||||
import testUtils from "@web/../tests/legacy_tests/helpers/test_utils";
|
||||
import { renderToString } from "@web/core/utils/render";
|
||||
|
||||
var AjaxService = require('web.AjaxService');
|
||||
var core = require('web.core');
|
||||
var Dialog = require('web.Dialog');
|
||||
var QWeb = require('web.QWeb');
|
||||
var Widget = require('web.Widget');
|
||||
var testUtils = require('web.test_utils');
|
||||
const Widget = publicWidget.Widget;
|
||||
|
||||
QUnit.module('core', {}, function () {
|
||||
|
||||
|
|
@ -101,7 +97,7 @@ QUnit.module('core', {}, function () {
|
|||
|
||||
assert.strictEqual(widget.el.nodeName, 'DIV', "should have generated the default element");
|
||||
assert.strictEqual(widget.el.attributes.length, 0, "should not have generated any attribute");
|
||||
assert.ok(_.isEmpty(widget.$el.html(), "should not have generated any content"));
|
||||
assert.ok(Object.keys(widget.$el.html() || {}).length === 0, "should not have generated any content");
|
||||
widget.destroy();
|
||||
});
|
||||
|
||||
|
|
@ -179,22 +175,18 @@ QUnit.module('core', {}, function () {
|
|||
QUnit.test('template', function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
core.qweb.add_template(
|
||||
'<no>' +
|
||||
'<t t-name="test.widget.template">' +
|
||||
'<ol>' +
|
||||
'<li t-foreach="5" t-as="counter" ' +
|
||||
't-attf-class="class-#{counter}">' +
|
||||
'<input/>' +
|
||||
'<t t-esc="counter"/>' +
|
||||
'</li>' +
|
||||
'</ol>' +
|
||||
'</t>' +
|
||||
'</no>'
|
||||
renderToString.app.addTemplate(
|
||||
"test.widget.template.1",
|
||||
`<ol>
|
||||
<li t-foreach="[0, 1, 2, 3, 4]" t-as="counter" t-key="counter_index" t-attf-class="class-#{counter}">
|
||||
<input/>
|
||||
<t t-esc="counter"/>
|
||||
</li>
|
||||
</ol>`
|
||||
);
|
||||
|
||||
var widget = new (Widget.extend({
|
||||
template: 'test.widget.template'
|
||||
template: 'test.widget.template.1'
|
||||
}))();
|
||||
widget.renderElement();
|
||||
|
||||
|
|
@ -208,15 +200,14 @@ QUnit.module('core', {}, function () {
|
|||
assert.expect(4);
|
||||
var $fix = $( "#qunit-fixture");
|
||||
|
||||
core.qweb.add_template(
|
||||
'<no>' +
|
||||
'<t t-name="test.widget.template">' +
|
||||
'<p><t t-esc="widget.value"/></p>' +
|
||||
'</t>' +
|
||||
'</no>'
|
||||
renderToString.app.addTemplate(
|
||||
"test.widget.template.2",
|
||||
`<p>
|
||||
<t t-esc="widget.value"/>
|
||||
</p>`
|
||||
);
|
||||
var widget = new (Widget.extend({
|
||||
template: 'test.widget.template'
|
||||
template: 'test.widget.template.2'
|
||||
}))();
|
||||
widget.value = 42;
|
||||
|
||||
|
|
@ -233,35 +224,23 @@ QUnit.module('core', {}, function () {
|
|||
});
|
||||
|
||||
|
||||
QUnit.module('Widgets, with QWeb', {
|
||||
beforeEach: function() {
|
||||
this.oldQWeb = core.qweb;
|
||||
core.qweb = new QWeb();
|
||||
core.qweb.add_template(
|
||||
'<no>' +
|
||||
'<t t-name="test.widget.template">' +
|
||||
'<ol>' +
|
||||
'<li t-foreach="5" t-as="counter" ' +
|
||||
't-attf-class="class-#{counter}">' +
|
||||
'<input/>' +
|
||||
'<t t-esc="counter"/>' +
|
||||
'</li>' +
|
||||
'</ol>' +
|
||||
'</t>' +
|
||||
'</no>'
|
||||
);
|
||||
},
|
||||
afterEach: function() {
|
||||
core.qweb = this.oldQWeb;
|
||||
},
|
||||
});
|
||||
QUnit.module('Widgets, with QWeb');
|
||||
|
||||
QUnit.test('basic-alias', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
renderToString.app.addTemplate(
|
||||
"test.widget.template.3",
|
||||
`<ol>
|
||||
<li t-foreach="[0,1,2,3,4]" t-as="counter" t-key="counter_index" t-attf-class="class-#{counter}">
|
||||
<input/>
|
||||
<t t-esc="counter"/>
|
||||
</li>
|
||||
</ol>`
|
||||
);
|
||||
|
||||
var widget = new (Widget.extend({
|
||||
template: 'test.widget.template'
|
||||
template: 'test.widget.template.3'
|
||||
}))();
|
||||
widget.renderElement();
|
||||
|
||||
|
|
@ -274,9 +253,19 @@ QUnit.module('core', {}, function () {
|
|||
QUnit.test('delegate', async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
renderToString.app.addTemplate(
|
||||
"test.widget.template.4",
|
||||
`<ol>
|
||||
<li t-foreach="[0,1,2,3,4]" t-as="counter" t-key="counter_index" t-attf-class="class-#{counter}">
|
||||
<input/>
|
||||
<t t-esc="counter"/>
|
||||
</li>
|
||||
</ol>`
|
||||
);
|
||||
|
||||
var a = [];
|
||||
var widget = new (Widget.extend({
|
||||
template: 'test.widget.template',
|
||||
template: 'test.widget.template.4',
|
||||
events: {
|
||||
'click': function () {
|
||||
a[0] = true;
|
||||
|
|
@ -302,11 +291,21 @@ QUnit.module('core', {}, function () {
|
|||
QUnit.test('undelegate', async function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
renderToString.app.addTemplate(
|
||||
"test.widget.template.5",
|
||||
`<ol>
|
||||
<li t-foreach="[0,1,2,3,4]" t-as="counter" t-key="counter_index" t-attf-class="class-#{counter}">
|
||||
<input/>
|
||||
<t t-esc="counter"/>
|
||||
</li>
|
||||
</ol>`
|
||||
);
|
||||
|
||||
var clicked = false;
|
||||
var newclicked = false;
|
||||
|
||||
var widget = new (Widget.extend({
|
||||
template: 'test.widget.template',
|
||||
template: 'test.widget.template.5',
|
||||
events: { 'click li': function () { clicked = true; } }
|
||||
}))();
|
||||
|
||||
|
|
@ -327,150 +326,6 @@ QUnit.module('core', {}, function () {
|
|||
|
||||
QUnit.module('Widget, and async stuff');
|
||||
|
||||
QUnit.test("alive(alive)", async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
var widget = new (Widget.extend({}));
|
||||
|
||||
await widget.start()
|
||||
.then(function () {return widget.alive(Promise.resolve()) ;})
|
||||
.then(function () { assert.ok(true); });
|
||||
|
||||
widget.destroy();
|
||||
});
|
||||
|
||||
QUnit.test("alive(dead)", function (assert) {
|
||||
assert.expect(1);
|
||||
var widget = new (Widget.extend({}));
|
||||
|
||||
return new Promise(function (resolve, reject) {
|
||||
widget.start()
|
||||
.then(function () {
|
||||
// destroy widget
|
||||
widget.destroy();
|
||||
var promise = Promise.resolve();
|
||||
// leave time for alive() to do its stuff
|
||||
promise.then(function () {
|
||||
return Promise.resolve();
|
||||
}).then(function () {
|
||||
assert.ok(true);
|
||||
resolve();
|
||||
});
|
||||
// ensure that widget.alive() refuses to resolve or reject
|
||||
return widget.alive(promise);
|
||||
}).then(function () {
|
||||
reject();
|
||||
assert.ok(false, "alive() should not terminate by default");
|
||||
}).catch(function() {
|
||||
reject();
|
||||
assert.ok(false, "alive() should not terminate by default");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("alive(alive, true)", async function (assert) {
|
||||
assert.expect(1);
|
||||
var widget = new (Widget.extend({}));
|
||||
await widget.start()
|
||||
.then(function () { return widget.alive(Promise.resolve(), true); })
|
||||
.then(function () { assert.ok(true); });
|
||||
widget.destroy();
|
||||
});
|
||||
|
||||
QUnit.test("alive(dead, true)", function (assert) {
|
||||
assert.expect(1);
|
||||
var done = assert.async();
|
||||
|
||||
var widget = new (Widget.extend({}));
|
||||
|
||||
widget.start()
|
||||
.then(function () {
|
||||
// destroy widget
|
||||
widget.destroy();
|
||||
return widget.alive(Promise.resolve(), true);
|
||||
}).then(function () {
|
||||
assert.ok(false, "alive(p, true) should fail its promise");
|
||||
done();
|
||||
}, function () {
|
||||
assert.ok(true, "alive(p, true) should fail its promise");
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("calling _rpc on destroyed widgets", async function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
var def;
|
||||
var parent = new Widget();
|
||||
await testUtils.mock.addMockEnvironment(parent, {
|
||||
session: {
|
||||
rpc: function () {
|
||||
def = testUtils.makeTestPromise();
|
||||
def.abort = def.reject;
|
||||
return def;
|
||||
},
|
||||
},
|
||||
services: {
|
||||
ajax: AjaxService
|
||||
},
|
||||
});
|
||||
var widget = new Widget(parent);
|
||||
|
||||
widget._rpc({route: '/a/route'}).then(function () {
|
||||
assert.ok(true, "The ajax call should be resolve");
|
||||
});
|
||||
def.resolve();
|
||||
await testUtils.nextMicrotaskTick();
|
||||
def = null;
|
||||
|
||||
widget._rpc({route: '/a/route'}).then(function () {
|
||||
throw Error("Calling _rpc on a destroyed widget should return a " +
|
||||
"promise that remains pending forever");
|
||||
}).catch(function () {
|
||||
throw Error("Calling _rpc on a destroyed widget should return a " +
|
||||
"promise that remains pending forever");
|
||||
});
|
||||
widget.destroy();
|
||||
def.resolve();
|
||||
await testUtils.nextMicrotaskTick();
|
||||
def = null;
|
||||
|
||||
widget._rpc({route: '/a/route'}).then(function () {
|
||||
throw Error("Calling _rpc on a destroyed widget should return a " +
|
||||
"promise that remains pending forever");
|
||||
}).catch(function () {
|
||||
throw Error("Calling _rpc on a destroyed widget should return a " +
|
||||
"promise that remains pending forever");
|
||||
});
|
||||
assert.ok(!def, "trigger_up is not performed and the call returns a " +
|
||||
"promise that remains pending forever");
|
||||
|
||||
assert.ok(true,
|
||||
"there should be no crash when calling _rpc on a destroyed widget");
|
||||
parent.destroy();
|
||||
});
|
||||
|
||||
QUnit.test("calling do_hide on a widget destroyed before being rendered", async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
const MyWidget = Widget.extend({
|
||||
willStart() {
|
||||
return new Promise(() => {});
|
||||
}
|
||||
});
|
||||
|
||||
const widget = new MyWidget();
|
||||
widget.appendTo(document.createDocumentFragment());
|
||||
widget.destroy();
|
||||
|
||||
// those calls should not crash
|
||||
widget.do_hide();
|
||||
widget.do_show();
|
||||
widget.do_toggle(true);
|
||||
|
||||
assert.ok(true);
|
||||
});
|
||||
|
||||
QUnit.test('start is not called when widget is destroyed', function (assert) {
|
||||
assert.expect(0);
|
||||
const $fix = $("#qunit-fixture");
|
||||
|
|
@ -506,25 +361,4 @@ QUnit.module('core', {}, function () {
|
|||
parent.destroy();
|
||||
assert.verifySteps(['destroy'], "child should have been detroyed only once");
|
||||
});
|
||||
|
||||
|
||||
QUnit.module('Widgets, Dialog');
|
||||
|
||||
QUnit.test("don't close dialog on backdrop click", async function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
var dialog = new Dialog(null);
|
||||
dialog.open();
|
||||
await dialog.opened();
|
||||
|
||||
assert.strictEqual($('.modal.show').length, 1, "a dialog should have opened");
|
||||
var $backdrop = $('.modal-backdrop');
|
||||
assert.strictEqual($backdrop.length, 1, "the dialog should have a modal backdrop");
|
||||
testUtils.dom.click('.modal.show'); // Click on backdrop is in fact a direct click on the .modal element
|
||||
assert.strictEqual($('.modal.show').length, 1, "the dialog should still be opened");
|
||||
|
||||
dialog.close();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
667
odoo-bringout-oca-ocb-web/web/static/tests/legacy/qunit.js
Normal file
667
odoo-bringout-oca-ocb-web/web/static/tests/legacy/qunit.js
Normal file
|
|
@ -0,0 +1,667 @@
|
|||
/** @odoo-module alias=@web/../tests/qunit default=false */
|
||||
|
||||
import { isVisible as isElemVisible } from "@web/core/utils/ui";
|
||||
import { fullTraceback, fullAnnotatedTraceback } from "@web/core/errors/error_utils";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { Component, whenReady } from "@odoo/owl";
|
||||
|
||||
const consoleError = console.error;
|
||||
|
||||
function setQUnitDebugMode() {
|
||||
whenReady(() => document.body.classList.add("debug")); // make the test visible to the naked eye
|
||||
QUnit.config.debug = true; // allows for helper functions to behave differently (logging, the HTML element in which the test occurs etc...)
|
||||
QUnit.config.testTimeout = 60 * 60 * 1000;
|
||||
// Allows for interacting with the test when it is over
|
||||
// In fact, this will pause QUnit.
|
||||
// Also, logs useful info in the console.
|
||||
QUnit.testDone(async (...args) => {
|
||||
console.groupCollapsed("Debug Test output");
|
||||
console.log(...args);
|
||||
console.groupEnd();
|
||||
await new Promise(() => {});
|
||||
});
|
||||
}
|
||||
|
||||
// need to do this outside of the setup function so the QUnit.debug is defined when we need it
|
||||
QUnit.debug = (name, cb) => {
|
||||
setQUnitDebugMode();
|
||||
QUnit.only(name, cb);
|
||||
};
|
||||
|
||||
// need to do this outside of the setup function so it is executed quickly
|
||||
QUnit.config.autostart = false;
|
||||
|
||||
export function setupQUnit() {
|
||||
// -----------------------------------------------------------------------------
|
||||
// QUnit config
|
||||
// -----------------------------------------------------------------------------
|
||||
QUnit.config.testTimeout = 1 * 60 * 1000;
|
||||
QUnit.config.hidepassed = window.location.href.match(/[?&]testId=/) === null;
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// QUnit assert
|
||||
// -----------------------------------------------------------------------------
|
||||
/**
|
||||
* Checks that the target contains exactly n matches for the selector.
|
||||
*
|
||||
* Example: assert.containsN(document.body, '.modal', 0)
|
||||
*/
|
||||
function containsN(target, selector, n, msg) {
|
||||
let $el;
|
||||
if (target._widgetRenderAndInsert) {
|
||||
$el = target.$el; // legacy widget
|
||||
} else if (target instanceof Component) {
|
||||
if (!target.el) {
|
||||
throw new Error(
|
||||
`containsN assert with selector '${selector}' called on an unmounted component`
|
||||
);
|
||||
}
|
||||
$el = $(target.el);
|
||||
} else {
|
||||
$el = target instanceof Element ? $(target) : target;
|
||||
}
|
||||
msg = msg || `Selector '${selector}' should have exactly ${n} matches inside the target`;
|
||||
QUnit.assert.strictEqual($el.find(selector).length, n, msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks that the target contains exactly 0 match for the selector.
|
||||
*
|
||||
* @param {Element} el
|
||||
* @param {string} selector
|
||||
* @param {string} [msg]
|
||||
*/
|
||||
function containsNone(target, selector, msg) {
|
||||
containsN(target, selector, 0, msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks that the target contains exactly 1 match for the selector.
|
||||
*
|
||||
* @param {Element} el
|
||||
* @param {string} selector
|
||||
* @param {string} [msg]
|
||||
*/
|
||||
function containsOnce(target, selector, msg) {
|
||||
containsN(target, selector, 1, msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function, to check if a given element has (or has not) classnames.
|
||||
*
|
||||
* @private
|
||||
* @param {Element | jQuery | Widget} el
|
||||
* @param {string} classNames
|
||||
* @param {boolean} shouldHaveClass
|
||||
* @param {string} [msg]
|
||||
*/
|
||||
function _checkClass(el, classNames, shouldHaveClass, msg) {
|
||||
if (el) {
|
||||
if (el._widgetRenderAndInsert) {
|
||||
el = el.el; // legacy widget
|
||||
} else if (!(el instanceof Element)) {
|
||||
el = el[0];
|
||||
}
|
||||
}
|
||||
msg =
|
||||
msg ||
|
||||
`target should ${shouldHaveClass ? "have" : "not have"} classnames ${classNames}`;
|
||||
const isFalse = classNames.split(" ").some((cls) => {
|
||||
const hasClass = el.classList.contains(cls);
|
||||
return shouldHaveClass ? !hasClass : hasClass;
|
||||
});
|
||||
QUnit.assert.ok(!isFalse, msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks that the target element has the given classnames.
|
||||
*
|
||||
* @param {Element} el
|
||||
* @param {string} classNames
|
||||
* @param {string} [msg]
|
||||
*/
|
||||
function hasClass(el, classNames, msg) {
|
||||
_checkClass(el, classNames, true, msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks that the target element does not have the given classnames.
|
||||
*
|
||||
* @param {Element} el
|
||||
* @param {string} classNames
|
||||
* @param {string} [msg]
|
||||
*/
|
||||
function doesNotHaveClass(el, classNames, msg) {
|
||||
_checkClass(el, classNames, false, msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks that the target element (described by widget/jquery or html element)
|
||||
* - exists
|
||||
* - is unique
|
||||
* - has the given attribute with the proper value
|
||||
*
|
||||
* @param {Component | Element | Widget | jQuery} w
|
||||
* @param {string} attr
|
||||
* @param {string} value
|
||||
* @param {string} [msg]
|
||||
*/
|
||||
function hasAttrValue(target, attr, value, msg) {
|
||||
let $el;
|
||||
if (target._widgetRenderAndInsert) {
|
||||
$el = target.$el; // legacy widget
|
||||
} else if (target instanceof Component) {
|
||||
if (!target.el) {
|
||||
throw new Error(
|
||||
`hasAttrValue assert with attr '${attr}' called on an unmounted component`
|
||||
);
|
||||
}
|
||||
$el = $(target.el);
|
||||
} else {
|
||||
$el = target instanceof Element ? $(target) : target;
|
||||
}
|
||||
|
||||
if ($el.length !== 1) {
|
||||
const descr = `hasAttrValue (${attr}: ${value})`;
|
||||
QUnit.assert.ok(
|
||||
false,
|
||||
`Assertion '${descr}' targets ${$el.length} elements instead of 1`
|
||||
);
|
||||
} else {
|
||||
msg = msg || `attribute '${attr}' of target should be '${value}'`;
|
||||
QUnit.assert.strictEqual($el.attr(attr), value, msg);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function, to check if a given element
|
||||
* - is unique (if it is a jquery node set)
|
||||
* - is (or not) visible
|
||||
*
|
||||
* @private
|
||||
* @param {Element | jQuery | Widget} el
|
||||
* @param {boolean} shouldBeVisible
|
||||
* @param {string} [msg]
|
||||
*/
|
||||
function _checkVisible(el, shouldBeVisible, msg) {
|
||||
if (el) {
|
||||
if (el._widgetRenderAndInsert) {
|
||||
el = el.el; // legacy widget
|
||||
} else if (!(el instanceof Element)) {
|
||||
el = el[0];
|
||||
}
|
||||
}
|
||||
msg = msg || `target should ${shouldBeVisible ? "" : "not"} be visible`;
|
||||
const _isVisible = isElemVisible(el);
|
||||
const condition = shouldBeVisible ? _isVisible : !_isVisible;
|
||||
QUnit.assert.ok(condition, msg);
|
||||
}
|
||||
function isVisible(el, msg) {
|
||||
return _checkVisible(el, true, msg);
|
||||
}
|
||||
function isNotVisible(el, msg) {
|
||||
return _checkVisible(el, false, msg);
|
||||
}
|
||||
function expectErrors() {
|
||||
QUnit.config.current.expectErrors = true;
|
||||
QUnit.config.current.unverifiedErrors = [];
|
||||
}
|
||||
function verifyErrors(expectedErrors) {
|
||||
if (!QUnit.config.current.expectErrors) {
|
||||
QUnit.pushFailure(`assert.expectErrors() must be called at the beginning of the test`);
|
||||
return;
|
||||
}
|
||||
const unverifiedErrors = QUnit.config.current.unverifiedErrors;
|
||||
QUnit.config.current.assert.deepEqual(unverifiedErrors, expectedErrors, "verifying errors");
|
||||
QUnit.config.current.unverifiedErrors = [];
|
||||
}
|
||||
QUnit.assert.containsN = containsN;
|
||||
QUnit.assert.containsNone = containsNone;
|
||||
QUnit.assert.containsOnce = containsOnce;
|
||||
QUnit.assert.doesNotHaveClass = doesNotHaveClass;
|
||||
QUnit.assert.hasClass = hasClass;
|
||||
QUnit.assert.hasAttrValue = hasAttrValue;
|
||||
QUnit.assert.isVisible = isVisible;
|
||||
QUnit.assert.isNotVisible = isNotVisible;
|
||||
QUnit.assert.expectErrors = expectErrors;
|
||||
QUnit.assert.verifyErrors = verifyErrors;
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// QUnit logs
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* If we want to log several errors, we have to log all of them at once, as
|
||||
* browser_js is closed as soon as an error is logged.
|
||||
*/
|
||||
let errorMessages = [];
|
||||
async function logErrors() {
|
||||
const messages = errorMessages.slice();
|
||||
errorMessages = [];
|
||||
const infos = await Promise.all(messages);
|
||||
consoleError(infos.map((info) => info.error || info).join("\n"));
|
||||
// Only log the source of the errors in "info" log level to allow matching the same
|
||||
// error with its log message, as source contains asset file name which changes
|
||||
console.info(
|
||||
infos
|
||||
.map((info) =>
|
||||
info.source ? `${info.error}\n${info.source.replace(/^/gm, "\t")}\n` : info
|
||||
)
|
||||
.join("\n")
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* If we want to log several errors, we have to log all of them at once, as
|
||||
* browser_js is closed as soon as an error is logged.
|
||||
*/
|
||||
QUnit.done(async (result) => {
|
||||
await odoo.loader.checkErrorProm;
|
||||
const moduleLoadingError = document.querySelector(".o_module_error");
|
||||
if (moduleLoadingError) {
|
||||
errorMessages.unshift(moduleLoadingError.innerText);
|
||||
}
|
||||
if (result.failed) {
|
||||
errorMessages.push(`${result.failed} / ${result.total} tests failed.`);
|
||||
}
|
||||
if (!result.failed && !moduleLoadingError) {
|
||||
// use console.dir for this log to appear on runbot sub-builds page
|
||||
console.dir(
|
||||
`QUnit: passed ${testPassedCount} tests (${
|
||||
result.passed
|
||||
} assertions), took ${Math.round(result.runtime / 1000)}s`
|
||||
);
|
||||
console.log("QUnit test suite done.");
|
||||
} else {
|
||||
logErrors();
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* This is done mostly for the .txt log file generated by the runbot.
|
||||
*/
|
||||
QUnit.moduleDone(async (result) => {
|
||||
if (!result.failed) {
|
||||
console.log(
|
||||
`"${result.name}" passed ${result.tests.length} tests (${result.total} assertions).`
|
||||
);
|
||||
} else {
|
||||
console.log(
|
||||
'"' + result.name + '"',
|
||||
"failed",
|
||||
result.failed,
|
||||
"tests out of",
|
||||
result.total,
|
||||
"."
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* This logs various data in the console, which will be available in the log
|
||||
* .txt file generated by the runbot.
|
||||
*/
|
||||
QUnit.log((result) => {
|
||||
if (result.result) {
|
||||
return;
|
||||
}
|
||||
errorMessages.push(
|
||||
Promise.resolve(result.annotateProm).then(() => {
|
||||
let info = `QUnit test failed: ${result.module} > ${result.name} :`;
|
||||
if (result.message) {
|
||||
info += `\n\tmessage: "${result.message}"`;
|
||||
}
|
||||
if ("expected" in result) {
|
||||
info += `\n\texpected: "${result.expected}"`;
|
||||
}
|
||||
if (result.actual !== null) {
|
||||
info += `\n\tactual: "${result.actual}"`;
|
||||
}
|
||||
return {
|
||||
error: info,
|
||||
source: result.source,
|
||||
};
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* The purpose of this function is to reset the timer nesting level of the execution context
|
||||
* to 0, to prevent situations where a setTimeout with a timeout of 0 may end up being
|
||||
* scheduled after another one that also has a timeout of 0 that was called later.
|
||||
* Example code:
|
||||
* (async () => {
|
||||
* const timeout = () => new Promise((resolve) => setTimeout(resolve, 0));
|
||||
* const animationFrame = () => new Promise((resolve) => requestAnimationFrame(resolve));
|
||||
*
|
||||
* for (let i = 0; i < 4; i++) {
|
||||
* await timeout();
|
||||
* }
|
||||
* timeout().then(() => console.log("after timeout"));
|
||||
* await animationFrame()
|
||||
* timeout().then(() => console.log("after animationFrame"));
|
||||
* // logs "after animationFrame" before "after timeout"
|
||||
* })()
|
||||
*
|
||||
* When the browser runs a task that was the result of a timer (setTimeout or setInterval),
|
||||
* that task has an intrinsic "timer nesting level". If you schedule another task with
|
||||
* a timer from within such a task, the new task has the existing task's timer nesting level,
|
||||
* plus one. When the timer nesting level of a task is greater than 5, the `timeout` parameter
|
||||
* for setTimeout/setInterval will be forced to at least 4 (see step 5 in the timer initialization
|
||||
* steps in the HTML spec: https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html#timer-initialisation-steps).
|
||||
*
|
||||
* In the above example, every `await timeout()` besides inside the loop schedules a new task
|
||||
* from within a task that was initiated by a timer, causing the nesting level to be 5 after
|
||||
* the loop. The first timeout after the loop is now forced to 4.
|
||||
*
|
||||
* When we await the animation frame promise, we create a task that is *not* initiated by a timer,
|
||||
* reseting the nesting level to 0, causing the timeout following it to properly be treated as 0,
|
||||
* as such the callback that was registered by it is oftentimes executed before the previous one.
|
||||
*
|
||||
* While we can't prevent this from happening within a given test, we want to at least prevent
|
||||
* the timer nesting level to propagate from one test to the next as this can be a cause of
|
||||
* indeterminism. To avoid slowing down the tests by waiting one frame after every test,
|
||||
* we instead use a MessageChannel to add a task with not nesting level to the event queue immediately.
|
||||
*/
|
||||
QUnit.testDone(async () => {
|
||||
return new Promise((resolve) => {
|
||||
const channel = new MessageChannel();
|
||||
channel.port1.onmessage = () => {
|
||||
channel.port1.close();
|
||||
channel.port2.close();
|
||||
resolve();
|
||||
};
|
||||
channel.port2.postMessage("");
|
||||
});
|
||||
});
|
||||
|
||||
// Append a "Rerun in debug" link.
|
||||
// Only works if the test is not hidden.
|
||||
QUnit.testDone(async ({ testId }) => {
|
||||
if (errorMessages.length > 0) {
|
||||
logErrors();
|
||||
}
|
||||
const testElement = document.getElementById(`qunit-test-output-${testId}`);
|
||||
if (!testElement) {
|
||||
// Is probably hidden because it passed
|
||||
return;
|
||||
}
|
||||
const reRun = testElement.querySelector("li a");
|
||||
const reRunDebug = document.createElement("a");
|
||||
reRunDebug.textContent = "Rerun in debug";
|
||||
const url = new URL(window.location);
|
||||
url.searchParams.set("testId", testId);
|
||||
url.searchParams.set("debugTest", "true");
|
||||
reRunDebug.setAttribute("href", url.href);
|
||||
reRun.parentElement.insertBefore(reRunDebug, reRun.nextSibling);
|
||||
});
|
||||
|
||||
const debugTest = new URLSearchParams(location.search).get("debugTest");
|
||||
if (debugTest) {
|
||||
setQUnitDebugMode();
|
||||
}
|
||||
|
||||
// Override global UnhandledRejection that is assigned wayyy before this file
|
||||
// Do not really crash on non-errors rejections
|
||||
const qunitUnhandledReject = QUnit.onUnhandledRejection;
|
||||
QUnit.onUnhandledRejection = (reason) => {
|
||||
const error = reason instanceof Error && "cause" in reason ? reason.cause : reason;
|
||||
if (error instanceof Error) {
|
||||
qunitUnhandledReject(reason);
|
||||
}
|
||||
};
|
||||
|
||||
// Essentially prevents default error logging when the rejection was
|
||||
// not due to an actual error
|
||||
const windowUnhandledReject = window.onunhandledrejection;
|
||||
window.onunhandledrejection = (ev) => {
|
||||
const error =
|
||||
ev.reason instanceof Error && "cause" in ev.reason ? ev.reason.cause : ev.reason;
|
||||
if (!(error instanceof Error)) {
|
||||
ev.stopImmediatePropagation();
|
||||
ev.preventDefault();
|
||||
} else if (windowUnhandledReject) {
|
||||
windowUnhandledReject.call(window, ev);
|
||||
}
|
||||
};
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// FailFast
|
||||
// -----------------------------------------------------------------------------
|
||||
/**
|
||||
* We add here a 'fail fast' feature: we often want to stop the test suite after
|
||||
* the first failed test. This is also useful for the runbot test suites.
|
||||
*/
|
||||
QUnit.config.urlConfig.push({
|
||||
id: "failfast",
|
||||
label: "Fail Fast",
|
||||
tooltip: "Stop the test suite immediately after the first failed test.",
|
||||
});
|
||||
|
||||
QUnit.begin(function () {
|
||||
if (odoo.debug && odoo.debug.includes("assets")) {
|
||||
QUnit.annotateTraceback = fullAnnotatedTraceback;
|
||||
} else {
|
||||
QUnit.annotateTraceback = (err) => Promise.resolve(fullTraceback(err));
|
||||
}
|
||||
const config = QUnit.config;
|
||||
if (config.failfast) {
|
||||
QUnit.testDone(function (details) {
|
||||
if (details.failed > 0) {
|
||||
config.queue.length = 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Add sort button
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
let sortButtonAppended = false;
|
||||
/**
|
||||
* Add a sort button on top of the QUnit result page, so we can see which tests
|
||||
* take the most time.
|
||||
*/
|
||||
function addSortButton() {
|
||||
sortButtonAppended = true;
|
||||
var $sort = $("<label> sort by time (desc)</label>").css({ float: "right" });
|
||||
$("h2#qunit-userAgent").append($sort);
|
||||
$sort.click(function () {
|
||||
var $ol = $("ol#qunit-tests");
|
||||
var $results = $ol.children("li").get();
|
||||
$results.sort(function (a, b) {
|
||||
var timeA = Number($(a).find("span.runtime").first().text().split(" ")[0]);
|
||||
var timeB = Number($(b).find("span.runtime").first().text().split(" ")[0]);
|
||||
if (timeA < timeB) {
|
||||
return 1;
|
||||
} else if (timeA > timeB) {
|
||||
return -1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
$.each($results, function (idx, $itm) {
|
||||
$ol.append($itm);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
QUnit.done(() => {
|
||||
if (!sortButtonAppended) {
|
||||
addSortButton();
|
||||
}
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Add statistics
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
let passedEl;
|
||||
let failedEl;
|
||||
let skippedEl;
|
||||
let todoCompletedEl;
|
||||
let todoUncompletedEl;
|
||||
function insertStats() {
|
||||
const toolbar = document.querySelector("#qunit-testrunner-toolbar .qunit-url-config");
|
||||
const statsEl = document.createElement("label");
|
||||
passedEl = document.createElement("span");
|
||||
passedEl.classList.add("text-success", "ms-5", "me-3");
|
||||
statsEl.appendChild(passedEl);
|
||||
todoCompletedEl = document.createElement("span");
|
||||
todoCompletedEl.classList.add("text-warning", "me-3");
|
||||
statsEl.appendChild(todoCompletedEl);
|
||||
failedEl = document.createElement("span");
|
||||
failedEl.classList.add("text-danger", "me-3");
|
||||
statsEl.appendChild(failedEl);
|
||||
todoUncompletedEl = document.createElement("span");
|
||||
todoUncompletedEl.classList.add("text-primary", "me-3");
|
||||
statsEl.appendChild(todoUncompletedEl);
|
||||
skippedEl = document.createElement("span");
|
||||
skippedEl.classList.add("text-dark");
|
||||
statsEl.appendChild(skippedEl);
|
||||
toolbar.appendChild(statsEl);
|
||||
}
|
||||
|
||||
let testPassedCount = 0;
|
||||
let testFailedCount = 0;
|
||||
let testSkippedCount = 0;
|
||||
let todoCompletedCount = 0;
|
||||
let todoUncompletedCount = 0;
|
||||
QUnit.testDone(({ skipped, failed, todo }) => {
|
||||
if (!passedEl) {
|
||||
insertStats();
|
||||
}
|
||||
if (!skipped) {
|
||||
if (failed > 0) {
|
||||
if (todo) {
|
||||
todoUncompletedCount++;
|
||||
} else {
|
||||
testFailedCount++;
|
||||
}
|
||||
} else {
|
||||
if (todo) {
|
||||
todoCompletedCount++;
|
||||
} else {
|
||||
testPassedCount++;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
testSkippedCount++;
|
||||
}
|
||||
passedEl.innerText = `${testPassedCount} passed`;
|
||||
if (todoCompletedCount > 0) {
|
||||
todoCompletedEl.innerText = `${todoCompletedCount} todo completed`;
|
||||
}
|
||||
if (todoUncompletedCount > 0) {
|
||||
todoUncompletedEl.innerText = `${todoUncompletedCount} todo uncompleted`;
|
||||
}
|
||||
if (testFailedCount > 0) {
|
||||
failedEl.innerText = `${testFailedCount} failed`;
|
||||
}
|
||||
if (testSkippedCount > 0) {
|
||||
skippedEl.innerText = `${testSkippedCount} skipped`;
|
||||
}
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// FIXME: This sounds stupid, it feels stupid... but it fixes visibility check in folded <details> since Chromium 97+ 💩
|
||||
// Since https://bugs.chromium.org/p/chromium/issues/detail?id=1185950
|
||||
// See regression report https://bugs.chromium.org/p/chromium/issues/detail?id=1276028
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
QUnit.begin(() => {
|
||||
const el = document.createElement("style");
|
||||
el.innerText = "details:not([open]) > :not(summary) { display: none; }";
|
||||
document.head.appendChild(el);
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Error management
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
QUnit.on("OdooAfterTestHook", (info) => {
|
||||
const { expectErrors, unverifiedErrors } = QUnit.config.current;
|
||||
if (expectErrors && unverifiedErrors.length) {
|
||||
QUnit.pushFailure(
|
||||
`Expected assert.verifyErrors() to be called before end of test. Unverified errors: ${unverifiedErrors}`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
const { onUnhandledRejection } = QUnit;
|
||||
QUnit.onUnhandledRejection = () => {};
|
||||
QUnit.onError = () => {};
|
||||
|
||||
console.error = function () {
|
||||
if (QUnit.config.current) {
|
||||
QUnit.pushFailure(`console.error called with "${arguments[0]}"`);
|
||||
} else {
|
||||
consoleError(...arguments);
|
||||
}
|
||||
};
|
||||
|
||||
function onUncaughtErrorInTest(error) {
|
||||
if (!QUnit.config.current.expectErrors) {
|
||||
// we did not expect any error, so notify qunit to add a failure
|
||||
onUnhandledRejection(error);
|
||||
} else {
|
||||
// we expected errors, so store it, it will be checked later (see verifyErrors)
|
||||
while (error instanceof Error && "cause" in error) {
|
||||
error = error.cause;
|
||||
}
|
||||
QUnit.config.current.unverifiedErrors.push(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// e.g. setTimeout(() => throw new Error()) (event handler crashes synchronously)
|
||||
window.addEventListener("error", async (ev) => {
|
||||
if (!QUnit.config.current) {
|
||||
return; // we are not in a test -> do nothing
|
||||
}
|
||||
// do not log to the console as this will kill python test early
|
||||
ev.preventDefault();
|
||||
// if the error service is deployed, we'll get to the patched default handler below if no
|
||||
// other handler handled the error, so do nothing here
|
||||
if (registry.category("services").get("error", false)) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
ev.message === "ResizeObserver loop limit exceeded" ||
|
||||
ev.message === "ResizeObserver loop completed with undelivered notifications."
|
||||
) {
|
||||
return;
|
||||
}
|
||||
onUncaughtErrorInTest(ev.error);
|
||||
});
|
||||
|
||||
// e.g. Promise.resolve().then(() => throw new Error()) (crash in event handler after async boundary)
|
||||
window.addEventListener("unhandledrejection", async (ev) => {
|
||||
if (!QUnit.config.current) {
|
||||
return; // we are not in a test -> do nothing
|
||||
}
|
||||
// do not log to the console as this will kill python test early
|
||||
ev.preventDefault();
|
||||
// if the error service is deployed, we'll get to the patched default handler below if no
|
||||
// other handler handled the error, so do nothing here
|
||||
if (registry.category("services").get("error", false)) {
|
||||
return;
|
||||
}
|
||||
onUncaughtErrorInTest(ev.reason);
|
||||
});
|
||||
|
||||
// This is an approximation, but we can't directly import the default error handler, because
|
||||
// it's not the same in all tested environments (e.g. /web and /pos), so we get the last item
|
||||
// from the handler registry and assume it is the default one, which handles all "not already
|
||||
// handled" errors, like tracebacks.
|
||||
const errorHandlerRegistry = registry.category("error_handlers");
|
||||
const [defaultHandlerName, defaultHandler] = errorHandlerRegistry.getEntries().at(-1);
|
||||
const testDefaultHandler = (env, uncaughtError, originalError) => {
|
||||
onUncaughtErrorInTest(originalError);
|
||||
return defaultHandler(env, uncaughtError, originalError);
|
||||
};
|
||||
errorHandlerRegistry.add(defaultHandlerName, testDefaultHandler, {
|
||||
sequence: Number.POSITIVE_INFINITY,
|
||||
force: true,
|
||||
});
|
||||
}
|
||||
|
|
@ -1,192 +0,0 @@
|
|||
odoo.define('web.qweb_tests', function (require) {
|
||||
"use strict";
|
||||
|
||||
const {Markup} = require('web.utils');
|
||||
|
||||
var qwebPath = '/web/static/lib/qweb/';
|
||||
const {hushConsole} = require('@web/../tests/helpers/utils');
|
||||
|
||||
function trim(s) {
|
||||
return s.replace(/(^\s+|\s+$)/g, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Promise-based wrapper for QWeb2.Engine#add_template
|
||||
*
|
||||
* The base version is callbacks-based which is a bit shit, and it also has
|
||||
* variable asynchronicity: it'll be async if passed a URL but not if passed
|
||||
* either a Document or an XML string.
|
||||
*
|
||||
* Either way this converts the `(error, doc)` callback to a `Promise<Document>`
|
||||
* as in it literally returns the parsed DOM Document.
|
||||
*
|
||||
* @param qweb the qweb instance to load the template into
|
||||
* @param {String|Document} template qweb template (Document, template file, or template URL)
|
||||
*/
|
||||
function add_template(qweb, template) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
qweb.add_template(template, (error, doc) => error ? reject(error) : resolve(doc));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the template file, and executes all the test template in a
|
||||
* qunit module $title
|
||||
*
|
||||
* @param assert QUnit assertion module
|
||||
* @param {String|Document} templateFile template container to load
|
||||
* @param {Object} [context] additional rendering context
|
||||
*/
|
||||
async function loadTest(assert, templateFile, context) {
|
||||
const qweb = new window.QWeb2.Engine();
|
||||
const doc = await add_template(qweb, qwebPath + templateFile);
|
||||
|
||||
assert.expect(doc.querySelectorAll('result').length);
|
||||
|
||||
const templates = qweb.templates;
|
||||
for (const template in templates) {
|
||||
if (!templates.hasOwnProperty(template)) {
|
||||
continue;
|
||||
}
|
||||
// ignore templates whose name starts with _, they're
|
||||
// helpers/internal
|
||||
if (/^_/.test(template)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const results = doc.querySelector(`result#${template}`).textContent.replace(/\r/g, '');
|
||||
const params = doc.querySelector(`params#${template}`) || {textContent: 'null'};
|
||||
const args = {...JSON.parse(params.textContent), ...context};
|
||||
|
||||
try {
|
||||
assert.equal(trim(qweb.render(template, args)), trim(results), template);
|
||||
} catch (error) {
|
||||
assert.notOk(error.stack || error, `Rendering error for ${template} (in ${templateFile} with context ${JSON.stringify(args)}).`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const table = [
|
||||
{name: 'Output', file: 'qweb-test-output.xml'},
|
||||
{name: 'Context-setting', file: 'qweb-test-set.xml'},
|
||||
{name: 'Conditionals', file: 'qweb-test-conditionals.xml'},
|
||||
{name: 'Attributes manipulation', file: 'qweb-test-attributes.xml'},
|
||||
{name: 'Templates calling (to the faraway pages)', file: 'qweb-test-call.xml', context: {True: true}},
|
||||
{name: 'Foreach', file: 'qweb-test-foreach.xml'},
|
||||
{
|
||||
name: 'Global', file: 'qweb-test-global.xml',
|
||||
// test uses python syntax
|
||||
context: {bool: (v) => !!v ? 'True' : 'False'},
|
||||
fixture: {
|
||||
before() {
|
||||
this.WORD_REPLACEMENT = window.QWeb2.WORD_REPLACEMENT;
|
||||
window.QWeb2.WORD_REPLACEMENT = _.extend(
|
||||
{not: '!', None: 'undefined'},
|
||||
this.WORD_REPLACEMENT
|
||||
)
|
||||
},
|
||||
after() {
|
||||
window.QWeb2.WORD_REPLACEMENT = this.WORD_REPLACEMENT;
|
||||
}
|
||||
}
|
||||
},
|
||||
{name: 'Template Inheritance', file: 'qweb-test-extend.xml'},
|
||||
];
|
||||
QUnit.module('QWeb', {
|
||||
beforeEach() {
|
||||
this.oldConsole = window.console;
|
||||
window.console = hushConsole;
|
||||
},
|
||||
afterEach() {window.console = this.oldConsole;}
|
||||
}, () => {
|
||||
for(const {name, file, context, fixture} of table) {
|
||||
QUnit.test(name, async assert => {
|
||||
// fake expect to avoid qunit being a pain in the ass, loadTest will
|
||||
// update it
|
||||
assert.expect(1);
|
||||
if (fixture && 'before' in fixture) { await fixture.before(); }
|
||||
try {
|
||||
await loadTest(assert, file, context);
|
||||
} finally {
|
||||
if (fixture && 'after' in fixture) { await fixture.after(); }
|
||||
}
|
||||
});
|
||||
}
|
||||
QUnit.test('escape', assert => {
|
||||
// not strictly about qweb...
|
||||
assert.expect(8);
|
||||
assert.equal(_.escape('a'), 'a');
|
||||
assert.equal(_.escape('<a>'), '<a>');
|
||||
assert.equal(_.escape({[_.escapeMethod]() { return 'a'; }}), 'a');
|
||||
assert.equal(_.escape({[_.escapeMethod]() { return '<a>'; }}), '<a>');
|
||||
assert.equal(_.escape(Markup('a')), 'a');
|
||||
assert.equal(_.escape(Markup('<a>')), '<a>');
|
||||
assert.equal(_.escape(Markup`a`), 'a');
|
||||
assert.equal(_.escape(Markup`<a>`), '<a>');
|
||||
});
|
||||
QUnit.module('t-out', {}, () => {
|
||||
QUnit.test("basics", async assert => {
|
||||
assert.expect(5);
|
||||
const qweb = new QWeb2.Engine;
|
||||
await add_template(qweb, `<templates>
|
||||
<t t-name="t-out"><p><t t-out="value"/></p></t>
|
||||
</templates>`);
|
||||
|
||||
assert.equal(
|
||||
qweb.render('t-out', {value: '<i>test</i>'}),
|
||||
'<p><i>test</i></p>',
|
||||
"regular t-out should just escape the contents"
|
||||
);
|
||||
assert.equal(
|
||||
qweb.render('t-out', {value: Markup('<i>test</i>')}),
|
||||
'<p><i>test</i></p>',
|
||||
"Markup contents should not be escaped"
|
||||
);
|
||||
assert.equal(
|
||||
qweb.render('t-out', {value: Markup`<i>test ${1}</i>`}),
|
||||
'<p><i>test 1</i></p>',
|
||||
"markup template string should not be escaped"
|
||||
);
|
||||
const teststr = '<i>test</i>';
|
||||
assert.equal(
|
||||
qweb.render('t-out', {value: Markup`<b>${teststr}</b>`}),
|
||||
'<p><b><i>test</i></b></p>',
|
||||
"the markup template should not be escaped but what it uses should be"
|
||||
);
|
||||
const testMarkup = Markup(teststr);
|
||||
assert.equal(
|
||||
qweb.render('t-out', {value: Markup`<b>${testMarkup}</b>`}),
|
||||
'<p><b><i>test</i></b></p>',
|
||||
"markupception"
|
||||
);
|
||||
});
|
||||
QUnit.test("Set", async assert => {
|
||||
assert.expect(4);
|
||||
const qweb = new QWeb2.Engine;
|
||||
await add_template(qweb, `<templates>
|
||||
<t t-name="litval">
|
||||
<t t-set="x" t-value="'<a/>'"/>
|
||||
<x><t t-out="x"/></x>
|
||||
</t>
|
||||
<t t-name="body">
|
||||
<t t-set="x"><a/></t>
|
||||
<x><t t-out="x"/></x>
|
||||
</t>
|
||||
<t t-name="value">
|
||||
<t t-set="x" t-value="val"/>
|
||||
<x><t t-out="x"/></x>
|
||||
</t>
|
||||
<t t-name="bodyout">
|
||||
<t t-set="x"><t t-out="val"/></t>
|
||||
<x><t t-out="x"/></x>
|
||||
</t>
|
||||
</templates>`);
|
||||
|
||||
assert.equal(trim(qweb.render('litval')), '<x><a/></x>');
|
||||
assert.equal(trim(qweb.render('body')), '<x><a></a></x>');
|
||||
assert.equal(trim(qweb.render('value', {val: '<a/>'})), '<x><a/></x>');
|
||||
assert.equal(trim(qweb.render('bodyout', {val: '<a/>'})), '<x><a/></x>');
|
||||
});
|
||||
})
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,328 @@
|
|||
/** @odoo-module alias=@web/../tests/search/helpers default=false */
|
||||
|
||||
import { Component, xml } from "@odoo/owl";
|
||||
import { makeTestEnv } from "@web/../tests/helpers/mock_env";
|
||||
import {
|
||||
click,
|
||||
editInput,
|
||||
getFixture,
|
||||
mount,
|
||||
mouseEnter,
|
||||
triggerEvent,
|
||||
triggerEvents,
|
||||
} from "@web/../tests/helpers/utils";
|
||||
import { commandService } from "@web/core/commands/command_service";
|
||||
import { dialogService } from "@web/core/dialog/dialog_service";
|
||||
import { fieldService } from "@web/core/field_service";
|
||||
import { treeProcessorService } from "@web/core/tree_editor/tree_processor";
|
||||
import { hotkeyService } from "@web/core/hotkeys/hotkey_service";
|
||||
import { notificationService } from "@web/core/notifications/notification_service";
|
||||
import { ormService } from "@web/core/orm_service";
|
||||
import { popoverService } from "@web/core/popover/popover_service";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { CustomFavoriteItem } from "@web/search/custom_favorite_item/custom_favorite_item";
|
||||
import { WithSearch } from "@web/search/with_search/with_search";
|
||||
import { getDefaultConfig } from "@web/views/view";
|
||||
import { viewService } from "@web/views/view_service";
|
||||
import { actionService } from "@web/webclient/actions/action_service";
|
||||
import { MainComponentsContainer } from "@web/core/main_components_container";
|
||||
import { nameService } from "@web/core/name_service";
|
||||
import { datetimePickerService } from "@web/core/datetime/datetimepicker_service";
|
||||
|
||||
const serviceRegistry = registry.category("services");
|
||||
const favoriteMenuRegistry = registry.category("favoriteMenu");
|
||||
|
||||
export function setupControlPanelServiceRegistry() {
|
||||
serviceRegistry.add("action", actionService);
|
||||
serviceRegistry.add("dialog", dialogService);
|
||||
serviceRegistry.add("field", fieldService);
|
||||
serviceRegistry.add("tree_processor", treeProcessorService);
|
||||
serviceRegistry.add("hotkey", hotkeyService);
|
||||
serviceRegistry.add("name", nameService);
|
||||
serviceRegistry.add("notification", notificationService);
|
||||
serviceRegistry.add("orm", ormService);
|
||||
serviceRegistry.add("popover", popoverService);
|
||||
serviceRegistry.add("view", viewService);
|
||||
serviceRegistry.add("command", commandService);
|
||||
serviceRegistry.add("datetime_picker", datetimePickerService);
|
||||
}
|
||||
|
||||
export function setupControlPanelFavoriteMenuRegistry() {
|
||||
favoriteMenuRegistry.add(
|
||||
"custom-favorite-item",
|
||||
{ Component: CustomFavoriteItem, groupNumber: 3 },
|
||||
{ sequence: 0 }
|
||||
);
|
||||
}
|
||||
|
||||
export async function makeWithSearch(params) {
|
||||
const props = { ...params };
|
||||
|
||||
const serverData = props.serverData || undefined;
|
||||
const mockRPC = props.mockRPC || undefined;
|
||||
const config = {
|
||||
...getDefaultConfig(),
|
||||
...props.config,
|
||||
};
|
||||
|
||||
delete props.serverData;
|
||||
delete props.mockRPC;
|
||||
delete props.config;
|
||||
const componentProps = props.componentProps || {};
|
||||
delete props.componentProps;
|
||||
delete props.Component;
|
||||
|
||||
class Parent extends Component {
|
||||
static template = xml`
|
||||
<WithSearch t-props="withSearchProps" t-slot-scope="search">
|
||||
<Component t-props="getProps(search)"/>
|
||||
</WithSearch>
|
||||
<MainComponentsContainer />
|
||||
`;
|
||||
static components = { Component: params.Component, WithSearch, MainComponentsContainer };
|
||||
static props = ["*"];
|
||||
setup() {
|
||||
this.withSearchProps = props;
|
||||
}
|
||||
|
||||
getProps(search) {
|
||||
const props = Object.assign({}, componentProps, {
|
||||
context: search.context,
|
||||
domain: search.domain,
|
||||
groupBy: search.groupBy,
|
||||
orderBy: search.orderBy,
|
||||
comparison: search.comparison,
|
||||
display: Object.assign({}, search.display, componentProps.display),
|
||||
});
|
||||
return filterPropsForComponent(params.Component, props);
|
||||
}
|
||||
}
|
||||
|
||||
const env = await makeTestEnv({ serverData, mockRPC });
|
||||
const searchEnv = Object.assign(Object.create(env), { config });
|
||||
const parent = await mount(Parent, getFixture(), { env: searchEnv, props });
|
||||
const parentNode = parent.__owl__;
|
||||
const withSearchNode = getUniqueChild(parentNode);
|
||||
const componentNode = getUniqueChild(withSearchNode);
|
||||
const component = componentNode.component;
|
||||
return component;
|
||||
}
|
||||
|
||||
/** This function is aim to be used only in the tests.
|
||||
* It will filter the props that are needed by the Component.
|
||||
* This is to avoid errors of props validation. This occurs for example, on ControlPanel tests.
|
||||
* In production, View use WithSearch for the Controllers, and the Layout send only the props that
|
||||
* need to the ControlPanel.
|
||||
*
|
||||
* @param {Component} Component
|
||||
* @param {Object} props
|
||||
* @returns {Object} filtered props
|
||||
*/
|
||||
function filterPropsForComponent(Component, props) {
|
||||
// This if, can be removed once all the Components have the props defined
|
||||
if (Component.props) {
|
||||
let componentKeys = null;
|
||||
if (Component.props instanceof Array) {
|
||||
componentKeys = Component.props.map((x) => x.replace("?", ""));
|
||||
} else {
|
||||
componentKeys = Object.keys(Component.props);
|
||||
}
|
||||
if (componentKeys.includes("*")) {
|
||||
return props;
|
||||
} else {
|
||||
return Object.keys(props)
|
||||
.filter((k) => componentKeys.includes(k))
|
||||
.reduce((o, k) => {
|
||||
o[k] = props[k];
|
||||
return o;
|
||||
}, {});
|
||||
}
|
||||
} else {
|
||||
return props;
|
||||
}
|
||||
}
|
||||
|
||||
function getUniqueChild(node) {
|
||||
return Object.values(node.children)[0];
|
||||
}
|
||||
|
||||
function getNode(target) {
|
||||
return target instanceof Component ? target.el : target;
|
||||
}
|
||||
|
||||
export function findItem(target, selector, finder = 0) {
|
||||
const el = getNode(target);
|
||||
const elems = [...el.querySelectorAll(selector)];
|
||||
if (Number.isInteger(finder)) {
|
||||
return elems[finder];
|
||||
}
|
||||
return elems.find((el) => el.innerText.trim().toLowerCase() === String(finder).toLowerCase());
|
||||
}
|
||||
|
||||
/** Menu (generic) */
|
||||
|
||||
export async function toggleMenu(el, menuFinder) {
|
||||
const menu = findItem(el, `button.dropdown-toggle`, menuFinder);
|
||||
await click(menu);
|
||||
}
|
||||
|
||||
export async function toggleMenuItem(el, itemFinder) {
|
||||
const item = findItem(el, `.o_menu_item`, itemFinder);
|
||||
if (item.classList.contains("dropdown-toggle")) {
|
||||
await mouseEnter(item);
|
||||
} else {
|
||||
await click(item);
|
||||
}
|
||||
}
|
||||
|
||||
export async function toggleMenuItemOption(el, itemFinder, optionFinder) {
|
||||
const item = findItem(el, `.o_menu_item`, itemFinder);
|
||||
const option = findItem(item.parentNode, ".o_item_option", optionFinder);
|
||||
if (option.classList.contains("dropdown-toggle")) {
|
||||
await mouseEnter(option);
|
||||
} else {
|
||||
await click(option);
|
||||
}
|
||||
}
|
||||
|
||||
export function isItemSelected(el, itemFinder) {
|
||||
const item = findItem(el, `.o_menu_item`, itemFinder);
|
||||
return item.classList.contains("selected");
|
||||
}
|
||||
|
||||
export function isOptionSelected(el, itemFinder, optionFinder) {
|
||||
const item = findItem(el, `.o_menu_item`, itemFinder);
|
||||
const option = findItem(item.parentNode, ".o_item_option", optionFinder);
|
||||
return option.classList.contains("selected");
|
||||
}
|
||||
|
||||
export function getMenuItemTexts(target) {
|
||||
const el = getNode(target);
|
||||
return [...el.querySelectorAll(`.dropdown-menu .o_menu_item`)].map((e) => e.innerText.trim());
|
||||
}
|
||||
|
||||
export function getVisibleButtons(el) {
|
||||
return [
|
||||
...$(el).find(
|
||||
[
|
||||
"div.o_control_panel_breadcrumbs button:visible", // button in the breadcrumbs
|
||||
"div.o_control_panel_actions button:visible", // buttons for list selection
|
||||
].join(",")
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
/** Filter menu */
|
||||
|
||||
export async function openAddCustomFilterDialog(el) {
|
||||
await click(findItem(el, `.o_filter_menu .o_menu_item.o_add_custom_filter`));
|
||||
}
|
||||
|
||||
/** Group by menu */
|
||||
|
||||
export async function selectGroup(el, fieldName) {
|
||||
el.querySelector(".o_add_custom_group_menu").value = fieldName;
|
||||
await triggerEvent(el, ".o_add_custom_group_menu", "change");
|
||||
}
|
||||
|
||||
export async function groupByMenu(el, fieldName) {
|
||||
await toggleSearchBarMenu(el);
|
||||
await selectGroup(el, fieldName);
|
||||
}
|
||||
|
||||
/** Favorite menu */
|
||||
|
||||
export async function deleteFavorite(el, favoriteFinder) {
|
||||
const favorite = findItem(el, `.o_favorite_menu .o_menu_item`, favoriteFinder);
|
||||
await click(findItem(favorite, "i.fa-trash-o"));
|
||||
}
|
||||
|
||||
export async function toggleSaveFavorite(el) {
|
||||
await click(findItem(el, `.o_favorite_menu .o_add_favorite`));
|
||||
}
|
||||
|
||||
export async function editFavoriteName(el, name) {
|
||||
const input = findItem(
|
||||
el,
|
||||
`.o_favorite_menu .o_add_favorite + .o_accordion_values input[type="text"]`
|
||||
);
|
||||
input.value = name;
|
||||
await triggerEvents(input, null, ["input", "change"]);
|
||||
}
|
||||
|
||||
export async function saveFavorite(el) {
|
||||
await click(findItem(el, `.o_favorite_menu .o_add_favorite + .o_accordion_values button`));
|
||||
}
|
||||
|
||||
/** Search bar */
|
||||
|
||||
export function getFacetTexts(target) {
|
||||
const el = getNode(target);
|
||||
return [...el.querySelectorAll(`div.o_searchview_facet`)].map((facet) =>
|
||||
facet.innerText.trim()
|
||||
);
|
||||
}
|
||||
|
||||
export async function removeFacet(el, facetFinder = 0) {
|
||||
const facet = findItem(el, `div.o_searchview_facet`, facetFinder);
|
||||
await click(facet.querySelector(".o_facet_remove"));
|
||||
}
|
||||
|
||||
export async function editSearch(el, value) {
|
||||
const input = findItem(el, `.o_searchview input`);
|
||||
input.value = value;
|
||||
await triggerEvent(input, null, "input");
|
||||
}
|
||||
|
||||
export async function validateSearch(el) {
|
||||
const input = findItem(el, `.o_searchview input`);
|
||||
await triggerEvent(input, null, "keydown", { key: "Enter" });
|
||||
}
|
||||
|
||||
/** Switch View */
|
||||
|
||||
export async function switchView(el, viewType) {
|
||||
await click(findItem(el, `button.o_switch_view.o_${viewType}`));
|
||||
}
|
||||
|
||||
/** Pager */
|
||||
|
||||
export function getPagerValue(el) {
|
||||
const valueEl = findItem(el, ".o_pager .o_pager_value");
|
||||
return valueEl.innerText.trim().split("-").map(Number);
|
||||
}
|
||||
|
||||
export function getPagerLimit(el) {
|
||||
const limitEl = findItem(el, ".o_pager .o_pager_limit");
|
||||
return Number(limitEl.innerText.trim());
|
||||
}
|
||||
|
||||
export async function pagerNext(el) {
|
||||
await click(findItem(el, ".o_pager button.o_pager_next"));
|
||||
}
|
||||
|
||||
export async function pagerPrevious(el) {
|
||||
await click(findItem(el, ".o_pager button.o_pager_previous"));
|
||||
}
|
||||
|
||||
export async function editPager(el, value) {
|
||||
await click(findItem(el, ".o_pager .o_pager_value"));
|
||||
await editInput(getNode(el), ".o_pager .o_pager_value.o_input", value);
|
||||
}
|
||||
|
||||
/////////////////////////////////////
|
||||
// Action Menu
|
||||
/////////////////////////////////////
|
||||
/**
|
||||
* @param {EventTarget} el
|
||||
* @param {string} [menuFinder="Action"]
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export async function toggleActionMenu(el) {
|
||||
await click(el.querySelector(".o_cp_action_menus .dropdown-toggle"));
|
||||
}
|
||||
|
||||
/** SearchBarMenu */
|
||||
export async function toggleSearchBarMenu(el) {
|
||||
await click(findItem(el, `.o_searchview_dropdown_toggler`));
|
||||
}
|
||||
|
|
@ -1,242 +0,0 @@
|
|||
odoo.define('web.data_manager_tests', function (require) {
|
||||
"use strict";
|
||||
|
||||
const config = require('web.config');
|
||||
const DataManager = require('web.DataManager');
|
||||
const MockServer = require('web.MockServer');
|
||||
const rpc = require('web.rpc');
|
||||
const testUtils = require('web.test_utils');
|
||||
|
||||
/**
|
||||
* Create a simple data manager with mocked functions:
|
||||
* - mockRPC -> rpc.query
|
||||
* - isDebug -> config.isDebug
|
||||
* @param {Object} params
|
||||
* @param {Object} params.archs
|
||||
* @param {Object} params.data
|
||||
* @param {Function} params.isDebug
|
||||
* @param {Function} params.mockRPC
|
||||
* @returns {DataManager}
|
||||
*/
|
||||
function createDataManager({ archs, data, isDebug, mockRPC }) {
|
||||
const dataManager = new DataManager();
|
||||
const server = new MockServer(data, { archs });
|
||||
|
||||
const serverMethods = {
|
||||
async get_views({ kwargs, model }) {
|
||||
const models = {};
|
||||
models[model] = server.fieldsGet(model);
|
||||
const views = {};
|
||||
for (const [viewId, viewType] of kwargs.views) {
|
||||
const arch = archs[[model, viewId || false, viewType].join()];
|
||||
views[viewType] = server.getView({ arch, model, viewId });
|
||||
for (const modelName of views[viewType].models) {
|
||||
models[modelName] = server.fieldsGet(modelName);
|
||||
}
|
||||
}
|
||||
const result = { models, views };
|
||||
if (kwargs.options.load_filters && views.search) {
|
||||
views.search.filters = data['ir.filters'].records.filter(r => r.model_id === model);
|
||||
}
|
||||
return result;
|
||||
},
|
||||
async get_filters({ args, model }) {
|
||||
return data[model].records.filter(r => r.model_id === args[0]);
|
||||
},
|
||||
async create_or_replace({ args }) {
|
||||
const id = data['ir.filters'].records.reduce((i, r) => Math.max(i, r.id), 0) + 1;
|
||||
const filter = Object.assign(args[0], { id });
|
||||
data['ir.filters'].records.push(filter);
|
||||
return id;
|
||||
},
|
||||
async unlink({ args }) {
|
||||
data['ir.filters'].records = data['ir.filters'].records.filter(
|
||||
r => r.id !== args[0]
|
||||
);
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
testUtils.mock.patch(rpc, {
|
||||
async query({ method }) {
|
||||
this._super = serverMethods[method].bind(this, ...arguments);
|
||||
return mockRPC.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
testUtils.mock.patch(config, { isDebug });
|
||||
|
||||
return dataManager;
|
||||
}
|
||||
|
||||
QUnit.module("Services", {
|
||||
beforeEach() {
|
||||
this.archs = {
|
||||
'oui,10,kanban': '<kanban/>',
|
||||
'oui,20,search': '<search/>',
|
||||
};
|
||||
this.data = {
|
||||
oui: { fields: {}, records: [] },
|
||||
'ir.filters': {
|
||||
fields: {
|
||||
context: { type: "Text", string: "Context" },
|
||||
domain: { type: "Text", string: "Domain" },
|
||||
model_id: { type: "Selection", string: "Model" },
|
||||
name: { type: "Char", string: "Name" },
|
||||
},
|
||||
records: [{
|
||||
id: 2,
|
||||
context: '{}',
|
||||
domain: '[]',
|
||||
model_id: 'oui',
|
||||
name: "Favorite",
|
||||
}]
|
||||
}
|
||||
};
|
||||
this.loadViewsParams = {
|
||||
model: "oui",
|
||||
context: {},
|
||||
views_descr: [
|
||||
[10, 'kanban'],
|
||||
[20, 'search'],
|
||||
],
|
||||
};
|
||||
},
|
||||
afterEach() {
|
||||
testUtils.mock.unpatch(rpc);
|
||||
testUtils.mock.unpatch(config);
|
||||
},
|
||||
}, function () {
|
||||
|
||||
QUnit.module("Data manager");
|
||||
|
||||
QUnit.test("Load views with filters (non-debug mode)", async function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
const dataManager = createDataManager({
|
||||
archs: this.archs,
|
||||
data: this.data,
|
||||
isDebug() {
|
||||
return false;
|
||||
},
|
||||
async mockRPC({ method, model }) {
|
||||
assert.step([model, method].join('.'));
|
||||
return this._super(...arguments);
|
||||
},
|
||||
});
|
||||
|
||||
const firstLoad = await dataManager.load_views(this.loadViewsParams, {
|
||||
load_filters: true,
|
||||
});
|
||||
const secondLoad = await dataManager.load_views(this.loadViewsParams, {
|
||||
load_filters: true,
|
||||
});
|
||||
const filters = await dataManager.load_filters({ modelName: 'oui' });
|
||||
|
||||
assert.deepEqual(firstLoad, secondLoad,
|
||||
"query with same params and options should yield the same results");
|
||||
assert.deepEqual(firstLoad.search.favoriteFilters, filters,
|
||||
"load filters should yield the same result as the first load_views' filters");
|
||||
assert.verifySteps(['oui.get_views'],
|
||||
"only load once when not in assets debugging");
|
||||
});
|
||||
|
||||
QUnit.test("Load views with filters (debug mode)", async function (assert) {
|
||||
assert.expect(6);
|
||||
|
||||
const dataManager = createDataManager({
|
||||
archs: this.archs,
|
||||
data: this.data,
|
||||
isDebug() {
|
||||
return true; // assets
|
||||
},
|
||||
async mockRPC({ method, model }) {
|
||||
assert.step([model, method].join('.'));
|
||||
return this._super(...arguments);
|
||||
},
|
||||
});
|
||||
|
||||
const firstLoad = await dataManager.load_views(this.loadViewsParams, {
|
||||
load_filters: true,
|
||||
});
|
||||
const secondLoad = await dataManager.load_views(this.loadViewsParams, {
|
||||
load_filters: true,
|
||||
});
|
||||
const filters = await dataManager.load_filters({ modelName: 'oui' });
|
||||
|
||||
assert.deepEqual(firstLoad, secondLoad,
|
||||
"query with same params and options should yield the same results");
|
||||
assert.deepEqual(firstLoad.search.favoriteFilters, filters,
|
||||
"load filters should yield the same result as the first load_views' filters");
|
||||
assert.verifySteps([
|
||||
'oui.get_views',
|
||||
'oui.get_views',
|
||||
'ir.filters.get_filters',
|
||||
], "reload each time when in assets debugging");
|
||||
});
|
||||
|
||||
QUnit.test("Cache invalidation and filters addition/deletion", async function (assert) {
|
||||
assert.expect(10);
|
||||
|
||||
const dataManager = createDataManager({
|
||||
archs: this.archs,
|
||||
data: this.data,
|
||||
isDebug() {
|
||||
return false; // Cache only works if 'debug !== assets'
|
||||
},
|
||||
async mockRPC({ method, model }) {
|
||||
assert.step([model, method].join('.'));
|
||||
return this._super(...arguments);
|
||||
},
|
||||
});
|
||||
|
||||
// A few unnecessary 'load_filters' are done in this test to assert
|
||||
// that the cache invalidation mechanics are working.
|
||||
let filters;
|
||||
|
||||
const firstLoad = await dataManager.load_views(this.loadViewsParams, {
|
||||
load_filters: true,
|
||||
});
|
||||
// Cache is valid -> should not trigger an RPC
|
||||
filters = await dataManager.load_filters({ modelName: 'oui' });
|
||||
assert.deepEqual(firstLoad.search.favoriteFilters, filters,
|
||||
"load_filters and load_views.search should return the same filters");
|
||||
|
||||
const filterId = await dataManager.create_filter({
|
||||
context: "{}",
|
||||
domain: "[]",
|
||||
model_id: 'oui',
|
||||
name: "Temp",
|
||||
});
|
||||
// Cache is not valid anymore -> triggers a 'get_filters'
|
||||
filters = await dataManager.load_filters({ modelName: 'oui' });
|
||||
// Cache is valid -> should not trigger an RPC
|
||||
filters = await dataManager.load_filters({ modelName: 'oui' });
|
||||
|
||||
assert.strictEqual(filters.length, 2,
|
||||
"A new filter should have been added");
|
||||
assert.ok(filters.find(f => f.id === filterId) === filters[filters.length - 1],
|
||||
"Create filter should return the id of the last created filter");
|
||||
|
||||
await dataManager.delete_filter(filterId);
|
||||
|
||||
// Views cache is valid but filters cache is not -> triggers a 'get_filters'
|
||||
const secondLoad = await dataManager.load_views(this.loadViewsParams, {
|
||||
load_filters: true,
|
||||
});
|
||||
filters = secondLoad.search.favoriteFilters;
|
||||
// Filters cache is once again valid -> no RPC
|
||||
const expectedFilters = await dataManager.load_filters({ modelName: 'oui' });
|
||||
|
||||
assert.deepEqual(filters, expectedFilters,
|
||||
"Filters loaded by the load_views should be equal to the result of a load_filters");
|
||||
|
||||
assert.verifySteps([
|
||||
'oui.get_views',
|
||||
'ir.filters.create_or_replace',
|
||||
'ir.filters.get_filters',
|
||||
'ir.filters.unlink',
|
||||
'ir.filters.get_filters',
|
||||
], "server should have been called only when needed");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,80 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import AbstractAction from "web.AbstractAction";
|
||||
import core from "web.core";
|
||||
import * as LegacyRegistry from "web.Registry";
|
||||
import { registerCleanup } from "../../helpers/cleanup";
|
||||
import { nextTick, patchWithCleanup } from "../../helpers/utils";
|
||||
import { createWebClient, doAction } from "../../webclient/helpers";
|
||||
|
||||
let legacyParams;
|
||||
|
||||
QUnit.module("Service Provider Adapter Notification", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
legacyParams = {
|
||||
serviceRegistry: new LegacyRegistry(),
|
||||
};
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"can display and close a sticky danger notification with a title (legacy)",
|
||||
async function (assert) {
|
||||
assert.expect(8);
|
||||
let notifId;
|
||||
let timeoutCB;
|
||||
patchWithCleanup(browser, {
|
||||
setTimeout: (cb, delay) => {
|
||||
if (!delay) {
|
||||
return; // Timeouts from router service
|
||||
}
|
||||
timeoutCB = cb;
|
||||
assert.step("time: " + delay);
|
||||
return 1;
|
||||
},
|
||||
});
|
||||
const NotifyAction = AbstractAction.extend({
|
||||
on_attach_callback() {
|
||||
notifId = this.call("notification", "notify", {
|
||||
title: "Some title",
|
||||
message: "I'm a danger notification",
|
||||
type: "danger",
|
||||
sticky: true,
|
||||
});
|
||||
},
|
||||
});
|
||||
const CloseAction = AbstractAction.extend({
|
||||
on_attach_callback() {
|
||||
this.call("notification", "close", notifId, false, 3000);
|
||||
},
|
||||
});
|
||||
core.action_registry.add("NotifyTestLeg", NotifyAction);
|
||||
core.action_registry.add("CloseTestLeg", CloseAction);
|
||||
registerCleanup(() => {
|
||||
delete core.action_registry.map.NotifyTestLeg;
|
||||
delete core.action_registry.map.CloseTestLeg;
|
||||
});
|
||||
const webClient = await createWebClient({ legacyParams });
|
||||
await doAction(webClient, "NotifyTestLeg");
|
||||
await nextTick();
|
||||
assert.containsOnce(document.body, ".o_notification");
|
||||
const notif = document.body.querySelector(".o_notification");
|
||||
assert.strictEqual(notif.querySelector(".o_notification_title").textContent, "Some title");
|
||||
assert.strictEqual(
|
||||
notif.querySelector(".o_notification_content").textContent,
|
||||
"I'm a danger notification"
|
||||
);
|
||||
assert.hasClass(notif, "border-danger");
|
||||
|
||||
//Close the notification
|
||||
await doAction(webClient, "CloseTestLeg");
|
||||
await nextTick();
|
||||
assert.containsOnce(document.body, ".o_notification");
|
||||
// simulate end of timeout
|
||||
timeoutCB();
|
||||
await nextTick();
|
||||
assert.containsNone(document.body, ".o_notification");
|
||||
assert.verifySteps(["time: 3000"]);
|
||||
}
|
||||
);
|
||||
});
|
||||
407
odoo-bringout-oca-ocb-web/web/static/tests/legacy/setup.js
Normal file
407
odoo-bringout-oca-ocb-web/web/static/tests/legacy/setup.js
Normal file
|
|
@ -0,0 +1,407 @@
|
|||
/** @odoo-module alias=@web/../tests/setup default=false */
|
||||
|
||||
import { assets } from "@web/core/assets";
|
||||
import { user, _makeUser } from "@web/core/user";
|
||||
import { browser, makeRAMLocalStorage } from "@web/core/browser/browser";
|
||||
import { patchTimeZone, patchWithCleanup } from "@web/../tests/helpers/utils";
|
||||
import { memoize } from "@web/core/utils/functions";
|
||||
import { registerCleanup } from "./helpers/cleanup";
|
||||
import { prepareRegistriesWithCleanup } from "./helpers/mock_env";
|
||||
import { session as sessionInfo } from "@web/session";
|
||||
import { config as transitionConfig } from "@web/core/transition";
|
||||
import { loadLanguages } from "@web/core/l10n/translation";
|
||||
|
||||
transitionConfig.disabled = true;
|
||||
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { App, EventBus, whenReady } from "@odoo/owl";
|
||||
import { currencies } from "@web/core/currency";
|
||||
import { cookie } from "@web/core/browser/cookie";
|
||||
import { router } from "@web/core/browser/router";
|
||||
import { registerTemplateProcessor } from "@web/core/templates";
|
||||
|
||||
function forceLocaleAndTimezoneWithCleanup() {
|
||||
const originalLocale = luxon.Settings.defaultLocale;
|
||||
luxon.Settings.defaultLocale = "en";
|
||||
registerCleanup(() => {
|
||||
luxon.Settings.defaultLocale = originalLocale;
|
||||
});
|
||||
patchTimeZone(60);
|
||||
}
|
||||
|
||||
function makeMockLocation() {
|
||||
return Object.assign(document.createElement("a"), {
|
||||
href: window.location.origin + "/odoo",
|
||||
assign(url) {
|
||||
this.href = url;
|
||||
},
|
||||
reload() {},
|
||||
});
|
||||
}
|
||||
|
||||
function patchOwlApp() {
|
||||
patchWithCleanup(App.prototype, {
|
||||
destroy() {
|
||||
if (!this.destroyed) {
|
||||
super.destroy(...arguments);
|
||||
this.destroyed = true;
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function patchCookie() {
|
||||
const cookieJar = {};
|
||||
|
||||
patchWithCleanup(cookie, {
|
||||
get _cookieMonster() {
|
||||
return Object.entries(cookieJar)
|
||||
.filter(([, value]) => value !== "kill")
|
||||
.map((cookie) => cookie.join("="))
|
||||
.join("; ");
|
||||
},
|
||||
set _cookieMonster(value) {
|
||||
const cookies = value.split("; ");
|
||||
for (const cookie of cookies) {
|
||||
const [key, value] = cookie.split(/=(.*)/);
|
||||
if (!["path", "max-age"].includes(key)) {
|
||||
cookieJar[key] = value;
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function patchBrowserWithCleanup() {
|
||||
const originalAddEventListener = browser.addEventListener;
|
||||
const originalRemoveEventListener = browser.removeEventListener;
|
||||
const originalSetTimeout = browser.setTimeout;
|
||||
const originalClearTimeout = browser.clearTimeout;
|
||||
const originalSetInterval = browser.setInterval;
|
||||
const originalClearInterval = browser.clearInterval;
|
||||
|
||||
let nextAnimationFrameHandle = 1;
|
||||
const animationFrameHandles = new Set();
|
||||
const mockLocation = makeMockLocation();
|
||||
let historyStack = [[null, mockLocation.href]];
|
||||
let currentHistoryStack = 0;
|
||||
patchWithCleanup(browser, {
|
||||
// patch addEventListner to automatically remove listeners bound (via
|
||||
// browser.addEventListener) during a test (e.g. during the deployment of a service)
|
||||
addEventListener() {
|
||||
originalAddEventListener(...arguments);
|
||||
registerCleanup(() => {
|
||||
originalRemoveEventListener(...arguments);
|
||||
});
|
||||
},
|
||||
// patch setTimeout to automatically remove timeouts bound (via
|
||||
// browser.setTimeout) during a test (e.g. during the deployment of a service)
|
||||
setTimeout() {
|
||||
const timeout = originalSetTimeout(...arguments);
|
||||
registerCleanup(() => {
|
||||
originalClearTimeout(timeout);
|
||||
});
|
||||
return timeout;
|
||||
},
|
||||
// patch setInterval to automatically remove callbacks registered (via
|
||||
// browser.setInterval) during a test (e.g. during the deployment of a service)
|
||||
setInterval() {
|
||||
const interval = originalSetInterval(...arguments);
|
||||
registerCleanup(() => {
|
||||
originalClearInterval(interval);
|
||||
});
|
||||
return interval;
|
||||
},
|
||||
// patch BeforeInstallPromptEvent to prevent the pwa service to return an uncontrolled
|
||||
// canPromptToInstall value depending the browser settings (we ensure the value is always falsy)
|
||||
BeforeInstallPromptEvent: undefined,
|
||||
navigator: {
|
||||
mediaDevices: browser.navigator.mediaDevices,
|
||||
permissions: browser.navigator.permissions,
|
||||
userAgent: browser.navigator.userAgent.replace(/\([^)]*\)/, "(X11; Linux x86_64)"),
|
||||
sendBeacon: () => {
|
||||
throw new Error("sendBeacon called in test but not mocked");
|
||||
},
|
||||
},
|
||||
// in tests, we never want to interact with the real url or reload the page
|
||||
location: mockLocation,
|
||||
history: {
|
||||
pushState(state, title, url) {
|
||||
historyStack = historyStack.slice(0, currentHistoryStack + 1);
|
||||
historyStack.push([state, url]);
|
||||
currentHistoryStack++;
|
||||
mockLocation.assign(url);
|
||||
},
|
||||
replaceState(state, title, url) {
|
||||
historyStack[currentHistoryStack] = [state, url];
|
||||
mockLocation.assign(url);
|
||||
},
|
||||
back() {
|
||||
currentHistoryStack--;
|
||||
const [state, url] = historyStack[currentHistoryStack];
|
||||
if (!url) {
|
||||
throw new Error("there is no history");
|
||||
}
|
||||
mockLocation.assign(url);
|
||||
window.dispatchEvent(new PopStateEvent("popstate", { state }));
|
||||
},
|
||||
forward() {
|
||||
currentHistoryStack++;
|
||||
const [state, url] = historyStack[currentHistoryStack];
|
||||
if (!url) {
|
||||
throw new Error("No more history");
|
||||
}
|
||||
mockLocation.assign(url);
|
||||
window.dispatchEvent(new PopStateEvent("popstate", { state }));
|
||||
},
|
||||
get length() {
|
||||
return historyStack.length;
|
||||
},
|
||||
},
|
||||
// in tests, we never want to interact with the real local/session storages.
|
||||
localStorage: makeRAMLocalStorage(),
|
||||
sessionStorage: makeRAMLocalStorage(),
|
||||
// Don't want original animation frames in tests
|
||||
requestAnimationFrame: (fn) => {
|
||||
const handle = nextAnimationFrameHandle++;
|
||||
animationFrameHandles.add(handle);
|
||||
|
||||
Promise.resolve().then(() => {
|
||||
if (animationFrameHandles.has(handle)) {
|
||||
fn(16);
|
||||
}
|
||||
});
|
||||
|
||||
return handle;
|
||||
},
|
||||
cancelAnimationFrame: (handle) => {
|
||||
animationFrameHandles.delete(handle);
|
||||
},
|
||||
// BroadcastChannels need to be closed to be garbage collected
|
||||
BroadcastChannel: class SelfClosingBroadcastChannel extends BroadcastChannel {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
registerCleanup(() => this.close());
|
||||
}
|
||||
},
|
||||
// XHR: we don't want tests to do real RPCs
|
||||
XMLHttpRequest: class MockXHR {
|
||||
constructor() {
|
||||
throw new Error("XHR not patched in a test. Consider using patchRPCWithCleanup.");
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function patchBodyAddEventListener() {
|
||||
// In some cases, e.g. tooltip service, event handlers are registered on document.body and not
|
||||
// browser, because the events we listen to aren't triggered on window. We want to clear those
|
||||
// handlers as well after each test.
|
||||
const originalBodyAddEventListener = document.body.addEventListener;
|
||||
const originalBodyRemoveEventListener = document.body.removeEventListener;
|
||||
document.body.addEventListener = function () {
|
||||
originalBodyAddEventListener.call(this, ...arguments);
|
||||
registerCleanup(() => {
|
||||
originalBodyRemoveEventListener.call(this, ...arguments);
|
||||
});
|
||||
};
|
||||
registerCleanup(() => {
|
||||
document.body.addEventListener = originalBodyAddEventListener;
|
||||
});
|
||||
}
|
||||
|
||||
function patchOdoo() {
|
||||
patchWithCleanup(odoo, {
|
||||
debug: "",
|
||||
info: {
|
||||
db: sessionInfo.db,
|
||||
server_version: sessionInfo.server_version,
|
||||
server_version_info: sessionInfo.server_version_info,
|
||||
isEnterprise: sessionInfo.server_version_info.slice(-1)[0] === "e",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function cleanLoadedLanguages() {
|
||||
registerCleanup(() => {
|
||||
loadLanguages.installedLanguages = null;
|
||||
});
|
||||
}
|
||||
|
||||
function patchSessionInfo() {
|
||||
patchWithCleanup(sessionInfo, {
|
||||
qweb: "owl",
|
||||
// Commit: 3e847fc8f499c96b8f2d072ab19f35e105fd7749
|
||||
// to see what user_companies is
|
||||
user_companies: {
|
||||
allowed_companies: { 1: { id: 1, name: "Hermit" } },
|
||||
current_company: 1,
|
||||
},
|
||||
user_context: {
|
||||
lang: "en",
|
||||
tz: "taht",
|
||||
},
|
||||
db: "test",
|
||||
registry_hash: "05500d71e084497829aa807e3caa2e7e9782ff702c15b2f57f87f2d64d049bd0",
|
||||
is_admin: true,
|
||||
is_system: true,
|
||||
username: "thewise@odoo.com",
|
||||
name: "Mitchell",
|
||||
partner_id: 7,
|
||||
uid: 7,
|
||||
server_version: "1.0",
|
||||
server_version_info: [1, 0, 0, "final", 0, ""],
|
||||
});
|
||||
const mockedUser = _makeUser(sessionInfo);
|
||||
patchWithCleanup(user, mockedUser);
|
||||
patchWithCleanup(user, { hasGroup: () => Promise.resolve(false) });
|
||||
patchWithCleanup(currencies, {
|
||||
1: { name: "USD", digits: [69, 2], position: "before", symbol: "$" },
|
||||
2: { name: "EUR", digits: [69, 2], position: "after", symbol: "€" },
|
||||
});
|
||||
}
|
||||
|
||||
function replaceAttr(attrName, prefix, element) {
|
||||
const attrKey = `${prefix}${attrName}`;
|
||||
const attrValue = element.getAttribute(attrKey);
|
||||
element.removeAttribute(attrKey);
|
||||
element.setAttribute(`${prefix}data-${attrName}`, attrValue);
|
||||
}
|
||||
|
||||
registerTemplateProcessor((template) => {
|
||||
// We remove all the attributes `src` and `alt` from the template and replace them by
|
||||
// data attributes (e.g. `src` to `data-src`, `alt` to `data-alt`).
|
||||
// alt attribute causes issues with scroll tests. Indeed, alt is
|
||||
// displayed between the time we scroll programmatically and the time
|
||||
// we assert for the scroll position. The src attribute is removed
|
||||
// as well to make sure images won't trigger a GET request on the
|
||||
// server.
|
||||
for (const attrName of ["alt", "src"]) {
|
||||
for (const prefix of ["", "t-att-", "t-attf-"]) {
|
||||
for (const element of template.querySelectorAll(`*[${prefix}${attrName}]`)) {
|
||||
replaceAttr(attrName, prefix, element);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function patchAssets() {
|
||||
const { getBundle, loadJS, loadCSS } = assets;
|
||||
patch(assets, {
|
||||
getBundle: memoize(async function (xmlID) {
|
||||
console.log(
|
||||
"%c[assets] fetch libs from xmlID: " + xmlID,
|
||||
"color: #66e; font-weight: bold;"
|
||||
);
|
||||
return getBundle(xmlID);
|
||||
}),
|
||||
loadJS: memoize(async function (ressource) {
|
||||
if (ressource.match(/\/static(\/\S+\/|\/)libs?/)) {
|
||||
console.log(
|
||||
"%c[assets] fetch (mock) JS ressource: " + ressource,
|
||||
"color: #66e; font-weight: bold;"
|
||||
);
|
||||
return Promise.resolve();
|
||||
}
|
||||
console.log(
|
||||
"%c[assets] fetch JS ressource: " + ressource,
|
||||
"color: #66e; font-weight: bold;"
|
||||
);
|
||||
return loadJS(ressource);
|
||||
}),
|
||||
loadCSS: memoize(async function (ressource) {
|
||||
if (ressource.match(/\/static(\/\S+\/|\/)libs?/)) {
|
||||
console.log(
|
||||
"%c[assets] fetch (mock) CSS ressource: " + ressource,
|
||||
"color: #66e; font-weight: bold;"
|
||||
);
|
||||
return Promise.resolve();
|
||||
}
|
||||
console.log(
|
||||
"%c[assets] fetch CSS ressource: " + ressource,
|
||||
"color: #66e; font-weight: bold;"
|
||||
);
|
||||
return loadCSS(ressource);
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
function patchEventBus() {
|
||||
patchWithCleanup(EventBus.prototype, {
|
||||
addEventListener() {
|
||||
super.addEventListener(...arguments);
|
||||
registerCleanup(() => this.removeEventListener(...arguments));
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function setupTests() {
|
||||
// uncomment to debug memory leaks in qunit suite
|
||||
// if (window.gc) {
|
||||
// let memoryBeforeModule;
|
||||
// QUnit.moduleStart(({ tests }) => {
|
||||
// if (tests.length) {
|
||||
// window.gc();
|
||||
// memoryBeforeModule = window.performance.memory.usedJSHeapSize;
|
||||
// }
|
||||
// });
|
||||
// QUnit.moduleDone(({ name }) => {
|
||||
// if (memoryBeforeModule) {
|
||||
// window.gc();
|
||||
// const afterGc = window.performance.memory.usedJSHeapSize;
|
||||
// console.log(
|
||||
// `MEMINFO - After suite "${name}" - after gc: ${afterGc} delta: ${
|
||||
// afterGc - memoryBeforeModule
|
||||
// }`
|
||||
// );
|
||||
// memoryBeforeModule = null;
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
|
||||
QUnit.testStart(() => {
|
||||
prepareRegistriesWithCleanup();
|
||||
forceLocaleAndTimezoneWithCleanup();
|
||||
cleanLoadedLanguages();
|
||||
patchBrowserWithCleanup();
|
||||
registerCleanup(router.cancelPushes);
|
||||
patchCookie();
|
||||
patchBodyAddEventListener();
|
||||
patchEventBus();
|
||||
patchSessionInfo();
|
||||
patchOdoo();
|
||||
patchOwlApp();
|
||||
});
|
||||
|
||||
await whenReady();
|
||||
patchAssets();
|
||||
|
||||
// make sure images do not trigger a GET on the server
|
||||
new MutationObserver((mutations) => {
|
||||
const nodes = mutations.flatMap(({ target }) => {
|
||||
if (target.nodeName === "IMG" || target.nodeName === "IFRAME") {
|
||||
return target;
|
||||
}
|
||||
return [
|
||||
...target.getElementsByTagName("img"),
|
||||
...target.getElementsByTagName("iframe"),
|
||||
];
|
||||
});
|
||||
for (const node of nodes) {
|
||||
const src = node.getAttribute("src");
|
||||
if (src && src !== "about:blank") {
|
||||
node.dataset.src = src;
|
||||
if (node.nodeName === "IMG") {
|
||||
node.removeAttribute("src");
|
||||
} else {
|
||||
node.setAttribute("src", "about:blank");
|
||||
}
|
||||
node.dispatchEvent(new Event("load"));
|
||||
}
|
||||
}
|
||||
}).observe(document.body, {
|
||||
subtree: true,
|
||||
childList: true,
|
||||
attributeFilter: ["src"],
|
||||
});
|
||||
}
|
||||
685
odoo-bringout-oca-ocb-web/web/static/tests/legacy/utils.js
Normal file
685
odoo-bringout-oca-ocb-web/web/static/tests/legacy/utils.js
Normal file
|
|
@ -0,0 +1,685 @@
|
|||
/** @odoo-module alias=@web/../tests/utils default=false */
|
||||
|
||||
import { isVisible } from "@web/core/utils/ui";
|
||||
import { registerCleanup } from "@web/../tests/helpers/cleanup";
|
||||
import {
|
||||
click as webClick,
|
||||
getFixture,
|
||||
makeDeferred,
|
||||
triggerEvents as webTriggerEvents,
|
||||
} from "@web/../tests/helpers/utils";
|
||||
|
||||
/**
|
||||
* Create a fake object 'dataTransfer', linked to some files,
|
||||
* which is passed to drag and drop events.
|
||||
*
|
||||
* @param {Object[]} files
|
||||
* @returns {Object}
|
||||
*/
|
||||
function createFakeDataTransfer(files) {
|
||||
return {
|
||||
dropEffect: "all",
|
||||
effectAllowed: "all",
|
||||
files,
|
||||
items: [],
|
||||
types: ["Files"],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits until exactly one element matching the given `selector` is present in
|
||||
* `options.target` and then clicks on it.
|
||||
*
|
||||
* @param {string} selector
|
||||
* @param {ContainsOptions} [options] forwarded to `contains`
|
||||
* @param {boolean} [options.shiftKey]
|
||||
*/
|
||||
export async function click(selector, options = {}) {
|
||||
const { shiftKey } = options;
|
||||
delete options.shiftKey;
|
||||
await contains(selector, { click: { shiftKey }, ...options });
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits until exactly one element matching the given `selector` is present in
|
||||
* `options.target` and then dragenters `files` on it.
|
||||
*
|
||||
* @param {string} selector
|
||||
* @param {Object[]} files
|
||||
* @param {ContainsOptions} [options] forwarded to `contains`
|
||||
*/
|
||||
export async function dragenterFiles(selector, files, options) {
|
||||
await contains(selector, { dragenterFiles: files, ...options });
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits until exactly one element matching the given `selector` is present in
|
||||
* `options.target` and then dragovers `files` on it.
|
||||
*
|
||||
* @param {string} selector
|
||||
* @param {Object[]} files
|
||||
* @param {ContainsOptions} [options] forwarded to `contains`
|
||||
*/
|
||||
export async function dragoverFiles(selector, files, options) {
|
||||
await contains(selector, { dragoverFiles: files, ...options });
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits until exactly one element matching the given `selector` is present in
|
||||
* `options.target` and then drops `files` on it.
|
||||
*
|
||||
* @param {string} selector
|
||||
* @param {Object[]} files
|
||||
* @param {ContainsOptions} [options] forwarded to `contains`
|
||||
*/
|
||||
export async function dropFiles(selector, files, options) {
|
||||
await contains(selector, { dropFiles: files, ...options });
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits until exactly one element matching the given `selector` is present in
|
||||
* `options.target` and then inputs `files` on it.
|
||||
*
|
||||
* @param {string} selector
|
||||
* @param {Object[]} files
|
||||
* @param {ContainsOptions} [options] forwarded to `contains`
|
||||
*/
|
||||
export async function inputFiles(selector, files, options) {
|
||||
await contains(selector, { inputFiles: files, ...options });
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits until exactly one element matching the given `selector` is present in
|
||||
* `options.target` and then pastes `files` on it.
|
||||
*
|
||||
* @param {string} selector
|
||||
* @param {Object[]} files
|
||||
* @param {ContainsOptions} [options] forwarded to `contains`
|
||||
*/
|
||||
export async function pasteFiles(selector, files, options) {
|
||||
await contains(selector, { pasteFiles: files, ...options });
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits until exactly one element matching the given `selector` is present in
|
||||
* `options.target` and then focuses on it.
|
||||
*
|
||||
* @param {string} selector
|
||||
* @param {ContainsOptions} [options] forwarded to `contains`
|
||||
*/
|
||||
export async function focus(selector, options) {
|
||||
await contains(selector, { setFocus: true, ...options });
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits until exactly one element matching the given `selector` is present in
|
||||
* `options.target` and then inserts the given `content`.
|
||||
*
|
||||
* @param {string} selector
|
||||
* @param {string} content
|
||||
* @param {ContainsOptions} [options] forwarded to `contains`
|
||||
* @param {boolean} [options.replace=false]
|
||||
*/
|
||||
export async function insertText(selector, content, options = {}) {
|
||||
const { replace = false } = options;
|
||||
delete options.replace;
|
||||
await contains(selector, { ...options, insertText: { content, replace } });
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits until exactly one element matching the given `selector` is present in
|
||||
* `options.target` and then sets its `scrollTop` to the given value.
|
||||
*
|
||||
* @param {string} selector
|
||||
* @param {number|"bottom"} scrollTop
|
||||
* @param {ContainsOptions} [options] forwarded to `contains`
|
||||
*/
|
||||
export async function scroll(selector, scrollTop, options) {
|
||||
await contains(selector, { setScroll: scrollTop, ...options });
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits until exactly one element matching the given `selector` is present in
|
||||
* `options.target` and then triggers `event` on it.
|
||||
*
|
||||
* @param {string} selector
|
||||
* @param {(import("@web/../tests/helpers/utils").EventType|[import("@web/../tests/helpers/utils").EventType, EventInit])[]} events
|
||||
* @param {ContainsOptions} [options] forwarded to `contains`
|
||||
*/
|
||||
export async function triggerEvents(selector, events, options) {
|
||||
await contains(selector, { triggerEvents: events, ...options });
|
||||
}
|
||||
|
||||
function log(ok, message) {
|
||||
if (window.QUnit) {
|
||||
QUnit.assert.ok(ok, message);
|
||||
} else {
|
||||
if (ok) {
|
||||
console.log(message);
|
||||
} else {
|
||||
console.error(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let hasUsedContainsPositively = false;
|
||||
if (window.QUnit) {
|
||||
QUnit.testStart(() => (hasUsedContainsPositively = false));
|
||||
}
|
||||
/**
|
||||
* @typedef {[string, ContainsOptions]} ContainsTuple tuple representing params of the contains
|
||||
* function, where the first element is the selector, and the second element is the options param.
|
||||
* @typedef {Object} ContainsOptions
|
||||
* @property {ContainsTuple} [after] if provided, the found element(s) must be after the element
|
||||
* matched by this param.
|
||||
* @property {ContainsTuple} [before] if provided, the found element(s) must be before the element
|
||||
* matched by this param.
|
||||
* @property {Object} [click] if provided, clicks on the first found element
|
||||
* @property {ContainsTuple|ContainsTuple[]} [contains] if provided, the found element(s) must
|
||||
* contain the provided sub-elements.
|
||||
* @property {number} [count=1] numbers of elements to be found to declare the contains check
|
||||
* as successful. Elements are counted after applying all other filters.
|
||||
* @property {Object[]} [dragenterFiles] if provided, dragenters the given files on the found element
|
||||
* @property {Object[]} [dragoverFiles] if provided, dragovers the given files on the found element
|
||||
* @property {Object[]} [dropFiles] if provided, drops the given files on the found element
|
||||
* @property {Object[]} [inputFiles] if provided, inputs the given files on the found element
|
||||
* @property {{content:string, replace:boolean}} [insertText] if provided, adds to (or replace) the
|
||||
* value of the first found element by the given content.
|
||||
* @property {ContainsTuple} [parent] if provided, the found element(s) must have as
|
||||
* parent the node matching the parent parameter.
|
||||
* @property {Object[]} [pasteFiles] if provided, pastes the given files on the found element
|
||||
* @property {number|"bottom"} [scroll] if provided, the scrollTop of the found element(s)
|
||||
* must match.
|
||||
* Note: when using one of the scrollTop options, it is advised to ensure the height is not going
|
||||
* to change soon, by checking with a preceding contains that all the expected elements are in DOM.
|
||||
* @property {boolean} [setFocus] if provided, focuses the first found element.
|
||||
* @property {boolean} [shadowRoot] if provided, targets the shadowRoot of the found elements.
|
||||
* @property {number|"bottom"} [setScroll] if provided, sets the scrollTop on the first found
|
||||
* element.
|
||||
* @property {HTMLElement} [target=getFixture()]
|
||||
* @property {string[]} [triggerEvents] if provided, triggers the given events on the found element
|
||||
* @property {string} [text] if provided, the textContent of the found element(s) or one of their
|
||||
* descendants must match. Use `textContent` option for a match on the found element(s) only.
|
||||
* @property {string} [textContent] if provided, the textContent of the found element(s) must match.
|
||||
* Prefer `text` option for a match on the found element(s) or any of their descendants, usually
|
||||
* allowing for a simpler and less specific selector.
|
||||
* @property {string} [value] if provided, the input value of the found element(s) must match.
|
||||
* Note: value changes are not observed directly, another mutation must happen to catch them.
|
||||
* @property {boolean} [visible] if provided, the found element(s) must be (in)visible
|
||||
*/
|
||||
class Contains {
|
||||
/**
|
||||
* @param {string} selector
|
||||
* @param {ContainsOptions} [options={}]
|
||||
*/
|
||||
constructor(selector, options = {}) {
|
||||
this.selector = selector;
|
||||
this.options = options;
|
||||
this.options.count ??= 1;
|
||||
this.options.targetParam = this.options.target;
|
||||
this.options.target ??= getFixture();
|
||||
let selectorMessage = `${this.options.count} of "${this.selector}"`;
|
||||
if (this.options.visible !== undefined) {
|
||||
selectorMessage = `${selectorMessage} ${
|
||||
this.options.visible ? "visible" : "invisible"
|
||||
}`;
|
||||
}
|
||||
if (this.options.targetParam) {
|
||||
selectorMessage = `${selectorMessage} inside a specific target`;
|
||||
}
|
||||
if (this.options.parent) {
|
||||
selectorMessage = `${selectorMessage} inside a specific parent`;
|
||||
}
|
||||
if (this.options.contains) {
|
||||
selectorMessage = `${selectorMessage} with a specified sub-contains`;
|
||||
}
|
||||
if (this.options.text !== undefined) {
|
||||
selectorMessage = `${selectorMessage} with text "${this.options.text}"`;
|
||||
}
|
||||
if (this.options.textContent !== undefined) {
|
||||
selectorMessage = `${selectorMessage} with textContent "${this.options.textContent}"`;
|
||||
}
|
||||
if (this.options.value !== undefined) {
|
||||
selectorMessage = `${selectorMessage} with value "${this.options.value}"`;
|
||||
}
|
||||
if (this.options.scroll !== undefined) {
|
||||
selectorMessage = `${selectorMessage} with scroll "${this.options.scroll}"`;
|
||||
}
|
||||
if (this.options.after !== undefined) {
|
||||
selectorMessage = `${selectorMessage} after a specified element`;
|
||||
}
|
||||
if (this.options.before !== undefined) {
|
||||
selectorMessage = `${selectorMessage} before a specified element`;
|
||||
}
|
||||
this.selectorMessage = selectorMessage;
|
||||
if (this.options.contains && !Array.isArray(this.options.contains[0])) {
|
||||
this.options.contains = [this.options.contains];
|
||||
}
|
||||
if (this.options.count) {
|
||||
hasUsedContainsPositively = true;
|
||||
} else if (!hasUsedContainsPositively) {
|
||||
throw new Error(
|
||||
`Starting a test with "contains" of count 0 for selector "${this.selector}" is useless because it might immediately resolve. Start the test by checking that an expected element actually exists.`
|
||||
);
|
||||
}
|
||||
/** @type {string} */
|
||||
this.successMessage = undefined;
|
||||
/** @type {function} */
|
||||
this.executeError = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts this contains check, either immediately resolving if there is a
|
||||
* match, or registering appropriate listeners and waiting until there is a
|
||||
* match or a timeout (resolving or rejecting respectively).
|
||||
*
|
||||
* Success or failure messages will be logged with QUnit as well.
|
||||
*
|
||||
* @returns {Promise}
|
||||
*/
|
||||
run() {
|
||||
this.done = false;
|
||||
this.def = makeDeferred();
|
||||
this.scrollListeners = new Set();
|
||||
this.onScroll = () => this.runOnce("after scroll");
|
||||
if (!this.runOnce("immediately")) {
|
||||
this.timer = setTimeout(
|
||||
() => this.runOnce("Timeout of 5 seconds", { crashOnFail: true }),
|
||||
5000
|
||||
);
|
||||
this.observer = new MutationObserver((mutations) => {
|
||||
try {
|
||||
this.runOnce("after mutations");
|
||||
} catch (e) {
|
||||
this.def.reject(e); // prevents infinite loop in case of programming error
|
||||
}
|
||||
});
|
||||
this.observer.observe(this.options.target, {
|
||||
attributes: true,
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
registerCleanup(() => {
|
||||
if (!this.done) {
|
||||
this.runOnce("Test ended", { crashOnFail: true });
|
||||
}
|
||||
});
|
||||
}
|
||||
return this.def;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs this contains check once, immediately returning the result (or
|
||||
* undefined), and possibly resolving or rejecting the main promise
|
||||
* (and printing QUnit log) depending on options.
|
||||
* If undefined is returned it means the check was not successful.
|
||||
*
|
||||
* @param {string} whenMessage
|
||||
* @param {Object} [options={}]
|
||||
* @param {boolean} [options.crashOnFail=false]
|
||||
* @param {boolean} [options.executeOnSuccess=true]
|
||||
* @returns {HTMLElement[]|undefined}
|
||||
*/
|
||||
runOnce(whenMessage, { crashOnFail = false, executeOnSuccess = true } = {}) {
|
||||
const res = this.select();
|
||||
if (res?.length === this.options.count || crashOnFail) {
|
||||
// clean before doing anything else to avoid infinite loop due to side effects
|
||||
this.observer?.disconnect();
|
||||
clearTimeout(this.timer);
|
||||
for (const el of this.scrollListeners ?? []) {
|
||||
el.removeEventListener("scroll", this.onScroll);
|
||||
}
|
||||
this.done = true;
|
||||
}
|
||||
if (res?.length === this.options.count) {
|
||||
this.successMessage = `Found ${this.selectorMessage} (${whenMessage})`;
|
||||
if (executeOnSuccess) {
|
||||
this.executeAction(res[0]);
|
||||
}
|
||||
return res;
|
||||
} else {
|
||||
this.executeError = () => {
|
||||
let message = `Failed to find ${this.selectorMessage} (${whenMessage}).`;
|
||||
message = res
|
||||
? `${message} Found ${res.length} instead.`
|
||||
: `${message} Parent not found.`;
|
||||
if (this.parentContains) {
|
||||
if (this.parentContains.successMessage) {
|
||||
log(true, this.parentContains.successMessage);
|
||||
} else {
|
||||
this.parentContains.executeError();
|
||||
}
|
||||
}
|
||||
log(false, message);
|
||||
this.def?.reject(new Error(message));
|
||||
for (const childContains of this.childrenContains || []) {
|
||||
if (childContains.successMessage) {
|
||||
log(true, childContains.successMessage);
|
||||
} else {
|
||||
childContains.executeError();
|
||||
}
|
||||
}
|
||||
};
|
||||
if (crashOnFail) {
|
||||
this.executeError();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the action(s) given to this constructor on the found element,
|
||||
* prints the success messages, and resolves the main deferred.
|
||||
|
||||
* @param {HTMLElement} el
|
||||
*/
|
||||
executeAction(el) {
|
||||
let message = this.successMessage;
|
||||
if (this.options.click) {
|
||||
message = `${message} and clicked it`;
|
||||
webClick(el, undefined, {
|
||||
mouseEventInit: this.options.click,
|
||||
skipDisabledCheck: true,
|
||||
skipVisibilityCheck: true,
|
||||
});
|
||||
}
|
||||
if (this.options.dragenterFiles) {
|
||||
message = `${message} and dragentered ${this.options.dragenterFiles.length} file(s)`;
|
||||
const ev = new Event("dragenter", { bubbles: true });
|
||||
Object.defineProperty(ev, "dataTransfer", {
|
||||
value: createFakeDataTransfer(this.options.dragenterFiles),
|
||||
});
|
||||
el.dispatchEvent(ev);
|
||||
}
|
||||
if (this.options.dragoverFiles) {
|
||||
message = `${message} and dragovered ${this.options.dragoverFiles.length} file(s)`;
|
||||
const ev = new Event("dragover", { bubbles: true });
|
||||
Object.defineProperty(ev, "dataTransfer", {
|
||||
value: createFakeDataTransfer(this.options.dragoverFiles),
|
||||
});
|
||||
el.dispatchEvent(ev);
|
||||
}
|
||||
if (this.options.dropFiles) {
|
||||
message = `${message} and dropped ${this.options.dropFiles.length} file(s)`;
|
||||
const ev = new Event("drop", { bubbles: true });
|
||||
Object.defineProperty(ev, "dataTransfer", {
|
||||
value: createFakeDataTransfer(this.options.dropFiles),
|
||||
});
|
||||
el.dispatchEvent(ev);
|
||||
}
|
||||
if (this.options.inputFiles) {
|
||||
message = `${message} and inputted ${this.options.inputFiles.length} file(s)`;
|
||||
// could not use _createFakeDataTransfer as el.files assignation will only
|
||||
// work with a real FileList object.
|
||||
const dataTransfer = new window.DataTransfer();
|
||||
for (const file of this.options.inputFiles) {
|
||||
dataTransfer.items.add(file);
|
||||
}
|
||||
el.files = dataTransfer.files;
|
||||
/**
|
||||
* Changing files programatically is not supposed to trigger the event but
|
||||
* it does in Chrome versions before 73 (which is on runbot), so in that
|
||||
* case there is no need to make a manual dispatch, because it would lead to
|
||||
* the files being added twice.
|
||||
*/
|
||||
const versionRaw = navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./);
|
||||
const chromeVersion = versionRaw ? parseInt(versionRaw[2], 10) : false;
|
||||
if (!chromeVersion || chromeVersion >= 73) {
|
||||
el.dispatchEvent(new Event("change"));
|
||||
}
|
||||
}
|
||||
if (this.options.insertText !== undefined) {
|
||||
message = `${message} and inserted text "${this.options.insertText.content}" (replace: ${this.options.insertText.replace})`;
|
||||
el.focus();
|
||||
if (this.options.insertText.replace) {
|
||||
el.value = "";
|
||||
el.dispatchEvent(new window.KeyboardEvent("keydown", { key: "Backspace" }));
|
||||
el.dispatchEvent(new window.KeyboardEvent("keyup", { key: "Backspace" }));
|
||||
el.dispatchEvent(new window.InputEvent("input"));
|
||||
}
|
||||
for (const char of this.options.insertText.content) {
|
||||
el.value += char;
|
||||
el.dispatchEvent(new window.KeyboardEvent("keydown", { key: char }));
|
||||
el.dispatchEvent(new window.KeyboardEvent("keyup", { key: char }));
|
||||
el.dispatchEvent(new window.InputEvent("input"));
|
||||
}
|
||||
el.dispatchEvent(new window.InputEvent("change"));
|
||||
}
|
||||
if (this.options.pasteFiles) {
|
||||
message = `${message} and pasted ${this.options.pasteFiles.length} file(s)`;
|
||||
const ev = new Event("paste", { bubbles: true });
|
||||
Object.defineProperty(ev, "clipboardData", {
|
||||
value: createFakeDataTransfer(this.options.pasteFiles),
|
||||
});
|
||||
el.dispatchEvent(ev);
|
||||
}
|
||||
if (this.options.setFocus) {
|
||||
message = `${message} and focused it`;
|
||||
el.focus();
|
||||
}
|
||||
if (this.options.setScroll !== undefined) {
|
||||
message = `${message} and set scroll to "${this.options.setScroll}"`;
|
||||
el.scrollTop =
|
||||
this.options.setScroll === "bottom" ? el.scrollHeight : this.options.setScroll;
|
||||
}
|
||||
if (this.options.triggerEvents) {
|
||||
message = `${message} and triggered "${this.options.triggerEvents.join(", ")}" events`;
|
||||
webTriggerEvents(el, null, this.options.triggerEvents, {
|
||||
skipVisibilityCheck: true,
|
||||
});
|
||||
}
|
||||
if (this.parentContains) {
|
||||
log(true, this.parentContains.successMessage);
|
||||
}
|
||||
log(true, message);
|
||||
for (const childContains of this.childrenContains) {
|
||||
log(true, childContains.successMessage);
|
||||
}
|
||||
this.def?.resolve();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the found element(s) according to this constructor setup.
|
||||
* If undefined is returned it means the parent cannot be found
|
||||
*
|
||||
* @returns {HTMLElement[]|undefined}
|
||||
*/
|
||||
select() {
|
||||
const target = this.selectParent();
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
const baseRes = [...target.querySelectorAll(this.selector)]
|
||||
.map((el) => (this.options.shadowRoot ? el.shadowRoot : el))
|
||||
.filter((el) => el);
|
||||
/** @type {Contains[]} */
|
||||
this.childrenContains = [];
|
||||
const res = baseRes.filter((el, currentIndex) => {
|
||||
let condition =
|
||||
(this.options.textContent === undefined ||
|
||||
el.textContent.trim() === this.options.textContent) &&
|
||||
(this.options.value === undefined || el.value === this.options.value) &&
|
||||
(this.options.scroll === undefined ||
|
||||
(this.options.scroll === "bottom"
|
||||
? Math.abs(el.scrollHeight - el.clientHeight - el.scrollTop) <= 1
|
||||
: Math.abs(el.scrollTop - this.options.scroll) <= 1));
|
||||
if (condition && this.options.text !== undefined) {
|
||||
if (
|
||||
el.textContent.trim() !== this.options.text &&
|
||||
[...el.querySelectorAll("*")].every(
|
||||
(el) => el.textContent.trim() !== this.options.text
|
||||
)
|
||||
) {
|
||||
condition = false;
|
||||
}
|
||||
}
|
||||
if (condition && this.options.contains) {
|
||||
for (const param of this.options.contains) {
|
||||
const childContains = new Contains(param[0], { ...param[1], target: el });
|
||||
if (
|
||||
!childContains.runOnce(`as child of el ${currentIndex + 1})`, {
|
||||
executeOnSuccess: false,
|
||||
})
|
||||
) {
|
||||
condition = false;
|
||||
}
|
||||
this.childrenContains.push(childContains);
|
||||
}
|
||||
}
|
||||
if (condition && this.options.visible !== undefined) {
|
||||
if (isVisible(el) !== this.options.visible) {
|
||||
condition = false;
|
||||
}
|
||||
}
|
||||
if (condition && this.options.after) {
|
||||
const afterContains = new Contains(this.options.after[0], {
|
||||
...this.options.after[1],
|
||||
target,
|
||||
});
|
||||
const afterEl = afterContains.runOnce(`as "after"`, {
|
||||
executeOnSuccess: false,
|
||||
})?.[0];
|
||||
if (
|
||||
!afterEl ||
|
||||
!(el.compareDocumentPosition(afterEl) & Node.DOCUMENT_POSITION_PRECEDING)
|
||||
) {
|
||||
condition = false;
|
||||
}
|
||||
this.childrenContains.push(afterContains);
|
||||
}
|
||||
if (condition && this.options.before) {
|
||||
const beforeContains = new Contains(this.options.before[0], {
|
||||
...this.options.before[1],
|
||||
target,
|
||||
});
|
||||
const beforeEl = beforeContains.runOnce(`as "before"`, {
|
||||
executeOnSuccess: false,
|
||||
})?.[0];
|
||||
if (
|
||||
!beforeEl ||
|
||||
!(el.compareDocumentPosition(beforeEl) & Node.DOCUMENT_POSITION_FOLLOWING)
|
||||
) {
|
||||
condition = false;
|
||||
}
|
||||
this.childrenContains.push(beforeContains);
|
||||
}
|
||||
return condition;
|
||||
});
|
||||
if (
|
||||
this.options.scroll !== undefined &&
|
||||
this.scrollListeners &&
|
||||
baseRes.length === this.options.count &&
|
||||
res.length !== this.options.count
|
||||
) {
|
||||
for (const el of baseRes) {
|
||||
if (!this.scrollListeners.has(el)) {
|
||||
this.scrollListeners.add(el);
|
||||
el.addEventListener("scroll", this.onScroll);
|
||||
}
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the found element that should act as the target (parent) for the
|
||||
* main selector.
|
||||
* If undefined is returned it means the parent cannot be found.
|
||||
*
|
||||
* @returns {HTMLElement|undefined}
|
||||
*/
|
||||
selectParent() {
|
||||
if (this.options.parent) {
|
||||
this.parentContains = new Contains(this.options.parent[0], {
|
||||
...this.options.parent[1],
|
||||
target: this.options.target,
|
||||
});
|
||||
return this.parentContains.runOnce(`as parent`, { executeOnSuccess: false })?.[0];
|
||||
}
|
||||
return this.options.target;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits until `count` elements matching the given `selector` are present in
|
||||
* `options.target`.
|
||||
*
|
||||
* @param {string} selector
|
||||
* @param {ContainsOptions} [options]
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export async function contains(selector, options) {
|
||||
await new Contains(selector, options).run();
|
||||
}
|
||||
|
||||
const stepState = {
|
||||
expectedSteps: null,
|
||||
deferred: null,
|
||||
timeout: null,
|
||||
currentSteps: [],
|
||||
|
||||
clear() {
|
||||
clearTimeout(this.timeout);
|
||||
this.timeout = null;
|
||||
this.deferred = null;
|
||||
this.currentSteps = [];
|
||||
this.expectedSteps = null;
|
||||
},
|
||||
|
||||
check({ crashOnFail = false } = {}) {
|
||||
const success =
|
||||
this.expectedSteps.length === this.currentSteps.length &&
|
||||
this.expectedSteps.every((s, i) => s === this.currentSteps[i]);
|
||||
if (!success && !crashOnFail) {
|
||||
return;
|
||||
}
|
||||
QUnit.config.current.assert.verifySteps(this.expectedSteps);
|
||||
if (success) {
|
||||
this.deferred.resolve();
|
||||
} else {
|
||||
this.deferred.reject(new Error("Steps do not match."));
|
||||
}
|
||||
this.clear();
|
||||
},
|
||||
};
|
||||
|
||||
if (window.QUnit) {
|
||||
QUnit.testStart(() =>
|
||||
registerCleanup(() => {
|
||||
if (stepState.expectedSteps) {
|
||||
stepState.check({ crashOnFail: true });
|
||||
} else {
|
||||
stepState.clear();
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate the completion of a test step. This step must then be verified by
|
||||
* calling `assertSteps`.
|
||||
*
|
||||
* @param {string} step
|
||||
*/
|
||||
export function step(step) {
|
||||
stepState.currentSteps.push(step);
|
||||
QUnit.config.current.assert.step(step);
|
||||
if (stepState.expectedSteps) {
|
||||
stepState.check();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for the given steps to be executed or for the timeout to be reached.
|
||||
*
|
||||
* @param {string[]} steps
|
||||
*/
|
||||
export function assertSteps(steps) {
|
||||
if (stepState.expectedSteps) {
|
||||
stepState.check({ crashOnFail: true });
|
||||
}
|
||||
stepState.expectedSteps = steps;
|
||||
stepState.deferred = makeDeferred();
|
||||
stepState.timeout = setTimeout(() => stepState.check({ crashOnFail: true }), 2000);
|
||||
stepState.check();
|
||||
return stepState.deferred;
|
||||
}
|
||||
|
|
@ -1,172 +0,0 @@
|
|||
odoo.define("base.abstract_controller_tests", function (require) {
|
||||
"use strict";
|
||||
|
||||
|
||||
var testUtils = require("web.test_utils");
|
||||
var createView = testUtils.createView;
|
||||
var BasicView = require("web.BasicView");
|
||||
var BasicRenderer = require("web.BasicRenderer");
|
||||
const AbstractRenderer = require('web.AbstractRendererOwl');
|
||||
const RendererWrapper = require('web.RendererWrapper');
|
||||
|
||||
const { xml, onMounted, onWillUnmount, onWillDestroy } = require("@odoo/owl");
|
||||
|
||||
function getHtmlRenderer(html) {
|
||||
return BasicRenderer.extend({
|
||||
start: function () {
|
||||
this.$el.html(html);
|
||||
return this._super.apply(this, arguments);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getOwlView(owlRenderer, viewType) {
|
||||
viewType = viewType || "test";
|
||||
return BasicView.extend({
|
||||
viewType: viewType,
|
||||
config: _.extend({}, BasicView.prototype.config, {
|
||||
Renderer: owlRenderer,
|
||||
}),
|
||||
getRenderer() {
|
||||
return new RendererWrapper(null, this.config.Renderer, {});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getHtmlView(html, viewType) {
|
||||
viewType = viewType || "test";
|
||||
return BasicView.extend({
|
||||
viewType: viewType,
|
||||
config: _.extend({}, BasicView.prototype.config, {
|
||||
Renderer: getHtmlRenderer(html)
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
QUnit.module("LegacyViews", {
|
||||
beforeEach: function () {
|
||||
this.data = {
|
||||
test_model: {
|
||||
fields: {},
|
||||
records: []
|
||||
}
|
||||
};
|
||||
}
|
||||
}, function () {
|
||||
QUnit.module('AbstractController');
|
||||
|
||||
QUnit.test('click on a a[type="action"] child triggers the correct action', async function (assert) {
|
||||
assert.expect(7);
|
||||
|
||||
var html =
|
||||
"<div>" +
|
||||
'<a name="a1" type="action" class="simple">simple</a>' +
|
||||
'<a name="a2" type="action" class="with-child">' +
|
||||
"<span>child</input>" +
|
||||
"</a>" +
|
||||
'<a type="action" data-model="foo" data-method="bar" class="method">method</a>' +
|
||||
'<a type="action" data-model="foo" data-res-id="42" class="descr">descr</a>' +
|
||||
'<a type="action" data-model="foo" class="descr2">descr2</a>' +
|
||||
"</div>";
|
||||
|
||||
var view = await createView({
|
||||
View: getHtmlView(html, "test"),
|
||||
data: this.data,
|
||||
model: "test_model",
|
||||
arch: "<test/>",
|
||||
intercepts: {
|
||||
do_action: function (event) {
|
||||
assert.step(event.data.action.name || event.data.action);
|
||||
}
|
||||
},
|
||||
mockRPC: function (route, args) {
|
||||
if (args.model === 'foo' && args.method === 'bar') {
|
||||
assert.step("method");
|
||||
return Promise.resolve({name: 'method'});
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
}
|
||||
});
|
||||
await testUtils.dom.click(view.$(".simple"));
|
||||
await testUtils.dom.click(view.$(".with-child span"));
|
||||
await testUtils.dom.click(view.$(".method"));
|
||||
await testUtils.dom.click(view.$(".descr"));
|
||||
await testUtils.dom.click(view.$(".descr2"));
|
||||
assert.verifySteps(["a1", "a2", "method", "method", "descr", "descr2"]);
|
||||
|
||||
view.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('OWL Renderer correctly destroyed', async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
class Renderer extends AbstractRenderer {
|
||||
setup() {
|
||||
onWillDestroy(() => {
|
||||
assert.step("destroy");
|
||||
});
|
||||
}
|
||||
}
|
||||
Renderer.template = xml`<div>Test</div>`;
|
||||
|
||||
var view = await createView({
|
||||
View: getOwlView(Renderer, "test"),
|
||||
data: this.data,
|
||||
model: "test_model",
|
||||
arch: "<test/>",
|
||||
});
|
||||
view.destroy();
|
||||
|
||||
assert.verifySteps(["destroy"]);
|
||||
|
||||
});
|
||||
|
||||
QUnit.test('Correctly set focus to search panel with Owl Renderer', async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
class Renderer extends AbstractRenderer { }
|
||||
Renderer.template = xml`<div>Test</div>`;
|
||||
|
||||
var view = await createView({
|
||||
View: getOwlView(Renderer, "test"),
|
||||
data: this.data,
|
||||
model: "test_model",
|
||||
arch: "<test/>",
|
||||
});
|
||||
assert.hasClass(document.activeElement, "o_searchview_input");
|
||||
view.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('Owl Renderer mounted/willUnmount hooks are properly called', async function (assert) {
|
||||
// This test could be removed as soon as controllers and renderers will
|
||||
// both be converted in Owl.
|
||||
assert.expect(3);
|
||||
|
||||
class Renderer extends AbstractRenderer {
|
||||
setup() {
|
||||
onMounted(() => {
|
||||
assert.step("mounted");
|
||||
});
|
||||
onWillUnmount(() => {
|
||||
assert.step("unmounted");
|
||||
});
|
||||
}
|
||||
}
|
||||
Renderer.template = xml`<div>Test</div>`;
|
||||
|
||||
const view = await createView({
|
||||
View: getOwlView(Renderer, "test"),
|
||||
data: this.data,
|
||||
model: "test_model",
|
||||
arch: "<test/>",
|
||||
});
|
||||
|
||||
view.destroy();
|
||||
|
||||
assert.verifySteps([
|
||||
"mounted",
|
||||
"unmounted",
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,130 +0,0 @@
|
|||
odoo.define('web.abstract_model_tests', function (require) {
|
||||
"use strict";
|
||||
|
||||
const AbstractModel = require('web.AbstractModel');
|
||||
const Domain = require('web.Domain');
|
||||
|
||||
QUnit.module('LegacyViews', {}, function () {
|
||||
QUnit.module('AbstractModel');
|
||||
|
||||
QUnit.test('leave sample mode when unknown route is called on sample server', async function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
const Model = AbstractModel.extend({
|
||||
_isEmpty() {
|
||||
return true;
|
||||
},
|
||||
async __load() {
|
||||
if (this.isSampleModel) {
|
||||
await this._rpc({ model: 'partner', method: 'unknown' });
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const model = new Model(null, {
|
||||
modelName: 'partner',
|
||||
fields: {},
|
||||
useSampleModel: true,
|
||||
SampleModel: Model,
|
||||
});
|
||||
|
||||
assert.ok(model.useSampleModel);
|
||||
assert.notOk(model._isInSampleMode);
|
||||
|
||||
await model.load({});
|
||||
|
||||
assert.notOk(model.useSampleModel);
|
||||
assert.notOk(model._isInSampleMode);
|
||||
|
||||
model.destroy();
|
||||
});
|
||||
|
||||
QUnit.test("don't cath general error on sample server in sample mode", async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
const error = new Error();
|
||||
|
||||
const Model = AbstractModel.extend({
|
||||
_isEmpty() {
|
||||
return true;
|
||||
},
|
||||
async __reload() {
|
||||
if (this.isSampleModel) {
|
||||
await this._rpc({ model: 'partner', method: 'read_group' });
|
||||
}
|
||||
},
|
||||
async _rpc() {
|
||||
throw error;
|
||||
},
|
||||
});
|
||||
|
||||
const model = new Model(null, {
|
||||
modelName: 'partner',
|
||||
fields: {},
|
||||
useSampleModel: true,
|
||||
SampleModel: Model,
|
||||
});
|
||||
|
||||
assert.ok(model.useSampleModel);
|
||||
assert.notOk(model._isInSampleMode);
|
||||
|
||||
await model.load({});
|
||||
|
||||
assert.ok(model.useSampleModel);
|
||||
assert.ok(model._isInSampleMode);
|
||||
|
||||
async function reloadModel() {
|
||||
try {
|
||||
await model.reload();
|
||||
} catch (e) {
|
||||
assert.strictEqual(e, error);
|
||||
}
|
||||
}
|
||||
|
||||
await reloadModel();
|
||||
|
||||
model.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('fetch sample data: concurrency', async function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
const Model = AbstractModel.extend({
|
||||
_isEmpty() {
|
||||
return true;
|
||||
},
|
||||
__get() {
|
||||
return { isSample: !!this.isSampleModel };
|
||||
},
|
||||
});
|
||||
|
||||
const model = new Model(null, {
|
||||
modelName: 'partner',
|
||||
fields: {},
|
||||
useSampleModel: true,
|
||||
SampleModel: Model,
|
||||
});
|
||||
|
||||
await model.load({ domain: Domain.FALSE_DOMAIN, });
|
||||
|
||||
const beforeReload = model.get(null, { withSampleData: true });
|
||||
|
||||
const reloaded = model.reload(null, { domain: Domain.TRUE_DOMAIN });
|
||||
const duringReload = model.get(null, { withSampleData: true });
|
||||
|
||||
await reloaded;
|
||||
|
||||
const afterReload = model.get(null, { withSampleData: true });
|
||||
|
||||
assert.strictEqual(beforeReload.isSample, true,
|
||||
"Sample data flag must be true before reload"
|
||||
);
|
||||
assert.strictEqual(duringReload.isSample, true,
|
||||
"Sample data flag must be true during reload"
|
||||
);
|
||||
assert.strictEqual(afterReload.isSample, false,
|
||||
"Sample data flag must be true after reload"
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,109 +0,0 @@
|
|||
odoo.define('web.abstract_view_banner_tests', function (require) {
|
||||
"use strict";
|
||||
|
||||
var AbstractRenderer = require('web.AbstractRenderer');
|
||||
var AbstractView = require('web.AbstractView');
|
||||
|
||||
var testUtils = require('web.test_utils');
|
||||
var createView = testUtils.createView;
|
||||
|
||||
var TestRenderer = AbstractRenderer.extend({
|
||||
_renderView: function () {
|
||||
this.$el.addClass('test_content');
|
||||
return this._super();
|
||||
},
|
||||
});
|
||||
|
||||
var TestView = AbstractView.extend({
|
||||
type: 'test',
|
||||
config: _.extend({}, AbstractView.prototype.config, {
|
||||
Renderer: TestRenderer
|
||||
}),
|
||||
});
|
||||
|
||||
var test_css_url = '/test_assetsbundle/static/src/css/test_cssfile1.css';
|
||||
|
||||
QUnit.module('LegacyViews', {
|
||||
beforeEach: function () {
|
||||
this.data = {
|
||||
test_model: {
|
||||
fields: {},
|
||||
records: [],
|
||||
},
|
||||
};
|
||||
},
|
||||
afterEach: function () {
|
||||
$('head link[href$="' + test_css_url + '"]').remove();
|
||||
}
|
||||
}, function () {
|
||||
QUnit.module('BasicRenderer');
|
||||
|
||||
QUnit.test("The banner should be fetched from the route", function (assert) {
|
||||
var done = assert.async();
|
||||
assert.expect(6);
|
||||
|
||||
var banner_html =`
|
||||
<div class="modal o_onboarding_modal o_technical_modal" tabindex="-1" role="dialog"
|
||||
data-bs-backdrop="false">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-footer">
|
||||
<a type="action" class="btn btn-primary" data-bs-dismiss="modal"
|
||||
data-o-hide-banner="true">
|
||||
Remove
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="o_onboarding_container collapse show">
|
||||
<div class="o_onboarding_wrap">
|
||||
<a href="#" data-bs-toggle="modal" data-bs-target=".o_onboarding_modal"
|
||||
class="float-end o_onboarding_btn_close">
|
||||
<i class="fa fa-times" title="Close the onboarding panel" />
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<link type="text/css" href="` + test_css_url + `" rel="stylesheet">
|
||||
<div class="hello_banner">Here is the banner</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
createView({
|
||||
View: TestView,
|
||||
model: 'test_model',
|
||||
data: this.data,
|
||||
arch: '<test banner_route="/module/hello_banner"/>',
|
||||
mockRPC: function (route, args) {
|
||||
if (route === '/module/hello_banner') {
|
||||
assert.step(route);
|
||||
return Promise.resolve({html: banner_html});
|
||||
}
|
||||
return this._super(route, args);
|
||||
},
|
||||
}).then(async function (view) {
|
||||
var $banner = view.$('.hello_banner');
|
||||
assert.strictEqual($banner.length, 1,
|
||||
"The view should contain the response from the controller.");
|
||||
assert.verifySteps(['/module/hello_banner'], "The banner should be fetched.");
|
||||
|
||||
var $head_link = $('head link[href$="' + test_css_url + '"]');
|
||||
assert.strictEqual($head_link.length, 1,
|
||||
"The stylesheet should have been added to head.");
|
||||
|
||||
var $banner_link = $('link[href$="' + test_css_url + '"]', $banner);
|
||||
assert.strictEqual($banner_link.length, 0,
|
||||
"The stylesheet should have been removed from the banner.");
|
||||
|
||||
await testUtils.dom.click(view.$('.o_onboarding_btn_close')); // click on close to remove banner
|
||||
await testUtils.dom.click(view.$('.o_technical_modal .btn-primary:contains("Remove")')); // click on button remove from techinal modal
|
||||
assert.strictEqual(view.$('.o_onboarding_container.show').length, 0,
|
||||
"Banner should be removed from the view");
|
||||
|
||||
view.destroy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
|
|
@ -1,69 +0,0 @@
|
|||
odoo.define('web.abstract_view_tests', function (require) {
|
||||
"use strict";
|
||||
|
||||
const { registry } = require('@web/core/registry');
|
||||
const legacyViewRegistry = require('web.view_registry');
|
||||
var ListView = require('web.ListView');
|
||||
|
||||
const { createWebClient, doAction } = require('@web/../tests/webclient/helpers');
|
||||
|
||||
QUnit.module('LegacyViews', {
|
||||
beforeEach: function () {
|
||||
this.data = {
|
||||
fake_model: {
|
||||
fields: {},
|
||||
record: [],
|
||||
},
|
||||
foo: {
|
||||
fields: {
|
||||
foo: {string: "Foo", type: "char"},
|
||||
bar: {string: "Bar", type: "boolean"},
|
||||
},
|
||||
records: [
|
||||
{id: 1, bar: true, foo: "yop"},
|
||||
{id: 2, bar: true, foo: "blip"},
|
||||
]
|
||||
},
|
||||
};
|
||||
},
|
||||
}, function () {
|
||||
|
||||
QUnit.module('AbstractView');
|
||||
|
||||
QUnit.test('group_by from context can be a string, instead of a list of strings', async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
registry.category("views").remove("list"); // remove new list from registry
|
||||
legacyViewRegistry.add("list", ListView); // add legacy list -> will be wrapped and added to new registry
|
||||
|
||||
const serverData = {
|
||||
actions: {
|
||||
1: {
|
||||
id: 1,
|
||||
name: 'Foo',
|
||||
res_model: 'foo',
|
||||
type: 'ir.actions.act_window',
|
||||
views: [[false, 'list']],
|
||||
context: {
|
||||
group_by: 'bar',
|
||||
},
|
||||
}
|
||||
},
|
||||
views: {
|
||||
'foo,false,list': '<tree><field name="foo"/><field name="bar"/></tree>',
|
||||
'foo,false,search': '<search></search>',
|
||||
},
|
||||
models: this.data
|
||||
};
|
||||
|
||||
const mockRPC = (route, args) => {
|
||||
if (args.method === 'web_read_group') {
|
||||
assert.deepEqual(args.kwargs.groupby, ['bar']);
|
||||
}
|
||||
};
|
||||
const webClient = await createWebClient({ serverData, mockRPC });
|
||||
await doAction(webClient, 1);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,80 +0,0 @@
|
|||
odoo.define('web.basic_view_tests', function (require) {
|
||||
"use strict";
|
||||
|
||||
const BasicView = require('web.BasicView');
|
||||
const BasicRenderer = require("web.BasicRenderer");
|
||||
const testUtils = require('web.test_utils');
|
||||
const widgetRegistryOwl = require('web.widgetRegistry');
|
||||
const { LegacyComponent } = require("@web/legacy/legacy_component");
|
||||
|
||||
const { xml } = owl;
|
||||
|
||||
const createView = testUtils.createView;
|
||||
|
||||
QUnit.module('LegacyViews', {
|
||||
beforeEach: function () {
|
||||
this.data = {
|
||||
fake_model: {
|
||||
fields: {},
|
||||
record: [],
|
||||
},
|
||||
foo: {
|
||||
fields: {
|
||||
foo: { string: "Foo", type: "char" },
|
||||
bar: { string: "Bar", type: "boolean" },
|
||||
},
|
||||
records: [
|
||||
{ id: 1, bar: true, foo: "yop" },
|
||||
{ id: 2, bar: true, foo: "blip" },
|
||||
]
|
||||
},
|
||||
};
|
||||
},
|
||||
}, function () {
|
||||
|
||||
QUnit.module('BasicView');
|
||||
|
||||
QUnit.test('fields given in fieldDependencies of custom widget are loaded', async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
const basicView = BasicView.extend({
|
||||
viewType: "test",
|
||||
config: Object.assign({}, BasicView.prototype.config, {
|
||||
Renderer: BasicRenderer,
|
||||
})
|
||||
});
|
||||
|
||||
class MyWidget extends LegacyComponent {}
|
||||
MyWidget.fieldDependencies = {
|
||||
foo: { type: 'char' },
|
||||
bar: { type: 'boolean' },
|
||||
};
|
||||
MyWidget.template = xml/* xml */`
|
||||
<div class="custom-widget">Hello World!</div>
|
||||
`;
|
||||
widgetRegistryOwl.add('testWidget', MyWidget);
|
||||
|
||||
const view = await createView({
|
||||
View: basicView,
|
||||
data: this.data,
|
||||
model: "foo",
|
||||
arch:
|
||||
`<test>
|
||||
<widget name="testWidget"/>
|
||||
</test>`,
|
||||
mockRPC: function (route, args) {
|
||||
if (route === "/web/dataset/search_read") {
|
||||
assert.deepEqual(args.fields, ["foo", "bar"],
|
||||
"search_read should be called with dependent fields");
|
||||
return Promise.resolve();
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
}
|
||||
});
|
||||
|
||||
view.destroy();
|
||||
delete widgetRegistryOwl.map.testWidget;
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,555 @@
|
|||
/** @odoo-module alias=@web/../tests/views/calendar/helpers default=false */
|
||||
|
||||
import { uiService } from "@web/core/ui/ui_service";
|
||||
import { createElement } from "@web/core/utils/xml";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { Field } from "@web/views/fields/field";
|
||||
import { clearRegistryWithCleanup, makeTestEnv } from "../../helpers/mock_env";
|
||||
import { click, getFixture, mount, nextTick, triggerEvent } from "../../helpers/utils";
|
||||
import { setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
|
||||
export function makeEnv(services = {}) {
|
||||
clearRegistryWithCleanup(registry.category("main_components"));
|
||||
setupViewRegistries();
|
||||
services = Object.assign(
|
||||
{
|
||||
ui: uiService,
|
||||
},
|
||||
services
|
||||
);
|
||||
|
||||
for (const [key, service] of Object.entries(services)) {
|
||||
registry.category("services").add(key, service, { force: true });
|
||||
}
|
||||
|
||||
return makeTestEnv({
|
||||
config: {
|
||||
setDisplayName: () => {},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
export async function mountComponent(C, env, props) {
|
||||
const target = getFixture();
|
||||
return await mount(C, target, { env, props });
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
export function makeFakeDate() {
|
||||
return luxon.DateTime.local(2021, 7, 16, 8, 0, 0, 0);
|
||||
}
|
||||
|
||||
export function makeFakeRecords() {
|
||||
return {
|
||||
1: {
|
||||
id: 1,
|
||||
title: "1 day, all day in July",
|
||||
start: makeFakeDate(),
|
||||
isAllDay: true,
|
||||
end: makeFakeDate(),
|
||||
},
|
||||
2: {
|
||||
id: 2,
|
||||
title: "3 days, all day in July",
|
||||
start: makeFakeDate().plus({ days: 2 }),
|
||||
isAllDay: true,
|
||||
end: makeFakeDate().plus({ days: 4 }),
|
||||
},
|
||||
3: {
|
||||
id: 3,
|
||||
title: "1 day, all day in June",
|
||||
start: makeFakeDate().plus({ months: -1 }),
|
||||
isAllDay: true,
|
||||
end: makeFakeDate().plus({ months: -1 }),
|
||||
},
|
||||
4: {
|
||||
id: 4,
|
||||
title: "3 days, all day in June",
|
||||
start: makeFakeDate().plus({ months: -1, days: 2 }),
|
||||
isAllDay: true,
|
||||
end: makeFakeDate().plus({ months: -1, days: 4 }),
|
||||
},
|
||||
5: {
|
||||
id: 5,
|
||||
title: "Over June and July",
|
||||
start: makeFakeDate().startOf("month").plus({ days: -2 }),
|
||||
isAllDay: true,
|
||||
end: makeFakeDate().startOf("month").plus({ days: 2 }),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const FAKE_FILTER_SECTIONS = [
|
||||
{
|
||||
label: "Attendees",
|
||||
fieldName: "partner_ids",
|
||||
avatar: {
|
||||
model: "res.partner",
|
||||
field: "avatar_128",
|
||||
},
|
||||
hasAvatar: true,
|
||||
write: {
|
||||
model: "filter_partner",
|
||||
field: "partner_id",
|
||||
},
|
||||
canAddFilter: true,
|
||||
filters: [
|
||||
{
|
||||
type: "user",
|
||||
label: "Mitchell Admin",
|
||||
active: true,
|
||||
value: 3,
|
||||
colorIndex: 3,
|
||||
recordId: null,
|
||||
canRemove: false,
|
||||
hasAvatar: true,
|
||||
},
|
||||
{
|
||||
type: "all",
|
||||
label: "Everybody's calendar",
|
||||
active: false,
|
||||
value: "all",
|
||||
colorIndex: null,
|
||||
recordId: null,
|
||||
canRemove: false,
|
||||
hasAvatar: false,
|
||||
},
|
||||
{
|
||||
type: "record",
|
||||
label: "Brandon Freeman",
|
||||
active: true,
|
||||
value: 4,
|
||||
colorIndex: 4,
|
||||
recordId: 1,
|
||||
canRemove: true,
|
||||
hasAvatar: true,
|
||||
},
|
||||
{
|
||||
type: "record",
|
||||
label: "Marc Demo",
|
||||
active: false,
|
||||
value: 6,
|
||||
colorIndex: 6,
|
||||
recordId: 2,
|
||||
canRemove: true,
|
||||
hasAvatar: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Users",
|
||||
fieldName: "user_id",
|
||||
avatar: {
|
||||
model: null,
|
||||
field: null,
|
||||
},
|
||||
hasAvatar: false,
|
||||
write: {
|
||||
model: null,
|
||||
field: null,
|
||||
},
|
||||
canAddFilter: false,
|
||||
filters: [
|
||||
{
|
||||
type: "record",
|
||||
label: "Brandon Freeman",
|
||||
active: false,
|
||||
value: 1,
|
||||
colorIndex: false,
|
||||
recordId: null,
|
||||
canRemove: true,
|
||||
hasAvatar: true,
|
||||
},
|
||||
{
|
||||
type: "record",
|
||||
label: "Marc Demo",
|
||||
active: false,
|
||||
value: 2,
|
||||
colorIndex: false,
|
||||
recordId: null,
|
||||
canRemove: true,
|
||||
hasAvatar: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const FAKE_FIELDS = {
|
||||
id: { string: "Id", type: "integer" },
|
||||
user_id: { string: "User", type: "many2one", relation: "user", default: -1 },
|
||||
partner_id: {
|
||||
string: "Partner",
|
||||
type: "many2one",
|
||||
relation: "partner",
|
||||
related: "user_id.partner_id",
|
||||
default: 1,
|
||||
},
|
||||
name: { string: "Name", type: "char" },
|
||||
start_date: { string: "Start Date", type: "date" },
|
||||
stop_date: { string: "Stop Date", type: "date" },
|
||||
start: { string: "Start Datetime", type: "datetime" },
|
||||
stop: { string: "Stop Datetime", type: "datetime" },
|
||||
delay: { string: "Delay", type: "float" },
|
||||
allday: { string: "Is All Day", type: "boolean" },
|
||||
partner_ids: {
|
||||
string: "Attendees",
|
||||
type: "one2many",
|
||||
relation: "partner",
|
||||
default: [[6, 0, [1]]],
|
||||
},
|
||||
type: { string: "Type", type: "integer" },
|
||||
event_type_id: { string: "Event Type", type: "many2one", relation: "event_type" },
|
||||
color: { string: "Color", type: "integer", related: "event_type_id.color" },
|
||||
};
|
||||
|
||||
function makeFakeModelState() {
|
||||
const fakeFieldNode = createElement("field", { name: "name" });
|
||||
const fakeModels = { event: { fields: FAKE_FIELDS } };
|
||||
return {
|
||||
canCreate: true,
|
||||
canDelete: true,
|
||||
canEdit: true,
|
||||
date: makeFakeDate(),
|
||||
fieldMapping: {
|
||||
date_start: "start_date",
|
||||
date_stop: "stop_date",
|
||||
date_delay: "delay",
|
||||
all_day: "allday",
|
||||
color: "color",
|
||||
},
|
||||
fieldNames: ["start_date", "stop_date", "color", "delay", "allday", "user_id"],
|
||||
fields: FAKE_FIELDS,
|
||||
filterSections: FAKE_FILTER_SECTIONS,
|
||||
firstDayOfWeek: 0,
|
||||
isDateHidden: false,
|
||||
isTimeHidden: false,
|
||||
hasAllDaySlot: true,
|
||||
hasEditDialog: false,
|
||||
quickCreate: false,
|
||||
popoverFieldNodes: {
|
||||
name: Field.parseFieldNode(fakeFieldNode, fakeModels, "event", "calendar"),
|
||||
},
|
||||
activeFields: {
|
||||
name: {
|
||||
context: "{}",
|
||||
invisible: false,
|
||||
readonly: false,
|
||||
required: false,
|
||||
onChange: false,
|
||||
},
|
||||
},
|
||||
rangeEnd: makeFakeDate().endOf("month"),
|
||||
rangeStart: makeFakeDate().startOf("month"),
|
||||
records: makeFakeRecords(),
|
||||
resModel: "event",
|
||||
scale: "month",
|
||||
scales: ["day", "week", "month", "year"],
|
||||
unusualDays: [],
|
||||
};
|
||||
}
|
||||
|
||||
export function makeFakeModel(state = {}) {
|
||||
return {
|
||||
...makeFakeModelState(),
|
||||
load() {},
|
||||
createFilter() {},
|
||||
createRecord() {},
|
||||
unlinkFilter() {},
|
||||
unlinkRecord() {},
|
||||
updateFilter() {},
|
||||
updateRecord() {},
|
||||
...state,
|
||||
};
|
||||
}
|
||||
|
||||
// DOM Utils
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
async function scrollTo(el, scrollParam) {
|
||||
el.scrollIntoView(scrollParam);
|
||||
await new Promise(window.requestAnimationFrame);
|
||||
}
|
||||
|
||||
export function findPickedDate(target) {
|
||||
return target.querySelector(".o_datetime_picker .o_selected");
|
||||
}
|
||||
|
||||
export async function pickDate(target, date) {
|
||||
const day = date.split("-")[2];
|
||||
const iDay = parseInt(day, 10) - 1;
|
||||
const el = target.querySelectorAll(`.o_datetime_picker .o_date_item_cell:not(.o_out_of_range)`)[
|
||||
iDay
|
||||
];
|
||||
el.scrollIntoView();
|
||||
await click(el);
|
||||
}
|
||||
|
||||
export function expandCalendarView(target) {
|
||||
// Expends Calendar view and FC too
|
||||
let tmpElement = target.querySelector(".fc");
|
||||
do {
|
||||
tmpElement = tmpElement.parentElement;
|
||||
tmpElement.classList.add("h-100");
|
||||
} while (!tmpElement.classList.contains("o_view_controller"));
|
||||
}
|
||||
|
||||
export function findAllDaySlot(target, date) {
|
||||
return target.querySelector(`.fc-daygrid-body .fc-day[data-date="${date}"]`);
|
||||
}
|
||||
|
||||
export function findDateCell(target, date) {
|
||||
return target.querySelector(`.fc-day[data-date="${date}"]`);
|
||||
}
|
||||
|
||||
export function findEvent(target, eventId) {
|
||||
return target.querySelector(`.o_event[data-event-id="${eventId}"]`);
|
||||
}
|
||||
|
||||
export function findDateCol(target, date) {
|
||||
return target.querySelector(`.fc-col-header-cell.fc-day[data-date="${date}"]`);
|
||||
}
|
||||
|
||||
export function findTimeRow(target, time) {
|
||||
return target.querySelector(`.fc-timegrid-slot[data-time="${time}"]`);
|
||||
}
|
||||
|
||||
export async function triggerEventForCalendar(el, type, position = {}) {
|
||||
const rect = el.getBoundingClientRect();
|
||||
const x = position.x || rect.x + rect.width / 2;
|
||||
const y = position.y || rect.y + rect.height / 2;
|
||||
const attrs = {
|
||||
which: 1,
|
||||
clientX: x,
|
||||
clientY: y,
|
||||
};
|
||||
await triggerEvent(el, null, type, attrs);
|
||||
}
|
||||
|
||||
export async function clickAllDaySlot(target, date) {
|
||||
const el = findAllDaySlot(target, date);
|
||||
await scrollTo(el);
|
||||
await triggerEventForCalendar(el, "mousedown");
|
||||
await triggerEventForCalendar(el, "mouseup");
|
||||
await nextTick();
|
||||
}
|
||||
|
||||
export async function clickDate(target, date) {
|
||||
const el = findDateCell(target, date);
|
||||
await scrollTo(el);
|
||||
await triggerEventForCalendar(el, "mousedown");
|
||||
await triggerEventForCalendar(el, "mouseup");
|
||||
await nextTick();
|
||||
}
|
||||
|
||||
export async function clickEvent(target, eventId) {
|
||||
const el = findEvent(target, eventId);
|
||||
await scrollTo(el);
|
||||
await click(el);
|
||||
await nextTick();
|
||||
}
|
||||
|
||||
export async function selectTimeRange(target, startDateTime, endDateTime) {
|
||||
const [startDate, startTime] = startDateTime.split(" ");
|
||||
const [endDate, endTime] = endDateTime.split(" ");
|
||||
|
||||
const startCol = findDateCol(target, startDate);
|
||||
const endCol = findDateCol(target, endDate);
|
||||
const startRow = findTimeRow(target, startTime);
|
||||
const endRow = findTimeRow(target, endTime);
|
||||
|
||||
await scrollTo(startRow);
|
||||
const startColRect = startCol.getBoundingClientRect();
|
||||
const startRowRect = startRow.getBoundingClientRect();
|
||||
|
||||
await triggerEventForCalendar(startRow, "mousedown", {
|
||||
x: startColRect.x + startColRect.width / 2,
|
||||
y: startRowRect.y + 2,
|
||||
});
|
||||
|
||||
await scrollTo(endRow, false);
|
||||
const endColRect = endCol.getBoundingClientRect();
|
||||
const endRowRect = endRow.getBoundingClientRect();
|
||||
|
||||
await triggerEventForCalendar(endRow, "mousemove", {
|
||||
x: endColRect.x + endColRect.width / 2,
|
||||
y: endRowRect.y - 2,
|
||||
});
|
||||
await triggerEventForCalendar(endRow, "mouseup", {
|
||||
x: endColRect.x + endColRect.width / 2,
|
||||
y: endRowRect.y - 2,
|
||||
});
|
||||
await nextTick();
|
||||
}
|
||||
|
||||
export async function selectDateRange(target, startDate, endDate) {
|
||||
const start = findDateCell(target, startDate);
|
||||
const end = findDateCell(target, endDate);
|
||||
await scrollTo(start);
|
||||
await triggerEventForCalendar(start, "mousedown");
|
||||
await scrollTo(end);
|
||||
await triggerEventForCalendar(end, "mousemove");
|
||||
await triggerEventForCalendar(end, "mouseup");
|
||||
await nextTick();
|
||||
}
|
||||
|
||||
export async function selectAllDayRange(target, startDate, endDate) {
|
||||
const start = findAllDaySlot(target, startDate);
|
||||
const end = findAllDaySlot(target, endDate);
|
||||
await scrollTo(start);
|
||||
await triggerEventForCalendar(start, "mousedown");
|
||||
await scrollTo(end);
|
||||
await triggerEventForCalendar(end, "mousemove");
|
||||
await triggerEventForCalendar(end, "mouseup");
|
||||
await nextTick();
|
||||
}
|
||||
|
||||
export async function moveEventToDate(target, eventId, date, options = {}) {
|
||||
const event = findEvent(target, eventId);
|
||||
const cell = findDateCell(target, date);
|
||||
|
||||
await scrollTo(event);
|
||||
await triggerEventForCalendar(event, "mousedown");
|
||||
|
||||
await scrollTo(cell);
|
||||
await triggerEventForCalendar(cell, "mousemove");
|
||||
|
||||
if (!options.disableDrop) {
|
||||
await triggerEventForCalendar(cell, "mouseup");
|
||||
}
|
||||
await nextTick();
|
||||
}
|
||||
|
||||
export async function moveEventToTime(target, eventId, dateTime) {
|
||||
const event = findEvent(target, eventId);
|
||||
const [date, time] = dateTime.split(" ");
|
||||
|
||||
const col = findDateCol(target, date);
|
||||
const row = findTimeRow(target, time);
|
||||
|
||||
// Find event position
|
||||
await scrollTo(event);
|
||||
const eventRect = event.getBoundingClientRect();
|
||||
const eventPos = {
|
||||
x: eventRect.x + eventRect.width / 2,
|
||||
y: eventRect.y,
|
||||
};
|
||||
|
||||
await triggerEventForCalendar(event, "mousedown", eventPos);
|
||||
|
||||
// Find target position
|
||||
await scrollTo(row, false);
|
||||
const colRect = col.getBoundingClientRect();
|
||||
const rowRect = row.getBoundingClientRect();
|
||||
const toPos = {
|
||||
x: colRect.x + colRect.width / 2,
|
||||
y: rowRect.y - 1,
|
||||
};
|
||||
|
||||
await triggerEventForCalendar(row, "mousemove", toPos);
|
||||
await triggerEventForCalendar(row, "mouseup", toPos);
|
||||
await nextTick();
|
||||
}
|
||||
|
||||
export async function moveEventToAllDaySlot(target, eventId, date) {
|
||||
const event = findEvent(target, eventId);
|
||||
const slot = findAllDaySlot(target, date);
|
||||
|
||||
// Find event position
|
||||
await scrollTo(event);
|
||||
const eventRect = event.getBoundingClientRect();
|
||||
const eventPos = {
|
||||
x: eventRect.x + eventRect.width / 2,
|
||||
y: eventRect.y,
|
||||
};
|
||||
await triggerEventForCalendar(event, "mousedown", eventPos);
|
||||
|
||||
// Find target position
|
||||
await scrollTo(slot);
|
||||
const slotRect = slot.getBoundingClientRect();
|
||||
const toPos = {
|
||||
x: slotRect.x + slotRect.width / 2,
|
||||
y: slotRect.y - 1,
|
||||
};
|
||||
await triggerEventForCalendar(slot, "mousemove", toPos);
|
||||
await triggerEventForCalendar(slot, "mouseup", toPos);
|
||||
await nextTick();
|
||||
}
|
||||
|
||||
export async function resizeEventToTime(target, eventId, dateTime) {
|
||||
const event = findEvent(target, eventId);
|
||||
const [date, time] = dateTime.split(" ");
|
||||
|
||||
const col = findDateCol(target, date);
|
||||
const row = findTimeRow(target, time);
|
||||
|
||||
// Find event position
|
||||
await scrollTo(event);
|
||||
await triggerEventForCalendar(event, "mouseover");
|
||||
|
||||
// Find event resizer
|
||||
const resizer = event.querySelector(".fc-event-resizer-end");
|
||||
resizer.style.display = "block";
|
||||
resizer.style.width = "100%";
|
||||
resizer.style.height = "1em";
|
||||
resizer.style.bottom = "0";
|
||||
const resizerRect = resizer.getBoundingClientRect();
|
||||
const resizerPos = {
|
||||
x: resizerRect.x + resizerRect.width / 2,
|
||||
y: resizerRect.y + resizerRect.height / 2,
|
||||
};
|
||||
await triggerEventForCalendar(resizer, "mousedown", resizerPos);
|
||||
|
||||
// Find target position
|
||||
await scrollTo(row, false);
|
||||
const colRect = col.getBoundingClientRect();
|
||||
const rowRect = row.getBoundingClientRect();
|
||||
const toPos = {
|
||||
x: colRect.x + colRect.width / 2,
|
||||
y: rowRect.y - 1,
|
||||
};
|
||||
|
||||
await triggerEventForCalendar(row, "mousemove", toPos);
|
||||
await triggerEventForCalendar(row, "mouseup", toPos);
|
||||
await nextTick();
|
||||
}
|
||||
|
||||
export async function changeScale(target, scale) {
|
||||
await click(target, `.o_view_scale_selector .scale_button_selection`);
|
||||
await click(target, `.o-dropdown--menu .o_scale_button_${scale}`);
|
||||
await nextTick();
|
||||
}
|
||||
|
||||
export async function navigate(target, direction) {
|
||||
await click(target, `.o_calendar_navigation_buttons .o_calendar_button_${direction}`);
|
||||
}
|
||||
|
||||
export function findFilterPanelSection(target, sectionName) {
|
||||
return target.querySelector(`.o_calendar_filter[data-name="${sectionName}"]`);
|
||||
}
|
||||
|
||||
export function findFilterPanelFilter(target, sectionName, filterValue) {
|
||||
return findFilterPanelSection(target, sectionName).querySelector(
|
||||
`.o_calendar_filter_item[data-value="${filterValue}"]`
|
||||
);
|
||||
}
|
||||
|
||||
export function findFilterPanelSectionFilter(target, sectionName) {
|
||||
return findFilterPanelSection(target, sectionName).querySelector(
|
||||
`.o_calendar_filter_items_checkall`
|
||||
);
|
||||
}
|
||||
|
||||
export async function toggleFilter(target, sectionName, filterValue) {
|
||||
const el = findFilterPanelFilter(target, sectionName, filterValue).querySelector(`input`);
|
||||
await scrollTo(el);
|
||||
await click(el);
|
||||
}
|
||||
|
||||
export async function toggleSectionFilter(target, sectionName) {
|
||||
const el = findFilterPanelSectionFilter(target, sectionName).querySelector(`input`);
|
||||
await scrollTo(el);
|
||||
await click(el);
|
||||
}
|
||||
|
|
@ -1,109 +0,0 @@
|
|||
/* global Benchmark */
|
||||
odoo.define('web.form_benchmarks', function (require) {
|
||||
"use strict";
|
||||
|
||||
const FormView = require('web.FormView');
|
||||
const testUtils = require('web.test_utils');
|
||||
|
||||
const { createView } = testUtils;
|
||||
|
||||
QUnit.module('Form View', {
|
||||
beforeEach: function () {
|
||||
this.data = {
|
||||
foo: {
|
||||
fields: {
|
||||
foo: {string: "Foo", type: "char"},
|
||||
many2many: { string: "bar", type: "many2many", relation: 'bar'},
|
||||
},
|
||||
records: [
|
||||
{ id: 1, foo: "bar", many2many: []},
|
||||
],
|
||||
onchanges: {}
|
||||
},
|
||||
bar: {
|
||||
fields: {
|
||||
char: {string: "char", type: "char"},
|
||||
many2many: { string: "pokemon", type: "many2many", relation: 'pokemon'},
|
||||
},
|
||||
records: [],
|
||||
onchanges: {}
|
||||
},
|
||||
pokemon: {
|
||||
fields: {
|
||||
name: {string: "Name", type: "char"},
|
||||
},
|
||||
records: [],
|
||||
onchanges: {}
|
||||
},
|
||||
};
|
||||
this.arch = null;
|
||||
this.run = function (assert, viewParams, cb) {
|
||||
const data = this.data;
|
||||
const arch = this.arch;
|
||||
return new Promise(resolve => {
|
||||
new Benchmark.Suite({})
|
||||
.add('form', {
|
||||
defer: true,
|
||||
fn: async (deferred) => {
|
||||
const form = await createView(Object.assign({
|
||||
View: FormView,
|
||||
model: 'foo',
|
||||
data,
|
||||
arch,
|
||||
}, viewParams));
|
||||
if (cb) {
|
||||
await cb(form);
|
||||
}
|
||||
form.destroy();
|
||||
deferred.resolve();
|
||||
},
|
||||
})
|
||||
.on('cycle', event => {
|
||||
assert.ok(true, String(event.target));
|
||||
})
|
||||
.on('complete', resolve)
|
||||
.run({ async: true });
|
||||
});
|
||||
};
|
||||
}
|
||||
}, function () {
|
||||
QUnit.test('x2many with 250 rows, 2 fields (with many2many_tags, and modifiers), onchanges, and edition', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
this.data.foo.onchanges.many2many = function (obj) {
|
||||
obj.many2many = [5].concat(obj.many2many);
|
||||
};
|
||||
for (let i = 2; i < 500; i++) {
|
||||
this.data.bar.records.push({
|
||||
id: i,
|
||||
char: "automated data",
|
||||
});
|
||||
this.data.foo.records[0].many2many.push(i);
|
||||
}
|
||||
this.arch = `
|
||||
<form>
|
||||
<field name="many2many">
|
||||
<tree editable="top" limit="250">
|
||||
<field name="char"/>
|
||||
<field name="many2many" widget="many2many_tags" attrs="{'readonly': [('char', '==', 'toto')]}"/>
|
||||
</tree>
|
||||
</field>
|
||||
</form>`;
|
||||
return this.run(assert, { res_id: 1 }, async form => {
|
||||
await testUtils.form.clickEdit(form);
|
||||
await testUtils.dom.click(form.$('.o_data_cell:first'));
|
||||
await testUtils.fields.editInput(form.$('input:first'), "tralala");
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test('form view with 100 fields, half of them being invisible', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
this.arch = `
|
||||
<form>
|
||||
${[...Array(100)].map((_, i) => '<field name="foo"' + (i % 2 ? ' invisible="1"' : '') + '/>').join('')}
|
||||
</form>`;
|
||||
return this.run(assert);
|
||||
});
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,44 @@
|
|||
/** @odoo-module alias=@web/../tests/views/graph_view_tests default=false */
|
||||
|
||||
import { click, findChildren, triggerEvent } from "@web/../tests/helpers/utils";
|
||||
import { ensureArray } from "@web/core/utils/arrays";
|
||||
|
||||
// TODO: remove when dependant modules are converted
|
||||
|
||||
export function checkLabels(assert, graph, expectedLabels) {
|
||||
const labels = getGraphRenderer(graph).chart.data.labels.map((l) => l.toString());
|
||||
assert.deepEqual(labels, expectedLabels);
|
||||
}
|
||||
|
||||
export function checkLegend(assert, graph, expectedLegendLabels) {
|
||||
expectedLegendLabels = ensureArray(expectedLegendLabels);
|
||||
const { chart } = getGraphRenderer(graph);
|
||||
const actualLegendLabels = chart.config.options.plugins.legend.labels
|
||||
.generateLabels(chart)
|
||||
.map((o) => o.text);
|
||||
assert.deepEqual(actualLegendLabels, expectedLegendLabels);
|
||||
}
|
||||
|
||||
export function clickOnDataset(graph) {
|
||||
const { chart } = getGraphRenderer(graph);
|
||||
const meta = chart.getDatasetMeta(0);
|
||||
const rectangle = chart.canvas.getBoundingClientRect();
|
||||
const point = meta.data[0].getCenterPoint();
|
||||
return triggerEvent(chart.canvas, null, "click", {
|
||||
pageX: rectangle.left + point.x,
|
||||
pageY: rectangle.top + point.y,
|
||||
});
|
||||
}
|
||||
|
||||
export function getGraphRenderer(graph) {
|
||||
for (const { component } of Object.values(findChildren(graph).children)) {
|
||||
if (component.chart) {
|
||||
return component;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function selectMode(el, mode) {
|
||||
return click(el, `.o_graph_button[data-mode="${mode}"`);
|
||||
}
|
||||
|
|
@ -0,0 +1,128 @@
|
|||
/** @odoo-module alias=@web/../tests/views/helpers default=false */
|
||||
|
||||
import { makeTestEnv } from "@web/../tests/helpers/mock_env";
|
||||
import { getFixture, mount, nextTick } from "@web/../tests/helpers/utils";
|
||||
import { createDebugContext } from "@web/core/debug/debug_context";
|
||||
import { Dialog } from "@web/core/dialog/dialog";
|
||||
import { MainComponentsContainer } from "@web/core/main_components_container";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { View, getDefaultConfig } from "@web/views/view";
|
||||
import {
|
||||
makeFakeLocalizationService,
|
||||
patchUserWithCleanup,
|
||||
} from "../helpers/mock_services";
|
||||
import {
|
||||
setupControlPanelFavoriteMenuRegistry,
|
||||
setupControlPanelServiceRegistry,
|
||||
} from "../search/helpers";
|
||||
|
||||
import { Component, useSubEnv, xml } from "@odoo/owl";
|
||||
|
||||
const serviceRegistry = registry.category("services");
|
||||
|
||||
const rootDialogTemplate = xml`<Dialog><View t-props="props.viewProps"/></Dialog>`;
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* serverData: Object,
|
||||
* mockRPC?: Function,
|
||||
* type: string,
|
||||
* resModel: string,
|
||||
* [prop:string]: any
|
||||
* }} MakeViewParams
|
||||
*/
|
||||
|
||||
/**
|
||||
* @template {Component} T
|
||||
* @param {MakeViewParams} params
|
||||
* @param {boolean} [inDialog=false]
|
||||
* @returns {Promise<T>}
|
||||
*/
|
||||
async function _makeView(params, inDialog = false) {
|
||||
const props = { resId: false, ...params };
|
||||
const serverData = props.serverData;
|
||||
const mockRPC = props.mockRPC;
|
||||
const config = {
|
||||
...getDefaultConfig(),
|
||||
...props.config,
|
||||
};
|
||||
|
||||
delete props.serverData;
|
||||
delete props.mockRPC;
|
||||
delete props.config;
|
||||
|
||||
if (props.arch) {
|
||||
serverData.views = serverData.views || {};
|
||||
props.viewId = params.viewId || 100000001; // hopefully will not conflict with an id already in views
|
||||
serverData.views[`${props.resModel},${props.viewId},${props.type}`] = props.arch;
|
||||
delete props.arch;
|
||||
props.searchViewId = 100000002; // hopefully will not conflict with an id already in views
|
||||
const searchViewArch = props.searchViewArch || "<search/>";
|
||||
serverData.views[`${props.resModel},${props.searchViewId},search`] = searchViewArch;
|
||||
delete props.searchViewArch;
|
||||
}
|
||||
|
||||
const env = await makeTestEnv({ serverData, mockRPC });
|
||||
Object.assign(env, createDebugContext(env)); // This is needed if the views are in debug mode
|
||||
|
||||
const target = getFixture();
|
||||
const viewEnv = Object.assign(Object.create(env), { config });
|
||||
|
||||
await mount(MainComponentsContainer, target, { env });
|
||||
let viewNode;
|
||||
if (inDialog) {
|
||||
let root;
|
||||
class RootDialog extends Component {
|
||||
static components = { Dialog, View };
|
||||
static template = rootDialogTemplate;
|
||||
static props = ["*"];
|
||||
setup() {
|
||||
root = this;
|
||||
useSubEnv(viewEnv);
|
||||
}
|
||||
}
|
||||
env.services.dialog.add(RootDialog, { viewProps: props });
|
||||
await nextTick();
|
||||
const rootNode = root.__owl__;
|
||||
const dialogNode = Object.values(rootNode.children)[0];
|
||||
viewNode = Object.values(dialogNode.children)[0];
|
||||
} else {
|
||||
const view = await mount(View, target, { env: viewEnv, props });
|
||||
await nextTick();
|
||||
viewNode = view.__owl__;
|
||||
}
|
||||
const withSearchNode = Object.values(viewNode.children)[0];
|
||||
const concreteViewNode = Object.values(withSearchNode.children)[0];
|
||||
const concreteView = concreteViewNode.component;
|
||||
|
||||
return concreteView;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {MakeViewParams} params
|
||||
*/
|
||||
export function makeView(params) {
|
||||
return _makeView(params);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {MakeViewParams} params
|
||||
*/
|
||||
export function makeViewInDialog(params) {
|
||||
return _makeView(params, true);
|
||||
}
|
||||
|
||||
export function setupViewRegistries() {
|
||||
setupControlPanelFavoriteMenuRegistry();
|
||||
setupControlPanelServiceRegistry();
|
||||
patchUserWithCleanup({
|
||||
hasGroup: async (group) => {
|
||||
return [
|
||||
"base.group_allow_export",
|
||||
"base.group_user",
|
||||
].includes(group);
|
||||
},
|
||||
isInternalUser: true,
|
||||
});
|
||||
serviceRegistry.add("localization", makeFakeLocalizationService());
|
||||
}
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
/** @odoo-module alias=@web/../tests/views/kanban/helpers default=false */
|
||||
|
||||
import { makeFakeDialogService } from "@web/../tests/helpers/mock_services";
|
||||
import { click, editInput, getDropdownMenu, nextTick } from "@web/../tests/helpers/utils";
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
export function patchDialog(addDialog) {
|
||||
registry.category("services").add("dialog", makeFakeDialogService(addDialog), { force: true });
|
||||
}
|
||||
|
||||
// Kanban
|
||||
// WOWL remove this helper and use the control panel instead
|
||||
export async function reload(kanban, params = {}) {
|
||||
kanban.env.searchModel.reload(params);
|
||||
kanban.env.searchModel.search();
|
||||
await nextTick();
|
||||
}
|
||||
|
||||
export function getCard(target, cardIndex = 0) {
|
||||
return target.querySelectorAll(".o_kanban_record:not(.o_kanban_ghost)")[cardIndex];
|
||||
}
|
||||
|
||||
export function getColumn(target, groupIndex = 0, ignoreFolded = false) {
|
||||
let selector = ".o_kanban_group";
|
||||
if (ignoreFolded) {
|
||||
selector += ":not(.o_column_folded)";
|
||||
}
|
||||
return target.querySelectorAll(selector)[groupIndex];
|
||||
}
|
||||
|
||||
export function getColumnDropdownMenu(target, groupIndex = 0, ignoreFolded = false) {
|
||||
let selector = ".o_kanban_group";
|
||||
if (ignoreFolded) {
|
||||
selector += ":not(.o_column_folded)";
|
||||
}
|
||||
const column = target.querySelectorAll(selector)[groupIndex];
|
||||
return getDropdownMenu(target, column);
|
||||
}
|
||||
|
||||
export function getCardTexts(target, groupIndex) {
|
||||
const root = groupIndex >= 0 ? getColumn(target, groupIndex) : target;
|
||||
return [...root.querySelectorAll(".o_kanban_record:not(.o_kanban_ghost)")]
|
||||
.map((card) => card.innerText.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
export function getCounters(target) {
|
||||
return [...target.querySelectorAll(".o_animated_number")].map((counter) => counter.innerText);
|
||||
}
|
||||
|
||||
export function getProgressBars(target, columnIndex) {
|
||||
const column = getColumn(target, columnIndex);
|
||||
return [...column.querySelectorAll(".o_column_progress .progress-bar")];
|
||||
}
|
||||
|
||||
export function getTooltips(target, groupIndex) {
|
||||
const root = groupIndex >= 0 ? getColumn(target, groupIndex) : target;
|
||||
return [...root.querySelectorAll(".o_column_progress .progress-bar")]
|
||||
.map((card) => card.dataset.tooltip)
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
// Record
|
||||
export async function createRecord(target) {
|
||||
await click(target, ".o_control_panel_main_buttons button.o-kanban-button-new");
|
||||
await nextTick();
|
||||
}
|
||||
|
||||
export async function quickCreateRecord(target, groupIndex) {
|
||||
await click(getColumn(target, groupIndex), ".o_kanban_quick_add");
|
||||
await nextTick();
|
||||
}
|
||||
|
||||
export async function editQuickCreateInput(target, field, value) {
|
||||
await editInput(target, `.o_kanban_quick_create .o_field_widget[name=${field}] input`, value);
|
||||
}
|
||||
|
||||
export async function validateRecord(target) {
|
||||
await click(target, ".o_kanban_quick_create .o_kanban_add");
|
||||
}
|
||||
|
||||
export async function editRecord(target) {
|
||||
await click(target, ".o_kanban_quick_create .o_kanban_edit");
|
||||
}
|
||||
|
||||
export async function discardRecord(target) {
|
||||
await click(target, ".o_kanban_quick_create .o_kanban_cancel");
|
||||
}
|
||||
|
||||
export async function toggleRecordDropdown(target, recordIndex) {
|
||||
const group = target.querySelectorAll(`.o_kanban_record`)[recordIndex];
|
||||
await click(group, ".o_dropdown_kanban .dropdown-toggle");
|
||||
}
|
||||
|
||||
// Column
|
||||
export async function createColumn(target) {
|
||||
await click(target, ".o_column_quick_create > .o_quick_create_folded");
|
||||
}
|
||||
|
||||
export async function editColumnName(target, value) {
|
||||
await editInput(target, ".o_column_quick_create input", value);
|
||||
}
|
||||
|
||||
export async function validateColumn(target) {
|
||||
await click(target, ".o_column_quick_create .o_kanban_add");
|
||||
}
|
||||
|
||||
export async function loadMore(target, columnIndex) {
|
||||
await click(getColumn(target, columnIndex), ".o_kanban_load_more button");
|
||||
}
|
||||
|
|
@ -1,93 +0,0 @@
|
|||
/* global Benchmark */
|
||||
odoo.define('web.kanban_benchmarks', function (require) {
|
||||
"use strict";
|
||||
|
||||
const KanbanView = require('web.KanbanView');
|
||||
const { createView } = require('web.test_utils');
|
||||
|
||||
QUnit.module('Kanban View', {
|
||||
beforeEach: function () {
|
||||
this.data = {
|
||||
foo: {
|
||||
fields: {
|
||||
foo: {string: "Foo", type: "char"},
|
||||
bar: {string: "Bar", type: "boolean"},
|
||||
int_field: {string: "int_field", type: "integer", sortable: true},
|
||||
qux: {string: "my float", type: "float"},
|
||||
},
|
||||
records: [
|
||||
{ id: 1, bar: true, foo: "yop", int_field: 10, qux: 0.4},
|
||||
{id: 2, bar: true, foo: "blip", int_field: 9, qux: 13},
|
||||
]
|
||||
},
|
||||
};
|
||||
this.arch = null;
|
||||
this.run = function (assert) {
|
||||
const data = this.data;
|
||||
const arch = this.arch;
|
||||
return new Promise(resolve => {
|
||||
new Benchmark.Suite({})
|
||||
.add('kanban', {
|
||||
defer: true,
|
||||
fn: async (deferred) => {
|
||||
const kanban = await createView({
|
||||
View: KanbanView,
|
||||
model: 'foo',
|
||||
data,
|
||||
arch,
|
||||
});
|
||||
kanban.destroy();
|
||||
deferred.resolve();
|
||||
},
|
||||
})
|
||||
.on('cycle', event => {
|
||||
assert.ok(true, String(event.target));
|
||||
})
|
||||
.on('complete', resolve)
|
||||
.run({ async: true });
|
||||
});
|
||||
};
|
||||
}
|
||||
}, function () {
|
||||
QUnit.test('simple kanban view with 2 records', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
this.arch = `
|
||||
<kanban>
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div>
|
||||
<t t-esc="record.foo.value"/>
|
||||
<field name="foo"/>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>`;
|
||||
return this.run(assert);
|
||||
});
|
||||
|
||||
QUnit.test('simple kanban view with 200 records', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
for (let i = 2; i < 200; i++) {
|
||||
this.data.foo.records.push({
|
||||
id: i,
|
||||
foo: `automated data ${i}`,
|
||||
});
|
||||
}
|
||||
|
||||
this.arch = `
|
||||
<kanban>
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div>
|
||||
<t t-esc="record.foo.value"/>
|
||||
<field name="foo"/>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>`;
|
||||
return this.run(assert);
|
||||
});
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue