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 */ ``, 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} * => 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 */ ``, }); // 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} */ 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); });