Files
jikimo_sf/web_gantt/static/tests/gantt_dependency_tests.js
2023-04-14 17:42:23 +08:00

1345 lines
54 KiB
JavaScript

/** @odoo-module */
import testUtils, { createView } from "web.test_utils";
import { Domain } from "@web/core/domain";
import { registerCleanup } from "@web/../tests/helpers/cleanup";
import GanttView from "@web_gantt/js/gantt_view";
import GanttRenderer from "@web_gantt/js/gantt_renderer";
import GanttController from "@web_gantt/js/gantt_controller";
/**
* As the rendering of the connectors is made after the gantt rendering is injected in the dom and as the connectors
* are an owl component that needs to be mounted (async), we have no control on when they will actually be generated.
* For that reason we had to create the testPromise and extend both TaskGanttConnectorRenderer and TaskGanttConnectorView.
* */
let testPromise = testUtils.makeTestPromise();
const TestGanttRenderer = GanttRenderer.extend({
/**
* @override
*/
async updateConnectorContainerComponent() {
await this._super(...arguments);
return testPromise.resolve();
}
});
const TestGanttView = GanttView.extend({
config: Object.assign({}, GanttView.prototype.config, {
Renderer: TestGanttRenderer,
})
});
const actualDate = new Date(2021, 9, 10, 8, 0, 0);
const initialDate = new Date(actualDate.getTime() - actualDate.getTimezoneOffset() * 60 * 1000);
const ganttViewParams = {
arch: `<gantt date_start="planned_date_begin" date_stop="planned_date_end" default_scale="month" dependency_field="depend_on_ids"/>`,
domain: Domain.FALSE,
model: 'project.task',
viewOptions: {initialDate},
};
/**
* Returns the connector dict associated with the provided gantt
*
* @param gantt
* @return {Object} a dict of:
* - Keys:
* masterTaskId|masterTaskUserId|taskId|taskUserId
* - Values:
* connector
*/
function getConnectorsDict(gantt) {
const connectorsDict = { };
for (const connector of Object.values(gantt.renderer._connectors)) {
const connector_data = connector.data;
const masterUserId = JSON.parse(connector_data.masterRowId)[0].user_ids[0] || 0;
const slaveUserId = JSON.parse(connector_data.slaveRowId)[0].user_ids[0] || 0;
const testKey = `${connector_data.masterId}|${masterUserId}|${connector_data.slaveId}|${slaveUserId}`;
connectorsDict[testKey] = connector;
}
return connectorsDict;
}
const CSS = {
SELECTOR: {
CONNECTOR: 'svg.o_connector',
CONNECTOR_CONTAINER: '.o_connector_container',
CONNECTOR_CREATOR_BULLET: '.o_connector_creator_bullet',
CONNECTOR_CREATOR_WRAPPER: '.o_connector_creator_wrapper',
CONNECTOR_STROKE: '.o_connector_stroke',
CONNECTOR_STROKE_BUTTON: '.o_connector_stroke_button',
CONNECTOR_STROKE_BUTTONS: '.o_connector_stroke_buttons',
CONNECTOR_STROKE_RESCHEDULE_BUTTON: '.o_connector_stroke_reschedule_button',
CONNECTOR_STROKE_REMOVE_BUTTON: '.o_connector_stroke_remove_button',
INVISIBLE: '.invisible',
PILL: '.o_gantt_pill',
},
CLASS: {
CONNECTOR_HOVERED: 'o_connector_hovered',
PILL_HIGHLIGHT: 'highlight',
},
};
QUnit.module('LegacyViews > GanttView (legacy) > Gantt Dependency', {
async beforeEach() {
this.initialPopoverDefaultAnimation = Popover.Default.animation;
Popover.Default.animation = false;
testPromise = testUtils.makeTestPromise();
ganttViewParams.data = {
'project.task': {
fields: {
id: { string: 'ID', type: 'integer' },
name: { string: 'Name', type: 'char' },
planned_date_begin: { string: 'Start Date', type: 'datetime' },
planned_date_end: { string: 'Stop Date', type: 'datetime' },
project_id: { string: 'Project', type: 'many2one', relation: 'project.project' },
user_ids: { string: 'Assignees', type: 'many2many', relation: 'res.users' },
allow_task_dependencies: { string: 'Allow Task Dependencies', type: "boolean", default: true },
depend_on_ids: { string: 'Depends on', type: 'one2many', relation: 'project.task' },
display_warning_dependency_in_gantt: { string: 'Display warning dependency in Gantt', type: "boolean", default: true },
},
records: [
{
id: 1,
name: 'Task 1',
planned_date_begin: '2021-10-11 18:30:00',
planned_date_end: '2021-10-11 19:29:59',
project_id: 1,
user_ids: [1],
depend_on_ids: [],
},
{
id: 2,
name: 'Task 2',
planned_date_begin: '2021-10-12 11:30:00',
planned_date_end: '2021-10-12 12:29:59',
project_id: 1,
user_ids: [1],
depend_on_ids: [1],
},
],
},
'project.project': {
fields: {
id: {string: 'ID', type: 'integer'},
name: {string: 'Name', type: 'char'},
},
records: [
{id: 1, name: 'Project 1'},
],
},
'res.users': {
fields: {
id: {string: 'ID', type: 'integer'},
name: {string: 'Name', type: 'char'},
},
records: [
{id: 1, name: 'User 1'},
],
},
};
ganttViewParams.mockRPCHook = function (route, args) {
return null;
};
ganttViewParams.mockRPC = function (route, args) {
const prom = ganttViewParams.mockRPCHook(route, args);
if (prom !== null) {
return prom;
} else {
return this._super.apply(this, arguments);
}
};
ganttViewParams.View = TestGanttView;
},
async afterEach() {
Popover.Default.animation = this.initialPopoverDefaultAnimation;
},
});
QUnit.test('Connectors are correctly computed and rendered.', async function (assert) {
/**
* This test checks that:
* - That the connector is part of the props and both its props color is the expected one (=> 2 * testKeys.length tests).
* - There is no other connector than the one expected.
* - All connectors are rendered.
*/
/**
* Dict used to run all tests in one loop.
*
* - Keys:
* masterTaskId|masterTaskUserId|taskId|taskUserId
* - Values:
* n 'normal', w 'warning or e 'error'
*
* => Check that there is a connector between masterTaskId from group masterTaskUserId and taskId from group taskUserId with normal|error color.
*/
const tests = {
'1|1|2|1': 'n',
'1|1|2|3': 'n',
'2|1|3|0': 'n',
'2|3|3|0': 'n',
'2|1|4|2': 'n',
'2|3|4|3': 'n',
'4|2|6|1': 'n',
'4|3|6|3': 'n',
'5|0|6|1': 'n',
'5|0|6|3': 'n',
'6|1|7|1': 'n',
'6|1|7|2': 'n',
'6|3|7|2': 'n',
'6|3|7|3': 'n',
'7|1|8|1': 'n',
'7|2|8|1': 'n',
'7|2|8|3': 'n',
'7|3|8|3': 'n',
'8|1|9|2': 'n',
'8|3|9|2': 'n',
'10|2|11|2': 'e',
'12|2|13|2': 'w',
};
assert.expect(3 * Object.keys(tests).length + 2);
ganttViewParams.data = {
'project.task': {
fields: {
id: { string: 'ID', type: 'integer' },
name: { string: 'Name', type: 'char' },
planned_date_begin: { string: 'Start Date', type: 'datetime' },
planned_date_end: { string: 'Stop Date', type: 'datetime' },
project_id: { string: 'Project', type: 'many2one', relation: 'project.project' },
user_ids: { string: 'Assignees', type: 'many2many', relation: 'res.users' },
allow_task_dependencies: { string: 'Allow Task Dependencies', type: "boolean", default: true },
depend_on_ids: { string: 'Depends on', type: 'one2many', relation: 'project.task' },
},
records: [
{
id: 1,
name: 'Task 1',
planned_date_begin: '2021-10-11 18:30:00',
planned_date_end: '2021-10-11 19:29:59',
project_id: 1,
user_ids: [1],
depend_on_ids: [],
},
{
id: 2,
name: 'Task 2',
planned_date_begin: '2021-10-12 11:30:00',
planned_date_end: '2021-10-12 12:29:59',
project_id: 1,
user_ids: [1, 3],
depend_on_ids: [1],
},
{
id: 3,
name: 'Task 3',
planned_date_begin: '2021-10-13 06:30:00',
planned_date_end: '2021-10-13 07:29:59',
project_id: 1,
user_ids: [],
depend_on_ids: [2],
},
{
id: 4,
name: 'Task 4',
planned_date_begin: '2021-10-14 22:30:00',
planned_date_end: '2021-10-14 23:29:59',
project_id: 1,
user_ids: [2, 3],
depend_on_ids: [2],
},
{
id: 5,
name: 'Task 5',
planned_date_begin: '2021-10-15 01:53:10',
planned_date_end: '2021-10-15 02:34:34',
project_id: 1,
user_ids: [],
depend_on_ids: [],
},
{
id: 6,
name: 'Task 6',
planned_date_begin: '2021-10-16 23:00:00',
planned_date_end: '2021-10-16 23:21:01',
project_id: 1,
user_ids: [1, 3],
depend_on_ids: [4, 5],
},
{
id: 7,
name: 'Task 7',
planned_date_begin: '2021-10-17 10:30:12',
planned_date_end: '2021-10-17 11:29:59',
project_id: 1,
user_ids: [1, 2, 3],
depend_on_ids: [6],
},
{
id: 8,
name: 'Task 8',
planned_date_begin: '2021-10-18 06:30:12',
planned_date_end: '2021-10-18 07:29:59',
project_id: 1,
user_ids: [1, 3],
depend_on_ids: [7],
},
{
id: 9,
name: 'Task 9',
planned_date_begin: '2021-10-19 06:30:12',
planned_date_end: '2021-10-19 07:29:59',
project_id: 1,
user_ids: [2],
depend_on_ids: [8],
},
{
id: 10,
name: 'Task 10',
planned_date_begin: '2021-10-19 06:30:12',
planned_date_end: '2021-10-19 07:29:59',
project_id: 1,
user_ids: [2],
depend_on_ids: [],
},
{
id: 11,
name: 'Task 11',
planned_date_begin: '2021-10-18 06:30:12',
planned_date_end: '2021-10-18 07:29:59',
project_id: 1,
user_ids: [2],
depend_on_ids: [10],
},
{
id: 12,
name: 'Task 12',
planned_date_begin: '2021-10-18 06:30:12',
planned_date_end: '2021-10-19 07:29:59',
project_id: 1,
user_ids: [2],
depend_on_ids: [],
},
{
id: 13,
name: 'Task 13',
planned_date_begin: '2021-10-18 07:29:59',
planned_date_end: '2021-10-20 07:29:59',
project_id: 1,
user_ids: [2],
depend_on_ids: [12],
},
],
},
'project.project': {
fields: {
id: { string: 'ID', type: 'integer' },
name: { string: 'Name', type: 'char' },
},
records: [
{ id: 1, name: 'Project 1' },
],
},
'res.users': {
fields: {
id: { string: 'ID', type: 'integer' },
name: { string: 'Name', type: 'char' },
},
records: [
{ id: 1, name: 'User 1' },
{ id: 2, name: 'User 2' },
{ id: 3, name: 'User 3' },
{ id: 4, name: 'User 4' },
],
},
};
const gantt = await createView({ ...ganttViewParams, groupBy: ['user_ids'] });
registerCleanup(gantt.destroy);
await testPromise;
const connectorsDict = getConnectorsDict(gantt);
const connectorContainer = gantt.el.querySelector(CSS.SELECTOR.CONNECTOR_CONTAINER);
const connectorsDictCopy = { ...connectorsDict };
for (const [test_key, colorCode] of Object.entries(tests)) {
const [masterTaskId, masterTaskUserId, taskId, taskUserId] = test_key.split('|');
assert.ok(test_key in connectorsDict, `Connector between task ${masterTaskId} from group user ${masterTaskUserId} and task ${taskId} from group user ${taskUserId} should be present.`);
let color;
let connectorPropsColorMatch;
let colorMessage;
if (colorCode === 'n') {
color = gantt.renderer._connectorsStrokeColors.stroke;
connectorPropsColorMatch = !connectorsDict[test_key].style
|| !connectorsDict[test_key].style.stroke
|| !connectorsDict[test_key].style.stroke.color
|| connectorsDict[test_key].style.stroke.color === color;
colorMessage = 'Connector props style should be the default one';
} else {
switch (colorCode) {
case 'w':
color = gantt.renderer._connectorsStrokeWarningColors.stroke;
colorMessage = 'Connector props style should be the warning one';
break;
case 'e':
color = gantt.renderer._connectorsStrokeErrorColors.stroke;
colorMessage = 'Connector props style should be the error one';
break;
}
connectorPropsColorMatch = connectorsDict[test_key].style.stroke.color === color;
}
const connector_stroke = connectorContainer.querySelector(`${CSS.SELECTOR.CONNECTOR}[data-id="${connectorsDict[test_key].id}"] ${CSS.SELECTOR.CONNECTOR_STROKE}`);
assert.equal(connector_stroke.getAttribute('stroke'), color);
assert.ok(connectorPropsColorMatch, colorMessage);
delete connectorsDictCopy[test_key];
}
assert.notOk(Object.keys(connectorsDictCopy).length, 'There should not be more connectors than expected.');
assert.equal(gantt.el.querySelectorAll(CSS.SELECTOR.CONNECTOR).length, Object.keys(tests).length, 'All connectors should be rendered.');
});
QUnit.test('Connectors are rendered according to _shouldRenderRecordConnectors.', async function (assert) {
/**
* This test checks that _shouldRenderRecordConnectors effectively allows to prevent connectors to be rendered
* for records the function would return false for.
*/
assert.expect(1);
testUtils.mock.patch(TestGanttRenderer, {
_shouldRenderRecordConnectors(record) {
return record.id !== 1;
},
});
ganttViewParams.data = {
'project.task': {
fields: {
id: { string: 'ID', type: 'integer' },
name: { string: 'Name', type: 'char' },
planned_date_begin: { string: 'Start Date', type: 'datetime' },
planned_date_end: { string: 'Stop Date', type: 'datetime' },
project_id: { string: 'Project', type: 'many2one', relation: 'project.project' },
user_ids: { string: 'Assignees', type: 'many2many', relation: 'res.users' },
allow_task_dependencies: { string: 'Allow Task Dependencies', type: "boolean", default: true },
depend_on_ids: { string: 'Depends on', type: 'one2many', relation: 'project.task' },
},
records: [
{
id: 1,
name: 'Task 1',
planned_date_begin: '2021-10-11 18:30:00',
planned_date_end: '2021-10-11 19:29:59',
project_id: 1,
user_ids: [1],
depend_on_ids: [],
},
{
id: 2,
name: 'Task 2',
planned_date_begin: '2021-10-12 11:30:00',
planned_date_end: '2021-10-12 12:29:59',
project_id: 1,
user_ids: [1],
depend_on_ids: [1],
},
{
id: 3,
name: 'Task 3',
planned_date_begin: '2021-10-13 06:30:00',
planned_date_end: '2021-10-13 07:29:59',
project_id: 1,
user_ids: [],
depend_on_ids: [1,2],
},
],
},
'project.project': {
fields: {
id: { string: 'ID', type: 'integer' },
name: { string: 'Name', type: 'char' },
},
records: [
{ id: 1, name: 'Project 1' },
],
},
'res.users': {
fields: {
id: { string: 'ID', type: 'integer' },
name: { string: 'Name', type: 'char' },
},
records: [
{ id: 1, name: 'User 1' },
{ id: 2, name: 'User 2' },
{ id: 3, name: 'User 3' },
{ id: 4, name: 'User 4' },
],
},
};
const gantt = await createView({ ...ganttViewParams, groupBy: ['user_ids'] });
registerCleanup(gantt.destroy);
await testPromise;
const connectorsDict = getConnectorsDict(gantt);
assert.deepEqual(Object.keys(connectorsDict), ['2|1|3|0'], 'The only rendered connector should be the one from task_id 2 to task_id 3');
testUtils.mock.unpatch(TestGanttRenderer);
});
QUnit.test('Connectors are correctly computed and rendered when collapse_first_level is active.', async function (assert) {
/**
* This test checks that the connectors are correctly drew when collapse_first_level is active.
*/
assert.expect(9);
ganttViewParams.data = {
'project.task': {
fields: {
id: { string: 'ID', type: 'integer' },
name: { string: 'Name', type: 'char' },
planned_date_begin: { string: 'Start Date', type: 'datetime' },
planned_date_end: { string: 'Stop Date', type: 'datetime' },
project_id: { string: 'Project', type: 'many2one', relation: 'project.project' },
user_ids: { string: 'Assignees', type: 'many2many', relation: 'res.users' },
allow_task_dependencies: { string: 'Allow Task Dependencies', type: "boolean", default: true },
depend_on_ids: { string: 'Depends on', type: 'one2many', relation: 'project.task' },
},
records: [
{
id: 1,
name: 'Task 1',
planned_date_begin: '2021-10-11 18:30:00',
planned_date_end: '2021-10-11 19:29:59',
project_id: 1,
user_ids: [1],
depend_on_ids: [],
},
{
id: 2,
name: 'Task 2',
planned_date_begin: '2021-10-12 11:30:00',
planned_date_end: '2021-10-12 12:29:59',
project_id: 1,
user_ids: [1, 3],
depend_on_ids: [1],
},
{
id: 3,
name: 'Task 3',
planned_date_begin: '2021-10-13 06:30:00',
planned_date_end: '2021-10-13 07:29:59',
project_id: 1,
user_ids: [],
depend_on_ids: [2],
},
{
id: 4,
name: 'Task 4',
planned_date_begin: '2021-10-14 22:30:00',
planned_date_end: '2021-10-14 23:29:59',
project_id: 1,
user_ids: [2, 3],
depend_on_ids: [2],
},
{
id: 5,
name: 'Task 5',
planned_date_begin: '2021-10-15 01:53:10',
planned_date_end: '2021-10-15 02:34:34',
project_id: 1,
user_ids: [],
depend_on_ids: [],
},
{
id: 6,
name: 'Task 6',
planned_date_begin: '2021-10-16 23:00:00',
planned_date_end: '2021-10-16 23:21:01',
project_id: 1,
user_ids: [1, 3],
depend_on_ids: [4, 5],
},
{
id: 7,
name: 'Task 7',
planned_date_begin: '2021-10-17 10:30:12',
planned_date_end: '2021-10-17 11:29:59',
project_id: 1,
user_ids: [1, 2, 3],
depend_on_ids: [6],
},
{
id: 8,
name: 'Task 8',
planned_date_begin: '2021-10-18 06:30:12',
planned_date_end: '2021-10-18 07:29:59',
project_id: 1,
user_ids: [1, 3],
depend_on_ids: [7],
},
{
id: 9,
name: 'Task 9',
planned_date_begin: '2021-10-19 06:30:12',
planned_date_end: '2021-10-19 07:29:59',
project_id: 1,
user_ids: [2],
depend_on_ids: [8],
},
{
id: 10,
name: 'Task 10',
planned_date_begin: '2021-10-19 06:30:12',
planned_date_end: '2021-10-19 07:29:59',
project_id: 1,
user_ids: [2],
depend_on_ids: [],
},
{
id: 11,
name: 'Task 11',
planned_date_begin: '2021-10-18 06:30:12',
planned_date_end: '2021-10-18 07:29:59',
project_id: 1,
user_ids: [2],
depend_on_ids: [10],
},
{
id: 12,
name: 'Task 12',
planned_date_begin: '2021-10-18 06:30:12',
planned_date_end: '2021-10-19 07:29:59',
project_id: 1,
user_ids: [2],
depend_on_ids: [],
},
{
id: 13,
name: 'Task 13',
planned_date_begin: '2021-10-18 07:29:59',
planned_date_end: '2021-10-20 07:29:59',
project_id: 1,
user_ids: [2],
depend_on_ids: [12],
},
],
},
'project.project': {
fields: {
id: { string: 'ID', type: 'integer' },
name: { string: 'Name', type: 'char' },
},
records: [
{ id: 1, name: 'Project 1' },
],
},
'res.users': {
fields: {
id: { string: 'ID', type: 'integer' },
name: { string: 'Name', type: 'char' },
},
records: [
{ id: 1, name: 'User 1' },
{ id: 2, name: 'User 2' },
{ id: 3, name: 'User 3' },
{ id: 4, name: 'User 4' },
],
},
};
const ganttViewParamsWithCollapse = {
...ganttViewParams,
...{ arch: ganttViewParams.arch.replace('/>', ' collapse_first_level="1"/>') }
};
const gantt = await createView({ ...ganttViewParamsWithCollapse, groupBy: ['user_ids'] });
registerCleanup(gantt.destroy);
await testPromise;
assert.equal(gantt.el.querySelectorAll('.o_gantt_row_group.open').length, 4, '`collapse_first_level` is activated.');
function getConnectorCounts() {
return gantt.el.querySelectorAll('.o_connector').length;
}
let connectorsCount = getConnectorCounts();
assert.equal(connectorsCount, 22, 'All connectors are drawn.');
testPromise = testUtils.makeTestPromise();
gantt.el.querySelector('.o_gantt_row_group.open[data-row-id^="[{\\"user_ids\\":[1,\\"User 1\\"]}]"]').click();
await testPromise;
connectorsCount = getConnectorCounts();
assert.ok(gantt.el.querySelector('.o_gantt_row_group:not(.open)[data-row-id^="[{\\"user_ids\\":[1,\\"User 1\\"]}]"]'), 'Group has been closed.');
assert.equal(connectorsCount, 13, 'Only connectors between open groups are drawn.');
testPromise = testUtils.makeTestPromise();
gantt.el.querySelector('.o_gantt_row_group:not(.open)[data-row-id^="[{\\"user_ids\\":[1,\\"User 1\\"]}]"]').click();
await testPromise;
connectorsCount = getConnectorCounts();
assert.equal(connectorsCount, 22, 'All connectors are drawn after having reopen the only closed group.');
testPromise = testUtils.makeTestPromise();
gantt.el.querySelector('.o_gantt_row_group.open[data-row-id^="[{\\"user_ids\\":[1,\\"User 1\\"]}]"]').click();
await testPromise;
connectorsCount = getConnectorCounts();
assert.equal(connectorsCount, 13, 'Only connectors between open groups are drawn.');
testPromise = testUtils.makeTestPromise();
gantt.el.querySelector('.o_gantt_row_group.open[data-row-id^="[{\\"user_ids\\":[2,\\"User 2\\"]}]"]').click();
await testPromise;
connectorsCount = getConnectorCounts();
assert.equal(connectorsCount, 6, 'Only connectors between open groups are drawn.');
testPromise = testUtils.makeTestPromise();
gantt.el.querySelector('.o_gantt_row_group.open[data-row-id^="[{\\"user_ids\\":false}]"]').click();
await testPromise;
connectorsCount = getConnectorCounts();
assert.equal(connectorsCount, 4, 'Only connectors between open groups are drawn.');
testPromise = testUtils.makeTestPromise();
gantt.el.querySelector('.o_gantt_row_group.open[data-row-id^="[{\\"user_ids\\":[3,\\"User 3\\"]}]"]').click();
await testPromise;
connectorsCount = getConnectorCounts();
assert.equal(connectorsCount, 0, 'Only connectors between open groups are drawn.');
});
QUnit.test('Connector hovered state is triggered and color is set accordingly.', async function (assert) {
/**
* This test checks that:
* - The o_connector_hovered class is triggered according to the hover of the connector.
* - The color of the connector is set according to the provided styles.
*/
assert.expect(4);
const gantt = await createView({ ...ganttViewParams, groupBy: ['user_ids']});
registerCleanup(gantt.destroy);
await testPromise;
const connectorContainer = gantt.el.querySelector(CSS.SELECTOR.CONNECTOR_CONTAINER);
let connector = connectorContainer.querySelector(`${CSS.SELECTOR.CONNECTOR}[data-id="1"]`);
let connector_stroke = connector.querySelector(CSS.SELECTOR.CONNECTOR_STROKE);
assert.notOk(connector.classList.contains(CSS.CLASS.CONNECTOR_HOVERED), "Connectors that are not hovered don't contain the o_connector_hovered class.");
assert.equal(connector_stroke.getAttribute('stroke'), gantt.renderer._connectorsStrokeColors.stroke);
await testUtils.dom.triggerMouseEvent(connector, "mouseover");
await testUtils.nextTick();
await testUtils.returnAfterNextAnimationFrame();
connector = connectorContainer.querySelector(`${CSS.SELECTOR.CONNECTOR}[data-id="1"]`);
connector_stroke = connector.querySelector(CSS.SELECTOR.CONNECTOR_STROKE);
assert.ok(connector.classList.contains(CSS.CLASS.CONNECTOR_HOVERED), 'Hovered connectors contain the o_connector_hovered class');
assert.equal(connector_stroke.getAttribute('stroke'), gantt.renderer._connectorsStrokeColors.hoveredStroke);
});
QUnit.test('Buttons are displayed when hovering a connector.', async function (assert) {
assert.expect(2);
const gantt = await createView({ ...ganttViewParams, groupBy: ['user_ids']});
registerCleanup(gantt.destroy);
await testPromise;
const connectorContainer = gantt.el.querySelector(CSS.SELECTOR.CONNECTOR_CONTAINER);
const connector = connectorContainer.querySelector(`${CSS.SELECTOR.CONNECTOR}[data-id="1"]`);
assert.ok(connector.querySelector(CSS.SELECTOR.CONNECTOR_STROKE_BUTTON) === null, "Connectors that are not hovered don't display buttons.");
await testUtils.dom.triggerMouseEvent(connector, "mouseover");
await testUtils.nextTick();
await testUtils.returnAfterNextAnimationFrame();
assert.ok(connector.querySelector(CSS.SELECTOR.CONNECTOR_STROKE_BUTTON) !== null, "Connectors that are hovered display buttons.");
});
QUnit.test('Connector container is re-rendered.', async function (assert) {
assert.expect(1);
const gantt = await createView({ ...ganttViewParams, groupBy: ['user_ids']});
registerCleanup(gantt.destroy);
await testPromise;
testPromise = testUtils.makeTestPromise();
document.querySelector('button.o_gantt_button_next').click();
await testPromise;
assert.strictEqual(
document.querySelectorAll(`${CSS.SELECTOR.CONNECTOR_CONTAINER}`).length,
1,
"there should be a connector container."
);
});
/**
* We need to prevent the reload that is triggered after the rpc call to web_gantt_reschedule as it was
* causing race conditions.
*/
const TestConnectorButtonRPCGanttController = GanttController.extend({
reload() { },
});
// Connector's buttons RPC calls have been tested one by one as they trigger a reload of the view which
// was systematically causing the following test to fail.
async function testConnectorButtonRPC(assert, createButtonSelector, expectedStep) {
assert.expect(2);
ganttViewParams.mockRPCHook = (route, args) => {
if (args.model === 'project.task') {
if (args.method === 'web_gantt_reschedule' || args.method === 'write') {
const [rpc_arg1, rpc_arg2, rpc_arg3 = null] = args.args;
assert.step(`${args.method}|${rpc_arg1}|${JSON.stringify(rpc_arg2)}${rpc_arg3 ? `|${rpc_arg3}` : ''}`);
return Promise.resolve(true);
}
}
return null;
};
ganttViewParams.View = ganttViewParams.View.extend({
config: Object.assign({}, ganttViewParams.View.prototype.config, {
Controller: TestConnectorButtonRPCGanttController,
})
});
const gantt = await createView({ ...ganttViewParams, groupBy: ['user_ids']});
registerCleanup(gantt.destroy);
await testPromise;
const connectorContainer = gantt.el.querySelector(CSS.SELECTOR.CONNECTOR_CONTAINER);
const connector = connectorContainer.querySelector(`${CSS.SELECTOR.CONNECTOR}[data-id="1"]`);
await testUtils.dom.triggerMouseEvent(connector, "mouseover");
await testUtils.nextTick();
await testUtils.returnAfterNextAnimationFrame();
await testUtils.dom.click(connector.querySelector(createButtonSelector));
assert.verifySteps([expectedStep]);
}
QUnit.test('Correct RPC is called on connector buttons click.', async function (assert) {
await testConnectorButtonRPC(
assert,
`${CSS.SELECTOR.CONNECTOR_STROKE_REMOVE_BUTTON}`,
'write|2|{"depend_on_ids":[[3,1,false]]}'
);
});
QUnit.test('Correct RPC is called on connector buttons click.', async function (assert) {
await testConnectorButtonRPC(
assert,
`${CSS.SELECTOR.CONNECTOR_STROKE_RESCHEDULE_BUTTON}:first-of-type`,
'web_gantt_reschedule|backward|1|2'
);
});
QUnit.test('Correct RPC is called on connector buttons click.', async function (assert) {
await testConnectorButtonRPC(
assert,
`${CSS.SELECTOR.CONNECTOR_STROKE_RESCHEDULE_BUTTON}:last-of-type`,
'web_gantt_reschedule|forward|1|2'
);
});
QUnit.test('Correct RPC is called on connector buttons click.', async function (assert) {
ganttViewParams.data = {
'project.task': {
fields: {
id: { string: 'ID', type: 'integer' },
name: { string: 'Name', type: 'char' },
planned_date_begin: { string: 'Start Date', type: 'datetime' },
planned_date_end: { string: 'Stop Date', type: 'datetime' },
project_id: { string: 'Project', type: 'many2one', relation: 'project.project' },
user_ids: { string: 'Assignees', type: 'many2many', relation: 'res.users' },
allow_task_dependencies: { string: 'Allow Task Dependencies', type: "boolean", default: true },
depend_on_ids: { string: 'Depends on', type: 'one2many', relation: 'project.task' },
display_warning_dependency_in_gantt: { string: 'Display warning dependency in Gantt', type: "boolean", default: true },
},
records: [
{
id: 1,
name: 'Task 1',
planned_date_begin: '2021-10-12 11:30:00',
planned_date_end: '2021-10-12 12:29:59',
project_id: 1,
user_ids: [1],
depend_on_ids: [],
},{
id: 2,
name: 'Task 2',
planned_date_begin: '2021-10-11 18:30:00',
planned_date_end: '2021-10-11 19:29:59',
project_id: 1,
user_ids: [1],
depend_on_ids: [1],
},
],
},
'project.project': {
fields: {
id: { string: 'ID', type: 'integer' },
name: { string: 'Name', type: 'char' },
},
records: [
{ id: 1, name: 'Project 1' },
],
},
'res.users': {
fields: {
id: { string: 'ID', type: 'integer' },
name: { string: 'Name', type: 'char' },
},
records: [
{ id: 1, name: 'User 1' },
],
},
};
await testConnectorButtonRPC(
assert,
`${CSS.SELECTOR.CONNECTOR_STROKE_RESCHEDULE_BUTTON}:first-of-type`,
'web_gantt_reschedule|backward|1|2'
);
});
QUnit.test('Correct RPC is called on connector buttons click.', async function (assert) {
ganttViewParams.data = {
'project.task': {
fields: {
id: { string: 'ID', type: 'integer' },
name: { string: 'Name', type: 'char' },
planned_date_begin: { string: 'Start Date', type: 'datetime' },
planned_date_end: { string: 'Stop Date', type: 'datetime' },
project_id: { string: 'Project', type: 'many2one', relation: 'project.project' },
user_ids: { string: 'Assignees', type: 'many2many', relation: 'res.users' },
allow_task_dependencies: { string: 'Allow Task Dependencies', type: "boolean", default: true },
depend_on_ids: { string: 'Depends on', type: 'one2many', relation: 'project.task' },
display_warning_dependency_in_gantt: { string: 'Display warning dependency in Gantt', type: "boolean", default: true },
},
records: [
{
id: 1,
name: 'Task 1',
planned_date_begin: '2021-10-12 11:30:00',
planned_date_end: '2021-10-12 12:29:59',
project_id: 1,
user_ids: [1],
depend_on_ids: [],
},{
id: 2,
name: 'Task 2',
planned_date_begin: '2021-10-11 18:30:00',
planned_date_end: '2021-10-11 19:29:59',
project_id: 1,
user_ids: [1],
depend_on_ids: [1],
},
],
},
'project.project': {
fields: {
id: { string: 'ID', type: 'integer' },
name: { string: 'Name', type: 'char' },
},
records: [
{ id: 1, name: 'Project 1' },
],
},
'res.users': {
fields: {
id: { string: 'ID', type: 'integer' },
name: { string: 'Name', type: 'char' },
},
records: [
{ id: 1, name: 'User 1' },
],
},
};
await testConnectorButtonRPC(
assert,
`${CSS.SELECTOR.CONNECTOR_STROKE_RESCHEDULE_BUTTON}:last-of-type`,
'web_gantt_reschedule|forward|1|2'
);
});
QUnit.test('Hovering a task pill, all the pills of the same task, and their related connectors are highlighted.', async function (assert) {
/**
* This test checks that:
* - When hovering a pill:
* _ The pill gets highlighted.
* - The connectorCreators get visible on that pill.
* - All the pills (in case of m2m grouping) representing the same task are highlighted but their connectorCreators are invisible.
* - All the connected connectors are highlighted.
* - The connectors that are not connected to the pill are not highlighted.
* - The connectors buttons are not visible on the highlighted connectors (note: the buttons should only become visible when the connector is hovered).
*/
/**
* Dict used to run all tests in one loop.
*
* - Keys:
* masterTaskId|masterTaskUserId|taskId|taskUserId
* - Values:
* y 'expected to be hovered', n 'not expected to be hovered'
*
*/
const tests = {
'1|1|2|1': 'y',
'1|1|2|3': 'y',
'2|1|3|0': 'y',
'2|3|3|0': 'y',
'2|1|4|2': 'y',
'2|3|4|3': 'y',
'10|2|11|2': 'n',
};
assert.expect(3 * Object.keys(tests).length + 8);
ganttViewParams.data = {
'project.task': {
fields: {
id: { string: 'ID', type: 'integer' },
name: { string: 'Name', type: 'char' },
planned_date_begin: { string: 'Start Date', type: 'datetime' },
planned_date_end: { string: 'Stop Date', type: 'datetime' },
project_id: { string: 'Project', type: 'many2one', relation: 'project.project' },
user_ids: { string: 'Assignees', type: 'many2many', relation: 'res.users' },
allow_task_dependencies: { string: 'Allow Task Dependencies', type: "boolean", default: true },
depend_on_ids: { string: 'Depends on', type: 'one2many', relation: 'project.task' },
display_warning_dependency_in_gantt: { string: 'Display warning dependency in Gantt', type: "boolean", default: true },
},
records: [
{
id: 1,
name: 'Task 1',
planned_date_begin: '2021-10-11 18:30:00',
planned_date_end: '2021-10-10 19:29:59',
project_id: 1,
user_ids: [1],
depend_on_ids: [],
},
{
id: 2,
name: 'Task 2',
planned_date_begin: '2021-10-12 11:30:00',
planned_date_end: '2021-10-12 12:29:59',
project_id: 1,
user_ids: [1, 3],
depend_on_ids: [1],
},
{
id: 3,
name: 'Task 3',
planned_date_begin: '2021-10-13 06:30:00',
planned_date_end: '2021-10-13 07:29:59',
project_id: 1,
user_ids: [],
depend_on_ids: [2],
},
{
id: 4,
name: 'Task 4',
planned_date_begin: '2021-10-14 22:30:00',
planned_date_end: '2021-10-14 23:29:59',
project_id: 1,
user_ids: [2, 3],
depend_on_ids: [2],
},
{
id: 10,
name: 'Task 10',
planned_date_begin: '2021-10-19 06:30:12',
planned_date_end: '2021-10-19 07:29:59',
project_id: 1,
user_ids: [2],
depend_on_ids: [],
display_warning_dependency_in_gantt: false,
},
{
id: 11,
name: 'Task 11',
planned_date_begin: '2021-10-18 06:30:12',
planned_date_end: '2021-10-18 07:29:59',
project_id: 1,
user_ids: [2],
depend_on_ids: [10],
},
],
},
'project.project': {
fields: {
id: { string: 'ID', type: 'integer' },
name: { string: 'Name', type: 'char' },
},
records: [
{ id: 1, name: 'Project 1' },
],
},
'res.users': {
fields: {
id: { string: 'ID', type: 'integer' },
name: { string: 'Name', type: 'char' },
},
records: [
{ id: 1, name: 'User 1' },
{ id: 2, name: 'User 2' },
{ id: 3, name: 'User 3' },
],
},
};
const gantt = await createView({ ...ganttViewParams, groupBy: ['user_ids']});
registerCleanup(gantt.destroy);
await testPromise;
const connectorsDict = getConnectorsDict(gantt);
const connectorContainer = gantt.el.querySelector(CSS.SELECTOR.CONNECTOR_CONTAINER);
const taskPills = gantt.el.querySelectorAll(`${CSS.SELECTOR.PILL}[data-id="2"]`);
for (const taskPill of taskPills) {
// The pills are not highlighted.
assert.notOk(taskPill.classList.contains(CSS.CLASS.PILL_HIGHLIGHT), 'Pills should not be highlighted by default.');
// Check that connector creators (little pills antennas) are not displayed.
assert.ok(taskPill.parentElement.querySelectorAll(`${CSS.SELECTOR.CONNECTOR_CREATOR_WRAPPER}${CSS.SELECTOR.INVISIBLE}`).length === 2, 'Connector creators should be hidden by default.');
}
// Check that all connectors are not in hover state.
for (const test_key in tests) {
const connector = connectorContainer.querySelector(`${CSS.SELECTOR.CONNECTOR}[data-id="${connectorsDict[test_key].id}"]`);
assert.notOk(connector.classList.contains(CSS.CLASS.CONNECTOR_HOVERED), 'Connectors should not be in hovered state by default');
}
// Using jQuery trigger function as triggerEvent() for mouseenter does not bubble up and the event is not
// triggered in the renderer.
await $(taskPills[0]).trigger("mouseenter");
await testUtils.nextTick();
await testUtils.returnAfterNextAnimationFrame();
for (const taskPill of taskPills) {
// The pills are highlighted.
assert.ok(taskPill.classList.contains(CSS.CLASS.PILL_HIGHLIGHT), 'Pills should be highlighted when hovered (or when pill of the same id is hovered (m2m grouping)).');
// Check that connector creators (little pills antennas) are displayed (and only displayed) on the hovered pills.
const querySelector = `${CSS.SELECTOR.CONNECTOR_CREATOR_WRAPPER}${taskPill != taskPills[0] ? CSS.SELECTOR.INVISIBLE : `:not(${CSS.SELECTOR.INVISIBLE})`}`;
assert.ok(taskPill.parentElement.querySelectorAll(querySelector).length === 2, 'Connector creators should be displayed on the hovered pills and not on the others.');
}
// Check that all connectors are in the expected hover state.
for (const [test_key, hoverEffectExpected] of Object.entries(tests)) {
const connector = connectorContainer.querySelector(`${CSS.SELECTOR.CONNECTOR}[data-id="${connectorsDict[test_key].id}"]`);
if (hoverEffectExpected === 'y') {
assert.ok(connector.classList.contains(CSS.CLASS.CONNECTOR_HOVERED), 'Connectors that are connected to an highlighted pill should be in a hover state.');
} else {
assert.notOk(connector.classList.contains(CSS.CLASS.CONNECTOR_HOVERED), 'Connectors that are not connected to an highlighted pill should not be in a hover state.');
}
assert.ok(connector.querySelector(CSS.SELECTOR.CONNECTOR_STROKE_BUTTON) === null, "Connectors that are not hovered don't display buttons, even if they are highlighted.");
}
});
QUnit.test('Hovering a connector should cause the connected pills to get highlighted.', async function (assert) {
assert.expect(3);
const gantt = await createView({ ...ganttViewParams, groupBy: ['user_ids']});
registerCleanup(gantt.destroy);
await testPromise;
const connector = gantt.el.querySelector(`${CSS.SELECTOR.CONNECTOR}[data-id="1"]`);
let taskPills = gantt.el.querySelectorAll(`${CSS.SELECTOR.PILL}:not(.${CSS.CLASS.PILL_HIGHLIGHT})`);
assert.equal(taskPills.length, 2, 'Pills should not be highlighted by default.');
await testUtils.dom.triggerMouseEvent(connector, "mouseover");
await testUtils.nextTick();
await testUtils.returnAfterNextAnimationFrame();
taskPills = gantt.el.querySelectorAll(`${CSS.SELECTOR.PILL}.${CSS.CLASS.PILL_HIGHLIGHT}`);
assert.equal(taskPills.length, 2, 'Pills should be highlighted when linked connector is hovered.');
// Check that connector creators (little pills antennas) are displayed (and only displayed) on the hovered pills.
const querySelector = `${CSS.SELECTOR.CONNECTOR_CREATOR_WRAPPER}:not(${CSS.SELECTOR.INVISIBLE})`;
assert.ok(gantt.el.querySelectorAll(querySelector).length === 0, 'Connector creators should not be displayed if the pill is not hovered.');
});
QUnit.test('Connectors are displayed behind pills, except on hover.', async function (assert) {
assert.expect(2);
ganttViewParams.data = {
'project.task': {
fields: {
id: { string: 'ID', type: 'integer' },
name: { string: 'Name', type: 'char' },
planned_date_begin: { string: 'Start Date', type: 'datetime' },
planned_date_end: { string: 'Stop Date', type: 'datetime' },
project_id: { string: 'Project', type: 'many2one', relation: 'project.project' },
user_ids: { string: 'Assignees', type: 'many2many', relation: 'res.users' },
allow_task_dependencies: { string: 'Allow Task Dependencies', type: "boolean", default: true },
depend_on_ids: { string: 'Depends on', type: 'one2many', relation: 'project.task' },
display_warning_dependency_in_gantt: { string: 'Display warning dependency in Gantt', type: "boolean", default: true },
},
records: [
{
id: 1,
name: 'Task 1',
planned_date_begin: '2021-10-01 18:30:00',
planned_date_end: '2021-10-02 19:29:59',
project_id: 1,
user_ids: [1],
depend_on_ids: [],
},
{
id: 2,
name: 'Task 2',
planned_date_begin: '2021-10-04 11:30:00',
planned_date_end: '2021-10-05 12:29:59',
project_id: 1,
user_ids: [1],
depend_on_ids: [],
},
{
id: 3,
name: 'Task 3',
planned_date_begin: '2021-10-15 06:30:00',
planned_date_end: '2021-10-15 07:29:59',
project_id: 1,
user_ids: [1],
depend_on_ids: [1],
},
],
},
'project.project': {
fields: {
id: { string: 'ID', type: 'integer' },
name: { string: 'Name', type: 'char' },
},
records: [
{ id: 1, name: 'Project 1' },
],
},
'res.users': {
fields: {
id: { string: 'ID', type: 'integer' },
name: { string: 'Name', type: 'char' },
},
records: [
{ id: 1, name: 'User 1' },
],
},
};
const gantt = await createView({ ...ganttViewParams, groupBy: ['user_ids']});
registerCleanup(gantt.destroy);
await testPromise;
// For this tests, we need the elements to be visible in the viewport.
const viewElements = [...document.getElementById('qunit-fixture').children];
viewElements.forEach(el => document.body.prepend(el));
// As connectors have been generated based on the pills positions, we need to preserve the
// previous width of the gantt view.
const client = document.querySelector('.o_web_client');
client.style.width = '1000px';
await testUtils.nextTick();
await testUtils.returnAfterNextAnimationFrame();
const taskPill = gantt.el.querySelector(`${CSS.SELECTOR.PILL}[data-id="2"]`);
const taskPillLocation = taskPill.getBoundingClientRect();
const testLocationLeft = taskPillLocation.left + taskPill.offsetWidth/2;
const testLocationTop = taskPillLocation.top + taskPill.offsetHeight/2;
let test = document.elementFromPoint(testLocationLeft, testLocationTop);
assert.deepEqual(test, taskPill, "taskPill position");
const connector = gantt.el.querySelector(`${CSS.SELECTOR.CONNECTOR}[data-id="1"]`);
await testUtils.dom.triggerMouseEvent(connector, "mouseover");
await testUtils.nextTick();
await testUtils.returnAfterNextAnimationFrame();
test = document.elementFromPoint(taskPillLocation.left, testLocationTop);
assert.deepEqual(test.closest(CSS.SELECTOR.CONNECTOR), connector, "connector position");
});
QUnit.test('Create a connector from the gantt view.', async function (assert) {
assert.expect(2);
ganttViewParams.data = {
'project.task': {
fields: {
id: { string: 'ID', type: 'integer' },
name: { string: 'Name', type: 'char' },
planned_date_begin: { string: 'Start Date', type: 'datetime' },
planned_date_end: { string: 'Stop Date', type: 'datetime' },
project_id: { string: 'Project', type: 'many2one', relation: 'project.project' },
user_ids: { string: 'Assignees', type: 'many2many', relation: 'res.users' },
allow_task_dependencies: { string: 'Allow Task Dependencies', type: "boolean", default: true },
depend_on_ids: { string: 'Depends on', type: 'one2many', relation: 'project.task' },
display_warning_dependency_in_gantt: { string: 'Display warning dependency in Gantt', type: "boolean", default: true },
},
records: [
{
id: 1,
name: 'Task 1',
planned_date_begin: '2021-10-11 18:30:00',
planned_date_end: '2021-10-11 19:29:59',
project_id: 1,
user_ids: [1],
depend_on_ids: [],
},
{
id: 2,
name: 'Task 2',
planned_date_begin: '2021-10-12 11:30:00',
planned_date_end: '2021-10-12 12:29:59',
project_id: 1,
user_ids: [1],
depend_on_ids: [],
},
],
},
'project.project': {
fields: {
id: {string: 'ID', type: 'integer'},
name: {string: 'Name', type: 'char'},
},
records: [
{id: 1, name: 'Project 1'},
],
},
'res.users': {
fields: {
id: {string: 'ID', type: 'integer'},
name: {string: 'Name', type: 'char'},
},
records: [
{id: 1, name: 'User 1'},
],
},
};
ganttViewParams.mockRPCHook = (route, args) => {
if (args.model === 'project.task' && args.method === 'write') {
const [rpc_arg1, rpc_arg2] = args.args;
assert.step(`${args.method}|${rpc_arg1}|${JSON.stringify(rpc_arg2)}`);
return Promise.resolve(true);
}
return null;
};
const gantt = await createView({ ...ganttViewParams, groupBy: ['user_ids']});
registerCleanup(gantt.destroy);
await testPromise;
let taskPill = gantt.el.querySelector(`${CSS.SELECTOR.PILL}[data-id="1"]`);
await $(taskPill).trigger("mouseenter");
await testUtils.nextTick();
await testUtils.returnAfterNextAnimationFrame();
const connectorCreator = taskPill.parentElement.querySelector(`${CSS.SELECTOR.CONNECTOR_CREATOR_WRAPPER}:not(${CSS.SELECTOR.INVISIBLE}) ${CSS.SELECTOR.CONNECTOR_CREATOR_BULLET}`);
await testUtils.dom.triggerEvents(connectorCreator, "mousedown", { bubbles: true });
taskPill = gantt.el.querySelectorAll(`${CSS.SELECTOR.PILL}[data-id="2"]`);
await testUtils.dom.triggerEvents(taskPill, "mouseup", { bubbles: true });
assert.verifySteps(['write|2|{"depend_on_ids":[[4,1,false]]}'], 'Connector ui creation from task 1 to task 2 should result in an rpc call on project.task write.');
});