[IMP] web: use original router service in tests

Before this commit, the router service used in tests was almost
entirely mocked and some of the mocked features didn't correspond to
their original counterparts.

Now, the actual router service is used in tests and the browser object
is mocked instead, allowing to correctly test the routing features.
This commit is contained in:
Julien Mougenot
2021-06-18 21:31:28 +02:00
committed by Aaron Bohy
parent 36e03d1b98
commit d2985c033e
13 changed files with 419 additions and 258 deletions
@@ -25,6 +25,7 @@ export const browser = Object.assign({}, owl.browser, {
removeEventListener: window.removeEventListener.bind(window),
console: window.console,
location: window.location,
history: window.history,
navigator: navigator,
open: window.open.bind(window),
XMLHttpRequest: window.XMLHttpRequest,
@@ -5,6 +5,11 @@ import { shallowEqual } from "../utils/objects";
import { objectToUrlEncodedString } from "../utils/urls";
import { browser } from "./browser";
/**
* @typedef {{ [key: string]: string }} Query
* @typedef {{ [key: string]: any }} Route
*/
function parseString(str) {
const parts = str.split("&");
const result = {};
@@ -16,139 +21,161 @@ function parseString(str) {
return result;
}
/**
* For each push request (replaceState or pushState), filterout keys that have been locked before
* overrides locked keys that are explicitly re-locked or unlocked
* registers keys in "hash" in "lockedKeys" according to the "lock" Boolean
*
* @param {Set<string>} lockedKeys A set containing all keys that were locked
* @param {Query} hash An Object representing the pushed url hash
* @param {Query} currentHash The current hash compare against
* @param {Object} [options={}] Whether to lock all hash keys in "hash" to prevent them from being changed afterwards
* @param {Boolean} [options.lock] Whether to lock all hash keys in "hash" to prevent them from being changed afterwards
* @return {Query} The resulting "hash" where previous locking has been applied
*/
function applyLocking(lockedKeys, hash, currentHash, options = {}) {
const newHash = {};
for (const key in hash) {
if ("lock" in options) {
options.lock ? lockedKeys.add(key) : lockedKeys.delete(key);
} else if (lockedKeys.has(key)) {
// forbid implicit override of key
continue;
}
newHash[key] = hash[key];
}
for (const key in currentHash) {
if (lockedKeys.has(key) && !(key in newHash)) {
newHash[key] = currentHash[key];
}
}
return newHash;
}
function computeNewRoute(hash, replace, currentRoute) {
if (!replace) {
hash = Object.assign({}, currentRoute.hash, hash);
}
hash = sanitizeHash(hash);
if (!shallowEqual(currentRoute.hash, hash)) {
return Object.assign({}, currentRoute, { hash });
}
return false;
}
function sanitizeHash(hash) {
return Object.fromEntries(Object.entries(hash).filter(([, v]) => v !== undefined));
}
/**
* @param {string} hash
* @returns {any}
*/
export function parseHash(hash) {
return hash === "#" || hash === "" ? {} : parseString(hash.slice(1));
return hash && hash !== "#" ? parseString(hash.slice(1)) : {};
}
/**
* @param {string} search
* @returns {any}
*/
export function parseSearchQuery(search) {
return search === "" ? {} : parseString(search.slice(1));
return search ? parseString(search.slice(1)) : {};
}
/**
* @param {{ [key: string]: any }} route
* @returns
*/
export function routeToUrl(route) {
const search = objectToUrlEncodedString(route.search);
const hash = objectToUrlEncodedString(route.hash);
return route.pathname + (search ? "?" + search : "") + (hash ? "#" + hash : "");
}
export function redirect(env, url, wait) {
const load = () => browser.location.assign(url);
async function redirect(env, url, wait = false) {
if (wait) {
const wait_server = function () {
env.services
.rpc("/web/webclient/version_info", {})
.then(load)
.catch(() => browser.setTimeout(wait_server, 250));
};
browser.setTimeout(wait_server, 1000);
} else {
load();
await new Promise((resolve) => {
const waitForServer = (delay) => {
browser.setTimeout(async () => {
env.services
.rpc("/web/webclient/version_info", {})
.then(resolve)
.catch(() => waitForServer(250));
}, delay);
};
waitForServer(1000);
});
}
browser.location.assign(url);
}
function getRoute() {
const { pathname, search, hash } = window.location;
const { pathname, search, hash } = browser.location;
const searchQuery = parseSearchQuery(search);
const hashQuery = parseHash(hash);
return { pathname, search: searchQuery, hash: hashQuery };
}
/**
* @param {function} getCurrent function that returns the current route
* @returns {function} function to compute the next hash
*/
export function makePreProcessQuery(getCurrent) {
const lockedKeys = new Set();
return (hash) => {
const newHash = {};
Object.keys(hash).forEach((key) => {
if (lockedKeys.has(key)) {
return;
}
const k = key.split(" ");
let value;
if (k.length === 2) {
value = hash[key];
key = k[1];
if (k[0] === "lock") {
lockedKeys.add(key);
} else if (k[0] === "unlock") {
lockedKeys.delete(key);
} else {
return;
}
}
newHash[key] = value || hash[key];
});
const current = getCurrent();
Object.keys(current.hash).forEach((key) => {
if (lockedKeys.has(key) && !(key in newHash)) {
newHash[key] = current.hash[key];
}
});
return newHash;
};
}
function makeRouter(env) {
let bus = env.bus;
const bus = env.bus;
const lockedKeys = new Set();
let current = getRoute();
let pushTimeout;
browser.addEventListener("hashchange", () => {
current = getRoute();
bus.trigger("ROUTE_CHANGE");
});
function doPush(mode = "push", route) {
if (!shallowEqual(route.hash, current.hash)) {
const url = location.origin + routeToUrl(route);
if (mode === "push") {
window.history.pushState({}, url, url);
} else {
window.history.replaceState({}, url, url);
/**
* @param {string} mode
* @returns {(hash: string, options: any) => any}
*/
function makeDebouncedPush(mode) {
let allPushArgs = [];
function doPush() {
// Aggregates push/replace state arguments
const replace = allPushArgs.some(([, options]) => options && options.replace);
const newHash = allPushArgs.reduce((finalHash, [hash, options]) => {
hash = applyLocking(lockedKeys, hash, current.hash, options);
if (finalHash) {
hash = applyLocking(lockedKeys, hash, finalHash, options);
}
return Object.assign(finalHash || {}, hash);
}, null);
// Calculates new route based on aggregated hash and options
const newRoute = computeNewRoute(newHash, replace, current);
if (!newRoute) {
return;
}
// If the route changed: pushes or replaces browser state
const url = browser.location.origin + routeToUrl(newRoute);
if (mode === "push") {
browser.history.pushState({}, "", url);
} else {
browser.history.replaceState({}, "", url);
}
current = getRoute();
}
current = getRoute();
return function pushOrReplaceState(hash, options) {
allPushArgs.push([hash, options]);
browser.clearTimeout(pushTimeout);
pushTimeout = browser.setTimeout(() => {
doPush();
pushTimeout = null;
allPushArgs = [];
});
};
}
function getCurrent() {
return current;
}
const preProcessQuery = makePreProcessQuery(getCurrent);
return {
get current() {
return getCurrent();
return current;
},
pushState: makePushState(getCurrent, doPush.bind(null, "push"), preProcessQuery),
replaceState: makePushState(getCurrent, doPush.bind(null, "replace"), preProcessQuery),
pushState: makeDebouncedPush("push"),
replaceState: makeDebouncedPush("replace"),
redirect: (url, wait) => redirect(env, url, wait),
};
}
export function makePushState(getCurrent, doPush, preProcessQuery) {
let _replace = false;
let timeoutId;
let tempHash;
return (hash, replace = false) => {
clearTimeout(timeoutId);
hash = preProcessQuery(hash);
_replace = _replace || replace;
tempHash = Object.assign(tempHash || {}, hash);
timeoutId = setTimeout(() => {
tempHash = sanitizeHash(tempHash);
const current = getCurrent();
if (!_replace) {
tempHash = Object.assign({}, current.hash, tempHash);
}
const route = Object.assign({}, current, { hash: tempHash });
doPush(route);
tempHash = undefined;
timeoutId = undefined;
_replace = false;
});
cancelPushes: () => browser.clearTimeout(pushTimeout),
};
}
@@ -161,7 +188,7 @@ export const routerService = {
export function objectToQuery(obj) {
const query = {};
Object.entries(obj).forEach(([k, v]) => {
query[k] = v ? `${v}` : v;
query[k] = v ? String(v) : v;
});
return query;
}
+2 -2
View File
@@ -91,7 +91,7 @@ export const userService = {
};
const stringCIds = allowedCompanies.join(",");
router.replaceState({ "lock cids": stringCIds });
router.replaceState({ cids: stringCIds }, { lock: true });
cookie.setCookie("cids", stringCIds);
const setCompanies = makeSetCompanies(() => allowedCompanies);
@@ -126,7 +126,7 @@ export const userService = {
showEffect,
setCompanies: (mode, companyId) => {
const nextCompanyIds = setCompanies(mode, companyId).join(",");
router.pushState({ "lock cids": nextCompanyIds });
router.pushState({ cids: nextCompanyIds }, { lock: true });
cookie.setCookie("cids", nextCompanyIds);
browser.setTimeout(() => window.location.reload()); // history.pushState is a little async
},
@@ -1162,7 +1162,7 @@ function makeActionManager(env) {
const newState = {};
const action = controller.action;
if (action.id) {
newState.action = `${action.id}`;
newState.action = action.id;
} else if (action.type === "ir.actions.client") {
newState.action = action.tag;
}
@@ -1182,7 +1182,7 @@ function makeActionManager(env) {
newState.view_type = props.type;
newState.id = props.recordId ? `${props.recordId}` : undefined;
}
env.services.router.pushState(newState, true);
env.services.router.pushState(newState, { replace: true });
}
return {
doAction,
@@ -62,9 +62,7 @@ function makeMenus(env, menusData, fetchLoadMenus) {
env.bus.trigger("MENUS:APP-CHANGED");
// FIXME: lock API: maybe do something like
// pushState({menu_id: ...}, { lock: true}); ?
env.services.router.pushState({
"lock menu_id": `${menu.id}`,
});
env.services.router.pushState({ menu_id: menu.id }, { lock: true });
}
},
async reload() {
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="web.UserMenu.ShortCutsDialog">
<t t-name="web.UserMenu.ShortCutsDialog" owl="1">
<Dialog title="props.title">
<t t-call="web.UserMenu.shortcutsTable"/>
</Dialog>
@@ -1,15 +1,17 @@
/** @odoo-module **/
import { browser } from "@web/core/browser/browser";
import {
parseHash,
parseSearchQuery,
redirect,
routeToUrl,
} from "@web/core/browser/router_service";
import { patch, unpatch } from "@web/core/utils/patch";
import { parseHash, parseSearchQuery, routeToUrl } from "@web/core/browser/router_service";
import { makeTestEnv } from "../helpers/mock_env";
import { nextTick } from "../helpers/utils";
import { makeFakeRouterService } from "../helpers/mock_services";
import { nextTick, patchWithCleanup } from "../helpers/utils";
async function createRouter(params = {}) {
const env = params.env || {};
env.bus = env.bus || new owl.core.EventBus();
const router = await makeFakeRouterService(params).start(env);
return router;
}
QUnit.module("Router");
@@ -76,47 +78,179 @@ QUnit.test("can parse URI encoded strings", (assert) => {
QUnit.test("routeToUrl encodes URI compatible strings", (assert) => {
const route = { pathname: "/asf", search: {}, hash: {} };
assert.strictEqual(routeToUrl(route), "/asf");
route.search = { a: "11", g: "summer wine" };
assert.strictEqual(routeToUrl(route), "/asf?a=11&g=summer%20wine");
route.hash = { b: "2", c: "", e: "kloug,gloubi" };
assert.strictEqual(routeToUrl(route), "/asf?a=11&g=summer%20wine#b=2&c&e=kloug%2Cgloubi");
});
QUnit.test("can redirect an URL", async (assert) => {
patch(browser, "test.location", {
location: {
assign(url) {
assert.step(url);
},
},
setTimeout: (handler, delay) => {
patchWithCleanup(browser, {
setTimeout(handler, delay) {
handler();
assert.step(String(delay));
assert.step(`timeout: ${delay}`);
},
});
let firstCheckServer = true;
const env = await makeTestEnv({
browser,
mockRPC(...args) {
if (args[0] === "/web/webclient/version_info") {
async mockRPC(route) {
if (route === "/web/webclient/version_info") {
if (firstCheckServer) {
firstCheckServer = false;
return;
} else {
return true;
}
return Promise.resolve(true);
}
},
});
redirect(env, "/my/test/url");
assert.verifySteps(["/my/test/url"]);
redirect(env, "/my/test/url/2", true);
const onRedirect = (url) => assert.step(url);
const router = await createRouter({ env, onRedirect });
router.redirect("/my/test/url");
await nextTick();
assert.verifySteps(["1000", "250", "/my/test/url/2"]);
unpatch(browser, "test.location");
assert.verifySteps(["/my/test/url"]);
router.redirect("/my/test/url/2", true);
await nextTick();
assert.verifySteps(["timeout: 1000", "timeout: 250", "/my/test/url/2"]);
});
// TODO: test
// - push/replaceState requests occuring in the same tick are aggregated
// - we don't push twice the same url (different key orders)
// - undefined keys are not pushed in the url
// - lock stuff
QUnit.module("Router: Push state");
QUnit.test("can push in same timeout", async (assert) => {
const router = await createRouter();
assert.deepEqual(router.current.hash, {});
router.pushState({ k1: 2 });
assert.deepEqual(router.current.hash, {});
router.pushState({ k1: 3 });
assert.deepEqual(router.current.hash, {});
await nextTick();
assert.deepEqual(router.current.hash, { k1: 3 });
});
QUnit.test("can lock keys", async (assert) => {
const router = await createRouter();
router.pushState({ k1: 2 }, { lock: true });
await nextTick();
assert.deepEqual(router.current.hash, { k1: 2 });
router.pushState({ k1: 3 });
await nextTick();
assert.deepEqual(router.current.hash, { k1: 2 });
router.pushState({ k1: 4 }, { lock: true });
await nextTick();
assert.deepEqual(router.current.hash, { k1: 4 });
router.pushState({ k1: 3 });
await nextTick();
assert.deepEqual(router.current.hash, { k1: 4 });
});
QUnit.test("can re-lock keys in same final call", async (assert) => {
const router = await createRouter();
router.pushState({ k1: 2 }, { lock: true });
await nextTick();
router.pushState({ k1: 1 }, { lock: true });
router.pushState({ k1: 4 });
await nextTick();
assert.deepEqual(router.current.hash, { k1: 1 });
});
QUnit.test("can unlock keys", async (assert) => {
const router = await createRouter();
router.pushState({ k1: 2 }, { lock: true });
await nextTick();
assert.deepEqual(router.current.hash, { k1: 2 });
router.pushState({ k1: 3 });
await nextTick();
assert.deepEqual(router.current.hash, { k1: 2 });
router.pushState({ k1: 4 }, { lock: false });
await nextTick();
assert.deepEqual(router.current.hash, { k1: 4 });
router.pushState({ k1: 3 });
await nextTick();
assert.deepEqual(router.current.hash, { k1: 3 });
});
QUnit.test("can replace hash", async (assert) => {
const router = await createRouter();
router.pushState({ k1: 2 });
await nextTick();
assert.deepEqual(router.current.hash, { k1: 2 });
router.pushState({ k2: 3 }, { replace: true });
await nextTick();
assert.deepEqual(router.current.hash, { k2: 3 });
});
QUnit.test("can replace hash with locked keys", async (assert) => {
const router = await createRouter();
router.pushState({ k1: 2 }, { lock: true });
await nextTick();
assert.deepEqual(router.current.hash, { k1: 2 });
router.pushState({ k2: 3 }, { replace: true });
await nextTick();
assert.deepEqual(router.current.hash, { k1: 2, k2: 3 });
});
QUnit.test("can merge hash", async (assert) => {
const router = await createRouter();
router.pushState({ k1: 2 });
await nextTick();
assert.deepEqual(router.current.hash, { k1: 2 });
router.pushState({ k2: 3 });
await nextTick();
assert.deepEqual(router.current.hash, { k1: 2, k2: 3 });
});
QUnit.test("undefined keys are not pushed", async (assert) => {
const onPushState = () => assert.step("pushed state");
const router = await createRouter({ onPushState });
router.pushState({ k1: undefined });
await nextTick();
assert.verifySteps([]);
assert.deepEqual(router.current.hash, {});
});
QUnit.test("undefined keys destroy previous non locked keys", async (assert) => {
const router = await createRouter();
router.pushState({ k1: 1 });
await nextTick();
assert.deepEqual(router.current.hash, { k1: 1 });
router.pushState({ k1: undefined });
await nextTick();
assert.deepEqual(router.current.hash, {});
});
QUnit.test("do not re-push when hash is same", async (assert) => {
const onPushState = () => assert.step("pushed state");
const router = await createRouter({ onPushState });
router.pushState({ k1: 1, k2: 2 });
await nextTick();
assert.verifySteps(["pushed state"]);
router.pushState({ k2: 2, k1: 1 });
await nextTick();
assert.verifySteps([]);
});
@@ -1,12 +1,15 @@
/** @odoo-module **/
import { makePreProcessQuery, makePushState, routeToUrl } from "@web/core/browser/router_service";
import { browser } from "@web/core/browser/browser";
import { routerService } from "@web/core/browser/router_service";
import { localization } from "@web/core/l10n/localization";
import { translatedTerms } from "@web/core/l10n/translation";
import { rpcService } from "@web/core/network/rpc_service";
import { SIZES } from "@web/core/ui_service";
import { computeAllowedCompanyIds, makeSetCompanies } from "@web/core/user_service";
import { effectService } from "@web/webclient/effects/effect_service";
import { objectToUrlEncodedString } from "../../src/core/utils/urls";
import { registerCleanup } from "./cleanup";
import { patchWithCleanup } from "./utils";
const { Component } = owl;
@@ -27,7 +30,7 @@ export const defaultLocalization = {
weekStart: 7,
};
export function makeFakeLocalizationService(config) {
export function makeFakeLocalizationService() {
patchWithCleanup(localization, defaultLocalization);
return {
@@ -49,7 +52,7 @@ export function makeFakeUserService(values) {
const { uid, name, username, is_admin, user_companies, partner_id, user_context } = sessionInfo;
return {
name: "user",
start(env) {
start() {
let allowedCompanies = computeAllowedCompanyIds();
const setCompanies = makeSetCompanies(() => allowedCompanies);
const context = {
@@ -170,64 +173,70 @@ export function makeMockFetch(mockRPC) {
};
}
function stripUndefinedQueryKey(query) {
const keyValArray = Array.from(Object.entries(query)).filter(([k, v]) => v !== undefined);
// transform to Object.fromEntries in es > 2019
const newObj = {};
keyValArray.forEach(([k, v]) => {
newObj[k] = v;
export function makeMockLocation() {
const locationLink = Object.assign(document.createElement("a"), {
href: window.location.origin + window.location.pathname,
assign(url) {
this.href = url;
},
});
return new Proxy(locationLink, {
get(target, p) {
return target[p];
},
set(target, p, value) {
target[p] = value;
if (p === "hash") {
window.dispatchEvent(new HashChangeEvent("hashchange"));
}
return true;
},
});
return newObj;
}
function getRoute(route) {
route.hash = stripUndefinedQueryKey(route.hash);
route.search = stripUndefinedQueryKey(route.search);
return route;
}
export function makeFakeRouterService(params) {
let _current = {
pathname: "test.wowl",
search: {},
hash: {},
};
if (params && params.initialRoute) {
Object.assign(_current, params.initialRoute);
}
let current = getRoute(_current);
return {
start(env) {
function loadState(hash) {
current.hash = hash;
env.bus.trigger("ROUTE_CHANGE");
}
env.bus.on("test:hashchange", null, loadState);
function getCurrent() {
return current;
}
function doPush(mode = "push", route) {
const oldUrl = routeToUrl(current);
const newRoute = getRoute(route);
const newUrl = routeToUrl(newRoute);
if (params && params.onPushState && oldUrl !== newUrl) {
params.onPushState(mode, route.hash);
/**
* @param {Object} [params={}]
* @param {Object} [params.initialRoute] initial route object
* @param {Object} [params.onPushState] hook on the "pushState" method
* @param {Object} [params.onRedirect] hook on the "redirect" method
* @returns {RouterService}
*/
export function makeFakeRouterService(params = {}) {
const mockLocation = makeMockLocation();
Object.assign(mockLocation, params.initialRoute);
patchWithCleanup(browser, {
location: mockLocation,
history: {
pushState(state, title, url) {
mockLocation.assign(url);
if (params.onPushState) {
params.onPushState(url);
}
current = newRoute;
}
const preProcessQuery = makePreProcessQuery(getCurrent);
return {
get current() {
return getCurrent();
},
replaceState(state, title, url) {
mockLocation.assign(url);
if (params.onReplaceState) {
params.onReplaceState(url);
}
},
},
});
return {
start({ bus }) {
const router = routerService.start(...arguments);
bus.on("test:hashchange", null, (hash) => {
mockLocation.hash = objectToUrlEncodedString(hash);
});
registerCleanup(router.cancelPushes);
patchWithCleanup(router, {
async redirect() {
await this._super(...arguments);
if (params.onRedirect) {
params.onRedirect(...arguments);
}
},
pushState: makePushState(getCurrent, doPush.bind(null, "push"), preProcessQuery),
replaceState: makePushState(
getCurrent,
doPush.bind(null, "replace"),
preProcessQuery
),
redirect: (params && params.redirect) || (() => {}),
};
});
return router;
},
};
}
@@ -275,7 +284,7 @@ export const fakeCookieService = {
get current() {
return cookie;
},
setCookie(key, value, ttl) {
setCookie(key, value) {
if (value !== undefined) {
cookie[key] = value;
}
@@ -1,12 +1,11 @@
/** @odoo-module **/
import { browser } from "@web/core/browser/browser";
import { patch, unpatch } from "@web/core/utils/patch";
import * as LegacyRegistry from "web.Registry";
import core from "web.core";
import AbstractAction from "web.AbstractAction";
import core from "web.core";
import * as LegacyRegistry from "web.Registry";
import { registerCleanup } from "../../helpers/cleanup";
import { nextTick } from "../../helpers/utils";
import { nextTick, patchWithCleanup } from "../../helpers/utils";
import { createWebClient, doAction, getActionManagerTestConfig } from "../../webclient/actions/helpers";
let testConfig;
@@ -26,11 +25,14 @@ QUnit.module("Service Provider Adapter Notification", (hooks) => {
assert.expect(8);
let notifId;
let timeoutCB;
patch(browser, "mock.settimeout.cb", {
setTimeout: (cb, t) => {
timeoutCB = cb;
assert.step("time: " + t);
return 1;
patchWithCleanup(browser, {
setTimeout: (cb, delay) => {
if (!delay) {
return; // Timeouts from router service
}
timeoutCB = cb;
assert.step("time: " + delay);
return 1;
},
});
const NotifyAction = AbstractAction.extend({
@@ -75,7 +77,6 @@ QUnit.module("Service Provider Adapter Notification", (hooks) => {
await nextTick();
assert.containsNone(document.body, ".o_notification");
assert.verifySteps(["time: 3000"]);
unpatch(browser, "mock.settimeout.cb");
}
);
});
@@ -58,14 +58,14 @@ QUnit.module("ActionManager", (hooks) => {
);
assert.strictEqual(webClient.el.querySelector(".o_menu_brand").textContent, "App2");
assert.deepEqual(webClient.env.services.router.current.hash, {
action: "1001",
menu_id: "2",
action: 1001,
menu_id: 2,
});
});
QUnit.test("initial loading with action id", async (assert) => {
assert.expect(4);
const hash = { action: 1001 };
const hash = "#action=1001";
serviceRegistry.add("router", makeFakeRouterService({ initialRoute: { hash } }), {
force: true,
});
@@ -82,7 +82,7 @@ QUnit.module("ActionManager", (hooks) => {
QUnit.test("initial loading with action tag", async (assert) => {
assert.expect(3);
const hash = { action: "__test__client__action__" };
const hash = "#action=__test__client__action__";
serviceRegistry.add("router", makeFakeRouterService({ initialRoute: { hash } }), {
force: true,
});
@@ -112,11 +112,7 @@ QUnit.module("ActionManager", (hooks) => {
QUnit.test("correctly sends additional context", async (assert) => {
assert.expect(1);
const hash = {
action: 1001,
active_id: 4,
active_ids: "4,8",
};
const hash = "#action=1001&active_id=4&active_ids=4,8";
serviceRegistry.add("router", makeFakeRouterService({ initialRoute: { hash } }), {
force: true,
});
@@ -248,7 +244,7 @@ QUnit.module("ActionManager", (hooks) => {
1: { id: 1, children: [], name: "App1", appID: 1, actionID: 1001, xmlid: "menu_1" },
2: { id: 2, children: [], name: "App2", appID: 2, actionID: 1002, xmlid: "menu_2" },
};
const hash = { id: 2, model: "partner" };
const hash = "#id=2&model=partner";
serviceRegistry.add("router", makeFakeRouterService({ initialRoute: { hash } }), {
force: true,
});
@@ -533,7 +529,7 @@ QUnit.module("ActionManager", (hooks) => {
await loadState(webClient, { action: 3 });
currentHash = webClient.env.services.router.current.hash;
assert.deepEqual(currentHash, {
action: "3",
action: 3,
model: "partner",
view_type: "list",
});
@@ -542,8 +538,8 @@ QUnit.module("ActionManager", (hooks) => {
await legacyExtraNextTick();
currentHash = webClient.env.services.router.current.hash;
assert.deepEqual(currentHash, {
action: "3",
id: "1",
action: 3,
id: 1,
model: "partner",
view_type: "form",
});
@@ -585,7 +581,7 @@ QUnit.module("ActionManager", (hooks) => {
await loadState(webClient, { action: 9 });
currentHash = webClient.env.services.router.current.hash;
assert.deepEqual(currentHash, {
action: "9",
action: 9,
});
assert.verifySteps([], "should not push the loaded state");
await testUtils.dom.click($(webClient.el).find("#client_action_button"));
@@ -593,14 +589,14 @@ QUnit.module("ActionManager", (hooks) => {
assert.verifySteps(["push_state"], "should push the state of it changes afterwards");
currentHash = webClient.env.services.router.current.hash;
assert.deepEqual(currentHash, {
action: "9",
action: 9,
someValue: "X",
});
delete core.action_registry.map.ClientAction;
});
QUnit.test("change a param of an ir.actions.client in the url", async function (assert) {
assert.expect(13);
assert.expect(12);
const ClientAction = AbstractAction.extend({
hasControlPanel: true,
init: function (parent, action) {
@@ -622,8 +618,8 @@ QUnit.module("ActionManager", (hooks) => {
serviceRegistry.add(
"router",
makeFakeRouterService({
onPushState(mode) {
assert.step(`push_state ${mode}`);
onPushState() {
assert.step("push_state");
},
}),
{ force: true }
@@ -634,10 +630,10 @@ QUnit.module("ActionManager", (hooks) => {
assert.deepEqual(currentHash, {});
// execute the client action
await doAction(webClient, 9);
assert.verifySteps(["start", "push_state push"]);
assert.verifySteps(["start", "push_state"]);
currentHash = webClient.env.services.router.current.hash;
assert.deepEqual(currentHash, {
action: "9",
action: 9,
a: "default value",
});
assert.strictEqual(
@@ -656,10 +652,10 @@ QUnit.module("ActionManager", (hooks) => {
action: 9,
a: "new value",
});
assert.verifySteps(["start", "push_state push"]);
assert.verifySteps(["start"]); // No push state since the hash hasn't changed
currentHash = webClient.env.services.router.current.hash;
assert.deepEqual(currentHash, {
action: "9",
action: 9,
a: "new value",
});
assert.strictEqual(
@@ -783,15 +779,10 @@ QUnit.module("ActionManager", (hooks) => {
root: { id: "root", children: [1], name: "root", appID: "root" },
1: { id: 1, children: [], name: "App1", appID: 1, actionID: 1 },
};
serviceRegistry.add(
"router",
makeFakeRouterService({
initialRoute: {
hash: { home: 1 },
},
}),
{ force: true }
);
const hash = "#home=1";
serviceRegistry.add("router", makeFakeRouterService({ initialRoute: { hash } }), {
force: true,
});
const mockRPC = async function (route) {
assert.step(route);
};
@@ -1,10 +1,10 @@
/** @odoo-module **/
import { registry } from "@web/core/registry";
import testUtils from "web.test_utils";
import core from "web.core";
import Widget from "web.Widget";
import AbstractAction from "web.AbstractAction";
import core from "web.core";
import testUtils from "web.test_utils";
import Widget from "web.Widget";
import { makeTestEnv } from "../../helpers/mock_env";
import { legacyExtraNextTick } from "../../helpers/utils";
import { createWebClient, doAction, getActionManagerTestConfig } from "./helpers";
@@ -75,7 +75,7 @@ QUnit.module("ActionManager", (hooks) => {
QUnit.test("action in handler registry", async (assert) => {
assert.expect(2);
const env = await makeTestEnv(testConfig);
actionHandlersRegistry.add("ir.action_in_handler_registry", ({ env, action, options }) =>
actionHandlersRegistry.add("ir.action_in_handler_registry", ({ action }) =>
assert.step(action.type)
);
await doAction(env, {
@@ -145,7 +145,7 @@ QUnit.module("ActionManager", (hooks) => {
QUnit.test("action cache: additionalContext is respected", async function (assert) {
assert.expect(5);
const mockRPC = async (route, args) => {
const mockRPC = async (route) => {
if (route === "/web/action/load") {
assert.step("server loaded");
}
@@ -326,7 +326,7 @@ QUnit.module("ActionManager", (hooks) => {
this._super.apply(this, arguments);
},
});
const mockRPC = async function (route, args) {
const mockRPC = async function (route) {
if (route === "/web/dataset/search_read") {
await Promise.resolve(def);
}
@@ -378,7 +378,7 @@ QUnit.module("ActionManager", (hooks) => {
action: "Partners Action 4",
});
currentHash = webClient.env.services.router.current.hash;
assert.deepEqual(currentHash, { action: "4", model: "partner", view_type: "kanban" });
assert.deepEqual(currentHash, { action: 4, model: "partner", view_type: "kanban" });
await doAction(webClient, 8);
currentTitle = webClient.env.services.title.getParts();
assert.deepEqual(currentTitle, {
@@ -386,7 +386,7 @@ QUnit.module("ActionManager", (hooks) => {
action: "Favorite Ponies",
});
currentHash = webClient.env.services.router.current.hash;
assert.deepEqual(currentHash, { action: "8", model: "pony", view_type: "list" });
assert.deepEqual(currentHash, { action: 8, model: "pony", view_type: "list" });
await testUtils.dom.click($(webClient.el).find("tr.o_data_row:first"));
await legacyExtraNextTick();
currentTitle = webClient.env.services.title.getParts();
@@ -395,7 +395,7 @@ QUnit.module("ActionManager", (hooks) => {
action: "Twilight Sparkle",
});
currentHash = webClient.env.services.router.current.hash;
assert.deepEqual(currentHash, { action: "8", id: "4", model: "pony", view_type: "form" });
assert.deepEqual(currentHash, { action: 8, id: 4, model: "pony", view_type: "form" });
});
QUnit.test(
@@ -30,8 +30,8 @@ QUnit.module("ActionManager", (hooks) => {
await nextTick();
await nextTick();
urlState = webClient.env.services.router.current;
assert.strictEqual(urlState.hash.action, "1002");
assert.strictEqual(urlState.hash.menu_id, "2");
assert.strictEqual(urlState.hash.action, 1002);
assert.strictEqual(urlState.hash.menu_id, 2);
assert.strictEqual(
webClient.el.querySelector(".test_client_action").textContent.trim(),
"ClientAction_Id 2"
@@ -49,8 +49,8 @@ QUnit.module("ActionManager", (hooks) => {
await nextTick();
await nextTick();
urlState = webClient.env.services.router.current;
assert.strictEqual(urlState.hash.action, "1002");
assert.strictEqual(urlState.hash.menu_id, "2");
assert.strictEqual(urlState.hash.action, 1002);
assert.strictEqual(urlState.hash.menu_id, 2);
assert.strictEqual(
webClient.el.querySelector(".test_client_action").textContent.trim(),
"ClientAction_Id 2"
@@ -58,8 +58,8 @@ QUnit.module("ActionManager", (hooks) => {
assert.strictEqual(webClient.el.querySelector(".o_menu_brand").textContent, "App2");
await doAction(webClient, 1001, { clearBreadcrumbs: true });
urlState = webClient.env.services.router.current;
assert.strictEqual(urlState.hash.action, "1001");
assert.strictEqual(urlState.hash.menu_id, "2");
assert.strictEqual(urlState.hash.action, 1001);
assert.strictEqual(urlState.hash.menu_id, 2);
assert.strictEqual(
webClient.el.querySelector(".test_client_action").textContent.trim(),
"ClientAction_Id 1"
@@ -122,7 +122,7 @@ QUnit.module("ActionManager", (hooks) => {
assert.strictEqual(urlState.hash.arbitrary, "actionPushed");
await doAction(webClient, 1001);
urlState = webClient.env.services.router.current;
assert.strictEqual(urlState.hash.action, "1001");
assert.strictEqual(urlState.hash.action, 1001);
assert.strictEqual(urlState.hash.arbitrary, undefined);
});
@@ -152,8 +152,8 @@ QUnit.module("ActionManager", (hooks) => {
await nextTick();
await nextTick();
urlState = webClient.env.services.router.current;
assert.strictEqual(urlState.hash.action, "1002");
assert.strictEqual(urlState.hash.menu_id, "2");
assert.strictEqual(urlState.hash.action, 1002);
assert.strictEqual(urlState.hash.menu_id, 2);
});
QUnit.test("action in target new do not push state", async (assert) => {
@@ -162,7 +162,7 @@ QUnit.module("ActionManager", (hooks) => {
serviceRegistry.add(
"router",
makeFakeRouterService({
onPushState(mode, newState) {
onPushState() {
throw new Error("should not push state");
},
}),
@@ -178,30 +178,30 @@ QUnit.module("ActionManager", (hooks) => {
const webClient = await createWebClient({ testConfig });
await doAction(webClient, 4);
assert.deepEqual(webClient.env.services.router.current.hash, {
action: "4",
action: 4,
model: "partner",
view_type: "kanban",
});
await doAction(webClient, 8);
assert.deepEqual(webClient.env.services.router.current.hash, {
action: "8",
action: 8,
model: "pony",
view_type: "list",
});
await testUtils.dom.click($(webClient.el).find("tr.o_data_row:first"));
await legacyExtraNextTick();
assert.deepEqual(webClient.env.services.router.current.hash, {
action: "8",
action: 8,
model: "pony",
view_type: "form",
id: "4",
id: 4,
});
});
QUnit.test("push state after action is loaded, not before", async function (assert) {
assert.expect(2);
const def = testUtils.makeTestPromise();
const mockRPC = async function (route, args) {
const mockRPC = async function (route) {
if (route === "/web/dataset/search_read") {
await def;
}
@@ -215,7 +215,7 @@ QUnit.module("ActionManager", (hooks) => {
await testUtils.nextTick();
await legacyExtraNextTick();
assert.deepEqual(webClient.env.services.router.current.hash, {
action: "4",
action: 4,
model: "partner",
view_type: "kanban",
});
@@ -231,7 +231,7 @@ QUnit.module("ActionManager", (hooks) => {
const webClient = await createWebClient({ testConfig, mockRPC });
await doAction(webClient, 8);
assert.deepEqual(webClient.env.services.router.current.hash, {
action: "8",
action: 8,
model: "pony",
view_type: "list",
});
@@ -240,7 +240,7 @@ QUnit.module("ActionManager", (hooks) => {
// we make sure here that the list view is still in the dom
assert.containsOnce(webClient, ".o_list_view", "there should still be a list view in dom");
assert.deepEqual(webClient.env.services.router.current.hash, {
action: "8",
action: 8,
model: "pony",
view_type: "list",
});
@@ -19,7 +19,7 @@ QUnit.module("ActionManager", (hooks) => {
serviceRegistry.add(
"router",
makeFakeRouterService({
redirect: (url) => {
onRedirect(url) {
assert.step(url);
},
}),