From 25f95af418d5c3d3425066b0e10276e292cba403 Mon Sep 17 00:00:00 2001 From: Joffrey JAFFEUX Date: Wed, 8 Apr 2020 08:53:21 +0200 Subject: [PATCH] REFACTOR: local dates to improve reliability with DST and recurrence (#9379) This commit improves testing and separates local-date generation from dates with zone manipulations. --- .../javascripts/discourse-local-dates.js.es6 | 357 -------------- .../initializers/discourse-local-dates.js.es6 | 77 ++- .../lib/date-with-zone-helper.js.es6 | 89 ++++ .../javascripts/lib/local-date-builder.js.es6 | 243 +++++++++ plugins/discourse-local-dates/plugin.rb | 1 - .../acceptance/local-dates-test.js.es6 | 463 ------------------ .../lib/date-with-zone-helper-test.js.es6 | 168 +++++++ .../lib/local-date-builder-test.js.es6 | 420 ++++++++++++++++ 8 files changed, 994 insertions(+), 824 deletions(-) delete mode 100644 plugins/discourse-local-dates/assets/javascripts/discourse-local-dates.js.es6 create mode 100644 plugins/discourse-local-dates/assets/javascripts/lib/date-with-zone-helper.js.es6 create mode 100644 plugins/discourse-local-dates/assets/javascripts/lib/local-date-builder.js.es6 delete mode 100644 plugins/discourse-local-dates/test/javascripts/acceptance/local-dates-test.js.es6 create mode 100644 plugins/discourse-local-dates/test/javascripts/lib/date-with-zone-helper-test.js.es6 create mode 100644 plugins/discourse-local-dates/test/javascripts/lib/local-date-builder-test.js.es6 diff --git a/plugins/discourse-local-dates/assets/javascripts/discourse-local-dates.js.es6 b/plugins/discourse-local-dates/assets/javascripts/discourse-local-dates.js.es6 deleted file mode 100644 index aba41b68cef..00000000000 --- a/plugins/discourse-local-dates/assets/javascripts/discourse-local-dates.js.es6 +++ /dev/null @@ -1,357 +0,0 @@ -// discourse-skip-module -(function($) { - const DATE_TEMPLATE = ` - - - - - - - `; - - const PREVIEW_TEMPLATE = ` -
- - -
- `; - - function processElement($element, options = {}) { - clearTimeout(this.timeout); - - const utc = moment().utc(); - const dateTime = options.time - ? `${options.date} ${options.time}` - : options.date; - - let displayedTimezone; - if (options.time) { - displayedTimezone = options.displayedTimezone || moment.tz.guess(); - } else { - displayedTimezone = - options.displayedTimezone || options.timezone || moment.tz.guess(); - } - - // if timezone given we convert date and time from given zone to Etc/UTC - let utcDateTime; - if (options.timezone) { - utcDateTime = _applyZoneToDateTime(dateTime, options.timezone); - } else { - utcDateTime = moment.utc(dateTime); - } - - if (utcDateTime < utc) { - // if event is in the past we want to bump it no next occurrence when - // recurring is set - if (options.recurring) { - utcDateTime = _applyRecurrence(utcDateTime, options); - } else { - $element.addClass("past"); - } - } - - // once we have the correct UTC date we want - // we adjust it to watching user timezone - const adjustedDateTime = utcDateTime.tz(displayedTimezone); - - const previews = _generatePreviews( - adjustedDateTime.clone(), - displayedTimezone, - options - ); - const textPreview = _generateTextPreview(previews); - const htmlPreview = _generateHtmlPreview(previews); - - const formatedDateTime = _applyFormatting( - adjustedDateTime, - displayedTimezone, - options - ); - - $element - .html(DATE_TEMPLATE) - .attr("aria-label", textPreview) - .attr( - "data-html-tooltip", - `
${htmlPreview}
` - ) - .addClass("cooked-date") - .find(".relative-time") - .text(formatedDateTime); - - this.timeout = setTimeout( - () => processElement($element, options), - 60 * 1000 - ); - } - - function _formatTimezone(timezone) { - return timezone - .replace("_", " ") - .replace("Etc/", "") - .split("/"); - } - - function _zoneWithoutPrefix(timezone) { - const parts = _formatTimezone(timezone); - return parts[1] || parts[0]; - } - - function _applyZoneToDateTime(dateTime, timezone) { - return moment.tz(dateTime, timezone).utc(); - } - - function _translateCalendarKey(time, key) { - const translated = I18n.t(`discourse_local_dates.relative_dates.${key}`, { - time: "LT" - }); - - if (time) { - return translated - .split("LT") - .map(w => `[${w}]`) - .join("LT"); - } else { - return `[${translated.replace(" LT", "")}]`; - } - } - - function _calendarFormats(time) { - return { - sameDay: _translateCalendarKey(time, "today"), - nextDay: _translateCalendarKey(time, "tomorrow"), - lastDay: _translateCalendarKey(time, "yesterday"), - sameElse: "L" - }; - } - - function _isEqualZones(timezoneA, timezoneB) { - if ((timezoneA || timezoneB) && (!timezoneA || !timezoneB)) { - return false; - } - - if (timezoneA.includes(timezoneB) || timezoneB.includes(timezoneA)) { - return true; - } - - return ( - moment.tz(timezoneA).utcOffset() === moment.tz(timezoneB).utcOffset() - ); - } - - function _applyFormatting(dateTime, displayedTimezone, options) { - if (options.countdown) { - const diffTime = dateTime.diff(moment()); - if (diffTime < 0) { - return I18n.t("discourse_local_dates.relative_dates.countdown.passed"); - } else { - return moment.duration(diffTime).humanize(); - } - } - - const sameTimezone = _isEqualZones(displayedTimezone, moment.tz.guess()); - const inCalendarRange = dateTime.isBetween( - moment().subtract(2, "days"), - moment() - .add(1, "days") - .endOf("day") - ); - - if (options.calendar && inCalendarRange) { - if (sameTimezone) { - if (options.time) { - dateTime = dateTime.calendar(null, _calendarFormats(options.time)); - } else { - dateTime = dateTime.calendar(null, _calendarFormats(null)); - } - } else { - dateTime = dateTime.format(options.format); - dateTime = dateTime.replace("TZ", ""); - dateTime = `${dateTime} (${_zoneWithoutPrefix(displayedTimezone)})`; - } - } else { - if (options.time) { - dateTime = dateTime.format(options.format); - - if (options.displayedTimezone && !sameTimezone) { - dateTime = dateTime.replace("TZ", ""); - dateTime = `${dateTime} (${_zoneWithoutPrefix(displayedTimezone)})`; - } else { - dateTime = dateTime.replace( - "TZ", - _formatTimezone(displayedTimezone).join(": ") - ); - } - } else { - dateTime = dateTime.format(options.format); - - if (!sameTimezone) { - dateTime = dateTime.replace("TZ", ""); - dateTime = `${dateTime} (${_zoneWithoutPrefix(displayedTimezone)})`; - } else { - dateTime = dateTime.replace( - "TZ", - _zoneWithoutPrefix(displayedTimezone) - ); - } - } - } - - return dateTime; - } - - function _applyRecurrence(dateTime, { recurring, timezone }) { - const parts = recurring.split("."); - const count = parseInt(parts[0], 10); - const type = parts[1]; - const diff = moment().diff(dateTime, type); - const add = Math.ceil(diff + count); - - // we create new moment object from format - // to ensure it's created in user context - const wasDST = moment(dateTime.format()).isDST(); - let dateTimeWithRecurrence = moment(dateTime).add(add, type); - const isDST = moment(dateTimeWithRecurrence.format()).isDST(); - - // these dates are more or less DST "certain" - const noDSTOffset = moment - .tz({ month: 0, day: 1 }, timezone || "Etc/UTC") - .utcOffset(); - const withDSTOffset = moment - .tz({ month: 5, day: 1 }, timezone || "Etc/UTC") - .utcOffset(); - - // we remove the DST offset present when the date was created, - // and add current DST offset - if (!wasDST && isDST) { - dateTimeWithRecurrence.add(-withDSTOffset + noDSTOffset, "minutes"); - } - - // we add the DST offset present when the date was created, - // and remove current DST offset - if (wasDST && !isDST) { - dateTimeWithRecurrence.add(withDSTOffset - noDSTOffset, "minutes"); - } - - return dateTimeWithRecurrence; - } - - function _createDateTimeRange(dateTime, timezone) { - const dt = moment(dateTime).tz(timezone); - - return [dt.format("LLL"), "→", dt.add(24, "hours").format("LLL")].join(" "); - } - - function _generatePreviews(dateTime, displayedTimezone, options) { - const previewedTimezones = []; - const watchingUserTimezone = moment.tz.guess(); - const timezones = options.timezones.filter( - timezone => - !_isEqualZones(timezone, watchingUserTimezone) && - !_isEqualZones(timezone, options.timezone) - ); - - previewedTimezones.push({ - timezone: watchingUserTimezone, - current: true, - dateTime: options.time - ? moment(dateTime) - .tz(watchingUserTimezone) - .format("LLL") - : _createDateTimeRange(dateTime, watchingUserTimezone) - }); - - if ( - options.timezone && - displayedTimezone === watchingUserTimezone && - options.timezone !== displayedTimezone && - !_isEqualZones(displayedTimezone, options.timezone) - ) { - timezones.unshift(options.timezone); - } - - Array.from(new Set(timezones.filter(Boolean))).forEach(timezone => { - if (_isEqualZones(timezone, displayedTimezone)) { - return; - } - - if (_isEqualZones(timezone, watchingUserTimezone)) { - timezone = watchingUserTimezone; - } - - previewedTimezones.push({ - timezone, - dateTime: options.time - ? moment(dateTime) - .tz(timezone) - .format("LLL") - : _createDateTimeRange(dateTime, timezone) - }); - }); - - if (!previewedTimezones.length) { - previewedTimezones.push({ - timezone: "Etc/UTC", - dateTime: options.time - ? moment(dateTime) - .tz("Etc/UTC") - .format("LLL") - : _createDateTimeRange(dateTime, "Etc/UTC") - }); - } - - return _.uniq(previewedTimezones, "timezone"); - } - - function _generateTextPreview(previews) { - return previews - .map(preview => { - const formatedZone = _zoneWithoutPrefix(preview.timezone); - - if (preview.dateTime.match(/TZ/)) { - return preview.dateTime.replace(/TZ/, formatedZone); - } else { - return `${formatedZone} ${preview.dateTime}`; - } - }) - .join(", "); - } - - function _generateHtmlPreview(previews) { - return previews - .map(preview => { - const $template = $(PREVIEW_TEMPLATE); - - if (preview.current) $template.addClass("current"); - - $template.find(".timezone").text(_zoneWithoutPrefix(preview.timezone)); - $template.find(".date-time").text(preview.dateTime); - return $template[0].outerHTML; - }) - .join(""); - } - - $.fn.applyLocalDates = function() { - return this.each(function() { - const $element = $(this); - - const options = {}; - options.time = $element.attr("data-time"); - options.date = $element.attr("data-date"); - options.recurring = $element.attr("data-recurring"); - options.timezones = ( - $element.attr("data-timezones") || - Discourse.SiteSettings.discourse_local_dates_default_timezones || - "Etc/UTC" - ).split("|"); - options.timezone = $element.attr("data-timezone"); - options.calendar = ($element.attr("data-calendar") || "on") === "on"; - options.displayedTimezone = $element.attr("data-displayed-timezone"); - options.format = - $element.attr("data-format") || (options.time ? "LLL" : "LL"); - options.countdown = $element.attr("data-countdown"); - - processElement($element, options); - }); - }; -})(jQuery); diff --git a/plugins/discourse-local-dates/assets/javascripts/initializers/discourse-local-dates.js.es6 b/plugins/discourse-local-dates/assets/javascripts/initializers/discourse-local-dates.js.es6 index 352ea1159e6..7b3faee0c3c 100644 --- a/plugins/discourse-local-dates/assets/javascripts/initializers/discourse-local-dates.js.es6 +++ b/plugins/discourse-local-dates/assets/javascripts/initializers/discourse-local-dates.js.es6 @@ -1,11 +1,19 @@ import { withPluginApi } from "discourse/lib/plugin-api"; import showModal from "discourse/lib/show-modal"; +import LocalDateBuilder from "../lib/local-date-builder"; + +const DATE_TEMPLATE = ` + + + + + + +`; function initializeDiscourseLocalDates(api) { api.decorateCooked( - $elem => { - $(".discourse-local-date", $elem).applyLocalDates(); - }, + $elem => $(".discourse-local-date", $elem).applyLocalDates(), { id: "discourse-local-date" } ); @@ -37,6 +45,69 @@ export default { initialize(container) { const siteSettings = container.lookup("site-settings:main"); if (siteSettings.discourse_local_dates_enabled) { + $.fn.applyLocalDates = function() { + return this.each(function() { + const opts = {}; + const dataset = this.dataset; + opts.time = dataset.time; + opts.date = dataset.date; + opts.recurring = dataset.recurring; + opts.timezones = ( + dataset.timezones || + siteSettings.discourse_local_dates_default_timezones || + "Etc/UTC" + ) + .split("|") + .filter(Boolean); + opts.timezone = dataset.timezone; + opts.calendar = (dataset.calendar || "on") === "on"; + opts.displayedTimezone = dataset.displayedTimezone; + opts.format = dataset.format || (opts.time ? "LLL" : "LL"); + opts.countdown = dataset.countdown; + + const localDateBuilder = new LocalDateBuilder( + opts, + moment.tz.guess() + ).build(); + + const htmlPreviews = localDateBuilder.previews.map(preview => { + const previewNode = document.createElement("div"); + previewNode.classList.add("preview"); + if (preview.current) { + previewNode.classList.add("current"); + } + + const timezoneNode = document.createElement("span"); + timezoneNode.classList.add("timezone"); + timezoneNode.innerText = preview.timezone; + previewNode.appendChild(timezoneNode); + + const dateTimeNode = document.createElement("span"); + dateTimeNode.classList.add("date-time"); + dateTimeNode.innerText = preview.formated; + previewNode.appendChild(dateTimeNode); + + return previewNode; + }); + + const previewsNode = document.createElement("div"); + previewsNode.classList.add("locale-dates-previews"); + htmlPreviews.forEach(htmlPreview => + previewsNode.appendChild(htmlPreview) + ); + + this.innerHTML = DATE_TEMPLATE; + this.setAttribute("aria-label", localDateBuilder.textPreview); + this.dataset.htmlTooltip = previewsNode.outerHTML; + this.classList.add("cooked-date"); + if (localDateBuilder.pastEvent) { + this.classList.add("past"); + } + const relativeTime = this.querySelector(".relative-time"); + relativeTime.innerText = localDateBuilder.formated; + }); + }; + withPluginApi("0.8.8", initializeDiscourseLocalDates); } } diff --git a/plugins/discourse-local-dates/assets/javascripts/lib/date-with-zone-helper.js.es6 b/plugins/discourse-local-dates/assets/javascripts/lib/date-with-zone-helper.js.es6 new file mode 100644 index 00000000000..9ffc6edb173 --- /dev/null +++ b/plugins/discourse-local-dates/assets/javascripts/lib/date-with-zone-helper.js.es6 @@ -0,0 +1,89 @@ +const { getProperties } = Ember; + +/* + DateWithZoneHelper provides a limited list of helpers + to manipulate a moment object with timezones + + - add(count unit) adds a COUNT of UNITS to a date + - subtract(count unit) subtracts a COUNT of UNITS to a date + - format(format) formats a date with zone in a consitent way, optional moment format + - isDST() allows to know if a date in a specified timezone is currently under DST + - datetimeWithZone(timezone) returns a new moment object with timezone applied + - datetime returns the moment object + - repetitionsBetweenDates(duration, date) return the number of repertitions of + duration between two dates, eg for duration: "1.weeks", "2.months"... +*/ +export default class DateWithZoneHelper { + constructor(params = {}) { + this.timezone = params.timezone || "UTC"; + this.localTimezone = params.localTimezone || moment.tz.guess(); + + this.datetime = moment.tz( + getProperties(params, [ + "year", + "month", + "day", + "hour", + "minute", + "second" + ]), + this.timezone + ); + } + + isDST() { + return this.datetime.tz(this.localTimezone).isDST(); + } + + repetitionsBetweenDates(duration, date) { + const [count, unit] = duration.split("."); + const diff = this.datetime.diff(date, unit); + const repetitions = diff / parseInt(count, 10); + return Math.abs((Math.round(repetitions * 10) / 10).toFixed(1)); + } + + add(count, unit) { + return this._fromDatetime( + this.datetime.clone().add(count, unit), + this.timezone, + this.localTimezone + ); + } + + subtract(count, unit) { + return this._fromDatetime( + this.datetime.clone().subtract(count, unit), + this.timezone, + this.localTimezone + ); + } + + datetimeWithZone(timezone) { + return this.datetime.clone().tz(timezone); + } + + format(format) { + if (format) { + return this.datetime.tz(this.localTimezone).format(format); + } + + return this.datetime.tz(this.localTimezone).toISOString(true); + } + + static fromDatetime(datetime, timezone, localTimezone) { + return new DateWithZoneHelper({ + year: datetime.year(), + month: datetime.month(), + day: datetime.date(), + hour: datetime.hour(), + minute: datetime.minute(), + second: datetime.second(), + timezone, + localTimezone + }); + } + + _fromDatetime(datetime, timezone, localTimezone) { + return DateWithZoneHelper.fromDatetime(datetime, timezone, localTimezone); + } +} diff --git a/plugins/discourse-local-dates/assets/javascripts/lib/local-date-builder.js.es6 b/plugins/discourse-local-dates/assets/javascripts/lib/local-date-builder.js.es6 new file mode 100644 index 00000000000..3f81303cbf1 --- /dev/null +++ b/plugins/discourse-local-dates/assets/javascripts/lib/local-date-builder.js.es6 @@ -0,0 +1,243 @@ +import DateWithZoneHelper from "./date-with-zone-helper"; + +const TIME_FORMAT = "LLL"; +const DATE_FORMAT = "LL"; +const RANGE_SEPARATOR = "→"; + +export default class LocalDateBuilder { + constructor(params = {}, localTimezone) { + this.time = params.time; + this.date = params.date; + this.recurring = params.recurring; + this.timezones = Array.from( + new Set((params.timezones || []).filter(Boolean)) + ); + this.timezone = params.timezone || "UTC"; + this.calendar = + typeof params.calendar === "undefined" ? true : params.calendar; + this.displayedTimezone = params.displayedTimezone; + this.format = params.format || (this.time ? TIME_FORMAT : DATE_FORMAT); + this.countdown = params.countdown; + this.localTimezone = localTimezone; + } + + build() { + const [year, month, day] = this.date.split("-").map(x => parseInt(x, 10)); + const [hour, minute] = (this.time || "") + .split(":") + .map(x => (x ? parseInt(x, 10) : undefined)); + + let displayedTimezone; + if (this.time) { + displayedTimezone = this.displayedTimezone || this.localTimezone; + } else { + displayedTimezone = + this.displayedTimezone || this.localTimezone || this.timezone; + } + + let localDate = new DateWithZoneHelper({ + year, + month: month ? month - 1 : null, + day, + hour, + minute, + timezone: this.timezone, + localTimezone: this.localTimezone + }); + + if (this.recurring) { + const [count, type] = this.recurring.split("."); + + const repetitionsForType = localDate.repetitionsBetweenDates( + this.recurring, + moment.tz(this.localTimezone) + ); + + localDate = localDate.add(repetitionsForType + parseInt(count, 10), type); + } + + const previews = this._generatePreviews(localDate, displayedTimezone); + + return { + pastEvent: + !this.recurring && + moment.tz(this.localTimezone).isAfter(localDate.datetime), + formated: this._applyFormatting(localDate, displayedTimezone), + previews, + textPreview: this._generateTextPreviews(previews) + }; + } + + _generateTextPreviews(previews) { + return previews + .map(preview => { + const formatedZone = this._zoneWithoutPrefix(preview.timezone); + return `${formatedZone} ${preview.formated}`; + }) + .join(", "); + } + + _generatePreviews(localDate, displayedTimezone) { + const previewedTimezones = []; + + const timezones = this.timezones.filter( + timezone => + !this._isEqualZones(timezone, this.localTimezone) && + !this._isEqualZones(timezone, this.timezone) + ); + + previewedTimezones.push({ + timezone: this.localTimezone, + current: true, + formated: this._createDateTimeRange(localDate, this.time) + }); + + if ( + this.timezone && + displayedTimezone === this.localTimezone && + this.timezone !== displayedTimezone && + !this._isEqualZones(displayedTimezone, this.timezone) + ) { + timezones.unshift(this.timezone); + } + + timezones.forEach(timezone => { + if (this._isEqualZones(timezone, displayedTimezone)) { + return; + } + + if (this._isEqualZones(timezone, this.localTimezone)) { + timezone = this.localTimezone; + } + + previewedTimezones.push({ + timezone, + formated: this._createDateTimeRange( + localDate.datetimeWithZone(timezone), + this.time + ) + }); + }); + + if (!previewedTimezones.length) { + previewedTimezones.push({ + timezone: "Etc/UTC", + formated: this._createDateTimeRange( + localDate.datetimeWithZone("Etc/UTC"), + this.time + ) + }); + } + + return previewedTimezones.uniqBy("timezone"); + } + + _isEqualZones(timezoneA, timezoneB) { + if ((timezoneA || timezoneB) && (!timezoneA || !timezoneB)) { + return false; + } + + if (timezoneA.includes(timezoneB) || timezoneB.includes(timezoneA)) { + return true; + } + + return ( + moment.tz(timezoneA).utcOffset() === moment.tz(timezoneB).utcOffset() + ); + } + + _createDateTimeRange(startRange, time) { + // if a time has been given we do not attempt to automatically create a range + // instead we show only one date with a format showing the time + if (time) { + return startRange.format(TIME_FORMAT); + } else { + const endRange = startRange.add(24, "hours"); + return [ + startRange.format(this.format || "LLLL"), + RANGE_SEPARATOR, + endRange.format(this.format || "LLLL") + ].join(" "); + } + } + + _applyFormatting(localDate, displayedTimezone) { + if (this.countdown) { + const diffTime = moment.tz(this.localTimezone).diff(localDate.datetime); + + if (diffTime < 0) { + return moment.duration(diffTime).humanize(); + } else { + return I18n.t("discourse_local_dates.relative_dates.countdown.passed"); + } + } + + const sameTimezone = this._isEqualZones( + displayedTimezone, + this.localTimezone + ); + + if (this.calendar) { + const inCalendarRange = moment + .tz(this.localTimezone) + .isBetween( + localDate.subtract(2, "day").datetime, + localDate.add(1, "day").datetime.endOf("day") + ); + + if (inCalendarRange && sameTimezone) { + return localDate.datetime.calendar( + moment.tz(this.localTimezone), + this._calendarFormats(this.time ? this.time : null) + ); + } + } + + if (!sameTimezone) { + return this._formatWithZone(localDate, displayedTimezone, this.format); + } + + return localDate.format(this.format); + } + + _calendarFormats(time) { + return { + sameDay: this._translateCalendarKey(time, "today"), + nextDay: this._translateCalendarKey(time, "tomorrow"), + lastDay: this._translateCalendarKey(time, "yesterday"), + sameElse: "L" + }; + } + + _translateCalendarKey(time, key) { + const translated = I18n.t(`discourse_local_dates.relative_dates.${key}`, { + time: "LT" + }); + + if (time) { + return translated + .split("LT") + .map(w => `[${w}]`) + .join("LT"); + } else { + return `[${translated.replace(" LT", "")}]`; + } + } + + _formatTimezone(timezone) { + return timezone + .replace("_", " ") + .replace("Etc/", "") + .split("/"); + } + + _zoneWithoutPrefix(timezone) { + const [part1, part2] = this._formatTimezone(timezone); + return part2 || part1; + } + + _formatWithZone(localDate, displayedTimezone, format) { + let formated = localDate.datetimeWithZone(displayedTimezone).format(format); + return `${formated} (${this._zoneWithoutPrefix(displayedTimezone)})`; + } +} diff --git a/plugins/discourse-local-dates/plugin.rb b/plugins/discourse-local-dates/plugin.rb index d4ac0160df5..b4fdae639fc 100644 --- a/plugins/discourse-local-dates/plugin.rb +++ b/plugins/discourse-local-dates/plugin.rb @@ -6,7 +6,6 @@ # author: Joffrey Jaffeux hide_plugin if self.respond_to?(:hide_plugin) -register_asset 'javascripts/discourse-local-dates.js.es6' register_asset 'stylesheets/common/discourse-local-dates.scss' register_asset 'moment.js', :vendored_core_pretty_text register_asset 'moment-timezone.js', :vendored_core_pretty_text diff --git a/plugins/discourse-local-dates/test/javascripts/acceptance/local-dates-test.js.es6 b/plugins/discourse-local-dates/test/javascripts/acceptance/local-dates-test.js.es6 deleted file mode 100644 index 6d2a17f2a35..00000000000 --- a/plugins/discourse-local-dates/test/javascripts/acceptance/local-dates-test.js.es6 +++ /dev/null @@ -1,463 +0,0 @@ -import { acceptance } from "helpers/qunit-helpers"; - -const sandbox = sinon.createSandbox(); - -acceptance("Local Dates", { - loggedIn: true, - settings: { - discourse_local_dates_enabled: true, - discourse_local_dates_default_timezones: "Europe/Paris|America/Los_Angeles" - }, - beforeEach() { - freezeDateAndZone(); - }, - afterEach() { - sandbox.restore(); - moment.tz.setDefault(); - } -}); - -const DEFAULT_DATE = "2018-06-20"; -const DEFAULT_ZONE = "Europe/Paris"; -const DEFAULT_ZONE_FORMATED = DEFAULT_ZONE.split("/")[1]; - -function advance(count, unit = "days") { - return moment(DEFAULT_DATE) - .add(count, unit) - .format("YYYY-MM-DD"); -} - -function rewind(count, unit = "days") { - return moment(DEFAULT_DATE) - .subtract(count, unit) - .format("YYYY-MM-DD"); -} - -function freezeDateAndZone(date, zone, cb) { - date = date || DEFAULT_DATE; - zone = zone || DEFAULT_ZONE; - - sandbox.restore(); - sandbox.stub(moment.tz, "guess"); - moment.tz.guess.returns(zone); - moment.tz.setDefault(zone); - - const now = moment(date).valueOf(); - sandbox.useFakeTimers(now); - - if (cb) { - cb(); - - moment.tz.guess.returns(DEFAULT_ZONE); - moment.tz.setDefault(DEFAULT_ZONE); - sandbox.useFakeTimers(moment(DEFAULT_DATE).valueOf()); - } -} - -function generateHTML(options = {}) { - let output = ` { - const html = generateHTML({ date: advance(3), time: "02:00" }); - const transformed = $(html).applyLocalDates(); - - assert.equal( - transformed.text().trim(), - "June 23, 2018 2:00 AM", - "it uses moment LLL format" - ); -}); - -test("default format - no time specified", assert => { - let html = generateHTML({ date: advance(3) }); - let transformed = $(html).applyLocalDates(); - - assert.equal( - transformed.text().trim(), - "June 23, 2018", - "it uses moment LL format as default if not time is specified" - ); - - freezeDateAndZone(advance(1), "Pacific/Auckland", () => { - html = generateHTML({ date: advance(3) }); - transformed = $(html).applyLocalDates(); - - assert.equal( - transformed.text().trim(), - `June 23, 2018 (${DEFAULT_ZONE_FORMATED})`, - "it appends creator timezone if watching user timezone is different" - ); - }); - - freezeDateAndZone(advance(1), "Europe/Vienna", () => { - html = generateHTML({ date: advance(3) }); - transformed = $(html).applyLocalDates(); - - assert.equal( - transformed.text().trim(), - "June 23, 2018", - "it doesn’t append timezone if different but with the same utc offset" - ); - }); -}); - -test("today", assert => { - const html = generateHTML({ time: "16:00" }); - const transformed = $(html).applyLocalDates(); - - assert.equal(transformed.text().trim(), "Today 4:00 PM", "it display Today"); -}); - -test("today - no time", assert => { - const html = generateHTML(); - const transformed = $(html).applyLocalDates(); - - assert.equal( - transformed.text().trim(), - "Today", - "it display Today without time" - ); -}); - -test("yesterday", assert => { - const html = generateHTML({ date: rewind(1), time: "16:00" }); - const transformed = $(html).applyLocalDates(); - - assert.equal( - transformed.text().trim(), - "Yesterday 4:00 PM", - "it displays yesterday" - ); -}); - -test("yesterday - no time", assert => { - const html = generateHTML({ date: rewind(1) }); - const transformed = $(html).applyLocalDates(); - - assert.equal( - transformed.text().trim(), - "Yesterday", - "it displays yesterday without time" - ); -}); - -test("tomorrow", assert => { - const html = generateHTML({ date: advance(1), time: "16:00" }); - const transformed = $(html).applyLocalDates(); - - assert.equal( - transformed.text().trim(), - "Tomorrow 4:00 PM", - "it displays tomorrow" - ); -}); - -test("tomorrow - no time", assert => { - const html = generateHTML({ date: advance(1) }); - const transformed = $(html).applyLocalDates(); - - assert.equal( - transformed.text().trim(), - "Tomorrow", - "it displays tomorrow without time" - ); -}); - -test("today - no time with different zones", assert => { - const html = generateHTML(); - let transformed = $(html).applyLocalDates(); - - assert.equal( - transformed.text().trim(), - "Today", - "it displays today without time" - ); - - freezeDateAndZone(rewind(12, "hours"), "Pacific/Auckland", () => { - transformed = $(html).applyLocalDates(); - assert.equal( - transformed.text().trim(), - `June 20, 2018 (${DEFAULT_ZONE_FORMATED})`, - "it displays the date without calendar and creator timezone" - ); - }); -}); - -test("calendar off", assert => { - const html = generateHTML({ calendar: "off", time: "16:00" }); - const transformed = $(html).applyLocalDates(); - - assert.equal( - transformed.text().trim(), - "June 20, 2018 4:00 PM", - "it displays the date without Today" - ); -}); - -test("recurring", assert => { - const html = generateHTML({ recurring: "1.week", time: "16:00" }); - let transformed = $(html).applyLocalDates(); - - assert.equal( - transformed.text().trim(), - "Today 4:00 PM", - "it displays the next occurrence" - ); - - freezeDateAndZone(advance(1), null, () => { - transformed = $(html).applyLocalDates(); - - assert.equal( - transformed.text().trim(), - "June 27, 2018 4:00 PM", - "it displays the next occurrence" - ); - }); -}); - -test("format", assert => { - const html = generateHTML({ - date: advance(3), - format: "YYYY | MM - DD" - }); - const transformed = $(html).applyLocalDates(); - - assert.equal( - transformed.text().trim(), - "2018 | 06 - 23", - "it uses the given format" - ); -}); - -test("displayedTimezone", assert => { - let html = generateHTML({ - date: advance(3), - displayedTimezone: "America/Chicago", - time: "16:00" - }); - let transformed = $(html).applyLocalDates(); - - assert.equal( - transformed.text().trim(), - "June 23, 2018 9:00 AM (Chicago)", - "it displays timezone when different from watching user" - ); - - html = generateHTML({ - date: advance(3), - displayedTimezone: DEFAULT_ZONE, - time: "16:00" - }); - - transformed = $(html).applyLocalDates(); - assert.equal( - transformed.text().trim(), - "June 23, 2018 4:00 PM", - "it doesn’t display timezone when same from watching user" - ); - - html = generateHTML({ displayedTimezone: "Etc/UTC" }); - transformed = $(html).applyLocalDates(); - - assert.equal( - transformed.text().trim(), - "June 19, 2018 (UTC)", - "it displays timezone and drops calendar mode when timezone is different from watching user" - ); - - html = generateHTML({ displayedTimezone: DEFAULT_ZONE }); - transformed = $(html).applyLocalDates(); - - assert.equal( - transformed.text().trim(), - "Today", - "it doesn’t display timezone and doesn’t drop calendar mode when timezone is same from watching user" - ); - - html = generateHTML({ - timezone: "America/Chicago" - }); - transformed = $(html).applyLocalDates(); - - assert.equal( - transformed.text().trim(), - "June 20, 2018 (Chicago)", - "it uses timezone when displayedTimezone is not set" - ); - - html = generateHTML(); - transformed = $(html).applyLocalDates(); - - assert.equal( - transformed.text().trim(), - "Today", - "it uses user’s timezone when displayedTimezone and timezone are not set" - ); - - html = generateHTML({ - timezone: "America/Chicago", - displayedTimezone: "Pacific/Auckland" - }); - transformed = $(html).applyLocalDates(); - - assert.equal( - transformed.text().trim(), - "June 20, 2018 (Auckland)", - "it uses displayedTimezone over timezone" - ); -}); - -test("tooltip", assert => { - let html = generateHTML({ timezone: "America/Chicago" }); - let transformed = $(html).applyLocalDates(); - let htmlToolip = transformed.attr("data-html-tooltip"); - let currentUserPreview = $(htmlToolip).find(".preview.current"); - let timezone = currentUserPreview.find(".timezone").text(); - let dateTime = currentUserPreview.find(".date-time").text(); - - assert.equal( - timezone, - DEFAULT_ZONE_FORMATED, - "it adds watching user timezone as preview" - ); - assert.equal( - dateTime, - "June 20, 2018 7:00 AM → June 21, 2018 7:00 AM", - "it creates a range adjusted to watching user timezone" - ); - - freezeDateAndZone(DEFAULT_DATE, "Pacific/Auckland", () => { - html = generateHTML({ timezone: "Pacific/Auckland" }); - transformed = $(html).applyLocalDates(); - htmlToolip = transformed.attr("data-html-tooltip"); - currentUserPreview = $(htmlToolip).find(".preview.current"); - - assert.ok( - exists(currentUserPreview), - "it creates an entry if watching user has the same timezone than creator" - ); - }); - - html = generateHTML({ - timezones: "Etc/UTC", - timezone: "America/Chicago", - time: "14:00:00" - }); - transformed = $(html).applyLocalDates(); - htmlToolip = transformed.attr("data-html-tooltip"); - - assert.ok( - exists($(htmlToolip).find(".preview.current")), - "doesn’t create current timezone when displayed timezone equals watching user timezone" - ); - - let $firstPreview = $(htmlToolip).find(".preview:nth-child(2)"); - dateTime = $firstPreview.find(".date-time").text(); - timezone = $firstPreview.find(".timezone").text(); - assert.equal( - dateTime, - "June 20, 2018 2:00 PM", - "it doesn’t create range if time has been set" - ); - assert.equal(timezone, "Chicago", "it adds the timezone of the creator"); - - let $secondPreview = $(htmlToolip).find(".preview:nth-child(3)"); - dateTime = $secondPreview.find(".date-time").text(); - timezone = $secondPreview.find(".timezone").text(); - assert.equal(timezone, "UTC", "Etc/UTC is rewritten to UTC"); - - freezeDateAndZone(moment("2018-11-26 21:00:00"), "Europe/Vienna", () => { - html = generateHTML({ - date: "2018-11-22", - timezone: "America/Chicago", - time: "14:00" - }); - transformed = $(html).applyLocalDates(); - htmlToolip = transformed.attr("data-html-tooltip"); - - $firstPreview = $(htmlToolip).find(".preview:nth-child(2)"); - - assert.equal( - $firstPreview.find(".timezone").text(), - "Chicago", - "it adds the creator timezone to the previews" - ); - assert.equal( - $firstPreview.find(".date-time").text(), - "November 22, 2018 2:00 PM", - "it adds the creator timezone to the previews" - ); - }); - - freezeDateAndZone(DEFAULT_DATE, "Europe/Vienna", () => { - html = generateHTML({ - date: "2018-11-22", - timezone: "America/Chicago", - timezones: "Europe/Paris" - }); - transformed = $(html).applyLocalDates(); - htmlToolip = transformed.attr("data-html-tooltip"); - - $firstPreview = $(htmlToolip) - .find(".preview") - .first(); - - assert.equal( - $firstPreview.find(".timezone").text(), - "Vienna", - "it rewrites timezone with same offset and different name than watching user" - ); - }); -}); - -test("test utils", assert => { - assert.equal( - moment().format("LLLL"), - moment(DEFAULT_DATE).format("LLLL"), - "it has defaults" - ); - - assert.equal(moment.tz.guess(), DEFAULT_ZONE, "it has defaults"); - - freezeDateAndZone(advance(1), DEFAULT_ZONE, () => { - assert.equal( - moment().format("LLLL"), - moment(DEFAULT_DATE) - .add(1, "days") - .format("LLLL"), - "it applies new time" - ); - assert.equal(moment.tz.guess(), DEFAULT_ZONE); - }); - - assert.equal( - moment().format("LLLL"), - moment(DEFAULT_DATE).format("LLLL"), - "it restores time" - ); - - freezeDateAndZone(advance(1), "Pacific/Auckland", () => { - assert.equal( - moment().format("LLLL"), - moment(DEFAULT_DATE) - .add(1, "days") - .format("LLLL") - ); - assert.equal(moment.tz.guess(), "Pacific/Auckland", "it applies new zone"); - }); - - assert.equal(moment.tz.guess(), DEFAULT_ZONE, "it restores zone"); -}); diff --git a/plugins/discourse-local-dates/test/javascripts/lib/date-with-zone-helper-test.js.es6 b/plugins/discourse-local-dates/test/javascripts/lib/date-with-zone-helper-test.js.es6 new file mode 100644 index 00000000000..4f3351935a9 --- /dev/null +++ b/plugins/discourse-local-dates/test/javascripts/lib/date-with-zone-helper-test.js.es6 @@ -0,0 +1,168 @@ +import DateWithZoneHelper from "./date-with-zone-helper"; + +const PARIS = "Europe/Paris"; +const SYDNEY = "Australia/Sydney"; + +QUnit.module("lib:date-with-zone-helper"); + +function buildDateHelper(params = {}) { + return new DateWithZoneHelper({ + year: params.year || 2020, + day: params.day || 22, + month: params.month || 2, + hour: params.hour || 10, + minute: params.minute || 5, + timezone: params.timezone, + localTimezone: PARIS + }); +} + +QUnit.test("#format", assert => { + let date = buildDateHelper({ + day: 15, + month: 2, + hour: 15, + minute: 36, + timezone: PARIS + }); + assert.equal(date.format(), "2020-03-15T15:36:00.000+01:00"); +}); + +QUnit.test("#repetitionsBetweenDates", assert => { + let date; + + date = buildDateHelper({ + day: 15, + month: 1, + hour: 15, + minute: 36, + timezone: PARIS + }); + assert.equal( + date.repetitionsBetweenDates( + "1.hour", + moment.tz("2020-02-15 15:36", SYDNEY) + ), + 10, + "it correctly finds difference between timezones" + ); + + date = buildDateHelper({ + day: 15, + month: 1, + hour: 15, + minute: 36, + timezone: PARIS + }); + assert.equal( + date.repetitionsBetweenDates( + "1.minute", + moment.tz("2020-02-15 15:36", PARIS) + ), + 0, + "it correctly finds no difference" + ); + + date = buildDateHelper({ + day: 15, + month: 1, + hour: 15, + minute: 36, + timezone: PARIS + }); + assert.equal( + date.repetitionsBetweenDates( + "1.minute", + moment.tz("2020-02-15 15:37", PARIS) + ), + 1, + "it correctly finds no difference" + ); + + date = buildDateHelper({ + day: 15, + month: 1, + hour: 15, + minute: 36, + timezone: PARIS + }); + assert.equal( + date.repetitionsBetweenDates( + "2.minute", + moment.tz("2020-02-15 15:41", PARIS) + ), + 2.5, + "it correctly finds difference with a multiplicator" + ); +}); + +QUnit.test("#add", assert => { + let date; + let futureLocalDate; + + date = buildDateHelper({ + day: 19, + month: 2, + hour: 15, + minute: 36, + timezone: PARIS + }); + + assert.notOk(date.isDST()); + futureLocalDate = date.add(8, "months"); + assert.notOk(futureLocalDate.isDST()); + assert.equal( + futureLocalDate.format(), + "2020-11-19T15:36:00.000+01:00", + "it correctly adds from a !isDST date to a !isDST date" + ); + + date = buildDateHelper({ + day: 25, + month: 3, + hour: 15, + minute: 36, + timezone: PARIS + }); + assert.ok(date.isDST()); + futureLocalDate = date.add(1, "year"); + assert.ok(futureLocalDate.isDST()); + assert.equal( + futureLocalDate.format(), + "2021-04-25T15:36:00.000+02:00", + "it correctly adds from a isDST date to a isDST date" + ); + + date = buildDateHelper({ + day: 25, + month: 2, + hour: 15, + minute: 36, + timezone: PARIS + }); + assert.notOk(date.isDST()); + futureLocalDate = date.add(1, "week"); + assert.ok(futureLocalDate.isDST()); + assert.equal( + futureLocalDate.format(), + "2020-04-01T15:36:00.000+02:00", + "it correctly adds from a !isDST date to a isDST date" + ); + + date = buildDateHelper({ + day: 1, + month: 3, + hour: 15, + minute: 36, + timezone: PARIS + }); + + assert.ok(date.isDST()); + futureLocalDate = date.add(8, "months"); + assert.notOk(futureLocalDate.isDST()); + assert.equal( + futureLocalDate.format(), + "2020-12-01T15:36:00.000+01:00", + "it correctly adds from a isDST date to a !isDST date" + ); +}); diff --git a/plugins/discourse-local-dates/test/javascripts/lib/local-date-builder-test.js.es6 b/plugins/discourse-local-dates/test/javascripts/lib/local-date-builder-test.js.es6 new file mode 100644 index 00000000000..7895ca01cdf --- /dev/null +++ b/plugins/discourse-local-dates/test/javascripts/lib/local-date-builder-test.js.es6 @@ -0,0 +1,420 @@ +import LocalDateBuilder from "./local-date-builder"; + +const UTC = "Etc/UTC"; +const SYDNEY = "Australia/Sydney"; +const LOS_ANGELES = "America/Los_Angeles"; +const PARIS = "Europe/Paris"; +const LAGOS = "Africa/Lagos"; +const LONDON = "Europe/London"; + +QUnit.module("lib:local-date-builder"); + +const sandbox = sinon.createSandbox(); + +function freezeTime({ date, timezone }, cb) { + date = date || "2020-01-22 10:34"; + const newTimezone = timezone || PARIS; + const previousZone = moment.tz.guess(); + const now = moment.tz(date, newTimezone).valueOf(); + + sandbox.useFakeTimers(now); + sandbox.stub(moment.tz, "guess"); + moment.tz.guess.returns(newTimezone); + moment.tz.setDefault(newTimezone); + + cb(); + + moment.tz.guess.returns(previousZone); + moment.tz.setDefault(previousZone); + sandbox.restore(); +} + +QUnit.assert.buildsCorrectDate = function(options, expected, message) { + const localTimezone = options.localTimezone || PARIS; + delete options.localTimezone; + + const localDateBuilder = new LocalDateBuilder( + Object.assign( + {}, + { + date: "2020-03-22" + }, + options + ), + localTimezone + ); + + if (expected.formated) { + this.test.assert.equal( + localDateBuilder.build().formated, + expected.formated, + message || "it formates the date correctly" + ); + } + + if (expected.previews) { + this.test.assert.deepEqual( + localDateBuilder.build().previews, + expected.previews, + message || "it formates the previews correctly" + ); + } +}; + +QUnit.test("date", assert => { + freezeTime({ date: "2020-03-11" }, () => { + assert.buildsCorrectDate( + { date: "2020-03-22", timezone: PARIS }, + { formated: "March 22, 2020" }, + "it displays the date without time" + ); + + assert.buildsCorrectDate( + { date: "2020-04-11", time: "11:00" }, + { formated: "April 11, 2020 1:00 PM" }, + "it displays the date with time" + ); + }); +}); + +QUnit.test("option[format]", assert => { + freezeTime({ date: "2020-03-11" }, () => { + assert.buildsCorrectDate( + { format: "YYYY" }, + { formated: "2020" }, + "it uses custom format" + ); + }); +}); + +QUnit.test("option[displayedTimezone]", assert => { + freezeTime({}, () => { + assert.buildsCorrectDate( + { displayedTimezone: SYDNEY }, + { formated: "March 22, 2020 (Sydney)" }, + "it displays the timezone if the timezone is different from the date" + ); + }); + + freezeTime({}, () => { + assert.buildsCorrectDate( + { displayedTimezone: PARIS, timezone: PARIS }, + { formated: "March 22, 2020" }, + "it doesn't display the timezone if the timezone is the same than the date" + ); + }); + + freezeTime({}, () => { + assert.buildsCorrectDate( + { timezone: UTC, displayedTimezone: UTC }, + { formated: "March 22, 2020 (UTC)" }, + "it replaces `Etc/`" + ); + }); + + freezeTime({}, () => { + assert.buildsCorrectDate( + { timezone: LOS_ANGELES, displayedTimezone: LOS_ANGELES }, + { formated: "March 22, 2020 (Los Angeles)" }, + "it removes prefix and replaces `_`" + ); + }); +}); + +QUnit.test("option[timezone]", assert => { + freezeTime({}, () => { + assert.buildsCorrectDate( + { timezone: SYDNEY, displayedTimezone: PARIS }, + { formated: "March 21, 2020" }, + "it correctly parses a date with the given timezone context" + ); + }); +}); + +QUnit.test("option[recurring]", assert => { + freezeTime({ date: "2020-04-06 06:00", timezone: LAGOS }, () => { + assert.buildsCorrectDate( + { + date: "2019-11-25", + time: "11:00", + timezone: PARIS, + displayedTimezone: LAGOS, + recurring: "1.weeks" + }, + { + formated: "April 6, 2020 10:00 AM (Lagos)" + }, + "it correctly formats a recurring date starting from a !isDST timezone to a isDST timezone date when displayed to a user using a timezone with no DST" + ); + }); + + freezeTime({ date: "2020-04-06 01:00", timezone: SYDNEY }, () => { + assert.buildsCorrectDate( + { + date: "2020-03-09", + time: "02:00", + timezone: UTC, + recurring: "1.weeks", + calendar: false, + displayedTimezone: SYDNEY + }, + { + formated: "April 6, 2020 12:00 PM (Sydney)" + }, + "it correctly formats a recurring date spanning over weeks" + ); + }); + + freezeTime({ date: "2020-04-07 22:00" }, () => { + assert.buildsCorrectDate( + { + date: "2019-11-25", + time: "11:00", + recurring: "1.weeks", + timezone: PARIS + }, + { + formated: "April 13, 2020 11:00 AM" + }, + "it correctly adds from a !isDST date to a isDST date" + ); + }); + + freezeTime({ date: "2020-04-06 10:59" }, () => { + assert.buildsCorrectDate( + { + date: "2020-03-30", + time: "11:00", + recurring: "1.weeks", + timezone: PARIS + }, + { + formated: "Today 11:00 AM" + }, + "it works to the minute" + ); + }); + + freezeTime({ date: "2020-04-06 11:01" }, () => { + assert.buildsCorrectDate( + { + date: "2020-03-30", + time: "11:00", + recurring: "1.weeks", + timezone: PARIS + }, + { + formated: "April 13, 2020 11:00 AM" + }, + "it works to the minute" + ); + }); +}); + +QUnit.test("option[countown]", assert => { + freezeTime({ date: "2020-03-21 23:59" }, () => { + assert.buildsCorrectDate( + { + countdown: true, + timezone: PARIS + }, + { formated: "a minute" }, + "it shows the time remaining" + ); + }); + + freezeTime({ date: "2020-03-22 00:01" }, () => { + assert.buildsCorrectDate( + { + countdown: true, + timezone: PARIS + }, + { + formated: I18n.t( + "discourse_local_dates.relative_dates.countdown.passed" + ) + }, + "it shows the date has passed" + ); + }); +}); + +QUnit.test("option[calendar]", assert => { + freezeTime({ date: "2020-03-23 23:00" }, () => { + assert.buildsCorrectDate( + { date: "2020-03-22", time: "23:59", timezone: PARIS }, + { formated: "Yesterday 11:59 PM" }, + "it drops calendar mode when event date is more than one day before current date" + ); + }); + + freezeTime({ date: "2020-03-20 23:59" }, () => + assert.buildsCorrectDate( + { date: "2020-03-21", time: "00:00", timezone: PARIS }, + { formated: "Tomorrow 12:00 AM" } + ) + ); + + freezeTime({ date: "2020-03-20 23:59" }, () => { + assert.buildsCorrectDate( + { date: "2020-03-21", time: "23:59", timezone: PARIS }, + { formated: "Tomorrow 11:59 PM" } + ); + }); + + freezeTime({ date: "2020-03-21 00:00" }, () => + assert.buildsCorrectDate( + { date: "2020-03-21", time: "23:00", timezone: PARIS }, + { formated: "Today 11:00 PM" } + ) + ); + + freezeTime({ date: "2020-03-22 23:59" }, () => + assert.buildsCorrectDate( + { date: "2020-03-21", time: "23:59", timezone: PARIS }, + { formated: "Yesterday 11:59 PM" } + ) + ); + + freezeTime({ date: "2020-03-22 23:59" }, () => + assert.buildsCorrectDate( + { date: "2020-03-21", time: "23:59", timezone: PARIS }, + { formated: "Yesterday 11:59 PM" } + ) + ); + + freezeTime({ date: "2020-03-22 23:59" }, () => + assert.buildsCorrectDate( + { calendar: false, date: "2020-03-21", time: "23:59", timezone: PARIS }, + { formated: "March 21, 2020 11:59 PM" }, + "it doesn't use calendar when disabled" + ) + ); + + freezeTime({ date: "2020-03-24 01:00" }, () => + assert.buildsCorrectDate( + { date: "2020-03-21", timezone: PARIS }, + { formated: "March 21, 2020" }, + "it stops formating out of calendar range" + ) + ); +}); + +QUnit.test("previews", assert => { + freezeTime({ date: "2020-03-22" }, () => { + assert.buildsCorrectDate( + { timezone: PARIS }, + { + previews: [ + { + current: true, + formated: "March 22, 2020 → March 23, 2020", + timezone: "Europe/Paris" + } + ] + } + ); + }); + + freezeTime({ date: "2020-03-22", timezone: PARIS }, () => { + assert.buildsCorrectDate( + { timezone: PARIS, timezones: [SYDNEY] }, + { + previews: [ + { + current: true, + formated: "March 22, 2020 → March 23, 2020", + timezone: "Europe/Paris" + }, + { + formated: "March 23, 2020 → March 23, 2020", + timezone: "Australia/Sydney" + } + ] + } + ); + }); + + freezeTime({ date: "2020-03-22", timezone: PARIS }, () => { + assert.buildsCorrectDate( + { timezone: PARIS, displayedTimezone: LOS_ANGELES }, + { + previews: [ + { + current: true, + formated: "March 22, 2020 → March 23, 2020", + timezone: "Europe/Paris" + } + ] + } + ); + }); + + freezeTime({ date: "2020-03-22", timezone: PARIS }, () => { + assert.buildsCorrectDate( + { timezone: PARIS, isplayedTimezone: PARIS }, + { + previews: [ + { + current: true, + formated: "March 22, 2020 → March 23, 2020", + timezone: "Europe/Paris" + } + ] + } + ); + }); + + freezeTime({ date: "2020-03-22", timezone: PARIS }, () => { + assert.buildsCorrectDate( + { timezone: PARIS, timezones: [PARIS] }, + { + previews: [ + { + current: true, + formated: "March 22, 2020 → March 23, 2020", + timezone: "Europe/Paris" + } + ] + } + ); + }); + + freezeTime({ date: "2020-03-22", timezone: PARIS }, () => { + assert.buildsCorrectDate( + { time: "11:34", timezone: PARIS, timezones: [PARIS] }, + { + previews: [ + { + current: true, + formated: "March 22, 2020 11:34 AM", + timezone: "Europe/Paris" + } + ] + } + ); + }); + + freezeTime({ date: "2020-04-06", timezone: PARIS }, () => { + assert.buildsCorrectDate( + { timezone: PARIS, date: "2020-04-07", timezones: [LONDON, LAGOS] }, + { + previews: [ + { + current: true, + formated: "April 7, 2020 → April 8, 2020", + timezone: "Europe/Paris" + }, + { + formated: "April 7, 2020 → April 7, 2020", + timezone: "Europe/London" + }, + { + formated: "April 7, 2020 → April 7, 2020", + timezone: "Africa/Lagos" + } + ] + } + ); + }); +});