[IMP] web: Improve date utils & localization consistency

This commit introduces various improvements to simplify, speed-up and/or
strengthen consistency in the date utility functions and localization
service.

It also takes care of removing luxon<->moment conversion helpers since
they are only used in the date picker, which has been rewritten.

Part of task 3121497

Part-of: odoo/odoo#112171
This commit is contained in:
Julien Mougenot
2023-05-05 13:52:21 +02:00
parent 6a6a1efbe2
commit caf40dfe2d
6 changed files with 284 additions and 309 deletions
+253 -197
View File
@@ -4,20 +4,42 @@ import { localization } from "@web/core/l10n/localization";
import { _t } from "@web/core/l10n/translation";
import { memoize } from "@web/core/utils/functions";
import { sprintf } from "@web/core/utils/strings";
import { ensureArray } from "../utils/arrays";
const { DateTime, Settings } = luxon;
/**
* @typedef ConversionOptions
* This is a list of the available options to either:
* - convert a DateTime to a string (format)
* - convert a string to a DateTime (parse)
* All of these are optional and the default values are issued by the Localization service.
*
* @property {string} [format]
* Format used to format a DateTime or to parse a formatted string.
* > Default: the session localization format.
*
* @typedef {luxon.DateTime} DateTime
*
* @typedef {[NullableDateTime, NullableDateTime]} NullableDateRange
*
* @typedef {DateTime | false | null | undefined} NullableDateTime
*/
/**
* Limits defining a valid date.
* This is needed because the server only understands 4-digit years.
* Note: both of these are in the local timezone
*/
export const MIN_VALID_DATE = DateTime.fromObject({ year: 1000 });
export const MAX_VALID_DATE = DateTime.fromObject({ year: 9999 }).endOf("year");
const SERVER_DATE_FORMAT = "yyyy-MM-dd";
const SERVER_TIME_FORMAT = "HH:mm:ss";
const SERVER_DATETIME_FORMAT = `${SERVER_DATE_FORMAT} ${SERVER_TIME_FORMAT}`;
// -----------------------------------------------------------------------------
// Helpers
// -----------------------------------------------------------------------------
const alphaRegex = /[a-zA-Z]/g;
const nonAlphaRegex = /[^a-zA-Z]/g;
const nonDigitsRegex = /[^0-9]/g;
const nonAlphaRegex = /[^a-z]/gi;
const nonDigitRegex = /[^\d]/g;
const normalizeFormatTable = {
// Python strftime to luxon.js conversion table
@@ -44,29 +66,129 @@ const normalizeFormatTable = {
X: "HH:mm:ss",
};
const luxonToMomentFormatTable = {
c: "d",
d: "D",
o: "DDDD",
a: "A",
y: "Y",
};
const smartDateUnits = {
d: "days",
m: "months",
w: "weeks",
y: "years",
};
const smartDateRegex = new RegExp(`^([+-])(\\d+)([${Object.keys(smartDateUnits).join("")}]?)$`);
const smartDateRegex = new RegExp(
["^", "([+-])", "(\\d+)", `([${Object.keys(smartDateUnits).join("")}]?)`, "$"].join("\\s*"),
"i"
);
/** @type {WeakMap<DateTime, string>} */
const dateCache = new WeakMap();
/** @type {WeakMap<DateTime, string>} */
const dateTimeCache = new WeakMap();
class ConversionError extends Error {
name = "ConversionError";
}
//-----------------------------------------------------------------------------
// Helpers
//-----------------------------------------------------------------------------
/**
* @param {any} d1
* @param {any} d2
* Checks whether 2 given dates or date ranges are equal. Both values are allowed
* to be falsy or to not be of the same type (which will return false).
*
* @param {NullableDateTime | NullableDateRange} d1
* @param {NullableDateTime | NullableDateRange} d2
* @returns {boolean}
*/
export function areDateEquals(d1, d2) {
return d1 instanceof DateTime && d2 instanceof DateTime ? d1.equals(d2) : d1 === d2;
export function areDatesEqual(d1, d2) {
if (Array.isArray(d1) || Array.isArray(d2)) {
// One of the values is a date range -> checks deep equality between the ranges
d1 = ensureArray(d1);
d2 = ensureArray(d2);
return d1.length === d2.length && d1.every((d1Val, i) => areDatesEqual(d1Val, d2[i]));
}
if (d1 instanceof DateTime && d2 instanceof DateTime && d1 !== d2) {
// Both values are DateTime objects -> use Luxon's comparison
return d1.equals(d2);
} else {
// One of the values is not a DateTime object -> fallback to strict equal
return d1 === d2;
}
}
/**
* @param {DateTime} desired
* @param {DateTime} minDate
* @param {DateTime} maxDate
*/
export function clampDate(desired, minDate, maxDate) {
if (_fastLocalDateCompare(maxDate, "<", desired)) {
return maxDate;
}
if (_fastLocalDateCompare(minDate, ">", desired)) {
return minDate;
}
return desired;
}
/**
* Returns whether the given format is a 24-hour format.
* Falls back to localization time format if none is given.
*
* @param {string} format
*/
export function is24HourFormat(format) {
return /H/.test(format || localization.timeFormat);
}
/**
* @param {NullableDateTime | NullableDateRange} value
* @param {NullableDateRange} range
*/
export function isInRange(value, range) {
if (!value || !range) {
return false;
}
if (Array.isArray(value)) {
const actualValues = value.filter(Boolean);
if (actualValues.length < 2) {
return isInRange(actualValues[0], range);
}
return (
(_fastLocalDateCompare(value[0], "<=", range[0]) &&
_fastLocalDateCompare(range[0], "<=", value[1])) ||
(_fastLocalDateCompare(range[0], "<=", value[0]) &&
_fastLocalDateCompare(value[0], "<=", range[1]))
);
} else {
return (
_fastLocalDateCompare(range[0], "<=", value) &&
_fastLocalDateCompare(value, "<=", range[1])
);
}
}
/**
* Returns whether the given format uses a meridiem suffix (AM/PM).
* Falls back to localization time format if none is given.
*
* @param {string} format
*/
export function isMeridiemFormat(format) {
return /a/.test(format || localization.timeFormat);
}
/**
* Returns whether the given DateTime is valid.
* The date is considered valid if it:
* - is a DateTime object
* - has the "isValid" flag set to true
* - is between 1000-01-01 and 9999-12-31 (both included)
* @see MIN_VALID_DATE
* @see MAX_VALID_DATE
*
* @param {NullableDateTime} date
*/
function isValidDate(date) {
return date && date.isValid && isInRange(date, [MIN_VALID_DATE, MAX_VALID_DATE]);
}
/**
@@ -80,14 +202,14 @@ export function areDateEquals(d1, d2) {
* "-4y" will return now + 4 years
*
* @param {string} value
* @returns {DateTime|false} Luxon datetime object (in the user's local timezone)
* @returns {NullableDateTime} Luxon datetime object (in the user's local timezone)
*/
function parseSmartDateInput(value) {
const match = smartDateRegex.exec(value);
const match = value.match(smartDateRegex);
if (match) {
let date = DateTime.local();
const offset = parseInt(match[2], 10);
const unit = smartDateUnits[match[3] || "d"];
const unit = smartDateUnits[(match[3] || "d").toLowerCase()];
if (match[1] === "+") {
date = date.plus({ [unit]: offset });
} else {
@@ -99,46 +221,32 @@ function parseSmartDateInput(value) {
}
/**
* Enforces some restrictions to a Luxon DateTime object.
* Returns it if within those restrictions.
* Returns false otherwise.
* Removes any duplicate *subsequent* alphabetic characters in a given string.
* Example: "aa-bb-CCcc-ddD-c xxxx-Yy-ZZ" -> "a-b-Cc-dD-c x-Yy-Z"
*
* @param {DateTime | false} date
* @returns {boolean}
*/
function isValidDateTime(dt) {
return dt && dt.isValid && dt.year >= 1000 && dt.year < 10000;
}
/**
* Removes any duplicated alphabetic characters in a given string.
* Example: "aa-bb-CCcc-ddD xxxx-Yy-ZZ" -> "a-b-Cc-dD x-Yy-Z"
*
* @param {string} str
* @returns {string}
* @type {(str: string) => string}
*/
const stripAlphaDupes = memoize(function stripAlphaDupes(str) {
return str.replace(alphaRegex, (letter, index, str) => {
return letter === str[index - 1] ? "" : letter;
});
return str.replace(/[a-z]/gi, (letter, index, str) =>
letter === str[index - 1] ? "" : letter
);
});
/**
* Convert Python strftime to escaped luxon.js format.
*
* @param {string} value original format
* @returns {string} valid Luxon format
* @type {(format: string) => string}
*/
export const strftimeToLuxonFormat = memoize(function strftimeToLuxonFormat(value) {
export const strftimeToLuxonFormat = memoize(function strftimeToLuxonFormat(format) {
const output = [];
let inToken = false;
for (let index = 0; index < value.length; ++index) {
let character = value[index];
for (let index = 0; index < format.length; ++index) {
let character = format[index];
if (character === "%" && !inToken) {
inToken = true;
continue;
}
if (character.match(alphaRegex)) {
if (/[a-z]/gi.test(character)) {
if (inToken && normalizeFormatTable[character] !== undefined) {
character = normalizeFormatTable[character];
} else {
@@ -152,133 +260,123 @@ export const strftimeToLuxonFormat = memoize(function strftimeToLuxonFormat(valu
});
/**
* Converts a Luxon format to a moment.js format.
* NB: this is not a complete conversion, only the supported tokens are converted.
*
* @param {string} value original format
* @returns {string} valid moment.js format
* Lazy getter returning the start of the current day.
*/
export const luxonToMomentFormat = memoize(function luxonToMomentFormat(format) {
return format.replace(alphaRegex, (match) => {
return luxonToMomentFormatTable[match] || match;
});
});
export function today() {
return DateTime.local().startOf("day");
}
/**
* Converts a luxon's DateTime object into a moment.js object.
* NB: the passed object's values will be utilized as is, regardless of its corresponding timezone.
* So passing a luxon's DateTime having 8 as hours value will result in a moment.js object having
* also 8 as hours value. But the moment.js object will be in the browser's timezone.
* Fast comparison of local dates.
* This is faster than Luxon's internal number conversions but only works on DateTime
* objects sharing the exact same locale and timezone.
*
* @param {DateTime} dt a luxon's DateTime object
* @returns {moment} a moment.js object in the browser's timezone
* @private
* @param {NullableDateTime} d1
* @param {"<" | "<=" | ">" | ">=" | "=" | "!="} operator
* @param {NullableDateTime} d2
*/
export function luxonToMoment(dt) {
if (dt.isValid) {
const o = dt.toObject();
// Note: the month is 0-based in moment.js, but 1-based in luxon.js
return moment({ ...o, month: o.month - 1 });
} else {
return moment.invalid();
function _fastLocalDateCompare(d1, operator, d2) {
if (!d1 || !d2) {
return false;
}
const a = d1.ts;
const b = d2.ts;
switch (operator) {
case "<": {
return a < b;
}
case "<=": {
return a <= b;
}
case ">": {
return a > b;
}
case ">=": {
return a >= b;
}
case "=": {
return a === b;
}
case "!=": {
return a !== b;
}
}
}
/**
* Converts a moment.js object into a luxon's DateTime object.
* NB: the passed object's values will be utilized as is, regardless of its corresponding timezone.
* So passing a moment.js object having 8 as hours value will result in a luxon's DateTime object
* having also 8 as hours value. But the luxon's DateTime object will be in the user's timezone.
*
* @param {moment} dt a moment.js object
* @returns {DateTime} a luxon's DateTime object in the user's timezone
*/
export function momentToLuxon(dt) {
const o = dt.toObject();
// Note: the month is 0-based in moment.js, but 1-based in luxon.js
return DateTime.fromObject({
year: o.years,
month: o.months + 1,
day: o.date,
hour: o.hours,
minute: o.minutes,
second: o.seconds,
millisecond: o.milliseconds,
});
}
// -----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
// Formatting
// -----------------------------------------------------------------------------
/**
* Returns true if the given format is a 24-hour format.
* Returns false otherwise.
*
* @param {string} [format=localization.timeFormat]
* @returns true if the format contains a 24 hour format
*/
export function is24HourFormat(format) {
return (format || localization.timeFormat).indexOf("H") !== -1;
}
//-----------------------------------------------------------------------------
/**
* Formats a DateTime object to a date string
*
* @see formatDateTime
* @returns {string}
* @param {NullableDateTime} value
* @param {ConversionOptions} [options={}]
*/
export function formatDate(value, options = {}) {
if (value === false) {
if (!value) {
return "";
}
const format = options.format || localization.dateFormat;
const numberingSystem = options.numberingSystem || Settings.defaultNumberingSystem || "latn";
return value.toFormat(format, { numberingSystem });
return value.toFormat(format);
}
/**
* Formats a DateTime object to a datetime string
*
* @param {DateTime | false} value
* @param {Object} options
* @param {string} [options.format]
* Provided format used to format the input DateTime object.
*
* Default=the session localization format.
*
* @param {string} [options.numberingSystem]
* Provided numbering system used to parse the input value.
*
* Default=the default numbering system assigned to luxon
* @see localization_service.js
*
* @returns {string}
* @param {NullableDateTime} value
* @param {ConversionOptions} [options={}]
*/
export function formatDateTime(value, options = {}) {
if (value === false) {
if (!value) {
return "";
}
const format = options.format || localization.dateTimeFormat;
const numberingSystem = options.numberingSystem || Settings.defaultNumberingSystem || "latn";
return value.setZone("default").toFormat(format, { numberingSystem });
return value.setZone("default").toFormat(format);
}
// -----------------------------------------------------------------------------
/**
* Formats the given DateTime to the server date format.
* @param {DateTime} value
*/
export function serializeDate(value) {
if (!dateCache.has(value)) {
dateCache.set(value, value.toFormat(SERVER_DATE_FORMAT, { numberingSystem: "latn" }));
}
return dateCache.get(value);
}
/**
* Formats the given DateTime to the server datetime format.
* @param {DateTime} value
*/
export function serializeDateTime(value) {
if (!dateTimeCache.has(value)) {
dateTimeCache.set(
value,
value.setZone("utc").toFormat(SERVER_DATETIME_FORMAT, { numberingSystem: "latn" })
);
}
return dateTimeCache.get(value);
}
//-----------------------------------------------------------------------------
// Parsing
// -----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
/**
* Parses a string value to a Luxon DateTime object.
*
* @param {string} value
* @param {ConversionOptions} [options={}]
*
* @see parseDateTime (Note: since we're only interested by the date itself, the
* returned value will always be set at the start of the day)
* @returns {DateTime | false} Luxon DateTime object in user's timezone
*/
export function parseDate(value, options = {}) {
if (!value) {
return false;
}
return parseDateTime(value, { format: localization.dateFormat, ...options }).startOf("day");
const parsed = parseDateTime(value, { format: localization.dateFormat, ...options });
return parsed && parsed.startOf("day");
}
/**
@@ -296,24 +394,9 @@ export function parseDate(value, options = {}) {
* e.g. "2020-01-01T12:00:00+06:00" with the user's timezone being UTC+1,
* the returned value will express the same timestamp but in UTC+1 (here time will be 7:00).
*
* @param {object} options
* @param {string} [options.format]
* Provided format used to parse the input value.
* @param {ConversionOptions} options
*
* Default=the session localization format
*
* @param {string} [options.locale]
* Provided locale used to parse the input value.
*
* Default=the session localization locale
*
* @param {string} [options.numberingSystem]
* Provided numbering system used to parse the input value.
*
* Default=the default numbering system assigned to luxon
* @see localization_service.js
*
* @returns {DateTime | false} Luxon DateTime object in user's timezone
* @returns {NullableDateTime} Luxon DateTime object in user's timezone
*/
export function parseDateTime(value, options = {}) {
if (!value) {
@@ -324,30 +407,28 @@ export function parseDateTime(value, options = {}) {
const parseOpts = {
setZone: true,
zone: "default",
locale: options.locale,
numberingSystem: options.numberingSystem || Settings.defaultNumberingSystem || "latn",
};
// Base case: try parsing with the given format and options
let result = DateTime.fromFormat(value, fmt, parseOpts);
// Try parsing as a smart date
if (!isValidDateTime(result)) {
if (!isValidDate(result)) {
result = parseSmartDateInput(value);
}
// Try parsing with partial date parts
if (!isValidDateTime(result)) {
if (!isValidDate(result)) {
const fmtWoZero = stripAlphaDupes(fmt);
result = DateTime.fromFormat(value, fmtWoZero, parseOpts);
}
// Try parsing with custom shorthand date parts
if (!isValidDateTime(result)) {
if (!isValidDate(result)) {
// Luxon is not permissive regarding delimiting characters in the format.
// So if the value to parse has less characters than the format, we would
// try to parse without the delimiting characters.
const digitList = value.split(nonDigitsRegex).filter(Boolean);
const digitList = value.split(nonDigitRegex).filter(Boolean);
const fmtList = fmt.split(nonAlphaRegex).filter(Boolean);
const valWoSeps = digitList.join("");
@@ -374,22 +455,22 @@ export function parseDateTime(value, options = {}) {
}
// Try with defaul ISO or SQL formats
if (!isValidDateTime(result)) {
if (!isValidDate(result)) {
// Also try some fallback formats, but only if value counts more than
// four digit characters as this could get misinterpreted as the time of
// the actual date.
const valueDigits = value.replace(nonDigitsRegex, "");
const valueDigits = value.replace(nonDigitRegex, "");
if (valueDigits.length > 4) {
result = DateTime.fromISO(value, parseOpts); // ISO8601
if (!isValidDateTime(result)) {
if (!isValidDate(result)) {
result = DateTime.fromSQL(value, parseOpts); // last try: SQL
}
}
}
// No working parsing methods: throw an error
if (!isValidDateTime(result)) {
throw new Error(sprintf(_t("'%s' is not a correct date or datetime"), value));
if (!isValidDate(result)) {
throw new ConversionError(sprintf(_t("'%s' is not a correct date or datetime"), value));
}
return result.setZone("default");
@@ -398,46 +479,21 @@ export function parseDateTime(value, options = {}) {
/**
* Returns a date object parsed from the given serialized string.
* @param {string} value serialized date string, e.g. "2018-01-01"
* @returns {DateTime} parsed date object in user's timezone
*/
export function deserializeDate(value) {
return DateTime.fromSQL(value, { zone: "default", numberingSystem: "latn" });
return DateTime.fromSQL(value, { numberingSystem: "latn", zone: "default" }).reconfigure({
numberingSystem: Settings.defaultNumberingSystem,
});
}
/**
* Returns a datetime object parsed from the given serialized string.
* @param {string} value serialized datetime string, e.g. "2018-01-01 00:00:00", expressed in UTC
* @returns {DateTime} parsed datetime object in user's timezone
*/
export function deserializeDateTime(value) {
return DateTime.fromSQL(value, { zone: "utc", numberingSystem: "latn" }).setZone("default");
}
const dateCache = new WeakMap();
/**
* Returns a serialized string representing the given date.
* @param {DateTime} value DateTime object, its timezone does not matter
* @returns {string} serialized date, ready to be sent to the server
*/
export function serializeDate(value) {
if (!dateCache.has(value)) {
dateCache.set(value, value.toFormat(SERVER_DATE_FORMAT, { numberingSystem: "latn" }));
}
return dateCache.get(value);
}
const dateTimeCache = new WeakMap();
/**
* Returns a serialized string representing the given datetime.
* @param {DateTime} value DateTime object, its timezone does not matter
* @returns {string} serialized datetime, ready to be sent to the server
*/
export function serializeDateTime(value) {
if (!dateTimeCache.has(value)) {
dateTimeCache.set(
value,
value.setZone("utc").toFormat(SERVER_DATETIME_FORMAT, { numberingSystem: "latn" })
);
}
return dateTimeCache.get(value);
return DateTime.fromSQL(value, { numberingSystem: "latn", zone: "utc" })
.setZone("default")
.reconfigure({
numberingSystem: Settings.defaultNumberingSystem,
});
}
@@ -5,7 +5,7 @@ import { browser } from "../browser/browser";
import { registry } from "../registry";
import { strftimeToLuxonFormat } from "./dates";
import { localization } from "./localization";
import { translatedTerms, _t } from "./translation";
import { _t, translatedTerms } from "./translation";
const { Settings } = luxon;
@@ -57,17 +57,16 @@ export const localizationService = {
Object.setPrototypeOf(translatedTerms, terms);
env._t = _t;
if (lang) {
// Setup lang inside luxon. The locale codes received from the server contain "_",
// whereas the Intl codes use "-" (Unicode BCP 47). There's only one exception, which
// is locale "sr@latin", for which we manually fallback to the "sr-Latn-RS" locale.
const locale = lang === "sr@latin" ? "sr-Latn-RS" : lang.replace(/_/g, "-");
Settings.defaultLocale = locale;
for (const [re, numberingSystem] of NUMBERING_SYSTEMS) {
if (re.test(locale)) {
Settings.defaultNumberingSystem = numberingSystem;
break;
}
// Setup lang inside luxon. The locale codes received from the server contain "_",
// whereas the Intl codes use "-" (Unicode BCP 47). There's only one exception, which
// is locale "sr@latin", for which we manually fallback to the "sr-Latn-RS" locale.
const language = lang || browser.navigator.language;
const locale = language === "sr@latin" ? "sr-Latn-RS" : language.replace(/_/g, "-");
Settings.defaultLocale = locale;
for (const [re, numberingSystem] of NUMBERING_SYSTEMS) {
if (re.test(locale)) {
Settings.defaultNumberingSystem = numberingSystem;
break;
}
}
@@ -1,17 +1,15 @@
/** @odoo-module **/
import {
deserializeDate,
deserializeDateTime,
formatDate,
formatDateTime,
parseDate,
parseDateTime,
strftimeToLuxonFormat,
serializeDate,
serializeDateTime,
deserializeDate,
deserializeDateTime,
momentToLuxon,
luxonToMoment,
strftimeToLuxonFormat,
} from "@web/core/l10n/dates";
import { localization } from "@web/core/l10n/localization";
import { patch, unpatch } from "@web/core/utils/patch";
@@ -73,10 +71,10 @@ function runTestSet(assert, testSet, options) {
QUnit.module(
"utils",
{
before() {
beforeEach() {
moment.suppressDeprecationWarnings = true;
},
after() {
afterEach() {
moment.suppressDeprecationWarnings = false;
},
},
@@ -532,70 +530,6 @@ QUnit.module(
);
});
QUnit.test("luxonToMoment", async (assert) => {
// Timezone is only patched for luxon, as we do not use the lib moment-timezone anyway.
patchTimeZone(330);
patchDate(2022, 1, 21, 15, 30, 0);
const luxonDate = DateTime.local().set({
millisecond: 0, // force 0ms due to test execution time
});
assert.strictEqual(luxonDate.toISO(), "2022-02-21T15:30:00.000+05:30");
const momentDate = luxonToMoment(luxonDate);
// Here we only assert the values of the moment object, as it may be
// in another timezone than the user's timezone (the patched one) anyway.
assert.deepEqual(momentDate.toObject(), {
years: 2022,
months: 1, // 0-based
date: 21,
hours: 15,
minutes: 30,
seconds: 0,
milliseconds: 0,
});
});
QUnit.test("momentToLuxon", async (assert) => {
// Timezone is only patched for luxon, as we do not use the lib moment-timezone anyway.
patchTimeZone(330);
// Patching the date after the having patched the timezone is important,
// as it will allow the native Date object to apply the correct timezone offset.
// BUT the native dates will still be in the browser's timezone...
patchDate(2022, 1, 21, 15, 30, 0);
// ...thus the created moment object will be in the browser's timezone.
const momentDate = moment().millisecond(0); // force 0ms due to test execution time
const momentHourOffset = momentDate.utcOffset() / 60;
// NB: asserting the moment offset is not relevant as it comes from the browser's TZ.
assert.deepEqual(momentDate.toObject(), {
years: 2022,
months: 1, // 0-based
date: 21,
hours: 10 + momentHourOffset,
minutes: 0,
seconds: 0,
milliseconds: 0,
});
// momentToluxon uses the moment object as is and outputs the same values in a luxon's
// DateTime object in the user's timezone...
const luxonDate = momentToLuxon(momentDate);
// ...so the below assert is correct even if we would have naturally
// expected something like "2022-02-21T15:30:00.000+05:30"
assert.deepEqual(luxonDate.toObject(), {
year: 2022,
month: 2, // 1-based
day: 21,
hour: 10 + momentHourOffset,
minute: 0,
second: 0,
millisecond: 0,
});
assert.strictEqual(luxonDate.offset, 330, "should be in user's timezone");
});
// -----------------------------------------------------------------------------------------
// -- Date utils legacy comparison -> TESTS in the below module will get removed someday! --
// -----------------------------------------------------------------------------------------
@@ -1,21 +1,20 @@
/** @odoo-module **/
import { Component, status } from "@odoo/owl";
import { browser } from "@web/core/browser/browser";
import { routerService } from "@web/core/browser/router_service";
import { effectService } from "@web/core/effects/effect_service";
import { localization } from "@web/core/l10n/localization";
import { _t } from "@web/core/l10n/translation";
import { rpcService } from "@web/core/network/rpc_service";
import { ormService } from "@web/core/orm_service";
import { overlayService } from "@web/core/overlay/overlay_service";
import { uiService } from "@web/core/ui/ui_service";
import { userService } from "@web/core/user_service";
import { effectService } from "@web/core/effects/effect_service";
import { objectToUrlEncodedString } from "@web/core/utils/urls";
import { ConnectionAbortedError } from "../../src/core/network/rpc_service";
import { registerCleanup } from "./cleanup";
import { patchWithCleanup } from "./utils";
import { uiService } from "@web/core/ui/ui_service";
import { ConnectionAbortedError } from "../../src/core/network/rpc_service";
import { overlayService } from "@web/core/overlay/overlay_service";
import { Component, status } from "@odoo/owl";
import { ormService } from "@web/core/orm_service";
// -----------------------------------------------------------------------------
// Mock Services
@@ -37,7 +36,8 @@ export const defaultLocalization = {
* @param {Partial<typeof defaultLocalization>} [config]
*/
export function makeFakeLocalizationService(config = {}) {
patchWithCleanup(localization, Object.assign({}, defaultLocalization, config));
patchWithCleanup(localization, { ...defaultLocalization, ...config });
patchWithCleanup(luxon.Settings, { defaultNumberingSystem: "latn" });
return {
name: "localization",
+4 -8
View File
@@ -114,11 +114,7 @@ export function patchDate(year, month, day, hours, minutes, seconds) {
* -120 => UTC-2
*/
export function patchTimeZone(offset) {
const originalZone = luxon.Settings.defaultZone;
luxon.Settings.defaultZone = new luxon.FixedOffsetZone.instance(offset);
registerCleanup(() => {
luxon.Settings.defaultZone = originalZone;
});
patchWithCleanup(luxon.Settings, { defaultZone: new luxon.FixedOffsetZone.instance(offset) });
}
let nextId = 1;
@@ -382,7 +378,7 @@ export function click(el, selector, skipVisibilityCheck = false) {
return triggerEvents(
el,
selector,
["pointerdown", "mousedown", "pointerup", "mouseup", "click"],
["pointerdown", "mousedown", "focus", "pointerup", "mouseup", "click"],
{ skipVisibilityCheck }
);
}
@@ -512,9 +508,8 @@ export async function editSelectMenu(el, selector, value) {
* @param {string} hotkey
* @param {boolean} addOverlayModParts
* @param {KeyboardEventInit} eventAttrs
* @returns {{ keydownEvent: KeyboardEvent, keyupEvent: KeyboardEvent }}
*/
export function triggerHotkey(hotkey, addOverlayModParts = false, eventAttrs = {}) {
export async function triggerHotkey(hotkey, addOverlayModParts = false, eventAttrs = {}) {
eventAttrs.key = hotkey.split("+").pop();
if (/shift/i.test(hotkey)) {
@@ -545,6 +540,7 @@ export function triggerHotkey(hotkey, addOverlayModParts = false, eventAttrs = {
const keyupEvent = new KeyboardEvent("keyup", eventAttrs);
document.activeElement.dispatchEvent(keydownEvent);
document.activeElement.dispatchEvent(keyupEvent);
await nextTick();
return { keydownEvent, keyupEvent };
}
@@ -1,25 +1,15 @@
/** @odoo-module **/
import { constructDateDomain } from "@web/search/utils/dates";
import { defaultLocalization } from "@web/../tests/helpers/mock_services";
import { patchDate, patchTimeZone } from "@web/../tests/helpers/utils";
import { Domain } from "@web/core/domain";
import { localization } from "@web/core/l10n/localization";
import { patch, unpatch } from "@web/core/utils/patch";
import { patchDate } from "@web/../tests/helpers/utils";
import { registerCleanup } from "@web/../tests/helpers/cleanup";
import { translatedTerms } from "@web/core/l10n/translation";
import { patch, unpatch } from "@web/core/utils/patch";
import { constructDateDomain } from "@web/search/utils/dates";
const { DateTime } = luxon;
function patchTimeZone(offset) {
const fixedZone = new luxon.FixedOffsetZone.instance(offset);
const originalZone = luxon.Settings.defaultZone;
luxon.Settings.defaultZone = fixedZone.name;
registerCleanup(() => {
luxon.Settings.defaultZone = originalZone;
});
}
QUnit.module("Search", () => {
QUnit.module("SearchUtils", {
beforeEach() {