mirror of
https://github.com/bringout/oca-ocb-core.git
synced 2026-04-21 10:12:04 +02:00
vanilla 19.0
This commit is contained in:
parent
991d2234ca
commit
d1963a3c3a
3066 changed files with 1651266 additions and 922560 deletions
175
odoo-bringout-oca-ocb-web/web/static/lib/hoot/mock/animation.js
Normal file
175
odoo-bringout-oca-ocb-web/web/static/lib/hoot/mock/animation.js
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import { on } from "@odoo/hoot-dom";
|
||||
import { MockEventTarget } from "../hoot_utils";
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Global
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
const {
|
||||
Array: { isArray: $isArray },
|
||||
Element,
|
||||
Object: { assign: $assign, entries: $entries },
|
||||
scroll: windowScroll,
|
||||
scrollBy: windowScrollBy,
|
||||
scrollTo: windowScrollTo,
|
||||
} = globalThis;
|
||||
|
||||
const { animate, scroll, scrollBy, scrollIntoView, scrollTo } = Element.prototype;
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Internal
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
function forceInstantScroll(args) {
|
||||
return !allowAnimations && args[0] && typeof args[0] === "object"
|
||||
? [{ ...args[0], behavior: "instant" }, ...args.slice(1)]
|
||||
: args;
|
||||
}
|
||||
|
||||
const animationChangeBus = new MockEventTarget();
|
||||
const animationChangeCleanups = [];
|
||||
|
||||
let allowAnimations = true;
|
||||
let allowTransitions = false;
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Exports
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
export class MockAnimation extends MockEventTarget {
|
||||
static publicListeners = ["cancel", "finish", "remove"];
|
||||
|
||||
currentTime = null;
|
||||
effect = null;
|
||||
finished = Promise.resolve(this);
|
||||
id = "";
|
||||
pending = false;
|
||||
playState = "idle";
|
||||
playbackRate = 1;
|
||||
ready = Promise.resolve(this);
|
||||
replaceState = "active";
|
||||
startTime = null;
|
||||
timeline = {
|
||||
currentTime: this.currentTime,
|
||||
duration: null,
|
||||
};
|
||||
|
||||
cancel() {
|
||||
this.dispatchEvent(new AnimationPlaybackEvent("cancel"));
|
||||
}
|
||||
|
||||
commitStyles() {}
|
||||
|
||||
finish() {
|
||||
this.dispatchEvent(new AnimationPlaybackEvent("finish"));
|
||||
}
|
||||
|
||||
pause() {}
|
||||
|
||||
persist() {}
|
||||
|
||||
play() {
|
||||
this.dispatchEvent(new AnimationPlaybackEvent("finish"));
|
||||
}
|
||||
|
||||
reverse() {}
|
||||
|
||||
updatePlaybackRate() {}
|
||||
}
|
||||
|
||||
export function cleanupAnimations() {
|
||||
allowAnimations = true;
|
||||
allowTransitions = false;
|
||||
|
||||
while (animationChangeCleanups.length) {
|
||||
animationChangeCleanups.pop()();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns off all animations triggered programmatically (such as with `animate`),
|
||||
* as well as smooth scrolls.
|
||||
*
|
||||
* @param {boolean} [enable=false]
|
||||
*/
|
||||
export function disableAnimations(enable = false) {
|
||||
allowAnimations = enable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores all suppressed "animation" and "transition" properties for the current
|
||||
* test, as they are turned off by default.
|
||||
*
|
||||
* @param {boolean} [enable=true]
|
||||
*/
|
||||
export function enableTransitions(enable = true) {
|
||||
allowTransitions = enable;
|
||||
animationChangeBus.dispatchEvent(new CustomEvent("toggle-transitions"));
|
||||
}
|
||||
|
||||
/** @type {Element["animate"]} */
|
||||
export function mockedAnimate(...args) {
|
||||
if (allowAnimations) {
|
||||
return animate.call(this, ...args);
|
||||
}
|
||||
|
||||
// Apply style properties immediatly
|
||||
const keyframesList = $isArray(args[0]) ? args[0] : [args[0]];
|
||||
const style = {};
|
||||
for (const kf of keyframesList) {
|
||||
for (const [key, value] of $entries(kf)) {
|
||||
style[key] = $isArray(value) ? value.at(-1) : value;
|
||||
}
|
||||
}
|
||||
$assign(this.style, style);
|
||||
|
||||
// Return mock animation
|
||||
return new MockAnimation();
|
||||
}
|
||||
|
||||
/** @type {Element["scroll"]} */
|
||||
export function mockedScroll(...args) {
|
||||
return scroll.call(this, ...forceInstantScroll(args));
|
||||
}
|
||||
|
||||
/** @type {Element["scrollBy"]} */
|
||||
export function mockedScrollBy(...args) {
|
||||
return scrollBy.call(this, ...forceInstantScroll(args));
|
||||
}
|
||||
|
||||
/** @type {Element["scrollIntoView"]} */
|
||||
export function mockedScrollIntoView(...args) {
|
||||
return scrollIntoView.call(this, ...forceInstantScroll(args));
|
||||
}
|
||||
|
||||
/** @type {Element["scrollTo"]} */
|
||||
export function mockedScrollTo(...args) {
|
||||
return scrollTo.call(this, ...forceInstantScroll(args));
|
||||
}
|
||||
|
||||
/** @type {typeof window["scroll"]} */
|
||||
export function mockedWindowScroll(...args) {
|
||||
return windowScroll.call(this, ...forceInstantScroll(args));
|
||||
}
|
||||
|
||||
/** @type {typeof window["scrollBy"]} */
|
||||
export function mockedWindowScrollBy(...args) {
|
||||
return windowScrollBy.call(this, ...forceInstantScroll(args));
|
||||
}
|
||||
|
||||
/** @type {typeof window["scrollTo"]} */
|
||||
export function mockedWindowScrollTo(...args) {
|
||||
return windowScrollTo.call(this, ...forceInstantScroll(args));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {(allowTransitions: boolean) => any} onChange
|
||||
*/
|
||||
export function subscribeToTransitionChange(onChange) {
|
||||
onChange(allowTransitions);
|
||||
animationChangeCleanups.push(
|
||||
on(animationChangeBus, "toggle-transitions", () => onChange(allowTransitions))
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import { MockEventTarget } from "../hoot_utils";
|
||||
import { logger } from "../core/logger";
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Global
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
const {
|
||||
console,
|
||||
Object: { keys: $keys },
|
||||
} = globalThis;
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Internal
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
const DISPATCHING_METHODS = ["error", "trace", "warn"];
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Exports
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
export class MockConsole extends MockEventTarget {
|
||||
static {
|
||||
for (const fnName of $keys(console)) {
|
||||
if (DISPATCHING_METHODS.includes(fnName)) {
|
||||
const fn = logger[fnName];
|
||||
this.prototype[fnName] = function (...args) {
|
||||
this.dispatchEvent(new CustomEvent(fnName, { detail: args }));
|
||||
return fn.apply(this, arguments);
|
||||
};
|
||||
} else {
|
||||
this.prototype[fnName] = console[fnName];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
22
odoo-bringout-oca-ocb-web/web/static/lib/hoot/mock/crypto.js
Normal file
22
odoo-bringout-oca-ocb-web/web/static/lib/hoot/mock/crypto.js
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
/** @odoo-module */
|
||||
|
||||
export const mockCrypto = {
|
||||
subtle: {
|
||||
importKey: (_format, keyData, _algorithm, _extractable, _keyUsages) => {
|
||||
if (!keyData || keyData.length === 0) {
|
||||
throw Error(`KeyData is mandatory`);
|
||||
}
|
||||
return Promise.resolve("I'm a key");
|
||||
},
|
||||
encrypt: (_algorithm, _key, data) =>
|
||||
Promise.resolve(`encrypted data:${new TextDecoder().decode(data)}`),
|
||||
decrypt: (_algorithm, _key, data) =>
|
||||
Promise.resolve(new TextEncoder().encode(data.replace("encrypted data:", ""))),
|
||||
},
|
||||
getRandomValues: (typedArray) => {
|
||||
typedArray.forEach((_element, index) => {
|
||||
typedArray[index] = Math.round(Math.random() * 100);
|
||||
});
|
||||
return typedArray;
|
||||
},
|
||||
};
|
||||
253
odoo-bringout-oca-ocb-web/web/static/lib/hoot/mock/date.js
Normal file
253
odoo-bringout-oca-ocb-web/web/static/lib/hoot/mock/date.js
Normal file
|
|
@ -0,0 +1,253 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import { getTimeOffset, isTimeFrozen, resetTimeOffset } from "@web/../lib/hoot-dom/helpers/time";
|
||||
import { createMock, HootError, isNil } from "../hoot_utils";
|
||||
|
||||
/**
|
||||
* @typedef DateSpecs
|
||||
* @property {number} [year]
|
||||
* @property {number} [month] // 1-12
|
||||
* @property {number} [day] // 1-31
|
||||
* @property {number} [hour] // 0-23
|
||||
* @property {number} [minute] // 0-59
|
||||
* @property {number} [second] // 0-59
|
||||
* @property {number} [millisecond] // 0-999
|
||||
*/
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Global
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
const { Date, Intl } = globalThis;
|
||||
const { now: $now, UTC: $UTC } = Date;
|
||||
const { DateTimeFormat, Locale } = Intl;
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Internal
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @param {Date} baseDate
|
||||
*/
|
||||
function computeTimeZoneOffset(baseDate) {
|
||||
const utcDate = new Date(baseDate.toLocaleString(DEFAULT_LOCALE, { timeZone: "UTC" }));
|
||||
const tzDate = new Date(baseDate.toLocaleString(DEFAULT_LOCALE, { timeZone: timeZoneName }));
|
||||
return (utcDate - tzDate) / 60000; // in minutes
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} id
|
||||
*/
|
||||
function getDateParams() {
|
||||
return [...dateParams.slice(0, -1), dateParams.at(-1) + getTimeStampDiff() + getTimeOffset()];
|
||||
}
|
||||
|
||||
function getTimeStampDiff() {
|
||||
return isTimeFrozen() ? 0 : $now() - dateTimeStamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string | DateSpecs} dateSpecs
|
||||
*/
|
||||
function parseDateParams(dateSpecs) {
|
||||
/** @type {DateSpecs} */
|
||||
const specs =
|
||||
(typeof dateSpecs === "string" ? dateSpecs.match(DATE_REGEX)?.groups : dateSpecs) || {};
|
||||
return [
|
||||
specs.year ?? DEFAULT_DATE[0],
|
||||
(specs.month ?? DEFAULT_DATE[1]) - 1,
|
||||
specs.day ?? DEFAULT_DATE[2],
|
||||
specs.hour ?? DEFAULT_DATE[3],
|
||||
specs.minute ?? DEFAULT_DATE[4],
|
||||
specs.second ?? DEFAULT_DATE[5],
|
||||
specs.millisecond ?? DEFAULT_DATE[6],
|
||||
].map(Number);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {typeof dateParams} newDateParams
|
||||
*/
|
||||
function setDateParams(newDateParams) {
|
||||
dateParams = newDateParams;
|
||||
dateTimeStamp = $now();
|
||||
|
||||
resetTimeOffset();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string | number | null | undefined} tz
|
||||
*/
|
||||
function setTimeZone(tz) {
|
||||
if (typeof tz === "string") {
|
||||
if (!tz.includes("/")) {
|
||||
throw new HootError(`invalid time zone: must be in the format <Country/...Location>`);
|
||||
}
|
||||
|
||||
// Set TZ name
|
||||
timeZoneName = tz;
|
||||
// Set TZ offset based on name (must be computed for each date)
|
||||
timeZoneOffset = computeTimeZoneOffset;
|
||||
} else if (typeof tz === "number") {
|
||||
// Only set TZ offset
|
||||
timeZoneOffset = tz * -60;
|
||||
} else {
|
||||
// Reset both TZ name & offset
|
||||
timeZoneName = null;
|
||||
timeZoneOffset = null;
|
||||
}
|
||||
|
||||
for (const callback of timeZoneChangeCallbacks) {
|
||||
callback(tz ?? DEFAULT_TIMEZONE_NAME);
|
||||
}
|
||||
}
|
||||
|
||||
class MockDateTimeFormat extends DateTimeFormat {
|
||||
constructor(locales, options) {
|
||||
super(locales, {
|
||||
...options,
|
||||
timeZone: options?.timeZone ?? timeZoneName ?? DEFAULT_TIMEZONE_NAME,
|
||||
});
|
||||
}
|
||||
|
||||
/** @type {Intl.DateTimeFormat["format"]} */
|
||||
format(date) {
|
||||
return super.format(date || new MockDate());
|
||||
}
|
||||
|
||||
resolvedOptions() {
|
||||
return {
|
||||
...super.resolvedOptions(),
|
||||
timeZone: timeZoneName ?? DEFAULT_TIMEZONE_NAME,
|
||||
locale: locale ?? DEFAULT_LOCALE,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const DATE_REGEX =
|
||||
/(?<year>\d{4})[/-](?<month>\d{2})[/-](?<day>\d{2})([\sT]+(?<hour>\d{2}):(?<minute>\d{2}):(?<second>\d{2})(\.(?<millisecond>\d{3}))?)?/;
|
||||
const DEFAULT_DATE = [2019, 2, 11, 9, 30, 0, 0];
|
||||
const DEFAULT_LOCALE = "en-US";
|
||||
const DEFAULT_TIMEZONE_NAME = "Europe/Brussels";
|
||||
const DEFAULT_TIMEZONE_OFFSET = -60;
|
||||
|
||||
/** @type {((tz: string | number) => any)[]} */
|
||||
const timeZoneChangeCallbacks = [];
|
||||
|
||||
let dateParams = DEFAULT_DATE;
|
||||
let dateTimeStamp = $now();
|
||||
/** @type {string | null} */
|
||||
let locale = null;
|
||||
/** @type {string | null} */
|
||||
let timeZoneName = null;
|
||||
/** @type {number | ((date: Date) => number) | null} */
|
||||
let timeZoneOffset = null;
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Exports
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
export function cleanupDate() {
|
||||
setDateParams(DEFAULT_DATE);
|
||||
locale = null;
|
||||
timeZoneName = null;
|
||||
timeZoneOffset = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mocks the current date and time, and also the time zone if any.
|
||||
*
|
||||
* Date can either be an object describing the date and time to mock, or a
|
||||
* string in SQL or ISO format (time and millisecond values can be omitted).
|
||||
* @see {@link mockTimeZone} for the time zone params.
|
||||
*
|
||||
* @param {string | DateSpecs} [date]
|
||||
* @param {string | number | null} [tz]
|
||||
* @example
|
||||
* mockDate("2023-12-25T20:45:00"); // 2023-12-25 20:45:00 UTC
|
||||
* @example
|
||||
* mockDate({ year: 2023, month: 12, day: 25, hour: 20, minute: 45 }); // same as above
|
||||
* @example
|
||||
* mockDate("2019-02-11 09:30:00.001", +2);
|
||||
*/
|
||||
export function mockDate(date, tz) {
|
||||
setDateParams(date ? parseDateParams(date) : DEFAULT_DATE);
|
||||
if (!isNil(tz)) {
|
||||
setTimeZone(tz);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mocks the current locale.
|
||||
*
|
||||
* If the time zone hasn't been mocked already, it will be assigned to the first
|
||||
* time zone available in the given locale (if any).
|
||||
*
|
||||
* @param {string} newLocale
|
||||
* @example
|
||||
* mockTimeZone("ja-JP"); // UTC + 9
|
||||
*/
|
||||
export function mockLocale(newLocale) {
|
||||
locale = newLocale;
|
||||
|
||||
if (!isNil(locale) && isNil(timeZoneName)) {
|
||||
// Set TZ from locale (if not mocked already)
|
||||
const firstAvailableTZ = new Locale(locale).timeZones?.[0];
|
||||
if (!isNil(firstAvailableTZ)) {
|
||||
setTimeZone(firstAvailableTZ);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mocks the current time zone.
|
||||
*
|
||||
* Time zone can either be a time zone or an offset. Number offsets are expressed
|
||||
* in hours.
|
||||
*
|
||||
* @param {string | number | null} [tz]
|
||||
* @example
|
||||
* mockTimeZone(+10); // UTC + 10
|
||||
* @example
|
||||
* mockTimeZone("Europe/Brussels"); // UTC + 1 (or UTC + 2 in summer)
|
||||
* @example
|
||||
* mockTimeZone(null) // Resets to test default (+1)
|
||||
*/
|
||||
export function mockTimeZone(tz) {
|
||||
setTimeZone(tz);
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to changes made on the time zone (mocked) value.
|
||||
*
|
||||
* @param {(tz: string | number) => any} callback
|
||||
*/
|
||||
export function onTimeZoneChange(callback) {
|
||||
timeZoneChangeCallbacks.push(callback);
|
||||
}
|
||||
|
||||
export class MockDate extends Date {
|
||||
constructor(...args) {
|
||||
if (args.length === 1) {
|
||||
super(args[0]);
|
||||
} else {
|
||||
const params = getDateParams();
|
||||
for (let i = 0; i < params.length; i++) {
|
||||
args[i] ??= params[i];
|
||||
}
|
||||
super($UTC(...args));
|
||||
}
|
||||
}
|
||||
|
||||
getTimezoneOffset() {
|
||||
const offset = timeZoneOffset ?? DEFAULT_TIMEZONE_OFFSET;
|
||||
return typeof offset === "function" ? offset(this) : offset;
|
||||
}
|
||||
|
||||
static now() {
|
||||
return new MockDate().getTime();
|
||||
}
|
||||
}
|
||||
|
||||
export const MockIntl = createMock(Intl, {
|
||||
DateTimeFormat: { value: MockDateTimeFormat },
|
||||
});
|
||||
91
odoo-bringout-oca-ocb-web/web/static/lib/hoot/mock/math.js
Normal file
91
odoo-bringout-oca-ocb-web/web/static/lib/hoot/mock/math.js
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import { isNil, stringToNumber } from "../hoot_utils";
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Global
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
const {
|
||||
Math,
|
||||
Number: { isNaN: $isNaN, parseFloat: $parseFloat },
|
||||
Object: { defineProperties: $defineProperties },
|
||||
} = globalThis;
|
||||
const { floor: $floor, random: $random } = Math;
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Internal
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @param {unknown} [seed]
|
||||
*/
|
||||
function toValidSeed(seed) {
|
||||
if (isNil(seed)) {
|
||||
return generateSeed();
|
||||
}
|
||||
const nSeed = $parseFloat(seed);
|
||||
return $isNaN(nSeed) ? stringToNumber(nSeed) : nSeed;
|
||||
}
|
||||
|
||||
const DEFAULT_SEED = 1e16;
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Exports
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Generates a random 16-digit number.
|
||||
* This function uses the native (unpatched) {@link Math.random} method.
|
||||
*/
|
||||
export function generateSeed() {
|
||||
return $floor($random() * 1e16);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a seeded random number generator equivalent to the native
|
||||
* {@link Math.random} method.
|
||||
*
|
||||
* It exposes a `seed` property that can be changed at any time to reset the
|
||||
* generator.
|
||||
*
|
||||
* @param {number} seed
|
||||
* @example
|
||||
* const randA = makeSeededRandom(1e16);
|
||||
* const randB = makeSeededRandom(1e16);
|
||||
* randA() === randB(); // true
|
||||
* @example
|
||||
* const random = makeSeededRandom(1e16);
|
||||
* random() === random(); // false
|
||||
*/
|
||||
export function makeSeededRandom(seed) {
|
||||
function random() {
|
||||
state ^= (state << 13) >>> 0;
|
||||
state ^= (state >>> 17) >>> 0;
|
||||
state ^= (state << 5) >>> 0;
|
||||
|
||||
return ((state >>> 0) & 0x7fffffff) / 0x7fffffff; // Normalize to [0, 1)
|
||||
}
|
||||
|
||||
let state = seed;
|
||||
|
||||
$defineProperties(random, {
|
||||
seed: {
|
||||
get() {
|
||||
return seed;
|
||||
},
|
||||
set(value) {
|
||||
seed = toValidSeed(value);
|
||||
state = seed;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return random;
|
||||
}
|
||||
|
||||
/**
|
||||
* `random` function used internally to not generate unwanted calls on global
|
||||
* `Math.random` function (and possibly having a different seed).
|
||||
*/
|
||||
export const internalRandom = makeSeededRandom(DEFAULT_SEED);
|
||||
328
odoo-bringout-oca-ocb-web/web/static/lib/hoot/mock/navigator.js
Normal file
328
odoo-bringout-oca-ocb-web/web/static/lib/hoot/mock/navigator.js
Normal file
|
|
@ -0,0 +1,328 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import { isInstanceOf } from "../../hoot-dom/hoot_dom_utils";
|
||||
import { createMock, HootError, MIME_TYPE, MockEventTarget } from "../hoot_utils";
|
||||
import { getSyncValue, setSyncValue } from "./sync_values";
|
||||
|
||||
/**
|
||||
* @typedef {"android" | "ios" | "linux" | "mac" | "windows"} Platform
|
||||
*/
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Global
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
const {
|
||||
Blob,
|
||||
ClipboardItem = class NonSecureClipboardItem {},
|
||||
navigator,
|
||||
Object: { assign: $assign },
|
||||
Set,
|
||||
TypeError,
|
||||
} = globalThis;
|
||||
const { userAgent: $userAgent } = navigator;
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Internal
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
function getBlobValue(value) {
|
||||
return isInstanceOf(value, Blob) ? value.text() : value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the final synchronous value of several item types.
|
||||
*
|
||||
* @param {unknown} value
|
||||
* @param {string} type
|
||||
*/
|
||||
function getClipboardValue(value, type) {
|
||||
return getBlobValue(isInstanceOf(value, ClipboardItem) ? value.getType(type) : value);
|
||||
}
|
||||
|
||||
function getMockValues() {
|
||||
return {
|
||||
sendBeacon: throwNotImplemented("sendBeacon"),
|
||||
userAgent: makeUserAgent("linux"),
|
||||
/** @type {Navigator["vibrate"]} */
|
||||
vibrate: throwNotImplemented("vibrate"),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Record<PermissionName, { name: string; state: PermissionState }>}
|
||||
*/
|
||||
function getPermissions() {
|
||||
return {
|
||||
"background-sync": {
|
||||
state: "granted", // should always be granted
|
||||
name: "background_sync",
|
||||
},
|
||||
"local-fonts": {
|
||||
state: "denied",
|
||||
name: "local_fonts",
|
||||
},
|
||||
"payment-handler": {
|
||||
state: "denied",
|
||||
name: "payment_handler",
|
||||
},
|
||||
"persistent-storage": {
|
||||
state: "denied",
|
||||
name: "durable_storage",
|
||||
},
|
||||
"screen-wake-lock": {
|
||||
state: "denied",
|
||||
name: "screen_wake_lock",
|
||||
},
|
||||
"storage-access": {
|
||||
state: "denied",
|
||||
name: "storage-access",
|
||||
},
|
||||
"window-management": {
|
||||
state: "denied",
|
||||
name: "window_placement",
|
||||
},
|
||||
accelerometer: {
|
||||
state: "denied",
|
||||
name: "sensors",
|
||||
},
|
||||
camera: {
|
||||
state: "denied",
|
||||
name: "video_capture",
|
||||
},
|
||||
geolocation: {
|
||||
state: "denied",
|
||||
name: "geolocation",
|
||||
},
|
||||
gyroscope: {
|
||||
state: "denied",
|
||||
name: "sensors",
|
||||
},
|
||||
magnetometer: {
|
||||
state: "denied",
|
||||
name: "sensors",
|
||||
},
|
||||
microphone: {
|
||||
state: "denied",
|
||||
name: "audio_capture",
|
||||
},
|
||||
midi: {
|
||||
state: "denied",
|
||||
name: "midi",
|
||||
},
|
||||
notifications: {
|
||||
state: "denied",
|
||||
name: "notifications",
|
||||
},
|
||||
push: {
|
||||
state: "denied",
|
||||
name: "push",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function getUserAgentBrowser() {
|
||||
if (/Firefox/i.test($userAgent)) {
|
||||
return "Gecko/20100101 Firefox/1000.0"; // Firefox
|
||||
}
|
||||
if (/Chrome/i.test($userAgent)) {
|
||||
return "AppleWebKit/1000.00 (KHTML, like Gecko) Chrome/1000.00 Safari/1000.00"; // Chrome
|
||||
}
|
||||
if (/Safari/i.test($userAgent)) {
|
||||
return "AppleWebKit/1000.00 (KHTML, like Gecko) Version/1000.00 Safari/1000.00"; // Safari
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Platform} platform
|
||||
*/
|
||||
function makeUserAgent(platform) {
|
||||
const userAgent = ["Mozilla/5.0"];
|
||||
switch (platform.toLowerCase()) {
|
||||
case "android": {
|
||||
userAgent.push("(Linux; Android 1000)");
|
||||
break;
|
||||
}
|
||||
case "ios": {
|
||||
userAgent.push("(iPhone; CPU iPhone OS 1000_0 like Mac OS X)");
|
||||
break;
|
||||
}
|
||||
case "linux": {
|
||||
userAgent.push("(X11; Linux x86_64)");
|
||||
break;
|
||||
}
|
||||
case "mac":
|
||||
case "macintosh": {
|
||||
userAgent.push("(Macintosh; Intel Mac OS X 10_15_7)");
|
||||
break;
|
||||
}
|
||||
case "win":
|
||||
case "windows": {
|
||||
userAgent.push("(Windows NT 10.0; Win64; x64)");
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
userAgent.push(platform);
|
||||
}
|
||||
}
|
||||
if (userAgentBrowser) {
|
||||
userAgent.push(userAgentBrowser);
|
||||
}
|
||||
return userAgent.join(" ");
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} fnName
|
||||
*/
|
||||
function throwNotImplemented(fnName) {
|
||||
return function notImplemented() {
|
||||
throw new HootError(`unmocked navigator method: ${fnName}`);
|
||||
};
|
||||
}
|
||||
|
||||
/** @type {Set<MockPermissionStatus>} */
|
||||
const permissionStatuses = new Set();
|
||||
const userAgentBrowser = getUserAgentBrowser();
|
||||
const mockValues = getMockValues();
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Exports
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
export class MockClipboard {
|
||||
/** @type {unknown} */
|
||||
_value = null;
|
||||
|
||||
async read() {
|
||||
return this._value;
|
||||
}
|
||||
|
||||
async readText() {
|
||||
return String(getClipboardValue(this._value, MIME_TYPE.text) ?? "");
|
||||
}
|
||||
|
||||
async write(value) {
|
||||
this._value = value;
|
||||
}
|
||||
|
||||
async writeText(value) {
|
||||
this._value = String(getClipboardValue(value, MIME_TYPE.text) ?? "");
|
||||
}
|
||||
}
|
||||
|
||||
export class MockClipboardItem extends ClipboardItem {
|
||||
constructor(items) {
|
||||
super(items);
|
||||
|
||||
setSyncValue(this, items);
|
||||
}
|
||||
|
||||
// Added synchronous methods to enhance speed in tests
|
||||
|
||||
async getType(type) {
|
||||
return getSyncValue(this)[type];
|
||||
}
|
||||
}
|
||||
|
||||
export class MockPermissions {
|
||||
/**
|
||||
* @param {PermissionDescriptor} permissionDesc
|
||||
*/
|
||||
async query({ name }) {
|
||||
if (!(name in currentPermissions)) {
|
||||
throw new TypeError(
|
||||
`The provided value '${name}' is not a valid enum value of type PermissionName`
|
||||
);
|
||||
}
|
||||
return new MockPermissionStatus(name);
|
||||
}
|
||||
}
|
||||
|
||||
export class MockPermissionStatus extends MockEventTarget {
|
||||
static publicListeners = ["change"];
|
||||
|
||||
/** @type {typeof currentPermissions[PermissionName]} */
|
||||
_permission;
|
||||
|
||||
/**
|
||||
* @param {PermissionName} name
|
||||
* @param {PermissionState} value
|
||||
*/
|
||||
constructor(name) {
|
||||
super(...arguments);
|
||||
|
||||
this._permission = currentPermissions[name];
|
||||
permissionStatuses.add(this);
|
||||
}
|
||||
|
||||
get name() {
|
||||
return this._permission.name;
|
||||
}
|
||||
|
||||
get state() {
|
||||
return this._permission.state;
|
||||
}
|
||||
}
|
||||
|
||||
export const currentPermissions = getPermissions();
|
||||
|
||||
export const mockClipboard = new MockClipboard();
|
||||
|
||||
export const mockPermissions = new MockPermissions();
|
||||
|
||||
export const mockNavigator = createMock(navigator, {
|
||||
clipboard: { value: mockClipboard },
|
||||
maxTouchPoints: { get: () => (globalThis.ontouchstart === undefined ? 0 : 1) },
|
||||
permissions: { value: mockPermissions },
|
||||
sendBeacon: { get: () => mockValues.sendBeacon },
|
||||
serviceWorker: { get: () => undefined },
|
||||
userAgent: { get: () => mockValues.userAgent },
|
||||
vibrate: { get: () => mockValues.vibrate },
|
||||
});
|
||||
|
||||
export function cleanupNavigator() {
|
||||
permissionStatuses.clear();
|
||||
$assign(currentPermissions, getPermissions());
|
||||
$assign(mockValues, getMockValues());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {PermissionName} name
|
||||
* @param {PermissionState} [value]
|
||||
*/
|
||||
export function mockPermission(name, value) {
|
||||
if (!(name in currentPermissions)) {
|
||||
throw new TypeError(
|
||||
`The provided value '${name}' is not a valid enum value of type PermissionName`
|
||||
);
|
||||
}
|
||||
|
||||
currentPermissions[name].state = value;
|
||||
|
||||
for (const permissionStatus of permissionStatuses) {
|
||||
if (permissionStatus.name === name) {
|
||||
permissionStatus.dispatchEvent(new Event("change"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Navigator["sendBeacon"]} callback
|
||||
*/
|
||||
export function mockSendBeacon(callback) {
|
||||
mockValues.sendBeacon = callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Platform} platform
|
||||
*/
|
||||
export function mockUserAgent(platform = "linux") {
|
||||
mockValues.userAgent = makeUserAgent(platform);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Navigator["vibrate"]} callback
|
||||
*/
|
||||
export function mockVibrate(callback) {
|
||||
mockValues.vibrate = callback;
|
||||
}
|
||||
1028
odoo-bringout-oca-ocb-web/web/static/lib/hoot/mock/network.js
Normal file
1028
odoo-bringout-oca-ocb-web/web/static/lib/hoot/mock/network.js
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,79 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import { MockEventTarget } from "../hoot_utils";
|
||||
import { currentPermissions } from "./navigator";
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Global
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
const { Event, Promise, Set } = globalThis;
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Internal
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
/** @type {Set<MockNotification>} */
|
||||
const notifications = new Set();
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Exports
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Returns the list of notifications that have been created since the last call
|
||||
* to this function, consuming it in the process.
|
||||
*
|
||||
* @returns {MockNotification[]}
|
||||
*/
|
||||
export function flushNotifications() {
|
||||
const result = [...notifications];
|
||||
notifications.clear();
|
||||
return result;
|
||||
}
|
||||
|
||||
export class MockNotification extends MockEventTarget {
|
||||
static publicListeners = ["click", "close", "error", "show"];
|
||||
|
||||
/** @type {NotificationPermission} */
|
||||
static get permission() {
|
||||
return currentPermissions.notifications.state;
|
||||
}
|
||||
|
||||
/** @type {NotificationPermission} */
|
||||
get permission() {
|
||||
return this.constructor.permission;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} title
|
||||
* @param {NotificationOptions} [options]
|
||||
*/
|
||||
constructor(title, options) {
|
||||
super(...arguments);
|
||||
|
||||
this.title = title;
|
||||
this.options = options;
|
||||
|
||||
if (this.permission === "granted") {
|
||||
notifications.push(this);
|
||||
}
|
||||
}
|
||||
|
||||
static requestPermission() {
|
||||
return Promise.resolve(this.permission);
|
||||
}
|
||||
|
||||
click() {
|
||||
this.dispatchEvent(new Event("click"));
|
||||
}
|
||||
|
||||
close() {
|
||||
notifications.delete(this);
|
||||
this.dispatchEvent(new Event("close"));
|
||||
}
|
||||
|
||||
show() {
|
||||
this.dispatchEvent(new Event("show"));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
/** @odoo-module */
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Global
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
const {
|
||||
Object: { keys: $keys },
|
||||
StorageEvent,
|
||||
String,
|
||||
} = globalThis;
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Exports
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
export class MockStorage {
|
||||
get length() {
|
||||
return $keys(this).length;
|
||||
}
|
||||
|
||||
/** @type {typeof Storage.prototype.clear} */
|
||||
clear() {
|
||||
for (const key in this) {
|
||||
delete this[key];
|
||||
}
|
||||
}
|
||||
|
||||
/** @type {typeof Storage.prototype.getItem} */
|
||||
getItem(key) {
|
||||
key = String(key);
|
||||
return this[key] ?? null;
|
||||
}
|
||||
|
||||
/** @type {typeof Storage.prototype.key} */
|
||||
key(index) {
|
||||
return $keys(this).at(index);
|
||||
}
|
||||
|
||||
/** @type {typeof Storage.prototype.removeItem} */
|
||||
removeItem(key) {
|
||||
key = String(key);
|
||||
delete this[key];
|
||||
window.dispatchEvent(new StorageEvent("storage", { key, newValue: null }));
|
||||
}
|
||||
|
||||
/** @type {typeof Storage.prototype.setItem} */
|
||||
setItem(key, value) {
|
||||
key = String(key);
|
||||
value = String(value);
|
||||
this[key] = value;
|
||||
window.dispatchEvent(new StorageEvent("storage", { key, newValue: value }));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
/** @odoo-module */
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Global
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
const { Blob, TextEncoder } = globalThis;
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Internal
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
const syncValues = new WeakMap();
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Exports
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @param {any} object
|
||||
*/
|
||||
export function getSyncValue(object) {
|
||||
return syncValues.get(object);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {any} object
|
||||
* @param {any} value
|
||||
*/
|
||||
export function setSyncValue(object, value) {
|
||||
syncValues.set(object, value);
|
||||
}
|
||||
|
||||
export class MockBlob extends Blob {
|
||||
constructor(blobParts, options) {
|
||||
super(blobParts, options);
|
||||
|
||||
setSyncValue(this, blobParts);
|
||||
}
|
||||
|
||||
async arrayBuffer() {
|
||||
return new TextEncoder().encode(getSyncValue(this));
|
||||
}
|
||||
|
||||
async text() {
|
||||
return getSyncValue(this).join("");
|
||||
}
|
||||
}
|
||||
713
odoo-bringout-oca-ocb-web/web/static/lib/hoot/mock/window.js
Normal file
713
odoo-bringout-oca-ocb-web/web/static/lib/hoot/mock/window.js
Normal file
|
|
@ -0,0 +1,713 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import { EventBus } from "@odoo/owl";
|
||||
import { getCurrentDimensions, getDocument, getWindow } from "@web/../lib/hoot-dom/helpers/dom";
|
||||
import {
|
||||
mockedCancelAnimationFrame,
|
||||
mockedClearInterval,
|
||||
mockedClearTimeout,
|
||||
mockedRequestAnimationFrame,
|
||||
mockedSetInterval,
|
||||
mockedSetTimeout,
|
||||
} from "@web/../lib/hoot-dom/helpers/time";
|
||||
import { interactor } from "../../hoot-dom/hoot_dom_utils";
|
||||
import { MockEventTarget, strictEqual, waitForDocument } from "../hoot_utils";
|
||||
import { getRunner } from "../main_runner";
|
||||
import {
|
||||
MockAnimation,
|
||||
mockedAnimate,
|
||||
mockedScroll,
|
||||
mockedScrollBy,
|
||||
mockedScrollIntoView,
|
||||
mockedScrollTo,
|
||||
mockedWindowScroll,
|
||||
mockedWindowScrollBy,
|
||||
mockedWindowScrollTo,
|
||||
} from "./animation";
|
||||
import { MockConsole } from "./console";
|
||||
import { MockDate, MockIntl } from "./date";
|
||||
import { MockClipboardItem, mockNavigator } from "./navigator";
|
||||
import {
|
||||
MockBroadcastChannel,
|
||||
MockMessageChannel,
|
||||
MockMessagePort,
|
||||
MockRequest,
|
||||
MockResponse,
|
||||
MockSharedWorker,
|
||||
MockURL,
|
||||
MockWebSocket,
|
||||
MockWorker,
|
||||
MockXMLHttpRequest,
|
||||
MockXMLHttpRequestUpload,
|
||||
mockCookie,
|
||||
mockHistory,
|
||||
mockLocation,
|
||||
mockedFetch,
|
||||
} from "./network";
|
||||
import { MockNotification } from "./notification";
|
||||
import { MockStorage } from "./storage";
|
||||
import { MockBlob } from "./sync_values";
|
||||
import { mockCrypto } from "./crypto";
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Global
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
const {
|
||||
EventTarget,
|
||||
HTMLAnchorElement,
|
||||
MutationObserver,
|
||||
Number: { isNaN: $isNaN, parseFloat: $parseFloat },
|
||||
Object: {
|
||||
assign: $assign,
|
||||
defineProperties: $defineProperties,
|
||||
entries: $entries,
|
||||
getOwnPropertyDescriptor: $getOwnPropertyDescriptor,
|
||||
getPrototypeOf: $getPrototypeOf,
|
||||
keys: $keys,
|
||||
hasOwn: $hasOwn,
|
||||
},
|
||||
Reflect: { ownKeys: $ownKeys },
|
||||
Set,
|
||||
WeakMap,
|
||||
} = globalThis;
|
||||
|
||||
const { addEventListener, removeEventListener } = EventTarget.prototype;
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Internal
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @param {unknown} target
|
||||
* @param {Record<string, PropertyDescriptor>} descriptors
|
||||
*/
|
||||
function applyPropertyDescriptors(target, descriptors) {
|
||||
if (!originalDescriptors.has(target)) {
|
||||
originalDescriptors.set(target, {});
|
||||
}
|
||||
const targetDescriptors = originalDescriptors.get(target);
|
||||
const ownerDecriptors = new Map();
|
||||
for (const [property, rawDescriptor] of $entries(descriptors)) {
|
||||
const owner = findPropertyOwner(target, property);
|
||||
targetDescriptors[property] = $getOwnPropertyDescriptor(owner, property);
|
||||
const descriptor = { ...rawDescriptor };
|
||||
if ("value" in descriptor) {
|
||||
descriptor.writable = false;
|
||||
}
|
||||
if (!ownerDecriptors.has(owner)) {
|
||||
ownerDecriptors.set(owner, {});
|
||||
}
|
||||
const nextDescriptors = ownerDecriptors.get(owner);
|
||||
nextDescriptors[property] = descriptor;
|
||||
}
|
||||
for (const [owner, nextDescriptors] of ownerDecriptors) {
|
||||
$defineProperties(owner, nextDescriptors);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string[]} [changedKeys]
|
||||
*/
|
||||
function callMediaQueryChanges(changedKeys) {
|
||||
for (const mediaQueryList of mediaQueryLists) {
|
||||
if (!changedKeys || changedKeys.some((key) => mediaQueryList.media.includes(key))) {
|
||||
const event = new MediaQueryListEvent("change", {
|
||||
matches: mediaQueryList.matches,
|
||||
media: mediaQueryList.media,
|
||||
});
|
||||
mediaQueryList.dispatchEvent(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @param {T} target
|
||||
* @param {keyof T} property
|
||||
*/
|
||||
function findOriginalDescriptor(target, property) {
|
||||
if (originalDescriptors.has(target)) {
|
||||
const descriptors = originalDescriptors.get(target);
|
||||
if (descriptors && property in descriptors) {
|
||||
return descriptors[property];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {unknown} object
|
||||
* @param {string} property
|
||||
* @returns {unknown}
|
||||
*/
|
||||
function findPropertyOwner(object, property) {
|
||||
if ($hasOwn(object, property)) {
|
||||
return object;
|
||||
}
|
||||
const prototype = $getPrototypeOf(object);
|
||||
if (prototype) {
|
||||
return findPropertyOwner(prototype, property);
|
||||
}
|
||||
return object;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {unknown} object
|
||||
*/
|
||||
function getTouchDescriptors(object) {
|
||||
const descriptors = {};
|
||||
const toDelete = [];
|
||||
for (const eventName of TOUCH_EVENTS) {
|
||||
const fnName = `on${eventName}`;
|
||||
if (fnName in object) {
|
||||
const owner = findPropertyOwner(object, fnName);
|
||||
descriptors[fnName] = $getOwnPropertyDescriptor(owner, fnName);
|
||||
} else {
|
||||
toDelete.push(fnName);
|
||||
}
|
||||
}
|
||||
/** @type {({ descriptors?: Record<string, PropertyDescriptor>; toDelete?: string[]})} */
|
||||
const result = {};
|
||||
if ($keys(descriptors).length) {
|
||||
result.descriptors = descriptors;
|
||||
}
|
||||
if (toDelete.length) {
|
||||
result.toDelete = toDelete;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {typeof globalThis} view
|
||||
*/
|
||||
function getTouchTargets(view) {
|
||||
return [view, view.Document.prototype];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {typeof globalThis} view
|
||||
*/
|
||||
function getWatchedEventTargets(view) {
|
||||
return [
|
||||
view,
|
||||
view.document,
|
||||
// Permanent DOM elements
|
||||
view.HTMLDocument.prototype,
|
||||
view.HTMLBodyElement.prototype,
|
||||
view.HTMLHeadElement.prototype,
|
||||
view.HTMLHtmlElement.prototype,
|
||||
// Other event targets
|
||||
EventBus.prototype,
|
||||
MockEventTarget.prototype,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} type
|
||||
* @returns {PropertyDescriptor}
|
||||
*/
|
||||
function makeEventDescriptor(type) {
|
||||
let callback = null;
|
||||
return {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
get() {
|
||||
return callback;
|
||||
},
|
||||
set(value) {
|
||||
if (callback === value) {
|
||||
return;
|
||||
}
|
||||
if (typeof callback === "function") {
|
||||
this.removeEventListener(type, callback);
|
||||
}
|
||||
callback = value;
|
||||
if (typeof callback === "function") {
|
||||
this.addEventListener(type, callback);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} mediaQueryString
|
||||
*/
|
||||
function matchesQueryPart(mediaQueryString) {
|
||||
const [, key, value] = mediaQueryString.match(R_MEDIA_QUERY_PROPERTY) || [];
|
||||
let match = false;
|
||||
if (mockMediaValues[key]) {
|
||||
match = strictEqual(value, mockMediaValues[key]);
|
||||
} else if (key) {
|
||||
switch (key) {
|
||||
case "max-height": {
|
||||
match = getCurrentDimensions().height <= $parseFloat(value);
|
||||
break;
|
||||
}
|
||||
case "max-width": {
|
||||
match = getCurrentDimensions().width <= $parseFloat(value);
|
||||
break;
|
||||
}
|
||||
case "min-height": {
|
||||
match = getCurrentDimensions().height >= $parseFloat(value);
|
||||
break;
|
||||
}
|
||||
case "min-width": {
|
||||
match = getCurrentDimensions().width >= $parseFloat(value);
|
||||
break;
|
||||
}
|
||||
case "orientation": {
|
||||
const { width, height } = getCurrentDimensions();
|
||||
match = value === "landscape" ? width > height : width < height;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return mediaQueryString.startsWith("not") ? !match : match;
|
||||
}
|
||||
|
||||
/** @type {addEventListener} */
|
||||
function mockedAddEventListener(...args) {
|
||||
const runner = getRunner();
|
||||
if (runner.dry || !runner.suiteStack.length) {
|
||||
// Ignore listeners during dry run or outside of a test suite
|
||||
return;
|
||||
}
|
||||
if (!R_OWL_SYNTHETIC_LISTENER.test(String(args[1]))) {
|
||||
// Ignore cleanup for Owl synthetic listeners
|
||||
runner.after(removeEventListener.bind(this, ...args));
|
||||
}
|
||||
return addEventListener.call(this, ...args);
|
||||
}
|
||||
|
||||
/** @type {Document["elementFromPoint"]} */
|
||||
function mockedElementFromPoint(...args) {
|
||||
return mockedElementsFromPoint.call(this, ...args)[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Mocked version of {@link document.elementsFromPoint} to:
|
||||
* - remove "HOOT-..." elements from the result
|
||||
* - put the <body> & <html> elements at the end of the list, as they may be ordered
|
||||
* incorrectly due to the fixture being behind the body.
|
||||
* @type {Document["elementsFromPoint"]}
|
||||
*/
|
||||
function mockedElementsFromPoint(...args) {
|
||||
const { value: elementsFromPoint } = findOriginalDescriptor(this, "elementsFromPoint");
|
||||
const result = [];
|
||||
let hasDocumentElement = false;
|
||||
let hasBody = false;
|
||||
for (const element of elementsFromPoint.call(this, ...args)) {
|
||||
if (element.tagName.startsWith("HOOT")) {
|
||||
continue;
|
||||
}
|
||||
if (element === this.body) {
|
||||
hasBody = true;
|
||||
} else if (element === this.documentElement) {
|
||||
hasDocumentElement = true;
|
||||
} else {
|
||||
result.push(element);
|
||||
}
|
||||
}
|
||||
if (hasBody) {
|
||||
result.push(this.body);
|
||||
}
|
||||
if (hasDocumentElement) {
|
||||
result.push(this.documentElement);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function mockedHref() {
|
||||
return this.hasAttribute("href") ? new MockURL(this.getAttribute("href")).href : "";
|
||||
}
|
||||
|
||||
/** @type {typeof matchMedia} */
|
||||
function mockedMatchMedia(mediaQueryString) {
|
||||
return new MockMediaQueryList(mediaQueryString);
|
||||
}
|
||||
|
||||
/** @type {typeof removeEventListener} */
|
||||
function mockedRemoveEventListener(...args) {
|
||||
if (getRunner().dry) {
|
||||
// Ignore listeners during dry run
|
||||
return;
|
||||
}
|
||||
return removeEventListener.call(this, ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {MutationRecord[]} mutations
|
||||
*/
|
||||
function observeAddedNodes(mutations) {
|
||||
const runner = getRunner();
|
||||
for (const mutation of mutations) {
|
||||
for (const node of mutation.addedNodes) {
|
||||
if (runner.dry) {
|
||||
node.remove();
|
||||
} else {
|
||||
runner.after(node.remove.bind(node));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {PointerEvent} ev
|
||||
*/
|
||||
function onAnchorHrefClick(ev) {
|
||||
if (ev.defaultPrevented) {
|
||||
return;
|
||||
}
|
||||
const href = ev.target.closest("a[href]")?.href;
|
||||
if (!href) {
|
||||
return;
|
||||
}
|
||||
|
||||
ev.preventDefault();
|
||||
|
||||
// Assign href to mock location instead of actual location
|
||||
mockLocation.href = href;
|
||||
|
||||
const [, hash] = href.split("#");
|
||||
if (hash) {
|
||||
// Scroll to the target element if the href is/has a hash
|
||||
getDocument().getElementById(hash)?.scrollIntoView();
|
||||
}
|
||||
}
|
||||
|
||||
function onWindowResize() {
|
||||
callMediaQueryChanges();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {typeof globalThis} view
|
||||
*/
|
||||
function restoreTouch(view) {
|
||||
const touchObjects = getTouchTargets(view);
|
||||
for (let i = 0; i < touchObjects.length; i++) {
|
||||
const object = touchObjects[i];
|
||||
const { descriptors, toDelete } = originalTouchFunctions[i];
|
||||
if (descriptors) {
|
||||
$defineProperties(object, descriptors);
|
||||
}
|
||||
if (toDelete) {
|
||||
for (const fnName of toDelete) {
|
||||
delete object[fnName];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class MockMediaQueryList extends MockEventTarget {
|
||||
static publicListeners = ["change"];
|
||||
|
||||
get matches() {
|
||||
return this.media
|
||||
.split(R_COMMA)
|
||||
.some((orPart) => orPart.split(R_AND).every(matchesQueryPart));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} mediaQueryString
|
||||
*/
|
||||
constructor(mediaQueryString) {
|
||||
super(...arguments);
|
||||
|
||||
this.media = mediaQueryString.trim().toLowerCase();
|
||||
|
||||
mediaQueryLists.add(this);
|
||||
}
|
||||
}
|
||||
|
||||
const DEFAULT_MEDIA_VALUES = {
|
||||
"display-mode": "browser",
|
||||
pointer: "fine",
|
||||
"prefers-color-scheme": "light",
|
||||
"prefers-reduced-motion": "reduce",
|
||||
};
|
||||
|
||||
const TOUCH_EVENTS = ["touchcancel", "touchend", "touchmove", "touchstart"];
|
||||
|
||||
const R_AND = /\s*\band\b\s*/;
|
||||
const R_COMMA = /\s*,\s*/;
|
||||
const R_MEDIA_QUERY_PROPERTY = /\(\s*([\w-]+)\s*:\s*(.+)\s*\)/;
|
||||
const R_OWL_SYNTHETIC_LISTENER = /\bnativeToSyntheticEvent\b/;
|
||||
|
||||
/** @type {WeakMap<unknown, Record<string, PropertyDescriptor>>} */
|
||||
const originalDescriptors = new WeakMap();
|
||||
const originalTouchFunctions = getTouchTargets(globalThis).map(getTouchDescriptors);
|
||||
|
||||
/** @type {Set<MockMediaQueryList>} */
|
||||
const mediaQueryLists = new Set();
|
||||
const mockConsole = new MockConsole();
|
||||
const mockLocalStorage = new MockStorage();
|
||||
const mockMediaValues = { ...DEFAULT_MEDIA_VALUES };
|
||||
const mockSessionStorage = new MockStorage();
|
||||
let mockTitle = "";
|
||||
|
||||
// Mock descriptors
|
||||
const ANCHOR_MOCK_DESCRIPTORS = {
|
||||
href: {
|
||||
...$getOwnPropertyDescriptor(HTMLAnchorElement.prototype, "href"),
|
||||
get: mockedHref,
|
||||
},
|
||||
};
|
||||
const DOCUMENT_MOCK_DESCRIPTORS = {
|
||||
cookie: {
|
||||
get: () => mockCookie.get(),
|
||||
set: (value) => mockCookie.set(value),
|
||||
},
|
||||
elementFromPoint: { value: mockedElementFromPoint },
|
||||
elementsFromPoint: { value: mockedElementsFromPoint },
|
||||
title: {
|
||||
get: () => mockTitle,
|
||||
set: (value) => (mockTitle = value),
|
||||
},
|
||||
};
|
||||
const ELEMENT_MOCK_DESCRIPTORS = {
|
||||
animate: { value: mockedAnimate },
|
||||
scroll: { value: mockedScroll },
|
||||
scrollBy: { value: mockedScrollBy },
|
||||
scrollIntoView: { value: mockedScrollIntoView },
|
||||
scrollTo: { value: mockedScrollTo },
|
||||
};
|
||||
const WINDOW_MOCK_DESCRIPTORS = {
|
||||
Animation: { value: MockAnimation },
|
||||
Blob: { value: MockBlob },
|
||||
BroadcastChannel: { value: MockBroadcastChannel },
|
||||
cancelAnimationFrame: { value: mockedCancelAnimationFrame, writable: false },
|
||||
clearInterval: { value: mockedClearInterval, writable: false },
|
||||
clearTimeout: { value: mockedClearTimeout, writable: false },
|
||||
ClipboardItem: { value: MockClipboardItem },
|
||||
console: { value: mockConsole, writable: false },
|
||||
crypto: { value: mockCrypto, writable: false },
|
||||
Date: { value: MockDate, writable: false },
|
||||
fetch: { value: interactor("server", mockedFetch).as("fetch"), writable: false },
|
||||
history: { value: mockHistory },
|
||||
innerHeight: { get: () => getCurrentDimensions().height },
|
||||
innerWidth: { get: () => getCurrentDimensions().width },
|
||||
isSecureContext: { value: true, writable: false },
|
||||
Intl: { value: MockIntl },
|
||||
localStorage: { value: mockLocalStorage, writable: false },
|
||||
matchMedia: { value: mockedMatchMedia },
|
||||
MessageChannel: { value: MockMessageChannel },
|
||||
MessagePort: { value: MockMessagePort },
|
||||
navigator: { value: mockNavigator },
|
||||
Notification: { value: MockNotification },
|
||||
outerHeight: { get: () => getCurrentDimensions().height },
|
||||
outerWidth: { get: () => getCurrentDimensions().width },
|
||||
Request: { value: MockRequest, writable: false },
|
||||
requestAnimationFrame: { value: mockedRequestAnimationFrame, writable: false },
|
||||
Response: { value: MockResponse, writable: false },
|
||||
scroll: { value: mockedWindowScroll },
|
||||
scrollBy: { value: mockedWindowScrollBy },
|
||||
scrollTo: { value: mockedWindowScrollTo },
|
||||
sessionStorage: { value: mockSessionStorage, writable: false },
|
||||
setInterval: { value: mockedSetInterval, writable: false },
|
||||
setTimeout: { value: mockedSetTimeout, writable: false },
|
||||
SharedWorker: { value: MockSharedWorker },
|
||||
URL: { value: MockURL },
|
||||
WebSocket: { value: MockWebSocket },
|
||||
Worker: { value: MockWorker },
|
||||
XMLHttpRequest: { value: MockXMLHttpRequest },
|
||||
XMLHttpRequestUpload: { value: MockXMLHttpRequestUpload },
|
||||
};
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Exports
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
export function cleanupWindow() {
|
||||
const view = getWindow();
|
||||
|
||||
// Storages
|
||||
mockLocalStorage.clear();
|
||||
mockSessionStorage.clear();
|
||||
|
||||
// Media
|
||||
mediaQueryLists.clear();
|
||||
$assign(mockMediaValues, DEFAULT_MEDIA_VALUES);
|
||||
|
||||
// Title
|
||||
mockTitle = "";
|
||||
|
||||
// Listeners
|
||||
view.removeEventListener("click", onAnchorHrefClick);
|
||||
view.removeEventListener("resize", onWindowResize);
|
||||
|
||||
// Head & body attributes
|
||||
const { head, body } = view.document;
|
||||
for (const { name } of head.attributes) {
|
||||
head.removeAttribute(name);
|
||||
}
|
||||
for (const { name } of body.attributes) {
|
||||
body.removeAttribute(name);
|
||||
}
|
||||
|
||||
// Touch
|
||||
restoreTouch(view);
|
||||
}
|
||||
|
||||
export function getTitle() {
|
||||
const doc = getDocument();
|
||||
const titleDescriptor = findOriginalDescriptor(doc, "title");
|
||||
if (titleDescriptor) {
|
||||
return titleDescriptor.get.call(doc);
|
||||
} else {
|
||||
return doc.title;
|
||||
}
|
||||
}
|
||||
|
||||
export function getViewPortHeight() {
|
||||
const view = getWindow();
|
||||
const heightDescriptor = findOriginalDescriptor(view, "innerHeight");
|
||||
if (heightDescriptor) {
|
||||
return heightDescriptor.get.call(view);
|
||||
} else {
|
||||
return view.innerHeight;
|
||||
}
|
||||
}
|
||||
|
||||
export function getViewPortWidth() {
|
||||
const view = getWindow();
|
||||
const titleDescriptor = findOriginalDescriptor(view, "innerWidth");
|
||||
if (titleDescriptor) {
|
||||
return titleDescriptor.get.call(view);
|
||||
} else {
|
||||
return view.innerWidth;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, string>} name
|
||||
*/
|
||||
export function mockMatchMedia(values) {
|
||||
$assign(mockMediaValues, values);
|
||||
|
||||
callMediaQueryChanges($keys(values));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {boolean} setTouch
|
||||
*/
|
||||
export function mockTouch(setTouch) {
|
||||
const objects = getTouchTargets(getWindow());
|
||||
if (setTouch) {
|
||||
for (const object of objects) {
|
||||
const descriptors = {};
|
||||
for (const eventName of TOUCH_EVENTS) {
|
||||
const fnName = `on${eventName}`;
|
||||
if (!$hasOwn(object, fnName)) {
|
||||
descriptors[fnName] = makeEventDescriptor(eventName);
|
||||
}
|
||||
}
|
||||
$defineProperties(object, descriptors);
|
||||
}
|
||||
mockMatchMedia({ pointer: "coarse" });
|
||||
} else {
|
||||
for (const object of objects) {
|
||||
for (const eventName of TOUCH_EVENTS) {
|
||||
delete object[`on${eventName}`];
|
||||
}
|
||||
}
|
||||
mockMatchMedia({ pointer: "fine" });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {typeof globalThis} [view=getWindow()]
|
||||
*/
|
||||
export function patchWindow(view = getWindow()) {
|
||||
// Window (doesn't need to be ready)
|
||||
applyPropertyDescriptors(view, WINDOW_MOCK_DESCRIPTORS);
|
||||
|
||||
waitForDocument(view.document).then(() => {
|
||||
// Document
|
||||
applyPropertyDescriptors(view.document, DOCUMENT_MOCK_DESCRIPTORS);
|
||||
|
||||
// Element prototypes
|
||||
applyPropertyDescriptors(view.Element.prototype, ELEMENT_MOCK_DESCRIPTORS);
|
||||
applyPropertyDescriptors(view.HTMLAnchorElement.prototype, ANCHOR_MOCK_DESCRIPTORS);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} value
|
||||
*/
|
||||
export function setTitle(value) {
|
||||
const doc = getDocument();
|
||||
const titleDescriptor = findOriginalDescriptor(doc, "title");
|
||||
if (titleDescriptor) {
|
||||
titleDescriptor.set.call(doc, value);
|
||||
} else {
|
||||
doc.title = value;
|
||||
}
|
||||
}
|
||||
|
||||
export function setupWindow() {
|
||||
const view = getWindow();
|
||||
|
||||
// Listeners
|
||||
view.addEventListener("click", onAnchorHrefClick);
|
||||
view.addEventListener("resize", onWindowResize);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {typeof globalThis} [view=getWindow()]
|
||||
*/
|
||||
export function watchAddedNodes(view = getWindow()) {
|
||||
const observer = new MutationObserver(observeAddedNodes);
|
||||
observer.observe(view.document.head, { childList: true });
|
||||
|
||||
return function unwatchAddedNodes() {
|
||||
observer.disconnect();
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {typeof globalThis} [view=getWindow()]
|
||||
*/
|
||||
export function watchListeners(view = getWindow()) {
|
||||
const targets = getWatchedEventTargets(view);
|
||||
for (const target of targets) {
|
||||
target.addEventListener = mockedAddEventListener;
|
||||
target.removeEventListener = mockedRemoveEventListener;
|
||||
}
|
||||
|
||||
return function unwatchAllListeners() {
|
||||
for (const target of targets) {
|
||||
target.addEventListener = addEventListener;
|
||||
target.removeEventListener = removeEventListener;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a function checking that the given target does not contain any unexpected
|
||||
* key. The list of accepted keys is the initial list of keys of the target, along
|
||||
* with an optional `whiteList` argument.
|
||||
*
|
||||
* @template T
|
||||
* @param {T} target
|
||||
* @param {string[]} [whiteList]
|
||||
* @example
|
||||
* afterEach(watchKeys(window, ["odoo"]));
|
||||
*/
|
||||
export function watchKeys(target, whiteList) {
|
||||
const acceptedKeys = new Set([...$ownKeys(target), ...(whiteList || [])]);
|
||||
|
||||
return function checkKeys() {
|
||||
const keysDiff = $ownKeys(target).filter(
|
||||
(key) => $isNaN($parseFloat(key)) && !acceptedKeys.has(key)
|
||||
);
|
||||
for (const key of keysDiff) {
|
||||
const descriptor = $getOwnPropertyDescriptor(target, key);
|
||||
if (descriptor.configurable) {
|
||||
delete target[key];
|
||||
} else if (descriptor.writable) {
|
||||
target[key] = undefined;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue