vanilla 18.0

This commit is contained in:
Ernad Husremovic 2025-10-08 10:48:09 +02:00
parent 5454004ff9
commit d7f6d2725e
979 changed files with 428093 additions and 0 deletions

View file

@ -0,0 +1,142 @@
import { expect, test } from "@odoo/hoot";
import { click, manuallyDispatchProgrammaticEvent } from "@odoo/hoot-dom";
import { animationFrame } from "@odoo/hoot-mock";
import {
contains,
defineModels,
fields,
mockService,
models,
mountView,
onRpc,
patchWithCleanup,
} from "@web/../tests/web_test_helpers";
import { AttachDocumentWidget } from "@web/views/widgets/attach_document/attach_document";
class Partner extends models.Model {
display_name = fields.Char({ string: "Displayed name" });
_records = [
{
id: 1,
display_name: "first record",
},
];
}
defineModels([Partner]);
test("attach document widget calls action with attachment ids", async () => {
// FIXME: This ugly hack is needed because the input is not attached in the DOM
// The input should be attached to the component and hidden in some way to make
// the interaction easier and more natural.
let fileInput;
patchWithCleanup(AttachDocumentWidget.prototype, {
setup() {
super.setup();
fileInput = this.fileInput;
},
});
mockService("http", {
post(route, params) {
expect.step("post");
expect(route).toBe("/web/binary/upload_attachment");
expect(params.model).toBe("partner");
expect(params.id).toBe(1);
return '[{ "id": 5 }, { "id": 2 }]';
},
});
onRpc(async ({ args, kwargs, method, model }) => {
expect.step(method);
if (method === "my_action") {
expect(model).toBe("partner");
expect(args).toEqual([1]);
expect(kwargs.attachment_ids).toEqual([5, 2]);
return true;
}
if (method === "web_save") {
expect(args[1]).toEqual({ display_name: "yop" });
}
if (method === "web_read") {
expect(args[0]).toEqual([1]);
}
});
await mountView({
type: "form",
resModel: "partner",
resId: 1,
arch: /* xml */ `
<form>
<widget name="attach_document" action="my_action" string="Attach document"/>
<field name="display_name" required="1"/>
</form>`,
});
expect.verifySteps(["get_views", "web_read"]);
await contains("[name='display_name'] input").edit("yop");
await animationFrame();
await click(".o_attach_document");
await animationFrame();
await manuallyDispatchProgrammaticEvent(fileInput, "change");
await animationFrame();
expect.verifySteps(["web_save", "post", "my_action", "web_read"]);
});
test("attach document widget calls action with attachment ids on a new record", async () => {
// FIXME: This ugly hack is needed because the input is not attached in the DOM
// The input should be attached to the component and hidden in some way to make
// the interaction easier and more natural.
let fileInput;
patchWithCleanup(AttachDocumentWidget.prototype, {
setup() {
super.setup();
fileInput = this.fileInput;
},
});
mockService("http", {
post(route, params) {
expect.step("post");
expect(route).toBe("/web/binary/upload_attachment");
expect(params.model).toBe("partner");
expect(params.id).toBe(2);
return '[{ "id": 5 }, { "id": 2 }]';
},
});
onRpc(async (params) => {
expect.step(params.method);
if (params.method === "my_action") {
expect(params.model).toBe("partner");
expect(params.args).toEqual([2]);
expect(params.kwargs.attachment_ids).toEqual([5, 2]);
return true;
}
if (params.method === "web_save") {
expect(params.args[1]).toEqual({ display_name: "yop" });
}
if (params.method === "web_read") {
expect(params.args[0]).toEqual([2]);
}
});
await mountView({
type: "form",
resModel: "partner",
arch: /* xml */ `
<form>
<widget name="attach_document" action="my_action" string="Attach document"/>
<field name="display_name" required="1"/>
</form>`,
});
expect.verifySteps(["get_views", "onchange"]);
await contains("[name='display_name'] input").edit("yop");
await click(".o_attach_document");
await animationFrame();
await manuallyDispatchProgrammaticEvent(fileInput, "change");
await animationFrame();
expect.verifySteps(["web_save", "post", "my_action", "web_read"]);
});

View file

@ -0,0 +1,140 @@
import {
defineModels,
fields,
models,
mountView,
mountWithCleanup,
} from "@web/../tests/web_test_helpers";
import { Component, xml } from "@odoo/owl";
import { expect, test } from "@odoo/hoot";
import { DocumentationLink } from "@web/views/widgets/documentation_link/documentation_link";
class Partner extends models.Model {
bar = fields.Boolean();
}
defineModels([Partner]);
test("documentation_link: default label and icon", async () => {
await mountView({
type: "form",
resModel: "partner",
arch: /* xml */ `
<form>
<field name="bar"/>
<widget name="documentation_link" path="/this_is_a_test.html"/>
</form>`,
});
expect(".o_doc_link").toHaveText("View Documentation");
expect("a.alert-link").toHaveCount(0);
expect(".o_doc_link .fa-external-link").toHaveCount(1);
});
test("documentationLink: alert-link", async () => {
await mountView({
type: "form",
resModel: "partner",
arch: /* xml */ `
<form>
<field name="bar"/>
<widget name="documentation_link" path="/this_is_a_test.html" alert_link="true"/>
</form>`,
});
expect("a.alert-link").toHaveCount(1);
});
test("DocumentationLink Component: alert-link", async () => {
class Parent extends Component {
static components = { DocumentationLink };
static template = xml`
<DocumentationLink path="'/this_is_a_test.html'" alertLink="true"/>`;
static props = ["*"];
}
await mountWithCleanup(Parent);
expect("a.alert-link").toHaveCount(1);
});
test("documentation_link: given label", async () => {
await mountView({
type: "form",
resModel: "partner",
arch: /* xml */ `
<form>
<field name="bar"/>
<widget name="documentation_link" path="/this_is_a_test.html" label="docdoc"/>
</form>`,
});
expect(".o_doc_link").toHaveText("docdoc");
expect(".o_doc_link .fa").toHaveCount(0);
});
test("documentation_link: given icon", async () => {
await mountView({
type: "form",
resModel: "partner",
arch: /* xml */ `
<form>
<field name="bar"/>
<widget name="documentation_link" path="/this_is_a_test.html" icon="fa-question-circle"/>
</form>`,
});
expect(".o_doc_link").toHaveText("");
expect(".o_doc_link .fa-question-circle").toHaveCount(1);
});
test("documentation_link: given label and icon", async () => {
await mountView({
type: "form",
resModel: "partner",
arch: /* xml */ `
<form>
<field name="bar"/>
<widget name="documentation_link" path="/this_is_a_test.html" label="docdoc" icon="fa-question-circle"/>
</form>`,
});
expect(".o_doc_link").toHaveText("docdoc");
expect(".o_doc_link .fa-question-circle").toHaveCount(1);
});
test("documentation_link: relative path", async () => {
await mountView({
type: "form",
resModel: "partner",
arch: /* xml */ `
<form>
<field name="bar"/>
<widget name="documentation_link" path="/applications/technical/web/settings/this_is_a_test.html"/>
</form>`,
});
expect(".o_doc_link").toHaveAttribute(
"href",
"https://www.odoo.com/documentation/1.0/applications/technical/web/settings/this_is_a_test.html"
);
});
test("documentation_link: absolute path (http)", async () => {
await mountView({
type: "form",
resModel: "partner",
arch: /* xml */ `
<form>
<field name="bar"/>
<widget name="documentation_link" path="http://www.odoo.com/"/>
</form>`,
});
expect(".o_doc_link").toHaveAttribute("href", "http://www.odoo.com/");
});
test("documentation_link: absolute path (https)", async () => {
await mountView({
type: "form",
resModel: "partner",
arch: /* xml */ `
<form>
<field name="bar"/>
<widget name="documentation_link" path="https://www.odoo.com/"/>
</form>`,
});
expect(".o_doc_link").toHaveAttribute("href", "https://www.odoo.com/");
});

View file

@ -0,0 +1,40 @@
import { expect, test } from "@odoo/hoot";
import { mockPermission } from "@odoo/hoot-mock";
import { defineModels, models, mountView } from "@web/../tests/web_test_helpers";
class Partner extends models.Model {
_records = [{ id: 1 }];
}
defineModels([Partner]);
const viewData = {
type: "form",
resModel: "partner",
resId: 1,
arch: /* xml */ `<form><widget name="notification_alert"/></form>`,
};
test("notification alert should be displayed when notification denied", async () => {
mockPermission("notifications", "denied");
await mountView(viewData);
expect(".o_widget_notification_alert .alert").toHaveCount(1, {
message: "notification alert should be displayed when notification denied",
});
});
test("notification alert should not be displayed when notification granted", async () => {
mockPermission("notifications", "granted");
await mountView(viewData);
expect(".o_widget_notification_alert .alert").toHaveCount(0, {
message: "notification alert should not be displayed when notification granted",
});
});
test("notification alert should not be displayed when notification default", async () => {
mockPermission("notifications", "default");
await mountView(viewData);
expect(".o_widget_notification_alert .alert").toHaveCount(0, {
message: "notification alert should not be displayed when notification default",
});
});

View file

@ -0,0 +1,226 @@
import { NameAndSignature } from "@web/core/signature/name_and_signature";
import {
defineModels,
fields,
models,
mountView,
onRpc,
patchWithCleanup,
contains,
} from "@web/../tests/web_test_helpers";
import { beforeEach, test, expect } from "@odoo/hoot";
import { click, waitFor } from "@odoo/hoot-dom";
class Partner extends models.Model {
display_name = fields.Char();
product_id = fields.Many2one({ string: "Product Name", relation: "product" });
sign = fields.Binary({ string: "Signature" });
_records = [
{
id: 1,
display_name: "Pop's Chock'lit",
product_id: 7,
},
];
}
class Product extends models.Model {
name = fields.Char({ string: "Product Name" });
_records = [
{
id: 7,
name: "Veggie Burger",
},
];
}
defineModels([Partner, Product]);
beforeEach(async () => {
onRpc("/web/sign/get_fonts/", () => {
return {};
});
});
test.tags("desktop");
test("Signature widget renders a Sign button on desktop", async () => {
patchWithCleanup(NameAndSignature.prototype, {
setup() {
super.setup();
expect(this.props.signature.name).toBe("");
},
});
await mountView({
type: "form",
resModel: "partner",
resId: 1,
arch: /* xml */ `
<form>
<header>
<widget name="signature" string="Sign"/>
</header>
</form>`,
});
expect("button.o_sign_button").toHaveClass("btn-secondary", {
message: `The button must have the 'btn-secondary' class as "highlight=0"`,
});
expect(".o_widget_signature button.o_sign_button").toHaveCount(1, {
message: "Should have a signature widget button",
});
expect(".modal-dialog").toHaveCount(0, {
message: "Should not have any modal",
});
// Clicks on the sign button to open the sign modal.
await click(".o_widget_signature button.o_sign_button");
await waitFor(".modal .modal-body");
expect(".modal-dialog").toHaveCount(1, {
message: "Should have one modal opened",
});
});
test.tags("mobile");
test("Signature widget renders a Sign button on mobile", async () => {
patchWithCleanup(NameAndSignature.prototype, {
setup() {
super.setup();
expect(this.props.signature.name).toBe("");
},
});
await mountView({
type: "form",
resModel: "partner",
resId: 1,
arch: /* xml */ `
<form>
<header>
<widget name="signature" string="Sign"/>
</header>
</form>`,
});
await contains(`.o_cp_action_menus button:has(.fa-cog)`).click();
expect("button.o_sign_button").toHaveClass("btn-secondary", {
message: `The button must have the 'btn-secondary' class as "highlight=0"`,
});
expect(".o_widget_signature button.o_sign_button").toHaveCount(1, {
message: "Should have a signature widget button",
});
expect(".modal-dialog").toHaveCount(0, {
message: "Should not have any modal",
});
// Clicks on the sign button to open the sign modal.
await click(".o_widget_signature button.o_sign_button");
await waitFor(".modal .modal-body");
expect(".modal-dialog").toHaveCount(1, {
message: "Should have one modal opened",
});
});
test.tags("desktop");
test("Signature widget: full_name option on desktop", async () => {
patchWithCleanup(NameAndSignature.prototype, {
setup() {
super.setup();
expect.step(this.props.signature.name);
},
});
await mountView({
type: "form",
resModel: "partner",
resId: 1,
arch: /* xml */ `
<form>
<header>
<widget name="signature" string="Sign" full_name="display_name"/>
</header>
<field name="display_name"/>
</form>`,
});
// Clicks on the sign button to open the sign modal.
await click("span.o_sign_label");
await waitFor(".modal .modal-body");
expect(".modal .modal-body a.o_web_sign_auto_button").toHaveCount(1);
expect.verifySteps(["Pop's Chock'lit"]);
});
test.tags("mobile");
test("Signature widget: full_name option on mobile", async () => {
patchWithCleanup(NameAndSignature.prototype, {
setup() {
super.setup();
expect.step(this.props.signature.name);
},
});
await mountView({
type: "form",
resModel: "partner",
resId: 1,
arch: /* xml */ `
<form>
<header>
<widget name="signature" string="Sign" full_name="display_name"/>
</header>
<field name="display_name"/>
</form>`,
});
await contains(`.o_cp_action_menus button:has(.fa-cog)`).click();
// Clicks on the sign button to open the sign modal.
await click("span.o_sign_label");
await waitFor(".modal .modal-body");
expect(".modal .modal-body a.o_web_sign_auto_button").toHaveCount(1);
expect.verifySteps(["Pop's Chock'lit"]);
});
test.tags("desktop");
test("Signature widget: highlight option on desktop", async () => {
await mountView({
type: "form",
resModel: "partner",
resId: 1,
arch: /* xml */ `
<form>
<header>
<widget name="signature" string="Sign" highlight="1"/>
</header>
</form>`,
});
expect("button.o_sign_button").toHaveClass("btn-primary", {
message: `The button must have the 'btn-primary' class as "highlight=1"`,
});
// Clicks on the sign button to open the sign modal.
await click(".o_widget_signature button.o_sign_button");
await waitFor(".modal .modal-body");
expect(".modal .modal-body a.o_web_sign_auto_button").toHaveCount(0);
});
test.tags("mobile");
test("Signature widget: highlight option on mobile", async () => {
await mountView({
type: "form",
resModel: "partner",
resId: 1,
arch: /* xml */ `
<form>
<header>
<widget name="signature" string="Sign" highlight="1"/>
</header>
</form>`,
});
await contains(`.o_cp_action_menus button:has(.fa-cog)`).click();
expect("button.o_sign_button").toHaveClass("btn-primary", {
message: `The button must have the 'btn-primary' class as "highlight=1"`,
});
// Clicks on the sign button to open the sign modal.
await click(".o_widget_signature button.o_sign_button");
await waitFor(".modal .modal-body");
expect(".modal .modal-body a.o_web_sign_auto_button").toHaveCount(0);
});

View file

@ -0,0 +1,134 @@
import { expect, test } from "@odoo/hoot";
import { click, queryAllTexts } from "@odoo/hoot-dom";
import {
clickSave,
defineModels,
defineParams,
fields,
models,
mountView,
onRpc,
} from "@web/../tests/web_test_helpers";
class Partner extends models.Model {
sun = fields.Boolean();
mon = fields.Boolean();
tue = fields.Boolean();
wed = fields.Boolean();
thu = fields.Boolean();
fri = fields.Boolean();
sat = fields.Boolean();
_records = [
{
id: 1,
sun: false,
mon: false,
tue: false,
wed: false,
thu: false,
fri: false,
sat: false,
},
];
}
defineModels([Partner]);
test("simple week recurrence widget", async () => {
expect.assertions(13);
defineParams({ lang_parameters: { week_start: 1 } });
let writeCall = 0;
onRpc("web_save", ({ args }) => {
writeCall++;
if (writeCall === 1) {
expect(args[1].sun).toBe(true);
}
if (writeCall === 2) {
expect(args[1].sun).not.toBe(true);
expect(args[1].mon).toBe(true);
expect(args[1].tue).toBe(true);
}
});
await mountView({
type: "form",
resModel: "partner",
resId: 1,
arch: /* xml */ `<form><sheet><group><widget name="week_days" /></group></sheet></form>`,
});
expect(queryAllTexts(".o_recurrent_weekday_label")).toEqual(
["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
{ message: "labels should be short week names" }
);
expect(".form-check input:disabled").toHaveCount(0, {
message: "all inputs should be enabled in edit mode",
});
await click("td:nth-child(7) input");
expect("td:nth-child(7) input").toBeChecked({
message: "sunday checkbox should be checked",
});
await clickSave();
await click("td:nth-child(1) input");
expect("td:nth-child(1) input").toBeChecked({
message: "monday checkbox should be checked",
});
await click("td:nth-child(2) input");
expect("td:nth-child(2) input").toBeChecked({
message: "tuesday checkbox should be checked",
});
// uncheck Sunday checkbox and check write call
await click("td:nth-child(7) input");
expect("td:nth-child(7) input").not.toBeChecked({
message: "sunday checkbox should be unchecked",
});
await clickSave();
expect("td:nth-child(7) input").not.toBeChecked({
message: "sunday checkbox should be unchecked",
});
expect("td:nth-child(1) input").toBeChecked({ message: "monday checkbox should be checked" });
expect("td:nth-child(2) input").toBeChecked({
message: "tuesday checkbox should be checked",
});
});
test("week recurrence widget readonly modifiers", async () => {
defineParams({ lang_parameters: { week_start: 1 } });
await mountView({
type: "form",
resModel: "partner",
resId: 1,
arch: /* xml */ `<form><sheet><group><widget name="week_days" readonly="1"/></group></sheet></form>`,
});
expect(queryAllTexts(".o_recurrent_weekday_label")).toEqual(
["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
{ message: "labels should be short week names" }
);
expect(".form-check input:disabled").toHaveCount(7, {
message: "all inputs should be disabled in readonly mode",
});
});
test("week recurrence widget show week start as per language configuration", async () => {
defineParams({ lang_parameters: { week_start: 5 } });
await mountView({
type: "form",
resModel: "partner",
resId: 1,
arch: /* xml */ `<form><sheet><group><widget name="week_days"/></group></sheet></form>`,
});
expect(queryAllTexts(".o_recurrent_weekday_label")).toEqual(
["Fri", "Sat", "Sun", "Mon", "Tue", "Wed", "Thu"],
{ message: "labels should be short week names" }
);
});