UNPKG

cumulocity-cypress

Version:
313 lines (312 loc) 11.8 kB
import { FormatWidth, NgLocaleDataIndex, getNgLocale, getNgLocaleId, localizedDateFormat, localizedDateTimeFormat, localizedTimeFormat, registerDefaultLocales, registerLocale, } from "../locale/locale"; import * as dateFns from "date-fns"; import { throwError } from "../utils"; import { isValidDate, parseDate } from "../../shared/date"; const { _ } = Cypress; Cypress.datefns = dateFns; const defaultOptions = { log: true, invalid: "ignore", strictFormats: true, }; globalThis.registerLocale = registerLocale; globalThis.registerDefaultLocales = registerDefaultLocales; (async () => { await registerDefaultLocales(); })(); globalThis.setLocale = (localeId) => { const l = getNgLocale(localeId); if (l && _.isArray(l)) { Cypress.datefns.setDefaultOptions({ locale: l[NgLocaleDataIndex.DfnsLocale], }); } }; const isISODateSource = (arg) => { return arg != null && (_.isString(arg) || _.isNumber(arg) || _.isArray(arg)); }; const fromArguments = (args) => { let source = undefined; let options = undefined; if (args.length === 1) { source = args[0]; } else if (args.length > 1) { if (isISODateSource(args[1]) || typeof args[1] === "string") { source = args[1]; if (args.length > 2) { options = args[2]; } } else { source = args[0]; options = _.last(args); } } return [source, _.defaults({}, options, defaultOptions)]; }; Cypress.Commands.add("toDate", { prevSubject: "optional" }, (prevSubject, ...args) => { const [unsafeSource, options] = fromArguments([prevSubject, ...args]); const localizedFormats = options != null ? prepareLocalizedFormats(options) : []; const win = cy.state("window"); const language = options?.language ?? win.localStorage.getItem("c8y_language") ?? "en"; const consoleProps = options?.consoleProps ?? {}; if (!consoleProps.options) { consoleProps.options = options || null; } consoleProps.language = `${language} (${getNgLocaleId(language)})`; consoleProps.localizedFormats = localizedFormats || null; consoleProps.source = unsafeSource || null; let logger = options?.logger; let ourlogger = false; if (options?.log === true && options?.consoleProps == null) { logger = Cypress.log({ name: "toDate", message: `${unsafeSource || null}`, consoleProps: () => consoleProps, autoEnd: false, }); ourlogger = true; } if (!unsafeSource || !(_.isString(unsafeSource) || _.isNumber(unsafeSource) || (Array.isArray(unsafeSource) && !_.isEmpty(unsafeSource) && (unsafeSource.every((item) => typeof item === "string") || unsafeSource.every((item) => typeof item === "number"))))) { logger?.end(); throwError(`No or undefined source provided to cy.toDate.`); } const source = unsafeSource; const input = Array.isArray(source) ? source : [source]; const formats = []; let dates = input.map((item) => { let parsedDate; // try to read date from Angular date formats or number for (const format of localizedFormats) { parsedDate = parseDate(item, format); if (isValidDate(parsedDate)) { formats.push(format); return parsedDate; } } // try to read as ISO date if (!isValidDate(parsedDate) && _.isString(item)) { parsedDate = Cypress.datefns.parseISO(item); if (isValidDate(parsedDate)) { formats.push(undefined); } } // try to read as Date last. this might have some unexpected result if (!isValidDate(parsedDate) && options?.strictFormats === false) { parsedDate = new Date(item); if (isValidDate(parsedDate)) { formats.push(undefined); } } if (isValidDate(parsedDate)) { return parsedDate; } if (options?.invalid === "throw") { throwError(`'${item?.toString()}' could not be converted into a valid date. No matching format or invalid input.`); } return undefined; }); if (options?.invalid === "ignore") { dates = dates.filter((date) => date); if (_.isEmpty(dates)) { if (ourlogger) { logger?.end(); } return undefined; } } const result = Array.isArray(source) ? dates : dates[0]; consoleProps["Format"] = (Array.isArray(source) ? formats : formats[0]) || null; consoleProps["Yielded"] = result || null; if (ourlogger) { logger?.end(); } cy.wrap(result, { log: false }); }); Cypress.Commands.add("toISODate", { prevSubject: "optional" }, (prevSubject, ...args) => { const [source, options] = fromArguments([prevSubject, ...args]); const consoleProps = { source: source || null, options, }; let log = undefined; if (options?.log === true || options?.log == null) { log = Cypress.log({ name: "toISODate", message: `${source || null}`, consoleProps: () => consoleProps, autoEnd: false, }); } const o = { ...options, ...{ log: false, consoleProps, logger: log } }; cy.toDate(source, o).then((dates) => { const sources = Array.isArray(source) ? source : [source]; const d = Array.isArray(dates) ? dates : [dates]; let isoStrings = d.map((date, index) => { const defaultValue = options?.invalid === "keep" ? sources[index] : undefined; return date ? date.toISOString() : defaultValue; }); if (options?.invalid === "ignore") { isoStrings = isoStrings.filter((iso) => iso); if (_.isEmpty(isoStrings)) { log?.end(); return cy.wrap(Array.isArray(source) ? [] : undefined, { log: false, }); } } const result = Array.isArray(source) ? isoStrings : isoStrings[0]; consoleProps["Dates"] = dates || null; consoleProps["Yielded"] = result || null; log?.end(); cy.wrap(result, { log: false }); }); }); Cypress.Commands.add("dateFormat", { prevSubject: "optional" }, (prevSubject, ...args) => { const [source, opts] = fromArguments([prevSubject, ...args]); const options = _.pick(opts, ["invalid", "language", "log"]); const localizedFormats = prepareLocalizedFormats(options); const win = cy.state("window"); const language = options?.language ?? win.localStorage.getItem("c8y_language") ?? "en"; const consoleProps = { source, options, language: `${language} (${getNgLocaleId(language)})`, localizedFormats, }; if (options?.log === true) { Cypress.log({ name: "dateFormat", message: source, consoleProps: () => consoleProps, }); } if (!source) { throwError(`No or undefined provided source provided to cy.dateFormat.`); } const format = findDateFormatForSource(source, localizedFormats); if (!format && options?.invalid === "throw") { throwError(`'${source?.toString()}' could not be converted into a valid date. No matching format or invalid input.`); } consoleProps.yielded = format; cy.wrap(format, { log: false }); }); Cypress.Commands.add("compareDates", // @ts-expect-error { prevSubject: "optional" }, (prevSubject, source, target, options = defaultOptions) => { if ((!source && prevSubject) || (_.isObjectLike(source) && !_.isArray(source))) { source = prevSubject; } if (_.isString(target)) { target = Cypress.datefns.parseISO(target); if (!target) { throwError(`${target} is not a valid ISO formatted date.`); } } const localizedFormats = prepareLocalizedFormats(options).reverse(); const win = cy.state("window"); const language = options?.language ?? win.localStorage.getItem("c8y_language") ?? "en"; const consoleProps = { source, target, options, language: `${language} (${getNgLocaleId(language)})`, localizedFormats, }; if (options?.log === true) { Cypress.log({ name: "compareDates", message: source, consoleProps: () => consoleProps, }); } consoleProps.target = target; const unsafeFortmat = findDateFormatForSource(source, localizedFormats, true); consoleProps.format = unsafeFortmat; if (!unsafeFortmat) { if (options?.invalid === "throw") { throwError(`'${source?.toString()}' could not be converted into a valid date. No matching format or invalid input.`); } else { return cy.wrap(false); } } const format = unsafeFortmat; const formattedTarget = Cypress.datefns.format(target, format); consoleProps.formattedTarget = formattedTarget; if (formattedTarget) { return cy.wrap(_.isEqual(source, formattedTarget)); } else { throwError(`'${target?.toString()}' could not be formatted as string using ${format}.`); } return cy.wrap(false); }); function findDateFormatForSource(source, localizedFormats, requireRoundTrip = false) { if (!source) return undefined; const normalizedSource = source.replace(/[\u00A0\u202F]/g, " "); for (const format of localizedFormats) { const parsed = parseDate(source, format); if (!isValidDate(parsed)) { continue; } if (requireRoundTrip) { const roundTrip = Cypress.datefns .format(parsed, format) .replace(/[\u00A0\u202F]/g, " "); if (roundTrip !== normalizedSource) { continue; } } return format; } return undefined; } function prepareLocalizedFormats(options) { const win = cy.state("window"); const language = options?.language ?? win.localStorage.getItem("c8y_language") ?? "en"; const formatWidths = options?.formatWidth ? [options.formatWidth] : Object.values(FormatWidth).filter((n) => _.isNumber(n)); let localizedFormats; if (options?.format) { localizedFormats = [options?.format]; } else { const dateTimeFormats = formatWidths.map((f) => localizedDateTimeFormat(language, f)); const dateFormats = formatWidths.map((f) => localizedDateFormat(language, f)); const timeFormats = formatWidths.map((f) => localizedTimeFormat(language, f)); // Angular locale data can omit long/full dateTime patterns and fall back to short. // Compose additional combinations so legacy strings (for example, with "at") still parse. const combinedDateTimeFormats = dateFormats.flatMap((date) => { return timeFormats.flatMap((time) => [ `${date}, ${time}`, `${date} 'at' ${time}`, ]); }); localizedFormats = [ ...dateTimeFormats, ...combinedDateTimeFormats, ...dateFormats, ...timeFormats, ]; } // date-fns does not use z...zzzz. fix or converion will fail // https://github.com/date-fns/date-fns/issues/2088 return _.uniq(localizedFormats).map((format) => { let result = format.replace("zzzz", "'GMT'X"); result = result.replace("z", "X"); result = result.replace("OOOO", "'GMT'X"); result = result.replace("O", "X"); return result; }); }