web_tour/static/tests/tour_service_tests.js

743 lines
28 KiB
JavaScript
Raw Normal View History

2024-05-03 15:34:42 +03:00
/** @odoo-module **/
import { browser } from "@web/core/browser/browser";
import { MacroEngine } from "@web/core/macro";
import { registry } from "@web/core/registry";
import { tourService } from "@web_tour/tour_service/tour_service";
import { rpcService } from "@web/core/network/rpc_service";
import { userService } from "@web/core/user_service";
import { ormService } from "@web/core/orm_service";
import { notificationService } from "@web/core/notifications/notification_service";
import { effectService } from "@web/core/effects/effect_service";
import { registerCleanup } from "@web/../tests/helpers/cleanup";
import {
getFixture,
mount,
mockTimeout,
editInput,
click,
triggerEvent,
nextTick,
patchWithCleanup,
} from "@web/../tests/helpers/utils";
import { makeTestEnv } from "@web/../tests/helpers/mock_env";
import { Component, useState, xml } from "@odoo/owl";
import { session } from "@web/session";
let target, mock;
QUnit.module("Tour service", (hooks) => {
QUnit.module("tour_service");
let tourRegistry;
class Counter extends Component {
static template = xml/*html*/ `
<div class="counter">
<div class="interval">
<input type="number" t-model.number="state.interval" />
</div>
<div class="counter">
<span class="value" t-esc="state.value" />
<button class="inc" t-on-click="onIncrement">+</button>
</div>
</div>
`;
setup() {
this.state = useState({ interval: 1, value: 0 });
}
onIncrement() {
this.state.value += this.state.interval;
}
}
hooks.beforeEach(() => {
target = getFixture();
mock = mockTimeout();
tourRegistry = registry.category("web_tour.tours");
delete registry.subRegistries["web_tour.tours"];
let macroEngines = [];
patchWithCleanup(MacroEngine.prototype, {
start() {
super.start(...arguments);
macroEngines.push(this);
},
});
registerCleanup(() => {
macroEngines.forEach((e) => e.stop());
macroEngines = [];
});
registry
.category("services")
.add("rpc", rpcService)
.add("user", userService)
.add("orm", ormService)
.add("notification", notificationService)
.add("effect", effectService)
.add("tour_service", tourService);
patchWithCleanup(browser.console, {
// prevent form logging "tour successful" which would end the qunit suite test
log: () => {},
});
});
hooks.afterEach(() => {
registry.subRegistries["web_tour.tours"] = tourRegistry;
});
QUnit.test("Tours sequence", async function (assert) {
registry
.category("web_tour.tours")
.add("Tour 1", {
sequence: 10,
steps: () => [{ trigger: ".anchor" }],
})
.add("Tour 2", { steps: () => [{ trigger: ".anchor" }] })
.add("Tour 3", {
sequence: 5,
steps: () => [{ trigger: ".anchor", content: "Oui" }],
});
const env = await makeTestEnv({});
const sortedTours = env.services.tour_service.getSortedTours();
assert.strictEqual(sortedTours[0].name, "Tour 3");
});
QUnit.test("override existing tour by using saveAs", async function (assert) {
registry
.category("web_tour.tours")
.add("Tour 1", {
steps: () => [{ trigger: "#1" }],
saveAs: "homepage",
})
.add("Tour 2", {
steps: () => [{ trigger: "#2" }],
saveAs: "homepage",
});
const env = await makeTestEnv({});
const sortedTours = env.services.tour_service.getSortedTours();
assert.strictEqual(sortedTours.length, 1);
assert.deepEqual(sortedTours[0].steps, [{ shadow_dom: undefined, trigger: "#2" }]);
assert.deepEqual(sortedTours[0].name, "homepage");
});
QUnit.test("points to next step", async function (assert) {
registry.category("web_tour.tours").add("tour1", {
sequence: 10,
steps: () => [
{
trigger: "button.inc",
},
],
});
const env = await makeTestEnv({});
const { Component: OverlayContainer, props: overlayContainerProps } = registry
.category("main_components")
.get("OverlayContainer");
class Root extends Component {
static components = { OverlayContainer, Counter };
static template = xml/*html*/ `
<t>
<Counter />
<OverlayContainer t-props="props.overlayContainerProps" />
</t>
`;
}
await mount(Root, target, { env, props: { overlayContainerProps } });
env.services.tour_service.startTour("tour1", { mode: "manual" });
await mock.advanceTime(800);
assert.containsOnce(document.body, ".o_tour_pointer");
await click(target, "button.inc");
assert.containsNone(document.body, ".o_tour_pointer");
assert.strictEqual(target.querySelector("span.value").textContent, "1");
});
QUnit.test("next step with new anchor at same position", async (assert) => {
registry.category("web_tour.tours").add("tour1", {
sequence: 10,
steps: () => [{ trigger: "button.foo" }, { trigger: "button.bar" }],
});
const env = await makeTestEnv({});
const { Component: OverlayContainer, props: overlayContainerProps } = registry
.category("main_components")
.get("OverlayContainer");
class Dummy extends Component {
state = useState({ bool: true });
static template = xml/*html*/ `
<button class="foo w-100" t-if="state.bool" t-on-click="() => { state.bool = false; }">Foo</button>
<button class="bar w-100" t-if="!state.bool">Bar</button>
`;
}
class Root extends Component {
static components = { OverlayContainer, Dummy };
static template = xml/*html*/ `
<t>
<Dummy />
<OverlayContainer t-props="props.overlayContainerProps" />
</t>
`;
}
await mount(Root, target, { env, props: { overlayContainerProps } });
env.services.tour_service.startTour("tour1", { mode: "manual" });
await mock.advanceTime(100);
assert.containsOnce(document.body, ".o_tour_pointer");
// check position of the pointer relative to the foo button
let pointerRect = document.body.querySelector(".o_tour_pointer").getBoundingClientRect();
let buttonRect = document.body.querySelector("button.foo").getBoundingClientRect();
const leftValue1 = pointerRect.left - buttonRect.left;
const bottomValue1 = pointerRect.bottom - buttonRect.bottom;
assert.ok(leftValue1 !== 0);
assert.ok(bottomValue1 !== 0);
await click(target, "button.foo");
await mock.advanceTime(100);
assert.containsOnce(document.body, ".o_tour_pointer");
// check position of the pointer relative to the bar button
pointerRect = document.body.querySelector(".o_tour_pointer").getBoundingClientRect();
buttonRect = document.body.querySelector("button.bar").getBoundingClientRect();
const leftValue2 = pointerRect.left - buttonRect.left;
const bottomValue2 = pointerRect.bottom - buttonRect.bottom;
assert.strictEqual(bottomValue1, bottomValue2);
assert.strictEqual(leftValue1, leftValue2);
});
QUnit.test("scroller pointer to reach next step", async function (assert) {
patchWithCleanup(Element.prototype, {
scrollIntoView(options) {
super.scrollIntoView({ ...options, behavior: "instant" });
},
});
// The fixture should be shown for this test
target.style.position = "fixed";
target.style.top = "200px";
target.style.left = "50px";
registry.category("web_tour.tours").add("tour1", {
sequence: 10,
steps: () => [{ trigger: "button.inc", content: "Click to increment" }],
});
const env = await makeTestEnv({});
const { Component: OverlayContainer, props: overlayContainerProps } = registry
.category("main_components")
.get("OverlayContainer");
class Root extends Component {
static components = { OverlayContainer, Counter };
static template = xml/*html*/ `
<div class="scrollable-parent" style="overflow-y: scroll; height: 150px;">
<div class="top-filler" style="height: 300px" />
<Counter />
<OverlayContainer t-props="props.overlayContainerProps" />
<div class="bottom-filler" style="height: 300px" />
</div>
`;
}
await mount(Root, target, { env, props: { overlayContainerProps } });
env.services.tour_service.startTour("tour1", { mode: "manual" });
await mock.advanceTime(100); // awaits the macro engine
// Even if this seems weird, it should show the initial pointer.
// This is due to the fact the intersection observer has just been started and
// the pointer did not have the observations yet when the pointTo method was called.
// This is a bit tricky to change for now because the synchronism of the pointTo method
// is what permits to avoid multiple pointer to be shown at the same time
assert.containsOnce(document.body, ".o_tour_pointer");
assert.equal(
document.body.querySelector(".o_tour_pointer").textContent,
"Click to increment"
);
await mock.advanceTime(100); // awaits for the macro engine next check cycle
// now the scroller pointer should be shown
assert.containsOnce(document.body, ".o_tour_pointer");
assert.equal(
document.body.querySelector(".o_tour_pointer").textContent,
"Scroll down to reach the next step."
);
// awaiting the click here permits to the intersection observer to update
await click(document.body, ".o_tour_pointer");
assert.containsNone(document.body, ".o_tour_pointer");
await mock.advanceTime(100); // awaits for the macro engine next check cycle
assert.containsOnce(document.body, ".o_tour_pointer");
assert.equal(
document.body.querySelector(".o_tour_pointer").textContent,
"Click to increment"
);
document.querySelector(".scrollable-parent").scrollTop = 1000;
await nextTick(); // awaits the intersection observer to update after the scroll
await mock.advanceTime(100); // awaits for the macro engine next check cycle
assert.containsOnce(document.body, ".o_tour_pointer");
assert.equal(
document.body.querySelector(".o_tour_pointer").textContent,
"Scroll up to reach the next step."
);
// awaiting the click here permits to the intersection observer to update
await click(document.body, ".o_tour_pointer");
assert.containsNone(document.body, ".o_tour_pointer");
await mock.advanceTime(100); // awaits for the macro engine next check cycle
assert.containsOnce(document.body, ".o_tour_pointer");
assert.equal(
document.body.querySelector(".o_tour_pointer").textContent,
"Click to increment"
);
});
QUnit.test("scrolling to next step should update the pointer's height", async (assert) => {
patchWithCleanup(Element.prototype, {
scrollIntoView(options) {
super.scrollIntoView({ ...options, behavior: "instant" });
},
});
// The fixture should be shown for this test
target.style.position = "fixed";
target.style.top = "200px";
target.style.left = "50px";
const stepContent = "Click this pretty button to increment this magnificent counter !";
registry.category("web_tour.tours").add("tour1", {
sequence: 10,
steps: () => [
{
trigger: "button.inc",
content: stepContent,
},
],
});
const env = await makeTestEnv({});
const { Component: OverlayContainer, props: overlayContainerProps } = registry
.category("main_components")
.get("OverlayContainer");
class Root extends Component {
static components = { OverlayContainer, Counter };
static template = xml/*html*/ `
<div class="scrollable-parent" style="overflow-y: scroll; height: 150px;">
<Counter />
<div class="bottom-filler" style="height: 300px" />
</div>
<OverlayContainer t-props="props.overlayContainerProps" />
`;
}
await mount(Root, target, { env, props: { overlayContainerProps } });
env.services.tour_service.startTour("tour1", { mode: "manual" });
await mock.advanceTime(100); // awaits the macro engine
assert.containsOnce(document.body, ".o_tour_pointer");
assert.equal(document.body.querySelector(".o_tour_pointer").textContent, stepContent);
const pointer = document.body.querySelector(".o_tour_pointer");
assert.doesNotHaveClass(pointer, "o_open");
assert.strictEqual(pointer.style.height, "28px");
assert.strictEqual(pointer.style.width, "28px");
await triggerEvent(document.body, ".o_tour_pointer", "mouseenter");
await mock.advanceTime(100); // awaits for the macro engine next check cycle
assert.hasClass(pointer, "o_open");
const firstOpenHeight = pointer.style.height;
const firstOpenWidth = pointer.style.width;
await triggerEvent(document.body, ".o_tour_pointer", "mouseleave");
await mock.advanceTime(100); // awaits for the macro engine next check cycle
assert.doesNotHaveClass(pointer, "o_open");
document.querySelector(".scrollable-parent").scrollTop = 1000;
await nextTick(); // awaits the intersection observer to update after the scroll
await mock.advanceTime(100); // awaits for the macro engine next check cycle
// now the scroller pointer should be shown
assert.containsOnce(document.body, ".o_tour_pointer");
assert.equal(
document.body.querySelector(".o_tour_pointer").textContent,
"Scroll up to reach the next step."
);
document.querySelector(".scrollable-parent").scrollTop = 0;
await nextTick(); // awaits the intersection observer to update after the scroll
await mock.advanceTime(100); // awaits for the macro engine next check cycle
// now the true step pointer should be shown again
assert.containsOnce(document.body, ".o_tour_pointer");
assert.equal(document.body.querySelector(".o_tour_pointer").textContent, stepContent);
await triggerEvent(document.body, ".o_tour_pointer", "mouseenter");
await mock.advanceTime(100); // awaits for the macro engine next check cycle
assert.hasClass(pointer, "o_open");
const secondOpenHeight = pointer.style.height;
const secondOpenWidth = pointer.style.width;
assert.strictEqual(firstOpenHeight, secondOpenHeight);
assert.strictEqual(firstOpenWidth, secondOpenWidth);
});
QUnit.test("perform edit on next step", async function (assert) {
registry.category("web_tour.tours").add("tour1", {
sequence: 10,
steps: () => [
{
trigger: ".interval input",
},
{
trigger: "button.inc",
},
],
});
const env = await makeTestEnv({});
const { Component: OverlayContainer, props: overlayContainerProps } = registry
.category("main_components")
.get("OverlayContainer");
class Root extends Component {
static components = { OverlayContainer, Counter };
static template = xml/*html*/ `
<t>
<Counter />
<OverlayContainer t-props="props.overlayContainerProps" />
</t>
`;
}
await mount(Root, target, { env, props: { overlayContainerProps } });
env.services.tour_service.startTour("tour1", { mode: "manual" });
await mock.advanceTime(750);
assert.containsOnce(document.body, ".o_tour_pointer");
await editInput(target, ".interval input", "5");
assert.containsNone(document.body, ".o_tour_pointer");
await mock.advanceTime(750);
assert.containsOnce(document.body, ".o_tour_pointer");
await click(target, "button.inc");
assert.strictEqual(target.querySelector(".counter .value").textContent, "5");
});
QUnit.test("trigger an event when a step is consummed", async function (assert) {
registry.category("web_tour.tours").add("tour1", {
sequence: 10,
steps: () => [{ trigger: ".interval input" }],
});
const env = await makeTestEnv({});
const { Component: OverlayContainer, props: overlayContainerProps } = registry
.category("main_components")
.get("OverlayContainer");
class Root extends Component {
static components = { OverlayContainer, Counter };
static template = xml/*html*/ `
<t>
<Counter />
<OverlayContainer t-props="props.overlayContainerProps" />
</t>
`;
}
await mount(Root, target, { env, props: { overlayContainerProps } });
env.services.tour_service.startTour("tour1", { mode: "manual" });
env.services.tour_service.bus.addEventListener("STEP-CONSUMMED", ({ detail }) => {
assert.step(`Tour ${detail.tour.name}, step ${detail.step.trigger}`);
});
await mock.advanceTime(750);
await editInput(target, ".interval input", "5");
await mock.advanceTime(750);
assert.verifySteps(["Tour tour1, step .interval input"]);
});
QUnit.test("should show only 1 pointer at a time", async function (assert) {
const storage = new Map();
patchWithCleanup(browser.localStorage, {
getItem: (key) => storage.get(key),
setItem: (key, value) => storage.set(key, value),
removeItem: (key) => storage.delete(key),
tour__tour1__sequence: 0,
tour__tour2__sequence: 0,
});
registry.category("web_tour.tours").add("tour1", {
sequence: 10,
steps: () => [
{
trigger: ".interval input",
},
],
});
registry.category("web_tour.tours").add("tour2", {
sequence: 10,
steps: () => [
{
trigger: "button.inc",
},
],
});
const env = await makeTestEnv({});
const { Component: OverlayContainer, props: overlayContainerProps } = registry
.category("main_components")
.get("OverlayContainer");
class Root extends Component {
static components = { OverlayContainer, Counter };
static template = xml/*html*/ `
<t>
<Counter />
<OverlayContainer t-props="props.overlayContainerProps" />
</t>
`;
}
await mount(Root, target, { env, props: { overlayContainerProps } });
env.services.tour_service.startTour("tour1", { mode: "manual" });
env.services.tour_service.startTour("tour2", { mode: "manual" });
await mock.advanceTime(750);
assert.containsOnce(document.body, ".o_tour_pointer");
await editInput(target, ".interval input", "5");
assert.containsNone(document.body, ".o_tour_pointer");
await mock.advanceTime(750);
assert.containsOnce(document.body, ".o_tour_pointer");
});
QUnit.test("hovering to the anchor element should show the content", async function (assert) {
registry.category("web_tour.tours").add("tour1", {
sequence: 10,
steps: () => [
{
content: "content",
trigger: "button.inc",
},
],
});
const env = await makeTestEnv({});
const { Component: OverlayContainer, props: overlayContainerProps } = registry
.category("main_components")
.get("OverlayContainer");
class Root extends Component {
static components = { OverlayContainer, Counter };
static template = xml/*html*/ `
<t>
<Counter />
<OverlayContainer t-props="props.overlayContainerProps" />
</t>
`;
}
await mount(Root, target, { env, props: { overlayContainerProps } });
env.services.tour_service.startTour("tour1", { mode: "manual" });
await mock.advanceTime(750);
assert.containsOnce(target, ".o_tour_pointer");
triggerEvent(target, "button.inc", "mouseenter");
await nextTick();
assert.containsOnce(target, ".o_tour_pointer_content:not(.invisible)");
assert.strictEqual(
target.querySelector(".o_tour_pointer_content:not(.invisible)").textContent,
"content"
);
triggerEvent(target, "button.inc", "mouseleave");
await nextTick();
assert.containsOnce(target, ".o_tour_pointer_content.invisible");
});
QUnit.test(
"registering non-test tour after service is started auto-starts the tour",
async function (assert) {
patchWithCleanup(session, { tour_disable: false });
const env = await makeTestEnv({});
const { Component: OverlayContainer, props: overlayContainerProps } = registry
.category("main_components")
.get("OverlayContainer");
class Root extends Component {
static components = { OverlayContainer, Counter };
static template = xml/*html*/ `
<t>
<Counter />
<OverlayContainer t-props="props.overlayContainerProps" />
</t>
`;
}
await mount(Root, target, { env, props: { overlayContainerProps } });
assert.containsNone(target, ".o_tour_pointer");
registry.category("web_tour.tours").add("tour1", {
steps: () => [
{
content: "content",
trigger: "button.inc",
},
],
});
await mock.advanceTime(750);
await nextTick();
assert.containsOnce(target, ".o_tour_pointer");
}
);
QUnit.test(
"registering test tour after service is started doesn't auto-start the tour",
async function (assert) {
patchWithCleanup(session, { tour_disable: false });
const env = await makeTestEnv({});
const { Component: OverlayContainer, props: overlayContainerProps } = registry
.category("main_components")
.get("OverlayContainer");
class Root extends Component {
static components = { OverlayContainer, Counter };
static template = xml/*html*/ `
<t>
<Counter />
<OverlayContainer t-props="props.overlayContainerProps" />
</t>
`;
}
await mount(Root, target, { env, props: { overlayContainerProps } });
assert.containsNone(target, ".o_tour_pointer");
registry.category("web_tour.tours").add("tour1", {
test: true,
steps: () => [
{
content: "content",
trigger: "button.inc",
},
],
});
await mock.advanceTime(750);
await nextTick();
assert.containsNone(target, ".o_tour_pointer");
}
);
QUnit.test("a failing tour logs the step that failed", async function (assert) {
patchWithCleanup(browser.console, {
log: (s) => assert.step(`log: ${s}`),
warn: (s) => assert.step(`warn: ${s}`),
error: (s) => assert.step(`error: ${s}`),
});
const env = await makeTestEnv({});
const { Component: OverlayContainer, props: overlayContainerProps } = registry
.category("main_components")
.get("OverlayContainer");
class Root extends Component {
static components = { OverlayContainer };
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>
<OverlayContainer t-props="props.overlayContainerProps" />
</t>
`;
}
await mount(Root, target, { env, props: { overlayContainerProps } });
registry.category("web_tour.tours").add("tour1", {
test: true,
steps: () => [
{
content: "content",
trigger: ".button0",
},
{
content: "content",
trigger: ".button1",
},
{
content: "content",
trigger: ".button2",
},
{
content: "content",
trigger: ".button3",
},
{
content: "content",
trigger: ".wrong_selector",
},
{
content: "content",
trigger: ".button4",
},
{
content: "content",
trigger: ".button5",
},
{
content: "content",
trigger: ".button6",
},
{
content: "content",
trigger: ".button7",
},
],
});
env.services.tour_service.startTour("tour1", { mode: "auto" });
await mock.advanceTime(750);
assert.verifySteps(["log: Tour tour1 on step: 'content (trigger: .button0)'"]);
await mock.advanceTime(750);
assert.verifySteps(["log: Tour tour1 on step: 'content (trigger: .button1)'"]);
await mock.advanceTime(750);
assert.verifySteps(["log: Tour tour1 on step: 'content (trigger: .button2)'"]);
await mock.advanceTime(750);
assert.verifySteps(["log: Tour tour1 on step: 'content (trigger: .button3)'"]);
await mock.advanceTime(750);
assert.verifySteps(["log: Tour tour1 on step: 'content (trigger: .wrong_selector)'"]);
await mock.advanceTime(10000);
const expectedWarning = `warn: Tour tour1 failed at step content (trigger: .wrong_selector)
{
"content": "content",
"trigger": ".button1"
},
{
"content": "content",
"trigger": ".button2"
},
{
"content": "content",
"trigger": ".button3"
},
----- FAILING STEP -----
{
"content": "content",
"trigger": ".wrong_selector"
},
-----------------------
{
"content": "content",
"trigger": ".button4"
},
{
"content": "content",
"trigger": ".button5"
},
{
"content": "content",
"trigger": ".button6"
},`;
const expectedError = "error: Tour tour1 failed at step content (trigger: .wrong_selector)";
assert.verifySteps([expectedWarning, expectedError]);
});
});