/** @odoo-module **/
import { afterEach, beforeEach, describe, expect, test } from "@odoo/hoot";
import { queryFirst } from "@odoo/hoot-dom";
import { advanceTime, runAllTimers } from "@odoo/hoot-mock";
import { Component, xml } from "@odoo/owl";
import {
clearRegistry,
makeMockEnv,
mountWithCleanup,
patchWithCleanup,
} from "@web/../tests/web_test_helpers";
import { browser } from "@web/core/browser/browser";
import { Dialog } from "@web/core/dialog/dialog";
import { Macro } from "@web/core/macro";
import { registry } from "@web/core/registry";
import { useService } from "@web/core/utils/hooks";
describe.current.tags("desktop");
async function waitForStep(first = false) {
await advanceTime(10);
await advanceTime(500);
if (!first) {
await advanceTime(50);
}
}
const tourRegistry = registry.category("web_tour.tours");
let macro;
beforeEach(() => {
patchWithCleanup(Macro.prototype, {
start() {
super.start(...arguments);
macro = this;
},
});
patchWithCleanup(console, {
error: () => {},
warn: () => {},
log: () => {},
dir: () => {},
});
});
afterEach(async () => {
macro.stop();
clearRegistry(tourRegistry);
//Necessary in this case because the tours do not do
//synchronous setTimeouts one after the other.
await runAllTimers();
});
test("a failing tour logs the step that failed in run", async () => {
patchWithCleanup(browser.console, {
groupCollapsed: (s) => expect.step(`log: ${s}`),
log: (s) => expect.step(`log: ${s}`),
warn: (s) => {},
error: (s) => expect.step(`error: ${s}`),
});
class Root extends Component {
static components = {};
static template = xml/*html*/ `
`;
static props = ["*"];
}
await mountWithCleanup(Root);
tourRegistry.add("tour2", {
steps: () => [
{
trigger: ".button0",
run: "click",
},
{
trigger: ".button1",
run() {
const el = queryFirst(".wrong_selector");
el.click();
},
},
],
});
await odoo.startTour("tour2", { mode: "auto" }); // Use odoo to run tour from registry because this is a test tour
await waitForStep(true);
await waitForStep();
await waitForStep();
const expectedError = [
"log: [1/2] Tour tour2 → Step .button0",
`log: [2/2] Tour tour2 → Step .button1`,
[
"error: FAILED: [2/2] Tour tour2 → Step .button1.",
"ERROR IN ACTION: Cannot read properties of null (reading 'click')",
].join("\n"),
];
expect.verifySteps(expectedError);
});
test("a failing tour with disabled element", async () => {
patchWithCleanup(browser.console, {
log: (s) => {},
warn: (s) => {},
error: (s) => expect.step(`error: ${s}`),
});
class Root extends Component {
static components = {};
static template = xml/*html*/ `
`;
static props = ["*"];
}
await mountWithCleanup(Root);
tourRegistry.add("tour3", {
steps: () => [
{
trigger: ".button0",
run: "click",
},
{
trigger: ".button1",
run: "click",
},
{
trigger: ".button2",
run: "click",
},
],
});
await odoo.startTour("tour3", { mode: "auto" });
await waitForStep(true);
await waitForStep();
await waitForStep();
await advanceTime(10000);
const expectedError = [
`error: FAILED: [2/3] Tour tour3 → Step .button1.
Element has been found.
BUT: Element is not enabled. TIP: You can use :enable to wait the element is enabled before doing action on it.
TIMEOUT: The step failed to complete within 10000 ms.`,
];
expect.verifySteps(expectedError);
});
test("a failing tour logs the step that failed", async () => {
patchWithCleanup(browser.console, {
dir: (s) => expect.step(`runbot: ${s.replace(/[\s-]*/g, "")}`),
groupCollapsed: (s) => expect.step(`log: ${s}`),
log: (s) => expect.step(`log: ${s}`),
warn: (s) => expect.step(`warn: ${s.replace(/[\s-]*/gi, "")}`),
error: (s) => expect.step(`error: ${s}`),
});
class Root extends Component {
static components = {};
static template = xml/*html*/ `
`;
static props = ["*"];
}
await mountWithCleanup(Root);
tourRegistry.add("tour1", {
steps: () => [
{
content: "content",
trigger: ".button0",
run: "click",
},
{
content: "content",
trigger: ".button1",
run: "click",
},
{
content: "content",
trigger: ".button2",
run: "click",
},
{
content: "content",
trigger: ".button3",
run: "click",
},
{
content: "content",
trigger: ".wrong_selector",
timeout: 111,
run: "click",
},
{
content: "content",
trigger: ".button4",
run: "click",
},
{
content: "content",
trigger: ".button5",
run: "click",
},
{
content: "content",
trigger: ".button6",
run: "click",
},
{
content: "content",
trigger: ".button7",
run: "click",
},
],
});
await odoo.startTour("tour1", { mode: "auto" });
await waitForStep(true);
await waitForStep();
await waitForStep();
await waitForStep();
await waitForStep();
await waitForStep();
await waitForStep();
expect.verifySteps([
"log: [1/9] Tour tour1 → Step content (trigger: .button0)",
"log: [2/9] Tour tour1 → Step content (trigger: .button1)",
"log: [3/9] Tour tour1 → Step content (trigger: .button2)",
"log: [4/9] Tour tour1 → Step content (trigger: .button3)",
"log: [5/9] Tour tour1 → Step content (trigger: .wrong_selector)",
`error: FAILED: [5/9] Tour tour1 → Step content (trigger: .wrong_selector).
Element (.wrong_selector) has not been found.
TIMEOUT: The step failed to complete within 111 ms.`,
`runbot: {"content":"content","trigger":".button1","run":"click"},{"content":"content","trigger":".button2","run":"click"},{"content":"content","trigger":".button3","run":"click"},FAILED:[5/9]Tourtour1→Stepcontent(trigger:.wrong_selector){"content":"content","trigger":".wrong_selector","run":"click","timeout":111},{"content":"content","trigger":".button4","run":"click"},{"content":"content","trigger":".button5","run":"click"},{"content":"content","trigger":".button6","run":"click"},`,
]);
});
test("check tour with inactive steps", async () => {
class Root extends Component {
static components = {};
static template = xml/*html*/ `
`;
static props = ["*"];
}
await mountWithCleanup(Root);
registry.category("web_tour.tours").add("pipu_tour", {
steps: () => [
{
isActive: [".container:not(:has(.this_selector_is_not_here))"],
trigger: ".button0",
run() {
expect.step("this action 1 has not been skipped");
},
},
{
isActive: [".container:not(:has(.button0))"],
trigger: ".button1",
run() {
expect.step("this action 2 has been skipped");
},
},
{
isActive: [".container:not(:has(.this_selector_is_not_here))"],
trigger: ".button2",
run() {
expect.step("this action 3 has not been skipped");
},
},
],
});
await odoo.startTour("pipu_tour", { mode: "auto" });
await waitForStep(true);
await waitForStep();
await waitForStep();
await waitForStep();
expect.verifySteps([
"this action 1 has not been skipped",
"this action 3 has not been skipped",
]);
});
test("automatic tour with invisible element", async () => {
patchWithCleanup(browser.console, {
warn: (s) => {},
error: (s) => expect.step(`error: ${s}`),
});
await makeMockEnv();
class Root extends Component {
static components = {};
static template = xml/*html*/ `
`;
static props = ["*"];
}
await mountWithCleanup(Root);
registry.category("web_tour.tours").add("tour_de_wallonie", {
steps: () => [
{
trigger: ".button0",
run: "click",
},
{
trigger: ".button1",
run: "click",
},
{
trigger: ".button2",
run: "click",
},
],
});
await odoo.startTour("tour_de_wallonie", { mode: "auto" });
await waitForStep(true);
await waitForStep();
await waitForStep();
await advanceTime(10000);
expect.verifySteps([
`error: FAILED: [2/3] Tour tour_de_wallonie → Step .button1.
Element has been found.
BUT: Element is not visible. TIP: You can use :not(:visible) to force the search for an invisible element.
TIMEOUT: The step failed to complete within 10000 ms.`,
]);
});
test("automatic tour with invisible element but use :not(:visible))", async () => {
patchWithCleanup(browser.console, {
log: (s) => {
s.includes("tour succeeded") ? expect.step(`succeeded`) : false;
},
warn: (s) => {},
error: (s) => expect.step(`error: ${s}`),
});
await makeMockEnv();
class Root extends Component {
static components = {};
static template = xml/*html*/ `
`;
static props = ["*"];
}
await mountWithCleanup(Root);
registry.category("web_tour.tours").add("tour_de_wallonie", {
steps: () => [
{
trigger: ".button0",
run: "click",
},
{
trigger: ".button1:not(:visible)",
run: "click",
},
{
trigger: ".button2",
run: "click",
},
],
});
await odoo.startTour("tour_de_wallonie", { mode: "auto" });
await waitForStep(true);
await waitForStep();
await waitForStep();
await waitForStep();
expect.verifySteps(["succeeded"]);
});
test("automatic tour with alternative trigger", async () => {
let suppressLog = false;
patchWithCleanup(browser.console, {
groupCollapsed: (s) => {
expect.step("on step");
suppressLog = true;
},
groupEnd: () => {
suppressLog = false;
},
log: (s) => {
if (suppressLog) {
return;
}
if (s.toLowerCase().includes("tour tour_des_flandres succeeded")) {
expect.step("succeeded");
} else if (s !== "tour succeeded") {
expect.step("on step");
}
},
});
registry.category("web_tour.tours").add("tour_des_flandres", {
steps: () => [
{
trigger: ".interval, .button1",
},
{
trigger: ".interval, .button3",
},
{
trigger: ".interval1, .interval2, .button4",
},
{
trigger: ".button5",
},
],
});
class Root extends Component {
static components = {};
static template = xml/*html*/ `
`;
static props = ["*"];
}
await mountWithCleanup(Root);
await odoo.startTour("tour_des_flandres", { mode: "auto" });
await waitForStep(true);
await waitForStep();
await waitForStep();
await waitForStep();
await waitForStep();
await advanceTime(10000);
expect.verifySteps(["on step", "on step", "on step", "on step", "succeeded"]);
});
test("check not possible to click below modal", async () => {
patchWithCleanup(console, {
warn: () => {},
error: (s) => expect.step(`error: ${s}`),
log: (s) => expect.step(`log: ${s}`),
dir: () => {},
});
class DummyDialog extends Component {
static props = ["*"];
static components = { Dialog };
static template = xml`
`;
}
class Root extends Component {
static components = {};
static template = xml/*html*/ `
`;
static props = ["*"];
setup() {
this.dialogService = useService("dialog");
}
openDialog() {
this.dialogService.add(DummyDialog);
}
}
await mountWithCleanup(Root);
registry.category("web_tour.tours").add("tour_check_modal", {
steps: () => [
{
trigger: ".button0",
run: "click",
},
{
trigger: ".button1",
run: "click",
},
],
});
await odoo.startTour("tour_check_modal", { mode: "auto" });
await waitForStep(true);
await waitForStep();
await waitForStep();
await advanceTime(10000);
expect.verifySteps([
"log: [1/2] Tour tour_check_modal → Step .button0",
"log: [2/2] Tour tour_check_modal → Step .button1",
`error: FAILED: [2/2] Tour tour_check_modal → Step .button1.
Element has been found.
BUT: It is not allowed to do action on an element that's below a modal.
TIMEOUT: The step failed to complete within 10000 ms.`,
]);
});
test("a tour where hoot trigger failed", async () => {
patchWithCleanup(browser.console, {
error: (s) => expect.step(`error: ${s}`),
});
class Root extends Component {
static components = {};
static template = xml/*html*/ `
`;
static props = ["*"];
}
await mountWithCleanup(Root);
tourRegistry.add("tour_hoot_failed", {
steps: () => [
{
content: "content",
trigger: ".button0",
run: "click",
},
{
content: "content",
trigger: ".button1:brol(:machin)",
run: "click",
},
],
});
await odoo.startTour("tour_hoot_failed", { mode: "auto" });
await waitForStep(true);
await waitForStep();
await waitForStep();
expect.verifySteps([
`error: FAILED: [2/2] Tour tour_hoot_failed → Step content (trigger: .button1:brol(:machin)).
SyntaxError: Failed to execute 'querySelectorAll' on 'Element': '.button1:brol(:machin)' is not a valid selector.`,
]);
});
test("check for undeterminisms", async () => {
patchWithCleanup(browser.console, {
warn: (s) => {},
error: (s) => expect.step(`error: ${s}`),
log: (s) => expect.step(`log: ${s}`),
});
await makeMockEnv();
class Root extends Component {
static components = {};
static template = xml/*html*/ `
`;
static props = ["*"];
}
await mountWithCleanup(Root);
registry.category("web_tour.tours").add("tour_und", {
steps: () => [
{
trigger: ".button0",
run: "click",
},
{
trigger: ".button1",
run: "click",
},
{
trigger: ".button2",
run: "click",
},
],
});
await odoo.startTour("tour_und", {
mode: "auto",
delayToCheckUndeterminisms: 3000,
});
await advanceTime(10);
await advanceTime(500);
expect.verifySteps(["log: [1/3] Tour tour_und → Step .button0"]);
await advanceTime(3001);
await advanceTime(10);
await advanceTime(500);
expect.verifySteps(["log: [2/3] Tour tour_und → Step .button1"]);
await advanceTime(1000);
queryFirst(".button1").textContent = "Text has changed :)";
await advanceTime(1500);
await advanceTime(10000);
expect.verifySteps([
`error: FAILED: [2/3] Tour tour_und → Step .button1.
Element has been found.
UNDETERMINISM: two differents elements have been found in 3000ms for trigger .button1`,
]);
});