mirror of
https://github.com/bringout/oca-ocb-core.git
synced 2026-04-20 14:31:59 +02:00
19.0 vanilla
This commit is contained in:
parent
d1963a3c3a
commit
2d3ee4855a
7430 changed files with 2687981 additions and 2965473 deletions
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import { on } from "@odoo/hoot-dom";
|
||||
import { MockEventTarget } from "../hoot_utils";
|
||||
import { ensureTest } from "../main_runner";
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Global
|
||||
|
|
@ -95,6 +96,7 @@ export function cleanupAnimations() {
|
|||
* @param {boolean} [enable=false]
|
||||
*/
|
||||
export function disableAnimations(enable = false) {
|
||||
ensureTest("disableAnimations");
|
||||
allowAnimations = enable;
|
||||
}
|
||||
|
||||
|
|
@ -105,6 +107,7 @@ export function disableAnimations(enable = false) {
|
|||
* @param {boolean} [enable=true]
|
||||
*/
|
||||
export function enableTransitions(enable = true) {
|
||||
ensureTest("enableTransitions");
|
||||
allowTransitions = enable;
|
||||
animationChangeBus.dispatchEvent(new CustomEvent("toggle-transitions"));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,22 +1,71 @@
|
|||
/** @odoo-module */
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Global
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
const {
|
||||
Math: { random: $random, floor: $floor },
|
||||
TextEncoder,
|
||||
} = globalThis;
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Internal
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
/** @type {SubtleCrypto["decrypt"]} */
|
||||
function decrypt(algorithm, key, data) {
|
||||
return Promise.resolve($encode(data.replace("encrypted data:", "")));
|
||||
}
|
||||
|
||||
/** @type {SubtleCrypto["encrypt"]} */
|
||||
function encrypt(algorithm, key, data) {
|
||||
return Promise.resolve(`encrypted data:${$decode(data)}`);
|
||||
}
|
||||
|
||||
/** @type {Crypto["getRandomValues"]} */
|
||||
function getRandomValues(array) {
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
array[i] = $floor($random() * 256);
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
/** @type {SubtleCrypto["importKey"]} */
|
||||
function importKey(format, keyData, algorithm, extractable, keyUsages) {
|
||||
if (arguments.length < 5) {
|
||||
throw new TypeError(
|
||||
`Failed to execute 'importKey' on 'SubtleCrypto': 5 arguments required, but only ${arguments.length} present.`
|
||||
);
|
||||
}
|
||||
if (!keyData || keyData.length === 0) {
|
||||
throw new TypeError(
|
||||
`Failed to execute 'importKey' on 'SubtleCrypto': The provided value cannot be converted to a sequence.`
|
||||
);
|
||||
}
|
||||
const key = Symbol([algorithm, String(extractable), "secret", ...keyUsages].join("/"));
|
||||
return Promise.resolve(key);
|
||||
}
|
||||
|
||||
function randomUUID() {
|
||||
return [4, 2, 2, 2, 6]
|
||||
.map((length) => getRandomValues(new Uint8Array(length)).toHex())
|
||||
.join("-");
|
||||
}
|
||||
|
||||
const $encode = TextEncoder.prototype.encode.bind(new TextEncoder());
|
||||
const $decode = TextDecoder.prototype.decode.bind(new TextDecoder("utf-8"));
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Exports
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
export const mockCrypto = {
|
||||
getRandomValues,
|
||||
randomUUID,
|
||||
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;
|
||||
decrypt,
|
||||
encrypt,
|
||||
importKey,
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import { getTimeOffset, isTimeFrozen, resetTimeOffset } from "@web/../lib/hoot-dom/helpers/time";
|
||||
import { createMock, HootError, isNil } from "../hoot_utils";
|
||||
import { ensureTest } from "../main_runner";
|
||||
|
||||
/**
|
||||
* @typedef DateSpecs
|
||||
|
|
@ -161,7 +162,7 @@ export function cleanupDate() {
|
|||
* @see {@link mockTimeZone} for the time zone params.
|
||||
*
|
||||
* @param {string | DateSpecs} [date]
|
||||
* @param {string | number | null} [tz]
|
||||
* @param {string | number | null} [tz]
|
||||
* @example
|
||||
* mockDate("2023-12-25T20:45:00"); // 2023-12-25 20:45:00 UTC
|
||||
* @example
|
||||
|
|
@ -170,6 +171,7 @@ export function cleanupDate() {
|
|||
* mockDate("2019-02-11 09:30:00.001", +2);
|
||||
*/
|
||||
export function mockDate(date, tz) {
|
||||
ensureTest("mockDate");
|
||||
setDateParams(date ? parseDateParams(date) : DEFAULT_DATE);
|
||||
if (!isNil(tz)) {
|
||||
setTimeZone(tz);
|
||||
|
|
@ -187,6 +189,7 @@ export function mockDate(date, tz) {
|
|||
* mockTimeZone("ja-JP"); // UTC + 9
|
||||
*/
|
||||
export function mockLocale(newLocale) {
|
||||
ensureTest("mockLocale");
|
||||
locale = newLocale;
|
||||
|
||||
if (!isNil(locale) && isNil(timeZoneName)) {
|
||||
|
|
@ -213,6 +216,7 @@ export function mockLocale(newLocale) {
|
|||
* mockTimeZone(null) // Resets to test default (+1)
|
||||
*/
|
||||
export function mockTimeZone(tz) {
|
||||
ensureTest("mockTimeZone");
|
||||
setTimeZone(tz);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,15 @@
|
|||
/** @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";
|
||||
import {
|
||||
createMock,
|
||||
getSyncValue,
|
||||
HootError,
|
||||
MIME_TYPE,
|
||||
MockEventTarget,
|
||||
setSyncValue,
|
||||
} from "../hoot_utils";
|
||||
import { ensureTest } from "../main_runner";
|
||||
|
||||
/**
|
||||
* @typedef {"android" | "ios" | "linux" | "mac" | "windows"} Platform
|
||||
|
|
@ -220,7 +227,7 @@ export class MockClipboardItem extends ClipboardItem {
|
|||
// Added synchronous methods to enhance speed in tests
|
||||
|
||||
async getType(type) {
|
||||
return getSyncValue(this)[type];
|
||||
return getSyncValue(this, false)[type];
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -264,18 +271,137 @@ export class MockPermissionStatus extends MockEventTarget {
|
|||
}
|
||||
}
|
||||
|
||||
export class MockServiceWorker extends MockEventTarget {
|
||||
static publicListeners = ["error", "message"];
|
||||
|
||||
/** @type {ServiceWorkerState} */
|
||||
state = "activated";
|
||||
|
||||
/**
|
||||
* @param {string} scriptURL
|
||||
*/
|
||||
constructor(scriptURL) {
|
||||
super(...arguments);
|
||||
|
||||
/** @type {string} */
|
||||
this.scriptURL = scriptURL;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {any} _message
|
||||
*/
|
||||
postMessage(_message) {}
|
||||
}
|
||||
|
||||
export class MockServiceWorkerContainer extends MockEventTarget {
|
||||
static publicListeners = ["controllerchange", "message", "messageerror"];
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_readyResolved = false;
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @type {Map<string, MockServiceWorkerRegistration>}
|
||||
*/
|
||||
_registrations = new Map();
|
||||
|
||||
/** @type {MockServiceWorker | null} */
|
||||
controller = null;
|
||||
|
||||
get ready() {
|
||||
return this._readyPromise;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
|
||||
const { promise, resolve } = Promise.withResolvers();
|
||||
/**
|
||||
* @type {Promise<MockServiceWorkerRegistration>}
|
||||
* @private
|
||||
*/
|
||||
this._readyPromise = promise;
|
||||
/**
|
||||
* @type {(value: MockServiceWorkerRegistration) => void}
|
||||
* @private
|
||||
*/
|
||||
this._resolveReady = resolve;
|
||||
}
|
||||
|
||||
async getRegistration(scope = "/") {
|
||||
return this._registrations.get(scope);
|
||||
}
|
||||
|
||||
async getRegistrations() {
|
||||
return Array.from(this._registrations.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} scriptURL
|
||||
* @param {{ scope?: string }} [options]
|
||||
*/
|
||||
async register(scriptURL, options = {}) {
|
||||
const scope = options.scope ?? "/";
|
||||
|
||||
const registration = new MockServiceWorkerRegistration(scriptURL, scope);
|
||||
this._registrations.set(scope, registration);
|
||||
|
||||
if (!this.controller) {
|
||||
this.controller = registration.active;
|
||||
this.dispatchEvent(new Event("controllerchange"));
|
||||
}
|
||||
|
||||
if (!this._readyResolved) {
|
||||
this._readyResolved = true;
|
||||
this._resolveReady(registration);
|
||||
}
|
||||
|
||||
return registration;
|
||||
}
|
||||
|
||||
startMessages() {}
|
||||
}
|
||||
|
||||
export class MockServiceWorkerRegistration {
|
||||
/** @type {MockServiceWorker | null} */
|
||||
installing = null;
|
||||
/** @type {MockServiceWorker | null} */
|
||||
waiting = null;
|
||||
|
||||
/**
|
||||
* @param {string} scriptURL
|
||||
* @param {string} scope
|
||||
*/
|
||||
constructor(scriptURL, scope) {
|
||||
/** @type {MockServiceWorker | null} */
|
||||
this.active = new MockServiceWorker(scriptURL);
|
||||
/** @type {string} */
|
||||
this.scope = scope;
|
||||
}
|
||||
|
||||
async unregister() {
|
||||
return true;
|
||||
}
|
||||
|
||||
async update() {}
|
||||
}
|
||||
|
||||
export const currentPermissions = getPermissions();
|
||||
|
||||
export const mockClipboard = new MockClipboard();
|
||||
|
||||
export const mockPermissions = new MockPermissions();
|
||||
|
||||
export const mockServiceWorker = new MockServiceWorkerContainer();
|
||||
|
||||
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 },
|
||||
serviceWorker: { value: mockServiceWorker },
|
||||
userAgent: { get: () => mockValues.userAgent },
|
||||
vibrate: { get: () => mockValues.vibrate },
|
||||
});
|
||||
|
|
@ -291,6 +417,7 @@ export function cleanupNavigator() {
|
|||
* @param {PermissionState} [value]
|
||||
*/
|
||||
export function mockPermission(name, value) {
|
||||
ensureTest("mockPermission");
|
||||
if (!(name in currentPermissions)) {
|
||||
throw new TypeError(
|
||||
`The provided value '${name}' is not a valid enum value of type PermissionName`
|
||||
|
|
@ -310,6 +437,7 @@ export function mockPermission(name, value) {
|
|||
* @param {Navigator["sendBeacon"]} callback
|
||||
*/
|
||||
export function mockSendBeacon(callback) {
|
||||
ensureTest("mockSendBeacon");
|
||||
mockValues.sendBeacon = callback;
|
||||
}
|
||||
|
||||
|
|
@ -317,6 +445,7 @@ export function mockSendBeacon(callback) {
|
|||
* @param {Platform} platform
|
||||
*/
|
||||
export function mockUserAgent(platform = "linux") {
|
||||
ensureTest("mockUserAgent");
|
||||
mockValues.userAgent = makeUserAgent(platform);
|
||||
}
|
||||
|
||||
|
|
@ -324,5 +453,6 @@ export function mockUserAgent(platform = "linux") {
|
|||
* @param {Navigator["vibrate"]} callback
|
||||
*/
|
||||
export function mockVibrate(callback) {
|
||||
ensureTest("mockVibrate");
|
||||
mockValues.vibrate = callback;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,10 +7,22 @@ import {
|
|||
} from "@web/../lib/hoot-dom/helpers/time";
|
||||
import { isInstanceOf } from "../../hoot-dom/hoot_dom_utils";
|
||||
import { makeNetworkLogger } from "../core/logger";
|
||||
import { ensureArray, MIME_TYPE, MockEventTarget } from "../hoot_utils";
|
||||
import { getSyncValue, MockBlob, setSyncValue } from "./sync_values";
|
||||
import {
|
||||
ensureArray,
|
||||
getSyncValue,
|
||||
isNil,
|
||||
MIME_TYPE,
|
||||
MockEventTarget,
|
||||
setSyncValue,
|
||||
} from "../hoot_utils";
|
||||
import { ensureTest } from "../main_runner";
|
||||
|
||||
/**
|
||||
* @typedef {ResponseInit & {
|
||||
* type?: ResponseType;
|
||||
* url?: string;
|
||||
* }} MockResponseInit
|
||||
*
|
||||
* @typedef {AbortController
|
||||
* | MockBroadcastChannel
|
||||
* | MockMessageChannel
|
||||
|
|
@ -28,19 +40,32 @@ import { getSyncValue, MockBlob, setSyncValue } from "./sync_values";
|
|||
|
||||
const {
|
||||
AbortController,
|
||||
Blob,
|
||||
BroadcastChannel,
|
||||
document,
|
||||
fetch,
|
||||
FormData,
|
||||
Headers,
|
||||
Map,
|
||||
Math: { floor: $floor, max: $max, min: $min, random: $random },
|
||||
Object: { assign: $assign, create: $create, entries: $entries },
|
||||
Object: {
|
||||
assign: $assign,
|
||||
create: $create,
|
||||
defineProperty: $defineProperty,
|
||||
entries: $entries,
|
||||
},
|
||||
ProgressEvent,
|
||||
ReadableStream,
|
||||
Request,
|
||||
Response,
|
||||
Set,
|
||||
TextEncoder,
|
||||
Uint8Array,
|
||||
URL,
|
||||
WebSocket,
|
||||
XMLHttpRequest,
|
||||
} = globalThis;
|
||||
const { parse: $parse, stringify: $stringify } = globalThis.JSON;
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Internal
|
||||
|
|
@ -86,6 +111,34 @@ async function dispatchMessage(target, data, transfer) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {{ headers?: HeadersInit } | undefined} object
|
||||
* @param {string} content
|
||||
*/
|
||||
function getHeaders(object, content) {
|
||||
/** @type {Headers} */
|
||||
let headers;
|
||||
if (isInstanceOf(object?.headers, Headers)) {
|
||||
headers = object.headers;
|
||||
} else {
|
||||
headers = new Headers(object?.headers);
|
||||
}
|
||||
|
||||
if (content && !headers.has(HEADER.contentType)) {
|
||||
if (typeof content === "string") {
|
||||
headers.set(HEADER.contentType, MIME_TYPE.text);
|
||||
} else if (isInstanceOf(content, Blob)) {
|
||||
headers.set(HEADER.contentType, MIME_TYPE.blob);
|
||||
} else if (isInstanceOf(content, FormData)) {
|
||||
headers.set(HEADER.contentType, MIME_TYPE.formData);
|
||||
} else {
|
||||
headers.set(HEADER.contentType, MIME_TYPE.json);
|
||||
}
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {...NetworkInstance} instances
|
||||
*/
|
||||
|
|
@ -111,6 +164,37 @@ function markOpen(instance) {
|
|||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper used to parse JSON-RPC request/response parameters, and to make their
|
||||
* "jsonrpc", "id" and "method" properties non-enumerable, as to make them more
|
||||
* inconspicuous in console logs, effectively highlighting the 'params' or 'result'
|
||||
* keys.
|
||||
*
|
||||
* @param {string} stringParams
|
||||
*/
|
||||
function parseJsonRpcParams(stringParams) {
|
||||
const jsonParams = $assign($create(null), $parse(stringParams));
|
||||
if (jsonParams && "jsonrpc" in jsonParams) {
|
||||
$defineProperty(jsonParams, "jsonrpc", {
|
||||
value: jsonParams.jsonrpc,
|
||||
enumerable: false,
|
||||
});
|
||||
if ("id" in jsonParams) {
|
||||
$defineProperty(jsonParams, "id", {
|
||||
value: jsonParams.id,
|
||||
enumerable: false,
|
||||
});
|
||||
}
|
||||
if ("method" in jsonParams) {
|
||||
$defineProperty(jsonParams, "method", {
|
||||
value: jsonParams.method,
|
||||
enumerable: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
return jsonParams;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} min
|
||||
* @param {number} [max]
|
||||
|
|
@ -130,15 +214,50 @@ function parseNetworkDelay(min, max) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Uint8Array<ArrayBuffer> | string} value
|
||||
* @returns {Uint8Array<ArrayBuffer>}
|
||||
*/
|
||||
function toBytes(value) {
|
||||
return isInstanceOf(value, Uint8Array) ? value : new TextEncoder().encode(value);
|
||||
}
|
||||
|
||||
const DEFAULT_URL = "https://www.hoot.test/";
|
||||
const ENDLESS_PROMISE = new Promise(() => {});
|
||||
const HEADER = {
|
||||
contentLength: "Content-Length",
|
||||
contentType: "Content-Type",
|
||||
};
|
||||
const R_EQUAL = /\s*=\s*/;
|
||||
const R_INTERNAL_URL = /^(blob|data|file):/;
|
||||
const R_INTERNAL_URL = /^(blob|data):/;
|
||||
const R_SEMICOLON = /\s*;\s*/;
|
||||
|
||||
const requestResponseMixin = {
|
||||
async arrayBuffer() {
|
||||
return toBytes(this._readValue("arrayBuffer", true)).buffer;
|
||||
},
|
||||
async blob() {
|
||||
const value = this._readValue("blob", false);
|
||||
return isInstanceOf(value, Blob) ? value : new MockBlob([value]);
|
||||
},
|
||||
async bytes() {
|
||||
return toBytes(this._readValue("bytes", true));
|
||||
},
|
||||
async formData() {
|
||||
const data = this._readValue("formData", false);
|
||||
if (!isInstanceOf(data, FormData)) {
|
||||
throw new TypeError("Failed to fetch");
|
||||
}
|
||||
return data;
|
||||
},
|
||||
async json() {
|
||||
return $parse(this._readValue("json", true));
|
||||
},
|
||||
async text() {
|
||||
return this._readValue("text", true);
|
||||
},
|
||||
};
|
||||
|
||||
/** @type {Set<NetworkInstance>} */
|
||||
const openNetworkInstances = new Set();
|
||||
|
||||
|
|
@ -148,8 +267,8 @@ let getNetworkDelay = null;
|
|||
let mockFetchFn = null;
|
||||
/** @type {((websocket: ServerWebSocket) => any) | null} */
|
||||
let mockWebSocketConnection = null;
|
||||
/** @type {Array<(worker: MockSharedWorker | MockWorker) => any>} */
|
||||
let mockWorkerConnection = [];
|
||||
/** @type {((worker: MockSharedWorker | MockWorker) => any)[]} */
|
||||
const mockWorkerConnections = [];
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Exports
|
||||
|
|
@ -159,7 +278,7 @@ export function cleanupNetwork() {
|
|||
// Mocked functions
|
||||
mockFetchFn = null;
|
||||
mockWebSocketConnection = null;
|
||||
mockWorkerConnection = [];
|
||||
mockWorkerConnections.length = 0;
|
||||
|
||||
// Network instances
|
||||
for (const instance of openNetworkInstances) {
|
||||
|
|
@ -194,33 +313,57 @@ export function cleanupNetwork() {
|
|||
|
||||
/** @type {typeof fetch} */
|
||||
export async function mockedFetch(input, init) {
|
||||
if (R_INTERNAL_URL.test(input)) {
|
||||
// Internal URL: directly handled by the browser
|
||||
return fetch(input, init);
|
||||
}
|
||||
const strInput = String(input);
|
||||
const isInternalUrl = R_INTERNAL_URL.test(strInput);
|
||||
if (!mockFetchFn) {
|
||||
throw new Error("Can't make a request when fetch is not mocked");
|
||||
if (isInternalUrl) {
|
||||
// Internal URL without mocked 'fetch': directly handled by the browser
|
||||
return fetch(input, init);
|
||||
}
|
||||
throw new Error(
|
||||
`Could not fetch "${strInput}": cannot make a request when fetch is not mocked`
|
||||
);
|
||||
}
|
||||
init ||= {};
|
||||
const method = init.method?.toUpperCase() || (init.body ? "POST" : "GET");
|
||||
const { logRequest, logResponse } = makeNetworkLogger(method, input);
|
||||
|
||||
const controller = markOpen(new AbortController());
|
||||
init.signal = controller.signal;
|
||||
|
||||
logRequest(() => (typeof init.body === "string" ? JSON.parse(init.body) : init));
|
||||
init = { ...init };
|
||||
init.headers = getHeaders(init, init.body);
|
||||
init.method = init.method?.toUpperCase() || (isNil(init.body) ? "GET" : "POST");
|
||||
|
||||
// Allows 'signal' to not be logged with 'logRequest'.
|
||||
$defineProperty(init, "signal", {
|
||||
value: controller.signal,
|
||||
enumerable: false,
|
||||
});
|
||||
|
||||
const { logRequest, logResponse } = makeNetworkLogger(init.method, strInput);
|
||||
|
||||
logRequest(() => {
|
||||
const readableInit = {
|
||||
...init,
|
||||
// Make headers easier to read in the console
|
||||
headers: new Map(init.headers),
|
||||
};
|
||||
if (init.headers.get(HEADER.contentType) === MIME_TYPE.json) {
|
||||
return [parseJsonRpcParams(init.body), readableInit];
|
||||
} else {
|
||||
return [init.body, readableInit];
|
||||
}
|
||||
});
|
||||
|
||||
if (getNetworkDelay) {
|
||||
await getNetworkDelay();
|
||||
}
|
||||
|
||||
// keep separate from 'error', as it can be null or undefined even though the
|
||||
// callback has thrown an error.
|
||||
let failed = false;
|
||||
let result;
|
||||
let error, result;
|
||||
try {
|
||||
result = await mockFetchFn(input, init);
|
||||
} catch (err) {
|
||||
result = err;
|
||||
failed = true;
|
||||
error = err;
|
||||
}
|
||||
if (isOpen(controller)) {
|
||||
markClosed(controller);
|
||||
|
|
@ -228,33 +371,34 @@ export async function mockedFetch(input, init) {
|
|||
return ENDLESS_PROMISE;
|
||||
}
|
||||
if (failed) {
|
||||
throw result;
|
||||
throw error;
|
||||
}
|
||||
|
||||
/** @type {Headers} */
|
||||
let headers;
|
||||
if (result && isInstanceOf(result.headers, Headers)) {
|
||||
headers = result.headers;
|
||||
} else if (isInstanceOf(init.headers, Headers)) {
|
||||
headers = init.headers;
|
||||
} else {
|
||||
headers = new Headers(init.headers);
|
||||
if (isInternalUrl && isNil(result)) {
|
||||
// Internal URL without mocked result: directly handled by the browser
|
||||
return fetch(input, init);
|
||||
}
|
||||
|
||||
let contentType = headers.get(HEADER.contentType);
|
||||
// Result can be a request or the final request value
|
||||
const responseHeaders = getHeaders(result, result);
|
||||
|
||||
if (result instanceof MockResponse) {
|
||||
// Mocked response
|
||||
logResponse(async () => {
|
||||
const textValue = getSyncValue(result);
|
||||
return contentType === MIME_TYPE.json ? JSON.parse(textValue) : textValue;
|
||||
logResponse(() => {
|
||||
const textValue = getSyncValue(result, true);
|
||||
return [
|
||||
responseHeaders.get(HEADER.contentType) === MIME_TYPE.json
|
||||
? parseJsonRpcParams(textValue)
|
||||
: textValue,
|
||||
result,
|
||||
];
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
if (isInstanceOf(result, Response)) {
|
||||
// Actual fetch
|
||||
logResponse(() => "(go to network tab for request content)");
|
||||
logResponse(() => ["(go to network tab for request content)", result]);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
@ -262,26 +406,28 @@ export async function mockedFetch(input, init) {
|
|||
// Determine the return type based on:
|
||||
// - the content type header
|
||||
// - or the type of the returned value
|
||||
if (!contentType) {
|
||||
if (typeof result === "string") {
|
||||
contentType = MIME_TYPE.text;
|
||||
} else if (isInstanceOf(result, Blob)) {
|
||||
contentType = MIME_TYPE.blob;
|
||||
} else {
|
||||
contentType = MIME_TYPE.json;
|
||||
}
|
||||
}
|
||||
|
||||
if (contentType === MIME_TYPE.json) {
|
||||
if (responseHeaders.get(HEADER.contentType) === MIME_TYPE.json) {
|
||||
// JSON response
|
||||
const strBody = JSON.stringify(result ?? null);
|
||||
logResponse(() => result);
|
||||
return new MockResponse(strBody, { [HEADER.contentType]: contentType });
|
||||
const strBody = $stringify(result ?? null);
|
||||
const response = new MockResponse(strBody, {
|
||||
headers: responseHeaders,
|
||||
statusText: "OK",
|
||||
type: "basic",
|
||||
url: strInput,
|
||||
});
|
||||
logResponse(() => [parseJsonRpcParams(strBody), response]);
|
||||
return response;
|
||||
}
|
||||
|
||||
// Any other type (blob / text)
|
||||
logResponse(() => result);
|
||||
return new MockResponse(result, { [HEADER.contentType]: contentType });
|
||||
// Any other type
|
||||
const response = new MockResponse(result, {
|
||||
headers: responseHeaders,
|
||||
statusText: "OK",
|
||||
type: "basic",
|
||||
url: strInput,
|
||||
});
|
||||
logResponse(() => [result, response]);
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -310,6 +456,7 @@ export async function mockedFetch(input, init) {
|
|||
* });
|
||||
*/
|
||||
export function mockFetch(fetchFn) {
|
||||
ensureTest("mockFetch");
|
||||
mockFetchFn = fetchFn;
|
||||
}
|
||||
|
||||
|
|
@ -321,6 +468,7 @@ export function mockFetch(fetchFn) {
|
|||
* @param {typeof mockWebSocketConnection} [onWebSocketConnected]
|
||||
*/
|
||||
export function mockWebSocket(onWebSocketConnected) {
|
||||
ensureTest("mockWebSocket");
|
||||
mockWebSocketConnection = onWebSocketConnected;
|
||||
}
|
||||
|
||||
|
|
@ -330,7 +478,7 @@ export function mockWebSocket(onWebSocketConnected) {
|
|||
* (see {@link mockFetch});
|
||||
* - the `onWorkerConnected` callback will be called after a worker has been created.
|
||||
*
|
||||
* @param {typeof mockWorkerConnection} [onWorkerConnected]
|
||||
* @param {typeof mockWorkerConnections[number]} [onWorkerConnected]
|
||||
* @example
|
||||
* mockWorker((worker) => {
|
||||
* worker.addEventListener("message", (event) => {
|
||||
|
|
@ -339,7 +487,8 @@ export function mockWebSocket(onWebSocketConnected) {
|
|||
* });
|
||||
*/
|
||||
export function mockWorker(onWorkerConnected) {
|
||||
mockWorkerConnection.push(onWorkerConnected);
|
||||
ensureTest("mockWorker");
|
||||
mockWorkerConnections.push(onWorkerConnected);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -349,6 +498,42 @@ export function throttleNetwork(...args) {
|
|||
getNetworkDelay = parseNetworkDelay(...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {typeof mockFetchFn} fetchFn
|
||||
* @param {() => void} callback
|
||||
*/
|
||||
export async function withFetch(fetchFn, callback) {
|
||||
mockFetchFn = fetchFn;
|
||||
const result = await callback();
|
||||
mockFetchFn = null;
|
||||
return result;
|
||||
}
|
||||
|
||||
export class MockBlob extends Blob {
|
||||
constructor(blobParts, options) {
|
||||
super(blobParts, options);
|
||||
|
||||
setSyncValue(this, blobParts);
|
||||
}
|
||||
|
||||
async arrayBuffer() {
|
||||
return toBytes(getSyncValue(this, true)).buffer;
|
||||
}
|
||||
|
||||
async bytes() {
|
||||
return toBytes(getSyncValue(this, true));
|
||||
}
|
||||
|
||||
async stream() {
|
||||
const value = getSyncValue(this, true);
|
||||
return isInstanceOf(value, ReadableStream) ? value : new ReadableStream(value);
|
||||
}
|
||||
|
||||
async text() {
|
||||
return getSyncValue(this, true);
|
||||
}
|
||||
}
|
||||
|
||||
export class MockBroadcastChannel extends BroadcastChannel {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
|
|
@ -650,58 +835,91 @@ export class MockMessagePort extends MockEventTarget {
|
|||
}
|
||||
|
||||
export class MockRequest extends Request {
|
||||
static {
|
||||
Object.assign(this.prototype, requestResponseMixin);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {RequestInfo} input
|
||||
* @param {RequestInit} [init]
|
||||
*/
|
||||
constructor(input, init) {
|
||||
super(input, init);
|
||||
super(new MockURL(input), init);
|
||||
|
||||
setSyncValue(this, init?.body ?? null);
|
||||
}
|
||||
|
||||
async arrayBuffer() {
|
||||
return new TextEncoder().encode(getSyncValue(this));
|
||||
clone() {
|
||||
const request = new this.constructor(this.url, this);
|
||||
setSyncValue(request, getSyncValue(this, false));
|
||||
return request;
|
||||
}
|
||||
|
||||
async blob() {
|
||||
return new MockBlob([getSyncValue(this)]);
|
||||
}
|
||||
|
||||
async json() {
|
||||
return JSON.parse(getSyncValue(this));
|
||||
}
|
||||
|
||||
async text() {
|
||||
return getSyncValue(this);
|
||||
/**
|
||||
* In tests, requests objects are expected to be read by multiple network handlers.
|
||||
* As such, their 'body' isn't consumed upon reading.
|
||||
*
|
||||
* @protected
|
||||
* @param {string} reader
|
||||
* @param {boolean} toStringValue
|
||||
*/
|
||||
_readValue(reader, toStringValue) {
|
||||
return getSyncValue(this, toStringValue);
|
||||
}
|
||||
}
|
||||
|
||||
export class MockResponse extends Response {
|
||||
static {
|
||||
Object.assign(this.prototype, requestResponseMixin);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {BodyInit} body
|
||||
* @param {ResponseInit} [init]
|
||||
* @param {MockResponseInit} [init]
|
||||
*/
|
||||
constructor(body, init) {
|
||||
super(body, init);
|
||||
|
||||
if (init?.type) {
|
||||
$defineProperty(this, "type", {
|
||||
value: init.type,
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
writable: false,
|
||||
});
|
||||
}
|
||||
if (init?.url) {
|
||||
$defineProperty(this, "url", {
|
||||
value: String(new MockURL(init.url)),
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
writable: false,
|
||||
});
|
||||
}
|
||||
|
||||
setSyncValue(this, body ?? null);
|
||||
}
|
||||
|
||||
async arrayBuffer() {
|
||||
return new TextEncoder().encode(getSyncValue(this)).buffer;
|
||||
clone() {
|
||||
return new this.constructor(getSyncValue(this, false), this);
|
||||
}
|
||||
|
||||
async blob() {
|
||||
return new MockBlob([getSyncValue(this)]);
|
||||
}
|
||||
|
||||
async json() {
|
||||
return JSON.parse(getSyncValue(this));
|
||||
}
|
||||
|
||||
async text() {
|
||||
return getSyncValue(this);
|
||||
/**
|
||||
* Reading the 'body' of a response always consumes it, as opposed to the {@link MockRequest}
|
||||
* body.
|
||||
*
|
||||
* @protected
|
||||
* @param {string} reader
|
||||
* @param {boolean} toStringValue
|
||||
*/
|
||||
_readValue(reader, toStringValue) {
|
||||
if (this.bodyUsed) {
|
||||
throw new TypeError(
|
||||
`Failed to execute '${reader}' on '${this.constructor.name}': body stream already read`
|
||||
);
|
||||
}
|
||||
$defineProperty(this, "bodyUsed", { value: true, configurable: true, enumerable: true });
|
||||
return getSyncValue(this, toStringValue);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -732,7 +950,7 @@ export class MockSharedWorker extends MockEventTarget {
|
|||
// First port has to be started manually
|
||||
this._messageChannel.port2.start();
|
||||
|
||||
for (const onWorkerConnected of mockWorkerConnection) {
|
||||
for (const onWorkerConnected of mockWorkerConnections) {
|
||||
onWorkerConnected(this);
|
||||
}
|
||||
}
|
||||
|
|
@ -745,6 +963,10 @@ export class MockURL extends URL {
|
|||
}
|
||||
|
||||
export class MockWebSocket extends MockEventTarget {
|
||||
static CONNECTING = 0;
|
||||
static OPEN = 1;
|
||||
static CLOSING = 2;
|
||||
static CLOSED = 3;
|
||||
static publicListeners = ["close", "error", "message", "open"];
|
||||
|
||||
/**
|
||||
|
|
@ -788,7 +1010,7 @@ export class MockWebSocket extends MockEventTarget {
|
|||
markOpen(this);
|
||||
|
||||
this._readyState = WebSocket.OPEN;
|
||||
this._logger.logRequest(() => "connection open");
|
||||
this._logger.logRequest(() => ["connection open"]);
|
||||
|
||||
this.dispatchEvent(new Event("open"));
|
||||
});
|
||||
|
|
@ -808,7 +1030,7 @@ export class MockWebSocket extends MockEventTarget {
|
|||
if (this.readyState !== WebSocket.OPEN) {
|
||||
return;
|
||||
}
|
||||
this._logger.logRequest(() => data);
|
||||
this._logger.logRequest(() => [data]);
|
||||
dispatchMessage(this._serverWs, data);
|
||||
}
|
||||
}
|
||||
|
|
@ -839,7 +1061,7 @@ export class MockWorker extends MockEventTarget {
|
|||
this.dispatchEvent(new MessageEvent("message", { data: ev.data }));
|
||||
});
|
||||
|
||||
for (const onWorkerConnected of mockWorkerConnection) {
|
||||
for (const onWorkerConnected of mockWorkerConnections) {
|
||||
onWorkerConnected(this);
|
||||
}
|
||||
}
|
||||
|
|
@ -865,11 +1087,11 @@ export class MockWorker extends MockEventTarget {
|
|||
|
||||
export class MockXMLHttpRequest extends MockEventTarget {
|
||||
static publicListeners = ["error", "load"];
|
||||
static {
|
||||
// Assign status codes
|
||||
Object.assign(this, XMLHttpRequest);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_headers = {};
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
|
|
@ -877,28 +1099,72 @@ export class MockXMLHttpRequest extends MockEventTarget {
|
|||
/**
|
||||
* @private
|
||||
*/
|
||||
_readyState = XMLHttpRequest.UNSENT;
|
||||
/**
|
||||
* @type {Record<string, string>}
|
||||
* @private
|
||||
*/
|
||||
_requestHeaders = Object.create(null);
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_requestUrl = "";
|
||||
/**
|
||||
* @type {Response | null}
|
||||
* @private
|
||||
*/
|
||||
_response = null;
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_status = XMLHttpRequest.UNSENT;
|
||||
_responseMimeType = "";
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_url = "";
|
||||
_responseValue = null;
|
||||
|
||||
abort() {
|
||||
markClosed(this);
|
||||
get readyState() {
|
||||
return this._readyState;
|
||||
}
|
||||
|
||||
upload = new MockXMLHttpRequestUpload();
|
||||
|
||||
get response() {
|
||||
return this._response;
|
||||
return this._responseValue;
|
||||
}
|
||||
|
||||
get responseText() {
|
||||
return String(this._responseValue);
|
||||
}
|
||||
|
||||
get responseURL() {
|
||||
return this._response.url;
|
||||
}
|
||||
|
||||
get responseXML() {
|
||||
const parser = new DOMParser();
|
||||
try {
|
||||
return parser.parseFromString(this._responseValue, this._responseMimeType);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
get status() {
|
||||
return this._status;
|
||||
return this._response?.status || 0;
|
||||
}
|
||||
|
||||
get statusText() {
|
||||
return this._readyState >= XMLHttpRequest.LOADING ? "OK" : "";
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {XMLHttpRequestResponseType}
|
||||
*/
|
||||
responseType = "";
|
||||
upload = new MockXMLHttpRequestUpload();
|
||||
|
||||
abort() {
|
||||
this._setReadyState(XMLHttpRequest.DONE);
|
||||
markClosed(this);
|
||||
}
|
||||
|
||||
/** @type {XMLHttpRequest["dispatchEvent"]} */
|
||||
|
|
@ -909,12 +1175,31 @@ export class MockXMLHttpRequest extends MockEventTarget {
|
|||
return super.dispatchEvent(event);
|
||||
}
|
||||
|
||||
getAllResponseHeaders() {
|
||||
let result = "";
|
||||
for (const [key, value] of this._response?.headers || []) {
|
||||
result += `${key}: ${value}\r\n`;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** @type {XMLHttpRequest["getResponseHeader"]} */
|
||||
getResponseHeader(name) {
|
||||
return this._response?.headers.get(name) || "";
|
||||
}
|
||||
|
||||
/** @type {XMLHttpRequest["open"]} */
|
||||
open(method, url) {
|
||||
markOpen(this);
|
||||
|
||||
this._method = method;
|
||||
this._url = url;
|
||||
this._requestUrl = url;
|
||||
this._setReadyState(XMLHttpRequest.OPENED);
|
||||
}
|
||||
|
||||
/** @type {XMLHttpRequest["overrideMimeType"]} */
|
||||
overrideMimeType(mime) {
|
||||
this._responseMimeType = mime;
|
||||
}
|
||||
|
||||
/** @type {XMLHttpRequest["send"]} */
|
||||
|
|
@ -922,35 +1207,56 @@ export class MockXMLHttpRequest extends MockEventTarget {
|
|||
if (!isOpen(this)) {
|
||||
return ENDLESS_PROMISE;
|
||||
}
|
||||
this._setReadyState(XMLHttpRequest.HEADERS_RECEIVED);
|
||||
|
||||
try {
|
||||
const response = await window.fetch(this._url, {
|
||||
this._response = await window.fetch(this._requestUrl, {
|
||||
method: this._method,
|
||||
body,
|
||||
headers: this._headers,
|
||||
headers: this._requestHeaders,
|
||||
});
|
||||
this._status = response.status;
|
||||
if (new URL(this._url, mockLocation.origin).protocol === "blob:") {
|
||||
this._response = await response.arrayBuffer();
|
||||
this._setReadyState(XMLHttpRequest.LOADING);
|
||||
if (!this._responseMimeType) {
|
||||
if (this._response.url.startsWith("blob:")) {
|
||||
this._responseMimeType = MIME_TYPE.blob;
|
||||
} else {
|
||||
this._responseMimeType = this._response.headers.get(HEADER.contentType);
|
||||
}
|
||||
}
|
||||
if (this._response instanceof MockResponse) {
|
||||
// Mock response: get bound value (synchronously)
|
||||
this._responseValue = getSyncValue(this._response, false);
|
||||
} else if (this._responseMimeType === MIME_TYPE.blob) {
|
||||
// Actual "blob:" response: get array buffer
|
||||
this._responseValue = await this._response.arrayBuffer();
|
||||
} else if (this._responseMimeType === MIME_TYPE.json) {
|
||||
// JSON response: get parsed JSON value
|
||||
this._responseValue = await this._response.json();
|
||||
} else {
|
||||
this._response = await response.text();
|
||||
// Anything else: parse response body as text
|
||||
this._responseValue = await this._response.text();
|
||||
}
|
||||
this.dispatchEvent(new ProgressEvent("load"));
|
||||
} catch (error) {
|
||||
this.dispatchEvent(new ProgressEvent("error", { error }));
|
||||
} catch {
|
||||
this.dispatchEvent(new ProgressEvent("error"));
|
||||
}
|
||||
|
||||
this._setReadyState(XMLHttpRequest.DONE);
|
||||
markClosed(this);
|
||||
}
|
||||
|
||||
/** @type {XMLHttpRequest["setRequestHeader"]} */
|
||||
setRequestHeader(name, value) {
|
||||
this._headers[name] = value;
|
||||
this._requestHeaders[name] = value;
|
||||
}
|
||||
|
||||
/** @type {XMLHttpRequest["getResponseHeader"]} */
|
||||
getResponseHeader(name) {
|
||||
return this._headers[name];
|
||||
/**
|
||||
* @private
|
||||
* @param {number} readyState
|
||||
*/
|
||||
_setReadyState(readyState) {
|
||||
this._readyState = readyState;
|
||||
this.dispatchEvent(new Event("readystatechange"));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -989,7 +1295,6 @@ export class ServerWebSocket extends MockEventTarget {
|
|||
/**
|
||||
* @param {WebSocket} websocket
|
||||
* @param {ReturnType<typeof makeNetworkLogger>} logger
|
||||
* @param {ReturnType<typeof makeNetworkLogger>} logger
|
||||
*/
|
||||
constructor(websocket, logger) {
|
||||
super();
|
||||
|
|
@ -1001,7 +1306,7 @@ export class ServerWebSocket extends MockEventTarget {
|
|||
|
||||
this.addEventListener("close", (ev) => {
|
||||
dispatchClose(this._clientWs, ev);
|
||||
this._logger.logResponse(() => "connection closed");
|
||||
this._logger.logResponse(() => ["connection closed", ev]);
|
||||
});
|
||||
|
||||
mockWebSocketConnection?.(this);
|
||||
|
|
@ -1018,7 +1323,7 @@ export class ServerWebSocket extends MockEventTarget {
|
|||
if (!isOpen(this)) {
|
||||
return;
|
||||
}
|
||||
this._logger.logResponse(() => data);
|
||||
this._logger.logResponse(() => [data]);
|
||||
dispatchMessage(this._clientWs, data);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,48 +0,0 @@
|
|||
/** @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("");
|
||||
}
|
||||
}
|
||||
|
|
@ -12,7 +12,7 @@ import {
|
|||
} 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 { ensureTest, getRunner } from "../main_runner";
|
||||
import {
|
||||
MockAnimation,
|
||||
mockedAnimate,
|
||||
|
|
@ -25,9 +25,11 @@ import {
|
|||
mockedWindowScrollTo,
|
||||
} from "./animation";
|
||||
import { MockConsole } from "./console";
|
||||
import { mockCrypto } from "./crypto";
|
||||
import { MockDate, MockIntl } from "./date";
|
||||
import { MockClipboardItem, mockNavigator } from "./navigator";
|
||||
import {
|
||||
MockBlob,
|
||||
MockBroadcastChannel,
|
||||
MockMessageChannel,
|
||||
MockMessagePort,
|
||||
|
|
@ -46,8 +48,6 @@ import {
|
|||
} from "./network";
|
||||
import { MockNotification } from "./notification";
|
||||
import { MockStorage } from "./storage";
|
||||
import { MockBlob } from "./sync_values";
|
||||
import { mockCrypto } from "./crypto";
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Global
|
||||
|
|
@ -61,6 +61,7 @@ const {
|
|||
Object: {
|
||||
assign: $assign,
|
||||
defineProperties: $defineProperties,
|
||||
defineProperty: $defineProperty,
|
||||
entries: $entries,
|
||||
getOwnPropertyDescriptor: $getOwnPropertyDescriptor,
|
||||
getPrototypeOf: $getPrototypeOf,
|
||||
|
|
@ -70,9 +71,11 @@ const {
|
|||
Reflect: { ownKeys: $ownKeys },
|
||||
Set,
|
||||
WeakMap,
|
||||
WeakSet,
|
||||
} = globalThis;
|
||||
|
||||
const { addEventListener, removeEventListener } = EventTarget.prototype;
|
||||
const { preventDefault } = Event.prototype;
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Internal
|
||||
|
|
@ -327,6 +330,11 @@ function mockedMatchMedia(mediaQueryString) {
|
|||
return new MockMediaQueryList(mediaQueryString);
|
||||
}
|
||||
|
||||
function mockedPreventDefault() {
|
||||
preventedEvents.add(this);
|
||||
return preventDefault.call(this, ...arguments);
|
||||
}
|
||||
|
||||
/** @type {typeof removeEventListener} */
|
||||
function mockedRemoveEventListener(...args) {
|
||||
if (getRunner().dry) {
|
||||
|
|
@ -420,6 +428,18 @@ class MockMediaQueryList extends MockEventTarget {
|
|||
}
|
||||
}
|
||||
|
||||
class MockMutationObserver extends MutationObserver {
|
||||
disconnect() {
|
||||
activeMutationObservers.delete(this);
|
||||
return super.disconnect(...arguments);
|
||||
}
|
||||
|
||||
observe() {
|
||||
activeMutationObservers.add(this);
|
||||
return super.observe(...arguments);
|
||||
}
|
||||
}
|
||||
|
||||
const DEFAULT_MEDIA_VALUES = {
|
||||
"display-mode": "browser",
|
||||
pointer: "fine",
|
||||
|
|
@ -438,12 +458,16 @@ const R_OWL_SYNTHETIC_LISTENER = /\bnativeToSyntheticEvent\b/;
|
|||
const originalDescriptors = new WeakMap();
|
||||
const originalTouchFunctions = getTouchTargets(globalThis).map(getTouchDescriptors);
|
||||
|
||||
/** @type {Set<MockMutationObserver>} */
|
||||
const activeMutationObservers = new Set();
|
||||
/** @type {Set<MockMediaQueryList>} */
|
||||
const mediaQueryLists = new Set();
|
||||
const mockConsole = new MockConsole();
|
||||
const mockLocalStorage = new MockStorage();
|
||||
const mockMediaValues = { ...DEFAULT_MEDIA_VALUES };
|
||||
const mockSessionStorage = new MockStorage();
|
||||
/** @type {WeakSet<Event>} */
|
||||
const preventedEvents = new WeakSet();
|
||||
let mockTitle = "";
|
||||
|
||||
// Mock descriptors
|
||||
|
|
@ -493,6 +517,7 @@ const WINDOW_MOCK_DESCRIPTORS = {
|
|||
matchMedia: { value: mockedMatchMedia },
|
||||
MessageChannel: { value: MockMessageChannel },
|
||||
MessagePort: { value: MockMessagePort },
|
||||
MutationObserver: { value: MockMutationObserver },
|
||||
navigator: { value: mockNavigator },
|
||||
Notification: { value: MockNotification },
|
||||
outerHeight: { get: () => getCurrentDimensions().height },
|
||||
|
|
@ -547,6 +572,11 @@ export function cleanupWindow() {
|
|||
|
||||
// Touch
|
||||
restoreTouch(view);
|
||||
|
||||
// Mutation observers
|
||||
for (const observer of activeMutationObservers) {
|
||||
observer.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
export function getTitle() {
|
||||
|
|
@ -579,19 +609,37 @@ export function getViewPortWidth() {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Event} event
|
||||
*/
|
||||
export function isPrevented(event) {
|
||||
return event.defaultPrevented || preventedEvents.has(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, string>} name
|
||||
*/
|
||||
export function mockMatchMedia(values) {
|
||||
ensureTest("mockMatchMedia");
|
||||
$assign(mockMediaValues, values);
|
||||
|
||||
callMediaQueryChanges($keys(values));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Event} event
|
||||
*/
|
||||
export function mockPreventDefault(event) {
|
||||
$defineProperty(event, "preventDefault", {
|
||||
value: mockedPreventDefault,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {boolean} setTouch
|
||||
*/
|
||||
export function mockTouch(setTouch) {
|
||||
ensureTest("mockTouch");
|
||||
const objects = getTouchTargets(getWindow());
|
||||
if (setTouch) {
|
||||
for (const object of objects) {
|
||||
|
|
@ -695,13 +743,13 @@ export function watchListeners(view = getWindow()) {
|
|||
* afterEach(watchKeys(window, ["odoo"]));
|
||||
*/
|
||||
export function watchKeys(target, whiteList) {
|
||||
const acceptedKeys = new Set([...$ownKeys(target), ...(whiteList || [])]);
|
||||
const acceptedKeys = new Set($ownKeys(target).concat(whiteList || []));
|
||||
|
||||
return function checkKeys() {
|
||||
const keysDiff = $ownKeys(target).filter(
|
||||
(key) => $isNaN($parseFloat(key)) && !acceptedKeys.has(key)
|
||||
);
|
||||
for (const key of keysDiff) {
|
||||
for (const key of $ownKeys(target)) {
|
||||
if (!$isNaN($parseFloat(key)) || acceptedKeys.has(key)) {
|
||||
continue;
|
||||
}
|
||||
const descriptor = $getOwnPropertyDescriptor(target, key);
|
||||
if (descriptor.configurable) {
|
||||
delete target[key];
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue