mirror of
https://github.com/bringout/oca-ocb-core.git
synced 2026-04-20 18:52:02 +02:00
vanilla 18.0
This commit is contained in:
parent
0a7ae8db93
commit
5454004ff9
1963 changed files with 1187893 additions and 919508 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,168 +0,0 @@
|
|||
odoo.define('web.class_tests', function (require) {
|
||||
"use strict";
|
||||
|
||||
var Class = require('web.Class');
|
||||
|
||||
QUnit.module('core', {}, function () {
|
||||
|
||||
QUnit.module('Class');
|
||||
|
||||
|
||||
QUnit.test('Basic class creation', function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
var C = Class.extend({
|
||||
foo: function () {
|
||||
return this.somevar;
|
||||
}
|
||||
});
|
||||
var i = new C();
|
||||
i.somevar = 3;
|
||||
|
||||
assert.ok(i instanceof C);
|
||||
assert.strictEqual(i.foo(), 3);
|
||||
});
|
||||
|
||||
QUnit.test('Class initialization', function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
var C1 = Class.extend({
|
||||
init: function () {
|
||||
this.foo = 3;
|
||||
}
|
||||
});
|
||||
var C2 = Class.extend({
|
||||
init: function (arg) {
|
||||
this.foo = arg;
|
||||
}
|
||||
});
|
||||
|
||||
var i1 = new C1(),
|
||||
i2 = new C2(42);
|
||||
|
||||
assert.strictEqual(i1.foo, 3);
|
||||
assert.strictEqual(i2.foo, 42);
|
||||
});
|
||||
|
||||
QUnit.test('Inheritance', function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
var C0 = Class.extend({
|
||||
foo: function () {
|
||||
return 1;
|
||||
}
|
||||
});
|
||||
var C1 = C0.extend({
|
||||
foo: function () {
|
||||
return 1 + this._super();
|
||||
}
|
||||
});
|
||||
var C2 = C1.extend({
|
||||
foo: function () {
|
||||
return 1 + this._super();
|
||||
}
|
||||
});
|
||||
|
||||
assert.strictEqual(new C0().foo(), 1);
|
||||
assert.strictEqual(new C1().foo(), 2);
|
||||
assert.strictEqual(new C2().foo(), 3);
|
||||
});
|
||||
|
||||
QUnit.test('In-place extension', function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
var C0 = Class.extend({
|
||||
foo: function () {
|
||||
return 3;
|
||||
},
|
||||
qux: function () {
|
||||
return 3;
|
||||
},
|
||||
bar: 3
|
||||
});
|
||||
|
||||
C0.include({
|
||||
foo: function () {
|
||||
return 5;
|
||||
},
|
||||
qux: function () {
|
||||
return 2 + this._super();
|
||||
},
|
||||
bar: 5,
|
||||
baz: 5
|
||||
});
|
||||
|
||||
assert.strictEqual(new C0().bar, 5);
|
||||
assert.strictEqual(new C0().baz, 5);
|
||||
assert.strictEqual(new C0().foo(), 5);
|
||||
assert.strictEqual(new C0().qux(), 5);
|
||||
});
|
||||
|
||||
QUnit.test('In-place extension and inheritance', function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
var C0 = Class.extend({
|
||||
foo: function () { return 1; },
|
||||
bar: function () { return 1; }
|
||||
});
|
||||
var C1 = C0.extend({
|
||||
foo: function () { return 1 + this._super(); }
|
||||
});
|
||||
assert.strictEqual(new C1().foo(), 2);
|
||||
assert.strictEqual(new C1().bar(), 1);
|
||||
|
||||
C1.include({
|
||||
foo: function () { return 2 + this._super(); },
|
||||
bar: function () { return 1 + this._super(); }
|
||||
});
|
||||
assert.strictEqual(new C1().foo(), 4);
|
||||
assert.strictEqual(new C1().bar(), 2);
|
||||
});
|
||||
|
||||
QUnit.test('In-place extensions alter existing instances', function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
var C0 = Class.extend({
|
||||
foo: function () { return 1; },
|
||||
bar: function () { return 1; }
|
||||
});
|
||||
var i = new C0();
|
||||
assert.strictEqual(i.foo(), 1);
|
||||
assert.strictEqual(i.bar(), 1);
|
||||
|
||||
C0.include({
|
||||
foo: function () { return 2; },
|
||||
bar: function () { return 2 + this._super(); }
|
||||
});
|
||||
assert.strictEqual(i.foo(), 2);
|
||||
assert.strictEqual(i.bar(), 3);
|
||||
});
|
||||
|
||||
QUnit.test('In-place extension of subclassed types', function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
var C0 = Class.extend({
|
||||
foo: function () { return 1; },
|
||||
bar: function () { return 1; }
|
||||
});
|
||||
var C1 = C0.extend({
|
||||
foo: function () { return 1 + this._super(); },
|
||||
bar: function () { return 1 + this._super(); }
|
||||
});
|
||||
var i = new C1();
|
||||
|
||||
assert.strictEqual(i.foo(), 2);
|
||||
|
||||
C0.include({
|
||||
foo: function () { return 2; },
|
||||
bar: function () { return 2 + this._super(); }
|
||||
});
|
||||
|
||||
assert.strictEqual(i.foo(), 3);
|
||||
assert.strictEqual(i.bar(), 4);
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
|
@ -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']);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
|
@ -1,530 +0,0 @@
|
|||
odoo.define('web.widget_tests', function (require) {
|
||||
"use strict";
|
||||
|
||||
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');
|
||||
|
||||
QUnit.module('core', {}, function () {
|
||||
|
||||
QUnit.module('Widget');
|
||||
|
||||
QUnit.test('proxy (String)', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
var W = Widget.extend({
|
||||
exec: function () {
|
||||
this.executed = true;
|
||||
}
|
||||
});
|
||||
var w = new W();
|
||||
var fn = w.proxy('exec');
|
||||
fn();
|
||||
assert.ok(w.executed, 'should execute the named method in the right context');
|
||||
w.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('proxy (String)(*args)', function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
var W = Widget.extend({
|
||||
exec: function (arg) {
|
||||
this.executed = arg;
|
||||
}
|
||||
});
|
||||
var w = new W();
|
||||
var fn = w.proxy('exec');
|
||||
fn(42);
|
||||
assert.ok(w.executed, "should execute the named method in the right context");
|
||||
assert.strictEqual(w.executed, 42, "should be passed the proxy's arguments");
|
||||
w.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('proxy (String), include', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
// the proxy function should handle methods being changed on the class
|
||||
// and should always proxy "by name", to the most recent one
|
||||
var W = Widget.extend({
|
||||
exec: function () {
|
||||
this.executed = 1;
|
||||
}
|
||||
});
|
||||
var w = new W();
|
||||
var fn = w.proxy('exec');
|
||||
W.include({
|
||||
exec: function () { this.executed = 2; }
|
||||
});
|
||||
|
||||
fn();
|
||||
assert.strictEqual(w.executed, 2, "should be lazily resolved");
|
||||
w.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('proxy (Function)', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
var w = new (Widget.extend({ }))();
|
||||
|
||||
var fn = w.proxy(function () { this.executed = true; });
|
||||
fn();
|
||||
assert.ok(w.executed, "should set the function's context (like Function#bind)");
|
||||
w.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('proxy (Function)(*args)', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
var w = new (Widget.extend({ }))();
|
||||
|
||||
var fn = w.proxy(function (arg) { this.executed = arg; });
|
||||
fn(42);
|
||||
assert.strictEqual(w.executed, 42, "should be passed the proxy's arguments");
|
||||
w.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('renderElement, no template, default', function (assert) {
|
||||
assert.expect(7);
|
||||
|
||||
var widget = new (Widget.extend({ }))();
|
||||
|
||||
assert.strictEqual(widget.$el, undefined, "should not have a root element");
|
||||
|
||||
widget.renderElement();
|
||||
|
||||
assert.ok(widget.$el, "should have generated a root element");
|
||||
assert.strictEqual(widget.$el, widget.$el, "should provide $el alias");
|
||||
assert.ok(widget.$el.is(widget.el), "should provide raw DOM alias");
|
||||
|
||||
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"));
|
||||
widget.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('no template, custom tag', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
|
||||
var widget = new (Widget.extend({
|
||||
tagName: 'ul'
|
||||
}))();
|
||||
widget.renderElement();
|
||||
|
||||
assert.strictEqual(widget.el.nodeName, 'UL', "should have generated the custom element tag");
|
||||
widget.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('no template, @id', function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
var widget = new (Widget.extend({
|
||||
id: 'foo'
|
||||
}))();
|
||||
widget.renderElement();
|
||||
|
||||
assert.strictEqual(widget.el.attributes.length, 1, "should have one attribute");
|
||||
assert.hasAttrValue(widget.$el, 'id', 'foo', "should have generated the id attribute");
|
||||
assert.strictEqual(widget.el.id, 'foo', "should also be available via property");
|
||||
widget.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('no template, @className', function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
var widget = new (Widget.extend({
|
||||
className: 'oe_some_class'
|
||||
}))();
|
||||
widget.renderElement();
|
||||
|
||||
assert.strictEqual(widget.el.className, 'oe_some_class', "should have the right property");
|
||||
assert.hasAttrValue(widget.$el, 'class', 'oe_some_class', "should have the right attribute");
|
||||
widget.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('no template, bunch of attributes', function (assert) {
|
||||
assert.expect(9);
|
||||
|
||||
var widget = new (Widget.extend({
|
||||
attributes: {
|
||||
'id': 'some_id',
|
||||
'class': 'some_class',
|
||||
'data-foo': 'data attribute',
|
||||
'clark': 'gable',
|
||||
'spoiler': // don't read the next line if you care about Harry Potter...
|
||||
'snape kills dumbledore'
|
||||
}
|
||||
}))();
|
||||
widget.renderElement();
|
||||
|
||||
assert.strictEqual(widget.el.attributes.length, 5, "should have all the specified attributes");
|
||||
|
||||
assert.strictEqual(widget.el.id, 'some_id');
|
||||
assert.hasAttrValue(widget.$el, 'id', 'some_id');
|
||||
|
||||
assert.strictEqual(widget.el.className, 'some_class');
|
||||
assert.hasAttrValue(widget.$el, 'class', 'some_class');
|
||||
|
||||
assert.hasAttrValue(widget.$el, 'data-foo', 'data attribute');
|
||||
assert.strictEqual(widget.$el.data('foo'), 'data attribute');
|
||||
|
||||
assert.hasAttrValue(widget.$el, 'clark', 'gable');
|
||||
assert.hasAttrValue(widget.$el, 'spoiler', 'snape kills dumbledore');
|
||||
widget.destroy();
|
||||
});
|
||||
|
||||
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>'
|
||||
);
|
||||
|
||||
var widget = new (Widget.extend({
|
||||
template: 'test.widget.template'
|
||||
}))();
|
||||
widget.renderElement();
|
||||
|
||||
assert.strictEqual(widget.el.nodeName, 'OL');
|
||||
assert.strictEqual(widget.$el.children().length, 5);
|
||||
assert.strictEqual(widget.el.textContent, '01234');
|
||||
widget.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('repeated', async function (assert) {
|
||||
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>'
|
||||
);
|
||||
var widget = new (Widget.extend({
|
||||
template: 'test.widget.template'
|
||||
}))();
|
||||
widget.value = 42;
|
||||
|
||||
await widget.appendTo($fix)
|
||||
.then(function () {
|
||||
assert.strictEqual($fix.find('p').text(), '42', "DOM fixture should contain initial value");
|
||||
assert.strictEqual(widget.$el.text(), '42', "should set initial value");
|
||||
widget.value = 36;
|
||||
widget.renderElement();
|
||||
assert.strictEqual($fix.find('p').text(), '36', "DOM fixture should use new value");
|
||||
assert.strictEqual(widget.$el.text(), '36', "should set new value");
|
||||
});
|
||||
widget.destroy();
|
||||
});
|
||||
|
||||
|
||||
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.test('basic-alias', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
|
||||
var widget = new (Widget.extend({
|
||||
template: 'test.widget.template'
|
||||
}))();
|
||||
widget.renderElement();
|
||||
|
||||
assert.ok(widget.$('li:eq(3)').is(widget.$el.find('li:eq(3)')),
|
||||
"should do the same thing as calling find on the widget root");
|
||||
widget.destroy();
|
||||
});
|
||||
|
||||
|
||||
QUnit.test('delegate', async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
var a = [];
|
||||
var widget = new (Widget.extend({
|
||||
template: 'test.widget.template',
|
||||
events: {
|
||||
'click': function () {
|
||||
a[0] = true;
|
||||
assert.strictEqual(this, widget, "should trigger events in widget");
|
||||
},
|
||||
'click li.class-3': 'class3',
|
||||
'change input': function () { a[2] = true; }
|
||||
},
|
||||
class3: function () { a[1] = true; }
|
||||
}))();
|
||||
widget.renderElement();
|
||||
|
||||
await testUtils.dom.click(widget.$el, {allowInvisible: true});
|
||||
await testUtils.dom.click(widget.$('li:eq(3)'), {allowInvisible: true});
|
||||
await testUtils.fields.editAndTrigger(widget.$('input:last'), 'foo', 'change');
|
||||
|
||||
for(var i=0; i<3; ++i) {
|
||||
assert.ok(a[i], "should pass test " + i);
|
||||
}
|
||||
widget.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('undelegate', async function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
var clicked = false;
|
||||
var newclicked = false;
|
||||
|
||||
var widget = new (Widget.extend({
|
||||
template: 'test.widget.template',
|
||||
events: { 'click li': function () { clicked = true; } }
|
||||
}))();
|
||||
|
||||
widget.renderElement();
|
||||
widget.$el.on('click', 'li', function () { newclicked = true; });
|
||||
|
||||
await testUtils.dom.clickFirst(widget.$('li'), {allowInvisible: true});
|
||||
assert.ok(clicked, "should trigger bound events");
|
||||
assert.ok(newclicked, "should trigger bound events");
|
||||
|
||||
clicked = newclicked = false;
|
||||
widget._undelegateEvents();
|
||||
await testUtils.dom.clickFirst(widget.$('li'), {allowInvisible: true});
|
||||
assert.ok(!clicked, "undelegate should unbind events delegated");
|
||||
assert.ok(newclicked, "undelegate should only unbind events it created");
|
||||
widget.destroy();
|
||||
});
|
||||
|
||||
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");
|
||||
|
||||
// Note: willStart is always async
|
||||
const MyWidget = Widget.extend({
|
||||
start: function () {
|
||||
assert.ok(false, 'Should not call start method');
|
||||
},
|
||||
});
|
||||
|
||||
const widget = new MyWidget();
|
||||
widget.appendTo($fix);
|
||||
widget.destroy();
|
||||
|
||||
const divEl = document.createElement('div');
|
||||
$fix[0].appendChild(divEl);
|
||||
const widget2 = new MyWidget();
|
||||
widget2.attachTo(divEl);
|
||||
widget2.destroy();
|
||||
});
|
||||
|
||||
QUnit.test("don't destroy twice widget's children", function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
var parent = new Widget();
|
||||
new (Widget.extend({
|
||||
destroy: function () {
|
||||
assert.step('destroy');
|
||||
}
|
||||
}))(parent);
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -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,610 +0,0 @@
|
|||
odoo.define('web.test_utils_dom', function (require) {
|
||||
"use strict";
|
||||
|
||||
const concurrency = require('web.concurrency');
|
||||
const Widget = require('web.Widget');
|
||||
|
||||
const { Component } = owl;
|
||||
|
||||
/**
|
||||
* DOM Test Utils
|
||||
*
|
||||
* This module defines various utility functions to help simulate DOM events.
|
||||
*
|
||||
* Note that all methods defined in this module are exported in the main
|
||||
* testUtils file.
|
||||
*/
|
||||
|
||||
//-------------------------------------------------------------------------
|
||||
// Private functions
|
||||
//-------------------------------------------------------------------------
|
||||
|
||||
// TriggerEvent helpers
|
||||
const keyboardEventBubble = args => Object.assign({}, args, { bubbles: true, keyCode: args.which });
|
||||
const mouseEventMapping = args => Object.assign({}, args, {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
clientX: args ? args.clientX || args.pageX : undefined,
|
||||
clientY: args ? args.clientY || args.pageY : undefined,
|
||||
view: window,
|
||||
});
|
||||
const mouseEventNoBubble = args => Object.assign({}, args, {
|
||||
bubbles: false,
|
||||
cancelable: false,
|
||||
clientX: args ? args.clientX || args.pageX : undefined,
|
||||
clientY: args ? args.clientY || args.pageY : undefined,
|
||||
view: window,
|
||||
});
|
||||
const touchEventMapping = args => Object.assign({}, args, {
|
||||
cancelable: true,
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
view: window,
|
||||
rotation: 0.0,
|
||||
zoom: 1.0,
|
||||
});
|
||||
const touchEventCancelMapping = args => Object.assign({}, touchEventMapping(args), {
|
||||
cancelable: false,
|
||||
});
|
||||
const noBubble = args => Object.assign({}, args, { bubbles: false });
|
||||
const onlyBubble = args => Object.assign({}, args, { bubbles: true });
|
||||
// TriggerEvent constructor/args processor mapping
|
||||
const EVENT_TYPES = {
|
||||
auxclick: { constructor: MouseEvent, processParameters: mouseEventMapping },
|
||||
click: { constructor: MouseEvent, processParameters: mouseEventMapping },
|
||||
contextmenu: { constructor: MouseEvent, processParameters: mouseEventMapping },
|
||||
dblclick: { constructor: MouseEvent, processParameters: mouseEventMapping },
|
||||
mousedown: { constructor: MouseEvent, processParameters: mouseEventMapping },
|
||||
mouseup: { constructor: MouseEvent, processParameters: mouseEventMapping },
|
||||
|
||||
mousemove: { constructor: MouseEvent, processParameters: mouseEventMapping },
|
||||
mouseenter: { constructor: MouseEvent, processParameters: mouseEventNoBubble },
|
||||
mouseleave: { constructor: MouseEvent, processParameters: mouseEventNoBubble },
|
||||
mouseover: { constructor: MouseEvent, processParameters: mouseEventMapping },
|
||||
mouseout: { constructor: MouseEvent, processParameters: mouseEventMapping },
|
||||
|
||||
focus: { constructor: FocusEvent, processParameters: noBubble },
|
||||
focusin: { constructor: FocusEvent, processParameters: onlyBubble },
|
||||
blur: { constructor: FocusEvent, processParameters: noBubble },
|
||||
|
||||
cut: { constructor: ClipboardEvent, processParameters: onlyBubble },
|
||||
copy: { constructor: ClipboardEvent, processParameters: onlyBubble },
|
||||
paste: { constructor: ClipboardEvent, processParameters: onlyBubble },
|
||||
|
||||
keydown: { constructor: KeyboardEvent, processParameters: keyboardEventBubble },
|
||||
keypress: { constructor: KeyboardEvent, processParameters: keyboardEventBubble },
|
||||
keyup: { constructor: KeyboardEvent, processParameters: keyboardEventBubble },
|
||||
|
||||
drag: { constructor: DragEvent, processParameters: onlyBubble },
|
||||
dragend: { constructor: DragEvent, processParameters: onlyBubble },
|
||||
dragenter: { constructor: DragEvent, processParameters: onlyBubble },
|
||||
dragstart: { constructor: DragEvent, processParameters: onlyBubble },
|
||||
dragleave: { constructor: DragEvent, processParameters: onlyBubble },
|
||||
dragover: { constructor: DragEvent, processParameters: onlyBubble },
|
||||
drop: { constructor: DragEvent, processParameters: onlyBubble },
|
||||
|
||||
input: { constructor: InputEvent, processParameters: onlyBubble },
|
||||
|
||||
compositionstart: { constructor: CompositionEvent, processParameters: onlyBubble },
|
||||
compositionend: { constructor: CompositionEvent, processParameters: onlyBubble },
|
||||
};
|
||||
|
||||
if (typeof TouchEvent === 'function') {
|
||||
Object.assign(EVENT_TYPES, {
|
||||
touchstart: {constructor: TouchEvent, processParameters: touchEventMapping},
|
||||
touchend: {constructor: TouchEvent, processParameters: touchEventMapping},
|
||||
touchmove: {constructor: TouchEvent, processParameters: touchEventMapping},
|
||||
touchcancel: {constructor: TouchEvent, processParameters: touchEventCancelMapping},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an object is an instance of EventTarget.
|
||||
*
|
||||
* @param {Object} node
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function _isEventTarget(node) {
|
||||
if (!node) {
|
||||
throw new Error(`Provided node is ${node}.`);
|
||||
}
|
||||
if (node instanceof window.top.EventTarget) {
|
||||
return true;
|
||||
}
|
||||
const contextWindow = node.defaultView || // document
|
||||
(node.ownerDocument && node.ownerDocument.defaultView); // iframe node
|
||||
return contextWindow && node instanceof contextWindow.EventTarget;
|
||||
}
|
||||
|
||||
//-------------------------------------------------------------------------
|
||||
// Public functions
|
||||
//-------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Click on a specified element. If the option first or last is not specified,
|
||||
* this method also check the unicity and the visibility of the target.
|
||||
*
|
||||
* @param {string|EventTarget|EventTarget[]} el (if string: it is a (jquery) selector)
|
||||
* @param {Object} [options={}] click options
|
||||
* @param {boolean} [options.allowInvisible=false] if true, clicks on the
|
||||
* element event if it is invisible
|
||||
* @param {boolean} [options.first=false] if true, clicks on the first element
|
||||
* @param {boolean} [options.last=false] if true, clicks on the last element
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async function click(el, options = {}) {
|
||||
let matches, target;
|
||||
let selectorMsg = "";
|
||||
if (typeof el === 'string') {
|
||||
el = $(el);
|
||||
}
|
||||
if (el.disabled || (el instanceof jQuery && el.get(0).disabled)) {
|
||||
throw new Error("Can't click on a disabled button");
|
||||
}
|
||||
if (_isEventTarget(el)) {
|
||||
// EventTarget
|
||||
matches = [el];
|
||||
} else {
|
||||
// Any other iterable object containing EventTarget objects (jQuery, HTMLCollection, etc.)
|
||||
matches = [...el];
|
||||
}
|
||||
|
||||
const validMatches = options.allowInvisible ?
|
||||
matches : matches.filter(t => $(t).is(':visible'));
|
||||
|
||||
if (options.first) {
|
||||
if (validMatches.length === 1) {
|
||||
throw new Error(`There should be more than one visible target ${selectorMsg}. If` +
|
||||
' you are sure that there is exactly one target, please use the ' +
|
||||
'click function instead of the clickFirst function');
|
||||
}
|
||||
target = validMatches[0];
|
||||
} else if (options.last) {
|
||||
if (validMatches.length === 1) {
|
||||
throw new Error(`There should be more than one visible target ${selectorMsg}. If` +
|
||||
' you are sure that there is exactly one target, please use the ' +
|
||||
'click function instead of the clickLast function');
|
||||
}
|
||||
target = validMatches[validMatches.length - 1];
|
||||
} else if (validMatches.length !== 1) {
|
||||
throw new Error(`Found ${validMatches.length} elements to click on, instead of 1 ${selectorMsg}`);
|
||||
} else {
|
||||
target = validMatches[0];
|
||||
}
|
||||
if (validMatches.length === 0 && matches.length > 0) {
|
||||
throw new Error(`Element to click on is not visible ${selectorMsg}`);
|
||||
}
|
||||
if (target.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
return triggerEvent(target, 'click');
|
||||
}
|
||||
|
||||
/**
|
||||
* Click on the first 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 clickFirst(el, options) {
|
||||
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
|
||||
* events, but it is enough to help test drag and drop operations with jqueryUI
|
||||
* sortable.
|
||||
*
|
||||
* @todo: remove the withTrailingClick option when the jquery update branch is
|
||||
* merged. This is not the default as of now, because handlers are triggered
|
||||
* synchronously, which is not the same as the 'reality'.
|
||||
*
|
||||
* @param {jQuery|EventTarget} $el
|
||||
* @param {jQuery|EventTarget} $to
|
||||
* @param {Object} [options]
|
||||
* @param {string|Object} [options.position='center'] target position:
|
||||
* can either be one of {'top', 'bottom', 'left', 'right'} or
|
||||
* an object with two attributes (top and left))
|
||||
* @param {boolean} [options.disableDrop=false] whether to trigger the drop action
|
||||
* @param {boolean} [options.continueMove=false] whether to trigger the
|
||||
* mousedown action (will only work after another call of this function with
|
||||
* without this option)
|
||||
* @param {boolean} [options.withTrailingClick=false] if true, this utility
|
||||
* function will also trigger a click on the target after the mouseup event
|
||||
* (this is actually what happens when a drag and drop operation is done)
|
||||
* @param {jQuery|EventTarget} [options.mouseenterTarget=undefined] target of the mouseenter event
|
||||
* @param {jQuery|EventTarget} [options.mousedownTarget=undefined] target of the mousedown event
|
||||
* @param {jQuery|EventTarget} [options.mousemoveTarget=undefined] target of the mousemove event
|
||||
* @param {jQuery|EventTarget} [options.mouseupTarget=undefined] target of the mouseup event
|
||||
* @param {jQuery|EventTarget} [options.ctrlKey=undefined] if the ctrl key should be considered pressed at the time of mouseup
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async function dragAndDrop($el, $to, options) {
|
||||
let el = null;
|
||||
if (_isEventTarget($el)) {
|
||||
el = $el;
|
||||
$el = $(el);
|
||||
}
|
||||
if (_isEventTarget($to)) {
|
||||
$to = $($to);
|
||||
}
|
||||
options = options || {};
|
||||
const position = options.position || 'center';
|
||||
const elementCenter = $el.offset();
|
||||
const toOffset = $to.offset();
|
||||
|
||||
if (typeof position === 'object') {
|
||||
toOffset.top += position.top + 1;
|
||||
toOffset.left += position.left + 1;
|
||||
} else {
|
||||
toOffset.top += $to.outerHeight() / 2;
|
||||
toOffset.left += $to.outerWidth() / 2;
|
||||
const vertical_offset = (toOffset.top < elementCenter.top) ? -1 : 1;
|
||||
if (position === 'top') {
|
||||
toOffset.top -= $to.outerHeight() / 2 + vertical_offset;
|
||||
} else if (position === 'bottom') {
|
||||
toOffset.top += $to.outerHeight() / 2 - vertical_offset;
|
||||
} else if (position === 'left') {
|
||||
toOffset.left -= $to.outerWidth() / 2;
|
||||
} else if (position === 'right') {
|
||||
toOffset.left += $to.outerWidth() / 2;
|
||||
}
|
||||
}
|
||||
|
||||
if ($to[0].ownerDocument !== document) {
|
||||
// we are in an iframe
|
||||
const bound = $('iframe')[0].getBoundingClientRect();
|
||||
toOffset.left += bound.left;
|
||||
toOffset.top += bound.top;
|
||||
}
|
||||
await triggerEvent(options.mouseenterTarget || el || $el, 'mouseover', {}, true);
|
||||
if (!(options.continueMove)) {
|
||||
elementCenter.left += $el.outerWidth() / 2;
|
||||
elementCenter.top += $el.outerHeight() / 2;
|
||||
|
||||
await triggerEvent(options.mousedownTarget || el || $el, 'mousedown', {
|
||||
which: 1,
|
||||
pageX: elementCenter.left,
|
||||
pageY: elementCenter.top
|
||||
}, true);
|
||||
}
|
||||
await triggerEvent(options.mousemoveTarget || el || $el, 'mousemove', {
|
||||
which: 1,
|
||||
pageX: toOffset.left,
|
||||
pageY: toOffset.top
|
||||
}, true);
|
||||
|
||||
if (!options.disableDrop) {
|
||||
await triggerEvent(options.mouseupTarget || el || $el, 'mouseup', {
|
||||
which: 1,
|
||||
pageX: toOffset.left,
|
||||
pageY: toOffset.top,
|
||||
ctrlKey: options.ctrlKey,
|
||||
}, true);
|
||||
if (options.withTrailingClick) {
|
||||
await triggerEvent(options.mouseupTarget || el || $el, 'click', {}, true);
|
||||
}
|
||||
} else {
|
||||
// It's impossible to drag another element when one is already
|
||||
// being dragged. So it's necessary to finish the drop when the test is
|
||||
// over otherwise it's impossible for the next tests to drag and
|
||||
// drop elements.
|
||||
$el.on('remove', function () {
|
||||
triggerEvent($el, 'mouseup', {}, true);
|
||||
});
|
||||
}
|
||||
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
|
||||
*
|
||||
* This is useful to guarantee that OWL has had the time to render
|
||||
*
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async function returnAfterNextAnimationFrame() {
|
||||
await concurrency.delay(0);
|
||||
await new Promise(resolve => {
|
||||
window.requestAnimationFrame(resolve);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger an event on the specified target.
|
||||
* This function will dispatch a native event to an EventTarget or a
|
||||
* jQuery event to a jQuery object. This behaviour can be overridden by the
|
||||
* jquery option.
|
||||
*
|
||||
* @param {EventTarget|EventTarget[]} el
|
||||
* @param {string} eventType event type
|
||||
* @param {Object} [eventAttrs] event attributes
|
||||
* on a jQuery element with the `$.fn.trigger` function
|
||||
* @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) {
|
||||
let matches;
|
||||
let selectorMsg = "";
|
||||
if (_isEventTarget(el)) {
|
||||
matches = [el];
|
||||
} else {
|
||||
matches = [...el];
|
||||
}
|
||||
|
||||
if (matches.length !== 1) {
|
||||
throw new Error(`Found ${matches.length} elements to trigger "${eventType}" on, instead of 1 ${selectorMsg}`);
|
||||
}
|
||||
|
||||
const target = matches[0];
|
||||
let event;
|
||||
|
||||
if (!EVENT_TYPES[eventType] && !EVENT_TYPES[eventType.type]) {
|
||||
event = new Event(eventType, Object.assign({}, eventAttrs, { bubbles: true }));
|
||||
} else {
|
||||
if (typeof eventType === "object") {
|
||||
const { constructor, processParameters } = EVENT_TYPES[eventType.type];
|
||||
const eventParameters = processParameters(eventType);
|
||||
event = new constructor(eventType.type, eventParameters);
|
||||
} else {
|
||||
const { constructor, processParameters } = EVENT_TYPES[eventType];
|
||||
event = new constructor(eventType, processParameters(eventAttrs));
|
||||
}
|
||||
}
|
||||
target.dispatchEvent(event);
|
||||
return fast ? undefined : returnAfterNextAnimationFrame();
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger multiple events on the specified element.
|
||||
*
|
||||
* @param {EventTarget|EventTarget[]} el
|
||||
* @param {string[]} events the events you want to trigger
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async function triggerEvents(el, events) {
|
||||
if (el instanceof jQuery) {
|
||||
if (el.length !== 1) {
|
||||
throw new Error(`target has length ${el.length} instead of 1`);
|
||||
}
|
||||
}
|
||||
if (typeof events === 'string') {
|
||||
events = [events];
|
||||
}
|
||||
|
||||
for (let e = 0; e < events.length; e++) {
|
||||
await triggerEvent(el, events[e]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
click,
|
||||
clickFirst,
|
||||
clickLast,
|
||||
dragAndDrop,
|
||||
findItem,
|
||||
getNode,
|
||||
openDatepicker,
|
||||
returnAfterNextAnimationFrame,
|
||||
triggerEvent,
|
||||
triggerEvents,
|
||||
triggerKeypressEvent,
|
||||
triggerMouseEvent,
|
||||
triggerPositionalMouseEvent,
|
||||
triggerPositionalTapEvents,
|
||||
};
|
||||
});
|
||||
|
|
@ -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,
|
||||
};
|
||||
|
||||
});
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
odoo.define('web.testUtilsTests', function (require) {
|
||||
"use strict";
|
||||
|
||||
var testUtils = require('web.test_utils');
|
||||
|
||||
QUnit.module('web', {}, function () {
|
||||
QUnit.module('testUtils', {}, function () {
|
||||
|
||||
QUnit.module('patch date');
|
||||
|
||||
QUnit.test('new date', function (assert) {
|
||||
assert.expect(5);
|
||||
const unpatchDate = testUtils.mock.patchDate(2018, 9, 23, 14, 50, 0);
|
||||
|
||||
const date = new Date();
|
||||
|
||||
assert.strictEqual(date.getFullYear(), 2018);
|
||||
assert.strictEqual(date.getMonth(), 9);
|
||||
assert.strictEqual(date.getDate(), 23);
|
||||
assert.strictEqual(date.getHours(), 14);
|
||||
assert.strictEqual(date.getMinutes(), 50);
|
||||
unpatchDate();
|
||||
});
|
||||
|
||||
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');
|
||||
unpatchDate();
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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,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);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
|
@ -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
|
|
@ -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>');
|
||||
});
|
||||
})
|
||||
});
|
||||
});
|
||||
|
|
@ -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"]);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
|
@ -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;
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
|
@ -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
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,380 +0,0 @@
|
|||
odoo.define('web.kanban_model_tests', function (require) {
|
||||
"use strict";
|
||||
|
||||
var KanbanModel = require('web.KanbanModel');
|
||||
var testUtils = require('web.test_utils');
|
||||
|
||||
var createModel = testUtils.createModel;
|
||||
|
||||
QUnit.module('LegacyViews', {
|
||||
beforeEach: function () {
|
||||
this.data = {
|
||||
partner: {
|
||||
fields: {
|
||||
active: {string: "Active", type: "boolean", default: true},
|
||||
display_name: {string: "STRING", type: 'char'},
|
||||
foo: {string: "Foo", type: 'char'},
|
||||
bar: {string: "Bar", type: 'integer'},
|
||||
qux: {string: "Qux", type: 'many2one', relation: 'partner'},
|
||||
product_id: {string: "Favorite product", type: 'many2one', relation: 'product'},
|
||||
product_ids: {string: "Favorite products", type: 'one2many', relation: 'product'},
|
||||
category: {string: "Category M2M", type: 'many2many', relation: 'partner_type'},
|
||||
},
|
||||
records: [
|
||||
{id: 1, foo: 'blip', bar: 1, product_id: 37, category: [12], display_name: "first partner"},
|
||||
{id: 2, foo: 'gnap', bar: 2, product_id: 41, display_name: "second partner"},
|
||||
],
|
||||
onchanges: {},
|
||||
},
|
||||
product: {
|
||||
fields: {
|
||||
name: {string: "Product Name", type: "char"}
|
||||
},
|
||||
records: [
|
||||
{id: 37, display_name: "xphone"},
|
||||
{id: 41, display_name: "xpad"}
|
||||
]
|
||||
},
|
||||
partner_type: {
|
||||
fields: {
|
||||
display_name: {string: "Partner Type", type: "char"}
|
||||
},
|
||||
records: [
|
||||
{id: 12, display_name: "gold"},
|
||||
{id: 14, display_name: "silver"},
|
||||
{id: 15, display_name: "bronze"}
|
||||
]
|
||||
},
|
||||
};
|
||||
|
||||
// add related fields to category.
|
||||
this.data.partner.fields.category.relatedFields =
|
||||
$.extend(true, {}, this.data.partner_type.fields);
|
||||
this.params = {
|
||||
fields: this.data.partner.fields,
|
||||
limit: 40,
|
||||
modelName: 'partner',
|
||||
openGroupByDefault: true,
|
||||
viewType: 'kanban',
|
||||
};
|
||||
},
|
||||
}, function () {
|
||||
|
||||
QUnit.module('KanbanModel (legacy)');
|
||||
|
||||
QUnit.test('load grouped + add a new group', async function (assert) {
|
||||
var done = assert.async();
|
||||
assert.expect(22);
|
||||
|
||||
var calledRoutes = {};
|
||||
var model = await createModel({
|
||||
Model: KanbanModel,
|
||||
data: this.data,
|
||||
mockRPC: function (route) {
|
||||
if (!(route in calledRoutes)) {
|
||||
calledRoutes[route] = 1;
|
||||
} else {
|
||||
calledRoutes[route]++;
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
|
||||
var params = _.extend(this.params, {
|
||||
groupedBy: ['product_id'],
|
||||
fieldNames: ['foo'],
|
||||
});
|
||||
|
||||
model.load(params).then(async function (resultID) {
|
||||
// various checks on the load result
|
||||
var state = model.get(resultID);
|
||||
assert.ok(_.isEqual(state.groupedBy, ['product_id']), 'should be grouped by "product_id"');
|
||||
assert.strictEqual(state.data.length, 2, 'should have found 2 groups');
|
||||
assert.strictEqual(state.count, 2, 'both groups contain one record');
|
||||
var xphoneGroup = _.findWhere(state.data, {res_id: 37});
|
||||
assert.strictEqual(xphoneGroup.model, 'partner', 'group should have correct model');
|
||||
assert.ok(xphoneGroup, 'should have a group for res_id 37');
|
||||
assert.ok(xphoneGroup.isOpen, '"xphone" group should be open');
|
||||
assert.strictEqual(xphoneGroup.value, 'xphone', 'group 37 should be "xphone"');
|
||||
assert.strictEqual(xphoneGroup.count, 1, '"xphone" group should have one record');
|
||||
assert.strictEqual(xphoneGroup.data.length, 1, 'should have fetched the records in the group');
|
||||
assert.ok(_.isEqual(xphoneGroup.domain[0], ['product_id', '=', 37]),
|
||||
'domain should be correct');
|
||||
assert.strictEqual(xphoneGroup.limit, 40, 'limit in a group should be 40');
|
||||
|
||||
// add a new group
|
||||
await model.createGroup('xpod', resultID);
|
||||
state = model.get(resultID);
|
||||
assert.strictEqual(state.data.length, 3, 'should now have 3 groups');
|
||||
assert.strictEqual(state.count, 2, 'there are still 2 records');
|
||||
var xpodGroup = _.findWhere(state.data, {value: 'xpod'});
|
||||
assert.strictEqual(xpodGroup.model, 'partner', 'new group should have correct model');
|
||||
assert.ok(xpodGroup, 'should have an "xpod" group');
|
||||
assert.ok(xpodGroup.isOpen, 'new group should be open');
|
||||
assert.strictEqual(xpodGroup.count, 0, 'new group should contain no record');
|
||||
assert.ok(_.isEqual(xpodGroup.domain[0], ['product_id', '=', xpodGroup.res_id]),
|
||||
'new group should have correct domain');
|
||||
|
||||
// check the rpcs done
|
||||
assert.strictEqual(Object.keys(calledRoutes).length, 3, 'three different routes have been called');
|
||||
var nbReadGroups = calledRoutes['/web/dataset/call_kw/partner/web_read_group'];
|
||||
var nbSearchRead = calledRoutes['/web/dataset/search_read'];
|
||||
var nbNameCreate = calledRoutes['/web/dataset/call_kw/product/name_create'];
|
||||
assert.strictEqual(nbReadGroups, 1, 'should have done 1 read_group');
|
||||
assert.strictEqual(nbSearchRead, 2, 'should have done 2 search_read');
|
||||
assert.strictEqual(nbNameCreate, 1, 'should have done 1 name_create');
|
||||
model.destroy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test('archive/restore a column', async function (assert) {
|
||||
var done = assert.async();
|
||||
assert.expect(4);
|
||||
|
||||
var model = await createModel({
|
||||
Model: KanbanModel,
|
||||
data: this.data,
|
||||
mockRPC: function (route, args) {
|
||||
if (route === '/web/dataset/call_kw/partner/action_archive') {
|
||||
this.data.partner.records[0].active = false;
|
||||
return Promise.resolve();
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
|
||||
var params = _.extend(this.params, {
|
||||
groupedBy: ['product_id'],
|
||||
fieldNames: ['foo'],
|
||||
});
|
||||
|
||||
model.load(params).then(async function (resultID) {
|
||||
var state = model.get(resultID);
|
||||
var xphoneGroup = _.findWhere(state.data, {res_id: 37});
|
||||
var xpadGroup = _.findWhere(state.data, {res_id: 41});
|
||||
assert.strictEqual(xphoneGroup.count, 1, 'xphone group has one record');
|
||||
assert.strictEqual(xpadGroup.count, 1, 'xpad group has one record');
|
||||
|
||||
// archive the column 'xphone'
|
||||
var recordIDs = xphoneGroup.data.map(record => record.res_id);
|
||||
await model.actionArchive(recordIDs, xphoneGroup.id);
|
||||
state = model.get(resultID);
|
||||
xphoneGroup = _.findWhere(state.data, {res_id: 37});
|
||||
assert.strictEqual(xphoneGroup.count, 0, 'xphone group has no record anymore');
|
||||
xpadGroup = _.findWhere(state.data, {res_id: 41});
|
||||
assert.strictEqual(xpadGroup.count, 1, 'xpad group still has one record');
|
||||
model.destroy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test('kanban model does not allow nested groups', async function (assert) {
|
||||
var done = assert.async();
|
||||
assert.expect(2);
|
||||
|
||||
var model = await createModel({
|
||||
Model: KanbanModel,
|
||||
data: this.data,
|
||||
mockRPC: function (route, args) {
|
||||
if (args.method === 'web_read_group') {
|
||||
assert.deepEqual(args.kwargs.groupby, ['product_id'],
|
||||
"the second level of groupBy should have been removed");
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
|
||||
var params = _.extend(this.params, {
|
||||
groupedBy: ['product_id', 'qux'],
|
||||
fieldNames: ['foo'],
|
||||
});
|
||||
|
||||
model.load(params).then(function (resultID) {
|
||||
var state = model.get(resultID);
|
||||
|
||||
assert.deepEqual(state.groupedBy, ['product_id'],
|
||||
"the second level of groupBy should have been removed");
|
||||
|
||||
model.destroy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test('resequence columns and records', async function (assert) {
|
||||
var done = assert.async();
|
||||
assert.expect(8);
|
||||
|
||||
this.data.product.fields.sequence = {string: "Sequence", type: "integer"};
|
||||
this.data.partner.fields.sequence = {string: "Sequence", type: "integer"};
|
||||
this.data.partner.records.push({id: 3, foo: 'aaa', product_id: 37});
|
||||
|
||||
var nbReseq = 0;
|
||||
var model = await createModel({
|
||||
Model: KanbanModel,
|
||||
data: this.data,
|
||||
mockRPC: function (route, args) {
|
||||
if (route === '/web/dataset/resequence') {
|
||||
nbReseq++;
|
||||
if (nbReseq === 1) { // resequencing columns
|
||||
assert.deepEqual(args.ids, [41, 37],
|
||||
"ids should be correct");
|
||||
assert.strictEqual(args.model, 'product',
|
||||
"model should be correct");
|
||||
} else if (nbReseq === 2) { // resequencing records
|
||||
assert.deepEqual(args.ids, [3, 1],
|
||||
"ids should be correct");
|
||||
assert.strictEqual(args.model, 'partner',
|
||||
"model should be correct");
|
||||
}
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
var params = _.extend(this.params, {
|
||||
groupedBy: ['product_id'],
|
||||
fieldNames: ['foo'],
|
||||
});
|
||||
|
||||
model.load(params)
|
||||
.then(function (stateID) {
|
||||
var state = model.get(stateID);
|
||||
assert.strictEqual(state.data[0].res_id, 37,
|
||||
"first group should be res_id 37");
|
||||
|
||||
// resequence columns
|
||||
return model.resequence('product', [41, 37], stateID);
|
||||
})
|
||||
.then(function (stateID) {
|
||||
var state = model.get(stateID);
|
||||
assert.strictEqual(state.data[0].res_id, 41,
|
||||
"first group should be res_id 41 after resequencing");
|
||||
assert.strictEqual(state.data[1].data[0].res_id, 1,
|
||||
"first record should be res_id 1");
|
||||
|
||||
// resequence records
|
||||
return model.resequence('partner', [3, 1], state.data[1].id);
|
||||
})
|
||||
.then(function (groupID) {
|
||||
var group = model.get(groupID);
|
||||
assert.strictEqual(group.data[0].res_id, 3,
|
||||
"first record should be res_id 3 after resequencing");
|
||||
|
||||
model.destroy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test('add record to group', async function (assert) {
|
||||
var done = assert.async();
|
||||
assert.expect(8);
|
||||
|
||||
var self = this;
|
||||
var model = await createModel({
|
||||
Model: KanbanModel,
|
||||
data: this.data,
|
||||
});
|
||||
var params = _.extend(this.params, {
|
||||
groupedBy: ['product_id'],
|
||||
fieldNames: ['foo'],
|
||||
});
|
||||
|
||||
model.load(params).then(function (stateID) {
|
||||
self.data.partner.records.push({id: 3, foo: 'new record', product_id: 37});
|
||||
|
||||
var state = model.get(stateID);
|
||||
assert.deepEqual(state.res_ids, [1, 2],
|
||||
"state should have the correct res_ids");
|
||||
assert.strictEqual(state.count, 2,
|
||||
"state should have the correct count");
|
||||
assert.strictEqual(state.data[0].count, 1,
|
||||
"first group should contain one record");
|
||||
|
||||
return model.addRecordToGroup(state.data[0].id, 3).then(function () {
|
||||
var state = model.get(stateID);
|
||||
assert.deepEqual(state.res_ids, [3, 1, 2],
|
||||
"state should have the correct res_ids");
|
||||
assert.strictEqual(state.count, 3,
|
||||
"state should have the correct count");
|
||||
assert.deepEqual(state.data[0].res_ids, [3, 1],
|
||||
"new record's id should have been added to the res_ids");
|
||||
assert.strictEqual(state.data[0].count, 2,
|
||||
"first group should now contain two records");
|
||||
assert.strictEqual(state.data[0].data[0].data.foo, 'new record',
|
||||
"new record should have been fetched");
|
||||
});
|
||||
}).then(function() {
|
||||
model.destroy();
|
||||
done();
|
||||
})
|
||||
|
||||
});
|
||||
|
||||
QUnit.test('call get (raw: true) before loading x2many data', async function (assert) {
|
||||
// Sometimes, get can be called on a datapoint that is currently being
|
||||
// reloaded, and thus in a partially updated state (e.g. in a kanban
|
||||
// view, the user interacts with the searchview, and before the view is
|
||||
// fully reloaded, it clicks on CREATE). Ideally, this shouldn't happen,
|
||||
// but with the sync API of get, we can't change that easily. So at most,
|
||||
// we can ensure that it doesn't crash. Moreover, sensitive functions
|
||||
// requesting the state for more precise information that, e.g., the
|
||||
// count, can do that in the mutex to ensure that the state isn't
|
||||
// currently being reloaded.
|
||||
// In this test, we have a grouped kanban view with a one2many, whose
|
||||
// relational data is loaded in batch, once for all groups. We call get
|
||||
// when the search_read for the first group has returned, but not the
|
||||
// second (and thus, the read of the one2many hasn't started yet).
|
||||
// Note: this test can be removed as soon as search_reads are performed
|
||||
// alongside read_group.
|
||||
var done = assert.async();
|
||||
assert.expect(2);
|
||||
|
||||
this.data.partner.records[1].product_ids = [37, 41];
|
||||
this.params.fieldsInfo = {
|
||||
kanban: {
|
||||
product_ids: {
|
||||
fieldsInfo: {
|
||||
default: { display_name: {}, color: {} },
|
||||
},
|
||||
relatedFields: this.data.product.fields,
|
||||
viewType: 'default',
|
||||
},
|
||||
},
|
||||
};
|
||||
this.params.viewType = 'kanban';
|
||||
this.params.groupedBy = ['foo'];
|
||||
|
||||
var block;
|
||||
var def = testUtils.makeTestPromise();
|
||||
var model = await createModel({
|
||||
Model: KanbanModel,
|
||||
data: this.data,
|
||||
mockRPC: function (route) {
|
||||
var result = this._super.apply(this, arguments);
|
||||
if (route === '/web/dataset/search_read' && block) {
|
||||
block = false;
|
||||
return Promise.all([def]).then(_.constant(result));
|
||||
}
|
||||
return result;
|
||||
},
|
||||
});
|
||||
|
||||
model.load(this.params).then(function (handle) {
|
||||
block = true;
|
||||
model.reload(handle, {});
|
||||
|
||||
var state = model.get(handle, {raw: true});
|
||||
assert.strictEqual(state.count, 2);
|
||||
|
||||
def.resolve();
|
||||
|
||||
state = model.get(handle, {raw: true});
|
||||
assert.strictEqual(state.count, 2);
|
||||
}).then(function() {
|
||||
model.destroy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,114 +0,0 @@
|
|||
/* global Benchmark */
|
||||
odoo.define('web.list_benchmarks', function (require) {
|
||||
"use strict";
|
||||
|
||||
const ListView = require('web.ListView');
|
||||
const { createView } = require('web.test_utils');
|
||||
|
||||
QUnit.module('List 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, cb) {
|
||||
const data = this.data;
|
||||
const arch = this.arch;
|
||||
return new Promise(resolve => {
|
||||
new Benchmark.Suite({})
|
||||
.add('list', {
|
||||
defer: true,
|
||||
fn: async (deferred) => {
|
||||
const list = await createView({
|
||||
View: ListView,
|
||||
model: 'foo',
|
||||
data,
|
||||
arch,
|
||||
});
|
||||
if (cb) {
|
||||
cb(list);
|
||||
}
|
||||
list.destroy();
|
||||
deferred.resolve();
|
||||
},
|
||||
})
|
||||
.on('cycle', event => {
|
||||
assert.ok(true, String(event.target));
|
||||
})
|
||||
.on('complete', resolve)
|
||||
.run({ async: true });
|
||||
});
|
||||
};
|
||||
}
|
||||
}, function () {
|
||||
QUnit.test('simple readonly list with 2 rows and 2 fields', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
this.arch = '<tree><field name="foo"/><field name="int_field"/></tree>';
|
||||
return this.run(assert);
|
||||
});
|
||||
|
||||
QUnit.test('simple readonly list with 200 rows and 2 fields', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
for (let i = 2; i < 200; i++) {
|
||||
this.data.foo.records.push({
|
||||
id: i,
|
||||
foo: "automated data",
|
||||
int_field: 10 * i,
|
||||
});
|
||||
}
|
||||
this.arch = '<tree><field name="foo"/><field name="int_field"/></tree>';
|
||||
return this.run(assert);
|
||||
});
|
||||
|
||||
QUnit.test('simple readonly list with 200 rows and 2 fields (with widgets)', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
for (let i = 2; i < 200; i++) {
|
||||
this.data.foo.records.push({
|
||||
id: i,
|
||||
foo: "automated data",
|
||||
int_field: 10 * i,
|
||||
});
|
||||
}
|
||||
this.arch = '<tree><field name="foo" widget="char"/><field name="int_field" widget="integer"/></tree>';
|
||||
return this.run(assert);
|
||||
});
|
||||
|
||||
QUnit.test('editable list with 200 rows 4 fields', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
for (let i = 2; i < 200; i++) {
|
||||
this.data.foo.records.push({
|
||||
id: i,
|
||||
foo: "automated data",
|
||||
int_field: 10 * i,
|
||||
bar: i % 2 === 0,
|
||||
});
|
||||
}
|
||||
this.arch = `
|
||||
<tree editable="bottom">
|
||||
<field name="foo" attrs="{'readonly': [['bar', '=', True]]}"/>
|
||||
<field name="int_field"/>
|
||||
<field name="bar"/>
|
||||
<field name="qux"/>
|
||||
</tree>`;
|
||||
return this.run(assert, list => {
|
||||
list.$('.o_list_button_add').click();
|
||||
list.$('.o_list_button_discard').click();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,75 +0,0 @@
|
|||
odoo.define('web.qweb_view_tests', function (require) {
|
||||
"use strict";
|
||||
|
||||
const utils = require('web.test_utils');
|
||||
const { createWebClient, doAction } = require('@web/../tests/webclient/helpers');
|
||||
const { getFixture } = require("@web/../tests/helpers/utils");
|
||||
|
||||
QUnit.module("Views", {
|
||||
|
||||
}, function () {
|
||||
QUnit.module("QWeb");
|
||||
QUnit.test("basic", async function (assert) {
|
||||
assert.expect(14);
|
||||
|
||||
const serverData = {
|
||||
models: {
|
||||
test: {
|
||||
fields: {},
|
||||
records: [],
|
||||
}
|
||||
},
|
||||
views: {
|
||||
'test,5,qweb': '<div id="xxx"><t t-esc="ok"/></div>',
|
||||
'test,false,search': '<search/>'
|
||||
},
|
||||
};
|
||||
|
||||
const mockRPC = (route, args) => {
|
||||
if (/^\/web\/dataset\/call_kw/.test(route)) {
|
||||
switch (_.str.sprintf('%(model)s.%(method)s', args)) {
|
||||
case 'test.qweb_render_view':
|
||||
assert.step('fetch');
|
||||
assert.equal(args.kwargs.view_id, 5);
|
||||
return Promise.resolve(
|
||||
'<div>foo' +
|
||||
'<div data-model="test" data-method="wheee" data-id="42" data-other="5">' +
|
||||
'<a type="toggle" class="fa fa-caret-right">Unfold</a>' +
|
||||
'</div>' +
|
||||
'</div>'
|
||||
);
|
||||
case 'test.wheee':
|
||||
assert.step('unfold');
|
||||
assert.deepEqual(args.args, [42]);
|
||||
assert.deepEqual(args.kwargs, { other: 5, context: {} });
|
||||
return Promise.resolve('<div id="sub">ok</div>');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const target = getFixture();
|
||||
const webClient = await createWebClient({ serverData, mockRPC});
|
||||
|
||||
let resolved = false;
|
||||
const doActionProm = doAction(webClient, {
|
||||
type: 'ir.actions.act_window',
|
||||
views: [[false, 'qweb']],
|
||||
res_model: 'test',
|
||||
}).then(function () { resolved = true; });
|
||||
assert.ok(!resolved, "Action cannot be resolved synchronously");
|
||||
|
||||
await doActionProm;
|
||||
assert.ok(resolved, "Action is resolved asynchronously");
|
||||
|
||||
const content = target.querySelector('.o_content');
|
||||
assert.ok(/^\s*foo/.test(content.textContent));
|
||||
await utils.dom.click(content.querySelector('[type=toggle]'));
|
||||
assert.equal(content.querySelector('div#sub').textContent, 'ok', 'should have unfolded the sub-item');
|
||||
await utils.dom.click(content.querySelector('[type=toggle]'));
|
||||
assert.containsNone(content, "div#sub");
|
||||
await utils.dom.click(content.querySelector('[type=toggle]'));
|
||||
|
||||
assert.verifySteps(['fetch', 'unfold', 'unfold']);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,486 +0,0 @@
|
|||
odoo.define('web.sample_server_tests', function (require) {
|
||||
"use strict";
|
||||
|
||||
const SampleServer = require('web.SampleServer');
|
||||
const session = require('web.session');
|
||||
const { mock } = require('web.test_utils');
|
||||
|
||||
const {
|
||||
MAIN_RECORDSET_SIZE, SEARCH_READ_LIMIT, // Limits
|
||||
SAMPLE_COUNTRIES, SAMPLE_PEOPLE, SAMPLE_TEXTS, // Text values
|
||||
MAX_COLOR_INT, MAX_FLOAT, MAX_INTEGER, MAX_MONETARY, // Number values
|
||||
SUB_RECORDSET_SIZE, // Records sise
|
||||
} = SampleServer;
|
||||
|
||||
/**
|
||||
* Transforms random results into deterministic ones.
|
||||
*/
|
||||
class DeterministicSampleServer extends SampleServer {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.arrayElCpt = 0;
|
||||
this.boolCpt = 0;
|
||||
this.subRecordIdCpt = 0;
|
||||
}
|
||||
_getRandomArrayEl(array) {
|
||||
return array[this.arrayElCpt++ % array.length];
|
||||
}
|
||||
_getRandomBool() {
|
||||
return Boolean(this.boolCpt++ % 2);
|
||||
}
|
||||
_getRandomSubRecordId() {
|
||||
return (this.subRecordIdCpt++ % SUB_RECORDSET_SIZE) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
QUnit.module("Sample Server (legacy)", {
|
||||
beforeEach() {
|
||||
this.fields = {
|
||||
'res.users': {
|
||||
display_name: { string: "Name", type: 'char' },
|
||||
name: { string: "Reference", type: 'char' },
|
||||
email: { string: "Email", type: 'char' },
|
||||
phone_number: { string: "Phone number", type: 'char' },
|
||||
brol_machin_url_truc: { string: "URL", type: 'char' },
|
||||
urlemailphone: { string: "Whatever", type: 'char' },
|
||||
active: { string: "Active", type: 'boolean' },
|
||||
is_alive: { string: "Is alive", type: 'boolean' },
|
||||
description: { string: "Description", type: 'text' },
|
||||
birthday: { string: "Birthday", type: 'date' },
|
||||
arrival_date: { string: "Date of arrival", type: 'datetime' },
|
||||
height: { string: "Height", type: 'float' },
|
||||
color: { string: "Color", type: 'integer' },
|
||||
age: { string: "Age", type: 'integer' },
|
||||
salary: { string: "Salary", type: 'monetary' },
|
||||
currency: { string: "Currency", type: 'many2one', relation: 'res.currency' },
|
||||
manager_id: { string: "Manager", type: 'many2one', relation: 'res.users' },
|
||||
cover_image_id: { string: "Cover Image", type: 'many2one', relation: 'ir.attachment' },
|
||||
managed_ids: { string: "Managing", type: 'one2many', relation: 'res.users' },
|
||||
tag_ids: { string: "Tags", type: 'many2many', relation: 'tag' },
|
||||
type: { string: "Type", type: 'selection', selection: [
|
||||
['client', "Client"], ['partner', "Partner"], ['employee', "Employee"]
|
||||
] },
|
||||
},
|
||||
'res.country': {
|
||||
display_name: { string: "Name", type: 'char' },
|
||||
},
|
||||
'hobbit': {
|
||||
display_name: { string: "Name", type: 'char' },
|
||||
profession: { string: "Profession", type: 'selection', selection: [
|
||||
['gardener', "Gardener"], ['brewer', "Brewer"], ['adventurer', "Adventurer"]
|
||||
] },
|
||||
age: { string: "Age", type: 'integer' },
|
||||
},
|
||||
'ir.attachment': {
|
||||
display_name: { string: "Name", type: 'char' },
|
||||
}
|
||||
};
|
||||
},
|
||||
}, function () {
|
||||
|
||||
QUnit.module("Basic behaviour");
|
||||
|
||||
QUnit.test("Sample data: people type + all field names", async function (assert) {
|
||||
assert.expect(26);
|
||||
|
||||
mock.patch(session, {
|
||||
company_currency_id: 4,
|
||||
});
|
||||
|
||||
const allFieldNames = Object.keys(this.fields['res.users']);
|
||||
const server = new DeterministicSampleServer('res.users', this.fields['res.users']);
|
||||
const { records } = await server.mockRpc({
|
||||
method: '/web/dataset/search_read',
|
||||
model: 'res.users',
|
||||
fields: allFieldNames,
|
||||
});
|
||||
const rec = records[0];
|
||||
|
||||
function assertFormat(fieldName, regex) {
|
||||
if (regex instanceof RegExp) {
|
||||
assert.ok(
|
||||
regex.test(rec[fieldName].toString()),
|
||||
`Field "${fieldName}" has the correct format`
|
||||
);
|
||||
} else {
|
||||
assert.strictEqual(
|
||||
typeof rec[fieldName], regex,
|
||||
`Field "${fieldName}" is of type ${regex}`
|
||||
);
|
||||
}
|
||||
}
|
||||
function assertBetween(fieldName, min, max) {
|
||||
const val = rec[fieldName];
|
||||
assert.ok(
|
||||
min <= val && val < max && parseInt(val, 10) === val,
|
||||
`Field "${fieldName}" should be an integer between ${min} and ${max}: ${val}`
|
||||
);
|
||||
}
|
||||
|
||||
// Basic fields
|
||||
assert.ok(SAMPLE_PEOPLE.includes(rec.display_name));
|
||||
assert.ok(SAMPLE_PEOPLE.includes(rec.name));
|
||||
assert.strictEqual(rec.email,
|
||||
`${rec.display_name.replace(/ /, ".").toLowerCase()}@sample.demo`
|
||||
);
|
||||
assertFormat('phone_number', /\+1 555 754 000\d/);
|
||||
assertFormat('brol_machin_url_truc', /http:\/\/sample\d\.com/);
|
||||
assert.strictEqual(rec.urlemailphone, false);
|
||||
assert.strictEqual(rec.active, true);
|
||||
assertFormat('is_alive', 'boolean');
|
||||
assert.ok(SAMPLE_TEXTS.includes(rec.description));
|
||||
assertFormat('birthday', /\d{4}-\d{2}-\d{2}/);
|
||||
assertFormat('arrival_date', /\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/);
|
||||
assert.ok(rec.height >= 0 && rec.height <= MAX_FLOAT, "Field height should be between 0 and 100");
|
||||
assertBetween('color', 0, MAX_COLOR_INT);
|
||||
assertBetween('age', 0, MAX_INTEGER);
|
||||
assertBetween('salary', 0, MAX_MONETARY);
|
||||
|
||||
// check float field have 2 decimal rounding
|
||||
assert.strictEqual(rec.height, parseFloat(parseFloat(rec.height).toFixed(2)));
|
||||
|
||||
const selectionValues = this.fields['res.users'].type.selection.map(
|
||||
(sel) => sel[0]
|
||||
);
|
||||
assert.ok(selectionValues.includes(rec.type));
|
||||
|
||||
// Relational fields
|
||||
assert.strictEqual(rec.currency[0], 4);
|
||||
// Currently we expect the currency name to be a latin string, which
|
||||
// is not important; in most case we only need the ID. The following
|
||||
// assertion can be removed if needed.
|
||||
assert.ok(SAMPLE_TEXTS.includes(rec.currency[1]));
|
||||
|
||||
assert.strictEqual(typeof rec.manager_id[0], 'number');
|
||||
assert.ok(SAMPLE_PEOPLE.includes(rec.manager_id[1]));
|
||||
|
||||
assert.strictEqual(rec.cover_image_id, false);
|
||||
|
||||
assert.strictEqual(rec.managed_ids.length, 2);
|
||||
assert.ok(rec.managed_ids.every(
|
||||
(id) => typeof id === 'number')
|
||||
);
|
||||
|
||||
assert.strictEqual(rec.tag_ids.length, 2);
|
||||
assert.ok(rec.tag_ids.every(
|
||||
(id) => typeof id === 'number')
|
||||
);
|
||||
|
||||
mock.unpatch(session);
|
||||
});
|
||||
|
||||
QUnit.test("Sample data: country type", async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
const server = new DeterministicSampleServer('res.country', this.fields['res.country']);
|
||||
const { records } = await server.mockRpc({
|
||||
method: '/web/dataset/search_read',
|
||||
model: 'res.country',
|
||||
fields: ['display_name'],
|
||||
});
|
||||
|
||||
assert.ok(SAMPLE_COUNTRIES.includes(records[0].display_name));
|
||||
});
|
||||
|
||||
QUnit.test("Sample data: any type", async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
const server = new DeterministicSampleServer('hobbit', this.fields.hobbit);
|
||||
|
||||
const { records } = await server.mockRpc({
|
||||
method: '/web/dataset/search_read',
|
||||
model: 'hobbit',
|
||||
fields: ['display_name'],
|
||||
});
|
||||
|
||||
assert.ok(SAMPLE_TEXTS.includes(records[0].display_name));
|
||||
});
|
||||
|
||||
QUnit.module("RPC calls");
|
||||
|
||||
QUnit.test("Send 'search_read' RPC: valid field names", async function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
const server = new DeterministicSampleServer('hobbit', this.fields.hobbit);
|
||||
|
||||
const result = await server.mockRpc({
|
||||
method: '/web/dataset/search_read',
|
||||
model: 'hobbit',
|
||||
fields: ['display_name'],
|
||||
});
|
||||
|
||||
assert.deepEqual(
|
||||
Object.keys(result.records[0]),
|
||||
['id', 'display_name']
|
||||
);
|
||||
assert.strictEqual(result.length, SEARCH_READ_LIMIT);
|
||||
assert.ok(/\w+/.test(result.records[0].display_name),
|
||||
"Display name has been mocked"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("Send 'search_read' RPC: invalid field names", async function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
const server = new DeterministicSampleServer('hobbit', this.fields.hobbit);
|
||||
|
||||
const result = await server.mockRpc({
|
||||
method: '/web/dataset/search_read',
|
||||
model: 'hobbit',
|
||||
fields: ['name'],
|
||||
});
|
||||
|
||||
assert.deepEqual(
|
||||
Object.keys(result.records[0]),
|
||||
['id', 'name']
|
||||
);
|
||||
assert.strictEqual(result.length, SEARCH_READ_LIMIT);
|
||||
assert.strictEqual(result.records[0].name, false,
|
||||
`Field "name" doesn't exist => returns false`
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("Send 'web_read_group' RPC: no group", async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
const server = new DeterministicSampleServer('hobbit', this.fields.hobbit);
|
||||
server.setExistingGroups([]);
|
||||
|
||||
const result = await server.mockRpc({
|
||||
method: 'web_read_group',
|
||||
model: 'hobbit',
|
||||
groupBy: ['profession'],
|
||||
});
|
||||
|
||||
assert.deepEqual(result, { groups: [], length: 0 });
|
||||
});
|
||||
|
||||
QUnit.test("Send 'web_read_group' RPC: 2 groups", async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
const server = new DeterministicSampleServer('hobbit', this.fields.hobbit);
|
||||
const existingGroups = [
|
||||
{ profession: 'gardener', profession_count: 0 },
|
||||
{ profession: 'adventurer', profession_count: 0 },
|
||||
];
|
||||
server.setExistingGroups(existingGroups);
|
||||
|
||||
const result = await server.mockRpc({
|
||||
method: 'web_read_group',
|
||||
model: 'hobbit',
|
||||
groupBy: ['profession'],
|
||||
fields: [],
|
||||
});
|
||||
|
||||
assert.strictEqual(result.length, 2);
|
||||
assert.strictEqual(result.groups.length, 2);
|
||||
|
||||
assert.deepEqual(
|
||||
result.groups.map((g) => g.profession),
|
||||
["gardener", "adventurer"]
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
result.groups.reduce((acc, g) => acc + g.profession_count, 0),
|
||||
MAIN_RECORDSET_SIZE
|
||||
);
|
||||
assert.ok(
|
||||
result.groups.every((g) => g.profession_count === g.__data.length)
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("Send 'web_read_group' RPC: all groups", async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
const server = new DeterministicSampleServer('hobbit', this.fields.hobbit);
|
||||
const existingGroups = [
|
||||
{ profession: 'gardener', profession_count: 0 },
|
||||
{ profession: 'brewer', profession_count: 0 },
|
||||
{ profession: 'adventurer', profession_count: 0 },
|
||||
];
|
||||
server.setExistingGroups(existingGroups);
|
||||
|
||||
const result = await server.mockRpc({
|
||||
method: 'web_read_group',
|
||||
model: 'hobbit',
|
||||
groupBy: ['profession'],
|
||||
fields: [],
|
||||
});
|
||||
|
||||
assert.strictEqual(result.length, 3);
|
||||
assert.strictEqual(result.groups.length, 3);
|
||||
|
||||
assert.deepEqual(
|
||||
result.groups.map((g) => g.profession),
|
||||
["gardener", "brewer", "adventurer"]
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
result.groups.reduce((acc, g) => acc + g.profession_count, 0),
|
||||
MAIN_RECORDSET_SIZE
|
||||
);
|
||||
assert.ok(
|
||||
result.groups.every((g) => g.profession_count === g.__data.length)
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("Send 'read_group' RPC: no group", async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
const server = new DeterministicSampleServer('hobbit', this.fields.hobbit);
|
||||
|
||||
const result = await server.mockRpc({
|
||||
method: 'read_group',
|
||||
model: 'hobbit',
|
||||
fields: [],
|
||||
groupBy: [],
|
||||
});
|
||||
|
||||
assert.deepEqual(result, [{
|
||||
__count: MAIN_RECORDSET_SIZE,
|
||||
__domain: [],
|
||||
}]);
|
||||
});
|
||||
|
||||
QUnit.test("Send 'read_group' RPC: groupBy", async function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
const server = new DeterministicSampleServer('hobbit', this.fields.hobbit);
|
||||
|
||||
const result = await server.mockRpc({
|
||||
method: 'read_group',
|
||||
model: 'hobbit',
|
||||
fields: [],
|
||||
groupBy: ['profession'],
|
||||
});
|
||||
|
||||
assert.strictEqual(result.length, 3);
|
||||
assert.deepEqual(
|
||||
result.map((g) => g.profession),
|
||||
["adventurer", "brewer", "gardener"]
|
||||
);
|
||||
assert.strictEqual(
|
||||
result.reduce((acc, g) => acc + g.profession_count, 0),
|
||||
MAIN_RECORDSET_SIZE,
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("Send 'read_group' RPC: groupBy and field", async function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
const server = new DeterministicSampleServer('hobbit', this.fields.hobbit);
|
||||
|
||||
const result = await server.mockRpc({
|
||||
method: 'read_group',
|
||||
model: 'hobbit',
|
||||
fields: ['age'],
|
||||
groupBy: ['profession'],
|
||||
});
|
||||
|
||||
assert.strictEqual(result.length, 3);
|
||||
assert.deepEqual(
|
||||
result.map((g) => g.profession),
|
||||
["adventurer", "brewer", "gardener"]
|
||||
);
|
||||
assert.strictEqual(
|
||||
result.reduce((acc, g) => acc + g.profession_count, 0),
|
||||
MAIN_RECORDSET_SIZE,
|
||||
);
|
||||
assert.strictEqual(
|
||||
result.reduce((acc, g) => acc + g.age, 0),
|
||||
server.data.hobbit.records.reduce((acc, g) => acc + g.age, 0)
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("Send 'read_group' RPC: multiple groupBys and lazy", async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
const server = new DeterministicSampleServer('hobbit', this.fields.hobbit);
|
||||
|
||||
const result = await server.mockRpc({
|
||||
method: 'read_group',
|
||||
model: 'hobbit',
|
||||
fields: [],
|
||||
groupBy: ['profession', 'age'],
|
||||
});
|
||||
|
||||
assert.ok('profession' in result[0]);
|
||||
assert.notOk('age' in result[0]);
|
||||
});
|
||||
|
||||
QUnit.test("Send 'read_group' RPC: multiple groupBys and not lazy", async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
const server = new DeterministicSampleServer('hobbit', this.fields.hobbit);
|
||||
|
||||
const result = await server.mockRpc({
|
||||
method: 'read_group',
|
||||
model: 'hobbit',
|
||||
fields: [],
|
||||
groupBy: ['profession', 'age'],
|
||||
lazy: false,
|
||||
});
|
||||
|
||||
assert.ok('profession' in result[0]);
|
||||
assert.ok('age' in result[0]);
|
||||
});
|
||||
|
||||
QUnit.test("Send 'read' RPC: no id", async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
const server = new DeterministicSampleServer('hobbit', this.fields.hobbit);
|
||||
|
||||
const result = await server.mockRpc({
|
||||
method: 'read',
|
||||
model: 'hobbit',
|
||||
args: [
|
||||
[], ['display_name']
|
||||
],
|
||||
});
|
||||
|
||||
assert.deepEqual(result, []);
|
||||
});
|
||||
|
||||
QUnit.test("Send 'read' RPC: one id", async function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
const server = new DeterministicSampleServer('hobbit', this.fields.hobbit);
|
||||
|
||||
const result = await server.mockRpc({
|
||||
method: 'read',
|
||||
model: 'hobbit',
|
||||
args: [
|
||||
[1], ['display_name']
|
||||
],
|
||||
});
|
||||
|
||||
assert.strictEqual(result.length, 1);
|
||||
assert.ok(
|
||||
/\w+/.test(result[0].display_name),
|
||||
"Display name has been mocked"
|
||||
);
|
||||
assert.strictEqual(result[0].id, 1);
|
||||
});
|
||||
|
||||
QUnit.test("Send 'read' RPC: more than all available ids", async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
const server = new DeterministicSampleServer('hobbit', this.fields.hobbit);
|
||||
|
||||
const amount = MAIN_RECORDSET_SIZE + 3;
|
||||
const ids = new Array(amount).fill().map((_, i) => i + 1);
|
||||
const result = await server.mockRpc({
|
||||
method: 'read',
|
||||
model: 'hobbit',
|
||||
args: [
|
||||
ids, ['display_name']
|
||||
],
|
||||
});
|
||||
|
||||
assert.strictEqual(result.length, MAIN_RECORDSET_SIZE);
|
||||
});
|
||||
|
||||
// To be implemented if needed
|
||||
// QUnit.test("Send 'read_progress_bar' RPC", async function (assert) { ... });
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,404 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { getFixture, legacyExtraNextTick, patchWithCleanup } from "@web/../tests/helpers/utils";
|
||||
import {
|
||||
getFacetTexts,
|
||||
removeFacet,
|
||||
saveFavorite,
|
||||
setupControlPanelFavoriteMenuRegistry,
|
||||
setupControlPanelServiceRegistry,
|
||||
switchView,
|
||||
toggleFavoriteMenu,
|
||||
toggleMenu,
|
||||
toggleMenuItem,
|
||||
toggleSaveFavorite,
|
||||
} from "@web/../tests/search/helpers";
|
||||
import { createWebClient, doAction } from "@web/../tests/webclient/helpers";
|
||||
import { _lt } from "@web/core/l10n/translation";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { ControlPanel } from "@web/search/control_panel/control_panel";
|
||||
import { SearchModel } from "@web/search/search_model";
|
||||
import AbstractView from "web.AbstractView";
|
||||
import ActionModel from "web.ActionModel";
|
||||
import { mock } from "web.test_utils";
|
||||
import legacyViewRegistry from "web.view_registry";
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { LegacyComponent } from "@web/legacy/legacy_component";
|
||||
|
||||
import { xml } from "@odoo/owl";
|
||||
const viewRegistry = registry.category("views");
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
QUnit.module("LegacyViews", (hooks) => {
|
||||
hooks.beforeEach(async () => {
|
||||
target = getFixture();
|
||||
|
||||
serverData = {
|
||||
models: {
|
||||
foo: {
|
||||
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",
|
||||
store: true,
|
||||
sortable: true,
|
||||
},
|
||||
},
|
||||
records: [],
|
||||
},
|
||||
},
|
||||
views: {
|
||||
"foo,false,legacy_toy": `<legacy_toy/>`,
|
||||
"foo,false,toy": `<toy/>`,
|
||||
"foo,false,search": `
|
||||
<search>
|
||||
<field name="foo" operator="="/>
|
||||
<filter name="true_domain" string="True Domain" domain="[(1, '=', 1)]"/>
|
||||
<filter name="date_domain" string="Date Filter" date="date_field" domain="[]"/>
|
||||
<filter name="group_by_bar" string="Bar" context="{ 'group_by': 'bar' }"/>
|
||||
<filter name="group_by_date_field" string="Date GroupBy" context="{ 'group_by': 'date_field' }"/>
|
||||
</search>
|
||||
`,
|
||||
},
|
||||
};
|
||||
setupControlPanelFavoriteMenuRegistry();
|
||||
setupControlPanelServiceRegistry();
|
||||
|
||||
class ToyController extends LegacyComponent {}
|
||||
ToyController.template = xml`<div class="o_toy_view"><ControlPanel /></div>`;
|
||||
ToyController.components = { ControlPanel};
|
||||
|
||||
viewRegistry.add("toy", {
|
||||
type: "toy",
|
||||
display_name: _lt("Toy view"),
|
||||
multiRecord: true,
|
||||
searchMenuTypes: ["filter", "groupBy", "comparison", "favorite"],
|
||||
Controller: ToyController,
|
||||
});
|
||||
|
||||
const LegacyToyView = AbstractView.extend({
|
||||
display_name: _lt("Legacy toy view"),
|
||||
icon: "fa fa-bars",
|
||||
multiRecord: true,
|
||||
viewType: "legacy_toy",
|
||||
searchMenuTypes: ["filter", "groupBy", "comparison", "favorite"],
|
||||
});
|
||||
|
||||
legacyViewRegistry.add("legacy_toy", LegacyToyView);
|
||||
});
|
||||
|
||||
QUnit.module("State Mappings");
|
||||
|
||||
QUnit.test(
|
||||
"legacy and new views can share search model state (no favorite)",
|
||||
async function (assert) {
|
||||
assert.expect(10);
|
||||
|
||||
const unpatchDate = mock.patchDate(2021, 6, 1, 10, 0, 0);
|
||||
|
||||
const webClient = await createWebClient({ serverData });
|
||||
|
||||
await doAction(webClient, {
|
||||
name: "Action name",
|
||||
res_model: "foo",
|
||||
type: "ir.actions.act_window",
|
||||
views: [
|
||||
[false, "toy"],
|
||||
[false, "legacy_toy"],
|
||||
],
|
||||
context: {
|
||||
search_default_foo: "ABC",
|
||||
search_default_true_domain: 1,
|
||||
search_default_date_domain: 1,
|
||||
search_default_group_by_bar: 50,
|
||||
search_default_group_by_date_field: 1,
|
||||
},
|
||||
});
|
||||
|
||||
assert.containsOnce(target, ".o_switch_view.o_toy.active");
|
||||
assert.deepEqual(
|
||||
getFacetTexts(target).map((s) => s.replace(/\s/, "")),
|
||||
[
|
||||
"FooABC",
|
||||
"TrueDomainorDate Filter: July 2021",
|
||||
"DateGroupBy: Month>Bar",
|
||||
]
|
||||
);
|
||||
|
||||
await toggleMenu(target, "Comparison");
|
||||
await toggleMenuItem(target, "Date Filter: Previous Period");
|
||||
|
||||
assert.deepEqual(
|
||||
getFacetTexts(target).map((s) => s.replace(/\s/, "")),
|
||||
[
|
||||
"FooABC",
|
||||
"TrueDomainorDate Filter: July 2021",
|
||||
"DateGroupBy: Month>Bar",
|
||||
"DateFilter: Previous Period",
|
||||
]
|
||||
);
|
||||
|
||||
await switchView(target, "legacy_toy");
|
||||
await legacyExtraNextTick();
|
||||
|
||||
assert.containsOnce(target, ".o_switch_view.o_legacy_toy.active");
|
||||
assert.deepEqual(
|
||||
getFacetTexts(target).map((s) => s.replace(/\s/, "")),
|
||||
[
|
||||
"FooABC",
|
||||
"TrueDomainorDate Filter: July 2021",
|
||||
"DateGroupBy: Month>Bar",
|
||||
"DateFilter: Previous Period",
|
||||
]
|
||||
);
|
||||
|
||||
await removeFacet(target, 1);
|
||||
|
||||
assert.deepEqual(
|
||||
getFacetTexts(target).map((s) => s.replace(/\s/, "")),
|
||||
[
|
||||
"FooABC",
|
||||
"DateGroupBy: Month>Bar",
|
||||
]
|
||||
);
|
||||
|
||||
await switchView(target, "toy");
|
||||
|
||||
assert.containsOnce(target, ".o_switch_view.o_toy.active");
|
||||
assert.deepEqual(
|
||||
getFacetTexts(target).map((s) => s.replace(/\s/, "")),
|
||||
[
|
||||
"FooABC",
|
||||
"DateGroupBy: Month>Bar",
|
||||
]
|
||||
);
|
||||
|
||||
// Check if update works
|
||||
await removeFacet(target, 1);
|
||||
|
||||
assert.deepEqual(
|
||||
getFacetTexts(target).map((s) => s.replace(/\s/, "")),
|
||||
[
|
||||
"FooABC",
|
||||
]
|
||||
);
|
||||
|
||||
await switchView(target, "legacy_toy");
|
||||
|
||||
assert.deepEqual(
|
||||
getFacetTexts(target).map((s) => s.replace(/\s/, "")),
|
||||
[
|
||||
"FooABC",
|
||||
]
|
||||
);
|
||||
|
||||
unpatchDate();
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test(
|
||||
"legacy and new views can share search model state (favorite)",
|
||||
async function (assert) {
|
||||
assert.expect(6);
|
||||
|
||||
serverData.models.foo.filters = [
|
||||
{
|
||||
context: "{}",
|
||||
domain: "[['foo', '=', 'qsdf']]",
|
||||
id: 7,
|
||||
is_default: true,
|
||||
name: "My favorite",
|
||||
sort: "[]",
|
||||
user_id: [2, "Mitchell Admin"],
|
||||
},
|
||||
];
|
||||
|
||||
const webClient = await createWebClient({ serverData });
|
||||
|
||||
await doAction(webClient, {
|
||||
name: "Action name",
|
||||
res_model: "foo",
|
||||
type: "ir.actions.act_window",
|
||||
views: [
|
||||
[false, "toy"],
|
||||
[false, "legacy_toy"],
|
||||
],
|
||||
});
|
||||
|
||||
assert.containsOnce(target, ".o_switch_view.o_toy.active");
|
||||
assert.deepEqual(getFacetTexts(target), ["My favorite"]);
|
||||
|
||||
await switchView(target, "legacy_toy");
|
||||
await legacyExtraNextTick();
|
||||
|
||||
assert.containsOnce(target, ".o_switch_view.o_legacy_toy.active");
|
||||
assert.deepEqual(getFacetTexts(target), ["My favorite"]);
|
||||
|
||||
await switchView(target, "toy");
|
||||
|
||||
assert.containsOnce(target, ".o_switch_view.o_toy.active");
|
||||
assert.deepEqual(getFacetTexts(target), ["My favorite"]);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test(
|
||||
"newly created favorite in a new view can be used in a legacy view",
|
||||
async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
patchWithCleanup(browser, { setTimeout: (fn) => fn() });
|
||||
const webClient = await createWebClient({
|
||||
serverData,
|
||||
mockRPC(_, args) {
|
||||
if (args.method === "create_or_replace") {
|
||||
assert.ok(typeof args.args[0].domain === "string");
|
||||
return 7; // fake serverId to simulate the creation of
|
||||
// the favorite in db.
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
await doAction(webClient, {
|
||||
name: "Action name",
|
||||
res_model: "foo",
|
||||
type: "ir.actions.act_window",
|
||||
views: [
|
||||
[false, "toy"],
|
||||
[false, "legacy_toy"],
|
||||
],
|
||||
});
|
||||
|
||||
assert.containsOnce(target, ".o_switch_view.o_toy.active");
|
||||
|
||||
await toggleFavoriteMenu(target);
|
||||
await toggleSaveFavorite(target);
|
||||
await saveFavorite(target);
|
||||
|
||||
assert.deepEqual(getFacetTexts(target), ["Action name"]);
|
||||
|
||||
await switchView(target, "legacy_toy");
|
||||
await legacyExtraNextTick();
|
||||
|
||||
assert.containsOnce(target, ".o_switch_view.o_legacy_toy.active");
|
||||
assert.deepEqual(getFacetTexts(target), ["Action name"]);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test(
|
||||
"legacy and new views with search model extensions can share search model state",
|
||||
async function (assert) {
|
||||
assert.expect(16);
|
||||
|
||||
serverData.views[
|
||||
"foo,1,legacy_toy"
|
||||
] = `<legacy_toy js_class="legacy_toy_with_extension"/>`;
|
||||
|
||||
class SearchModelExtension extends SearchModel {
|
||||
setup() {
|
||||
super.setup(...arguments);
|
||||
this.toyExtension = { locationId: "Grand-Rosière" };
|
||||
}
|
||||
|
||||
exportState() {
|
||||
const exportedState = super.exportState(...arguments);
|
||||
exportedState.toyExtension = this.toyExtension;
|
||||
return exportedState;
|
||||
}
|
||||
|
||||
_importState(state) {
|
||||
super._importState(state);
|
||||
this.toyExtension = state.toyExtension;
|
||||
assert.step(JSON.stringify(state.toyExtension));
|
||||
}
|
||||
}
|
||||
|
||||
const ToyView = viewRegistry.get("toy");
|
||||
ToyView.SearchModel = SearchModelExtension;
|
||||
|
||||
class ToyExtension extends ActionModel.Extension {
|
||||
importState(state) {
|
||||
super.importState(state); // done even if state is undefined in legacy code
|
||||
assert.step(JSON.stringify(state) || "no state");
|
||||
}
|
||||
prepareState() {
|
||||
Object.assign(this.state, {
|
||||
locationId: "The place to be",
|
||||
});
|
||||
}
|
||||
}
|
||||
ActionModel.registry.add("toyExtension", ToyExtension);
|
||||
|
||||
const LegacyToyView = legacyViewRegistry.get("legacy_toy");
|
||||
|
||||
const LegacyToyViewWithExtension = LegacyToyView.extend({
|
||||
_createSearchModel(params, extraExtensions = {}) {
|
||||
Object.assign(extraExtensions, { toyExtension: {} });
|
||||
return this._super(params, extraExtensions);
|
||||
},
|
||||
});
|
||||
legacyViewRegistry.add("legacy_toy_with_extension", LegacyToyViewWithExtension);
|
||||
|
||||
const webClient = await createWebClient({ serverData });
|
||||
|
||||
await doAction(webClient, {
|
||||
name: "Action name",
|
||||
res_model: "foo",
|
||||
type: "ir.actions.act_window",
|
||||
views: [
|
||||
[false, "toy"],
|
||||
[1, "legacy_toy"],
|
||||
],
|
||||
});
|
||||
|
||||
assert.containsOnce(target, ".o_switch_view.o_toy.active");
|
||||
|
||||
await switchView(target, "legacy_toy");
|
||||
await legacyExtraNextTick();
|
||||
|
||||
assert.containsOnce(target, ".o_switch_view.o_legacy_toy.active");
|
||||
assert.verifySteps([`{"locationId":"Grand-Rosière"}`]);
|
||||
|
||||
await switchView(target, "toy");
|
||||
|
||||
assert.containsOnce(target, ".o_switch_view.o_toy.active");
|
||||
assert.verifySteps([`{"locationId":"Grand-Rosière"}`]);
|
||||
|
||||
await doAction(webClient, {
|
||||
name: "Action name",
|
||||
res_model: "foo",
|
||||
type: "ir.actions.act_window",
|
||||
views: [
|
||||
[1, "legacy_toy"],
|
||||
[false, "toy"],
|
||||
],
|
||||
});
|
||||
await legacyExtraNextTick();
|
||||
|
||||
assert.containsOnce(target, ".o_switch_view.o_legacy_toy.active");
|
||||
assert.verifySteps([`no state`]);
|
||||
|
||||
await switchView(target, "toy");
|
||||
|
||||
assert.containsOnce(target, ".o_switch_view.o_toy.active");
|
||||
assert.verifySteps([`{"locationId":"The place to be"}`]);
|
||||
|
||||
await switchView(target, "legacy_toy");
|
||||
await legacyExtraNextTick();
|
||||
|
||||
assert.containsOnce(target, ".o_switch_view.o_legacy_toy.active");
|
||||
assert.verifySteps([`{"locationId":"The place to be"}`]);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
|
@ -1,772 +0,0 @@
|
|||
odoo.define('web.view_dialogs_tests', function (require) {
|
||||
"use strict";
|
||||
|
||||
var dialogs = require('web.view_dialogs');
|
||||
var ListController = require('web.ListController');
|
||||
var testUtils = require('web.test_utils');
|
||||
var Widget = require('web.Widget');
|
||||
var FormView = require('web.FormView');
|
||||
|
||||
const { browser } = require('@web/core/browser/browser');
|
||||
const { patchWithCleanup } = require('@web/../tests/helpers/utils');
|
||||
const cpHelpers = require('@web/../tests/search/helpers');
|
||||
var createView = testUtils.createView;
|
||||
|
||||
async function createParent(params) {
|
||||
var widget = new Widget();
|
||||
params.server = await testUtils.mock.addMockEnvironment(widget, params);
|
||||
return widget;
|
||||
}
|
||||
|
||||
QUnit.module('LegacyViews', {
|
||||
beforeEach: function () {
|
||||
this.data = {
|
||||
partner: {
|
||||
fields: {
|
||||
display_name: { string: "Displayed name", type: "char" },
|
||||
foo: {string: "Foo", type: 'char'},
|
||||
bar: {string: "Bar", type: "boolean"},
|
||||
instrument: {string: 'Instruments', type: 'many2one', relation: 'instrument'},
|
||||
},
|
||||
records: [
|
||||
{id: 1, foo: 'blip', display_name: 'blipblip', bar: true},
|
||||
{id: 2, foo: 'ta tata ta ta', display_name: 'macgyver', bar: false},
|
||||
{id: 3, foo: 'piou piou', display_name: "Jack O'Neill", bar: true},
|
||||
],
|
||||
},
|
||||
instrument: {
|
||||
fields: {
|
||||
name: {string: "name", type: "char"},
|
||||
badassery: {string: 'level', type: 'many2many', relation: 'badassery', domain: [['level', '=', 'Awsome']]},
|
||||
},
|
||||
},
|
||||
|
||||
badassery: {
|
||||
fields: {
|
||||
level: {string: 'level', type: "char"},
|
||||
},
|
||||
records: [
|
||||
{id: 1, level: 'Awsome'},
|
||||
],
|
||||
},
|
||||
|
||||
product: {
|
||||
fields : {
|
||||
name: {string: "name", type: "char" },
|
||||
partner : {string: 'Doors', type: 'one2many', relation: 'partner'},
|
||||
},
|
||||
records: [
|
||||
{id: 1, name: 'The end'},
|
||||
],
|
||||
},
|
||||
};
|
||||
},
|
||||
}, function () {
|
||||
|
||||
QUnit.module('ViewDialog (legacy)');
|
||||
|
||||
QUnit.test('formviewdialog buttons in footer are positioned properly', async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
var parent = await createParent({
|
||||
data: this.data,
|
||||
archs: {
|
||||
'partner,false,form':
|
||||
'<form string="Partner">' +
|
||||
'<sheet>' +
|
||||
'<group><field name="foo"/></group>' +
|
||||
'<footer><button string="Custom Button" type="object" class="btn-primary"/></footer>' +
|
||||
'</sheet>' +
|
||||
'</form>',
|
||||
},
|
||||
});
|
||||
|
||||
new dialogs.FormViewDialog(parent, {
|
||||
res_model: 'partner',
|
||||
res_id: 1,
|
||||
}).open();
|
||||
await testUtils.nextTick();
|
||||
|
||||
assert.notOk($('.modal-body button').length,
|
||||
"should not have any button in body");
|
||||
assert.strictEqual($('.modal-footer button').length, 1,
|
||||
"should have only one button in footer");
|
||||
parent.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('formviewdialog buttons in footer are not duplicated', async function (assert) {
|
||||
assert.expect(2);
|
||||
this.data.partner.fields.poney_ids = {string: "Poneys", type: "one2many", relation: 'partner'};
|
||||
this.data.partner.records[0].poney_ids = [];
|
||||
|
||||
var parent = await createParent({
|
||||
data: this.data,
|
||||
archs: {
|
||||
'partner,false,form':
|
||||
'<form string="Partner">' +
|
||||
'<field name="poney_ids"><tree editable="top"><field name="display_name"/></tree></field>' +
|
||||
'<footer><button string="Custom Button" type="object" class="btn-primary"/></footer>' +
|
||||
'</form>',
|
||||
},
|
||||
});
|
||||
|
||||
new dialogs.FormViewDialog(parent, {
|
||||
res_model: 'partner',
|
||||
res_id: 1,
|
||||
}).open();
|
||||
await testUtils.nextTick();
|
||||
|
||||
assert.strictEqual($('.modal button.btn-primary').length, 1,
|
||||
"should have 1 buttons in modal");
|
||||
|
||||
await testUtils.dom.click($('.o_field_x2many_list_row_add a'));
|
||||
await testUtils.fields.triggerKeydown($('input.o_input'), 'escape');
|
||||
|
||||
assert.strictEqual($('.modal button.btn-primary').length, 1,
|
||||
"should still have 1 buttons in modal");
|
||||
parent.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('SelectCreateDialog use domain, group_by and search default', async function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
var search = 0;
|
||||
var parent = await createParent({
|
||||
data: this.data,
|
||||
archs: {
|
||||
'partner,false,list':
|
||||
'<tree string="Partner">' +
|
||||
'<field name="display_name"/>' +
|
||||
'<field name="foo"/>' +
|
||||
'</tree>',
|
||||
'partner,false,search':
|
||||
'<search>' +
|
||||
'<field name="foo" filter_domain="[(\'display_name\',\'ilike\',self), (\'foo\',\'ilike\',self)]"/>' +
|
||||
'<group expand="0" string="Group By">' +
|
||||
'<filter name="groupby_bar" context="{\'group_by\' : \'bar\'}"/>' +
|
||||
'</group>' +
|
||||
'</search>',
|
||||
},
|
||||
mockRPC: function (route, args) {
|
||||
if (args.method === 'web_read_group') {
|
||||
assert.deepEqual(args.kwargs, {
|
||||
context: {
|
||||
search_default_foo: "piou",
|
||||
search_default_groupby_bar: true,
|
||||
},
|
||||
domain: ["&", ["display_name", "like", "a"], "&", ["display_name", "ilike", "piou"], ["foo", "ilike", "piou"]],
|
||||
fields: ["display_name", "foo", "bar"],
|
||||
groupby: ["bar"],
|
||||
orderby: '',
|
||||
lazy: true,
|
||||
limit: 80,
|
||||
}, "should search with the complete domain (domain + search), and group by 'bar'");
|
||||
}
|
||||
if (search === 0 && route === '/web/dataset/search_read') {
|
||||
search++;
|
||||
assert.deepEqual(args, {
|
||||
context: {
|
||||
search_default_foo: "piou",
|
||||
search_default_groupby_bar: true,
|
||||
bin_size: true
|
||||
}, // not part of the test, may change
|
||||
domain: ["&", ["display_name", "like", "a"], "&", ["display_name", "ilike", "piou"], ["foo", "ilike", "piou"]],
|
||||
fields: ["display_name", "foo"],
|
||||
model: "partner",
|
||||
limit: 80,
|
||||
sort: ""
|
||||
}, "should search with the complete domain (domain + search)");
|
||||
} else if (search === 1 && route === '/web/dataset/search_read') {
|
||||
assert.deepEqual(args, {
|
||||
context: {
|
||||
search_default_foo: "piou",
|
||||
search_default_groupby_bar: true,
|
||||
bin_size: true
|
||||
}, // not part of the test, may change
|
||||
domain: [["display_name", "like", "a"]],
|
||||
fields: ["display_name", "foo"],
|
||||
model: "partner",
|
||||
limit: 80,
|
||||
sort: ""
|
||||
}, "should search with the domain");
|
||||
}
|
||||
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
|
||||
new dialogs.SelectCreateDialog(parent, {
|
||||
no_create: true,
|
||||
readonly: true,
|
||||
res_model: 'partner',
|
||||
domain: [['display_name', 'like', 'a']],
|
||||
context: {
|
||||
search_default_groupby_bar: true,
|
||||
search_default_foo: 'piou',
|
||||
},
|
||||
}).open();
|
||||
await testUtils.nextTick();
|
||||
const modal = document.body.querySelector(".modal");
|
||||
await cpHelpers.removeFacet(modal, "Bar");
|
||||
await cpHelpers.removeFacet(modal);
|
||||
|
||||
parent.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('SelectCreateDialog correctly evaluates domains', async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
var parent = await createParent({
|
||||
data: this.data,
|
||||
archs: {
|
||||
'partner,false,list':
|
||||
'<tree string="Partner">' +
|
||||
'<field name="display_name"/>' +
|
||||
'<field name="foo"/>' +
|
||||
'</tree>',
|
||||
'partner,false,search':
|
||||
'<search>' +
|
||||
'<field name="foo"/>' +
|
||||
'</search>',
|
||||
},
|
||||
mockRPC: function (route, args) {
|
||||
if (route === '/web/dataset/search_read') {
|
||||
assert.deepEqual(args.domain, [['id', '=', 2]],
|
||||
"should have correctly evaluated the domain");
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
session: {
|
||||
user_context: {uid: 2},
|
||||
},
|
||||
});
|
||||
|
||||
new dialogs.SelectCreateDialog(parent, {
|
||||
no_create: true,
|
||||
readonly: true,
|
||||
res_model: 'partner',
|
||||
domain: "[['id', '=', uid]]",
|
||||
}).open();
|
||||
await testUtils.nextTick();
|
||||
|
||||
parent.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('SelectCreateDialog list view in readonly', async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
var parent = await createParent({
|
||||
data: this.data,
|
||||
archs: {
|
||||
'partner,false,list':
|
||||
'<tree string="Partner" editable="bottom">' +
|
||||
'<field name="display_name"/>' +
|
||||
'<field name="foo"/>' +
|
||||
'</tree>',
|
||||
'partner,false,search':
|
||||
'<search/>'
|
||||
},
|
||||
});
|
||||
|
||||
var dialog;
|
||||
new dialogs.SelectCreateDialog(parent, {
|
||||
res_model: 'partner',
|
||||
}).open().then(function (result) {
|
||||
dialog = result;
|
||||
});
|
||||
await testUtils.nextTick();
|
||||
|
||||
// click on the first row to see if the list is editable
|
||||
await testUtils.dom.click(dialog.$('.o_legacy_list_view tbody tr:first td:not(.o_list_record_selector):first'));
|
||||
|
||||
assert.equal(dialog.$('.o_legacy_list_view tbody tr:first td:not(.o_list_record_selector):first input').length, 0,
|
||||
"list view should not be editable in a SelectCreateDialog");
|
||||
|
||||
parent.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('SelectCreateDialog cascade x2many in create mode', async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
var form = await createView({
|
||||
View: FormView,
|
||||
model: 'product',
|
||||
data: this.data,
|
||||
arch: '<form>' +
|
||||
'<field name="name"/>' +
|
||||
'<field name="partner" widget="one2many" >' +
|
||||
'<tree editable="top">' +
|
||||
'<field name="display_name"/>' +
|
||||
'<field name="instrument"/>' +
|
||||
'</tree>' +
|
||||
'</field>' +
|
||||
'</form>',
|
||||
res_id: 1,
|
||||
archs: {
|
||||
'partner,false,form': '<form>' +
|
||||
'<field name="name"/>' +
|
||||
'<field name="instrument" widget="one2many" mode="tree"/>' +
|
||||
'</form>',
|
||||
|
||||
'instrument,false,form': '<form>'+
|
||||
'<field name="name"/>'+
|
||||
'<field name="badassery">' +
|
||||
'<tree>'+
|
||||
'<field name="level"/>'+
|
||||
'</tree>' +
|
||||
'</field>' +
|
||||
'</form>',
|
||||
|
||||
'badassery,false,list': '<tree>'+
|
||||
'<field name="level"/>'+
|
||||
'</tree>',
|
||||
|
||||
'badassery,false,search': '<search>'+
|
||||
'<field name="level"/>'+
|
||||
'</search>',
|
||||
},
|
||||
|
||||
mockRPC: function(route, args) {
|
||||
if (route === '/web/dataset/call_kw/partner/get_formview_id') {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
if (route === '/web/dataset/call_kw/instrument/get_formview_id') {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
if (route === '/web/dataset/call_kw/instrument/create') {
|
||||
assert.deepEqual(args.args, [{badassery: [[6, false, [1]]], name: "ABC"}],
|
||||
'The method create should have been called with the right arguments');
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
return this._super(route, args);
|
||||
},
|
||||
});
|
||||
|
||||
await testUtils.form.clickEdit(form);
|
||||
await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
|
||||
await testUtils.fields.many2one.createAndEdit("instrument");
|
||||
|
||||
var $modal = $('.modal-lg');
|
||||
|
||||
assert.equal($modal.length, 1,
|
||||
'There should be one modal');
|
||||
|
||||
await testUtils.dom.click($modal.find('.o_field_x2many_list_row_add a'));
|
||||
|
||||
var $modals = $('.modal-lg');
|
||||
|
||||
assert.equal($modals.length, 2,
|
||||
'There should be two modals');
|
||||
|
||||
var $second_modal = $modals.not($modal);
|
||||
await testUtils.dom.click($second_modal.find('.o_list_table.table.table-sm.table-striped.o_list_table_ungrouped .o_data_row input[type=checkbox]'));
|
||||
|
||||
await testUtils.dom.click($second_modal.find('.o_select_button'));
|
||||
|
||||
$modal = $('.modal-lg');
|
||||
|
||||
assert.equal($modal.length, 1,
|
||||
'There should be one modal');
|
||||
|
||||
assert.equal($modal.find('.o_data_cell').text(), 'Awsome',
|
||||
'There should be one item in the list of the modal');
|
||||
|
||||
await testUtils.dom.click($modal.find('.btn.btn-primary'));
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('Form dialog and subview with _view_ref contexts', async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
this.data.instrument.records = [{id: 1, name: 'Tromblon', badassery: [1]}];
|
||||
this.data.partner.records[0].instrument = 1;
|
||||
|
||||
var form = await createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<form>' +
|
||||
'<field name="name"/>' +
|
||||
'<field name="instrument" context="{\'tree_view_ref\': \'some_tree_view\'}"/>' +
|
||||
'</form>',
|
||||
res_id: 1,
|
||||
archs: {
|
||||
'instrument,false,form': '<form>'+
|
||||
'<field name="name"/>'+
|
||||
'<field name="badassery" context="{\'tree_view_ref\': \'some_other_tree_view\'}"/>' +
|
||||
'</form>',
|
||||
|
||||
'badassery,false,list': '<tree>'+
|
||||
'<field name="level"/>'+
|
||||
'</tree>',
|
||||
},
|
||||
viewOptions: {
|
||||
mode: 'edit',
|
||||
},
|
||||
|
||||
mockRPC: function(route, args) {
|
||||
if (args.method === 'get_formview_id') {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
return this._super(route, args);
|
||||
},
|
||||
|
||||
interceptsPropagate: {
|
||||
load_views: function (ev) {
|
||||
var evaluatedContext = ev.data.context;
|
||||
if (ev.data.modelName === 'instrument') {
|
||||
assert.deepEqual(evaluatedContext, {tree_view_ref: 'some_tree_view'},
|
||||
'The correct _view_ref should have been sent to the server, first time');
|
||||
}
|
||||
if (ev.data.modelName === 'badassery') {
|
||||
assert.deepEqual(evaluatedContext, {
|
||||
base_model_name: 'instrument',
|
||||
tree_view_ref: 'some_other_tree_view',
|
||||
}, 'The correct _view_ref should have been sent to the server for the subview');
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await testUtils.dom.click(form.$('.o_field_widget[name="instrument"] button.o_external_button'));
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test("Form dialog replaces the context with _createContext method when specified", async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
const parent = await createParent({
|
||||
data: this.data,
|
||||
archs: {
|
||||
"partner,false,form":
|
||||
`<form string="Partner">
|
||||
<sheet>
|
||||
<group><field name="foo"/></group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
},
|
||||
|
||||
mockRPC: function (route, args) {
|
||||
if (args.method === "create") {
|
||||
assert.step(JSON.stringify(args.kwargs.context));
|
||||
}
|
||||
return this._super(route, args);
|
||||
},
|
||||
});
|
||||
|
||||
new dialogs.FormViewDialog(parent, {
|
||||
res_model: "partner",
|
||||
context: { answer: 42 },
|
||||
_createContext: () => ({ dolphin: 64 }),
|
||||
}).open();
|
||||
await testUtils.nextTick();
|
||||
|
||||
assert.notOk($(".modal-body button").length,
|
||||
"should not have any button in body");
|
||||
assert.strictEqual($(".modal-footer button").length, 3,
|
||||
"should have 3 buttons in footer");
|
||||
|
||||
await testUtils.dom.click($(".modal-footer button:contains(Save & New)"));
|
||||
await testUtils.dom.click($(".modal-footer button:contains(Save & New)"));
|
||||
assert.verifySteps(['{"answer":42}', '{"dolphin":64}']);
|
||||
parent.destroy();
|
||||
});
|
||||
|
||||
QUnit.test("Form dialog keeps full context when no _createContext is specified", async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
const parent = await createParent({
|
||||
data: this.data,
|
||||
archs: {
|
||||
"partner,false,form":
|
||||
`<form string="Partner">
|
||||
<sheet>
|
||||
<group><field name="foo"/></group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
},
|
||||
|
||||
mockRPC: function (route, args) {
|
||||
if (args.method === "create") {
|
||||
assert.step(JSON.stringify(args.kwargs.context));
|
||||
}
|
||||
return this._super(route, args);
|
||||
},
|
||||
});
|
||||
|
||||
new dialogs.FormViewDialog(parent, {
|
||||
res_model: "partner",
|
||||
context: { answer: 42 }
|
||||
}).open();
|
||||
await testUtils.nextTick();
|
||||
|
||||
assert.notOk($(".modal-body button").length,
|
||||
"should not have any button in body");
|
||||
assert.strictEqual($(".modal-footer button").length, 3,
|
||||
"should have 3 buttons in footer");
|
||||
|
||||
await testUtils.dom.click($(".modal-footer button:contains(Save & New)"));
|
||||
await testUtils.dom.click($(".modal-footer button:contains(Save & New)"));
|
||||
assert.verifySteps(['{"answer":42}', '{"answer":42}']);
|
||||
parent.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('SelectCreateDialog: save current search', async function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
testUtils.mock.patch(ListController, {
|
||||
getOwnedQueryParams: function () {
|
||||
return {
|
||||
context: {
|
||||
shouldBeInFilterContext: true,
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
// save favorite needs this
|
||||
patchWithCleanup(browser, {
|
||||
setTimeout: (fn) => fn(),
|
||||
});
|
||||
|
||||
var parent = await createParent({
|
||||
data: this.data,
|
||||
archs: {
|
||||
'partner,false,list':
|
||||
'<tree>' +
|
||||
'<field name="display_name"/>' +
|
||||
'</tree>',
|
||||
'partner,false,search':
|
||||
'<search>' +
|
||||
'<filter name="bar" help="Bar" domain="[(\'bar\', \'=\', True)]"/>' +
|
||||
'</search>',
|
||||
|
||||
},
|
||||
env: {
|
||||
dataManager: {
|
||||
create_filter: function (filter) {
|
||||
assert.strictEqual(filter.domain, `[("bar", "=", True)]`,
|
||||
"should save the correct domain");
|
||||
const expectedContext = {
|
||||
group_by: [], // default groupby is an empty list
|
||||
shouldBeInFilterContext: true,
|
||||
};
|
||||
assert.deepEqual(filter.context, expectedContext,
|
||||
"should save the correct context");
|
||||
},
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
var dialog;
|
||||
new dialogs.SelectCreateDialog(parent, {
|
||||
context: {shouldNotBeInFilterContext: false},
|
||||
res_model: 'partner',
|
||||
}).open().then(function (result) {
|
||||
dialog = result;
|
||||
});
|
||||
await testUtils.nextTick();
|
||||
|
||||
|
||||
assert.containsN(dialog, '.o_data_row', 3, "should contain 3 records");
|
||||
|
||||
// filter on bar
|
||||
const modal = document.body.querySelector(".modal");
|
||||
await cpHelpers.toggleFilterMenu(modal);
|
||||
await cpHelpers.toggleMenuItem(modal, "Bar");
|
||||
|
||||
assert.containsN(dialog, '.o_data_row', 2, "should contain 2 records");
|
||||
|
||||
// save filter
|
||||
await cpHelpers.toggleFavoriteMenu(modal);
|
||||
await cpHelpers.toggleSaveFavorite(modal);
|
||||
await cpHelpers.editFavoriteName(modal, "some name");
|
||||
await cpHelpers.saveFavorite(modal);
|
||||
|
||||
testUtils.mock.unpatch(ListController);
|
||||
parent.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('SelectCreateDialog calls on_selected with every record matching the domain', async function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
const parent = await createParent({
|
||||
data: this.data,
|
||||
archs: {
|
||||
'partner,false,list':
|
||||
'<tree limit="2" string="Partner">' +
|
||||
'<field name="display_name"/>' +
|
||||
'<field name="foo"/>' +
|
||||
'</tree>',
|
||||
'partner,false,search':
|
||||
'<search>' +
|
||||
'<field name="foo"/>' +
|
||||
'</search>',
|
||||
},
|
||||
session: {},
|
||||
});
|
||||
|
||||
new dialogs.SelectCreateDialog(parent, {
|
||||
res_model: 'partner',
|
||||
on_selected: function(records) {
|
||||
assert.equal(records.length, 3);
|
||||
assert.strictEqual(records.map((r) => r.display_name).toString(), "blipblip,macgyver,Jack O'Neill");
|
||||
assert.strictEqual(records.map((r) => r.id).toString(), "1,2,3");
|
||||
}
|
||||
}).open();
|
||||
await testUtils.nextTick();
|
||||
|
||||
await testUtils.dom.click($('thead .o_list_record_selector input'));
|
||||
await testUtils.dom.click($('.o_list_selection_box .o_list_select_domain'));
|
||||
await testUtils.dom.click($('.modal .o_select_button'));
|
||||
|
||||
parent.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('SelectCreateDialog calls on_selected with every record matching without selecting a domain', async function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
const parent = await createParent({
|
||||
data: this.data,
|
||||
archs: {
|
||||
'partner,false,list':
|
||||
'<tree limit="2" string="Partner">' +
|
||||
'<field name="display_name"/>' +
|
||||
'<field name="foo"/>' +
|
||||
'</tree>',
|
||||
'partner,false,search':
|
||||
'<search>' +
|
||||
'<field name="foo"/>' +
|
||||
'</search>',
|
||||
},
|
||||
session: {},
|
||||
});
|
||||
|
||||
new dialogs.SelectCreateDialog(parent, {
|
||||
res_model: 'partner',
|
||||
on_selected: function(records) {
|
||||
assert.equal(records.length, 2);
|
||||
assert.strictEqual(records.map((r) => r.display_name).toString(), "blipblip,macgyver");
|
||||
assert.strictEqual(records.map((r) => r.id).toString(), "1,2");
|
||||
}
|
||||
}).open();
|
||||
await testUtils.nextTick();
|
||||
|
||||
await testUtils.dom.click($('thead .o_list_record_selector input'));
|
||||
await testUtils.dom.click($('.o_list_selection_box '));
|
||||
await testUtils.dom.click($('.modal .o_select_button'));
|
||||
|
||||
parent.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('propagate can_create onto the search popup o2m', async function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
this.data.instrument.records = [
|
||||
{id: 1, name: 'Tromblon1'},
|
||||
{id: 2, name: 'Tromblon2'},
|
||||
{id: 3, name: 'Tromblon3'},
|
||||
{id: 4, name: 'Tromblon4'},
|
||||
{id: 5, name: 'Tromblon5'},
|
||||
{id: 6, name: 'Tromblon6'},
|
||||
{id: 7, name: 'Tromblon7'},
|
||||
{id: 8, name: 'Tromblon8'},
|
||||
];
|
||||
|
||||
var form = await createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<form>' +
|
||||
'<field name="name"/>' +
|
||||
'<field name="instrument" can_create="false"/>' +
|
||||
'</form>',
|
||||
res_id: 1,
|
||||
archs: {
|
||||
'instrument,false,list': '<tree>'+
|
||||
'<field name="name"/>'+
|
||||
'</tree>',
|
||||
'instrument,false,search': '<search>'+
|
||||
'<field name="name"/>'+
|
||||
'</search>',
|
||||
},
|
||||
viewOptions: {
|
||||
mode: 'edit',
|
||||
},
|
||||
|
||||
mockRPC: function(route, args) {
|
||||
if (args.method === 'get_formview_id') {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
return this._super(route, args);
|
||||
},
|
||||
});
|
||||
|
||||
await testUtils.fields.many2one.clickOpenDropdown('instrument');
|
||||
|
||||
assert.containsNone(form, '.ui-autocomplete a:contains(Start typing...)');
|
||||
|
||||
await testUtils.fields.editInput(form.el.querySelector(".o_field_many2one[name=instrument] input"), "a");
|
||||
|
||||
assert.containsNone(form, '.ui-autocomplete a:contains(Create and Edit)');
|
||||
|
||||
await testUtils.fields.editInput(form.el.querySelector(".o_field_many2one[name=instrument] input"), "");
|
||||
await testUtils.fields.many2one.clickItem('instrument', 'Search More...');
|
||||
|
||||
var $modal = $('.modal-dialog.modal-lg');
|
||||
|
||||
assert.strictEqual($modal.length, 1, 'Modal present');
|
||||
|
||||
assert.strictEqual($modal.find('.modal-footer button').text(), "Cancel",
|
||||
'Only the cancel button is present in modal');
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('formviewdialog is not closed when button handlers return a rejected promise', async function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
this.data.partner.fields.poney_ids = { string: "Poneys", type: "one2many", relation: 'partner' };
|
||||
this.data.partner.records[0].poney_ids = [];
|
||||
var reject = true;
|
||||
|
||||
var parent = await createParent({
|
||||
data: this.data,
|
||||
archs: {
|
||||
'partner,false,form':
|
||||
'<form string="Partner">' +
|
||||
'<field name="poney_ids"><tree><field name="display_name"/></tree></field>' +
|
||||
'</form>',
|
||||
},
|
||||
});
|
||||
|
||||
new dialogs.FormViewDialog(parent, {
|
||||
res_model: 'partner',
|
||||
res_id: 1,
|
||||
buttons: [{
|
||||
text: 'Click me !',
|
||||
classes: "btn-secondary o_form_button_magic",
|
||||
close: true,
|
||||
click: function () {
|
||||
return reject ? Promise.reject() : Promise.resolve();
|
||||
},
|
||||
}],
|
||||
}).open();
|
||||
|
||||
await testUtils.nextTick();
|
||||
assert.strictEqual($('.modal').length, 1, "should have a modal displayed");
|
||||
|
||||
await testUtils.dom.click($('.modal .o_form_button_magic'));
|
||||
assert.strictEqual($('.modal').length, 1, "modal should still be opened");
|
||||
|
||||
reject = false;
|
||||
await testUtils.dom.click($('.modal .o_form_button_magic'));
|
||||
assert.strictEqual($('.modal').length, 0, "modal should be closed");
|
||||
|
||||
parent.destroy();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
|
@ -1,420 +0,0 @@
|
|||
odoo.define('web.data_export_tests', function (require) {
|
||||
"use strict";
|
||||
|
||||
const data = require('web.data');
|
||||
const framework = require('web.framework');
|
||||
const ListView = require('web.ListView');
|
||||
const testUtils = require('web.test_utils');
|
||||
|
||||
const createView = testUtils.createView;
|
||||
|
||||
QUnit.module('widgets', {
|
||||
beforeEach: function () {
|
||||
this.data = {
|
||||
'partner': {
|
||||
fields: {
|
||||
foo: {string: "Foo", type: "char"},
|
||||
bar: {string: "Bar", type: "char"},
|
||||
unexportable: {string: "Unexportable", type: "boolean", exportable: false},
|
||||
},
|
||||
records: [
|
||||
{
|
||||
id: 1,
|
||||
foo: "yop",
|
||||
bar: "bar-blup",
|
||||
}, {
|
||||
id: 2,
|
||||
foo: "yop",
|
||||
bar: "bar-yop",
|
||||
}, {
|
||||
id: 3,
|
||||
foo: "blup",
|
||||
bar: "bar-blup",
|
||||
}
|
||||
]
|
||||
},
|
||||
'ir.exports': {
|
||||
fields: {
|
||||
name: {string: "Name", type: "char"},
|
||||
},
|
||||
records: [],
|
||||
},
|
||||
};
|
||||
this.mockSession = {
|
||||
async user_has_group(g) { return g === 'base.group_allow_export'; }
|
||||
}
|
||||
this.mockDataExportRPCs = function (route) {
|
||||
if (route === '/web/export/formats') {
|
||||
return Promise.resolve([
|
||||
{tag: 'csv', label: 'CSV'},
|
||||
{tag: 'xls', label: 'Excel'},
|
||||
]);
|
||||
}
|
||||
if (route === '/web/export/get_fields') {
|
||||
return Promise.resolve([
|
||||
{
|
||||
field_type: "one2many",
|
||||
string: "Activities",
|
||||
required: false,
|
||||
value: "activity_ids/id",
|
||||
id: "activity_ids",
|
||||
params: {"model": "mail.activity", "prefix": "activity_ids", "name": "Activities"},
|
||||
relation_field: "res_id",
|
||||
children: true,
|
||||
}, {
|
||||
children: false,
|
||||
field_type: 'char',
|
||||
id: "foo",
|
||||
relation_field: null,
|
||||
required: false,
|
||||
string: 'Foo',
|
||||
value: "foo",
|
||||
}
|
||||
]);
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
};
|
||||
}
|
||||
}, function () {
|
||||
|
||||
QUnit.module('Data Export');
|
||||
|
||||
|
||||
QUnit.test('exporting all data in list view', async function (assert) {
|
||||
assert.expect(8);
|
||||
|
||||
var blockUI = framework.blockUI;
|
||||
var unblockUI = framework.unblockUI;
|
||||
framework.blockUI = function () {
|
||||
assert.step('block UI');
|
||||
};
|
||||
framework.unblockUI = function () {
|
||||
assert.step('unblock UI');
|
||||
};
|
||||
|
||||
var list = await createView({
|
||||
View: ListView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<tree><field name="foo"/></tree>',
|
||||
viewOptions: {
|
||||
hasActionMenus: true,
|
||||
},
|
||||
mockRPC: this.mockDataExportRPCs,
|
||||
session: {
|
||||
...this.mockSession,
|
||||
get_file: function (params) {
|
||||
assert.step(params.url);
|
||||
params.complete();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
await testUtils.dom.click(list.$('thead th.o_list_record_selector input'));
|
||||
|
||||
await testUtils.controlPanel.toggleActionMenu(list);
|
||||
await testUtils.controlPanel.toggleMenuItem(list, 'Export');
|
||||
|
||||
assert.strictEqual($('.modal').length, 1, "a modal dialog should be open");
|
||||
assert.strictEqual($('div.o_tree_column:contains(Activities)').length, 1,
|
||||
"the Activities field should be in the list of exportable fields");
|
||||
assert.strictEqual($('.modal .o_export_field').length, 1, "There should be only one export field");
|
||||
assert.strictEqual($('.modal .o_export_field').data('field_id'), 'foo', "There should be only one export field");
|
||||
|
||||
// select the field Description, click on add, then export and close
|
||||
await testUtils.dom.click($('.modal .o_tree_column:contains(Foo) .o_add_field'));
|
||||
await testUtils.dom.click($('.modal span:contains(Export)'));
|
||||
await testUtils.dom.click($('.modal span:contains(Close)'));
|
||||
list.destroy();
|
||||
framework.blockUI = blockUI;
|
||||
framework.unblockUI = unblockUI;
|
||||
assert.verifySteps([
|
||||
'block UI',
|
||||
'/web/export/csv',
|
||||
'unblock UI',
|
||||
]);
|
||||
});
|
||||
|
||||
QUnit.test('exporting data in list view (multi pages)', async function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
let expectedData;
|
||||
const list = await createView({
|
||||
View: ListView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<tree limit="2"><field name="foo"/></tree>',
|
||||
domain: [['id', '<', 1000]],
|
||||
viewOptions: {
|
||||
hasActionMenus: true,
|
||||
},
|
||||
mockRPC: this.mockDataExportRPCs,
|
||||
session: {
|
||||
...this.mockSession,
|
||||
get_file: function (params) {
|
||||
const data = JSON.parse(params.data.data);
|
||||
assert.deepEqual({ids: data.ids, domain: data.domain}, expectedData);
|
||||
params.complete();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// select all records (first page) and export
|
||||
expectedData = {
|
||||
ids: [1, 2],
|
||||
domain: [['id', '<', 1000]],
|
||||
};
|
||||
await testUtils.dom.click(list.$('thead th.o_list_record_selector input'));
|
||||
|
||||
await testUtils.controlPanel.toggleActionMenu(list);
|
||||
await testUtils.controlPanel.toggleMenuItem(list, 'Export');
|
||||
|
||||
assert.containsOnce(document.body, '.modal');
|
||||
|
||||
await testUtils.dom.click($('.modal span:contains(Export)'));
|
||||
await testUtils.dom.click($('.modal span:contains(Close)'));
|
||||
|
||||
// select all domain and export
|
||||
expectedData = {
|
||||
ids: false,
|
||||
domain: [['id', '<', 1000]],
|
||||
};
|
||||
await testUtils.dom.click(list.$('.o_list_selection_box .o_list_select_domain'));
|
||||
|
||||
await testUtils.controlPanel.toggleActionMenu(list);
|
||||
await testUtils.controlPanel.toggleMenuItem(list, 'Export');
|
||||
|
||||
assert.containsOnce(document.body, '.modal');
|
||||
|
||||
await testUtils.dom.click($('.modal span:contains(Export)'));
|
||||
await testUtils.dom.click($('.modal span:contains(Close)'));
|
||||
|
||||
list.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('exporting view with non-exportable field', async function (assert) {
|
||||
assert.expect(0);
|
||||
|
||||
var list = await createView({
|
||||
View: ListView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<tree><field name="unexportable"/></tree>',
|
||||
viewOptions: {
|
||||
hasActionMenus: true,
|
||||
},
|
||||
mockRPC: this.mockDataExportRPCs,
|
||||
session: {
|
||||
...this.mockSession,
|
||||
get_file: function (params) {
|
||||
assert.step(params.url);
|
||||
params.complete();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await testUtils.dom.click(list.$('thead th.o_list_record_selector input'));
|
||||
|
||||
await testUtils.controlPanel.toggleActionMenu(list);
|
||||
await testUtils.controlPanel.toggleMenuItem(list, 'Export');
|
||||
|
||||
list.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('saving fields list when exporting data', async function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
var create = data.DataSet.prototype.create;
|
||||
|
||||
data.DataSet.prototype.create = function () {
|
||||
assert.step('create');
|
||||
return Promise.resolve([]);
|
||||
};
|
||||
|
||||
var list = await createView({
|
||||
View: ListView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<tree><field name="foo"/></tree>',
|
||||
viewOptions: {
|
||||
hasActionMenus: true,
|
||||
},
|
||||
session: this.mockSession,
|
||||
mockRPC: this.mockDataExportRPCs,
|
||||
});
|
||||
|
||||
|
||||
// Open the export modal
|
||||
await testUtils.dom.click(list.$('thead th.o_list_record_selector input'));
|
||||
await testUtils.controlPanel.toggleActionMenu(list);
|
||||
await testUtils.controlPanel.toggleMenuItem(list, 'Export');
|
||||
|
||||
assert.strictEqual($('.modal').length, 1,
|
||||
"a modal dialog should be open");
|
||||
|
||||
// Select 'Activities' in fields to export
|
||||
await testUtils.dom.click($('.modal .o_export_tree_item:contains(Activities) .o_add_field'));
|
||||
assert.strictEqual($('.modal .o_fields_list .o_export_field').length, 2,
|
||||
"there should be two items in the fields list");
|
||||
// Save as template
|
||||
await testUtils.fields.editAndTrigger($('.modal .o_exported_lists_select'), 'new_template', ['change']);
|
||||
await testUtils.fields.editInput($('.modal .o_save_list .o_save_list_name'), 'fields list');
|
||||
await testUtils.dom.click($('.modal .o_save_list .o_save_list_btn'));
|
||||
|
||||
assert.verifySteps(['create'],
|
||||
"create should have been called");
|
||||
|
||||
// Close the modal and destroy list
|
||||
await testUtils.dom.click($('.modal button span:contains(Close)'));
|
||||
list.destroy();
|
||||
|
||||
// restore create function
|
||||
data.DataSet.prototype.create = create;
|
||||
});
|
||||
|
||||
QUnit.test('Export dialog UI test', async function (assert) {
|
||||
assert.expect(5);
|
||||
var list = await createView({
|
||||
View: ListView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<tree><field name="foo"/></tree>',
|
||||
viewOptions: {
|
||||
hasActionMenus: true,
|
||||
},
|
||||
session: this.mockSession,
|
||||
mockRPC: this.mockDataExportRPCs,
|
||||
});
|
||||
|
||||
|
||||
// Open the export modal
|
||||
await testUtils.dom.click(list.$('thead th.o_list_record_selector input'));
|
||||
await testUtils.controlPanel.toggleActionMenu(list);
|
||||
await testUtils.controlPanel.toggleMenuItem(list, 'Export');
|
||||
|
||||
assert.strictEqual($('.modal .o_export_tree_item:visible').length, 2, "There should be only two items visible");
|
||||
await testUtils.dom.click($('.modal .o_export_search_input'));
|
||||
$('.modal .o_export_search_input').val('Activities').trigger($.Event('input', {
|
||||
keyCode: 65,
|
||||
}));
|
||||
assert.strictEqual($('.modal .o_export_tree_item:visible').length, 1, "Only match item visible");
|
||||
// Add field
|
||||
await testUtils.dom.click($('.modal div:contains(Activities) .o_add_field'));
|
||||
assert.strictEqual($('.modal .o_fields_list li').length, 2, "There should be two fields in export field list.");
|
||||
assert.strictEqual($('.modal .o_fields_list li:eq(1)').text(), "Activities",
|
||||
"string of second field in export list should be 'Activities'");
|
||||
// Remove field
|
||||
await testUtils.dom.click($('.modal .o_fields_list li:first .o_remove_field'));
|
||||
assert.strictEqual($('.modal .o_fields_list li').length, 1, "There should be only one field in list");
|
||||
list.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('Direct export button invisible', async function (assert) {
|
||||
assert.expect(1)
|
||||
|
||||
let list = await createView({
|
||||
View: ListView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: `<tree export_xlsx="0"><field name="foo"/></tree>`,
|
||||
session: this.mockSession,
|
||||
});
|
||||
assert.containsNone(list, '.o_list_export_xlsx')
|
||||
list.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('Direct export list ', async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
let list = await createView({
|
||||
View: ListView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: `
|
||||
<tree export_xlsx="1">
|
||||
<field name="foo"/>
|
||||
<field name="bar"/>
|
||||
</tree>`,
|
||||
domain: [['bar', '!=', 'glou']],
|
||||
session: {
|
||||
...this.mockSession,
|
||||
get_file(args) {
|
||||
let data = JSON.parse(args.data.data);
|
||||
assert.strictEqual(args.url, '/web/export/xlsx', "should call get_file with the correct url");
|
||||
assert.deepEqual(data, {
|
||||
context: {},
|
||||
model: 'partner',
|
||||
domain: [['bar', '!=', 'glou']],
|
||||
groupby: [],
|
||||
ids: false,
|
||||
import_compat: false,
|
||||
fields: [{
|
||||
name: 'foo',
|
||||
label: 'Foo',
|
||||
type: 'char',
|
||||
}, {
|
||||
name: 'bar',
|
||||
label: 'Bar',
|
||||
type: 'char',
|
||||
}]
|
||||
}, "should be called with correct params");
|
||||
args.complete();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Download
|
||||
await testUtils.dom.click(list.$buttons.find('.o_list_export_xlsx'));
|
||||
|
||||
list.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('Direct export grouped list ', async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
let list = await createView({
|
||||
View: ListView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: `
|
||||
<tree>
|
||||
<field name="foo"/>
|
||||
<field name="bar"/>
|
||||
</tree>`,
|
||||
groupBy: ['foo', 'bar'],
|
||||
domain: [['bar', '!=', 'glou']],
|
||||
session: {
|
||||
...this.mockSession,
|
||||
get_file(args) {
|
||||
let data = JSON.parse(args.data.data);
|
||||
assert.strictEqual(args.url, '/web/export/xlsx', "should call get_file with the correct url");
|
||||
assert.deepEqual(data, {
|
||||
context: {},
|
||||
model: 'partner',
|
||||
domain: [['bar', '!=', 'glou']],
|
||||
groupby: ['foo', 'bar'],
|
||||
ids: false,
|
||||
import_compat: false,
|
||||
fields: [{
|
||||
name: 'foo',
|
||||
label: 'Foo',
|
||||
type: 'char',
|
||||
}, {
|
||||
name: 'bar',
|
||||
label: 'Bar',
|
||||
type: 'char',
|
||||
}]
|
||||
}, "should be called with correct params");
|
||||
args.complete();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await testUtils.dom.click(list.$buttons.find('.o_list_export_xlsx'));
|
||||
|
||||
list.destroy();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
|
@ -1,315 +0,0 @@
|
|||
odoo.define('web.domain_selector_tests', function (require) {
|
||||
"use strict";
|
||||
|
||||
const FormView = require("web.FormView");
|
||||
var DomainSelector = require("web.DomainSelector");
|
||||
var Widget = require("web.Widget");
|
||||
var testUtils = require("web.test_utils");
|
||||
const { createWebClient, doAction } = require('@web/../tests/webclient/helpers');
|
||||
const { registry } = require('@web/core/registry');
|
||||
const legacyViewRegistry = require('web.view_registry');
|
||||
|
||||
QUnit.module('widgets', {}, function () {
|
||||
|
||||
QUnit.module('DomainSelector', {
|
||||
beforeEach: function () {
|
||||
this.data = {
|
||||
partner: {
|
||||
fields: {
|
||||
foo: {string: "Foo", type: "char", searchable: true},
|
||||
bar: {string: "Bar", type: "boolean", searchable: true},
|
||||
nice_datetime: {string: "Datetime", type: "datetime", searchable: true},
|
||||
product_id: {string: "Product", type: "many2one", relation: "product", searchable: true},
|
||||
},
|
||||
records: [{
|
||||
id: 1,
|
||||
foo: "yop",
|
||||
bar: true,
|
||||
product_id: 37,
|
||||
}, {
|
||||
id: 2,
|
||||
foo: "blip",
|
||||
bar: true,
|
||||
product_id: false,
|
||||
}, {
|
||||
id: 4,
|
||||
foo: "abc",
|
||||
bar: false,
|
||||
product_id: 41,
|
||||
}],
|
||||
onchanges: {},
|
||||
},
|
||||
product: {
|
||||
fields: {
|
||||
name: {string: "Product Name", type: "char", searchable: true}
|
||||
},
|
||||
records: [{
|
||||
id: 37,
|
||||
display_name: "xphone",
|
||||
}, {
|
||||
id: 41,
|
||||
display_name: "xpad",
|
||||
}]
|
||||
},
|
||||
};
|
||||
},
|
||||
}, function () {
|
||||
|
||||
QUnit.test("creating a domain from scratch", async function (assert) {
|
||||
assert.expect(12);
|
||||
|
||||
var $target = $("#qunit-fixture");
|
||||
|
||||
// Create the domain selector and its mock environment
|
||||
var domainSelector = new DomainSelector(null, "partner", [], {
|
||||
readonly: false,
|
||||
debugMode: true,
|
||||
});
|
||||
await testUtils.mock.addMockEnvironment(domainSelector, {data: this.data});
|
||||
await domainSelector.appendTo($target);
|
||||
|
||||
// As we gave an empty domain, there should be a visible button to add
|
||||
// the first domain part
|
||||
var $domainAddFirstNodeButton = domainSelector.$(".o_domain_add_first_node_button:visible");
|
||||
assert.strictEqual($domainAddFirstNodeButton.length, 1,
|
||||
"there should be a button to create first domain element");
|
||||
|
||||
// Clicking on the button should add a visible field selector in the
|
||||
// widget so that the user can change the field chain
|
||||
await testUtils.dom.click($domainAddFirstNodeButton);
|
||||
var $fieldSelector = domainSelector.$(".o_field_selector:visible");
|
||||
assert.strictEqual($fieldSelector.length, 1,
|
||||
"there should be a field selector");
|
||||
|
||||
// Focusing the field selector input should open a field selector popover
|
||||
$fieldSelector.trigger('focusin');
|
||||
await testUtils.nextTick();
|
||||
var $fieldSelectorPopover = $fieldSelector.find(".o_field_selector_popover:visible");
|
||||
assert.strictEqual($fieldSelectorPopover.length, 1,
|
||||
"field selector popover should be visible");
|
||||
|
||||
// The field selector popover should contain the list of "partner"
|
||||
// fields. "Bar" should be among them.
|
||||
var $lis = $fieldSelectorPopover.find("li");
|
||||
var $barLi = $();
|
||||
$lis.each(function () {
|
||||
var $li = $(this);
|
||||
if ($li.html().indexOf("Bar") >= 0) {
|
||||
$barLi = $li;
|
||||
}
|
||||
});
|
||||
assert.strictEqual($barLi.length, 1,
|
||||
"field selector popover should contain the 'Bar' field");
|
||||
|
||||
// Clicking the "Bar" field should change the internal domain and this
|
||||
// should be displayed in the debug textarea
|
||||
await testUtils.dom.click($barLi);
|
||||
assert.containsOnce(domainSelector, "textarea.o_domain_debug_input");
|
||||
assert.strictEqual(
|
||||
domainSelector.$(".o_domain_debug_input").val(),
|
||||
'[["bar","=",True]]',
|
||||
"the domain input should contain a domain with 'bar'"
|
||||
);
|
||||
|
||||
// There should be a "+" button to add a domain part; clicking on it
|
||||
// should add the default "['id', '=', 1]" domain
|
||||
var $plus = domainSelector.$(".fa-plus-circle");
|
||||
assert.strictEqual($plus.length, 1, "there should be a '+' button");
|
||||
await testUtils.dom.click($plus);
|
||||
assert.strictEqual(
|
||||
domainSelector.$(".o_domain_debug_input").val(),
|
||||
'["&",["bar","=",True],["id","=",1]]',
|
||||
"the domain input should contain a domain with 'bar' and 'id'");
|
||||
|
||||
// There should be two "..." buttons to add a domain group; clicking on
|
||||
// the first one, should add this group with defaults "['id', '=', 1]"
|
||||
// domains and the "|" operator
|
||||
var $dots = domainSelector.$(".fa-ellipsis-h");
|
||||
assert.strictEqual($dots.length, 2, "there should be two '...' buttons");
|
||||
await testUtils.dom.click($dots.first());
|
||||
assert.strictEqual(
|
||||
domainSelector.$(".o_domain_debug_input").val(),
|
||||
'["&","&",["bar","=",True],"|",["id","=",1],["id","=",1],["id","=",1]]',
|
||||
"the domain input should contain a domain with 'bar', 'id' and a subgroup"
|
||||
);
|
||||
|
||||
// There should be five "-" buttons to remove domain part; clicking on
|
||||
// the two last ones, should leave a domain with only the "bar" and
|
||||
// "foo" fields, with the initial "&" operator
|
||||
var $minus = domainSelector.$(".o_domain_delete_node_button");
|
||||
assert.strictEqual($minus.length, 5, "there should be five 'x' buttons");
|
||||
await testUtils.dom.click($minus.last());
|
||||
await testUtils.dom.click(domainSelector.$(".o_domain_delete_node_button").last());
|
||||
assert.strictEqual(
|
||||
domainSelector.$(".o_domain_debug_input").val(),
|
||||
'["&",["bar","=",True],["id","=",1]]',
|
||||
"the domain input should contain a domain with 'bar' and 'id'"
|
||||
);
|
||||
domainSelector.destroy();
|
||||
});
|
||||
|
||||
QUnit.test("building a domain with a datetime", async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
var $target = $("#qunit-fixture");
|
||||
|
||||
// Create the domain selector and its mock environment
|
||||
var domainSelector = new DomainSelector(null, "partner", [["nice_datetime", "=", "2017-03-27 15:42:00"]], {
|
||||
readonly: false,
|
||||
});
|
||||
await testUtils.mock.addMockEnvironment(domainSelector, {data: this.data});
|
||||
await domainSelector.appendTo($target);
|
||||
|
||||
// Check that there is a datepicker to choose the date
|
||||
var $datepicker = domainSelector.$(".o_datepicker:visible");
|
||||
assert.strictEqual($datepicker.length, 1,
|
||||
"there should be a datepicker");
|
||||
|
||||
var val = $datepicker.find('input').val();
|
||||
await testUtils.dom.openDatepicker($datepicker);
|
||||
await testUtils.dom.clickFirst($('.bootstrap-datetimepicker-widget :not(.today)[data-action="selectDay"]'));
|
||||
assert.notEqual(domainSelector.$(".o_datepicker:visible input").val(), val,
|
||||
"datepicker value should have changed");
|
||||
await testUtils.dom.click($('.bootstrap-datetimepicker-widget a[data-action=close]'));
|
||||
|
||||
domainSelector.destroy();
|
||||
});
|
||||
|
||||
QUnit.test("building a domain with a m2o without following the relation", async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
var $target = $("#qunit-fixture");
|
||||
|
||||
// Create the domain selector and its mock environment
|
||||
var domainSelector = new DomainSelector(null, "partner", [["product_id", "ilike", 1]], {
|
||||
debugMode: true,
|
||||
readonly: false,
|
||||
});
|
||||
await testUtils.mock.addMockEnvironment(domainSelector, {data: this.data});
|
||||
await domainSelector.appendTo($target);
|
||||
|
||||
await testUtils.fields.editAndTrigger(domainSelector.$('.o_domain_leaf_value_input'),
|
||||
'pad', ['input', 'change']);
|
||||
assert.strictEqual(domainSelector.$('.o_domain_debug_input').val(), '[["product_id","ilike","pad"]]',
|
||||
"string should have been allowed as m2o value");
|
||||
|
||||
domainSelector.destroy();
|
||||
});
|
||||
|
||||
QUnit.test("editing a domain with `parent` key", async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
var $target = $("#qunit-fixture");
|
||||
|
||||
// Create the domain selector and its mock environment
|
||||
var domainSelector = new DomainSelector(null, "product", "[['name','=',parent.foo]]", {
|
||||
debugMode: true,
|
||||
readonly: false,
|
||||
});
|
||||
await testUtils.mock.addMockEnvironment(domainSelector, {data: this.data});
|
||||
await domainSelector.appendTo($target);
|
||||
|
||||
assert.strictEqual(domainSelector.$el.text(), "This domain is not supported.",
|
||||
"an error message should be displayed because of the `parent` key");
|
||||
|
||||
domainSelector.destroy();
|
||||
});
|
||||
|
||||
QUnit.test("creating a domain with a default option", async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
var $target = $("#qunit-fixture");
|
||||
|
||||
// Create the domain selector and its mock environment
|
||||
var domainSelector = new DomainSelector(null, "partner", [], {
|
||||
readonly: false,
|
||||
debugMode: true,
|
||||
default: [["foo","=","kikou"]],
|
||||
});
|
||||
await testUtils.mock.addMockEnvironment(domainSelector, {data: this.data});
|
||||
await domainSelector.appendTo($target);
|
||||
|
||||
// Clicking on the button should add a visible field selector in the
|
||||
// widget so that the user can change the field chain
|
||||
await testUtils.dom.click(domainSelector.$(".o_domain_add_first_node_button:visible"));
|
||||
|
||||
assert.strictEqual(
|
||||
domainSelector.$(".o_domain_debug_input").val(),
|
||||
'[["foo","=","kikou"]]',
|
||||
"the domain input should contain the default domain");
|
||||
|
||||
domainSelector.destroy();
|
||||
});
|
||||
|
||||
QUnit.test("inline domain editor in modal", async function (assert) {
|
||||
registry.category("views").remove("form"); // remove new form from registry
|
||||
legacyViewRegistry.add("form", FormView); // add legacy form -> will be wrapped and added to new registry
|
||||
|
||||
assert.expect(1);
|
||||
|
||||
const serverData = {
|
||||
actions: {
|
||||
5: {
|
||||
id: 5,
|
||||
name: "Partner Form",
|
||||
res_model: "partner",
|
||||
target: "new",
|
||||
type: "ir.actions.act_window",
|
||||
views: [["view_ref", "form"]],
|
||||
},
|
||||
},
|
||||
models: this.data,
|
||||
views: {
|
||||
"partner,view_ref,form": `
|
||||
<form>
|
||||
<field name="foo" string="Domain" widget="domain" options="{'model': 'partner'}"/>
|
||||
</form>
|
||||
`,
|
||||
},
|
||||
};
|
||||
|
||||
const webClient = await createWebClient({ serverData });
|
||||
await doAction(webClient, 5);
|
||||
assert.strictEqual(document.querySelector('div[name="foo"]').closest('.modal-body').style.overflow,
|
||||
'visible', "modal should have visible overflow if there is inline domain field widget");
|
||||
});
|
||||
|
||||
QUnit.test("edit a domain with the debug textarea", async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
const $target = $("#qunit-fixture");
|
||||
let newValue;
|
||||
|
||||
// Create the domain selector and its mock environment
|
||||
const Parent = Widget.extend({
|
||||
custom_events: {
|
||||
domain_changed: (e) => {
|
||||
assert.deepEqual(e.data.domain, newValue);
|
||||
assert.ok(e.data.debug);
|
||||
},
|
||||
},
|
||||
});
|
||||
const parent = new Parent(null);
|
||||
const domainSelector = new DomainSelector(parent, "partner", [["product_id", "ilike", 1]], {
|
||||
debugMode: true,
|
||||
readonly: false,
|
||||
});
|
||||
await testUtils.mock.addMockEnvironment(domainSelector, {data: this.data});
|
||||
await domainSelector.appendTo($target);
|
||||
|
||||
assert.containsOnce(domainSelector, ".o_domain_node", "should have a single domain node");
|
||||
newValue = `
|
||||
[
|
||||
['product_id', 'ilike', 1],
|
||||
['id', '=', 0]
|
||||
]`;
|
||||
await testUtils.fields.editAndTrigger(domainSelector.$('.o_domain_debug_input'), newValue, ["change"]);
|
||||
assert.strictEqual(domainSelector.$('.o_domain_debug_input').val(), newValue,
|
||||
"the domain should not have been formatted");
|
||||
assert.containsOnce(domainSelector, ".o_domain_node", "should still have a single domain node");
|
||||
|
||||
domainSelector.destroy();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,325 +0,0 @@
|
|||
odoo.define('web.model_field_selector_tests', function (require) {
|
||||
"use strict";
|
||||
|
||||
var ModelFieldSelector = require("web.ModelFieldSelector");
|
||||
var testUtils = require("web.test_utils");
|
||||
|
||||
QUnit.module('widgets', {}, function () {
|
||||
|
||||
QUnit.module('ModelFieldSelector', {
|
||||
beforeEach: function () {
|
||||
this.data = {
|
||||
partner: {
|
||||
fields: {
|
||||
foo: {string: "Foo", type: "char", searchable: true},
|
||||
bar: {string: "Bar", type: "boolean", searchable: true},
|
||||
product_id: {string: "Product", type: "many2one", relation: "product", searchable: true},
|
||||
},
|
||||
records: [{
|
||||
id: 1,
|
||||
foo: "yop",
|
||||
bar: true,
|
||||
product_id: 37,
|
||||
}, {
|
||||
id: 2,
|
||||
foo: "blip",
|
||||
bar: true,
|
||||
product_id: false,
|
||||
}, {
|
||||
id: 4,
|
||||
foo: "abc",
|
||||
bar: false,
|
||||
product_id: 41,
|
||||
}],
|
||||
onchanges: {},
|
||||
},
|
||||
product: {
|
||||
fields: {
|
||||
name: {string: "Product Name", type: "char", searchable: true}
|
||||
},
|
||||
records: [{
|
||||
id: 37,
|
||||
display_name: "xphone",
|
||||
}, {
|
||||
id: 41,
|
||||
display_name: "xpad",
|
||||
}]
|
||||
},
|
||||
};
|
||||
},
|
||||
}, function () {
|
||||
|
||||
QUnit.test("creating a field chain from scratch", async function (assert) {
|
||||
assert.expect(14);
|
||||
|
||||
var $target = $("#qunit-fixture");
|
||||
|
||||
// Create the field selector and its mock environment
|
||||
var fieldSelector = new ModelFieldSelector(null, "partner", [], {
|
||||
readonly: false,
|
||||
debugMode: true,
|
||||
});
|
||||
await testUtils.mock.addMockEnvironment(fieldSelector.popover, {data: this.data});
|
||||
await fieldSelector.appendTo($target);
|
||||
var $value = fieldSelector.$("> .o_field_selector_value");
|
||||
|
||||
// Focusing the field selector input should open a field selector popover
|
||||
fieldSelector.$el.trigger('focusin');
|
||||
var $fieldSelectorPopover = fieldSelector.$(".o_field_selector_popover:visible");
|
||||
assert.strictEqual($fieldSelectorPopover.length, 1,
|
||||
"field selector popover should be visible");
|
||||
|
||||
// The field selector popover should contain the list of "partner"
|
||||
// fields. "Bar" should be among them.
|
||||
var $lis = $fieldSelectorPopover.find("li");
|
||||
var $barLi = $();
|
||||
$lis.each(function () {
|
||||
var $li = $(this);
|
||||
if ($li.html().indexOf("Bar") >= 0) {
|
||||
$barLi = $li;
|
||||
}
|
||||
});
|
||||
assert.strictEqual($barLi.length, 1,
|
||||
"field selector popover should contain the 'Bar' field");
|
||||
|
||||
// Clicking the "Bar" field should close the popover and set the field
|
||||
// chain to "bar" as it is a basic field
|
||||
await testUtils.dom.click($barLi);
|
||||
assert.notOk($fieldSelectorPopover.is("visible"),
|
||||
"field selector popover should be closed now");
|
||||
assert.strictEqual(getValueFromDOM($value), "Bar",
|
||||
"field selector value should be displayed with a 'Bar' tag");
|
||||
|
||||
assert.deepEqual(fieldSelector.getSelectedField(), {
|
||||
model: "partner",
|
||||
name: "bar",
|
||||
searchable: true,
|
||||
string: "Bar",
|
||||
type: "boolean",
|
||||
}, "the selected field should be correctly set");
|
||||
|
||||
// Focusing the input again should open the same popover
|
||||
fieldSelector.$el.trigger('focusin');
|
||||
await testUtils.nextTick();
|
||||
assert.ok($fieldSelectorPopover.is(":visible"),
|
||||
"field selector popover should be visible");
|
||||
|
||||
// The field selector popover should contain the list of "partner"
|
||||
// fields. "Product" should be among them.
|
||||
$lis = $fieldSelectorPopover.find("li");
|
||||
var $productLi = $();
|
||||
$lis.each(function () {
|
||||
var $li = $(this);
|
||||
if ($li.html().indexOf("Product") >= 0) {
|
||||
$productLi = $li;
|
||||
}
|
||||
});
|
||||
assert.strictEqual($productLi.length, 1,
|
||||
"field selector popover should contain the 'Product' field");
|
||||
|
||||
// Clicking on the "Product" field should update the popover to show
|
||||
// the product fields (so only "Product Name" should be there)
|
||||
await testUtils.dom.click($productLi);
|
||||
$lis = $fieldSelectorPopover.find("li");
|
||||
assert.strictEqual($lis.length, 1,
|
||||
"there should be only one field proposition for 'product' model");
|
||||
assert.ok($lis.first().html().indexOf("Product Name") >= 0,
|
||||
"the name of the only suggestion should be 'Product Name'");
|
||||
|
||||
// Clicking on "Product Name" should close the popover and set the chain
|
||||
// to "product_id.name"
|
||||
await testUtils.dom.click($lis.first());
|
||||
assert.notOk($fieldSelectorPopover.is("visible"),
|
||||
"field selector popover should be closed now");
|
||||
assert.strictEqual(getValueFromDOM($value), "Product -> Product Name",
|
||||
"field selector value should be displayed with two tags: 'Product' and 'Product Name'");
|
||||
|
||||
// Remove the current selection and recreate it again
|
||||
fieldSelector.$el.trigger('focusin');
|
||||
await testUtils.nextTick();
|
||||
await testUtils.dom.click(fieldSelector.$('.o_field_selector_prev_page'));
|
||||
await testUtils.dom.click(fieldSelector.$('.o_field_selector_close'));
|
||||
|
||||
fieldSelector.$el.trigger('focusin');
|
||||
await testUtils.nextTick();
|
||||
$fieldSelectorPopover = fieldSelector.$(".o_field_selector_popover:visible");
|
||||
$lis = $fieldSelectorPopover.find("li");
|
||||
$productLi = $();
|
||||
$lis.each(function () {
|
||||
var $li = $(this);
|
||||
if ($li.html().indexOf("Product") >= 0) {
|
||||
$productLi = $li;
|
||||
}
|
||||
});
|
||||
assert.strictEqual($productLi.length, 1,
|
||||
"field selector popover should contain the 'Product' field");
|
||||
|
||||
await testUtils.dom.click($productLi);
|
||||
$lis = $fieldSelectorPopover.find("li");
|
||||
await testUtils.dom.click($lis.first());
|
||||
assert.notOk($fieldSelectorPopover.is("visible"),
|
||||
"field selector popover should be closed now");
|
||||
assert.strictEqual(getValueFromDOM($value), "Product -> Product Name",
|
||||
"field selector value should be displayed with two tags: 'Product' and 'Product Name'");
|
||||
|
||||
fieldSelector.destroy();
|
||||
|
||||
function getValueFromDOM($dom) {
|
||||
return _.map($dom.find(".o_field_selector_chain_part"), function (part) {
|
||||
return $(part).text().trim();
|
||||
}).join(" -> ");
|
||||
}
|
||||
});
|
||||
|
||||
QUnit.test("default field chain should set the page data correctly", async function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
var $target = $("#qunit-fixture");
|
||||
|
||||
// Create the field selector and its mock environment
|
||||
// passing 'product_id' as a prefilled field-chain
|
||||
var fieldSelector = new ModelFieldSelector(null, "partner", ['product_id'], {
|
||||
readonly: false,
|
||||
debugMode: true,
|
||||
});
|
||||
await testUtils.mock.addMockEnvironment(fieldSelector.popover, {data: this.data});
|
||||
await fieldSelector.appendTo($target);
|
||||
|
||||
// Focusing the field selector input should open a field selector popover
|
||||
fieldSelector.$el.trigger('focusin');
|
||||
var $fieldSelectorPopover = fieldSelector.$(".o_field_selector_popover:visible");
|
||||
assert.strictEqual($fieldSelectorPopover.length, 1,
|
||||
"field selector popover should be visible");
|
||||
|
||||
// The field selector popover should contain the list of "product"
|
||||
// fields. "Product Name" should be among them.
|
||||
var $lis = $fieldSelectorPopover.find("li");
|
||||
assert.strictEqual($lis.length, 1,
|
||||
"there should be only one field proposition for 'product' model");
|
||||
assert.ok($lis.first().html().indexOf("Product Name") >= 0,
|
||||
"the name of the only suggestion should be 'Product Name'");
|
||||
|
||||
|
||||
fieldSelector.destroy();
|
||||
});
|
||||
|
||||
QUnit.test("use the filter option", async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
var $target = $("#qunit-fixture");
|
||||
|
||||
// Create the field selector and its mock environment
|
||||
var fieldSelector = new ModelFieldSelector(null, "partner", [], {
|
||||
readonly: false,
|
||||
filter: function (field) {
|
||||
return field.type === 'many2one';
|
||||
},
|
||||
});
|
||||
await testUtils.mock.addMockEnvironment(fieldSelector.popover, {data: this.data});
|
||||
await fieldSelector.appendTo($target);
|
||||
|
||||
fieldSelector.$el.trigger('focusin');
|
||||
await testUtils.nextTick();
|
||||
var $fieldSelectorPopover = fieldSelector.$(".o_field_selector_popover:visible");
|
||||
var $lis = $fieldSelectorPopover.find("li");
|
||||
assert.strictEqual($lis.length, 1, "there should only be one element");
|
||||
assert.strictEqual($lis.text().trim(), "Product", "the available field should be the many2one");
|
||||
|
||||
fieldSelector.destroy();
|
||||
});
|
||||
|
||||
QUnit.test("default `showSearchInput` option", async function (assert) {
|
||||
assert.expect(6);
|
||||
|
||||
var $target = $("#qunit-fixture");
|
||||
|
||||
// Create the field selector and its mock environment
|
||||
var fieldSelector = new ModelFieldSelector(null, "partner", [], {
|
||||
readonly: false,
|
||||
});
|
||||
await testUtils.mock.addMockEnvironment(fieldSelector.popover, {data: this.data});
|
||||
await fieldSelector.appendTo($target);
|
||||
|
||||
fieldSelector.$el.trigger('focusin');
|
||||
await testUtils.nextTick();
|
||||
var $fieldSelectorPopover = fieldSelector.$(".o_field_selector_popover:visible");
|
||||
var $searchInput = $fieldSelectorPopover.find(".o_field_selector_search input");
|
||||
assert.strictEqual($searchInput.length, 1, "there should be a search input");
|
||||
|
||||
// without search
|
||||
assert.strictEqual($fieldSelectorPopover.find("li").length, 3, "there should be three available fields");
|
||||
assert.strictEqual($fieldSelectorPopover.find("li").text().trim().replace(/\s+/g, ' '), "Bar Foo Product", "the available field should be correct");
|
||||
await testUtils.fields.editAndTrigger($searchInput, 'xx', 'keyup');
|
||||
|
||||
assert.strictEqual($fieldSelectorPopover.find("li").length, 0, "there shouldn't be any element");
|
||||
await testUtils.fields.editAndTrigger($searchInput, 'Pro', 'keyup');
|
||||
assert.strictEqual($fieldSelectorPopover.find("li").length, 1, "there should only be one element");
|
||||
assert.strictEqual($fieldSelectorPopover.find("li").text().trim().replace(/\s+/g, ' '), "Product", "the available field should be the Product");
|
||||
|
||||
fieldSelector.destroy();
|
||||
});
|
||||
|
||||
QUnit.test("false `showSearchInput` option", async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
var $target = $("#qunit-fixture");
|
||||
|
||||
// Create the field selector and its mock environment
|
||||
var fieldSelector = new ModelFieldSelector(null, "partner", [], {
|
||||
readonly: false,
|
||||
showSearchInput: false,
|
||||
});
|
||||
await testUtils.mock.addMockEnvironment(fieldSelector.popover, { data: this.data });
|
||||
await fieldSelector.appendTo($target);
|
||||
|
||||
fieldSelector.$el.trigger('focusin');
|
||||
await testUtils.nextTick();
|
||||
var $fieldSelectorPopover = fieldSelector.$(".o_field_selector_popover:visible");
|
||||
var $searchInput = $fieldSelectorPopover.find(".o_field_selector_search input");
|
||||
assert.strictEqual($searchInput.length, 0, "there should be no search input");
|
||||
|
||||
fieldSelector.destroy();
|
||||
});
|
||||
|
||||
QUnit.test("create a field chain with value 1 i.e. TRUE_LEAF", async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
var $target = $("#qunit-fixture");
|
||||
|
||||
//create the field selector with domain value ["1"]
|
||||
var fieldSelector = new ModelFieldSelector(null, "partner", ["1"], {
|
||||
readonly: false,
|
||||
showSearchInput: false,
|
||||
});
|
||||
await testUtils.mock.addMockEnvironment(fieldSelector.popover, {data: this.data});
|
||||
await fieldSelector.appendTo($target);
|
||||
|
||||
var $fieldName = fieldSelector.$('.o_field_selector_chain_part');
|
||||
assert.strictEqual($fieldName.text().trim(), "1",
|
||||
"field name value should be 1.");
|
||||
|
||||
fieldSelector.destroy();
|
||||
});
|
||||
|
||||
QUnit.test("create a field chain with value 0 i.e. FALSE_LEAF", async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
var $target = $("#qunit-fixture");
|
||||
|
||||
//create the field selector with domain value ["0"]
|
||||
var fieldSelector = new ModelFieldSelector(null, "partner", ["0"], {
|
||||
readonly: false,
|
||||
showSearchInput: false,
|
||||
});
|
||||
await testUtils.mock.addMockEnvironment(fieldSelector.popover, {data: this.data});
|
||||
await fieldSelector.appendTo($target);
|
||||
|
||||
var $fieldName = fieldSelector.$('.o_field_selector_chain_part');
|
||||
assert.strictEqual($fieldName.text().trim(), "0",
|
||||
"field name value should be 0.");
|
||||
|
||||
fieldSelector.destroy();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,80 +0,0 @@
|
|||
odoo.define('web.name_and_signature_tests', function (require) {
|
||||
"use strict";
|
||||
|
||||
const { NameAndSignature } = require("web.name_and_signature");
|
||||
const testUtils = require("web.test_utils");
|
||||
const MockedNameAndSignature = NameAndSignature.extend({
|
||||
events: {
|
||||
...NameAndSignature.prototype.events,
|
||||
'signature_changed': () => {},
|
||||
},
|
||||
_onChangeSignature: () => {},
|
||||
_drawCurrentName: () => {},
|
||||
});
|
||||
|
||||
async function MockedNameAndSignatureGenerator (options) {
|
||||
const parent = $("#qunit-fixture");
|
||||
const mockedNameAndSignature = new MockedNameAndSignature(parent, options);
|
||||
await testUtils.mock.addMockEnvironment(mockedNameAndSignature, {
|
||||
mockRPC: function (route, args) {
|
||||
if (route == "/web/sign/get_fonts/") {
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
});
|
||||
await mockedNameAndSignature.appendTo(parent);
|
||||
await mockedNameAndSignature.resetSignature();
|
||||
return mockedNameAndSignature;
|
||||
}
|
||||
|
||||
QUnit.module('widgets legacy', {}, function () {
|
||||
QUnit.module('name_and_signature', {
|
||||
beforeEach: function () {
|
||||
this.defaultName = 'Don Toliver'
|
||||
},
|
||||
}, function () {
|
||||
QUnit.test("test name_and_signature widget", async function (assert) {
|
||||
assert.expect(5);
|
||||
const nameAndSignature = await MockedNameAndSignatureGenerator({
|
||||
defaultName: this.defaultName
|
||||
});
|
||||
assert.equal(nameAndSignature.signMode, 'auto');
|
||||
const nameInput = nameAndSignature.$el.find('.o_web_sign_name_input');
|
||||
assert.ok(nameInput.length);
|
||||
assert.equal(this.defaultName, nameInput.val());
|
||||
const drawButton = nameAndSignature.$el.find('.o_web_sign_draw_button');
|
||||
assert.ok(drawButton.length);
|
||||
await drawButton.click();
|
||||
assert.equal(nameAndSignature.signMode, 'draw');
|
||||
});
|
||||
|
||||
QUnit.test("test name_and_signature widget without name", async function (assert) {
|
||||
assert.expect(4);
|
||||
const nameAndSignature = await MockedNameAndSignatureGenerator({});
|
||||
assert.equal(nameAndSignature.signMode, 'auto');
|
||||
assert.ok(nameAndSignature.signatureAreaHidden);
|
||||
const nameInput = nameAndSignature.$el.find('.o_web_sign_name_input');
|
||||
assert.ok(nameInput.length);
|
||||
await nameInput.val(this.defaultName).trigger('input');
|
||||
assert.notOk(nameAndSignature.signatureAreaHidden);
|
||||
});
|
||||
|
||||
QUnit.test("test name_and_signature widget with noInputName and default name", async function (assert) {
|
||||
assert.expect(1);
|
||||
const nameAndSignature = await MockedNameAndSignatureGenerator({
|
||||
noInputName: true,
|
||||
defaultName: this.defaultName
|
||||
});
|
||||
assert.equal(nameAndSignature.signMode, 'auto');
|
||||
});
|
||||
|
||||
QUnit.test("test name_and_signature widget with noInputName", async function (assert) {
|
||||
assert.expect(1);
|
||||
const nameAndSignature = await MockedNameAndSignatureGenerator({
|
||||
noInputName: true,
|
||||
});
|
||||
assert.equal(nameAndSignature.signMode, 'draw');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,150 +0,0 @@
|
|||
odoo.define('web.week_days_tests', function (require) {
|
||||
"use strict";
|
||||
|
||||
const FormView = require('web.FormView');
|
||||
const testUtils = require('web.test_utils');
|
||||
|
||||
QUnit.module('WeeklyRecurrence', {
|
||||
beforeEach() {
|
||||
this.data = {
|
||||
partner: {
|
||||
fields: {
|
||||
id: {strin: "id", type:"integer"},
|
||||
mon: {string: "Mon", type: "boolean"},
|
||||
tue: {string: "Tue", type: "boolean"},
|
||||
wed: {string: "Wed", type: "boolean"},
|
||||
thu: {string: "Thu", type: "boolean"},
|
||||
fri: {string: "Fri", type: "boolean"},
|
||||
sat: {string: "Sat", type: "boolean"},
|
||||
sun: {string: "Sun", type: "boolean"},
|
||||
|
||||
},
|
||||
records: [
|
||||
{
|
||||
id: 1,
|
||||
mon: false,
|
||||
tue: false,
|
||||
wed: false,
|
||||
thu: false,
|
||||
fri: false,
|
||||
sat: false,
|
||||
sun: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
},
|
||||
}, function () {
|
||||
QUnit.module('WeeklyRecurrenceWidget');
|
||||
|
||||
QUnit.test('simple week recurrence widget', async function (assert) {
|
||||
assert.expect(14);
|
||||
|
||||
let writeCall = 0;
|
||||
const form = await testUtils.createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
res_id: 1,
|
||||
arch:
|
||||
`<form string="Partners">
|
||||
<sheet>
|
||||
<group>
|
||||
<widget name="week_days"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
mockRPC: function (route, args) {
|
||||
if (args.method === 'write') {
|
||||
writeCall++;
|
||||
if (writeCall === 1) {
|
||||
assert.ok(args.args[1].sun,
|
||||
"value of sunday should be true");
|
||||
this.data.partner.records[0].sun = args.args[1].sun;
|
||||
}
|
||||
if (writeCall === 2) {
|
||||
assert.notOk(args.args[1].sun,
|
||||
"value of sunday should be false");
|
||||
assert.ok(args.args[1].mon,
|
||||
"value of monday should be true");
|
||||
assert.ok(args.args[1].tue,
|
||||
"value of tuesday should be true");
|
||||
this.data.partner.records[0].sun = args.args[1].sun;
|
||||
this.data.partner.records[0].mon = args.args[1].mon;
|
||||
this.data.partner.records[0].tue = args.args[1].tue;
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
|
||||
assert.containsN(form, 'input:disabled', 7,
|
||||
"all inputs should be disabled in readonly mode");
|
||||
const labelsTexts = [...form.el.querySelectorAll('.o_recurrent_weekday_label')].map(el => el.innerText.trim());
|
||||
assert.deepEqual(labelsTexts, ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
|
||||
"labels should be short week names");
|
||||
|
||||
await testUtils.form.clickEdit(form);
|
||||
assert.containsNone(form, 'input:disabled', 7,
|
||||
"all inputs should be enabled in readonly mode");
|
||||
|
||||
await testUtils.dom.click(form.el.querySelector('input[id^="sun"]'));
|
||||
assert.ok(form.el.querySelector('input[id^="sun"]').checked,
|
||||
"sunday checkbox should be checked");
|
||||
await testUtils.form.clickSave(form);
|
||||
|
||||
await testUtils.form.clickEdit(form);
|
||||
await testUtils.dom.click(form.el.querySelector('input[id^="mon"]'));
|
||||
assert.ok(form.el.querySelector('input[id^="mon"]').checked,
|
||||
"monday checkbox should be checked");
|
||||
|
||||
await testUtils.dom.click(form.el.querySelector('input[id^="tue"]'));
|
||||
assert.ok(form.el.querySelector('input[id^="tue"]').checked,
|
||||
"tuesday checkbox should be checked");
|
||||
|
||||
// uncheck Sunday checkbox and check write call
|
||||
await testUtils.dom.click(form.el.querySelector('input[id^="sun"]'));
|
||||
assert.notOk(form.el.querySelector('input[id^="sun"]').checked,
|
||||
"sunday checkbox should be unchecked");
|
||||
|
||||
await testUtils.form.clickSave(form);
|
||||
assert.notOk(form.el.querySelector('input[id^="sun"]').checked,
|
||||
"sunday checkbox should be unchecked");
|
||||
assert.ok(form.el.querySelector('input[id^="mon"]').checked,
|
||||
"monday checkbox should be checked");
|
||||
assert.ok(form.el.querySelector('input[id^="tue"]').checked,
|
||||
"tuesday checkbox should be checked");
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('week recurrence widget show week start as per language configuration', async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
const form = await testUtils.createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
res_id: 1,
|
||||
arch:
|
||||
`<form string="Partners">
|
||||
<sheet>
|
||||
<group>
|
||||
<widget name="week_days"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
translateParameters: {
|
||||
week_start: 5,
|
||||
},
|
||||
});
|
||||
|
||||
const labelsTexts = [...form.el.querySelectorAll('.o_recurrent_weekday_label')].map(el => el.innerText.trim());
|
||||
assert.deepEqual(labelsTexts, ['Fri', 'Sat', 'Sun', 'Mon', 'Tue', 'Wed', 'Thu'],
|
||||
"labels should be short week names");
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue