mirror of
https://github.com/bringout/oca-ocb-core.git
synced 2026-04-20 00:52:07 +02:00
vanilla 17.0
This commit is contained in:
parent
d72e748793
commit
a9bcec8e91
1986 changed files with 1613876 additions and 568976 deletions
|
|
@ -51,16 +51,8 @@ if (window.QUnit) {
|
|||
{ tagName: "SCRIPT", attr: "id", value: "" },
|
||||
{ tagName: "DIV", attr: "class", value: "o_notification_manager" },
|
||||
{ tagName: "DIV", attr: "class", value: "tooltip fade bs-tooltip-auto" },
|
||||
{
|
||||
tagName: "DIV",
|
||||
attr: "class",
|
||||
value: "tooltip fade bs-tooltip-auto show",
|
||||
},
|
||||
{
|
||||
tagName: "DIV",
|
||||
attr: "class",
|
||||
value: "tooltip tooltip-field-info fade bs-tooltip-auto",
|
||||
},
|
||||
{ tagName: "DIV", attr: "class", value: "tooltip fade bs-tooltip-auto show" },
|
||||
{ tagName: "DIV", attr: "class", value: "tooltip tooltip-field-info fade bs-tooltip-auto" },
|
||||
{
|
||||
tagName: "DIV",
|
||||
attr: "class",
|
||||
|
|
|
|||
|
|
@ -1,37 +0,0 @@
|
|||
odoo.define("web.SessionOverrideForTests", (require) => {
|
||||
// Override the Session.session_reload function
|
||||
// The wowl test infrastructure does set a correct odoo global value before each test
|
||||
// while the session is built only once for all tests.
|
||||
// So if a test does a session_reload, it will merge the odoo global of that test
|
||||
// into the session, and will alter every subsequent test of the suite.
|
||||
// Obviously, we don't want that, ever.
|
||||
const { session: sessionInfo } = require("@web/session");
|
||||
const initialSessionInfo = Object.assign({}, sessionInfo);
|
||||
const Session = require("web.Session");
|
||||
const { patch } = require("@web/core/utils/patch");
|
||||
patch(Session.prototype, "web.SessionTestPatch", {
|
||||
async session_reload() {
|
||||
for (const key in sessionInfo) {
|
||||
delete sessionInfo[key];
|
||||
}
|
||||
for (const key in initialSessionInfo) {
|
||||
sessionInfo[key] = initialSessionInfo[key];
|
||||
}
|
||||
return await this._super(...arguments);
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
odoo.define("web.test_legacy", async (require) => {
|
||||
require("web.SessionOverrideForTests");
|
||||
require("web.test_utils");
|
||||
const session = require("web.session");
|
||||
await session.is_bound; // await for templates from server
|
||||
|
||||
const FormView = require("web.FormView");
|
||||
const ListView = require("web.ListView");
|
||||
const viewRegistry = require("web.view_registry");
|
||||
viewRegistry.add("legacy_form", FormView).add("legacy_list", ListView);
|
||||
|
||||
return { legacyProm: session.is_bound };
|
||||
});
|
||||
|
|
@ -1,65 +0,0 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { uiService } from "@web/core/ui/ui_service";
|
||||
import { patch, unpatch } from "@web/core/utils/patch";
|
||||
import { makeLegacyDialogMappingService } from "@web/legacy/utils";
|
||||
import { hotkeyService } from "@web/core/hotkeys/hotkey_service";
|
||||
import core from "web.core";
|
||||
import makeTestEnvironment from "web.test_env";
|
||||
import { registerCleanup } from "./cleanup";
|
||||
import { makeTestEnv } from "./mock_env";
|
||||
import { patchWithCleanup } from "./utils";
|
||||
import * as FavoriteMenu from "web.FavoriteMenu";
|
||||
import * as CustomFavoriteItem from "web.CustomFavoriteItem";
|
||||
|
||||
const serviceRegistry = registry.category("services");
|
||||
|
||||
export async function makeLegacyDialogMappingTestEnv() {
|
||||
const coreBusListeners = [];
|
||||
patch(core.bus, "legacy.core.bus.listeners", {
|
||||
on(eventName, thisArg, callback) {
|
||||
this._super(...arguments);
|
||||
coreBusListeners.push({ eventName, callback });
|
||||
},
|
||||
});
|
||||
|
||||
const legacyEnv = makeTestEnvironment({ bus: core.bus });
|
||||
serviceRegistry.add("ui", uiService);
|
||||
serviceRegistry.add("hotkey", hotkeyService);
|
||||
serviceRegistry.add("legacy_dialog_mapping", makeLegacyDialogMappingService(legacyEnv));
|
||||
|
||||
const env = await makeTestEnv();
|
||||
legacyEnv.services.hotkey = env.services.hotkey;
|
||||
legacyEnv.services.ui = env.services.ui;
|
||||
|
||||
registerCleanup(() => {
|
||||
for (const listener of coreBusListeners) {
|
||||
core.bus.off(listener.eventName, listener.callback);
|
||||
}
|
||||
unpatch(core.bus, "legacy.core.bus.listeners");
|
||||
});
|
||||
|
||||
return {
|
||||
legacyEnv,
|
||||
env,
|
||||
};
|
||||
}
|
||||
|
||||
function clearLegacyRegistryWithCleanup(r) {
|
||||
const patch = {
|
||||
// To improve? Initial data in registry is ignored.
|
||||
map: Object.create(null),
|
||||
// Preserve onAdd listeners
|
||||
listeners: [...r.listeners],
|
||||
_scoreMapping: Object.create(null),
|
||||
_sortedKeys: null,
|
||||
};
|
||||
patchWithCleanup(r, patch);
|
||||
}
|
||||
|
||||
export function prepareLegacyRegistriesWithCleanup() {
|
||||
// Clear FavoriteMenu registry and add the "Save favorite" item.
|
||||
clearLegacyRegistryWithCleanup(FavoriteMenu.registry);
|
||||
FavoriteMenu.registry.add("favorite-generator-menu", CustomFavoriteItem, 0);
|
||||
}
|
||||
|
|
@ -2,12 +2,12 @@
|
|||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { makeEnv, startServices } from "@web/env";
|
||||
import FormController from "web.FormController";
|
||||
import { SERVICES_METADATA } from "../../src/env";
|
||||
import { registerCleanup } from "./cleanup";
|
||||
import { makeMockServer } from "./mock_server";
|
||||
import { mocks } from "./mock_services";
|
||||
import { patchWithCleanup } from "./utils";
|
||||
import { Component } from "@odoo/owl";
|
||||
|
||||
function prepareRegistry(registry, keepContent = false) {
|
||||
const _addEventListener = registry.addEventListener.bind(registry);
|
||||
|
|
@ -48,44 +48,49 @@ export function clearServicesMetadataWithCleanup() {
|
|||
});
|
||||
}
|
||||
|
||||
function prepareRegistriesWithCleanup() {
|
||||
// Clone registries
|
||||
cloneRegistryWithCleanup(registry.category("actions"));
|
||||
cloneRegistryWithCleanup(registry.category("views"));
|
||||
cloneRegistryWithCleanup(registry.category("error_handlers"));
|
||||
cloneRegistryWithCleanup(registry.category("command_provider"));
|
||||
cloneRegistryWithCleanup(registry.category("command_setup"));
|
||||
cloneRegistryWithCleanup(registry.category("view_widgets"));
|
||||
cloneRegistryWithCleanup(registry.category("fields"));
|
||||
cloneRegistryWithCleanup(registry.category("wowlToLegacyServiceMappers"));
|
||||
export const registryNamesToCloneWithCleanup = [
|
||||
"actions",
|
||||
"command_provider",
|
||||
"command_setup",
|
||||
"error_handlers",
|
||||
"fields",
|
||||
"fields",
|
||||
"main_components",
|
||||
"view_widgets",
|
||||
"views",
|
||||
];
|
||||
|
||||
cloneRegistryWithCleanup(registry.category("main_components"));
|
||||
cloneRegistryWithCleanup(registry.category("fields"));
|
||||
export const utils = {
|
||||
prepareRegistriesWithCleanup() {
|
||||
// Clone registries
|
||||
registryNamesToCloneWithCleanup.forEach((registryName) =>
|
||||
cloneRegistryWithCleanup(registry.category(registryName))
|
||||
);
|
||||
|
||||
// Clear registries
|
||||
clearRegistryWithCleanup(registry.category("command_categories"));
|
||||
clearRegistryWithCleanup(registry.category("debug"));
|
||||
clearRegistryWithCleanup(registry.category("error_dialogs"));
|
||||
clearRegistryWithCleanup(registry.category("favoriteMenu"));
|
||||
clearRegistryWithCleanup(registry.category("ir.actions.report handlers"));
|
||||
clearRegistryWithCleanup(registry.category("wowlToLegacyServiceMappers"));
|
||||
// Clear registries
|
||||
clearRegistryWithCleanup(registry.category("command_categories"));
|
||||
clearRegistryWithCleanup(registry.category("debug"));
|
||||
clearRegistryWithCleanup(registry.category("error_dialogs"));
|
||||
clearRegistryWithCleanup(registry.category("favoriteMenu"));
|
||||
clearRegistryWithCleanup(registry.category("ir.actions.report handlers"));
|
||||
clearRegistryWithCleanup(registry.category("main_components"));
|
||||
|
||||
clearRegistryWithCleanup(registry.category("services"));
|
||||
clearServicesMetadataWithCleanup();
|
||||
clearRegistryWithCleanup(registry.category("services"));
|
||||
clearServicesMetadataWithCleanup();
|
||||
|
||||
clearRegistryWithCleanup(registry.category("systray"));
|
||||
clearRegistryWithCleanup(registry.category("user_menuitems"));
|
||||
clearRegistryWithCleanup(registry.category("kanban_examples"));
|
||||
clearRegistryWithCleanup(registry.category("__processed_archs__"));
|
||||
clearRegistryWithCleanup(registry.category("action_menus"));
|
||||
// fun fact: at least one registry is missing... this shows that we need a
|
||||
// better design for the way we clear these registries...
|
||||
}
|
||||
clearRegistryWithCleanup(registry.category("systray"));
|
||||
clearRegistryWithCleanup(registry.category("user_menuitems"));
|
||||
clearRegistryWithCleanup(registry.category("kanban_examples"));
|
||||
clearRegistryWithCleanup(registry.category("__processed_archs__"));
|
||||
// fun fact: at least one registry is missing... this shows that we need a
|
||||
// better design for the way we clear these registries...
|
||||
},
|
||||
};
|
||||
|
||||
// This is exported in a utils object to allow for patching
|
||||
export const utils = {
|
||||
prepareRegistriesWithCleanup,
|
||||
};
|
||||
export function prepareRegistriesWithCleanup() {
|
||||
return utils.prepareRegistriesWithCleanup();
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {import("@web/env").OdooEnv} OdooEnv
|
||||
|
|
@ -118,19 +123,26 @@ export async function makeTestEnv(config = {}) {
|
|||
await makeMockServer(config.serverData, config.mockRPC);
|
||||
}
|
||||
|
||||
// remove the multi-click delay for the quick edit in form views
|
||||
// todo: move this elsewhere (setup?)
|
||||
const initialQuickEditDelay = FormController.prototype.multiClickTime;
|
||||
FormController.prototype.multiClickTime = 0;
|
||||
registerCleanup(() => {
|
||||
FormController.prototype.multiClickTime = initialQuickEditDelay;
|
||||
});
|
||||
|
||||
let env = makeEnv();
|
||||
await startServices(env);
|
||||
owl.Component.env = env;
|
||||
Component.env = env;
|
||||
if ("config" in config) {
|
||||
env = Object.assign(Object.create(env), { config: config.config });
|
||||
}
|
||||
return env;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a test environment for dialog tests
|
||||
*
|
||||
* @param {*} config
|
||||
* @returns {Promise<OdooEnv>}
|
||||
*/
|
||||
export async function makeDialogTestEnv(config = {}) {
|
||||
const env = await makeTestEnv(config);
|
||||
env.dialogData = {
|
||||
isActive: true,
|
||||
close() {},
|
||||
};
|
||||
return env;
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,19 +1,19 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { Component, status } from "@odoo/owl";
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { routerService } from "@web/core/browser/router_service";
|
||||
import { localization } from "@web/core/l10n/localization";
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import { rpcService } from "@web/core/network/rpc_service";
|
||||
import { userService } from "@web/core/user_service";
|
||||
import { effectService } from "@web/core/effects/effect_service";
|
||||
import { localization } from "@web/core/l10n/localization";
|
||||
import { rpcService } from "@web/core/network/rpc_service";
|
||||
import { ormService } from "@web/core/orm_service";
|
||||
import { overlayService } from "@web/core/overlay/overlay_service";
|
||||
import { uiService } from "@web/core/ui/ui_service";
|
||||
import { userService } from "@web/core/user_service";
|
||||
import { objectToUrlEncodedString } from "@web/core/utils/urls";
|
||||
import { ConnectionAbortedError } from "../../src/core/network/rpc_service";
|
||||
import { registerCleanup } from "./cleanup";
|
||||
import { patchWithCleanup } from "./utils";
|
||||
import { uiService } from "@web/core/ui/ui_service";
|
||||
import { ConnectionAbortedError } from "../../src/core/network/rpc_service";
|
||||
|
||||
import { Component, status } from "@odoo/owl";
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Mock Services
|
||||
|
|
@ -35,12 +35,13 @@ export const defaultLocalization = {
|
|||
* @param {Partial<typeof defaultLocalization>} [config]
|
||||
*/
|
||||
export function makeFakeLocalizationService(config = {}) {
|
||||
patchWithCleanup(localization, Object.assign({}, defaultLocalization, config));
|
||||
patchWithCleanup(localization, { ...defaultLocalization, ...config });
|
||||
patchWithCleanup(luxon.Settings, { defaultNumberingSystem: "latn" });
|
||||
|
||||
return {
|
||||
name: "localization",
|
||||
start: async (env) => {
|
||||
env._t = _t;
|
||||
return localization;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -59,15 +60,33 @@ function buildMockRPC(mockRPC) {
|
|||
export function makeFakeRPCService(mockRPC) {
|
||||
return {
|
||||
name: "rpc",
|
||||
start() {
|
||||
start(env) {
|
||||
const rpcService = buildMockRPC(mockRPC);
|
||||
return function () {
|
||||
let nextId = 1;
|
||||
return function (route, params = {}, settings = {}) {
|
||||
let rejectFn;
|
||||
const data = {
|
||||
id: nextId++,
|
||||
jsonrpc: "2.0",
|
||||
method: "call",
|
||||
params: params,
|
||||
};
|
||||
env.bus.trigger("RPC:REQUEST", { data, url: route, settings });
|
||||
const rpcProm = new Promise((resolve, reject) => {
|
||||
rejectFn = reject;
|
||||
rpcService(...arguments)
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
.then((result) => {
|
||||
env.bus.trigger("RPC:RESPONSE", { data, settings, result });
|
||||
resolve(result);
|
||||
})
|
||||
.catch((error) => {
|
||||
env.bus.trigger("RPC:RESPONSE", {
|
||||
data,
|
||||
settings,
|
||||
error,
|
||||
});
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
rpcProm.abort = (rejectError = true) => {
|
||||
if (rejectError) {
|
||||
|
|
@ -110,13 +129,13 @@ export function makeMockXHR(response, sendCb, def) {
|
|||
if (typeof data === "string") {
|
||||
try {
|
||||
data = JSON.parse(data);
|
||||
} catch (_e) {
|
||||
} catch {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
try {
|
||||
await sendCb.call(this, data);
|
||||
} catch (_e) {
|
||||
} catch {
|
||||
listener = this._errorListener;
|
||||
}
|
||||
}
|
||||
|
|
@ -151,8 +170,7 @@ export function makeMockFetch(mockRPC) {
|
|||
try {
|
||||
res = await _rpc(route, params);
|
||||
status = 200;
|
||||
} catch (_e) {
|
||||
res = { error: _e.message };
|
||||
} catch {
|
||||
status = 500;
|
||||
}
|
||||
const blob = new Blob([JSON.stringify(res || {})], { type: "application/json" });
|
||||
|
|
@ -170,7 +188,6 @@ export function makeMockFetch(mockRPC) {
|
|||
|
||||
/**
|
||||
* @param {Object} [params={}]
|
||||
* @param {Object} [params.onRedirect] hook on the "redirect" method
|
||||
* @returns {typeof routerService}
|
||||
*/
|
||||
export function makeFakeRouterService(params = {}) {
|
||||
|
|
@ -182,14 +199,6 @@ export function makeFakeRouterService(params = {}) {
|
|||
browser.location.hash = objectToUrlEncodedString(hash);
|
||||
});
|
||||
registerCleanup(router.cancelPushes);
|
||||
patchWithCleanup(router, {
|
||||
async redirect() {
|
||||
await this._super(...arguments);
|
||||
if (params.onRedirect) {
|
||||
params.onRedirect(...arguments);
|
||||
}
|
||||
},
|
||||
});
|
||||
return router;
|
||||
},
|
||||
};
|
||||
|
|
@ -209,25 +218,6 @@ export const fakeCommandService = {
|
|||
},
|
||||
};
|
||||
|
||||
export const fakeCookieService = {
|
||||
start() {
|
||||
const cookie = {};
|
||||
return {
|
||||
get current() {
|
||||
return cookie;
|
||||
},
|
||||
setCookie(key, value) {
|
||||
if (value !== undefined) {
|
||||
cookie[key] = value;
|
||||
}
|
||||
},
|
||||
deleteCookie(key) {
|
||||
delete cookie[key];
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export const fakeTitleService = {
|
||||
start() {
|
||||
let current = {};
|
||||
|
|
@ -293,14 +283,27 @@ export function makeFakeUserService(hasGroup = () => false) {
|
|||
export const fakeCompanyService = {
|
||||
start() {
|
||||
return {
|
||||
availableCompanies: {},
|
||||
allowedCompanyIds: [],
|
||||
allowedCompanies: {},
|
||||
activeCompanyIds: [],
|
||||
currentCompany: {},
|
||||
setCompanies: () => {},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export function makeFakeBarcodeService() {
|
||||
return {
|
||||
start() {
|
||||
return {
|
||||
bus: {
|
||||
async addEventListener() {},
|
||||
async removeEventListener() {},
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function makeFakeHTTPService(getResponse, postResponse) {
|
||||
getResponse =
|
||||
getResponse ||
|
||||
|
|
@ -326,11 +329,20 @@ export function makeFakeHTTPService(getResponse, postResponse) {
|
|||
};
|
||||
}
|
||||
|
||||
function makeFakeActionService() {
|
||||
return {
|
||||
start() {
|
||||
return {
|
||||
doAction() {},
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const mocks = {
|
||||
color_scheme: () => fakeColorSchemeService,
|
||||
company: () => fakeCompanyService,
|
||||
command: () => fakeCommandService,
|
||||
cookie: () => fakeCookieService,
|
||||
effect: () => effectService, // BOI The real service ? Is this what we want ?
|
||||
localization: makeFakeLocalizationService,
|
||||
notification: makeFakeNotificationService,
|
||||
|
|
@ -340,4 +352,7 @@ export const mocks = {
|
|||
ui: () => uiService,
|
||||
user: () => userService,
|
||||
dialog: makeFakeDialogService,
|
||||
orm: () => ormService,
|
||||
action: makeFakeActionService,
|
||||
overlay: () => overlayService,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,15 +1,41 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { templates } from "@web/core/assets";
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { isMacOS } from "@web/core/browser/feature_detection";
|
||||
import { download } from "@web/core/network/download";
|
||||
import { Deferred } from "@web/core/utils/concurrency";
|
||||
import { patch, unpatch } from "@web/core/utils/patch";
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { isVisible } from "@web/core/utils/ui";
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import { registerCleanup } from "./cleanup";
|
||||
import { templates } from "@web/core/assets";
|
||||
|
||||
import { App, onMounted, onPatched, useComponent } from "@odoo/owl";
|
||||
import {
|
||||
App,
|
||||
onError,
|
||||
onMounted,
|
||||
onPatched,
|
||||
onRendered,
|
||||
onWillDestroy,
|
||||
onWillPatch,
|
||||
onWillRender,
|
||||
onWillStart,
|
||||
onWillUnmount,
|
||||
onWillUpdateProps,
|
||||
useComponent,
|
||||
} from "@odoo/owl";
|
||||
|
||||
/**
|
||||
* @typedef {keyof HTMLElementEventMap | keyof WindowEventMap} EventType
|
||||
*
|
||||
* @typedef {Side | `${Side}-${Side}` | { x?: number, y?: number }} Position
|
||||
*
|
||||
* @typedef {"bottom" | "left" | "right" | "top"} Side
|
||||
*
|
||||
* @typedef TriggerEventOptions
|
||||
* @property {boolean} [skipVisibilityCheck=false]
|
||||
* @property {boolean} [sync=false]
|
||||
*/
|
||||
|
||||
/**
|
||||
* Patch the native Date object
|
||||
|
|
@ -22,13 +48,14 @@ import { App, onMounted, onPatched, useComponent } from "@odoo/owl";
|
|||
* @param {number} [hours]
|
||||
* @param {number} [minutes]
|
||||
* @param {number} [seconds]
|
||||
* @param {number} [ms=0]
|
||||
*/
|
||||
export function patchDate(year, month, day, hours, minutes, seconds) {
|
||||
export function patchDate(year, month, day, hours, minutes, seconds, ms = 0) {
|
||||
var RealDate = window.Date;
|
||||
var actualDate = new RealDate();
|
||||
|
||||
// By default, RealDate uses the browser offset, so we must replace it with the offset fixed in luxon.
|
||||
var fakeDate = new RealDate(year, month, day, hours, minutes, seconds);
|
||||
var fakeDate = new RealDate(year, month, day, hours, minutes, seconds, ms);
|
||||
if (!(luxon.Settings.defaultZone instanceof luxon.FixedOffsetZone)) {
|
||||
throw new Error("luxon.Settings.defaultZone must be a FixedOffsetZone");
|
||||
}
|
||||
|
|
@ -114,31 +141,23 @@ export function patchDate(year, month, day, hours, minutes, seconds) {
|
|||
* -120 => UTC-2
|
||||
*/
|
||||
export function patchTimeZone(offset) {
|
||||
const originalZone = luxon.Settings.defaultZone;
|
||||
luxon.Settings.defaultZone = new luxon.FixedOffsetZone.instance(offset);
|
||||
registerCleanup(() => {
|
||||
luxon.Settings.defaultZone = originalZone;
|
||||
});
|
||||
patchWithCleanup(luxon.Settings, { defaultZone: luxon.FixedOffsetZone.instance(offset) });
|
||||
}
|
||||
|
||||
let nextId = 1;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Object} obj object to patch
|
||||
* @param {Object} patchValue the actual patch description
|
||||
* @param {{pure?: boolean}} [options]
|
||||
*/
|
||||
export function patchWithCleanup(obj, patchValue, options) {
|
||||
const patchName = `__test_patch_${nextId++}__`;
|
||||
patch(obj, patchName, patchValue, options);
|
||||
export function patchWithCleanup(obj, patchValue) {
|
||||
const unpatch = patch(obj, patchValue);
|
||||
registerCleanup(() => {
|
||||
unpatch(obj, patchName);
|
||||
unpatch();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {HTMLElement}
|
||||
* @returns {Element}
|
||||
*/
|
||||
export function getFixture() {
|
||||
if (!window.QUnit) {
|
||||
|
|
@ -147,7 +166,7 @@ export function getFixture() {
|
|||
if (QUnit.config.debug) {
|
||||
return document.body;
|
||||
} else {
|
||||
return document.querySelector("#qunit-fixture");
|
||||
return document.getElementById("qunit-fixture");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -175,175 +194,217 @@ export function findElement(el, selector) {
|
|||
return target;
|
||||
}
|
||||
|
||||
function keyboardEventBubble(args) {
|
||||
return Object.assign({}, args, {
|
||||
bubbles: true,
|
||||
keyCode: args.which,
|
||||
cancelable: true,
|
||||
});
|
||||
}
|
||||
//-----------------------------------------------------------------------------
|
||||
// Event init attributes mappers
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
function mouseEventMapping(args) {
|
||||
return {
|
||||
clientX: args ? args.pageX : undefined,
|
||||
clientY: args ? args.pageY : undefined,
|
||||
...args,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
view: window,
|
||||
};
|
||||
}
|
||||
/** @param {EventInit} [args] */
|
||||
const mapBubblingEvent = (args) => ({ ...args, bubbles: true });
|
||||
|
||||
function mouseEventNoBubble(args) {
|
||||
return {
|
||||
clientX: args ? args.pageX : undefined,
|
||||
clientY: args ? args.pageY : undefined,
|
||||
...args,
|
||||
bubbles: false,
|
||||
cancelable: false,
|
||||
view: window,
|
||||
};
|
||||
}
|
||||
/** @param {EventInit} [args] */
|
||||
const mapNonBubblingEvent = (args) => ({ ...args, bubbles: false });
|
||||
|
||||
function touchEventMapping(args) {
|
||||
return {
|
||||
...args,
|
||||
cancelable: true,
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
view: window,
|
||||
rotation: 0.0,
|
||||
zoom: 1.0,
|
||||
touches: args.touches ? [...args.touches.map((e) => new Touch(e))] : undefined,
|
||||
};
|
||||
}
|
||||
/** @param {EventInit} [args={}] */
|
||||
const mapBubblingPointerEvent = (args = {}) => ({
|
||||
clientX: args.pageX,
|
||||
clientY: args.pageY,
|
||||
...args,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
view: window,
|
||||
});
|
||||
|
||||
function touchEventCancelMapping(args) {
|
||||
return {
|
||||
...touchEventMapping(args),
|
||||
cancelable: false,
|
||||
};
|
||||
}
|
||||
/** @param {EventInit} [args] */
|
||||
const mapNonBubblingPointerEvent = (args) => ({
|
||||
...mapBubblingPointerEvent(args),
|
||||
bubbles: false,
|
||||
cancelable: false,
|
||||
});
|
||||
|
||||
function noBubble(args) {
|
||||
return Object.assign({}, args, { bubbles: false });
|
||||
}
|
||||
/** @param {EventInit} [args={}] */
|
||||
const mapCancelableTouchEvent = (args = {}) => ({
|
||||
...args,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
composed: true,
|
||||
rotation: 0.0,
|
||||
touches: args.touches ? [...args.touches.map((e) => new Touch(e))] : undefined,
|
||||
view: window,
|
||||
zoom: 1.0,
|
||||
});
|
||||
|
||||
function onlyBubble(args) {
|
||||
return Object.assign({}, args, { bubbles: true });
|
||||
}
|
||||
/** @param {EventInit} [args] */
|
||||
const mapNonCancelableTouchEvent = (args) => ({
|
||||
...mapCancelableTouchEvent(args),
|
||||
cancelable: false,
|
||||
});
|
||||
|
||||
// TriggerEvent constructor/args processor mapping
|
||||
const EVENT_TYPES = {
|
||||
auxclick: { constructor: MouseEvent, processParameters: mouseEventMapping },
|
||||
click: { constructor: MouseEvent, processParameters: mouseEventMapping },
|
||||
contextmenu: {
|
||||
constructor: MouseEvent,
|
||||
processParameters: mouseEventMapping,
|
||||
},
|
||||
dblclick: { constructor: MouseEvent, processParameters: mouseEventMapping },
|
||||
mousedown: { constructor: MouseEvent, processParameters: mouseEventMapping },
|
||||
mouseup: { constructor: MouseEvent, processParameters: mouseEventMapping },
|
||||
mousemove: { constructor: MouseEvent, processParameters: mouseEventMapping },
|
||||
mouseenter: {
|
||||
constructor: MouseEvent,
|
||||
processParameters: mouseEventNoBubble,
|
||||
},
|
||||
mouseleave: {
|
||||
constructor: MouseEvent,
|
||||
processParameters: mouseEventNoBubble,
|
||||
},
|
||||
mouseover: { constructor: MouseEvent, processParameters: mouseEventMapping },
|
||||
mouseout: { constructor: MouseEvent, processParameters: mouseEventMapping },
|
||||
focus: { constructor: FocusEvent, processParameters: noBubble },
|
||||
focusin: { constructor: FocusEvent, processParameters: onlyBubble },
|
||||
blur: { constructor: FocusEvent, processParameters: noBubble },
|
||||
cut: { constructor: ClipboardEvent, processParameters: onlyBubble },
|
||||
copy: { constructor: ClipboardEvent, processParameters: onlyBubble },
|
||||
paste: { constructor: ClipboardEvent, processParameters: onlyBubble },
|
||||
keydown: {
|
||||
constructor: KeyboardEvent,
|
||||
processParameters: keyboardEventBubble,
|
||||
},
|
||||
keypress: {
|
||||
constructor: KeyboardEvent,
|
||||
processParameters: keyboardEventBubble,
|
||||
},
|
||||
keyup: { constructor: KeyboardEvent, processParameters: keyboardEventBubble },
|
||||
drag: { constructor: DragEvent, processParameters: onlyBubble },
|
||||
dragend: { constructor: DragEvent, processParameters: onlyBubble },
|
||||
dragenter: { constructor: DragEvent, processParameters: onlyBubble },
|
||||
dragstart: { constructor: DragEvent, processParameters: onlyBubble },
|
||||
dragleave: { constructor: DragEvent, processParameters: onlyBubble },
|
||||
dragover: { constructor: DragEvent, processParameters: onlyBubble },
|
||||
drop: { constructor: DragEvent, processParameters: onlyBubble },
|
||||
input: { constructor: InputEvent, processParameters: onlyBubble },
|
||||
compositionstart: {
|
||||
constructor: CompositionEvent,
|
||||
processParameters: onlyBubble,
|
||||
},
|
||||
compositionend: {
|
||||
constructor: CompositionEvent,
|
||||
processParameters: onlyBubble,
|
||||
},
|
||||
/** @param {EventInit} [args] */
|
||||
const mapKeyboardEvent = (args) => ({
|
||||
...args,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
});
|
||||
|
||||
/**
|
||||
* @template {typeof Event} T
|
||||
* @param {EventType} eventType
|
||||
* @returns {[T, (attrs: EventInit) => EventInit]}
|
||||
*/
|
||||
const getEventConstructor = (eventType) => {
|
||||
switch (eventType) {
|
||||
// Mouse events
|
||||
case "auxclick":
|
||||
case "click":
|
||||
case "contextmenu":
|
||||
case "dblclick":
|
||||
case "mousedown":
|
||||
case "mouseup":
|
||||
case "mousemove":
|
||||
case "mouseover":
|
||||
case "mouseout": {
|
||||
return [MouseEvent, mapBubblingPointerEvent];
|
||||
}
|
||||
case "mouseenter":
|
||||
case "mouseleave": {
|
||||
return [MouseEvent, mapNonBubblingPointerEvent];
|
||||
}
|
||||
// Pointer events
|
||||
case "pointerdown":
|
||||
case "pointerup":
|
||||
case "pointermove":
|
||||
case "pointerover":
|
||||
case "pointerout": {
|
||||
return [PointerEvent, mapBubblingPointerEvent];
|
||||
}
|
||||
case "pointerenter":
|
||||
case "pointerleave": {
|
||||
return [PointerEvent, mapNonBubblingPointerEvent];
|
||||
}
|
||||
// Focus events
|
||||
case "focusin": {
|
||||
return [FocusEvent, mapBubblingEvent];
|
||||
}
|
||||
case "focus":
|
||||
case "blur": {
|
||||
return [FocusEvent, mapNonBubblingEvent];
|
||||
}
|
||||
// Clipboard events
|
||||
case "cut":
|
||||
case "copy":
|
||||
case "paste": {
|
||||
return [ClipboardEvent, mapBubblingEvent];
|
||||
}
|
||||
// Keyboard events
|
||||
case "keydown":
|
||||
case "keypress":
|
||||
case "keyup": {
|
||||
return [KeyboardEvent, mapKeyboardEvent];
|
||||
}
|
||||
// Drag events
|
||||
case "drag":
|
||||
case "dragend":
|
||||
case "dragenter":
|
||||
case "dragstart":
|
||||
case "dragleave":
|
||||
case "dragover":
|
||||
case "drop": {
|
||||
return [DragEvent, mapBubblingEvent];
|
||||
}
|
||||
// Input events
|
||||
case "input": {
|
||||
return [InputEvent, mapBubblingEvent];
|
||||
}
|
||||
// Composition events
|
||||
case "compositionstart":
|
||||
case "compositionend": {
|
||||
return [CompositionEvent, mapBubblingEvent];
|
||||
}
|
||||
// UI events
|
||||
case "scroll": {
|
||||
return [UIEvent, mapNonBubblingEvent];
|
||||
}
|
||||
// Touch events
|
||||
case "touchstart":
|
||||
case "touchend":
|
||||
case "touchmove": {
|
||||
return [TouchEvent, mapCancelableTouchEvent];
|
||||
}
|
||||
case "touchcancel": {
|
||||
return [TouchEvent, mapNonCancelableTouchEvent];
|
||||
}
|
||||
// Default: base Event constructor
|
||||
default: {
|
||||
return [Event, mapBubblingEvent];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (typeof TouchEvent === "function") {
|
||||
Object.assign(EVENT_TYPES, {
|
||||
touchstart: {
|
||||
constructor: TouchEvent,
|
||||
processParameters: touchEventMapping,
|
||||
},
|
||||
touchend: { constructor: TouchEvent, processParameters: touchEventMapping },
|
||||
touchmove: {
|
||||
constructor: TouchEvent,
|
||||
processParameters: touchEventMapping,
|
||||
},
|
||||
touchcancel: {
|
||||
constructor: TouchEvent,
|
||||
processParameters: touchEventCancelMapping,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function _makeEvent(eventType, eventAttrs) {
|
||||
let event;
|
||||
if (eventType in EVENT_TYPES) {
|
||||
const { constructor, processParameters } = EVENT_TYPES[eventType];
|
||||
event = new constructor(eventType, processParameters(eventAttrs));
|
||||
} else {
|
||||
event = new Event(eventType, Object.assign({}, eventAttrs, { bubbles: true }));
|
||||
}
|
||||
return event;
|
||||
}
|
||||
|
||||
export function triggerEvent(el, selector, eventType, eventAttrs = {}, options = {}) {
|
||||
const event = _makeEvent(eventType, eventAttrs);
|
||||
/**
|
||||
* @template {EventType} T
|
||||
* @param {Element} el
|
||||
* @param {string | null | undefined | false} selector
|
||||
* @param {T} eventType
|
||||
* @param {EventInit} [eventInit]
|
||||
* @param {TriggerEventOptions} [options={}]
|
||||
* @returns {GlobalEventHandlersEventMap[T] | Promise<GlobalEventHandlersEventMap[T]>}
|
||||
*/
|
||||
export function triggerEvent(el, selector, eventType, eventInit, options = {}) {
|
||||
const errors = [];
|
||||
const target = findElement(el, selector);
|
||||
|
||||
// Error handling
|
||||
if (typeof eventType !== "string") {
|
||||
errors.push("event type must be a string");
|
||||
}
|
||||
if (!target) {
|
||||
throw new Error(`Can't find a target to trigger ${eventType} event`);
|
||||
errors.push("cannot find target");
|
||||
} else if (!options.skipVisibilityCheck && !isVisible(target)) {
|
||||
errors.push("target is not visible");
|
||||
}
|
||||
if (!options.skipVisibilityCheck) {
|
||||
if (!isVisible(target)) {
|
||||
throw new Error(`Called triggerEvent ${eventType} on invisible target`);
|
||||
}
|
||||
if (errors.length) {
|
||||
throw new Error(
|
||||
`Cannot trigger event${eventType ? ` "${eventType}"` : ""}${
|
||||
selector ? ` (with selector "${selector}")` : ""
|
||||
}: ${errors.join(" and ")}`
|
||||
);
|
||||
}
|
||||
|
||||
// Actual dispatch
|
||||
const [Constructor, processParams] = getEventConstructor(eventType);
|
||||
const event = new Constructor(eventType, processParams(eventInit));
|
||||
target.dispatchEvent(event);
|
||||
if (!options.fast) {
|
||||
|
||||
if (window.QUnit && QUnit.config.debug) {
|
||||
const group = `%c[${event.type.toUpperCase()}]`;
|
||||
console.groupCollapsed(group, "color: #b52c9b");
|
||||
console.log(target, event);
|
||||
console.groupEnd(group, "color: #b52c9b");
|
||||
}
|
||||
|
||||
if (options.sync) {
|
||||
return event;
|
||||
} else {
|
||||
return nextTick().then(() => event);
|
||||
}
|
||||
return event;
|
||||
}
|
||||
|
||||
export async function triggerEvents(el, querySelector, events, options) {
|
||||
for (let e = 0; e < events.length; e++) {
|
||||
if (Array.isArray(events[e])) {
|
||||
triggerEvent(el, querySelector, events[e][0], events[e][1], options);
|
||||
} else {
|
||||
triggerEvent(el, querySelector, events[e], {}, options);
|
||||
}
|
||||
/**
|
||||
* @param {Element} el
|
||||
* @param {string | null | undefined | false} selector
|
||||
* @param {(EventType | [EventType, EventInit])[]} [eventDefs]
|
||||
* @param {TriggerEventOptions} [options={}]
|
||||
*/
|
||||
export function triggerEvents(el, selector, eventDefs, options = {}) {
|
||||
const events = [...eventDefs].map((eventDef) => {
|
||||
const [eventType, eventInit] = Array.isArray(eventDef) ? eventDef : [eventDef, {}];
|
||||
return triggerEvent(el, selector, eventType, eventInit, options);
|
||||
});
|
||||
if (options.sync) {
|
||||
return events;
|
||||
} else {
|
||||
return nextTick().then(() => events);
|
||||
}
|
||||
await nextTick();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -353,11 +414,11 @@ export async function triggerEvents(el, querySelector, events, options) {
|
|||
* the end of the scrollable area, the event can be transmitted
|
||||
* to its nearest parent until it can be triggered
|
||||
*
|
||||
* @param {HTMLElement} target target of the scroll event
|
||||
* @param {Element} target target of the scroll event
|
||||
* @param {Object} coordinates
|
||||
* @param {Number} coordinates[left] coordinates to scroll horizontally
|
||||
* @param {Number} coordinates[top] coordinates to scroll vertically
|
||||
* @param {Boolean} canPropagate states if the scroll can propagate to a scrollable parent
|
||||
* @param {number} coordinates.left coordinates to scroll horizontally
|
||||
* @param {number} coordinates.top coordinates to scroll vertically
|
||||
* @param {boolean} canPropagate states if the scroll can propagate to a scrollable parent
|
||||
*/
|
||||
export async function triggerScroll(
|
||||
target,
|
||||
|
|
@ -389,33 +450,52 @@ export async function triggerScroll(
|
|||
}
|
||||
});
|
||||
target.scrollTo(scrollCoordinates);
|
||||
target.dispatchEvent(new UIEvent("scroll"));
|
||||
await nextTick();
|
||||
await triggerEvent(target, null, "scroll");
|
||||
if (!canPropagate || !Object.entries(coordinates).length) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
target.parentElement
|
||||
? triggerScroll(target.parentElement, coordinates)
|
||||
: window.dispatchEvent(new UIEvent("scroll"));
|
||||
: triggerEvent(window, null, "scroll");
|
||||
await nextTick();
|
||||
}
|
||||
|
||||
export function click(el, selector, skipVisibilityCheck = false) {
|
||||
return triggerEvent(
|
||||
export function click(
|
||||
el,
|
||||
selector,
|
||||
{ mouseEventInit = {}, skipDisabledCheck = false, skipVisibilityCheck = false } = {}
|
||||
) {
|
||||
if (!skipDisabledCheck && el.disabled) {
|
||||
throw new Error("Can't click on a disabled button");
|
||||
}
|
||||
return triggerEvents(
|
||||
el,
|
||||
selector,
|
||||
"click",
|
||||
{ bubbles: true, cancelable: true },
|
||||
["pointerdown", "mousedown", "focus", "pointerup", "mouseup", ["click", mouseEventInit]],
|
||||
{ skipVisibilityCheck }
|
||||
);
|
||||
}
|
||||
|
||||
export function clickCreate(htmlElement) {
|
||||
if (htmlElement.querySelectorAll(".o_form_button_create").length) {
|
||||
return click(htmlElement, ".o_form_button_create");
|
||||
} else if (htmlElement.querySelectorAll(".o_list_button_create").length) {
|
||||
return click(htmlElement, ".o_list_button_create");
|
||||
if (
|
||||
htmlElement.querySelectorAll(
|
||||
".o_control_panel_main_buttons .d-none.d-xl-inline-flex .o_form_button_create"
|
||||
).length
|
||||
) {
|
||||
return click(
|
||||
htmlElement,
|
||||
".o_control_panel_main_buttons .d-none.d-xl-inline-flex .o_form_button_create"
|
||||
);
|
||||
} else if (
|
||||
htmlElement.querySelectorAll(
|
||||
".o_control_panel_main_buttons .d-none.d-xl-inline-flex .o_list_button_create"
|
||||
).length
|
||||
) {
|
||||
return click(
|
||||
htmlElement,
|
||||
".o_control_panel_main_buttons .d-none.d-xl-inline-flex .o_list_button_create"
|
||||
);
|
||||
} else {
|
||||
throw new Error("No edit button found to be clicked.");
|
||||
}
|
||||
|
|
@ -435,8 +515,10 @@ export async function clickSave(htmlElement) {
|
|||
}
|
||||
if (htmlElement.querySelectorAll(".o_form_button_save").length) {
|
||||
return click(htmlElement, ".o_form_button_save");
|
||||
} else if (htmlElement.querySelectorAll(".o_list_button_save").length) {
|
||||
return click(htmlElement, ".o_list_button_save");
|
||||
}
|
||||
const listSaveButtons = htmlElement.querySelectorAll(".o_list_button_save");
|
||||
if (listSaveButtons.length) {
|
||||
return listSaveButtons.length >= 2 ? click(listSaveButtons[1]) : click(listSaveButtons[0]);
|
||||
} else {
|
||||
throw new Error("No save button found to be clicked.");
|
||||
}
|
||||
|
|
@ -448,19 +530,19 @@ export async function clickDiscard(htmlElement) {
|
|||
}
|
||||
if (htmlElement.querySelectorAll(".o_form_button_cancel").length) {
|
||||
return click(htmlElement, ".o_form_button_cancel");
|
||||
} else if (htmlElement.querySelectorAll(".o_list_button_discard").length) {
|
||||
return click(htmlElement, ".o_list_button_discard");
|
||||
} else if ($(htmlElement).find(".o_list_button_discard:visible").length) {
|
||||
return click($(htmlElement).find(".o_list_button_discard:visible").get(0));
|
||||
} else {
|
||||
throw new Error("No discard button found to be clicked.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers a mouseenter event on the given target. If no
|
||||
* Trigger pointerenter and mouseenter events on the given target. If no
|
||||
* coordinates are given, the event is located by default
|
||||
* in the middle of the target to simplify the test process
|
||||
*
|
||||
* @param {HTMLElement} el
|
||||
* @param {Element} el
|
||||
* @param {string} selector
|
||||
* @param {Object} coordinates position of the mouseenter event
|
||||
*/
|
||||
|
|
@ -470,7 +552,18 @@ export async function mouseEnter(el, selector, coordinates) {
|
|||
clientX: target.getBoundingClientRect().left + target.getBoundingClientRect().width / 2,
|
||||
clientY: target.getBoundingClientRect().top + target.getBoundingClientRect().height / 2,
|
||||
};
|
||||
return triggerEvent(target, null, "mouseenter", atPos);
|
||||
return triggerEvents(target, null, ["pointerenter", "mouseenter"], atPos);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger pointerleave and mouseleave events on the given target.
|
||||
*
|
||||
* @param {Element} el
|
||||
* @param {string} selector
|
||||
*/
|
||||
export async function mouseLeave(el, selector) {
|
||||
const target = el.querySelector(selector) || el;
|
||||
return triggerEvents(target, null, ["pointerleave", "mouseleave"]);
|
||||
}
|
||||
|
||||
export async function editInput(el, selector, value) {
|
||||
|
|
@ -520,15 +613,24 @@ export function editSelect(el, selector, value) {
|
|||
return triggerEvent(select, null, "change");
|
||||
}
|
||||
|
||||
export async function editSelectMenu(el, selector, value) {
|
||||
const dropdown = el.querySelector(selector);
|
||||
await click(dropdown.querySelector(".dropdown-toggle"));
|
||||
for (const item of Array.from(dropdown.querySelectorAll(".dropdown-item"))) {
|
||||
if (item.textContent === value) {
|
||||
return click(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers an hotkey properly disregarding the operating system.
|
||||
*
|
||||
* @param {string} hotkey
|
||||
* @param {boolean} addOverlayModParts
|
||||
* @param {KeyboardEventInit} eventAttrs
|
||||
* @returns {{ keydownEvent: KeyboardEvent, keyupEvent: KeyboardEvent }}
|
||||
*/
|
||||
export function triggerHotkey(hotkey, addOverlayModParts = false, eventAttrs = {}) {
|
||||
export async function triggerHotkey(hotkey, addOverlayModParts = false, eventAttrs = {}) {
|
||||
eventAttrs.key = hotkey.split("+").pop();
|
||||
|
||||
if (/shift/i.test(hotkey)) {
|
||||
|
|
@ -555,15 +657,17 @@ export function triggerHotkey(hotkey, addOverlayModParts = false, eventAttrs = {
|
|||
eventAttrs.bubbles = true;
|
||||
}
|
||||
|
||||
const keydownEvent = new KeyboardEvent("keydown", eventAttrs);
|
||||
const keyupEvent = new KeyboardEvent("keyup", eventAttrs);
|
||||
document.activeElement.dispatchEvent(keydownEvent);
|
||||
document.activeElement.dispatchEvent(keyupEvent);
|
||||
return { keydownEvent, keyupEvent };
|
||||
}
|
||||
const [keydownEvent, keyupEvent] = await triggerEvents(
|
||||
document.activeElement,
|
||||
null,
|
||||
[
|
||||
["keydown", eventAttrs],
|
||||
["keyup", eventAttrs],
|
||||
],
|
||||
{ skipVisibilityCheck: true }
|
||||
);
|
||||
|
||||
export async function legacyExtraNextTick() {
|
||||
return nextTick();
|
||||
return { keydownEvent, keyupEvent };
|
||||
}
|
||||
|
||||
export function mockDownload(cb) {
|
||||
|
|
@ -622,21 +726,39 @@ export function mockTimeout() {
|
|||
|
||||
export function mockAnimationFrame() {
|
||||
const callbacks = new Map();
|
||||
let currentTime = 0;
|
||||
let id = 1;
|
||||
patchWithCleanup(browser, {
|
||||
requestAnimationFrame(fn) {
|
||||
callbacks.set(id, fn);
|
||||
callbacks.set(id, { fn, scheduledFor: 16 + currentTime, id });
|
||||
return id++;
|
||||
},
|
||||
cancelAnimationFrame(id) {
|
||||
callbacks.delete(id);
|
||||
},
|
||||
performance: { now: () => currentTime },
|
||||
});
|
||||
return function execRegisteredCallbacks() {
|
||||
for (const fn of callbacks.values()) {
|
||||
fn();
|
||||
}
|
||||
callbacks.clear();
|
||||
return {
|
||||
execRegisteredAnimationFrames() {
|
||||
for (const { fn } of callbacks.values()) {
|
||||
fn(currentTime);
|
||||
}
|
||||
callbacks.clear();
|
||||
},
|
||||
async advanceFrame(count = 1) {
|
||||
// wait here so all microtasktick scheduled in this frame can be
|
||||
// executed and possibly register their own timeout
|
||||
await nextTick();
|
||||
currentTime += 16 * count;
|
||||
for (const { fn, scheduledFor, id } of callbacks.values()) {
|
||||
if (scheduledFor <= currentTime) {
|
||||
fn(currentTime);
|
||||
callbacks.delete(id);
|
||||
}
|
||||
}
|
||||
// wait here to make sure owl can update the UI
|
||||
await nextTick();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -650,7 +772,7 @@ export async function mount(Comp, target, config = {}) {
|
|||
props,
|
||||
};
|
||||
if (env.services && "localization" in env.services) {
|
||||
configuration.translateFn = env._t;
|
||||
configuration.translateFn = _t;
|
||||
}
|
||||
const app = new App(Comp, configuration);
|
||||
registerCleanup(() => app.destroy());
|
||||
|
|
@ -685,29 +807,42 @@ export function useChild() {
|
|||
onPatched(setChild);
|
||||
}
|
||||
|
||||
const lifeCycleHooks = [
|
||||
"onError",
|
||||
"onMounted",
|
||||
"onPatched",
|
||||
"onRendered",
|
||||
"onWillDestroy",
|
||||
"onWillPatch",
|
||||
"onWillRender",
|
||||
"onWillStart",
|
||||
"onWillUnmount",
|
||||
"onWillUpdateProps",
|
||||
];
|
||||
export function useLogLifeCycle(logFn, name = "") {
|
||||
const component = owl.useComponent();
|
||||
const component = useComponent();
|
||||
let loggedName = `${component.constructor.name}`;
|
||||
if (name) {
|
||||
loggedName = `${component.constructor.name} ${name}`;
|
||||
}
|
||||
for (const hook of lifeCycleHooks) {
|
||||
owl[hook](() => {
|
||||
logFn(`${hook} ${loggedName}`);
|
||||
});
|
||||
}
|
||||
onError(() => {
|
||||
logFn(`onError ${loggedName}`);
|
||||
});
|
||||
onMounted(() => {
|
||||
logFn(`onMounted ${loggedName}`);
|
||||
});
|
||||
onPatched(() => {
|
||||
logFn(`onPatched ${loggedName}`);
|
||||
});
|
||||
onRendered(() => {
|
||||
logFn(`onRendered ${loggedName}`);
|
||||
});
|
||||
onWillDestroy(() => {
|
||||
logFn(`onWillDestroy ${loggedName}`);
|
||||
});
|
||||
onWillPatch(() => {
|
||||
logFn(`onWillPatch ${loggedName}`);
|
||||
});
|
||||
onWillRender(() => {
|
||||
logFn(`onWillRender ${loggedName}`);
|
||||
});
|
||||
onWillStart(() => {
|
||||
logFn(`onWillStart ${loggedName}`);
|
||||
});
|
||||
onWillUnmount(() => {
|
||||
logFn(`onWillUnmount ${loggedName}`);
|
||||
});
|
||||
onWillUpdateProps(() => {
|
||||
logFn(`onWillUpdateProps ${loggedName}`);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -732,10 +867,8 @@ function getDifferentParents(n1, n2) {
|
|||
/**
|
||||
* Helper performing a drag and drop sequence.
|
||||
*
|
||||
* - the 'fromSelector' is used to determine the element on which the drag will
|
||||
* start;
|
||||
* - the 'toSelector' will determine the element on which the first one will be
|
||||
* dropped.
|
||||
* - 'from' is used to determine the element on which the drag will start;
|
||||
* - 'target' will determine the element on which the first one will be dropped.
|
||||
*
|
||||
* The first element will be dragged by its center, and will be dropped on the
|
||||
* bottom-right inner pixel of the target element. This behavior covers both
|
||||
|
|
@ -749,14 +882,13 @@ function getDifferentParents(n1, n2) {
|
|||
* Note that only the last event is awaited, since all the others are
|
||||
* considered to be synchronous.
|
||||
*
|
||||
* @param {Element|string} from
|
||||
* @param {Element|string} to
|
||||
* @param {string} [position] "top" | "bottom" | "left" | "right"
|
||||
* @returns {Promise<void>}
|
||||
* @param {Element | string} from
|
||||
* @param {Element | string} to
|
||||
* @param {Position} [position]
|
||||
*/
|
||||
export async function dragAndDrop(from, to, position) {
|
||||
const dropFunction = drag(from, to, position);
|
||||
await dropFunction();
|
||||
const { drop } = await drag(from);
|
||||
await drop(to, position);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -764,73 +896,132 @@ export async function dragAndDrop(from, to, position) {
|
|||
*
|
||||
* - the 'from' selector is used to determine the element on which the drag will
|
||||
* start;
|
||||
* - the 'to' selector will determine the element on which the dragged element will be
|
||||
* - the 'target' selector will determine the element on which the dragged element will be
|
||||
* moved.
|
||||
*
|
||||
* Returns a drop function
|
||||
* @param {Element|string} from
|
||||
* @param {Element|string} to
|
||||
* @param {string} [position] "top" | "bottom" | "left" | "right"
|
||||
* @returns {function: Promise<void>}
|
||||
*
|
||||
* @param {Element | string} from
|
||||
*/
|
||||
export function drag(from, to, position) {
|
||||
const fixture = getFixture();
|
||||
from = from instanceof Element ? from : fixture.querySelector(from);
|
||||
to = to instanceof Element ? to : fixture.querySelector(to);
|
||||
|
||||
// Mouse down on main target
|
||||
const fromRect = from.getBoundingClientRect();
|
||||
const toRect = to.getBoundingClientRect();
|
||||
triggerEvent(from, null, "mousedown", {
|
||||
clientX: fromRect.x + fromRect.width / 2,
|
||||
clientY: fromRect.y + fromRect.height / 2,
|
||||
});
|
||||
|
||||
// Find target position
|
||||
const toPos = {
|
||||
clientX: toRect.x + toRect.width / 2,
|
||||
clientY: toRect.y + toRect.height / 2,
|
||||
export async function drag(from, pointerType = "mouse") {
|
||||
const assertIsDragging = (fn, endDrag) => {
|
||||
return {
|
||||
async [fn.name](...args) {
|
||||
if (dragEndReason) {
|
||||
throw new Error(
|
||||
`Cannot execute drag helper '${fn.name}': drag sequence has been ended by '${dragEndReason}'.`
|
||||
);
|
||||
}
|
||||
await fn(...args);
|
||||
if (endDrag) {
|
||||
dragEndReason = fn.name;
|
||||
}
|
||||
},
|
||||
}[fn.name];
|
||||
};
|
||||
if (position && typeof position === "object") {
|
||||
// x and y coordinates start from the element's initial coordinates
|
||||
toPos.clientX += position.x || 0;
|
||||
toPos.clientY += position.y || 0;
|
||||
} else {
|
||||
switch (position) {
|
||||
case "top": {
|
||||
toPos.clientY = toRect.y - 1;
|
||||
break;
|
||||
|
||||
const cancel = assertIsDragging(async function cancel() {
|
||||
await triggerEvent(window, null, "keydown", { key: "Escape" });
|
||||
}, true);
|
||||
|
||||
/**
|
||||
* @param {Element | string} [to]
|
||||
* @param {Position} [position]
|
||||
*/
|
||||
const drop = assertIsDragging(async function drop(to, position) {
|
||||
if (to) {
|
||||
await moveTo(to, position);
|
||||
}
|
||||
await triggerEvent(target || source, null, "pointerup", targetPosition);
|
||||
}, true);
|
||||
|
||||
/**
|
||||
* @param {Element | string} selector
|
||||
*/
|
||||
const getEl = (selector) =>
|
||||
selector instanceof Element ? selector : fixture.querySelector(selector);
|
||||
|
||||
/**
|
||||
* @param {Position} [position]
|
||||
*/
|
||||
const getTargetPosition = (position) => {
|
||||
const tRect = target.getBoundingClientRect();
|
||||
const tPos = {
|
||||
clientX: Math.floor(tRect.x),
|
||||
clientY: Math.floor(tRect.y),
|
||||
};
|
||||
if (position && typeof position === "object") {
|
||||
// x and y coordinates start from the element's initial coordinates
|
||||
tPos.clientX += position.x || 0;
|
||||
tPos.clientY += position.y || 0;
|
||||
} else {
|
||||
const positions = typeof position === "string" ? position.split("-") : [];
|
||||
|
||||
// X position
|
||||
if (positions.includes("left")) {
|
||||
tPos.clientX -= 1;
|
||||
} else if (positions.includes("right")) {
|
||||
tPos.clientX += Math.ceil(tRect.width) + 1;
|
||||
} else {
|
||||
tPos.clientX += Math.floor(tRect.width / 2);
|
||||
}
|
||||
case "bottom": {
|
||||
toPos.clientY = toRect.y + toRect.height + 1;
|
||||
break;
|
||||
}
|
||||
case "left": {
|
||||
toPos.clientX = toRect.x - 1;
|
||||
break;
|
||||
}
|
||||
case "right": {
|
||||
toPos.clientX = toRect.x + toRect.width + 1;
|
||||
break;
|
||||
|
||||
// Y position
|
||||
if (positions.includes("top")) {
|
||||
tPos.clientY -= 1;
|
||||
} else if (positions.includes("bottom")) {
|
||||
tPos.clientY += Math.ceil(tRect.height) + 1;
|
||||
} else {
|
||||
tPos.clientY += Math.floor(tRect.height / 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Move, enter and drop the element on the target
|
||||
triggerEvent(window, null, "mousemove", toPos);
|
||||
// "mouseenter" is fired on every parent of `to` that do not contain
|
||||
// `from` (typically: different parent lists).
|
||||
for (const target of getDifferentParents(from, to)) {
|
||||
triggerEvent(target, null, "mouseenter", toPos);
|
||||
}
|
||||
|
||||
return function () {
|
||||
return drop(from, toPos);
|
||||
return tPos;
|
||||
};
|
||||
}
|
||||
|
||||
function drop(from, toPos) {
|
||||
return triggerEvent(from, null, "mouseup", toPos);
|
||||
/**
|
||||
* @param {Element | string} [to]
|
||||
* @param {Position} [position]
|
||||
*/
|
||||
const moveTo = assertIsDragging(async function moveTo(to, position) {
|
||||
target = getEl(to);
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Recompute target position
|
||||
targetPosition = getTargetPosition(position);
|
||||
|
||||
// Move, enter and drop the element on the target
|
||||
await triggerEvent(source, null, "pointermove", targetPosition);
|
||||
|
||||
// "pointerenter" is fired on every parent of `target` that do not contain
|
||||
// `from` (typically: different parent lists).
|
||||
for (const parent of getDifferentParents(source, target)) {
|
||||
triggerEvent(parent, null, "pointerenter", targetPosition);
|
||||
}
|
||||
await nextTick();
|
||||
|
||||
return dragHelpers;
|
||||
}, false);
|
||||
|
||||
const dragHelpers = { cancel, drop, moveTo };
|
||||
const fixture = getFixture();
|
||||
|
||||
const source = getEl(from instanceof Element ? from : fixture.querySelector(from));
|
||||
const sourceRect = source.getBoundingClientRect();
|
||||
|
||||
let dragEndReason = null;
|
||||
let target;
|
||||
let targetPosition;
|
||||
|
||||
// Pointer down on main target
|
||||
await triggerEvent(source, null, "pointerdown", {
|
||||
pointerType,
|
||||
clientX: sourceRect.x + sourceRect.width / 2,
|
||||
clientY: sourceRect.y + sourceRect.height / 2,
|
||||
});
|
||||
|
||||
return dragHelpers;
|
||||
}
|
||||
|
||||
export async function clickDropdown(target, fieldName) {
|
||||
|
|
@ -854,7 +1045,7 @@ export async function clickOpenedDropdownItem(target, fieldName, itemContent) {
|
|||
if (indexToClick === -1) {
|
||||
throw new Error(`The element '${itemContent}' does not exist in the dropdown`);
|
||||
}
|
||||
await click(dropdownItems[indexToClick], null, "click");
|
||||
await click(dropdownItems[indexToClick]);
|
||||
}
|
||||
|
||||
export async function selectDropdownItem(target, fieldName, itemContent) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue