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
+ )