diff --git a/addons/hr_org_chart/static/tests/views/hierarchy_view_tests.js b/addons/hr_org_chart/static/tests/views/hierarchy_view_tests.js new file mode 100644 index 00000000000..af4e53700fe --- /dev/null +++ b/addons/hr_org_chart/static/tests/views/hierarchy_view_tests.js @@ -0,0 +1,689 @@ +/** @odoo-module **/ + +import { click, drag, dragAndDrop, getFixture, getNodesTextContent } from "@web/../tests/helpers/utils"; +import { makeView, setupViewRegistries } from "@web/../tests/views/helpers"; + +let serverData, target; + +QUnit.module("Views", (hooks) => { + hooks.beforeEach(() => { + serverData = { + models: { + "hr.employee": { + fields: { + parent_id: { string: "Manager", type: "many2one", relation: "hr.employee" }, + name: { string: "Name" }, + child_ids: { string: "Subordinates", type: "one2many", relation: "hr.employee", relation_field: "parent_id" }, + }, + records: [ + { id: 1, name: "Albert", parent_id: false, child_ids: [2, 3] }, + { id: 2, name: "Georges", parent_id: 1, child_ids: [] }, + { id: 3, name: "Josephine", parent_id: 1, child_ids: [4] }, + { id: 4, name: "Louis", parent_id: 3, child_ids: [] }, + ], + }, + }, + views: { + "hr.employee,false,hierarchy": ` + + + +
+ +
+
+ +
+
+
+
+ `, + "hr.employee,1,hierarchy": ` + + + + +
+ +
+
+ +
+
+
+
+ `, + "hr.employee,false,form": ` +
+ + + + + + +
+ `, + }, + }; + setupViewRegistries(); + target = getFixture(); + }); + + QUnit.module("Hierarchy View"); + + QUnit.test("load hierarchy view", async function (assert) { + await makeView({ + type: "hierarchy", + resModel: "hr.employee", + serverData, + }); + + assert.containsOnce(target, ".o_hierarchy_view"); + assert.containsN(target, ".o_hierarchy_button_add", 2); + assert.containsOnce(target, ".o_hierarchy_view .o_hierarchy_renderer"); + assert.containsOnce(target, ".o_hierarchy_view .o_hierarchy_renderer > .o_hierarchy_container"); + assert.containsN(target, ".o_hierarchy_row", 2); + assert.containsOnce(target, ".o_hierarchy_separator"); + assert.containsN(target, ".o_hierarchy_line_part", 2); + assert.containsOnce(target, ".o_hierarchy_line_left"); + assert.containsOnce(target, ".o_hierarchy_line_right"); + assert.containsN(target, ".o_hierarchy_node_container", 3); + assert.containsN(target, ".o_hierarchy_node", 3); + assert.containsNone(target, ".o_hierarchy_node_highlighted"); + assert.containsN(target, ".o_hierarchy_node_button", 2); + assert.containsOnce(target, ".o_hierarchy_node_button.btn-primary"); + assert.strictEqual(target.querySelector(".o_hierarchy_node_button.btn-primary").textContent.trim(), "Unfold 1"); + // check nodes in each row + const row = target.querySelector(".o_hierarchy_row"); + assert.containsOnce(row, ".o_hierarchy_node"); + assert.strictEqual(row.querySelector(".o_hierarchy_node_content").textContent, "Albert"); + assert.containsOnce(target, ".o_hierarchy_node_button.btn-secondary"); + assert.strictEqual(target.querySelector(".o_hierarchy_node_button.btn-secondary").textContent.trim(), "Fold"); + }); + + QUnit.test("display child nodes", async function (assert) { + await makeView({ + type: "hierarchy", + resModel: "hr.employee", + serverData, + async mockRPC(route, args) { + if (args.method === "read") { + assert.step("get child data"); + } else if (args.method === "read_group") { + assert.step("fetch descendants"); + } + } + }); + + assert.containsN(target, ".o_hierarchy_row", 2); + assert.containsN(target, ".o_hierarchy_node_button", 2); + assert.containsOnce(target, ".o_hierarchy_node_button.btn-secondary"); + assert.containsOnce(target, ".o_hierarchy_node_button.btn-primary"); + await click(target, ".o_hierarchy_node_button.btn-primary"); + assert.containsN(target, ".o_hierarchy_row", 3); + assert.containsN(target, ".o_hierarchy_separator", 2); + assert.containsN(target, ".o_hierarchy_line_part", 4); + assert.containsN(target, ".o_hierarchy_line_left", 2); + assert.containsN(target, ".o_hierarchy_line_right", 2); + assert.containsN(target, ".o_hierarchy_node_container", 4); + assert.containsN(target, ".o_hierarchy_node", 4); + assert.containsN(target, ".o_hierarchy_node_button", 2); + assert.containsNone(target, ".o_hierarchy_node_button.btn-primary"); + assert.containsN(target, ".o_hierarchy_node_button.btn-secondary", 2); + assert.strictEqual(target.querySelector(".o_hierarchy_node_button.btn-secondary").textContent.trim(), "Fold"); + // check nodes in each row + const rows = target.querySelectorAll(".o_hierarchy_row"); + let row = rows[0]; + assert.containsOnce(row, ".o_hierarchy_node"); + assert.strictEqual(row.querySelector(".o_hierarchy_node_content").textContent, "Albert"); + row = rows[1]; + assert.containsN(row, ".o_hierarchy_node", 2); + assert.deepEqual( + getNodesTextContent(row.querySelectorAll(".o_hierarchy_node_content")), + [ // Name + Parent name + "GeorgesAlbert", + "JosephineAlbert", + ], + ); + row = rows[2]; + assert.containsOnce(row, ".o_hierarchy_node"); + assert.strictEqual(row.querySelector(".o_hierarchy_node_content").textContent, "LouisJosephine"); + assert.verifySteps([ + "get child data", + "fetch descendants", + ]); + }); + + QUnit.test("display child nodes with child_field set on the view", async function (assert) { + await makeView({ + type: "hierarchy", + resModel: "hr.employee", + serverData, + viewId: 1, + async mockRPC(route, args) { + if (args.method === "read") { + assert.step("get child data with descendants"); + } + } + }); + + assert.containsN(target, ".o_hierarchy_row", 2); + assert.containsN(target, ".o_hierarchy_node_button", 2); + assert.containsOnce(target, ".o_hierarchy_node_button.btn-secondary"); + assert.containsOnce(target, ".o_hierarchy_node_button.btn-primary"); + await click(target, ".o_hierarchy_node_button.btn-primary"); + assert.containsN(target, ".o_hierarchy_row", 3); + assert.containsN(target, ".o_hierarchy_separator", 2); + assert.containsN(target, ".o_hierarchy_line_part", 4); + assert.containsN(target, ".o_hierarchy_line_left", 2); + assert.containsN(target, ".o_hierarchy_line_right", 2); + assert.containsN(target, ".o_hierarchy_node_container", 4); + assert.containsN(target, ".o_hierarchy_node", 4); + assert.containsN(target, ".o_hierarchy_node_button", 2); + assert.containsNone(target, ".o_hierarchy_node_button.btn-primary"); + assert.containsN(target, ".o_hierarchy_node_button.btn-secondary", 2); + assert.verifySteps([ + "get child data with descendants", + ]); + }); + + QUnit.test("collapse child nodes", async function (assert) { + await makeView({ + type: "hierarchy", + resModel: "hr.employee", + serverData, + }); + + assert.containsN(target, ".o_hierarchy_row", 2); + assert.containsOnce(target, ".o_hierarchy_separator"); + assert.containsN(target, ".o_hierarchy_line_part", 2); + assert.containsOnce(target, ".o_hierarchy_line_left"); + assert.containsOnce(target, ".o_hierarchy_line_right"); + assert.containsN(target, ".o_hierarchy_node_container", 3); + assert.containsN(target, ".o_hierarchy_node", 3); + await click(target, ".o_hierarchy_node_button.btn-secondary"); + assert.containsOnce(target, ".o_hierarchy_row"); + assert.containsNone(target, ".o_hierarchy_separator"); + assert.containsNone(target, ".o_hierarchy_line_part", 2); + assert.containsNone(target, ".o_hierarchy_line_left"); + assert.containsNone(target, ".o_hierarchy_line_right"); + assert.containsOnce(target, ".o_hierarchy_node_container"); + assert.containsOnce(target, ".o_hierarchy_node"); + assert.containsNone(target, ".o_hierarchy_node_button.btn-secondary"); + assert.containsOnce(target, ".o_hierarchy_node_button"); + assert.containsOnce(target, ".o_hierarchy_node_container:not(.o_hierarchy_node_button)"); + assert.deepEqual(getNodesTextContent(target.querySelectorAll(".o_hierarchy_row .o_hierarchy_node_content")), ["Albert"]); + }); + + QUnit.test("display the parent above the line when many records on the parent row", async function (assert) { + serverData.models["hr.employee"].records.push({ + name: "Alfred", + parent_id: false, + child_ids: [], + }) + await makeView({ + type: "hierarchy", + resModel: "hr.employee", + serverData, + }); + + assert.containsOnce(target, ".o_hierarchy_row"); + assert.containsNone(target, ".o_hierarchy_separator"); + assert.containsN(target, ".o_hierarchy_node", 2); + assert.containsOnce(target, ".o_hierarchy_node_button.btn-primary"); + await click(target, ".o_hierarchy_node_button.btn-primary"); + assert.containsN(target, ".o_hierarchy_row", 2); + assert.containsOnce(target, ".o_hierarchy_separator"); + assert.containsOnce(target, ".o_hierarchy_line_left"); + assert.containsOnce(target, ".o_hierarchy_line_right"); + assert.containsOnce(target, ".o_hierarchy_parent_node_container"); + assert.strictEqual(target.querySelector(".o_hierarchy_parent_node_container").textContent, "Albert"); + }); + + QUnit.test("search record in hierarchy view", async function (assert) { + await makeView({ + type: "hierarchy", + resModel: "hr.employee", + serverData, + domain: [["id", "=", 4]], // simulate a search + }); + + assert.containsN(target, ".o_hierarchy_row", 2); + assert.containsN(target, ".o_hierarchy_node", 2); + assert.containsN(target, ".o_hierarchy_separator", 1); + assert.containsOnce(target, ".o_hierarchy_node_highlighted"); + assert.strictEqual(target.querySelector(".o_hierarchy_node_highlighted").textContent.trim(), "LouisJosephine"); + }); + + QUnit.test("search record in hierarchy view with child field name defined in the arch", async function (assert) { + await makeView({ + type: "hierarchy", + resModel: "hr.employee", + viewId: 1, + serverData, + domain: [["id", "=", 4]], // simulate a search + }); + + assert.containsN(target, ".o_hierarchy_row", 2); + assert.containsN(target, ".o_hierarchy_node", 2); + assert.containsN(target, ".o_hierarchy_separator", 1); + assert.containsOnce(target, ".o_hierarchy_node_highlighted"); + assert.strictEqual(target.querySelector(".o_hierarchy_node_highlighted").textContent.trim(), "LouisJosephine"); + }); + + QUnit.test("fetch parent record", async function (assert) { + await makeView({ + type: "hierarchy", + resModel: "hr.employee", + serverData, + domain: [["id", "=", 4]], // simulate a search + }); + + assert.containsN(target, ".o_hierarchy_row", 2); + assert.containsN(target, ".o_hierarchy_node", 2); + assert.containsN(target, ".o_hierarchy_separator", 1); + assert.containsOnce(target, ".o_hierarchy_node_highlighted"); + assert.strictEqual(target.querySelector(".o_hierarchy_node_highlighted").textContent.trim(), "LouisJosephine"); + let rows = target.querySelectorAll(".o_hierarchy_row"); + let row = rows[0]; + assert.containsOnce(row, ".o_hierarchy_node"); + assert.strictEqual(row.querySelector(".o_hierarchy_node_content").textContent, "JosephineAlbert"); + row = rows[1]; + assert.containsOnce(row, ".o_hierarchy_node"); + assert.strictEqual(row.querySelector(".o_hierarchy_node_content").textContent, "LouisJosephine"); + assert.containsOnce( + target, + ".o_hierarchy_node_container button .fa-chevron-up", + "Button to fetch the parent node should be visible on the first node displayed in the view." + ); + await click(target, ".o_hierarchy_node_container button .fa-chevron-up"); + assert.containsN(target, ".o_hierarchy_row", 3); + assert.containsN(target, ".o_hierarchy_node", 4); + assert.containsN(target, ".o_hierarchy_separator", 2); + assert.containsOnce(target, ".o_hierarchy_node_highlighted"); + assert.strictEqual(target.querySelector(".o_hierarchy_node_highlighted").textContent.trim(), "LouisJosephine"); + rows = target.querySelectorAll(".o_hierarchy_row"); + row = rows[0]; + assert.containsOnce(row, ".o_hierarchy_node"); + assert.strictEqual(row.querySelector(".o_hierarchy_node_content").textContent, "Albert"); + row = rows[1]; + assert.containsN(row, ".o_hierarchy_node", 2); + assert.deepEqual( + getNodesTextContent(row.querySelectorAll(".o_hierarchy_node_content")), + ["GeorgesAlbert", "JosephineAlbert"], + ); + row = rows[2]; + assert.containsOnce(row, ".o_hierarchy_node"); + assert.strictEqual(row.querySelector(".o_hierarchy_node_content").textContent, "LouisJosephine"); + }); + + QUnit.test("fetch parent when there are many records without the same parent in the same row", async function (assert) { + serverData.models["hr.employee"].records.push( + { id: 5, name: "Lisa", parent_id: 2, child_ids: []}, + ); + await makeView({ + type: "hierarchy", + resModel: "hr.employee", + serverData, + domain: [["name", "ilike", "l"]], // simulate a search + }); + assert.containsOnce(target, ".o_hierarchy_row"); + assert.containsN(target, ".o_hierarchy_node_container", 3); + assert.deepEqual( + getNodesTextContent(target.querySelectorAll(".o_hierarchy_node_content")), + [ + "LisaGeorges", "LouisJosephine", "Albert", + ], + ); + assert.containsN(target, ".o_hierarchy_node_container button .fa-chevron-up", 2); + const firstNode = target.querySelector(".o_hierarchy_node_container"); + await click(firstNode, ".o_hierarchy_node_container button .fa-chevron-up"); + assert.containsN(target, ".o_hierarchy_row", 2); + assert.containsN(target, ".o_hierarchy_node", 2); + assert.deepEqual( + getNodesTextContent(target.querySelectorAll(".o_hierarchy_node_content")), + [ + "GeorgesAlbert", "LisaGeorges", + ], + ); + assert.containsOnce(target, ".o_hierarchy_node_container button .fa-chevron-up"); + await click(target, ".o_hierarchy_node_container button .fa-chevron-up"); + assert.containsN(target, ".o_hierarchy_row", 3); + assert.containsN(target, ".o_hierarchy_node", 4); + }); + + QUnit.test("fetch parent when parent record is in the same row", async function (assert) { + serverData.models["hr.employee"].records.push( + { id: 5, name: "Lisa", parent_id: 2, child_ids: []}, + ); + await makeView({ + type: "hierarchy", + resModel: "hr.employee", + serverData, + domain: [["id", "in", [1, 2, 3, 4, 5]]], // simulate a search + }); + assert.containsOnce(target, ".o_hierarchy_row"); + assert.containsN(target, ".o_hierarchy_node_container", 5); + assert.containsN(target, ".o_hierarchy_node_container button .fa-chevron-up", 4); + const firstNodeWithParentBtn = target.querySelector(".o_hierarchy_node_container:has(button .fa-chevron-up)"); + await click(firstNodeWithParentBtn, ".o_hierarchy_node_container button .fa-chevron-up"); + assert.containsN(target, ".o_hierarchy_row", 2); + assert.containsN(target, ".o_hierarchy_node", 3); + assert.deepEqual( + getNodesTextContent(target.querySelectorAll(".o_hierarchy_node_content")), + [ + "Albert", "GeorgesAlbert", "JosephineAlbert", + ], + ); + }); + + QUnit.test("fetch parent of node with children displayed", async function (assert) { + serverData.models["hr.employee"].records.push( + { id: 5, name: "Lisa", parent_id: 2, child_ids: []}, + ); + serverData.models["hr.employee"].records.find((rec) => rec.id === 2).child_ids.push(5); + await makeView({ + type: "hierarchy", + resModel: "hr.employee", + serverData, + domain: [["id", "in", [1, 2, 3, 4, 5]]], // simulate a search + }); + assert.containsOnce(target, ".o_hierarchy_row"); + assert.containsN(target, ".o_hierarchy_node_container", 5); + assert.containsN(target, ".o_hierarchy_node_container button .fa-chevron-up", 4); + const georgesNode = target.querySelector(".o_hierarchy_node_container:has(button[name=hierarchy_search_parent_node])"); + assert.strictEqual(georgesNode.querySelector(".o_hierarchy_node_content").textContent, "GeorgesAlbert"); + await click(georgesNode, "button[name=hierarchy_search_subsidiaries]"); + assert.containsN(target, ".o_hierarchy_row", 2); + assert.containsN(target, ".o_hierarchy_node", 5); + const rows = target.querySelectorAll(".o_hierarchy_row"); + let row = rows[0]; + assert.containsN(row, ".o_hierarchy_node", 4); + assert.deepEqual( + getNodesTextContent(row.querySelectorAll(".o_hierarchy_node_content")), + ["Albert", "GeorgesAlbert", "JosephineAlbert", "LouisJosephine"], + ); + row = rows[1]; + assert.containsN(row, ".o_hierarchy_node", 1); + assert.strictEqual(row.querySelector(".o_hierarchy_node_content").textContent, "LisaGeorges"); + const firstNodeWithParentBtn = target.querySelector(".o_hierarchy_node_container:has(button .fa-chevron-up)"); + await click(firstNodeWithParentBtn, ".o_hierarchy_node_container button .fa-chevron-up"); + assert.containsN(target, ".o_hierarchy_row", 3); + assert.containsN(target, ".o_hierarchy_node", 4); + }); + + QUnit.test("drag and drop is disabled by default", async function (assert) { + await makeView({ + type: "hierarchy", + resModel: "hr.employee", + serverData, + }); + assert.containsN(target, ".o_hierarchy_row", 2); + assert.containsN(target, ".o_hierarchy_node", 3); + assert.deepEqual( + getNodesTextContent(target.querySelectorAll(".o_hierarchy_node_content")), + ["Albert", "GeorgesAlbert", "JosephineAlbert"], + ); + + const nodeContainers = target.querySelectorAll(".o_hierarchy_node_container"); + const georgesNodeContainer = nodeContainers[1]; + assert.strictEqual(georgesNodeContainer.querySelector(".o_hierarchy_node_content").textContent, "GeorgesAlbert"); + + await dragAndDrop( + georgesNodeContainer.querySelector(".o_hierarchy_node"), + ".o_hierarchy_row:first-child" + ); + + assert.containsN(target, ".o_hierarchy_row", 2); + assert.containsN(target, ".o_hierarchy_node", 3); + assert.deepEqual( + getNodesTextContent(target.querySelectorAll(".o_hierarchy_node_content")), + ["Albert", "GeorgesAlbert", "JosephineAlbert"], + ); + }); + + QUnit.test("drag and drop record on another row", async function (assert) { + serverData.views["hr.employee,false,hierarchy"] = serverData.views["hr.employee,false,hierarchy"].replace("", ""); + await makeView({ + type: "hierarchy", + resModel: "hr.employee", + serverData, + }); + assert.containsN(target, ".o_hierarchy_row", 2); + assert.containsN(target, ".o_hierarchy_node", 3); + assert.deepEqual( + getNodesTextContent(target.querySelectorAll(".o_hierarchy_node_content")), + ["Albert", "GeorgesAlbert", "JosephineAlbert"], + ); + + const nodeContainers = target.querySelectorAll(".o_hierarchy_node_container"); + const georgesNodeContainer = nodeContainers[1]; + assert.strictEqual(georgesNodeContainer.querySelector(".o_hierarchy_node_content").textContent, "GeorgesAlbert"); + + await dragAndDrop( + georgesNodeContainer.querySelector(".o_hierarchy_node"), + ".o_hierarchy_row:first-child" + ); + + assert.containsN(target, ".o_hierarchy_row", 2); + assert.containsN(target, ".o_hierarchy_node", 3); + assert.deepEqual( + getNodesTextContent(target.querySelectorAll(".o_hierarchy_node_content")), + ["Albert", "Georges", "JosephineAlbert"], + "Georges should no longer have a manager" + ); + }); + + QUnit.test("drag and drop record on sibling node", async function (assert) { + serverData.views["hr.employee,false,hierarchy"] = serverData.views["hr.employee,false,hierarchy"].replace("", ""); + await makeView({ + type: "hierarchy", + resModel: "hr.employee", + serverData, + }); + assert.containsN(target, ".o_hierarchy_row", 2); + assert.containsN(target, ".o_hierarchy_node", 3); + assert.deepEqual( + getNodesTextContent(target.querySelectorAll(".o_hierarchy_node_content")), + ["Albert", "GeorgesAlbert", "JosephineAlbert"], + ); + + const nodeContainers = target.querySelectorAll(".o_hierarchy_node_container"); + const georgesNodeContainer = nodeContainers[1]; + const josephineNodeContainer = nodeContainers[2]; + assert.strictEqual(georgesNodeContainer.querySelector(".o_hierarchy_node_content").textContent, "GeorgesAlbert"); + assert.strictEqual(josephineNodeContainer.querySelector(".o_hierarchy_node_content").textContent, "JosephineAlbert"); + + await dragAndDrop( + georgesNodeContainer.querySelector(".o_hierarchy_node"), + josephineNodeContainer, + ); + + assert.containsN(target, ".o_hierarchy_row", 3); + assert.containsN(target, ".o_hierarchy_node", 4); + assert.deepEqual( + getNodesTextContent(target.querySelectorAll(".o_hierarchy_node_content")), + ["Albert", "JosephineAlbert", "LouisJosephine", "GeorgesJosephine"], + "Georges should have Josephine as manager" + ); + }); + + QUnit.test("drag and drop node unfolded on first row", async function (assert) { + serverData.views["hr.employee,false,hierarchy"] = serverData.views["hr.employee,false,hierarchy"].replace("", ""); + await makeView({ + type: "hierarchy", + resModel: "hr.employee", + serverData, + }); + assert.containsN(target, ".o_hierarchy_row", 2); + assert.containsN(target, ".o_hierarchy_node", 3); + assert.deepEqual( + getNodesTextContent(target.querySelectorAll(".o_hierarchy_node_content")), + ["Albert", "GeorgesAlbert", "JosephineAlbert"], + ); + + const nodeContainers = target.querySelectorAll(".o_hierarchy_node_container"); + const josephineNodeContainer = nodeContainers[2]; + assert.strictEqual(josephineNodeContainer.querySelector(".o_hierarchy_node_content").textContent, "JosephineAlbert"); + await click(josephineNodeContainer, "button[name='hierarchy_search_subsidiaries']"); + + await dragAndDrop( + josephineNodeContainer.querySelector(".o_hierarchy_node"), + ".o_hierarchy_row:first-child" + ); + + assert.containsN(target, ".o_hierarchy_row", 2); + assert.containsN(target, ".o_hierarchy_node", 3); + assert.deepEqual( + getNodesTextContent(target.querySelectorAll(".o_hierarchy_node_content")), + ["Albert", "Josephine", "LouisJosephine"], + "Georges should have Josephine as manager" + ); + }); + + QUnit.test("drag and drop node when other node is unfolded on first row", async function (assert) { + serverData.views["hr.employee,false,hierarchy"] = serverData.views["hr.employee,false,hierarchy"].replace("", ""); + await makeView({ + type: "hierarchy", + resModel: "hr.employee", + serverData, + }); + assert.containsN(target, ".o_hierarchy_row", 2); + assert.containsN(target, ".o_hierarchy_node", 3); + assert.deepEqual( + getNodesTextContent(target.querySelectorAll(".o_hierarchy_node_content")), + ["Albert", "GeorgesAlbert", "JosephineAlbert"], + ); + + const nodeContainers = target.querySelectorAll(".o_hierarchy_node_container"); + const georgesNodeContainer = nodeContainers[1]; + const josephineNodeContainer = nodeContainers[2]; + assert.strictEqual(georgesNodeContainer.querySelector(".o_hierarchy_node_content").textContent, "GeorgesAlbert"); + assert.strictEqual(josephineNodeContainer.querySelector(".o_hierarchy_node_content").textContent, "JosephineAlbert"); + await click(josephineNodeContainer, "button[name='hierarchy_search_subsidiaries']"); + + await dragAndDrop( + georgesNodeContainer.querySelector(".o_hierarchy_node"), + ".o_hierarchy_row:first-child" + ); + + assert.containsN(target, ".o_hierarchy_row", 3); + assert.containsN(target, ".o_hierarchy_node", 4); + assert.deepEqual( + getNodesTextContent(target.querySelectorAll(".o_hierarchy_node_content")), + ["Albert", "Georges", "JosephineAlbert", "LouisJosephine"], + "Georges should no longer have a manager" + ); + }); + + QUnit.test("drag node and move it on a row", async function (assert) { + serverData.views["hr.employee,false,hierarchy"] = serverData.views["hr.employee,false,hierarchy"].replace("", ""); + await makeView({ + type: "hierarchy", + resModel: "hr.employee", + serverData, + }); + + assert.containsN(target, ".o_hierarchy_node", 3); + const nodeContainers = target.querySelectorAll(".o_hierarchy_node_container"); + const georgesNodeContainer = nodeContainers[1]; + assert.strictEqual(georgesNodeContainer.querySelector(".o_hierarchy_node_content").textContent, "GeorgesAlbert"); + + const { drop, moveTo } = await drag(georgesNodeContainer.querySelector(".o_hierarchy_node")); + + await moveTo(".o_hierarchy_row:first-child"); + assert.hasClass(georgesNodeContainer, "o_hierarchy_dragged"); + assert.hasClass(target.querySelector(".o_hierarchy_row"), "o_hierarchy_hover"); + + await drop(); + assert.containsNone(target, ".o_hierarchy_node.o_hierarchy_dragged"); + assert.containsNone(target, ".o_hierarchy_row.o_hierarchy_hover"); + }); + + QUnit.test("drag node and move it on another node", async function (assert) { + serverData.views["hr.employee,false,hierarchy"] = serverData.views["hr.employee,false,hierarchy"].replace("", ""); + await makeView({ + type: "hierarchy", + resModel: "hr.employee", + serverData, + }); + + assert.containsN(target, ".o_hierarchy_node", 3); + const nodeContainers = target.querySelectorAll(".o_hierarchy_node_container"); + const georgesNodeContainer = nodeContainers[1]; + const josephineNodeContainer = nodeContainers[2]; + assert.strictEqual(georgesNodeContainer.querySelector(".o_hierarchy_node_content").textContent, "GeorgesAlbert"); + assert.strictEqual(josephineNodeContainer.querySelector(".o_hierarchy_node_content").textContent, "JosephineAlbert"); + + const { drop, moveTo } = await drag(georgesNodeContainer.querySelector(".o_hierarchy_node")); + + await moveTo(josephineNodeContainer.querySelector(".o_hierarchy_node")); + assert.hasClass(georgesNodeContainer, "o_hierarchy_dragged"); + assert.hasClass(georgesNodeContainer.querySelector(".o_hierarchy_node"), "shadow"); + assert.hasClass(josephineNodeContainer, "o_hierarchy_hover"); + + await drop(); + assert.containsNone(target, ".o_hierarchy_node.o_hierarchy_dragged"); + assert.containsNone(target, ".o_hierarchy_node.o_hierarchy_hover"); + assert.containsNone(target, ".o_hierarchy_node.shadow"); + }); + + QUnit.test("check default icon is correctly used inside button to display child nodes", async function (assert) { + await makeView({ + type: "hierarchy", + resModel: "hr.employee", + serverData, + }); + + assert.containsOnce(target, ".o_hierarchy_node button[name=hierarchy_search_subsidiaries].btn-primary"); + assert.strictEqual(target.querySelector(".o_hierarchy_node button[name=hierarchy_search_subsidiaries].btn-primary").textContent.trim(), "Unfold 1"); + assert.containsOnce( + target, + ".o_hierarchy_node button[name=hierarchy_search_subsidiaries] i.fa-share-alt.o_hierarchy_icon", + "The default icon of the hierarchy view should be displayed inside the button to unfold the node." + ); + }); + + QUnit.test("use other icon used next to Unfold string displayed inside the button", async function (assert) { + serverData.views["hr.employee,false,hierarchy"] = serverData.views["hr.employee,false,hierarchy"].replace("", ""); + await makeView({ + type: "hierarchy", + resModel: "hr.employee", + serverData, + }); + + assert.containsOnce(target, ".o_hierarchy_node button[name=hierarchy_search_subsidiaries].btn-primary"); + assert.strictEqual(target.querySelector(".o_hierarchy_node button[name=hierarchy_search_subsidiaries].btn-primary").textContent.trim(), "Unfold 1"); + assert.containsOnce( + target, + ".o_hierarchy_node button[name=hierarchy_search_subsidiaries] i.fa-users", + "The icon defined in the attribute icon in hierarchy tag should be displayed inside the button to unfold the node instead of the default one." + ); + }); + + QUnit.test("use `hierarchy_res_id` context to load the view at that specific node with its siblings and parent node", async function (assert) { + serverData.models["hr.employee"].records.push( + { id: 5, name: "Lisa", parent_id: 3, child_ids: []}, + ); + serverData.models["hr.employee"].records.find((rec) => rec.id === 3).child_ids.push(5); + await makeView({ + type: "hierarchy", + resModel: "hr.employee", + serverData, + context: { + hierarchy_res_id: 5, + }, + }); + + assert.containsN(target, ".o_hierarchy_row", 2); + assert.containsN(target, ".o_hierarchy_node", 3); + assert.deepEqual( + getNodesTextContent(target.querySelectorAll(".o_hierarchy_node_content")), + ["JosephineAlbert", "LisaJosephine", "LouisJosephine"] + ); + assert.containsOnce(target, ".o_hierarchy_node_container button[name=hierarchy_search_parent_node]"); + assert.containsOnce(target, ".o_hierarchy_node_highlighted"); + assert.containsOnce(target, ".o_hierarchy_node.o_hierarchy_node_highlighted"); + assert.strictEqual(target.querySelector(".o_hierarchy_node_highlighted .o_hierarchy_node_content").textContent, "LisaJosephine"); + }); +}); diff --git a/addons/hr_org_chart/static/tests/views/mock_server.js b/addons/hr_org_chart/static/tests/views/mock_server.js new file mode 100644 index 00000000000..d047ebc07b4 --- /dev/null +++ b/addons/hr_org_chart/static/tests/views/mock_server.js @@ -0,0 +1,78 @@ +/** @odoo-module **/ + +import { patch } from "@web/core/utils/patch"; +import { MockServer } from "@web/../tests/helpers/mock_server"; + +patch(MockServer.prototype, { + async _performRPC(route, args) { + if (args.method === "hierarchy_read") { + return this.mockHierarchyRead(args.model, args.args, args.kwargs); + } + return super._performRPC(route, args); + }, + + mockHierarchyRead(modelName, args, kwargs) { + const [domain, fields, parentFieldName, childFieldName] = args; + if (!(parentFieldName in fields)) { + fields.push(parentFieldName); + } + let records = this.mockSearchRead(modelName, [domain, fields], kwargs); + let focusedRecordId = false; + let fetchChildIdsForAllRecords = false; + if (records.length === 1) { + const record = records[0]; + let domain = [[parentFieldName, "=", record.id], ["id", "!=", record.id]]; + if (record[parentFieldName]) { + focusedRecordId = record.id; + const parentResId = record[parentFieldName][0]; + domain = [ + ["id", "!=", record.id], + "|", + ["id", "=", parentResId], + [parentFieldName, "in", [parentResId, record.id]], + ]; + } + records.push(...this.mockSearchRead(modelName, [domain, fields], kwargs)); + } else if (!records.length) { + records = this.mockSearchRead( + modelName, + [ + [[parentFieldName, "=", false]], + fields, + ], + kwargs, + ); + } else { + fetchChildIdsForAllRecords = true + } + const childrenIdsPerRecordId = {}; + if (!childFieldName) { + const parentResIds = []; + for (const rec of records) { + if (rec[parentFieldName]) { + parentResIds.push(rec[parentFieldName][0]); + } + } + const recordIds = records.map((rec) => rec.id); + const data = this.mockReadGroup(modelName, { + domain: [[parentFieldName, "in", fetchChildIdsForAllRecords ? recordIds : recordIds.filter((id) => !parentResIds.includes(id))]], + groupby: [parentFieldName], + fields: ["id:array_agg"], + }); + for (const d of data) { + childrenIdsPerRecordId[d[parentFieldName][0]] = d.id; + } + } + if (focusedRecordId || Object.keys(childrenIdsPerRecordId).length) { + for (const record of records) { + if (record.id in childrenIdsPerRecordId) { + record.__child_ids__ = childrenIdsPerRecordId[record.id]; + } + if (record.id === focusedRecordId) { + record.__focus__ = true; + } + } + } + return records; + }, +}) diff --git a/addons/hr_org_chart/tests/test_employee.py b/addons/hr_org_chart/tests/test_employee.py index a5ed1c2ca61..fc9f86c9468 100644 --- a/addons/hr_org_chart/tests/test_employee.py +++ b/addons/hr_org_chart/tests/test_employee.py @@ -10,46 +10,104 @@ class TestEmployee(TestHrCommon): @classmethod def setUpClass(cls): super().setUpClass() - - def test_is_subordinate(self): - employee_georges, employee_paul, employee_pierre = self.env['hr.employee'].with_user(self.res_users_hr_officer).create([ + cls.employee_georges, cls.employee_paul, cls.employee_pierre = cls.env['hr.employee'].with_user(cls.res_users_hr_officer).create([ {'name': 'Georges'}, {'name': 'Paul'}, {'name': 'Pierre'}, ]) - self.res_users_hr_officer.employee_id = employee_georges - employee_paul.parent_id = employee_georges - employees = employee_paul + employee_pierre + + def test_is_subordinate(self): + self.res_users_hr_officer.employee_id = self.employee_georges + self.employee_paul.parent_id = self.employee_georges + employees = self.employee_paul + self.employee_pierre self.assertTrue( - employee_paul.is_subordinate, + self.employee_paul.is_subordinate, 'Paul should be a subordinate of the current user since the current is his manager.') self.assertFalse( - employee_pierre.is_subordinate, + self.employee_pierre.is_subordinate, 'Pierre should not be a subordinate of the current user since Pierre has no manager.') - self.assertEqual(employees.filtered_domain(employees._search_is_subordinate('=', True)), employee_paul) - self.assertEqual(employees.filtered_domain(employees._search_is_subordinate('=', False)), employee_pierre) + self.assertEqual(employees.filtered_domain(employees._search_is_subordinate('=', True)), self.employee_paul) + self.assertEqual(employees.filtered_domain(employees._search_is_subordinate('=', False)), self.employee_pierre) - employee_pierre.parent_id = employee_paul + self.employee_pierre.parent_id = self.employee_paul self.assertTrue( - employee_paul.is_subordinate, + self.employee_paul.is_subordinate, 'Paul should be a subordinate of the current user since the current is his manager.') self.assertTrue( - employee_pierre.is_subordinate, + self.employee_pierre.is_subordinate, "Pierre should now be a subordinate of the current user since Paul is his manager and the current user is the Paul's manager.") self.assertEqual(employees.filtered_domain(employees._search_is_subordinate('=', True)), employees) self.assertFalse(employees.filtered_domain(employees._search_is_subordinate('=', False))) - employee_paul.parent_id = False + self.employee_paul.parent_id = False employees._compute_is_subordinate() self.assertFalse( - employee_paul.is_subordinate, + self.employee_paul.is_subordinate, 'Paul should no longer be a subordinate of the current user since Paul has no manager.') self.assertFalse( - employee_pierre.is_subordinate, + self.employee_pierre.is_subordinate, "Pierre should not be a subordinate of the current user since Paul is his manager and the current user is not the Paul's manager.") self.assertFalse(employees.filtered_domain(employees._search_is_subordinate('=', True))) self.assertEqual(employees.filtered_domain(employees._search_is_subordinate('=', False)), employees) + + def test_hierarchy_read(self): + HrEmployee = self.env['hr.employee'] + employees = self.employee_georges + self.employee_paul + self.employee_pierre + result = HrEmployee.hierarchy_read([('id', 'in', employees.ids)], ['id'], 'parent_id') + for emp in employees: + self.assertIn({'id': emp.id, 'parent_id': False}, result) + + self.employee_georges.parent_id = self.employee_paul + self.employee_pierre.parent_id = self.employee_paul + result = HrEmployee.hierarchy_read([('id', 'in', employees.ids)], ['id'], 'parent_id') + self.assertEqual(len(result), 3) + for emp in employees: + emp_dict = {'id': emp.id, 'parent_id': emp.parent_id.id and (emp.parent_id.id, emp.parent_id.display_name)} + if not emp.parent_id: + emp_dict['__child_ids__'] = [self.employee_georges.id, self.employee_pierre.id] + self.assertIn(emp_dict, result) + + employee_count = HrEmployee.search_count([('id', 'not in', employees.ids), ('parent_id', '=', False)]) + result = HrEmployee.hierarchy_read([('parent_id', '=', False)], ['id'], 'parent_id') + self.assertEqual(len(result), 1 + employee_count) + for employee_dict in result: + self.assertFalse(employee_dict['parent_id'], "Each employee in the result should not have any parent set.") + self.assertIn({'id': self.employee_paul.id, 'parent_id': False, '__child_ids__': [self.employee_georges.id, self.employee_pierre.id]}, result) + + result = HrEmployee.hierarchy_read([('id', '=', self.employee_paul.id)], ['id'], 'parent_id') + self.assertEqual(len(result), 3) + for emp in employees: + emp_dict = {'id': emp.id, 'parent_id': emp.parent_id.id and (emp.parent_id.id, emp.parent_id.display_name)} + self.assertIn(emp_dict, result) + + result = HrEmployee.hierarchy_read([('id', '=', self.employee_georges.id)], ['id'], 'parent_id') + self.assertEqual(len(result), 3) + for emp in employees: + emp_dict = {'id': emp.id, 'parent_id': emp.parent_id.id and (emp.parent_id.id, emp.parent_id.display_name)} + if emp == self.employee_georges: + emp_dict['__focus__'] = True + self.assertIn(emp_dict, result) + + self.employee_pierre.parent_id = self.employee_georges + result = HrEmployee.hierarchy_read([('id', '=', self.employee_georges.id)], ['id'], 'parent_id') + self.assertEqual(len(result), 3) + for emp in employees: + emp_dict = {'id': emp.id, 'parent_id': emp.parent_id.id and (emp.parent_id.id, emp.parent_id.display_name)} + if emp == self.employee_georges: + emp_dict['__focus__'] = True + self.assertIn(emp_dict, result) + + result = HrEmployee.hierarchy_read([('id', '=', self.employee_pierre.id)], ['id'], 'parent_id') + self.assertEqual(len(result), 2) + self.assertIn( + {'id': self.employee_pierre.id, 'parent_id': (self.employee_georges.id, self.employee_georges.name), '__focus__': True}, + result + ) + self.assertIn( + {'id': self.employee_georges.id, 'parent_id': (self.employee_paul.id, self.employee_paul.name)}, + result + )