Odoo18-Base/addons/web/static/tests/core/router.test.js
2025-01-06 10:57:38 +07:00

1931 lines
68 KiB
JavaScript

import { describe, expect, getFixture, test } from "@odoo/hoot";
import { click, on } from "@odoo/hoot-dom";
import { tick } from "@odoo/hoot-mock";
import { patchWithCleanup } from "@web/../tests/web_test_helpers";
import { browser } from "@web/core/browser/browser";
import {
parseHash,
parseSearchQuery,
router,
routerBus,
startRouter,
stateToUrl,
urlToState,
} from "@web/core/browser/router";
import { redirect } from "@web/core/utils/urls";
const _urlToState = (url) => urlToState(new URL(url));
function createRouter(params = {}) {
if (params.onPushState) {
patchWithCleanup(browser.history, {
pushState() {
super.pushState(...arguments);
params.onPushState(...arguments);
},
});
}
if (params.onReplaceState) {
patchWithCleanup(browser.history, {
replaceState() {
super.replaceState(...arguments);
params.onReplaceState(...arguments);
},
});
}
startRouter();
}
describe.current.tags("headless");
describe("parseHash", () => {
test("can parse an empty hash", () => {
expect(parseHash("")).toEqual({});
});
test("can parse an single hash", () => {
expect(parseHash("#")).toEqual({});
});
test("can parse a hash with a single key/value pair", () => {
expect(parseHash("#action=114")).toEqual({ action: 114 });
});
test("can parse a hash with 2 key/value pairs", () => {
expect(parseHash("#action=114&active_id=mail.box_inbox")).toEqual({
action: 114,
active_id: "mail.box_inbox",
});
});
test("a missing value is encoded as an empty string", () => {
expect(parseHash("#action")).toEqual({ action: "" });
});
test("a missing value is encoded as an empty string -- 2", () => {
expect(parseHash("#action=")).toEqual({ action: "" });
});
test("can parse a realistic hash", () => {
expect(parseHash("#action=114&active_id=mail.box_inbox&menu_id=91")).toEqual({
action: 114,
active_id: "mail.box_inbox",
menu_id: 91,
});
});
test("can parse URI encoded strings", () => {
expect(parseHash("#comma=that%2Cis")).toEqual({ comma: "that,is" });
});
});
describe("parseSearchQuery", () => {
test("can parse an empty search", () => {
expect(parseSearchQuery("")).toEqual({});
});
test("can parse an simple search with no value", () => {
expect(parseSearchQuery("?a")).toEqual({ a: "" });
});
test("can parse an simple search with a value", () => {
expect(parseSearchQuery("?a=1")).toEqual({ a: 1 });
});
test("can parse an search with 2 key/value pairs", () => {
expect(parseSearchQuery("?a=1&b=2")).toEqual({ a: 1, b: 2 });
});
test("can parse URI encoded strings", () => {
expect(parseHash("#comma=that%2Cis")).toEqual({ comma: "that,is" });
});
});
describe("stateToUrl", () => {
test("encodes URI compatible strings", (assert) => {
expect(stateToUrl({})).toBe("/odoo");
expect(stateToUrl({ a: "11", b: "summer wine" })).toBe("/odoo?a=11&b=summer%20wine");
expect(stateToUrl({ b: "2", c: "", e: "kloug,gloubi" })).toBe(
"/odoo?b=2&c=&e=kloug%2Cgloubi"
);
});
test("backwards compatibility: no action stack, action encoded in path", (assert) => {
expect(stateToUrl({})).toBe("/odoo");
// action
expect(stateToUrl({ action: "some-path" })).toBe("/odoo/some-path");
expect(stateToUrl({ active_id: 5, action: "some-path" })).toBe("/odoo/5/some-path");
expect(stateToUrl({ active_id: "some-active_id", action: "some-path" })).toBe(
"/odoo/some-path?active_id=some-active_id",
{ message: "only numeric active_id are encoded in path" }
);
expect(stateToUrl({ action: "some-path", resId: 2 })).toBe("/odoo/some-path/2");
expect(stateToUrl({ action: "some-path", resId: "some-resId" })).toBe(
"/odoo/some-path?resId=some-resId",
{ message: "only numeric resId are encoded in path" }
);
expect(stateToUrl({ active_id: 5, action: "some-path", resId: 2 })).toBe(
"/odoo/5/some-path/2"
);
expect(stateToUrl({ active_id: 5, action: "some-path", resId: "new" })).toBe(
"/odoo/5/some-path/new"
);
expect(stateToUrl({ action: 1, resId: 2 })).toBe("/odoo/action-1/2", {
message: "action id instead of path/tag",
});
expect(stateToUrl({ action: "module.xml_id", resId: 2 })).toBe(
"/odoo/action-module.xml_id/2",
{ message: "action xml_id instead of path/tag" }
);
// model
expect(stateToUrl({ model: "some.model" })).toBe("/odoo/some.model");
expect(stateToUrl({ model: "some.model", resId: 2 })).toBe("/odoo/some.model/2");
expect(stateToUrl({ active_id: 5, model: "some.model" })).toBe("/odoo/5/some.model");
expect(stateToUrl({ active_id: 5, model: "some.model", resId: 2 })).toBe(
"/odoo/5/some.model/2"
);
expect(stateToUrl({ active_id: 5, model: "some.model", resId: "new" })).toBe(
"/odoo/5/some.model/new"
);
expect(stateToUrl({ active_id: 5, model: "some.model", view_type: "some_viewtype" })).toBe(
"/odoo/5/some.model?view_type=some_viewtype"
);
// edge cases
expect(
stateToUrl({ active_id: 5, action: "some-path", resId: 2, some_key: "some_value" })
).toBe("/odoo/5/some-path/2?some_key=some_value", {
message: "pieces of state unrelated to actions are added as query string",
});
expect(
stateToUrl({ active_id: 5, action: "some-path", model: "some.model", resId: 2 })
).toBe("/odoo/5/some-path/2", { message: "action has priority on model" });
expect(stateToUrl({ active_id: 5, model: "some.model", resId: 2, view_type: "list" })).toBe(
"/odoo/5/some.model/2?view_type=list",
{ message: "view_type and resId aren't incompatible" }
// Should they be? view_type will just be stripped by action_service
);
});
test("actionStack: one action", () => {
expect(stateToUrl({ actionStack: [] })).toBe("/odoo");
// action
expect(stateToUrl({ actionStack: [{ action: "some-path" }] })).toBe("/odoo/some-path");
expect(stateToUrl({ actionStack: [{ active_id: 5, action: "some-path" }] })).toBe(
"/odoo/5/some-path"
);
expect(stateToUrl({ actionStack: [{ action: "some-path", resId: 2 }] })).toBe(
"/odoo/some-path/2"
);
expect(stateToUrl({ actionStack: [{ active_id: 5, action: "some-path", resId: 2 }] })).toBe(
"/odoo/5/some-path/2"
);
expect(
stateToUrl({ actionStack: [{ active_id: 5, action: "some-path", resId: "new" }] })
).toBe("/odoo/5/some-path/new");
expect(stateToUrl({ actionStack: [{ action: 1, resId: 2 }] })).toBe("/odoo/action-1/2", {
message: "numerical action id instead of path",
});
expect(stateToUrl({ actionStack: [{ action: "module.xml_id", resId: 2 }] })).toBe(
"/odoo/action-module.xml_id/2",
{ message: "action xml_id instead of path" }
);
// model
expect(stateToUrl({ actionStack: [{ model: "some.model" }] })).toBe("/odoo/some.model");
expect(stateToUrl({ actionStack: [{ model: "some.model", resId: 2 }] })).toBe(
"/odoo/some.model/2"
);
expect(stateToUrl({ actionStack: [{ active_id: 5, model: "some.model" }] })).toBe(
"/odoo/5/some.model"
);
expect(stateToUrl({ actionStack: [{ active_id: 5, model: "some.model", resId: 2 }] })).toBe(
"/odoo/5/some.model/2"
);
expect(
stateToUrl({ actionStack: [{ active_id: 5, model: "some.model", resId: "new" }] })
).toBe("/odoo/5/some.model/new");
expect(
stateToUrl({
actionStack: [{ active_id: 5, model: "some.model", view_type: "some_viewtype" }],
})
).toBe("/odoo/5/some.model", { message: "view_type is ignored in the action stack" });
expect(
stateToUrl({
actionStack: [{ active_id: 5, model: "some.model" }],
view_type: "some_viewtype",
})
).toBe("/odoo/5/some.model?view_type=some_viewtype", {
message: "view_type is added if it's on the state itself",
});
expect(
stateToUrl({ actionStack: [{ active_id: 5, model: "model_no_dot", resId: 2 }] })
).toBe("/odoo/5/m-model_no_dot/2");
// edge cases
expect(
stateToUrl({
actionStack: [
{ active_id: 5, action: "some-path", resId: 2, some_key: "some_value" },
],
})
).toBe("/odoo/5/some-path/2", {
message: "pieces of state unrelated to actions are ignored in the actionStack",
});
expect(
stateToUrl({
actionStack: [{ active_id: 5, action: "some-path", resId: 2 }],
some_key: "some_value",
})
).toBe("/odoo/5/some-path/2?some_key=some_value", {
message:
"pieces of state unrelated to actions are added as query string even with actionStack",
});
expect(
stateToUrl({
actionStack: [{ active_id: 5, action: "some-path", model: "some.model", resId: 2 }],
})
).toBe("/odoo/5/some-path/2", { message: "action has priority on model" });
expect(
stateToUrl({
actionStack: [{ active_id: 5, model: "some.model", resId: 2 }],
view_type: "list",
})
).toBe(
"/odoo/5/some.model/2?view_type=list",
{ message: "view_type and resId aren't incompatible" }
// Should they be? view_type will just be stripped by action_service
);
});
test("actionStack: multiple actions", () => {
// different actions
expect(
stateToUrl({ actionStack: [{ action: "some-path" }, { action: "other-path" }] })
).toBe("/odoo/some-path/other-path");
expect(
stateToUrl({
actionStack: [{ active_id: 5, action: "some-path" }, { action: "other-path" }],
})
).toBe("/odoo/5/some-path/other-path");
expect(
stateToUrl({
actionStack: [{ action: "some-path" }, { active_id: 7, action: "other-path" }],
})
).toBe(
// On reload, this will generate a form view for the first action even though there was
// originally none. This is probably fine.
"/odoo/some-path/7/other-path"
);
expect(
stateToUrl({
actionStack: [{ action: "some-path", resId: 2 }, { action: "other-path" }],
})
).toBe(
// On reload, the second action will have an active_id even though it originally didn't
// have one. This might be a problem?
"/odoo/some-path/2/other-path"
);
expect(
stateToUrl({
actionStack: [{ action: "some-path" }, { action: "other-path", resId: 2 }],
})
).toBe(
// On reload, this will generate an action in the default multi-record view for the second
// action. This is the desired behaviour.
"/odoo/some-path/other-path/2"
);
expect(
stateToUrl({
actionStack: [
{ active_id: 5, action: "some-path", resId: 2 },
{ action: "other-path" },
],
})
).toBe("/odoo/5/some-path/2/other-path");
expect(
stateToUrl({
actionStack: [
{ action: "some-path" },
{ active_id: 5, action: "other-path", resId: 2 },
],
})
).toBe("/odoo/some-path/5/other-path/2");
expect(
stateToUrl({
actionStack: [
{ action: "some-path" },
{ active_id: 5, action: "other-path", resId: "new" },
],
})
).toBe("/odoo/some-path/5/other-path/new");
expect(
stateToUrl({
actionStack: [
{ action: "some-path", resId: 5 },
{ active_id: 5, action: "other-path" },
],
})
).toBe("/odoo/some-path/5/other-path", {
message:
"action with resId followed by action with same value as active_id is not duplicated",
});
expect(
stateToUrl({
actionStack: [
{ action: "some-path", resId: 5 },
{ active_id: 5, action: "other-path", resId: 2 },
],
})
).toBe("/odoo/some-path/5/other-path/2");
expect(
stateToUrl({
actionStack: [{ action: 1 }, { active_id: 5, action: 6, resId: 2 }],
})
).toBe("/odoo/action-1/5/action-6/2", { message: "numerical actions" });
expect(
stateToUrl({
actionStack: [
{ action: "module.xml_id" },
{ active_id: 5, action: "module.other_xml_id", resId: 2 },
],
})
).toBe("/odoo/action-module.xml_id/5/action-module.other_xml_id/2", {
message: "actions as xml_ids",
});
// same action twice
expect(
stateToUrl({ actionStack: [{ action: "some-path" }, { action: "some-path" }] })
).toBe("/odoo/some-path", {
message: "consolidates identical actions into one path segment",
});
expect(
stateToUrl({
actionStack: [{ active_id: 5, action: "some-path" }, { action: "some-path" }],
})
).toBe("/odoo/5/some-path/some-path", {
message: "doesn't consolidate the same action with different active_id",
});
expect(
stateToUrl({
actionStack: [
{ action: "some-path", resId: 7 },
{ active_id: 7, action: "some-path" },
],
})
).toBe("/odoo/some-path/7/some-path", {
message:
"doesn't remove multirecord action if it follows the same action in mono-record mode",
});
expect(
stateToUrl({
actionStack: [{ action: "some-path", resId: 2 }, { action: "some-path" }],
})
).toBe("/odoo/some-path/2/some-path");
expect(
stateToUrl({
actionStack: [
{ active_id: 7, action: "some-path", resId: 7 },
{ active_id: 7, action: "some-path" },
],
})
).toBe("/odoo/7/some-path/7/some-path", {
message:
"doesn't remove multirecord action if it follows the same action in mono-record mode even if the active_id are the same",
});
expect(
stateToUrl({
actionStack: [{ action: "some-path" }, { action: "some-path", resId: 2 }],
})
).toBe("/odoo/some-path/2", {
message: "consolidates multi-record action with mono-record action",
});
expect(
stateToUrl({
actionStack: [
{ active_id: 5, action: "some-path" },
{ active_id: 5, action: "some-path", resId: 2 },
],
})
).toBe("/odoo/5/some-path/2", {
message:
"consolidates multi-record action with mono-record action if they have the same active_id",
});
expect(
stateToUrl({
actionStack: [
{ active_id: 5, action: "some-path", resId: 2 },
{ action: "some-path" },
],
})
).toBe("/odoo/5/some-path/2/some-path");
expect(
stateToUrl({
actionStack: [
{ action: "some-path" },
{ active_id: 5, action: "some-path", resId: 2 },
],
})
).toBe("/odoo/some-path/5/some-path/2", {
message:
"doesn't consolidate mono-record action into preceding multi-record action if active_id is not the same",
});
expect(
stateToUrl({
actionStack: [
{ action: "some-path" },
{ active_id: 5, action: "some-path", resId: "new" },
],
})
).toBe("/odoo/some-path/5/some-path/new");
expect(
stateToUrl({
actionStack: [
{ action: "some-path", resId: 5 },
{ active_id: 5, action: "some-path" },
],
})
).toBe("/odoo/some-path/5/some-path", {
message:
"action with resId followed by action with same value as active_id is not duplicated",
});
expect(
stateToUrl({
actionStack: [
{ action: "some-path", resId: 5 },
{ active_id: 5, action: "some-path", resId: 2 },
],
})
).toBe("/odoo/some-path/5/some-path/2", {
message: "doesn't consolidate two mono-record actions",
});
expect(
stateToUrl({
actionStack: [
{ active_id: 5, action: "some-path", resId: 5 },
{ active_id: 5, action: "some-path", resId: 2 },
],
})
).toBe("/odoo/5/some-path/5/some-path/2", {
message: "doesn't consolidate two mono-record actions even with same active_id",
});
expect(
stateToUrl({
actionStack: [{ action: 1 }, { active_id: 5, action: 1, resId: 2 }],
})
).toBe("/odoo/action-1/5/action-1/2", { message: "numerical actions" });
expect(
stateToUrl({
actionStack: [
{ action: "module.xml_id" },
{ active_id: 5, action: "module.xml_id", resId: 2 },
],
})
).toBe("/odoo/action-module.xml_id/5/action-module.xml_id/2", {
message: "actions as xml_ids",
});
// model
expect(
stateToUrl({ actionStack: [{ model: "some.model" }, { model: "other.model" }] })
).toBe("/odoo/some.model/other.model");
expect(
stateToUrl({
actionStack: [{ active_id: 5, model: "some.model" }, { model: "other.model" }],
})
).toBe("/odoo/5/some.model/other.model");
expect(
stateToUrl({
actionStack: [{ model: "some.model" }, { active_id: 7, model: "other.model" }],
})
).toBe("/odoo/some.model/7/other.model");
expect(
stateToUrl({
actionStack: [{ model: "some.model", resId: 2 }, { model: "other.model" }],
})
).toBe("/odoo/some.model/2/other.model");
expect(
stateToUrl({
actionStack: [{ model: "some.model" }, { model: "other.model", resId: 2 }],
})
).toBe("/odoo/some.model/other.model/2");
expect(
stateToUrl({
actionStack: [
{ active_id: 5, model: "some.model", resId: 2 },
{ model: "other.model" },
],
})
).toBe("/odoo/5/some.model/2/other.model");
expect(
stateToUrl({
actionStack: [
{ model: "some.model" },
{ active_id: 5, model: "other.model", resId: 2 },
],
})
).toBe("/odoo/some.model/5/other.model/2");
expect(
stateToUrl({
actionStack: [
{ model: "some.model" },
{ active_id: 5, model: "other.model", resId: "new" },
],
})
).toBe("/odoo/some.model/5/other.model/new");
expect(
stateToUrl({
actionStack: [
{ model: "some.model", resId: 5 },
{ active_id: 5, model: "other.model" },
],
})
).toBe("/odoo/some.model/5/other.model", {
message:
"action with resId followed by action with same value as active_id is not duplicated",
});
expect(
stateToUrl({
actionStack: [
{ model: "some.model", resId: 5 },
{ active_id: 5, model: "other.model", resId: 2 },
],
})
).toBe("/odoo/some.model/5/other.model/2");
expect(
stateToUrl({
actionStack: [
{ model: "model_no_dot", resId: 5 },
{ active_id: 5, model: "no_dot_model", resId: 2 },
],
})
).toBe("/odoo/m-model_no_dot/5/m-no_dot_model/2");
// action + model
expect(
stateToUrl({ actionStack: [{ action: "some-path" }, { model: "some.model" }] })
).toBe("/odoo/some-path/some.model");
expect(
stateToUrl({
actionStack: [{ active_id: 5, action: "some-path" }, { model: "some.model" }],
})
).toBe("/odoo/5/some-path/some.model");
expect(
stateToUrl({
actionStack: [{ action: "some-path" }, { active_id: 7, model: "some.model" }],
})
).toBe("/odoo/some-path/7/some.model");
expect(
stateToUrl({
actionStack: [{ action: "some-path", resId: 2 }, { model: "some.model" }],
})
).toBe("/odoo/some-path/2/some.model");
expect(
stateToUrl({
actionStack: [{ action: "some-path" }, { model: "some.model", resId: 2 }],
})
).toBe("/odoo/some-path/some.model/2");
expect(
stateToUrl({
actionStack: [
{ active_id: 5, action: "some-path", resId: 2 },
{ model: "some.model" },
],
})
).toBe("/odoo/5/some-path/2/some.model");
expect(
stateToUrl({
actionStack: [
{ action: "some-path" },
{ active_id: 5, model: "some.model", resId: 2 },
],
})
).toBe("/odoo/some-path/5/some.model/2");
expect(
stateToUrl({
actionStack: [
{ action: "some-path" },
{ active_id: 5, model: "some.model", resId: "new" },
],
})
).toBe("/odoo/some-path/5/some.model/new");
expect(
stateToUrl({
actionStack: [
{ action: "some-path", resId: 5 },
{ active_id: 5, model: "some.model" },
],
})
).toBe("/odoo/some-path/5/some.model", {
message:
"action with resId followed by action with same value as active_id is not duplicated",
});
expect(
stateToUrl({
actionStack: [
{ action: "some-path", resId: 5 },
{ active_id: 5, model: "some.model", resId: 2 },
],
})
).toBe("/odoo/some-path/5/some.model/2");
expect(
stateToUrl({
actionStack: [
{ action: 1, resId: 5 },
{ active_id: 5, model: "model_no_dot", resId: 2 },
],
})
).toBe("/odoo/action-1/5/m-model_no_dot/2");
expect(
stateToUrl({
actionStack: [
{ action: "module.xml_id", resId: 5 },
{ active_id: 5, model: "model_no_dot", resId: 2 },
],
})
).toBe("/odoo/action-module.xml_id/5/m-model_no_dot/2");
// model + action
expect(
stateToUrl({ actionStack: [{ model: "some.model" }, { action: "other-path" }] })
).toBe("/odoo/some.model/other-path");
expect(
stateToUrl({
actionStack: [{ active_id: 5, model: "some.model" }, { action: "other-path" }],
})
).toBe("/odoo/5/some.model/other-path");
expect(
stateToUrl({
actionStack: [{ model: "some.model" }, { active_id: 7, action: "other-path" }],
})
).toBe("/odoo/some.model/7/other-path");
expect(
stateToUrl({
actionStack: [{ model: "some.model", resId: 2 }, { action: "other-path" }],
})
).toBe("/odoo/some.model/2/other-path");
expect(
stateToUrl({
actionStack: [{ model: "some.model" }, { action: "other-path", resId: 2 }],
})
).toBe("/odoo/some.model/other-path/2");
expect(
stateToUrl({
actionStack: [
{ active_id: 5, model: "some.model", resId: 2 },
{ action: "other-path" },
],
})
).toBe("/odoo/5/some.model/2/other-path");
expect(
stateToUrl({
actionStack: [
{ model: "some.model" },
{ active_id: 5, action: "other-path", resId: 2 },
],
})
).toBe("/odoo/some.model/5/other-path/2");
expect(
stateToUrl({
actionStack: [
{ model: "some.model" },
{ active_id: 5, action: "other-path", resId: "new" },
],
})
).toBe("/odoo/some.model/5/other-path/new");
expect(
stateToUrl({
actionStack: [
{ model: "some.model", resId: 5 },
{ active_id: 5, action: "other-path" },
],
})
).toBe("/odoo/some.model/5/other-path", {
message:
"action with resId followed by action with same value as active_id is not duplicated",
});
expect(
stateToUrl({
actionStack: [
{ model: "some.model", resId: 5 },
{ active_id: 5, action: "other-path", resId: 2 },
],
})
).toBe("/odoo/some.model/5/other-path/2");
expect(
stateToUrl({
actionStack: [
{ model: "model_no_dot", resId: 5 },
{ active_id: 5, action: 1, resId: 2 },
],
})
).toBe("/odoo/m-model_no_dot/5/action-1/2");
expect(
stateToUrl({
actionStack: [
{ model: "model_no_dot", resId: 5 },
{ active_id: 5, action: "module.xml_id", resId: 2 },
],
})
).toBe("/odoo/m-model_no_dot/5/action-module.xml_id/2");
// edge cases
expect(
stateToUrl({
actionStack: [
{ action: "some-path", resId: 5, some_key: "some_value" },
{ active_id: 5, action: "other-path", resId: 2, other_key: "other_value" },
],
})
).toBe("/odoo/some-path/5/other-path/2", {
message: "pieces of state unrelated to actions are ignored in the actionStack",
});
expect(
stateToUrl({
actionStack: [
{ action: "some-path", resId: 5 },
{ active_id: 5, action: "other-path", resId: 2 },
],
some_key: "some_value",
})
).toBe("/odoo/some-path/5/other-path/2?some_key=some_value", {
message:
"pieces of state unrelated to actions are added as query string even with actionStack",
});
expect(
stateToUrl({
actionStack: [
{ action: "some-path", model: "some.model", resId: 5 },
{ active_id: 5, action: "other-path", model: "other.model", resId: 2 },
],
})
).toBe("/odoo/some-path/5/other-path/2", { message: "action has priority on model" });
expect(
stateToUrl({
actionStack: [
{ action: "some-path", resId: 5 },
{ active_id: 5, model: "some.model", resId: 2 },
],
view_type: "list",
})
).toBe("/odoo/some-path/5/some.model/2?view_type=list", {
message: "view_type and resId aren't incompatible",
});
expect(
stateToUrl({
actionStack: [
{ action: "some-path", resId: 2 },
{ active_id: 5, action: "other-path" },
],
})
).toBe("/odoo/some-path/2/5/other-path", {
message:
"action with resId followed by action with different active_id gets both ids in a row",
});
expect(
stateToUrl({
actionStack: [
{ model: "some.model", resId: 2 },
{ active_id: 5, model: "other.model" },
],
})
).toBe("/odoo/some.model/2/5/other.model", {
message:
"action with resId followed by action with different active_id gets both ids in a row",
});
expect(
stateToUrl({
actionStack: [
{ action: "other-path", resId: 5 },
{ active_id: 5, action: "some-path" },
{ active_id: 5, action: "some-path", resId: 2 },
],
})
).toBe("/odoo/other-path/5/some-path/2", {
message:
"active_id of last action is correctly removed even if previous action's active id is also removed because of the preceding resId",
});
});
});
describe("urlToState", () => {
test("deserialize queryString", () => {
expect(_urlToState("/odoo?a=11&g=summer%20wine")).toEqual({
a: 11,
g: "summer wine",
});
expect(_urlToState("/odoo?g=2&c=&e=kloug%2Cgloubi")).toEqual({
g: 2,
c: "",
e: "kloug,gloubi",
});
});
test("deserialize action in legacy url form", () => {
expect(
_urlToState("/web#id=5&action=1&model=some.model&view_type=form&menu_id=137&cids=1")
).toEqual({
action: 1,
resId: 5,
cids: 1,
menu_id: 137,
model: "some.model",
actionStack: [
{
action: 1,
},
{
action: 1,
resId: 5,
},
],
});
expect(_urlToState("/web#id=5&model=some.model&view_type=form&menu_id=137&cids=1")).toEqual(
{
resId: 5,
cids: 1,
menu_id: 137,
model: "some.model",
actionStack: [
{
resId: 5,
model: "some.model",
},
],
},
{ message: "no action" }
);
});
test("deserialize single action", () => {
expect(_urlToState("")).toEqual({});
expect(_urlToState("/odoo")).toEqual({});
// action
expect(_urlToState("/odoo/some-path")).toEqual({
action: "some-path",
actionStack: [{ action: "some-path" }],
});
expect(_urlToState("/odoo/5/some-path")).toEqual({
active_id: 5,
action: "some-path",
actionStack: [{ active_id: 5, action: "some-path" }],
});
expect(_urlToState("/odoo/some-path/2")).toEqual(
{
action: "some-path",
resId: 2,
actionStack: [{ action: "some-path" }, { action: "some-path", resId: 2 }],
},
{ message: "two actions are created for action with resId" }
);
expect(_urlToState("/odoo/some-path/new")).toEqual(
{
action: "some-path",
resId: "new",
actionStack: [{ action: "some-path" }, { action: "some-path", resId: "new" }],
},
{ message: "new record" }
);
expect(_urlToState("/odoo/5/some-path/2")).toEqual({
active_id: 5,
action: "some-path",
resId: 2,
actionStack: [
{
active_id: 5,
action: "some-path",
},
{
active_id: 5,
action: "some-path",
resId: 2,
},
],
});
expect(_urlToState("/odoo/action-1/2")).toEqual({
action: 1,
resId: 2,
actionStack: [{ action: 1 }, { action: 1, resId: 2 }],
});
expect(_urlToState("/odoo/action-module.xml_id/2")).toEqual({
action: "module.xml_id",
resId: 2,
actionStack: [{ action: "module.xml_id" }, { action: "module.xml_id", resId: 2 }],
});
// model
expect(_urlToState("/odoo/some.model")).toEqual({
model: "some.model",
actionStack: [{ model: "some.model" }],
});
expect(_urlToState("/odoo/some.model/2")).toEqual(
{
model: "some.model",
resId: 2,
actionStack: [{ model: "some.model", resId: 2 }],
},
{ message: "single action is created for model with resId" }
);
expect(_urlToState("/odoo/some.model/new")).toEqual(
{
model: "some.model",
resId: "new",
actionStack: [{ model: "some.model", resId: "new" }],
},
{ message: "new record" }
);
expect(_urlToState("/odoo/5/some.model")).toEqual({
active_id: 5,
model: "some.model",
actionStack: [{ active_id: 5, model: "some.model" }],
});
expect(_urlToState("/odoo/5/some.model/2")).toEqual({
active_id: 5,
model: "some.model",
resId: 2,
actionStack: [{ active_id: 5, model: "some.model", resId: 2 }],
});
expect(_urlToState("/odoo/5/some.model?view_type=some_viewtype")).toEqual(
{
active_id: 5,
model: "some.model",
view_type: "some_viewtype",
actionStack: [{ active_id: 5, model: "some.model" }],
},
{ message: "view_type doesn't end up in the actionStack" }
);
expect(_urlToState("/odoo/m-model_no_dot/2")).toEqual({
model: "model_no_dot",
resId: 2,
actionStack: [{ model: "model_no_dot", resId: 2 }],
});
// edge cases
expect(_urlToState("/odoo/5/some-path/2?some_key=some_value")).toEqual(
{
active_id: 5,
action: "some-path",
resId: 2,
some_key: "some_value",
actionStack: [
{ active_id: 5, action: "some-path" },
{ active_id: 5, action: "some-path", resId: 2 },
],
},
{
message:
"pieces of state unrelated to actions end up on the state but not in the actionStack",
}
);
expect(_urlToState("/odoo/5/some.model/2?view_type=list")).toEqual(
{
active_id: 5,
model: "some.model",
resId: 2,
view_type: "list",
actionStack: [{ active_id: 5, model: "some.model", resId: 2 }],
},
{ message: "view_type and resId aren't incompatible" }
);
});
test("deserialize multiple actions", () => {
// action
expect(_urlToState("/odoo/some-path/other-path")).toEqual({
action: "other-path",
actionStack: [{ action: "some-path" }, { action: "other-path" }],
});
expect(_urlToState("/odoo/5/some-path/other-path")).toEqual({
action: "other-path",
actionStack: [{ active_id: 5, action: "some-path" }, { action: "other-path" }],
});
expect(_urlToState("/odoo/some-path/2/other-path")).toEqual({
action: "other-path",
active_id: 2,
actionStack: [
{ action: "some-path" },
{ action: "some-path", resId: 2 },
{ active_id: 2, action: "other-path" },
],
});
expect(_urlToState("/odoo/some-path/other-path/2")).toEqual({
action: "other-path",
resId: 2,
actionStack: [
{ action: "some-path" },
{ action: "other-path" },
{ action: "other-path", resId: 2 },
],
});
expect(_urlToState("/odoo/5/some-path/2/other-path")).toEqual({
action: "other-path",
active_id: 2,
actionStack: [
{ active_id: 5, action: "some-path" },
{ active_id: 5, action: "some-path", resId: 2 },
{ active_id: 2, action: "other-path" },
],
});
expect(_urlToState("/odoo/some-path/5/other-path/2")).toEqual({
active_id: 5,
action: "other-path",
resId: 2,
actionStack: [
{ action: "some-path" },
{ action: "some-path", resId: 5 },
{ active_id: 5, action: "other-path" },
{ active_id: 5, action: "other-path", resId: 2 },
],
});
expect(_urlToState("/odoo/some-path/5/other-path/new")).toEqual({
active_id: 5,
action: "other-path",
resId: "new",
actionStack: [
{ action: "some-path" },
{ action: "some-path", resId: 5 },
{ active_id: 5, action: "other-path" },
{ active_id: 5, action: "other-path", resId: "new" },
],
});
expect(_urlToState("/odoo/action-1/5/action-6/2")).toEqual({
active_id: 5,
action: 6,
resId: 2,
actionStack: [
{ action: 1 },
{ action: 1, resId: 5 },
{ active_id: 5, action: 6 },
{ active_id: 5, action: 6, resId: 2 },
],
});
expect(_urlToState("/odoo/action-module.xml_id/5/action-module.other_xml_id/2")).toEqual({
active_id: 5,
action: "module.other_xml_id",
resId: 2,
actionStack: [
{ action: "module.xml_id" },
{ action: "module.xml_id", resId: 5 },
{ active_id: 5, action: "module.other_xml_id" },
{ active_id: 5, action: "module.other_xml_id", resId: 2 },
],
});
// model
expect(_urlToState("/odoo/some.model/other.model")).toEqual(
{
model: "other.model",
actionStack: [{ model: "other.model" }],
},
{
message:
"model not followed by resId doesn't generate an action unless it's the last one",
}
);
expect(_urlToState("/odoo/5/some.model/other.model")).toEqual(
{
model: "other.model",
actionStack: [{ model: "other.model" }],
},
{
message:
"model not followed by resId doesn't generate an action unless it's the last one, even with an active_id",
}
);
expect(_urlToState("/odoo/some.model/7/other.model")).toEqual({
active_id: 7,
model: "other.model",
actionStack: [
{ model: "some.model", resId: 7 },
{ active_id: 7, model: "other.model" },
],
});
expect(_urlToState("/odoo/some.model/other.model/2")).toEqual({
model: "other.model",
resId: 2,
actionStack: [{ model: "other.model", resId: 2 }],
});
expect(_urlToState("/odoo/5/some.model/2/other.model")).toEqual({
active_id: 2,
model: "other.model",
actionStack: [
{ active_id: 5, model: "some.model", resId: 2 },
{ active_id: 2, model: "other.model" },
],
});
expect(_urlToState("/odoo/some.model/5/other.model/2")).toEqual({
active_id: 5,
model: "other.model",
resId: 2,
actionStack: [
{ model: "some.model", resId: 5 },
{ active_id: 5, model: "other.model", resId: 2 },
],
});
expect(_urlToState("/odoo/some.model/5/other.model/new")).toEqual({
active_id: 5,
model: "other.model",
resId: "new",
actionStack: [
{ model: "some.model", resId: 5 },
{ active_id: 5, model: "other.model", resId: "new" },
],
});
expect(_urlToState("/odoo/m-model_no_dot/5/m-no_dot_model/2")).toEqual({
active_id: 5,
model: "no_dot_model",
resId: 2,
actionStack: [
{ model: "model_no_dot", resId: 5 },
{ active_id: 5, model: "no_dot_model", resId: 2 },
],
});
// action + model
expect(_urlToState("/odoo/some-path/some.model")).toEqual({
model: "some.model",
actionStack: [{ action: "some-path" }, { model: "some.model" }],
});
expect(_urlToState("/odoo/5/some-path/some.model")).toEqual({
model: "some.model",
actionStack: [{ active_id: 5, action: "some-path" }, { model: "some.model" }],
});
expect(_urlToState("/odoo/some-path/7/some.model")).toEqual({
active_id: 7,
model: "some.model",
actionStack: [
{ action: "some-path" },
{ action: "some-path", resId: 7 },
{ active_id: 7, model: "some.model" },
],
});
expect(_urlToState("/odoo/some-path/some.model/2")).toEqual({
model: "some.model",
resId: 2,
actionStack: [{ action: "some-path" }, { model: "some.model", resId: 2 }],
});
expect(_urlToState("/odoo/5/some-path/2/some.model")).toEqual({
active_id: 2,
model: "some.model",
actionStack: [
{ active_id: 5, action: "some-path" },
{ active_id: 5, action: "some-path", resId: 2 },
{ active_id: 2, model: "some.model" },
],
});
expect(_urlToState("/odoo/some-path/5/some.model/2")).toEqual({
active_id: 5,
model: "some.model",
resId: 2,
actionStack: [
{ action: "some-path" },
{ action: "some-path", resId: 5 },
{ active_id: 5, model: "some.model", resId: 2 },
],
});
expect(_urlToState("/odoo/some-path/5/some.model/new")).toEqual({
active_id: 5,
model: "some.model",
resId: "new",
actionStack: [
{ action: "some-path" },
{ action: "some-path", resId: 5 },
{ active_id: 5, model: "some.model", resId: "new" },
],
});
expect(_urlToState("/odoo/action-1/5/m-model_no_dot/2")).toEqual({
active_id: 5,
model: "model_no_dot",
resId: 2,
actionStack: [
{ action: 1 },
{ action: 1, resId: 5 },
{ active_id: 5, model: "model_no_dot", resId: 2 },
],
});
expect(_urlToState("/odoo/action-module.xml_id/5/m-model_no_dot/2")).toEqual({
active_id: 5,
model: "model_no_dot",
resId: 2,
actionStack: [
{ action: "module.xml_id" },
{ action: "module.xml_id", resId: 5 },
{ active_id: 5, model: "model_no_dot", resId: 2 },
],
});
// model + action
expect(_urlToState("/odoo/some.model/other-path")).toEqual({
action: "other-path",
actionStack: [{ action: "other-path" }],
});
expect(_urlToState("/odoo/5/some.model/other-path")).toEqual({
action: "other-path",
actionStack: [{ action: "other-path" }],
});
expect(_urlToState("/odoo/some.model/2/other-path")).toEqual({
active_id: 2,
action: "other-path",
actionStack: [
{ model: "some.model", resId: 2 },
{ active_id: 2, action: "other-path" },
],
});
expect(_urlToState("/odoo/some.model/other-path/2")).toEqual({
action: "other-path",
resId: 2,
actionStack: [{ action: "other-path" }, { action: "other-path", resId: 2 }],
});
expect(_urlToState("/odoo/5/some.model/2/other-path")).toEqual({
active_id: 2,
action: "other-path",
actionStack: [
{ active_id: 5, model: "some.model", resId: 2 },
{ active_id: 2, action: "other-path" },
],
});
expect(_urlToState("/odoo/some.model/5/other-path/2")).toEqual({
active_id: 5,
action: "other-path",
resId: 2,
actionStack: [
{ model: "some.model", resId: 5 },
{ active_id: 5, action: "other-path" },
{ active_id: 5, action: "other-path", resId: 2 },
],
});
expect(_urlToState("/odoo/some.model/5/other-path/new")).toEqual({
active_id: 5,
action: "other-path",
resId: "new",
actionStack: [
{ model: "some.model", resId: 5 },
{ active_id: 5, action: "other-path" },
{ active_id: 5, action: "other-path", resId: "new" },
],
});
expect(_urlToState("/odoo/m-model_no_dot/5/action-1/2")).toEqual({
active_id: 5,
action: 1,
resId: 2,
actionStack: [
{ model: "model_no_dot", resId: 5 },
{ active_id: 5, action: 1 },
{ active_id: 5, action: 1, resId: 2 },
],
});
expect(_urlToState("/odoo/m-model_no_dot/5/action-module.xml_id/2")).toEqual({
active_id: 5,
action: "module.xml_id",
resId: 2,
actionStack: [
{ model: "model_no_dot", resId: 5 },
{ active_id: 5, action: "module.xml_id" },
{ active_id: 5, action: "module.xml_id", resId: 2 },
],
});
// edge cases
expect(_urlToState("/odoo/some-path/5/other-path/2?some_key=some_value")).toEqual({
active_id: 5,
action: "other-path",
resId: 2,
actionStack: [
{ action: "some-path" },
{ action: "some-path", resId: 5 },
{ active_id: 5, action: "other-path" },
{ active_id: 5, action: "other-path", resId: 2 },
],
some_key: "some_value",
});
expect(_urlToState("/odoo/some-path/5/some.model?view_type=list")).toEqual(
{
active_id: 5,
model: "some.model",
actionStack: [
{ action: "some-path" },
{ action: "some-path", resId: 5 },
{ active_id: 5, model: "some.model" },
],
view_type: "list",
},
{ message: "view_type doesn't end up in the actionStack" }
);
expect(_urlToState("/odoo/some-path/5/some.model/2?view_type=list")).toEqual(
{
active_id: 5,
model: "some.model",
resId: 2,
actionStack: [
{ action: "some-path" },
{ action: "some-path", resId: 5 },
{ active_id: 5, model: "some.model", resId: 2 },
],
view_type: "list",
},
{ message: "view_type and resId aren't incompatible" }
);
expect(_urlToState("/odoo/some-path/2/5/other-path")).toEqual(
{
active_id: 5,
action: "other-path",
actionStack: [
{ action: "some-path" },
{ action: "some-path", resId: 2 },
{ active_id: 5, action: "other-path" },
],
},
{ message: "resId immediately following active_id: action" }
);
expect(_urlToState("/odoo/some.model/2/5/other.model")).toEqual(
{
active_id: 5,
model: "other.model",
actionStack: [
{ model: "some.model", resId: 2 },
{ active_id: 5, model: "other.model" },
],
},
{ message: "resId immediately following active_id: model" }
);
});
});
describe("pushState", () => {
test("can push in same timeout", async () => {
createRouter();
expect(router.current).toEqual({});
router.pushState({ k1: 2 });
expect(router.current).toEqual({});
router.pushState({ k1: 3 });
expect(router.current).toEqual({});
await tick();
expect(router.current).toEqual({ k1: 3 });
});
test("can push state directly", async () => {
createRouter();
expect(router.current).toEqual({});
router.pushState({ k1: 2 }, { sync: true });
expect(router.current).toEqual({ k1: 2 });
router.pushState({ k1: 3 }, { sync: true });
expect(router.current).toEqual({ k1: 3 });
router.pushState({ k1: 4 });
router.pushState({ k2: 1 }, { sync: true });
expect(router.current).toEqual({ k1: 4, k2: 1 });
});
test("can lock keys", async () => {
createRouter();
router.addLockedKey("k1");
router.replaceState({ k1: 2 });
await tick();
expect(router.current).toEqual({ k1: 2 });
router.replaceState({ k1: 3 });
await tick();
expect(router.current).toEqual({ k1: 3 });
router.replaceState({ k2: 4 });
await tick();
expect(router.current).toEqual({ k1: 3, k2: 4 });
router.replaceState({ k1: 4 });
await tick();
expect(router.current).toEqual({ k1: 4, k2: 4 });
});
test("can re-lock keys in same final call", async () => {
createRouter();
router.addLockedKey("k1");
router.pushState({ k1: 2 });
await tick();
router.pushState({ k2: 1 });
router.pushState({ k1: 4 });
await tick();
expect(router.current).toEqual({ k1: 4, k2: 1 });
});
test("can replace search state", async () => {
createRouter();
router.pushState({ k1: 2 });
await tick();
expect(router.current).toEqual({ k1: 2 });
router.pushState({ k2: 3 }, { replace: true });
await tick();
expect(router.current).toEqual({ k2: 3 });
});
test("can replace search state with locked keys", async () => {
createRouter();
router.addLockedKey("k1");
router.pushState({ k1: 2 });
await tick();
expect(router.current).toEqual({ k1: 2 });
router.pushState({ k2: 3 }, { replace: true });
await tick();
expect(router.current).toEqual({ k1: 2, k2: 3 });
});
test("can merge hash", async () => {
createRouter();
router.pushState({ k1: 2 });
await tick();
expect(router.current).toEqual({ k1: 2 });
router.pushState({ k2: 3 });
await tick();
expect(router.current).toEqual({ k1: 2, k2: 3 });
});
test("undefined keys are not pushed", async () => {
redirect("/odoo");
const onPushState = () => expect.step("pushed state");
createRouter({ onPushState });
router.pushState({ k1: undefined });
await tick();
expect.verifySteps([]);
expect(router.current).toEqual({});
});
test("undefined keys destroy previous non locked keys", async () => {
createRouter();
router.pushState({ k1: 1 });
await tick();
expect(router.current).toEqual({ k1: 1 });
router.pushState({ k1: undefined });
await tick();
expect(router.current).toEqual({});
});
test("do not re-push when hash is same", async () => {
const onPushState = () => expect.step("pushed state");
createRouter({ onPushState });
router.pushState({ k1: 1, k2: 2 });
await tick();
expect.verifySteps(["pushed state"]);
router.pushState({ k2: 2, k1: 1 });
await tick();
expect.verifySteps([]);
});
test("do not re-push when hash is same (with integers as strings)", async () => {
const onPushState = () => expect.step("pushed state");
createRouter({ onPushState });
router.pushState({ k1: 1, k2: "2" });
await tick();
expect.verifySteps(["pushed state"]);
router.pushState({ k2: 2, k1: "1" });
await tick();
expect.verifySteps([]);
});
test("pushState adds action-related keys to last entry in actionStack", async () => {
createRouter();
router.pushState({ action: 1, resId: 2, actionStack: [{ action: 1, resId: 2 }] });
await tick();
expect(router.current).toEqual({
action: 1,
resId: 2,
actionStack: [{ action: 1, resId: 2 }],
});
router.pushState({
action: 3,
resId: 4,
view_type: "form",
model: "some.model",
active_id: 5,
someKey: "someVal",
});
await tick();
expect(router.current).toEqual({
action: 3,
resId: 4,
view_type: "form",
model: "some.model",
active_id: 5,
someKey: "someVal",
actionStack: [
{
action: 3,
resId: 4,
model: "some.model",
active_id: 5,
},
],
});
});
test("can hide keys", async () => {
createRouter();
router.hideKeyFromUrl("k1");
router.pushState({ k1: 2, k2: 3 });
await tick();
expect(router.current).toEqual({ k1: 2, k2: 3 });
expect(browser.location.href).toBe("https://www.hoot.test/odoo?k2=3");
});
test("different order of keys shouldn't push a new state", async () => {
redirect("/odoo?k1=2");
createRouter({
onPushState: () => expect.step("pushState"),
});
router.addLockedKey("z");
router.addLockedKey("a");
router.pushState({ z: 1, a: 2 });
await tick();
expect.verifySteps(["pushState"]);
expect(router.current).toEqual({ a: 2, z: 1, k1: 2 });
expect(browser.location.href).toBe("https://www.hoot.test/odoo?k1=2&z=1&a=2");
router.pushState({ k1: 2 }, { replace: true });
await tick();
expect.verifySteps([]);
expect(router.current).toEqual({ a: 2, z: 1, k1: 2 });
expect(browser.location.href).toBe("https://www.hoot.test/odoo?k1=2&z=1&a=2");
});
});
describe("History", () => {
test("properly handles history.back and history.forward", async () => {
redirect("/");
on(routerBus, "ROUTE_CHANGE", () => expect.step("ROUTE_CHANGE"));
createRouter();
router.pushState({ k1: 1 });
await tick();
expect(browser.location.href).toBe("https://www.hoot.test/odoo?k1=1");
router.pushState({ k2: 2 });
await tick();
expect(browser.location.href).toBe("https://www.hoot.test/odoo?k1=1&k2=2");
router.pushState({ k3: 3 }, { replace: true });
await tick();
expect(browser.location.href).toBe("https://www.hoot.test/odoo?k3=3");
browser.history.back(); // Click on back button
await tick();
expect(browser.location.href).toBe("https://www.hoot.test/odoo?k1=1&k2=2");
router.pushState({ k4: 3 }, { replace: true }); // Click on a link
await tick();
expect(browser.location.href).toBe("https://www.hoot.test/odoo?k4=3");
browser.history.back(); // Click on back button
await tick();
expect(browser.location.href).toBe("https://www.hoot.test/odoo?k1=1&k2=2");
browser.history.forward(); // Click on forward button
await tick();
expect(browser.location.href).toBe("https://www.hoot.test/odoo?k4=3");
expect.verifySteps(["ROUTE_CHANGE", "ROUTE_CHANGE", "ROUTE_CHANGE"]);
});
test("unserialized parts of action stack are preserved when going back/forward", async () => {
redirect("/odoo");
createRouter();
expect(router.current).toEqual({});
router.pushState({
actionStack: [{ action: "some-path", displayName: "A cool display name" }],
});
await tick();
expect(browser.location.href).toBe("https://www.hoot.test/odoo/some-path");
expect(router.current).toEqual({
actionStack: [{ action: "some-path", displayName: "A cool display name" }],
});
router.pushState({
actionStack: [{ action: "other-path", displayName: "A different display name" }],
});
await tick();
expect(browser.location.href).toBe("https://www.hoot.test/odoo/other-path");
expect(router.current).toEqual({
actionStack: [{ action: "other-path", displayName: "A different display name" }],
});
browser.history.back();
await tick();
expect(browser.location.href).toBe("https://www.hoot.test/odoo/some-path");
expect(router.current).toEqual({
actionStack: [{ action: "some-path", displayName: "A cool display name" }],
});
browser.history.forward();
await tick();
expect(browser.location.href).toBe("https://www.hoot.test/odoo/other-path");
expect(router.current).toEqual({
actionStack: [{ action: "other-path", displayName: "A different display name" }],
});
});
test("properly handles history.back with hidden keys", async () => {
redirect("/");
on(routerBus, "ROUTE_CHANGE", () => expect.step("ROUTE_CHANGE"));
createRouter();
router.hideKeyFromUrl("k1");
router.pushState({ k1: 1, k2: 2 });
await tick();
expect(router.current).toEqual({ k1: 1, k2: 2 });
expect(browser.location.href).toBe("https://www.hoot.test/odoo?k2=2");
router.pushState({ k3: 3 }, { replace: true }); // Click on a link
await tick();
expect(router.current).toEqual({ k3: 3 });
expect(browser.location.href).toBe("https://www.hoot.test/odoo?k3=3");
browser.history.back(); // Click on back button
await tick();
expect(router.current).toEqual({ k1: 1, k2: 2 });
expect(browser.location.href).toBe("https://www.hoot.test/odoo?k2=2");
expect.verifySteps(["ROUTE_CHANGE"]);
});
});
describe("Retrocompatibility", () => {
test("parse an url with hash (key/values)", async () => {
Object.assign(browser.location, { pathname: "/web" });
browser.location.hash = "#a=114&k=c.e&f=1&g=91";
createRouter();
expect(browser.location.search).toBe("?a=114&k=c.e&f=1&g=91");
expect(browser.location.hash).toBe("");
expect(router.current).toEqual({ a: 114, k: "c.e", f: 1, g: 91 });
expect(browser.location.pathname).toBe("/odoo");
});
test("parse an url with hash (key/values) and query string", async () => {
Object.assign(browser.location, { pathname: "/web" });
browser.location.hash = "#g=91";
browser.location.search = "?a=114&t=c.e&f=1";
createRouter();
expect(browser.location.search).toBe("?a=114&t=c.e&f=1&g=91");
expect(browser.location.hash).toBe("");
expect(router.current).toEqual({ a: 114, t: "c.e", f: 1, g: 91 });
expect(browser.location.pathname).toBe("/odoo");
});
test("parse an url with hash (anchor link)", async () => {
redirect("/odoo#anchor");
browser.location.hash = "#anchor";
createRouter();
expect(browser.location.search).toBe("");
expect(browser.location.hash).toBe("#anchor");
expect(browser.location.pathname).toBe("/odoo");
expect(router.current).toEqual({});
});
test("parse an url with hash (anchor link) and query string", async () => {
redirect("/odoo?a=114&g=c.e&f=1#anchor");
browser.location.hash = "#anchor";
browser.location.search = "?a=114&g=c.e&f=1";
createRouter();
expect(browser.location.search).toBe("?a=114&g=c.e&f=1");
expect(browser.location.hash).toBe("#anchor");
expect(router.current).toEqual({ a: 114, g: "c.e", f: 1 });
expect(browser.location.pathname).toBe("/odoo");
});
});
describe("internal links", () => {
test("click on internal link does a loadState instead of a full reload", async () => {
redirect("/odoo");
createRouter({ onPushState: () => expect.step("pushState") });
const fixture = getFixture();
const link = document.createElement("a");
link.href = "/odoo/some-action/2";
fixture.appendChild(link);
expect(router.current).toEqual({});
let defaultPrevented;
browser.addEventListener("click", (ev) => {
expect.step("click");
defaultPrevented = ev.defaultPrevented;
ev.preventDefault();
});
await click("a");
await tick();
expect.verifySteps(["click"]);
expect(router.current).toEqual({
action: "some-action",
actionStack: [
{
action: "some-action",
},
{
action: "some-action",
resId: 2,
},
],
resId: 2,
});
expect(defaultPrevented).toBe(true);
});
test("click on internal link with children does a loadState instead of a full reload", async () => {
redirect("/odoo");
createRouter({ onPushState: () => expect.step("pushState") });
const fixture = getFixture();
const link = document.createElement("a");
const span = document.createElement("span");
link.appendChild(span);
link.href = "/odoo/some-action/2";
fixture.appendChild(link);
expect(router.current).toEqual({});
let defaultPrevented;
browser.addEventListener("click", (ev) => {
expect.step("click");
defaultPrevented = ev.defaultPrevented;
ev.preventDefault();
});
await click("span");
await tick();
expect.verifySteps(["click"]);
expect(router.current).toEqual({
action: "some-action",
actionStack: [
{
action: "some-action",
},
{
action: "some-action",
resId: 2,
},
],
resId: 2,
});
expect(defaultPrevented).toBe(true);
});
test("click on internal link with different protocol does a loadState", async () => {
redirect("/odoo");
createRouter({ onPushState: () => expect.step("pushState") });
const fixture = getFixture();
const link = document.createElement("a");
link.href = "http://" + browser.location.host + "/odoo/some-action/2";
fixture.appendChild(link);
expect(router.current).toEqual({});
expect(browser.location.protocol).not.toBe(link.protocol, {
message:
"should have different protocols between the current location and the clicked link",
});
let defaultPrevented;
browser.addEventListener("click", (ev) => {
expect.step("click");
defaultPrevented = ev.defaultPrevented;
ev.preventDefault();
});
await click("a");
await tick();
expect.verifySteps(["click"]);
expect(router.current).toEqual({
action: "some-action",
actionStack: [
{
action: "some-action",
},
{
action: "some-action",
resId: 2,
},
],
resId: 2,
});
expect(defaultPrevented).toBe(true);
});
test("click on internal link with hash (key/values)", async () => {
redirect("/odoo");
createRouter({
onPushState: () => expect.step("pushState"),
onReplaceState: () => expect.step("replaceState"),
});
const fixture = getFixture();
const link = document.createElement("a");
link.href = "/odoo/1/action-114/22";
fixture.appendChild(link);
expect(router.current).toEqual({});
let defaultPrevented;
browser.addEventListener("click", (ev) => {
expect.step("click");
defaultPrevented = ev.defaultPrevented;
ev.preventDefault();
});
await click("a");
await tick();
expect.verifySteps(["click"]);
expect(router.current).toEqual({
action: 114,
active_id: 1,
actionStack: [
{
active_id: 1,
action: 114,
},
{
active_id: 1,
resId: 22,
action: 114,
},
],
resId: 22,
});
expect(defaultPrevented).toBe(true);
});
test("click on internal link with hash (anchor)", async () => {
redirect("/odoo");
createRouter({
onPushState: (_data, _unused, url) => {
expect.step("pushState: " + url);
},
onReplaceState: () => expect.step("replaceState"),
});
const fixture = getFixture();
const link = document.createElement("a");
link.href = "/odoo/1/action-114/22#anchorId";
fixture.appendChild(link);
expect(router.current).toEqual({});
let defaultPrevented;
browser.addEventListener("click", (ev) => {
expect.step("click");
defaultPrevented = ev.defaultPrevented;
ev.preventDefault();
});
await click("a");
await tick();
expect.verifySteps([
"pushState: https://www.hoot.test/odoo/1/action-114/22#anchorId",
"click",
]);
expect(router.current).toEqual({
action: 114,
active_id: 1,
actionStack: [
{
active_id: 1,
action: 114,
},
{
active_id: 1,
resId: 22,
action: 114,
},
],
resId: 22,
});
expect(defaultPrevented).toBe(true);
});
test("click on internal link with target _blank doesn't do a loadState", async () => {
redirect("/odoo");
createRouter({ onPushState: () => expect.step("pushState") });
const fixture = getFixture();
const link = document.createElement("a");
link.href = "/odoo/some-action/2";
link.target = "_blank";
fixture.appendChild(link);
expect(router.current).toEqual({});
let defaultPrevented;
link.addEventListener("click", (ev) => {
expect.step("click");
defaultPrevented = ev.defaultPrevented;
ev.preventDefault();
});
await click("a");
await tick();
expect.verifySteps(["click"]);
expect(router.current).toEqual({});
expect(defaultPrevented).toBe(false);
});
});