odoo18/addons_extensions/knowledge/static/tests/knowledge_hierarchy.test.js

332 lines
11 KiB
JavaScript

import KnowledgeHierarchy from "@knowledge/components/hierarchy/hierarchy";
import { registry } from "@web/core/registry";
import { standardWidgetProps } from "@web/views/widgets/standard_widget_props";
import { defineMailModels } from "@mail/../tests/mail_test_helpers";
import { after, before, expect, test } from "@odoo/hoot";
import { click, queryFirst } from "@odoo/hoot-dom";
import { animationFrame } from "@odoo/hoot-mock";
import {
defineModels,
fields,
MockServer,
models,
mountView,
onRpc,
patchWithCleanup,
} from "@web/../tests/web_test_helpers";
class Article extends models.Model {
name = fields.Char();
icon = fields.Char();
display_name = fields.Char();
parent_id = fields.Many2one({ relation: "article" });
root_article_id = fields.Many2one({ relation: "article" });
parent_path = fields.Char();
is_locked = fields.Boolean();
user_can_write = fields.Boolean();
user_has_access = fields.Boolean();
category = fields.Char();
_records = [
{
id: 1,
name: "Root Article",
icon: "🙂",
display_name: "🙂 Root Article",
parent_id: false,
root_article_id: 1,
parent_path: "1/",
is_locked: false,
user_can_write: true,
user_has_access: true,
category: "workspace",
},
{
id: 2,
name: "Article 2",
icon: "🙃",
display_name: "🙃 Article 2",
parent_id: 1,
root_article_id: 1,
parent_path: "1/2/",
is_locked: false,
user_can_write: true,
user_has_access: true,
category: "workspace",
},
{
id: 3,
name: "Article 3",
icon: false,
display_name: "Article 3",
parent_id: 2,
root_article_id: 1,
parent_path: "1/2/3/",
is_locked: false,
user_can_write: true,
user_has_access: true,
category: "workspace",
},
{
id: 4,
name: "Article 4",
icon: "😎",
display_name: "😎 Article 4",
parent_id: 3,
root_article_id: 1,
parent_path: "1/2/3/4/",
is_locked: false,
user_can_write: true,
user_has_access: true,
category: "workspace",
},
{
id: 5,
name: "Article 5",
icon: false,
display_name: "Article 5",
parent_id: false,
root_article_id: 5,
parent_path: "5/",
is_locked: false,
user_can_write: true,
user_has_access: true,
category: "workspace",
},
];
}
defineMailModels();
defineModels([Article]);
onRpc("get_sidebar_articles", function () {
return {
articles: this.env["article"]._filter(),
favorite_ids: [],
};
});
onRpc(
"get_article_hierarchy",
async function ({ args: [articleId], kwargs: { exclude_article_ids: excludeArticleIds } }) {
const Article = this.env["article"];
// returns ancestors of the current article (without the ones given in excludeArticleIds)
return Article.slice(0, Article.indexOf(Article.browse(articleId)[0]) - 1).filter(
(article) => !excludeArticleIds.includes(article.id)
);
}
);
let openArticle;
before(() => {
// Patch the component to make it a widget so that it can be tested without the complete topbar
patchWithCleanup(KnowledgeHierarchy.props, { ...standardWidgetProps });
registry.category("view_widgets").add("knowledge_hierarchy", {
component: KnowledgeHierarchy,
fieldDependencies: Object.entries(Article._fields).map(([fieldName, field]) => ({
name: fieldName,
type: field.type,
})),
});
// Patch to allow loading an article easily
patchWithCleanup(KnowledgeHierarchy.prototype, {
setup() {
super.setup();
openArticle = (resId) => this.env.openArticle(resId);
},
});
});
after(() => registry.category("view_widgets").remove("knowledge_hierarchy"));
/**
* Assert that the current article in the hierarchy is the given one
* @param {number} articleId
*/
const assertCurrentArticle = (articleId) => {
const [article] = MockServer.env["article"].browse(articleId);
expect(`.o_article_active:contains(${article.name})`).toHaveCount(1);
};
/**
* Assert that the hierarchy shows the articles in the same order as in the given array.
* @param {array} articleIds - array of article ids (or false for the dropdown toggle)
*/
const assertHierarchyArticles = async (articleIds) => {
expect(".o_hierarchy_item").toHaveCount(articleIds.length);
if (articleIds.length > 3) {
// Open dropdown to check articles between root and parent
await openHierarchyDropdown();
}
MockServer.env["article"].browse(articleIds).forEach((article, idx) => {
if (idx === articleIds.length - 1) {
// current article
expect(`.o_hierarchy_item:last-of-type:contains(${article.icon || "📄"})`).toHaveCount(
1
);
if (article.is_locked || !article.user_can_write) {
expect(`.o_hierarchy_item:last-of-type:contains(${article.name})`).toHaveCount(1);
} else {
expect(`.o_hierarchy_item:last-of-type input:value(${article.name})`).toHaveCount(
1
);
}
} else if (idx < articleIds.length - 2 && idx > 0) {
// article in dropdown
expect(
`.o-dropdown-item:nth-of-type(${idx}):contains(${article.display_name})`
).toHaveCount(1);
} else {
// root or parent article
expect(
`.o_hierarchy_item:nth-of-type(${idx + 1}) a:contains(${article.display_name})`
).toHaveCount(1);
}
});
};
/**
* Click on the given article in the hierarchy
* @param {number} id of the article to open
*/
const clickHierarchyArticle = async (articleId) => {
const [article] = MockServer.env["article"].browse(articleId);
const articleName = article.name;
const articleElement = queryFirst(`.o_hierarchy_item:contains(${articleName})`);
if (articleElement) {
await click(articleElement.querySelector("a"));
} else {
await openHierarchyDropdown();
await click(`.o-dropdown-item:contains(${articleName})`);
}
return animationFrame();
};
/**
* open the dropdown in the hierarchy
*/
const openHierarchyDropdown = async () => {
if (queryFirst(".o_hierarchy_item a:contains('...').show")) {
// dropdown already opened
return;
}
await click(".o_hierarchy_item a:contains('...')");
return animationFrame();
};
const viewParams = {
arch: /* xml */ `
<form js_class="knowledge_article_view_form">
<widget name="knowledge_hierarchy" class="d-flex"/>
</form>
`,
resModel: "article",
type: "form",
};
test("Hierarchy - Open articles in hierarchy", async function () {
await mountView({ ...viewParams, resId: 4 });
// Check that root, ellipsis, parent and current article are shown
await assertHierarchyArticles([1, 2, 3, 4]);
// Open article 3
await clickHierarchyArticle(3);
// Check that root, parent and current article are shown
await assertHierarchyArticles([1, 2, 3]);
// Open Article 2
await clickHierarchyArticle(2);
// Check that root and current article are shown
await assertHierarchyArticles([1, 2]);
// Open Root Article
await clickHierarchyArticle(1);
// Check that root is shown
await assertHierarchyArticles([1]);
openArticle(4);
await animationFrame();
await assertHierarchyArticles([1, 2, 3, 4]);
// Open Article 2 in the dropdown
await clickHierarchyArticle(2);
// Check that article 2 is opened
assertCurrentArticle(2);
});
test("Hierarchy - Inaccessible articles", async function () {
Article._records[1].user_has_access = false;
Article._records[3].user_can_write = false;
await mountView({ ...viewParams, resId: 4 });
// Check that the hierarchy is correct and that current article's icon and name are readonly
await assertHierarchyArticles([1, 2, 3, 4]);
expect(".o_hierarchy_item a.o_article_emoji").toHaveCount(0);
expect(".o_hierarchy_item input").toHaveCount(0);
// Check that the link to the article in the dropdown is disabled
const [article] = MockServer.env["article"].browse(2);
expect(`.o-dropdown-item.disabled:contains(${article.display_name})`).toHaveCount(1);
});
test("Hierarchy - Use breadcrumbs", async function () {
/**
* Assert that the breadcrumbs buttons are enabled/disabled
* @param {boolean} isPrevEnabled
* @param {boolean} isNextEnabled
*/
const assertBreadcrumbsButtons = (isPrevEnabled, isNextEnabled) => {
expect(
`.o_widget_knowledge_hierarchy .btn${
isPrevEnabled ? ":not(.disabled)" : ".disabled"
} .oi-chevron-left`
).toHaveCount(1);
expect(
`.o_widget_knowledge_hierarchy .btn${
isNextEnabled ? ":not(.disabled)" : ".disabled"
} .oi-chevron-right`
).toHaveCount(1);
};
const clickBack = async () => {
await click(".o_widget_knowledge_hierarchy .oi-chevron-left");
return animationFrame();
};
const clickNext = async () => {
await click(".o_widget_knowledge_hierarchy .oi-chevron-right");
return animationFrame();
};
await mountView({ ...viewParams, resId: 2 });
assertCurrentArticle(2);
// Back and Next should be disabled
assertBreadcrumbsButtons(false, false);
// Open an article
openArticle(1);
await animationFrame();
// Make sure current article changed
assertCurrentArticle(1);
// Back should be enabled, next should still be disabled
assertBreadcrumbsButtons(true, false);
// Use go back button, back should then be disabled and next should be enabled
await clickBack();
assertCurrentArticle(2);
assertBreadcrumbsButtons(false, true);
// Use go next button, back should then be enabled and next should be disabled
await clickNext();
assertCurrentArticle(1);
assertBreadcrumbsButtons(true, false);
// Open another article, back should then be enabled and next should be disabled
openArticle(5);
await animationFrame();
assertCurrentArticle(5);
assertBreadcrumbsButtons(true, false);
// Use go back button twice, back should then be disabled and next should be enabled
await clickBack();
await clickBack();
assertCurrentArticle(2);
assertBreadcrumbsButtons(false, true);
// Use go next button, back and next should then be enabled
await clickNext();
assertCurrentArticle(1);
assertBreadcrumbsButtons(true, true);
// Open another article, back should then be enabled and next should be disabled
openArticle(5);
await animationFrame();
assertCurrentArticle(5);
assertBreadcrumbsButtons(true, false);
});