mirror of
https://github.com/bringout/oca-ocb-web.git
synced 2026-04-21 02:12:05 +02:00
19.0 vanilla
This commit is contained in:
parent
20e6dadd87
commit
4b94f0abc5
205 changed files with 24700 additions and 14614 deletions
|
|
@ -0,0 +1,235 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { afterEach, beforeEach, describe, expect, test } from "@odoo/hoot";
|
||||
import { advanceTime, animationFrame, queryFirst } from "@odoo/hoot-dom";
|
||||
import { Component, xml } from "@odoo/owl";
|
||||
import { mountWithCleanup, patchWithCleanup } from "@web/../tests/web_test_helpers";
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { Macro } from "@web/core/macro";
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
|
||||
const mainErrorMessage = (trigger) =>
|
||||
`Error: Potential non deterministic behavior found in 300ms for trigger ${trigger}.`;
|
||||
|
||||
let macro;
|
||||
async function waitForMacro() {
|
||||
for (let i = 0; i < 50; i++) {
|
||||
await animationFrame();
|
||||
await advanceTime(265);
|
||||
if (macro.isComplete) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!macro.isComplete) {
|
||||
throw new Error(`Macro is not complete`);
|
||||
}
|
||||
}
|
||||
class Root extends Component {
|
||||
static components = {};
|
||||
static template = xml/*html*/ `
|
||||
<t>
|
||||
<div class="container">
|
||||
<button class="button0">Button 0</button>
|
||||
<button class="button1">Button 1</button>
|
||||
<button class="button2">Button 2</button>
|
||||
</div>
|
||||
</t>
|
||||
`;
|
||||
static props = ["*"];
|
||||
}
|
||||
|
||||
registry.category("web_tour.tours").add("tour_to_check_undeterminisms", {
|
||||
steps: () => [
|
||||
{
|
||||
trigger: ".button0",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
trigger: ".button1",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
trigger: ".container",
|
||||
},
|
||||
{
|
||||
trigger: ".button2",
|
||||
run: "click",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
patchWithCleanup(Macro.prototype, {
|
||||
start() {
|
||||
super.start(...arguments);
|
||||
macro = this;
|
||||
},
|
||||
});
|
||||
patchWithCleanup(browser.console, {
|
||||
log: (s) => expect.step(`log: ${s}`),
|
||||
error: (s) => {
|
||||
s = s.replace(/\n +at.*/g, ""); // strip stack trace
|
||||
expect.step(`error: ${s}`);
|
||||
},
|
||||
warn: () => {},
|
||||
dir: () => {},
|
||||
});
|
||||
await mountWithCleanup(Root);
|
||||
await odoo.startTour("tour_to_check_undeterminisms", {
|
||||
mode: "auto",
|
||||
delayToCheckUndeterminisms: 300,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
macro.stop();
|
||||
});
|
||||
|
||||
test("element is no longer visible", async () => {
|
||||
macro.onStep = ({ index }) => {
|
||||
if (index == 2) {
|
||||
setTimeout(() => {
|
||||
queryFirst(".container").classList.add("d-none");
|
||||
}, 400);
|
||||
}
|
||||
};
|
||||
await waitForMacro();
|
||||
const expectedError = `Initial element is no longer visible`;
|
||||
expect.verifySteps([
|
||||
"log: [1/4] Tour tour_to_check_undeterminisms → Step .button0",
|
||||
"log: [2/4] Tour tour_to_check_undeterminisms → Step .button1",
|
||||
`error: FAILED: [2/4] Tour tour_to_check_undeterminisms → Step .button1.
|
||||
${mainErrorMessage(".button1")}
|
||||
${expectedError}`,
|
||||
]);
|
||||
});
|
||||
|
||||
test("change text", async () => {
|
||||
macro.onStep = ({ index }) => {
|
||||
if (index == 2) {
|
||||
setTimeout(() => {
|
||||
queryFirst(".button1").textContent = "Text has changed :)";
|
||||
}, 400);
|
||||
}
|
||||
};
|
||||
await waitForMacro();
|
||||
expect.verifySteps([
|
||||
"log: [1/4] Tour tour_to_check_undeterminisms → Step .button0",
|
||||
"log: [2/4] Tour tour_to_check_undeterminisms → Step .button1",
|
||||
`error: FAILED: [2/4] Tour tour_to_check_undeterminisms → Step .button1.
|
||||
${mainErrorMessage(".button1")}
|
||||
Initial element has changed:
|
||||
{
|
||||
"node": "<button class=\\"button1\\">Text has changed :)</button>",
|
||||
"modifiedText": [
|
||||
{
|
||||
"before": "Button 1",
|
||||
"after": "Text has changed :)"
|
||||
}
|
||||
]
|
||||
}`,
|
||||
]);
|
||||
});
|
||||
|
||||
test("change attributes", async () => {
|
||||
macro.onStep = ({ index }) => {
|
||||
if (index == 2) {
|
||||
setTimeout(() => {
|
||||
const button1 = queryFirst(".button1");
|
||||
button1.classList.add("brol");
|
||||
button1.classList.remove("button1");
|
||||
button1.setAttribute("data-value", "42");
|
||||
}, 400);
|
||||
}
|
||||
};
|
||||
await waitForMacro();
|
||||
const expectedError = `{
|
||||
"node": "<button class=\\"brol\\" data-value=\\"42\\">Button 1</button>",
|
||||
"modifiedAttributes": [
|
||||
{
|
||||
"attributeName": "class",
|
||||
"before": "button1",
|
||||
"after": "brol"
|
||||
},
|
||||
{
|
||||
"attributeName": "data-value",
|
||||
"before": null,
|
||||
"after": "42"
|
||||
}
|
||||
]
|
||||
}`;
|
||||
expect.verifySteps([
|
||||
"log: [1/4] Tour tour_to_check_undeterminisms → Step .button0",
|
||||
"log: [2/4] Tour tour_to_check_undeterminisms → Step .button1",
|
||||
`error: FAILED: [2/4] Tour tour_to_check_undeterminisms → Step .button1.
|
||||
${mainErrorMessage(".button1")}
|
||||
Initial element has changed:
|
||||
${expectedError}`,
|
||||
]);
|
||||
});
|
||||
|
||||
test("add child node", async () => {
|
||||
macro.onStep = ({ index }) => {
|
||||
if (index == 4) {
|
||||
setTimeout(() => {
|
||||
const addElement = document.createElement("div");
|
||||
addElement.classList.add("brol");
|
||||
addElement.textContent = "Hello world !";
|
||||
queryFirst(".container").appendChild(addElement);
|
||||
}, 400);
|
||||
}
|
||||
};
|
||||
await waitForMacro();
|
||||
const expectedError = `{
|
||||
"node": "<div class=\\"container\\"><button class=\\"button0\\">Button 0</button><button class=\\"button1\\">Button 1</button><button class=\\"button2\\">Button 2</button><div class=\\"brol\\">Hello world !</div></div>",
|
||||
"modifiedText": [
|
||||
{
|
||||
"before": "Button 0Button 1Button 2",
|
||||
"after": "Button 0Button 1Button 2Hello world !"
|
||||
}
|
||||
],
|
||||
"addedNodes": [
|
||||
{
|
||||
"newNode": "<div class=\\"brol\\">Hello world !</div>"
|
||||
}
|
||||
]
|
||||
}`;
|
||||
expect.verifySteps([
|
||||
"log: [1/4] Tour tour_to_check_undeterminisms → Step .button0",
|
||||
"log: [2/4] Tour tour_to_check_undeterminisms → Step .button1",
|
||||
"log: [3/4] Tour tour_to_check_undeterminisms → Step .container",
|
||||
`error: FAILED: [3/4] Tour tour_to_check_undeterminisms → Step .container.
|
||||
${mainErrorMessage(".container")}
|
||||
Initial element has changed:
|
||||
${expectedError}`,
|
||||
]);
|
||||
});
|
||||
|
||||
test.skip("snapshot is the same but has mutated", async () => {
|
||||
macro.onStep = async ({ index }) => {
|
||||
if (index === 2) {
|
||||
setTimeout(() => {
|
||||
const button1 = queryFirst(".button1");
|
||||
button1.setAttribute("data-value", "42");
|
||||
button1.classList.add("brol");
|
||||
button1.removeAttribute("data-value");
|
||||
button1.classList.remove("brol");
|
||||
}, 400);
|
||||
}
|
||||
};
|
||||
await waitForMacro();
|
||||
const expectedError = `Initial element has mutated 4 times:
|
||||
[
|
||||
"attribute: data-value",
|
||||
"attribute: class"
|
||||
]`;
|
||||
expect.verifySteps([
|
||||
"log: [1/4] Tour tour_to_check_undeterminisms → Step .button0",
|
||||
"log: [2/4] Tour tour_to_check_undeterminisms → Step .button1",
|
||||
`error: FAILED: [2/4] Tour tour_to_check_undeterminisms → Step .button1.
|
||||
${mainErrorMessage(".button1")}
|
||||
${expectedError}`,
|
||||
]);
|
||||
});
|
||||
|
|
@ -1,67 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { disableTours } from "@web_tour/debug/debug_manager";
|
||||
|
||||
import { hotkeyService } from "@web/core/hotkeys/hotkey_service";
|
||||
import { ormService } from "@web/core/orm_service";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { uiService } from "@web/core/ui/ui_service";
|
||||
|
||||
import { click, getFixture, mount } from "@web/../tests/helpers/utils";
|
||||
import { makeTestEnv } from "@web/../tests/helpers/mock_env";
|
||||
import { makeFakeLocalizationService, fakeCommandService } from "@web/../tests/helpers/mock_services";
|
||||
import { DebugMenuParent } from "@web/../tests/core/debug/debug_manager_tests";
|
||||
|
||||
const debugRegistry = registry.category("debug");
|
||||
let target;
|
||||
|
||||
QUnit.module("Tours", (hooks) => {
|
||||
|
||||
QUnit.module("DebugManager");
|
||||
|
||||
hooks.beforeEach(async () => {
|
||||
target = getFixture();
|
||||
registry
|
||||
.category("services")
|
||||
.add("hotkey", hotkeyService)
|
||||
.add("ui", uiService)
|
||||
.add("orm", ormService)
|
||||
.add("localization", makeFakeLocalizationService())
|
||||
.add("command", fakeCommandService);
|
||||
});
|
||||
|
||||
QUnit.test("can disable tours", async (assert) => {
|
||||
debugRegistry.category("default").add("disableTours", disableTours);
|
||||
|
||||
const fakeTourService = {
|
||||
start(env) {
|
||||
return {
|
||||
getActiveTours() {
|
||||
return [{ name: 'a' }, { name: 'b' }];
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
registry.category("services").add("tour", fakeTourService);
|
||||
|
||||
const mockRPC = async (route, args) => {
|
||||
if (args.method === "check_access_rights") {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
if (args.method === "consume") {
|
||||
assert.step("consume");
|
||||
assert.deepEqual(args.args[0], ['a', 'b']);
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
};
|
||||
const env = await makeTestEnv({ mockRPC });
|
||||
|
||||
await mount(DebugMenuParent, target, { env });
|
||||
|
||||
await click(target.querySelector("button.dropdown-toggle"));
|
||||
|
||||
assert.containsOnce(target, ".dropdown-item");
|
||||
await click(target.querySelector(".dropdown-item"));
|
||||
assert.verifySteps(["consume"]);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,629 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { afterEach, beforeEach, describe, expect, test } from "@odoo/hoot";
|
||||
import { advanceTime, animationFrame, queryFirst } from "@odoo/hoot-dom";
|
||||
import { Component, xml } from "@odoo/owl";
|
||||
import {
|
||||
getService,
|
||||
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");
|
||||
|
||||
const tourRegistry = registry.category("web_tour.tours");
|
||||
let macro;
|
||||
async function waitForMacro() {
|
||||
for (let i = 0; i < 50; i++) {
|
||||
await animationFrame();
|
||||
await advanceTime(265);
|
||||
if (macro.isComplete) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!macro.isComplete) {
|
||||
throw new Error(`Macro is not complete`);
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
patchWithCleanup(Macro.prototype, {
|
||||
start() {
|
||||
super.start(...arguments);
|
||||
macro = this;
|
||||
},
|
||||
});
|
||||
patchWithCleanup(console, {
|
||||
error: () => {},
|
||||
warn: () => {},
|
||||
log: () => {},
|
||||
dir: () => {},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
macro.stop();
|
||||
});
|
||||
|
||||
test("Step Tour validity", async () => {
|
||||
patchWithCleanup(console, {
|
||||
error: (msg) => expect.step(msg),
|
||||
});
|
||||
const steps = [
|
||||
{
|
||||
Belgium: true,
|
||||
wins: "of course",
|
||||
EURO2024: true,
|
||||
trigger: "button.foo",
|
||||
},
|
||||
{
|
||||
my_title: "EURO2024",
|
||||
trigger: "button.bar",
|
||||
doku: "Lukaku 10",
|
||||
},
|
||||
{
|
||||
trigger: "button.bar",
|
||||
run: ["Enjoy euro 2024"],
|
||||
},
|
||||
{
|
||||
trigger: "button.bar",
|
||||
run() {},
|
||||
},
|
||||
];
|
||||
tourRegistry.add("tour1", {
|
||||
steps: () => steps,
|
||||
});
|
||||
await makeMockEnv({});
|
||||
const waited_error1 = `Error in schema for TourStep ${JSON.stringify(
|
||||
steps[0],
|
||||
null,
|
||||
4
|
||||
)}\nInvalid object: unknown key 'Belgium', unknown key 'wins', unknown key 'EURO2024'`;
|
||||
const waited_error2 = `Error in schema for TourStep ${JSON.stringify(
|
||||
steps[1],
|
||||
null,
|
||||
4
|
||||
)}\nInvalid object: unknown key 'my_title', unknown key 'doku'`;
|
||||
const waited_error3 = `Error in schema for TourStep ${JSON.stringify(
|
||||
steps[2],
|
||||
null,
|
||||
4
|
||||
)}\nInvalid object: 'run' is not a string or function or boolean`;
|
||||
await getService("tour_service").startTour("tour1");
|
||||
await animationFrame();
|
||||
expect.verifySteps([waited_error1, waited_error2, waited_error3]);
|
||||
});
|
||||
|
||||
test("a tour with invalid step trigger", async () => {
|
||||
patchWithCleanup(browser.console, {
|
||||
groupCollapsed: (s) => expect.step(`log: ${s}`),
|
||||
log: (s) => expect.step(`log: ${s}`),
|
||||
warn: (s) => {},
|
||||
error: (s) => expect.step(`error: ${s}`),
|
||||
});
|
||||
tourRegistry.add("tour_invalid_trigger", {
|
||||
steps: () => [
|
||||
{
|
||||
trigger: ".button0:contins(brol)",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
trigger: ".button1:has(machin)",
|
||||
run: "click",
|
||||
},
|
||||
],
|
||||
});
|
||||
await odoo.startTour("tour_invalid_trigger", { mode: "auto" }); // Use odoo to run tour from registry because this is a test tour
|
||||
await waitForMacro();
|
||||
const expectedSteps = [
|
||||
"log: [1/2] Tour tour_invalid_trigger → Step .button0:contins(brol)",
|
||||
`error: FAILED: [1/2] Tour tour_invalid_trigger → Step .button0:contins(brol).
|
||||
ERROR during find trigger:
|
||||
Failed to execute 'querySelectorAll' on 'Element': '.button0:contins(brol)' is not a valid selector.`,
|
||||
];
|
||||
expect.verifySteps(expectedSteps);
|
||||
});
|
||||
|
||||
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) => {
|
||||
s = s.replace(/\n +at.*/g, ""); // strip stack trace
|
||||
expect.step(`error: ${s}`);
|
||||
},
|
||||
});
|
||||
class Root extends Component {
|
||||
static components = {};
|
||||
static template = xml/*html*/ `
|
||||
<t>
|
||||
<button class="button0">Button 0</button>
|
||||
<button class="button1">Button 1</button>
|
||||
<button class="button2">Button 2</button>
|
||||
</t>
|
||||
`;
|
||||
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 waitForMacro();
|
||||
const expectedError = [
|
||||
"log: [1/2] Tour tour2 → Step .button0",
|
||||
`log: [2/2] Tour tour2 → Step .button1`,
|
||||
[
|
||||
"error: FAILED: [2/2] Tour tour2 → Step .button1.",
|
||||
`TypeError: 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*/ `
|
||||
<t>
|
||||
<button class="button0">Button 0</button>
|
||||
<button class="button1" disabled="">Button 1</button>
|
||||
<button class="button2">Button 2</button>
|
||||
</t>
|
||||
`;
|
||||
static props = ["*"];
|
||||
}
|
||||
|
||||
await mountWithCleanup(Root);
|
||||
tourRegistry.add("tour3", {
|
||||
timeout: 500,
|
||||
steps: () => [
|
||||
{
|
||||
trigger: ".button0",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
trigger: ".button1",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
trigger: ".button2",
|
||||
run: "click",
|
||||
},
|
||||
],
|
||||
});
|
||||
await odoo.startTour("tour3", { mode: "auto" });
|
||||
await waitForMacro();
|
||||
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 step failed to complete within 500 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*/ `
|
||||
<t>
|
||||
<button class="button0">Button 0</button>
|
||||
<button class="button1">Button 1</button>
|
||||
<button class="button2">Button 2</button>
|
||||
<button class="button3">Button 3</button>
|
||||
<button class="button4">Button 4</button>
|
||||
<button class="button5">Button 5</button>
|
||||
<button class="button6">Button 6</button>
|
||||
<button class="button7">Button 7</button>
|
||||
</t>
|
||||
`;
|
||||
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 waitForMacro();
|
||||
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 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*/ `
|
||||
<t>
|
||||
<div class="container">
|
||||
<button class="button0">Button 0</button>
|
||||
<button class="button1">Button 1</button>
|
||||
<button class="button2">Button 2</button>
|
||||
</div>
|
||||
</t>
|
||||
`;
|
||||
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 waitForMacro();
|
||||
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*/ `
|
||||
<t>
|
||||
<div class="container">
|
||||
<button class="button0">Button 0</button>
|
||||
<button class="button1" style="display:none;">Button 1</button>
|
||||
<button class="button2">Button 2</button>
|
||||
</div>
|
||||
</t>
|
||||
`;
|
||||
static props = ["*"];
|
||||
}
|
||||
|
||||
await mountWithCleanup(Root);
|
||||
registry.category("web_tour.tours").add("tour_de_wallonie", {
|
||||
timeout: 777,
|
||||
steps: () => [
|
||||
{
|
||||
trigger: ".button0",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
trigger: ".button1",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
trigger: ".button2",
|
||||
run: "click",
|
||||
},
|
||||
],
|
||||
});
|
||||
await odoo.startTour("tour_de_wallonie", { mode: "auto" });
|
||||
await waitForMacro();
|
||||
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 step failed to complete within 777 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*/ `
|
||||
<t>
|
||||
<div class="container">
|
||||
<button class="button0">Button 0</button>
|
||||
<button class="button1" style="display:none;">Button 1</button>
|
||||
<button class="button2">Button 2</button>
|
||||
</div>
|
||||
</t>
|
||||
`;
|
||||
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 waitForMacro();
|
||||
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*/ `
|
||||
<t>
|
||||
<div class="container">
|
||||
<button class="button0">Button 0</button>
|
||||
<button class="button1">Button 1</button>
|
||||
<button class="button2">Button 2</button>
|
||||
<button class="button3">Button 3</button>
|
||||
<button class="button4">Button 4</button>
|
||||
<button class="button5">Button 5</button>
|
||||
</div>
|
||||
</t>
|
||||
`;
|
||||
static props = ["*"];
|
||||
}
|
||||
await mountWithCleanup(Root);
|
||||
await odoo.startTour("tour_des_flandres", { mode: "auto" });
|
||||
await waitForMacro();
|
||||
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`
|
||||
<Dialog>
|
||||
<button class="a">A</button>
|
||||
<button class="b">B</button>
|
||||
</Dialog>
|
||||
`;
|
||||
}
|
||||
class Root extends Component {
|
||||
static components = {};
|
||||
static template = xml/*html*/ `
|
||||
<t>
|
||||
<div class="container">
|
||||
<div class="p-3"><button class="button0" t-on-click="openDialog">Button 0</button></div>
|
||||
<div class="p-3"><button class="button1">Button 1</button></div>
|
||||
<div class="p-3"><button class="button2">Button 2</button></div>
|
||||
<div class="p-3"><button class="button3">Button 3</button></div>
|
||||
</div>
|
||||
</t>
|
||||
`;
|
||||
static props = ["*"];
|
||||
setup() {
|
||||
this.dialogService = useService("dialog");
|
||||
}
|
||||
openDialog() {
|
||||
this.dialogService.add(DummyDialog);
|
||||
}
|
||||
}
|
||||
await mountWithCleanup(Root);
|
||||
|
||||
registry.category("web_tour.tours").add("tour_check_modal", {
|
||||
timeout: 888,
|
||||
steps: () => [
|
||||
{
|
||||
trigger: ".button0",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
trigger: ".button1",
|
||||
run: "click",
|
||||
},
|
||||
],
|
||||
});
|
||||
await odoo.startTour("tour_check_modal", { mode: "auto" });
|
||||
await waitForMacro();
|
||||
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 step failed to complete within 888 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*/ `
|
||||
<t>
|
||||
<button class="button0">Button 0</button>
|
||||
<button class="button1">Button 1</button>
|
||||
<button class="button2">Button 2</button>
|
||||
</t>
|
||||
`;
|
||||
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 waitForMacro();
|
||||
expect.verifySteps([
|
||||
`error: FAILED: [2/2] Tour tour_hoot_failed → Step content (trigger: .button1:brol(:machin)).
|
||||
ERROR during find trigger:
|
||||
Failed to execute 'querySelectorAll' on 'Element': '.button1:brol(:machin)' is not a valid selector.`,
|
||||
]);
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,249 +0,0 @@
|
|||
odoo.define('web_tour.tour_manager_tests', async function (require) {
|
||||
"use strict";
|
||||
|
||||
const core = require("web.core");
|
||||
const KanbanView = require('web.KanbanView');
|
||||
const TourManager = require('web_tour.TourManager');
|
||||
const testUtils = require('web.test_utils');
|
||||
const createView = testUtils.createView;
|
||||
|
||||
/**
|
||||
* Create a widget and a TourManager instance with a list of given Tour objects.
|
||||
* @see `TourManager.register()` for more details on the Tours registry system.
|
||||
* @param {Object} params aside from the parameters defined below, passed
|
||||
* to {@see addMockEnvironment}.
|
||||
* @param {string[]} [params.consumed_tours]
|
||||
* @param {boolean} [params.debug] also passed along
|
||||
* @param {boolean} [params.disabled]
|
||||
* @param {string} params.template inner HTML content of the widget
|
||||
* @param {Object[]} params.tours { {string} name, {Object} option, {Object[]} steps }
|
||||
*/
|
||||
async function createTourManager({ consumed_tours, disabled, template, tours, ...params }) {
|
||||
const parent = await testUtils.createParent(params);
|
||||
const tourManager = new TourManager(parent, consumed_tours, disabled);
|
||||
tourManager.running_step_delay = 0;
|
||||
for (const { name, options, steps } of tours) {
|
||||
tourManager.register(name, options, steps);
|
||||
}
|
||||
const _destroy = tourManager.destroy;
|
||||
tourManager.destroy = function () {
|
||||
tourManager.destroy = _destroy;
|
||||
parent.destroy();
|
||||
};
|
||||
await parent.prependTo(testUtils.prepareTarget(params.debug));
|
||||
parent.el.innerHTML = template;
|
||||
await tourManager._register_all(true);
|
||||
// Wait for possible tooltips to be loaded and appended.
|
||||
await testUtils.nextTick();
|
||||
return tourManager;
|
||||
}
|
||||
|
||||
QUnit.module("Tours", function () {
|
||||
|
||||
QUnit.module("Tour manager");
|
||||
|
||||
QUnit.test("Tours sequence", async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
const tourManager = await createTourManager({
|
||||
template: `
|
||||
<button class="btn anchor">Anchor</button>`,
|
||||
tours: [
|
||||
{ name: "Tour 1", options: { sequence: 10 }, steps: [{ trigger: '.anchor' }] },
|
||||
{ name: "Tour 2", options: {}, steps: [{ trigger: '.anchor' }] },
|
||||
{ name: "Tour 3", options: { sequence: 5 }, steps: [{ trigger: '.anchor', content: "Oui" }] },
|
||||
],
|
||||
// Use this test in "debug" mode because the tips need to be in
|
||||
// the viewport to be able to test their normal content
|
||||
// (otherwise, the tips would indicate to the users that they
|
||||
// have to scroll).
|
||||
debug: true,
|
||||
});
|
||||
|
||||
assert.containsOnce(document.body, '.o_tooltip:visible');
|
||||
assert.strictEqual($('.o_tooltip_content:visible').text(), "Oui",
|
||||
"content should be that of the third tour");
|
||||
|
||||
tourManager.destroy();
|
||||
});
|
||||
|
||||
QUnit.test("Displays a rainbow man by default at the end of tours", async function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
function onShowEffect(params) {
|
||||
assert.deepEqual(params, {
|
||||
fadeout: "medium",
|
||||
message: owl.markup("<strong><b>Good job!</b> You went through all steps of this tour.</strong>"),
|
||||
type: "rainbow_man"
|
||||
});
|
||||
}
|
||||
core.bus.on("show-effect", null, onShowEffect);
|
||||
|
||||
const tourManager = await createTourManager({
|
||||
data: { 'web_tour.tour': { fields: {}, consume() {} } },
|
||||
template: `<button class="btn anchor">Anchor</button>`,
|
||||
tours: [{
|
||||
name: "Some tour",
|
||||
options: {},
|
||||
steps: [{ trigger: '.anchor', content: "anchor" }],
|
||||
}],
|
||||
// Use this test in "debug" mode because the tips need to be in
|
||||
// the viewport to be able to test their normal content
|
||||
// (otherwise, the tips would indicate to the users that they
|
||||
// have to scroll).
|
||||
debug: true,
|
||||
});
|
||||
|
||||
assert.containsOnce(document.body, '.o_tooltip');
|
||||
await testUtils.dom.click($('.anchor'));
|
||||
assert.containsNone(document.body, '.o_tooltip');
|
||||
|
||||
tourManager.destroy();
|
||||
core.bus.off("show-effect", onShowEffect);
|
||||
});
|
||||
|
||||
QUnit.test("Click on invisible tip consumes it", async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
const tourManager = await createTourManager({
|
||||
data: { 'web_tour.tour': { fields: {}, consume() {} } },
|
||||
template: `
|
||||
<button class="btn anchor1">Anchor</button>
|
||||
<button class="btn anchor2">Anchor</button>
|
||||
`,
|
||||
tours: [{
|
||||
name: "Tour 1",
|
||||
options: { rainbowMan: false, sequence: 10 },
|
||||
steps: [{ trigger: '.anchor1', content: "1" }],
|
||||
}, {
|
||||
name: "Tour 2",
|
||||
options: { rainbowMan: false, sequence: 5 },
|
||||
steps: [{ trigger: '.anchor2', content: "2" }],
|
||||
}],
|
||||
// Use this test in "debug" mode because the tips need to be in
|
||||
// the viewport to be able to test their normal content
|
||||
// (otherwise, the tips would indicate to the users that they
|
||||
// have to scroll).
|
||||
debug: true,
|
||||
});
|
||||
|
||||
assert.containsN(document.body, '.o_tooltip', 2);
|
||||
assert.strictEqual($('.o_tooltip_content:visible').text(), "2");
|
||||
|
||||
await testUtils.dom.click($('.anchor1'));
|
||||
assert.containsOnce(document.body, '.o_tooltip');
|
||||
assert.strictEqual($('.o_tooltip_content:visible').text(), "2");
|
||||
|
||||
await testUtils.dom.click($('.anchor2'));
|
||||
assert.containsNone(document.body, '.o_tooltip');
|
||||
|
||||
tourManager.destroy();
|
||||
});
|
||||
|
||||
QUnit.test("Step anchor replaced", async function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
const tourManager = await createTourManager({
|
||||
observe: true,
|
||||
template: `<input class="anchor"/>`,
|
||||
tours: [{
|
||||
name: "Tour",
|
||||
options: { rainbowMan: false },
|
||||
steps: [{ trigger: "input.anchor" }],
|
||||
}],
|
||||
});
|
||||
|
||||
assert.containsOnce(document.body, '.o_tooltip:visible');
|
||||
|
||||
|
||||
const $anchor = $(".anchor");
|
||||
const $parent = $anchor.parent();
|
||||
$parent.empty();
|
||||
$parent.append($anchor);
|
||||
// Simulates the observer picking up the mutation and triggering an update
|
||||
tourManager.update();
|
||||
await testUtils.nextTick();
|
||||
|
||||
assert.containsOnce(document.body, '.o_tooltip:visible');
|
||||
|
||||
await testUtils.fields.editInput($('.anchor'), "AAA");
|
||||
|
||||
assert.containsNone(document.body, '.o_tooltip:visible');
|
||||
|
||||
tourManager.destroy();
|
||||
});
|
||||
|
||||
QUnit.test("kanban quick create VS tour tooltips", async function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
const kanban = await createView({
|
||||
View: KanbanView,
|
||||
model: 'partner',
|
||||
data: {
|
||||
partner: {
|
||||
fields: {
|
||||
foo: {string: "Foo", type: "char"},
|
||||
bar: {string: "Bar", type: "boolean"},
|
||||
},
|
||||
records: [
|
||||
{id: 1, bar: true, foo: "yop"},
|
||||
]
|
||||
}
|
||||
},
|
||||
arch: `<kanban>
|
||||
<field name="bar"/>
|
||||
<templates><t t-name="kanban-box">
|
||||
<div><field name="foo"/></div>
|
||||
</t></templates>
|
||||
</kanban>`,
|
||||
groupBy: ['bar'],
|
||||
});
|
||||
|
||||
// click to add an element
|
||||
await testUtils.dom.click(kanban.$('.o_kanban_header .o_kanban_quick_add i').first());
|
||||
assert.containsOnce(kanban, '.o_kanban_quick_create',
|
||||
"should have open the quick create widget");
|
||||
|
||||
// create tour manager targeting the kanban quick create in its steps
|
||||
const tourManager = await createTourManager({
|
||||
observe: true,
|
||||
template: kanban.$el.html(),
|
||||
tours: [{
|
||||
name: "Tour",
|
||||
options: { rainbowMan: false },
|
||||
steps: [{ trigger: "input[name='display_name']" }],
|
||||
}],
|
||||
});
|
||||
|
||||
assert.containsOnce(document.body, '.o_tooltip:visible');
|
||||
|
||||
await testUtils.dom.click($('.o_tooltip:visible'));
|
||||
assert.containsOnce(kanban, '.o_kanban_quick_create',
|
||||
"the quick create should not have been destroyed when tooltip is clicked");
|
||||
|
||||
kanban.destroy();
|
||||
tourManager.destroy();
|
||||
});
|
||||
|
||||
QUnit.test("Automatic tour disabling", async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
const options = {
|
||||
template: `<button class="btn anchor">Anchor</button>`,
|
||||
tours: [{ name: "Tour", options: {}, steps: [{ trigger: '.anchor' }] }],
|
||||
};
|
||||
|
||||
const enabledTM = await createTourManager({ disabled: false, ...options });
|
||||
|
||||
assert.containsOnce(document.body, '.o_tooltip:visible');
|
||||
|
||||
enabledTM.destroy();
|
||||
|
||||
const disabledTM = await createTourManager({ disabled: true, ...options });
|
||||
|
||||
assert.containsNone(document.body, '.o_tooltip:visible');
|
||||
|
||||
disabledTM.destroy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,465 @@
|
|||
import { beforeEach, describe, expect, test } from "@odoo/hoot";
|
||||
import { click, edit, keyDown, keyUp, press, queryOne } from "@odoo/hoot-dom";
|
||||
import { animationFrame, runAllTimers } from "@odoo/hoot-mock";
|
||||
import {
|
||||
contains,
|
||||
defineWebModels,
|
||||
mountWithCleanup,
|
||||
onRpc,
|
||||
patchWithCleanup,
|
||||
serverState,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
import { AutoComplete } from "@web/core/autocomplete/autocomplete";
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { TourRecorder } from "@web_tour/js/tour_recorder/tour_recorder";
|
||||
import {
|
||||
TOUR_RECORDER_ACTIVE_LOCAL_STORAGE_KEY,
|
||||
tourRecorderState,
|
||||
} from "@web_tour/js/tour_recorder/tour_recorder_state";
|
||||
import { Component, xml } from "@odoo/owl";
|
||||
import { useAutofocus } from "@web/core/utils/hooks";
|
||||
import { WebClient } from "@web/webclient/webclient";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
|
||||
let tourRecorder;
|
||||
|
||||
beforeEach(async () => {
|
||||
serverState.debug = "1";
|
||||
browser.localStorage.setItem(TOUR_RECORDER_ACTIVE_LOCAL_STORAGE_KEY, "1");
|
||||
patchWithCleanup(TourRecorder.prototype, {
|
||||
setup() {
|
||||
tourRecorder = this;
|
||||
return super.setup(...arguments);
|
||||
},
|
||||
});
|
||||
|
||||
defineWebModels();
|
||||
await mountWithCleanup(WebClient);
|
||||
});
|
||||
|
||||
const checkTourSteps = (expected) => {
|
||||
expect(tourRecorder.state.steps.map((s) => s.trigger)).toEqual(expected);
|
||||
};
|
||||
|
||||
test("Click on element with unique odoo class", async () => {
|
||||
await mountWithCleanup(
|
||||
`
|
||||
<div class="o_parent">
|
||||
<div class="o_child_1 click"></div>
|
||||
<div class="o_child_2"></div>
|
||||
<div class="o_child_3"></div>
|
||||
</div>
|
||||
`,
|
||||
{ noMainContainer: true }
|
||||
);
|
||||
|
||||
expect(".o_tour_recorder").toHaveCount(1);
|
||||
await click(".o_button_record");
|
||||
await animationFrame();
|
||||
await click(".click");
|
||||
await animationFrame();
|
||||
checkTourSteps([".o_child_1"]);
|
||||
|
||||
await click(".o_child_2");
|
||||
await animationFrame();
|
||||
checkTourSteps([".o_child_1", ".o_child_2"]);
|
||||
});
|
||||
|
||||
test("Click on element with no unique odoo class", async () => {
|
||||
await mountWithCleanup(
|
||||
`
|
||||
<div class="o_parent">
|
||||
<div class="o_child_1 click"></div>
|
||||
<div class="o_child_1"></div>
|
||||
<div class="o_child_1"></div>
|
||||
</div>
|
||||
`,
|
||||
{ noMainContainer: true }
|
||||
);
|
||||
|
||||
expect(".o_tour_recorder").toHaveCount(1);
|
||||
await click(".o_button_record");
|
||||
await animationFrame();
|
||||
await click(".click");
|
||||
await animationFrame();
|
||||
checkTourSteps([".o_child_1:nth-child(1)"]);
|
||||
});
|
||||
|
||||
test("Find the nearest odoo class", async () => {
|
||||
await mountWithCleanup(`<a class="click"></a>`, { noMainContainer: true });
|
||||
|
||||
expect(".o_tour_recorder").toHaveCount(1);
|
||||
await click(".o_button_record");
|
||||
await animationFrame();
|
||||
await click(".click");
|
||||
await animationFrame();
|
||||
checkTourSteps([".o_web_client > a"]);
|
||||
});
|
||||
|
||||
test("Click on elements with 'data-menu-xmlid' attribute", async () => {
|
||||
await mountWithCleanup(
|
||||
`
|
||||
<div>
|
||||
<div></div>
|
||||
<div data-menu-xmlid="my_menu_1" class="click_1"></div>
|
||||
<div data-menu-xmlid="my_menu_2" class="click_2 o_div"></div>
|
||||
<div></div>
|
||||
</div>
|
||||
`,
|
||||
{ noMainContainer: true }
|
||||
);
|
||||
|
||||
expect(".o_tour_recorder").toHaveCount(1);
|
||||
await click(".o_button_record");
|
||||
await animationFrame();
|
||||
await click(".click_1");
|
||||
await click(".click_2");
|
||||
await animationFrame();
|
||||
checkTourSteps([
|
||||
".o_web_client div[data-menu-xmlid='my_menu_1']",
|
||||
".o_div[data-menu-xmlid='my_menu_2']",
|
||||
]);
|
||||
});
|
||||
|
||||
test("Click on elements with 'name' attribute", async () => {
|
||||
await mountWithCleanup(
|
||||
`
|
||||
<div>
|
||||
<div></div>
|
||||
<div name="sale_id" class="click_1"></div>
|
||||
<div name="partner_id" class="click_2 o_div"></div>
|
||||
<div></div>
|
||||
</div>
|
||||
`,
|
||||
{ noMainContainer: true }
|
||||
);
|
||||
|
||||
expect(".o_tour_recorder").toHaveCount(1);
|
||||
await click(".o_button_record");
|
||||
await animationFrame();
|
||||
await click(".click_1");
|
||||
await click(".click_2");
|
||||
await animationFrame();
|
||||
checkTourSteps([".o_web_client div[name='sale_id']", ".o_div[name='partner_id']"]);
|
||||
});
|
||||
|
||||
test("Click on element that have a link or button has parent", async () => {
|
||||
await mountWithCleanup(
|
||||
`
|
||||
<div>
|
||||
<button class="o_button"><i class="click_1">icon</i></button>
|
||||
<a class="o_link"><span class="click_2">This is my link</span></a>
|
||||
</div>
|
||||
`,
|
||||
{ noMainContainer: true }
|
||||
);
|
||||
|
||||
expect(".o_tour_recorder").toHaveCount(1);
|
||||
await click(".o_button_record");
|
||||
await animationFrame();
|
||||
await click(".click_1");
|
||||
await click(".click_2");
|
||||
await animationFrame();
|
||||
checkTourSteps([".o_button", ".o_link"]);
|
||||
});
|
||||
|
||||
test("Click on element with path that can be reduced", async () => {
|
||||
await mountWithCleanup(
|
||||
`
|
||||
<div class=".o_parent">
|
||||
<div name="field_name">
|
||||
<div class="o_div_2">
|
||||
<div class="o_div_3 click"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div name="field_partner_id">
|
||||
<div class="o_div_2">
|
||||
<div class="o_div_3"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
{ noMainContainer: true }
|
||||
);
|
||||
|
||||
expect(".o_tour_recorder").toHaveCount(1);
|
||||
await click(".o_button_record");
|
||||
await animationFrame();
|
||||
await click(".click");
|
||||
await animationFrame();
|
||||
checkTourSteps(["div[name='field_name'] .o_div_3"]);
|
||||
});
|
||||
|
||||
test("Click on input", async () => {
|
||||
await mountWithCleanup(
|
||||
`
|
||||
<div class=".o_parent">
|
||||
<input type="text" class="click o_input"/>
|
||||
</div>
|
||||
`,
|
||||
{ noMainContainer: true }
|
||||
);
|
||||
|
||||
expect(".o_tour_recorder").toHaveCount(1);
|
||||
await click(".o_button_record");
|
||||
await animationFrame();
|
||||
await click(".click");
|
||||
await animationFrame();
|
||||
expect(".o_button_record").toHaveText("Record (recording keyboard)");
|
||||
checkTourSteps([".o_input"]);
|
||||
});
|
||||
|
||||
test("Click on tag that is inside a contenteditable", async () => {
|
||||
await mountWithCleanup(
|
||||
`
|
||||
<div class=".o_parent">
|
||||
<div class="o_editor" contenteditable="true">
|
||||
<p class="click oe-hint oe-command-temporary-hint" placeholder="My placeholder..."></p>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
{ noMainContainer: true }
|
||||
);
|
||||
|
||||
expect(".o_tour_recorder").toHaveCount(1);
|
||||
await click(".o_button_record");
|
||||
await animationFrame();
|
||||
await click(".click");
|
||||
await animationFrame();
|
||||
expect(".o_button_record").toHaveText("Record (recording keyboard)");
|
||||
checkTourSteps([".o_editor[contenteditable='true']"]);
|
||||
});
|
||||
|
||||
test("Remove step during recording", async () => {
|
||||
await mountWithCleanup(
|
||||
`
|
||||
<div class="o_parent">
|
||||
<div class="o_child click"></div>
|
||||
</div>
|
||||
`,
|
||||
{ noMainContainer: true }
|
||||
);
|
||||
|
||||
expect(".o_tour_recorder").toHaveCount(1);
|
||||
await click(".o_button_record");
|
||||
await animationFrame();
|
||||
await click(".click");
|
||||
await animationFrame();
|
||||
checkTourSteps([".o_child"]);
|
||||
await click(".o_button_steps");
|
||||
await animationFrame();
|
||||
contains(".o_button_delete_step").click();
|
||||
await click(".o_button_steps");
|
||||
await animationFrame();
|
||||
checkTourSteps([]);
|
||||
});
|
||||
|
||||
test("Edit input", async () => {
|
||||
await mountWithCleanup(
|
||||
`
|
||||
<div class="o_parent">
|
||||
<input type="text" class="click o_input"/>
|
||||
</div>
|
||||
`,
|
||||
{ noMainContainer: true }
|
||||
);
|
||||
|
||||
expect(".o_tour_recorder").toHaveCount(1);
|
||||
await click(".o_button_record");
|
||||
await animationFrame();
|
||||
await click(".click");
|
||||
await animationFrame();
|
||||
expect(".o_button_record").toHaveText("Record (recording keyboard)");
|
||||
expect(".click").toBeFocused();
|
||||
await edit("Bismillah");
|
||||
await animationFrame();
|
||||
checkTourSteps([".o_input"]);
|
||||
expect(tourRecorder.state.steps.map((s) => s.run)).toEqual(["edit Bismillah"]);
|
||||
});
|
||||
|
||||
test("Save custom tour", async () => {
|
||||
onRpc("web_tour.tour", "create", ({ args }) => {
|
||||
const tour = args[0][0];
|
||||
expect.step(tour.name);
|
||||
expect.step(tour.url);
|
||||
expect.step(tour.step_ids.length);
|
||||
return true;
|
||||
});
|
||||
|
||||
await mountWithCleanup(
|
||||
`
|
||||
<div class="o_parent">
|
||||
<div class="click"></div>
|
||||
</div>
|
||||
`,
|
||||
{ noMainContainer: true }
|
||||
);
|
||||
|
||||
expect(".o_tour_recorder").toHaveCount(1);
|
||||
await click(".o_button_record");
|
||||
await animationFrame();
|
||||
await click(".click");
|
||||
await animationFrame();
|
||||
checkTourSteps([".o_parent > div"]);
|
||||
|
||||
await click(".o_button_save");
|
||||
await animationFrame();
|
||||
await contains("input[name='name']").click();
|
||||
await edit("tour_name");
|
||||
await animationFrame();
|
||||
await click(".o_button_save_confirm");
|
||||
await runAllTimers(); // Wait that the save notification disappear
|
||||
|
||||
expect.verifySteps(["tour_name", "/", 1]);
|
||||
});
|
||||
|
||||
test("Drag and drop", async () => {
|
||||
await mountWithCleanup(
|
||||
`
|
||||
<div class="o_parent">
|
||||
<div>
|
||||
<div class="o_drag">drag me</div>
|
||||
</div>
|
||||
<div class="o_drop"></div>
|
||||
</div>
|
||||
`,
|
||||
{ noMainContainer: true }
|
||||
);
|
||||
|
||||
expect(".o_tour_recorder").toHaveCount(1);
|
||||
await click(".o_button_record");
|
||||
await animationFrame();
|
||||
await contains(".o_drag").dragAndDrop(".o_drop");
|
||||
await animationFrame();
|
||||
checkTourSteps([".o_drag"]);
|
||||
expect(tourRecorder.state.steps.map((s) => s.run)).toEqual(["drag_and_drop .o_drop"]);
|
||||
});
|
||||
|
||||
test("Edit contenteditable", async () => {
|
||||
await mountWithCleanup(
|
||||
`
|
||||
<div class="o_parent">
|
||||
<div class="o_editor click" contenteditable="true" style="width: 50px; height: 50px">
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
{ noMainContainer: true }
|
||||
);
|
||||
|
||||
expect(".o_tour_recorder").toHaveCount(1);
|
||||
await click(".o_button_record");
|
||||
await animationFrame();
|
||||
await click(".click");
|
||||
await animationFrame();
|
||||
expect(".o_editor").toBeFocused();
|
||||
expect(".o_button_record").toHaveText("Record (recording keyboard)");
|
||||
await keyDown("B");
|
||||
await animationFrame();
|
||||
queryOne(".o_editor").appendChild(document.createTextNode("Bismillah"));
|
||||
await keyUp("B");
|
||||
await animationFrame();
|
||||
checkTourSteps([".o_editor[contenteditable='true']"]);
|
||||
expect(tourRecorder.state.steps.map((s) => s.run)).toEqual(["editor Bismillah"]);
|
||||
});
|
||||
|
||||
test("Selecting item in autocomplete field through Enter", async () => {
|
||||
class Dummy extends Component {
|
||||
static components = { AutoComplete };
|
||||
static template = xml`<AutoComplete id="'autocomplete'" value="'World'" sources="sources"/>`;
|
||||
static props = ["*"];
|
||||
|
||||
sources = [
|
||||
{
|
||||
options: [
|
||||
{ label: "World", onSelect() {} },
|
||||
{ label: "Hello", onSelect() {} },
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
expect(".o_tour_recorder").toHaveCount(1);
|
||||
await click(".o_button_record");
|
||||
await animationFrame();
|
||||
|
||||
await mountWithCleanup(Dummy);
|
||||
await click("#autocomplete");
|
||||
await animationFrame();
|
||||
await press("Enter");
|
||||
checkTourSteps([
|
||||
".o-autocomplete--input",
|
||||
".o-autocomplete--dropdown-item > a:contains('World'), .fa-circle-o-notch",
|
||||
]);
|
||||
expect(tourRecorder.state.steps.map((s) => s.run)).toEqual(["click", "click"]);
|
||||
});
|
||||
|
||||
test("Edit input after autofocus", async () => {
|
||||
class Dummy extends Component {
|
||||
static components = {};
|
||||
static template = xml/*html*/ `
|
||||
<t>
|
||||
<div class="container">
|
||||
<input type="text" class="o_input" t-ref="input"/>
|
||||
</div>
|
||||
</t>
|
||||
`;
|
||||
static props = ["*"];
|
||||
|
||||
setup() {
|
||||
useAutofocus({ refName: "input" });
|
||||
}
|
||||
}
|
||||
|
||||
expect(".o_tour_recorder").toHaveCount(1);
|
||||
await click(".o_button_record");
|
||||
await animationFrame();
|
||||
|
||||
await mountWithCleanup(Dummy);
|
||||
|
||||
expect(".o_input").toBeFocused();
|
||||
expect(".o_button_record").toHaveText("Record");
|
||||
await edit("Bismillah");
|
||||
await animationFrame();
|
||||
expect(".o_button_record").toHaveText("Record (recording keyboard)");
|
||||
checkTourSteps([".o_input"]);
|
||||
expect(tourRecorder.state.steps.map((s) => s.run)).toEqual(["edit Bismillah"]);
|
||||
});
|
||||
|
||||
test("Check Tour Recorder State", async () => {
|
||||
await mountWithCleanup(
|
||||
`
|
||||
<div class="o_parent">
|
||||
<div class="o_child_1 click"></div>
|
||||
<div class="o_child_2"></div>
|
||||
<div class="o_child_3"></div>
|
||||
</div>
|
||||
`,
|
||||
{ noMainContainer: true }
|
||||
);
|
||||
|
||||
expect(".o_tour_recorder").toHaveCount(1);
|
||||
expect(tourRecorderState.isRecording()).toBe("0");
|
||||
await click(".o_button_record");
|
||||
await animationFrame();
|
||||
expect(tourRecorderState.isRecording()).toBe("1");
|
||||
expect(tourRecorderState.getCurrentTourRecorder()).toEqual([]);
|
||||
await click(".click");
|
||||
await animationFrame();
|
||||
checkTourSteps([".o_child_1"]);
|
||||
|
||||
await click(".o_child_2");
|
||||
await animationFrame();
|
||||
checkTourSteps([".o_child_1", ".o_child_2"]);
|
||||
expect(tourRecorderState.getCurrentTourRecorder()).toEqual([
|
||||
{ trigger: ".o_child_1", run: "click" },
|
||||
{ trigger: ".o_child_2", run: "click" },
|
||||
]);
|
||||
|
||||
await click(".o_button_record");
|
||||
await animationFrame();
|
||||
await click(".o_tour_recorder_close_button");
|
||||
await animationFrame();
|
||||
expect(tourRecorderState.getCurrentTourRecorder()).toEqual([]);
|
||||
expect(tourRecorderState.isRecording()).toBe("0");
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue