Odoo18-Base/extra-addons/web_gantt/static/tests/gantt_dependency.test.js

922 lines
31 KiB
JavaScript

import { beforeEach, describe, expect, test } from "@odoo/hoot";
import {
hover,
pointerDown,
queryAll,
queryFirst,
queryOne,
queryRect,
resize,
} from "@odoo/hoot-dom";
import { advanceFrame, animationFrame, mockDate, runAllTimers } from "@odoo/hoot-mock";
import {
contains,
defineModels,
fields,
findComponent,
models,
onRpc,
patchWithCleanup,
} from "@web/../tests/web_test_helpers";
import {
clickConnectorButton,
getConnector,
getConnectorMap,
} from "@web_gantt/../tests/gantt_dependency_helpers";
import { COLORS } from "@web_gantt/gantt_connector";
import {
CLASSES,
SELECTORS,
getPill,
getPillWrapper,
mountGanttView,
} from "./web_gantt_test_helpers";
import { GanttRenderer } from "@web_gantt/gantt_renderer";
/** @typedef {import("@web_gantt/gantt_renderer").ConnectorProps} ConnectorProps */
/** @typedef {import("@web_gantt/gantt_renderer").PillId} PillId */
/**
* @typedef {`[${ResId},${ResId},${ResId},${ResId}]`} ConnectorTaskIds
* In the following order: [masterTaskId, masterTaskUserId, taskId, taskUserId]
*/
/** @typedef {number | false} ResId */
const ganttViewParams = {
resModel: "project.task",
arch: /* xml */ `<gantt date_start="planned_date_begin" date_stop="date_deadline" default_scale="month" dependency_field="depend_on_ids" color="color" />`,
groupBy: ["user_ids"],
};
let nextColor = 1;
class ProjectTask extends models.Model {
_name = "project.task";
name = fields.Char();
planned_date_begin = fields.Datetime({ string: "Start Date" });
date_deadline = fields.Datetime({ string: "Stop Date" });
user_ids = fields.Many2many({ string: "Assignees", relation: "res.users" });
allow_task_dependencies = fields.Boolean({ default: true });
depend_on_ids = fields.One2many({ string: "Depends on", relation: "project.task" });
display_warning_dependency_in_gantt = fields.Boolean({ default: true });
color = fields.Integer({ default: () => nextColor++ });
_records = [
{
id: 1,
name: "Task 1",
planned_date_begin: "2021-10-11 18:30:00",
date_deadline: "2021-10-11 19:29:59",
user_ids: [1],
depend_on_ids: [],
},
{
id: 2,
name: "Task 2",
planned_date_begin: "2021-10-12 11:30:00",
date_deadline: "2021-10-12 12:29:59",
user_ids: [1, 3],
depend_on_ids: [1],
},
{
id: 3,
name: "Task 3",
planned_date_begin: "2021-10-13 06:30:00",
date_deadline: "2021-10-13 07:29:59",
user_ids: [],
depend_on_ids: [2],
},
{
id: 4,
name: "Task 4",
planned_date_begin: "2021-10-14 22:30:00",
date_deadline: "2021-10-14 23:29:59",
user_ids: [2, 3],
depend_on_ids: [2],
},
{
id: 5,
name: "Task 5",
planned_date_begin: "2021-10-15 01:53:10",
date_deadline: "2021-10-15 02:34:34",
user_ids: [],
depend_on_ids: [],
},
{
id: 6,
name: "Task 6",
planned_date_begin: "2021-10-16 23:00:00",
date_deadline: "2021-10-16 23:21:01",
user_ids: [1, 3],
depend_on_ids: [4, 5],
},
{
id: 7,
name: "Task 7",
planned_date_begin: "2021-10-17 10:30:12",
date_deadline: "2021-10-17 11:29:59",
user_ids: [1, 2, 3],
depend_on_ids: [6],
},
{
id: 8,
name: "Task 8",
planned_date_begin: "2021-10-18 06:30:12",
date_deadline: "2021-10-18 07:29:59",
user_ids: [1, 3],
depend_on_ids: [7],
},
{
id: 9,
name: "Task 9",
planned_date_begin: "2021-10-19 06:30:12",
date_deadline: "2021-10-19 07:29:59",
user_ids: [2],
depend_on_ids: [8],
},
{
id: 10,
name: "Task 10",
planned_date_begin: "2021-10-19 06:30:12",
date_deadline: "2021-10-19 07:29:59",
user_ids: [2],
depend_on_ids: [],
},
{
id: 11,
name: "Task 11",
planned_date_begin: "2021-10-18 06:30:12",
date_deadline: "2021-10-18 07:29:59",
user_ids: [2],
depend_on_ids: [10],
},
{
id: 12,
name: "Task 12",
planned_date_begin: "2021-10-18 06:30:12",
date_deadline: "2021-10-19 07:29:59",
user_ids: [2],
depend_on_ids: [],
},
{
id: 13,
name: "Task 13",
planned_date_begin: "2021-10-18 07:29:59",
date_deadline: "2021-10-20 07:29:59",
user_ids: [2],
depend_on_ids: [12],
},
];
}
class ResUsers extends models.Model {
_name = "res.users";
name = fields.Char();
_records = [
{ id: 1, name: "User 1" },
{ id: 2, name: "User 2" },
{ id: 3, name: "User 3" },
{ id: 4, name: "User 4" },
];
}
defineModels([ProjectTask, ResUsers]);
describe.current.tags("desktop");
beforeEach(() => mockDate("2021-10-10T08:00:00", +1));
test("Connectors are correctly computed and rendered.", async () => {
/**
* @type {Map<ConnectorTaskIds, keyof typeof COLORS>}
* => Check that there is a connector between masterTaskId from group masterTaskUserId and taskId from group taskUserId with normal|error color.
*/
const testMap = new Map([
["[1,1,2,1]", "default"],
["[1,1,2,3]", "default"],
["[2,1,3,false]", "default"],
["[2,3,3,false]", "default"],
["[2,1,4,2]", "default"],
["[2,3,4,3]", "default"],
["[4,2,6,1]", "default"],
["[4,3,6,3]", "default"],
["[5,false,6,1]", "default"],
["[5,false,6,3]", "default"],
["[6,1,7,1]", "default"],
["[6,1,7,2]", "default"],
["[6,3,7,2]", "default"],
["[6,3,7,3]", "default"],
["[7,1,8,1]", "default"],
["[7,2,8,1]", "default"],
["[7,2,8,3]", "default"],
["[7,3,8,3]", "default"],
["[8,1,9,2]", "default"],
["[8,3,9,2]", "default"],
["[10,2,11,2]", "error"],
["[12,2,13,2]", "warning"],
]);
const view = await mountGanttView(ganttViewParams);
const renderer = findComponent(view, (c) => c instanceof GanttRenderer);
const connectorMap = getConnectorMap(renderer);
for (const [testKey, colorCode] of testMap.entries()) {
const [masterTaskId, masterTaskUserId, taskId, taskUserId] = JSON.parse(testKey);
expect(connectorMap.has(testKey)).toBe(true, {
message: `There should be a connector between task ${masterTaskId} from group user ${masterTaskUserId} and task ${taskId} from group user ${taskUserId}.`,
});
const connector = connectorMap.get(testKey);
const connectorEl = getConnector(connector.id);
expect(connectorEl).not.toBe(null);
const connectorStroke = queryFirst(SELECTORS.connectorStroke, { root: connectorEl });
expect(connectorStroke).toHaveAttribute("stroke", COLORS[colorCode].color);
}
expect(testMap).toHaveLength(connectorMap.size);
expect(SELECTORS.connector).toHaveCount(testMap.size);
});
test("Connectors are correctly rendered.", async () => {
patchWithCleanup(GanttRenderer.prototype, {
shouldRenderRecordConnectors(record) {
return record.id !== 1;
},
});
ProjectTask._records = [
{
id: 1,
name: "Task 1",
planned_date_begin: "2021-10-11 18:30:00",
date_deadline: "2021-10-11 19:29:59",
user_ids: [1],
depend_on_ids: [],
},
{
id: 2,
name: "Task 2",
planned_date_begin: "2021-10-12 11:30:00",
date_deadline: "2021-10-12 12:29:59",
user_ids: [1],
depend_on_ids: [1],
},
{
id: 3,
name: "Task 3",
planned_date_begin: "2021-10-13 06:30:00",
date_deadline: "2021-10-13 07:29:59",
user_ids: [],
depend_on_ids: [1, 2],
},
];
const view = await mountGanttView(ganttViewParams);
const renderer = findComponent(view, (c) => c instanceof GanttRenderer);
const connectorMap = getConnectorMap(renderer);
expect([...connectorMap.keys()]).toEqual(["[2,1,3,false]"], {
message: "The only rendered connector should be the one from task_id 2 to task_id 3",
});
});
test("Connectors are correctly computed and rendered when consolidation is active.", async () => {
ProjectTask._records = [
{
id: 1,
name: "Task 1",
planned_date_begin: "2021-10-11 18:30:00",
date_deadline: "2021-10-11 19:29:59",
user_ids: [1],
depend_on_ids: [],
},
{
id: 2,
name: "Task 2",
planned_date_begin: "2021-10-12 11:30:00",
date_deadline: "2021-10-12 12:29:59",
user_ids: [1, 3],
depend_on_ids: [1],
},
{
id: 3,
name: "Task 3",
planned_date_begin: "2021-10-13 06:30:00",
date_deadline: "2021-10-13 07:29:59",
user_ids: [],
depend_on_ids: [2],
},
{
id: 4,
name: "Task 4",
planned_date_begin: "2021-10-14 22:30:00",
date_deadline: "2021-10-14 23:29:59",
user_ids: [2, 3],
depend_on_ids: [2],
},
{
id: 5,
name: "Task 5",
planned_date_begin: "2021-10-15 01:53:10",
date_deadline: "2021-10-15 02:34:34",
user_ids: [],
depend_on_ids: [],
},
{
id: 6,
name: "Task 6",
planned_date_begin: "2021-10-16 23:00:00",
date_deadline: "2021-10-16 23:21:01",
user_ids: [1, 3],
depend_on_ids: [4, 5],
},
{
id: 7,
name: "Task 7",
planned_date_begin: "2021-10-17 10:30:12",
date_deadline: "2021-10-17 11:29:59",
user_ids: [1, 2, 3],
depend_on_ids: [6],
},
{
id: 8,
name: "Task 8",
planned_date_begin: "2021-10-18 06:30:12",
date_deadline: "2021-10-18 07:29:59",
user_ids: [1, 3],
depend_on_ids: [7],
},
{
id: 9,
name: "Task 9",
planned_date_begin: "2021-10-19 06:30:12",
date_deadline: "2021-10-19 07:29:59",
user_ids: [2],
depend_on_ids: [8],
},
{
id: 10,
name: "Task 10",
planned_date_begin: "2021-10-19 06:30:12",
date_deadline: "2021-10-19 07:29:59",
user_ids: [2],
depend_on_ids: [],
},
{
id: 11,
name: "Task 11",
planned_date_begin: "2021-10-18 06:30:12",
date_deadline: "2021-10-18 07:29:59",
user_ids: [2],
depend_on_ids: [10],
},
{
id: 12,
name: "Task 12",
planned_date_begin: "2021-10-18 06:30:12",
date_deadline: "2021-10-19 07:29:59",
user_ids: [2],
depend_on_ids: [],
},
{
id: 13,
name: "Task 13",
planned_date_begin: "2021-10-18 07:29:59",
date_deadline: "2021-10-20 07:29:59",
user_ids: [2],
depend_on_ids: [12],
},
];
await mountGanttView({
...ganttViewParams,
arch: /* xml */ `<gantt date_start="planned_date_begin" date_stop="date_deadline" default_scale="month" dependency_field="depend_on_ids" consolidation_max="{'user_ids': 100 }"/>`,
});
// groups have been created of r
expect(".o_gantt_row_header.o_gantt_group.o_group_open").toHaveCount(4);
function getGroupRow(index) {
return queryAll(".o_gantt_row_header.o_gantt_group")[index];
}
expect(SELECTORS.connector).toHaveCount(22);
await contains(getGroupRow(1)).click();
expect(getGroupRow(1)).not.toHaveClass("o_group_open");
expect(SELECTORS.connector).toHaveCount(13);
await contains(getGroupRow(1)).click();
expect(SELECTORS.connector).toHaveCount(22);
await contains(getGroupRow(1)).click();
expect(SELECTORS.connector).toHaveCount(13);
await contains(getGroupRow(2)).click();
expect(SELECTORS.connector).toHaveCount(6);
await contains(getGroupRow(0)).click();
expect(SELECTORS.connector).toHaveCount(4);
await contains(getGroupRow(3)).click();
expect(SELECTORS.connector).toHaveCount(0);
});
test("Connector hovered state is triggered and color is set accordingly.", async () => {
await mountGanttView(ganttViewParams);
expect(getConnector(1)).not.toHaveClass(CLASSES.highlightedConnector);
expect(queryFirst(SELECTORS.connectorStroke, { root: getConnector(1) })).toHaveAttribute(
"stroke",
COLORS.default.color
);
await hover(getConnector(1));
await animationFrame();
expect(getConnector(1)).toHaveClass(CLASSES.highlightedConnector);
expect(queryFirst(SELECTORS.connectorStroke, { root: getConnector(1) })).toHaveAttribute(
"stroke",
COLORS.default.highlightedColor
);
});
test("Buttons are displayed when hovering a connector.", async () => {
await mountGanttView(ganttViewParams);
expect(queryAll(SELECTORS.connectorStrokeButton, { root: getConnector(1) })).toHaveCount(0);
await hover(getConnector(1));
await animationFrame();
expect(queryAll(SELECTORS.connectorStrokeButton, { root: getConnector(1) })).toHaveCount(3);
});
test("Buttons are displayed when hovering a connector after a pill has been hovered.", async () => {
await mountGanttView(ganttViewParams);
expect(queryAll(SELECTORS.connectorStrokeButton, { root: getConnector(1) })).toHaveCount(0);
await hover(getPill("Task 1"));
await animationFrame();
expect(queryAll(SELECTORS.connectorStrokeButton, { root: getConnector(1) })).toHaveCount(0);
expect(getConnector(1)).toHaveClass(CLASSES.highlightedConnector);
await hover(getConnector(1));
await animationFrame();
expect(getConnector(1)).toHaveClass(CLASSES.highlightedConnector);
expect(queryAll(SELECTORS.connectorStrokeButton, { root: getConnector(1) })).toHaveCount(3);
});
test("Connector buttons: remove a dependency", async () => {
onRpc(({ method, model, args }) => {
if (model === "project.task" && ["web_gantt_reschedule", "write"].includes(method)) {
expect.step([method, args]);
return true;
}
});
await mountGanttView(ganttViewParams);
await clickConnectorButton(getConnector(1), "remove");
expect.verifySteps([["write", [[2], { depend_on_ids: [[3, 1, false]] }]]]);
});
test("Connector buttons: reschedule task backward date.", async () => {
onRpc(({ method, model, args }) => {
if (model === "project.task" && ["web_gantt_reschedule", "write"].includes(method)) {
expect.step([method, args]);
return {};
}
});
await mountGanttView(ganttViewParams);
await clickConnectorButton(getConnector(1), "reschedule-backward");
expect.verifySteps([
[
"web_gantt_reschedule",
["backward", 1, 2, "depend_on_ids", null, "planned_date_begin", "date_deadline"],
],
]);
});
test("Connector buttons: reschedule task forward date.", async () => {
onRpc(({ args, method, model }) => {
if (model === "project.task" && ["web_gantt_reschedule", "write"].includes(method)) {
expect.step([method, args]);
return {};
}
});
await mountGanttView(ganttViewParams);
await clickConnectorButton(getConnector(1), "reschedule-forward");
expect.verifySteps([
[
"web_gantt_reschedule",
["forward", 1, 2, "depend_on_ids", null, "planned_date_begin", "date_deadline"],
],
]);
});
test("Connector buttons: reschedule task start backward, different data.", async () => {
onRpc(({ method, model, args }) => {
if (model === "project.task" && ["web_gantt_reschedule", "write"].includes(method)) {
expect.step([method, args]);
return {};
}
});
await mountGanttView(ganttViewParams);
await clickConnectorButton(getConnector(1), "reschedule-backward");
expect.verifySteps([
[
"web_gantt_reschedule",
["backward", 1, 2, "depend_on_ids", null, "planned_date_begin", "date_deadline"],
],
]);
});
test("Connector buttons: reschedule task forward, different data.", async () => {
onRpc(({ method, model, args }) => {
if (model === "project.task" && ["web_gantt_reschedule", "write"].includes(method)) {
expect.step([method, args]);
return {};
}
});
await mountGanttView(ganttViewParams);
await clickConnectorButton(getConnector(1), "reschedule-forward");
expect.verifySteps([
[
"web_gantt_reschedule",
["forward", 1, 2, "depend_on_ids", null, "planned_date_begin", "date_deadline"],
],
]);
});
test("Hovering a task pill should highlight related tasks and dependencies", async () => {
/** @type {Map<ConnectorTaskIds, boolean>} */
const testMap = new Map([
["[1,1,2,1]", true],
["[1,1,2,3]", true],
["[2,1,3,false]", true],
["[2,3,3,false]", true],
["[2,1,4,2]", true],
["[2,3,4,3]", true],
["[10,2,11,2]", false],
]);
ProjectTask._records = [
{
id: 1,
name: "Task 1",
planned_date_begin: "2021-10-10 18:30:00",
date_deadline: "2021-10-11 19:29:59",
user_ids: [1],
depend_on_ids: [],
},
{
id: 2,
name: "Task 2",
planned_date_begin: "2021-10-12 11:30:00",
date_deadline: "2021-10-12 12:29:59",
user_ids: [1, 3],
depend_on_ids: [1],
},
{
id: 3,
name: "Task 3",
planned_date_begin: "2021-10-13 06:30:00",
date_deadline: "2021-10-13 07:29:59",
user_ids: [],
depend_on_ids: [2],
},
{
id: 4,
name: "Task 4",
planned_date_begin: "2021-10-14 22:30:00",
date_deadline: "2021-10-14 23:29:59",
user_ids: [2, 3],
depend_on_ids: [2],
},
{
id: 10,
name: "Task 10",
planned_date_begin: "2021-10-19 06:30:12",
date_deadline: "2021-10-19 07:29:59",
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",
date_deadline: "2021-10-18 07:29:59",
user_ids: [2],
depend_on_ids: [10],
},
];
const view = await mountGanttView(ganttViewParams);
const renderer = findComponent(view, (c) => c instanceof GanttRenderer);
const connectorMap = getConnectorMap(renderer);
const pills = [];
for (const wrapper of queryAll(SELECTORS.pillWrapper)) {
const pillId = wrapper.dataset.pillId;
pills.push({
el: queryFirst(SELECTORS.pill, { root: wrapper }),
recordId: renderer.pills[pillId].record.id,
});
}
const task2Pills = pills.filter((p) => p.recordId === 2);
expect(task2Pills).toHaveLength(2);
expect(CLASSES.highlightedPill).toHaveCount(0);
// Check that all connectors are not in hover state.
for (const testKey of testMap.keys()) {
expect(getConnector(connectorMap.get(testKey).id)).not.toHaveClass(
CLASSES.highlightedConnector
);
}
await contains(getPill("Task 2", { nth: 1 })).hover();
// Both pills should be highlighted
expect(getPillWrapper("Task 2", { nth: 1 })).toHaveClass(CLASSES.highlightedPill);
expect(getPillWrapper("Task 2", { nth: 2 })).toHaveClass(CLASSES.highlightedPill);
// The rest of the pills should not be highlighted nor display connector creators
for (const { el, recordId } of pills) {
if (recordId !== 2) {
expect(el).not.toHaveClass(CLASSES.highlightedPill);
}
}
// Check that all connectors are in the expected hover state.
for (const [testKey, shouldBeHighlighted] of testMap.entries()) {
const connector = getConnector(connectorMap.get(testKey).id);
if (shouldBeHighlighted) {
expect(connector).toHaveClass(CLASSES.highlightedConnector);
} else {
expect(connector).not.toHaveClass(CLASSES.highlightedConnector);
}
expect(queryAll(SELECTORS.connectorStrokeButton, { root: connector })).toHaveCount(0);
}
});
test("Hovering a connector should cause the connected pills to get highlighted.", async () => {
await mountGanttView(ganttViewParams);
expect(SELECTORS.highlightedConnector).toHaveCount(0);
expect(SELECTORS.highlightedPill).toHaveCount(0);
await contains(getConnector(1)).hover();
expect(SELECTORS.highlightedConnector).toHaveCount(1);
expect(SELECTORS.highlightedPill).toHaveCount(2);
});
test("Connectors are displayed behind pills, except on hover.", async () => {
const getZIndex = (el) => Number(getComputedStyle(el).zIndex) || 0;
await mountGanttView(ganttViewParams);
expect(getZIndex(getPillWrapper("Task 2"))).toBeGreaterThan(getZIndex(getConnector(1)));
await contains(getConnector(1)).hover();
expect(getZIndex(getPillWrapper("Task 2"))).toBeLessThan(getZIndex(getConnector(1)));
});
test("Create a connector from the gantt view.", async () => {
onRpc("write", ({ args, method }) => expect.step([method, args]));
await mountGanttView(ganttViewParams);
// Explicitly shows the connector creator wrapper since its "display: none"
// disappears on native CSS hover, which cannot be programatically emulated.
const rightWrapper = queryFirst(SELECTORS.connectorCreatorWrapper);
rightWrapper.classList.add("d-block");
await contains(
`${SELECTORS.connectorCreatorWrapper} ${SELECTORS.connectorCreatorBullet}:first`
).dragAndDrop(getPill("Task 2"));
expect.verifySteps([["write", [[2], { depend_on_ids: [[4, 3, false]] }]]]);
});
test("Create a connector from the gantt view: going fast", async () => {
await mountGanttView({
...ganttViewParams,
domain: [["id", "in", [1, 3]]],
});
// Explicitly shows the connector creator wrapper since its "display: none"
// disappears on native CSS hover, which cannot be programatically emulated.
const rightWrapper = queryFirst(SELECTORS.connectorCreatorWrapper, {
root: getPillWrapper("Task 1"),
});
rightWrapper.classList.add("d-block");
const connectorBullet = queryFirst(SELECTORS.connectorCreatorBullet, { root: rightWrapper });
const bulletRect = queryRect(connectorBullet);
const initialPosition = {
x: Math.floor(bulletRect.left + bulletRect.width / 2), // floor to avoid sub-pixel positioning
y: Math.floor(bulletRect.top + bulletRect.height / 2), // floor to avoid sub-pixel positioning
};
await pointerDown(connectorBullet, {
position: { clientX: initialPosition.x, clientY: initialPosition.y },
});
// Here we simulate a fast move, using arbitrary values.
const currentPosition = {
x: Math.floor(initialPosition.x + 123), // floor to avoid sub-pixel positioning
y: Math.floor(initialPosition.y + 12), // floor to avoid sub-pixel positioning
};
await hover(SELECTORS.cellContainer, {
position: { clientX: currentPosition.x, clientY: currentPosition.y },
});
await animationFrame();
// Then we check that the connector stroke is correctly positioned.
const connectorStroke = queryOne(SELECTORS.connectorStroke, { root: getConnector("new") });
expect(connectorStroke).toHaveRect({
top: initialPosition.y,
right: currentPosition.x,
bottom: currentPosition.y,
left: initialPosition.x,
});
});
test("Connectors should be rendered if connected pill is not visible", async () => {
// We first need to bump all the ids for users 2, 3 and 4 to make them disappear.
for (const record of ResUsers._records.slice(1)) {
record.id += 1000;
}
for (const record of ProjectTask._records) {
record.user_ids = record.user_ids.map((id) => (id > 1 ? id + 1000 : id));
}
// Generate a lot of users so that the connectors are far beyond the visible
// viewport, hence generating fake extra pills to render the connectors.
for (let i = 0; i < 100; i++) {
const id = 100 + i;
ResUsers._records.push({ id, name: `User ${id}` });
ProjectTask._records.push({
id,
name: `Task ${id}`,
planned_date_begin: "2021-10-11 18:30:00",
date_deadline: "2021-10-11 19:29:59",
user_ids: [id],
depend_on_ids: [],
});
}
ProjectTask._records[12].user_ids = [199];
await mountGanttView(ganttViewParams);
expect(queryAll(SELECTORS.connector, { visible: true })).toHaveCount(13);
});
test("No display of resize handles when creating a connector", async () => {
await mountGanttView(ganttViewParams);
// Explicitly shows the connector creator wrapper since its "display: none"
// disappears on native CSS hover, which cannot be programatically emulated.
const rightWrapper = queryFirst(SELECTORS.connectorCreatorWrapper);
rightWrapper.classList.add("d-block");
// Creating a connector and hover another pill while dragging it
const { cancel, moveTo } = await contains(SELECTORS.connectorCreatorBullet, {
root: rightWrapper,
}).drag();
await moveTo(getPill("Task 2"));
expect(SELECTORS.resizeHandle).toHaveCount(0);
await cancel();
});
test("Renderer in connect mode when creating a connector", async () => {
await mountGanttView(ganttViewParams);
// Explicitly shows the connector creator wrapper since its "display: none"
// disappears on native CSS hover, which cannot be programatically emulated.
const rightWrapper = queryFirst(SELECTORS.connectorCreatorWrapper);
rightWrapper.classList.add("d-block");
// Creating a connector and hover another pill while dragging it
const { cancel, moveTo } = await contains(SELECTORS.connectorCreatorBullet, {
root: rightWrapper,
}).drag();
await moveTo(getPill("Task 2"));
expect(SELECTORS.renderer).toHaveClass("o_connect");
await cancel();
});
test("Connector creators of initial pill are highlighted when creating a connector", async () => {
await mountGanttView(ganttViewParams);
// Explicitly shows the connector creator wrapper since its "display: none"
// disappears on native CSS hover, which cannot be programatically emulated.
const rightWrapper = queryFirst`${SELECTORS.pillWrapper} ${SELECTORS.connectorCreatorWrapper}`;
rightWrapper.classList.add("d-block");
// Creating a connector and hover another pill while dragging it
const { cancel, moveTo } = await contains(SELECTORS.connectorCreatorBullet, {
root: rightWrapper,
}).drag();
await moveTo(getPill("Task 2"));
expect(`${SELECTORS.pillWrapper}:first`).toHaveClass(CLASSES.lockedConnectorCreator);
await cancel();
});
test("Connector creators of hovered pill are highlighted when creating a connector", async () => {
await mountGanttView(ganttViewParams);
// Explicitly shows the connector creator wrapper since its "display: none"
// disappears on native CSS hover, which cannot be programatically emulated.
const rightWrapper = queryFirst(SELECTORS.connectorCreatorWrapper);
rightWrapper.classList.add("d-block");
// Creating a connector and hover another pill while dragging it
const { cancel, moveTo } = await contains(SELECTORS.connectorCreatorBullet, {
root: rightWrapper,
}).drag();
const destinationWrapper = getPillWrapper("Task 2");
const destinationPill = queryFirst(SELECTORS.pill, { root: destinationWrapper });
await moveTo(destinationPill);
// moveTo only triggers a pointerenter event on destination pill,
// a pointermove event is still needed to highlight it
await contains(destinationPill).hover();
expect(destinationWrapper).toHaveClass(CLASSES.highlightedConnectorCreator);
await cancel();
});
test("Switch to full-size browser: the connections between pills should be diplayed", async () => {
await resize({ width: 375, height: 667 });
await mountGanttView(ganttViewParams);
// Mobile view
expect("svg.o_gantt_connector").toHaveCount(0, {
message: "Gantt connectors should not be visible in small/mobile view",
});
// Resizing browser to leave mobile view
await resize({ width: 1366, height: 768 });
await runAllTimers();
expect("svg.o_gantt_connector").toHaveCount(22, {
message: "Gantt connectors should be visible when switching to desktop view",
});
});
test("Connect two very distant pills", async () => {
ProjectTask._records = [
ProjectTask._records[0],
{
id: 2,
name: "Task 2",
planned_date_begin: "2021-11-18 08:00:00",
date_deadline: "2021-11-18 16:00:00",
user_ids: [2],
depend_on_ids: [],
},
];
onRpc("write", ({ args }) => {
expect.step(JSON.stringify(args));
});
await mountGanttView({
...ganttViewParams,
context: {
default_start_date: "2021-10-01",
default_stop_date: "2021-11-30",
},
});
expect(SELECTORS.connector).toHaveCount(0);
// Explicitly shows the connector creator wrapper since its "display: none"
// disappears on native CSS hover, which cannot be programatically emulated.
const rightWrapper = queryFirst(SELECTORS.connectorCreatorWrapper);
rightWrapper.classList.add("d-block");
// Creating a connector and hover another pill while dragging it
const { drop, moveTo } = await contains(SELECTORS.connectorCreatorBullet, {
root: rightWrapper,
}).drag();
const selector = `${SELECTORS.pill}:contains('Task 2')`;
expect(selector).toHaveCount(0);
await moveTo(SELECTORS.pill, { relative: true, position: { x: 1500 } });
await advanceFrame(200);
await drop(selector);
expect.verifySteps([`[[2],{"depend_on_ids":[[4,1,false]]}]`]);
expect(SELECTORS.connector).toHaveCount(1);
});