Browse Source

Follow-up 373974d07ab33a1bfb4aa6a91a74ee80dbbcd773 - Maintenance: Stabilize a test by mimicking behavior of the date function used in the front end.

Dusan Vuckovic 2 years ago
parent
commit
3e13566610

+ 81 - 0
public/assets/tests/qunit/view_helpers.js

@@ -38,3 +38,84 @@ QUnit.test('relative_time', assert => {
 
   clock.restore()
 })
+
+QUnit.test('relative_time - setMonth() sanity check', assert => {
+  let func = App.ViewHelpers.relative_time
+
+  clock = sinon.useFakeTimers({ now: new Date('2023-01-01T12:00:00.000Z') })
+
+  assert.equal(func(1, 'month'), '2023-02-01T12:00:00.000Z', '1st of the month - 1 month')
+  assert.equal(func(2, 'month'), '2023-03-01T12:00:00.000Z', '1st of the month - 2 months')
+  assert.equal(func(3, 'month'), '2023-04-01T11:00:00.000Z', '1st of the month - 3 months')
+  assert.equal(func(4, 'month'), '2023-05-01T11:00:00.000Z', '1st of the month - 4 months')
+  assert.equal(func(5, 'month'), '2023-06-01T11:00:00.000Z', '1st of the month - 5 months')
+  assert.equal(func(6, 'month'), '2023-07-01T11:00:00.000Z', '1st of the month - 6 months')
+  assert.equal(func(7, 'month'), '2023-08-01T11:00:00.000Z', '1st of the month - 7 months')
+  assert.equal(func(8, 'month'), '2023-09-01T11:00:00.000Z', '1st of the month - 8 months')
+  assert.equal(func(9, 'month'), '2023-10-01T11:00:00.000Z', '1st of the month - 9 months')
+  assert.equal(func(10, 'month'), '2023-11-01T12:00:00.000Z', '1st of the month - 10 months')
+  assert.equal(func(11, 'month'), '2023-12-01T12:00:00.000Z', '1st of the month - 11 months')
+  assert.equal(func(12, 'month'), '2024-01-01T12:00:00.000Z', '1st of the month - 12 months')
+
+  clock = sinon.useFakeTimers({ now: new Date('2023-01-28T12:00:00.000Z') })
+
+  assert.equal(func(1, 'month'), '2023-02-28T12:00:00.000Z', '28th of the month - 1 month')
+  assert.equal(func(2, 'month'), '2023-03-28T11:00:00.000Z', '28th of the month - 2 months')
+  assert.equal(func(3, 'month'), '2023-04-28T11:00:00.000Z', '28th of the month - 3 months')
+  assert.equal(func(4, 'month'), '2023-05-28T11:00:00.000Z', '28th of the month - 4 months')
+  assert.equal(func(5, 'month'), '2023-06-28T11:00:00.000Z', '28th of the month - 5 months')
+  assert.equal(func(6, 'month'), '2023-07-28T11:00:00.000Z', '28th of the month - 6 months')
+  assert.equal(func(7, 'month'), '2023-08-28T11:00:00.000Z', '28th of the month - 7 months')
+  assert.equal(func(8, 'month'), '2023-09-28T11:00:00.000Z', '28th of the month - 8 months')
+  assert.equal(func(9, 'month'), '2023-10-28T11:00:00.000Z', '28th of the month - 9 months')
+  assert.equal(func(10, 'month'), '2023-11-28T12:00:00.000Z', '28th of the month - 10 months')
+  assert.equal(func(11, 'month'), '2023-12-28T12:00:00.000Z', '28th of the month - 11 months')
+  assert.equal(func(12, 'month'), '2024-01-28T12:00:00.000Z', '28th of the month - 12 months')
+
+  clock = sinon.useFakeTimers({ now: new Date('2023-01-29T12:00:00.000Z') })
+
+  assert.equal(func(1, 'month'), '2023-03-01T12:00:00.000Z', '29th of the month - 1 month')
+  assert.equal(func(2, 'month'), '2023-03-29T11:00:00.000Z', '29th of the month - 2 months')
+  assert.equal(func(3, 'month'), '2023-04-29T11:00:00.000Z', '29th of the month - 3 months')
+  assert.equal(func(4, 'month'), '2023-05-29T11:00:00.000Z', '29th of the month - 4 months')
+  assert.equal(func(5, 'month'), '2023-06-29T11:00:00.000Z', '29th of the month - 5 months')
+  assert.equal(func(6, 'month'), '2023-07-29T11:00:00.000Z', '29th of the month - 6 months')
+  assert.equal(func(7, 'month'), '2023-08-29T11:00:00.000Z', '29th of the month - 7 months')
+  assert.equal(func(8, 'month'), '2023-09-29T11:00:00.000Z', '29th of the month - 8 months')
+  assert.equal(func(9, 'month'), '2023-10-29T12:00:00.000Z', '29th of the month - 9 months')
+  assert.equal(func(10, 'month'), '2023-11-29T12:00:00.000Z', '29th of the month - 10 months')
+  assert.equal(func(11, 'month'), '2023-12-29T12:00:00.000Z', '29th of the month - 11 months')
+  assert.equal(func(12, 'month'), '2024-01-29T12:00:00.000Z', '29th of the month - 12 months')
+
+  clock = sinon.useFakeTimers({ now: new Date('2023-01-30T12:00:00.000Z') })
+
+  assert.equal(func(1, 'month'), '2023-03-02T12:00:00.000Z', '30th of the month - 1 month')
+  assert.equal(func(2, 'month'), '2023-03-30T11:00:00.000Z', '30th of the month - 2 months')
+  assert.equal(func(3, 'month'), '2023-04-30T11:00:00.000Z', '30th of the month - 3 months')
+  assert.equal(func(4, 'month'), '2023-05-30T11:00:00.000Z', '30th of the month - 4 months')
+  assert.equal(func(5, 'month'), '2023-06-30T11:00:00.000Z', '30th of the month - 5 months')
+  assert.equal(func(6, 'month'), '2023-07-30T11:00:00.000Z', '30th of the month - 6 months')
+  assert.equal(func(7, 'month'), '2023-08-30T11:00:00.000Z', '30th of the month - 7 months')
+  assert.equal(func(8, 'month'), '2023-09-30T11:00:00.000Z', '30th of the month - 8 months')
+  assert.equal(func(9, 'month'), '2023-10-30T12:00:00.000Z', '30th of the month - 9 months')
+  assert.equal(func(10, 'month'), '2023-11-30T12:00:00.000Z', '30th of the month - 10 months')
+  assert.equal(func(11, 'month'), '2023-12-30T12:00:00.000Z', '30th of the month - 11 months')
+  assert.equal(func(12, 'month'), '2024-01-30T12:00:00.000Z', '30th of the month - 12 months')
+
+  clock = sinon.useFakeTimers({ now: new Date('2023-01-31T12:00:00.000Z') })
+
+  assert.equal(func(1, 'month'), '2023-03-03T12:00:00.000Z', '31st of the month - 1 month')
+  assert.equal(func(2, 'month'), '2023-03-31T11:00:00.000Z', '31st of the month - 2 months')
+  assert.equal(func(3, 'month'), '2023-05-01T11:00:00.000Z', '31st of the month - 3 months')
+  assert.equal(func(4, 'month'), '2023-05-31T11:00:00.000Z', '31st of the month - 4 months')
+  assert.equal(func(5, 'month'), '2023-07-01T11:00:00.000Z', '31st of the month - 5 months')
+  assert.equal(func(6, 'month'), '2023-07-31T11:00:00.000Z', '31st of the month - 6 months')
+  assert.equal(func(7, 'month'), '2023-08-31T11:00:00.000Z', '31st of the month - 7 months')
+  assert.equal(func(8, 'month'), '2023-10-01T11:00:00.000Z', '31st of the month - 8 months')
+  assert.equal(func(9, 'month'), '2023-10-31T12:00:00.000Z', '31st of the month - 9 months')
+  assert.equal(func(10, 'month'), '2023-12-01T12:00:00.000Z', '31st of the month - 10 months')
+  assert.equal(func(11, 'month'), '2023-12-31T12:00:00.000Z', '31st of the month - 11 months')
+  assert.equal(func(12, 'month'), '2024-01-31T12:00:00.000Z', '31st of the month - 12 months')
+
+  clock.restore()
+})

+ 182 - 0
spec/rspec/time_helper_spec.rb

@@ -0,0 +1,182 @@
+# Copyright (C) 2012-2023 Zammad Foundation, https://zammad-foundation.org/
+
+require 'rails_helper'
+
+RSpec.describe 'TimeHelperCache', time_zone: 'Europe/London' do
+  context 'with frontend_relative_month' do
+    before do
+      freeze_time
+      travel_to DateTime.parse(datestamp)
+    end
+
+    context "when it's the 1st day of the month" do
+      let(:datestamp) { '2023-01-01T12:00:00.000Z' }
+
+      it 'matches ECMAScript results' do
+        expect([
+                 frontend_relative_month(Time.current, 1),
+                 frontend_relative_month(Time.current, 2),
+                 frontend_relative_month(Time.current, 3),
+                 frontend_relative_month(Time.current, 4),
+                 frontend_relative_month(Time.current, 5),
+                 frontend_relative_month(Time.current, 6),
+                 frontend_relative_month(Time.current, 7),
+                 frontend_relative_month(Time.current, 8),
+                 frontend_relative_month(Time.current, 9),
+                 frontend_relative_month(Time.current, 10),
+                 frontend_relative_month(Time.current, 11),
+                 frontend_relative_month(Time.current, 12),
+               ]).to eq([
+                          '2023-02-01T12:00:00.000Z',
+                          '2023-03-01T12:00:00.000Z',
+                          '2023-04-01T11:00:00.000Z',
+                          '2023-05-01T11:00:00.000Z',
+                          '2023-06-01T11:00:00.000Z',
+                          '2023-07-01T11:00:00.000Z',
+                          '2023-08-01T11:00:00.000Z',
+                          '2023-09-01T11:00:00.000Z',
+                          '2023-10-01T11:00:00.000Z',
+                          '2023-11-01T12:00:00.000Z',
+                          '2023-12-01T12:00:00.000Z',
+                          '2024-01-01T12:00:00.000Z',
+                        ])
+      end
+    end
+
+    context "when it's the 28th day of the month" do
+      let(:datestamp) { '2023-01-28T12:00:00.000Z' }
+
+      it 'matches ECMAScript result' do
+        expect([
+                 frontend_relative_month(Time.current, 1),
+                 frontend_relative_month(Time.current, 2),
+                 frontend_relative_month(Time.current, 3),
+                 frontend_relative_month(Time.current, 4),
+                 frontend_relative_month(Time.current, 5),
+                 frontend_relative_month(Time.current, 6),
+                 frontend_relative_month(Time.current, 7),
+                 frontend_relative_month(Time.current, 8),
+                 frontend_relative_month(Time.current, 9),
+                 frontend_relative_month(Time.current, 10),
+                 frontend_relative_month(Time.current, 11),
+                 frontend_relative_month(Time.current, 12),
+               ]).to eq([
+                          '2023-02-28T12:00:00.000Z',
+                          '2023-03-28T11:00:00.000Z',
+                          '2023-04-28T11:00:00.000Z',
+                          '2023-05-28T11:00:00.000Z',
+                          '2023-06-28T11:00:00.000Z',
+                          '2023-07-28T11:00:00.000Z',
+                          '2023-08-28T11:00:00.000Z',
+                          '2023-09-28T11:00:00.000Z',
+                          '2023-10-28T11:00:00.000Z',
+                          '2023-11-28T12:00:00.000Z',
+                          '2023-12-28T12:00:00.000Z',
+                          '2024-01-28T12:00:00.000Z',
+                        ])
+      end
+    end
+
+    context "when it's the 29th day of the month" do
+      let(:datestamp) { '2023-01-29T12:00:00.000Z' }
+
+      it 'matches ECMAScript result' do
+        expect([
+                 frontend_relative_month(Time.current, 1),
+                 frontend_relative_month(Time.current, 2),
+                 frontend_relative_month(Time.current, 3),
+                 frontend_relative_month(Time.current, 4),
+                 frontend_relative_month(Time.current, 5),
+                 frontend_relative_month(Time.current, 6),
+                 frontend_relative_month(Time.current, 7),
+                 frontend_relative_month(Time.current, 8),
+                 frontend_relative_month(Time.current, 9),
+                 frontend_relative_month(Time.current, 10),
+                 frontend_relative_month(Time.current, 11),
+                 frontend_relative_month(Time.current, 12),
+               ]).to eq([
+                          '2023-03-01T12:00:00.000Z',
+                          '2023-03-29T11:00:00.000Z',
+                          '2023-04-29T11:00:00.000Z',
+                          '2023-05-29T11:00:00.000Z',
+                          '2023-06-29T11:00:00.000Z',
+                          '2023-07-29T11:00:00.000Z',
+                          '2023-08-29T11:00:00.000Z',
+                          '2023-09-29T11:00:00.000Z',
+                          '2023-10-29T12:00:00.000Z',
+                          '2023-11-29T12:00:00.000Z',
+                          '2023-12-29T12:00:00.000Z',
+                          '2024-01-29T12:00:00.000Z',
+                        ])
+      end
+    end
+
+    context "when it's the 30th day of the month" do
+      let(:datestamp) { '2023-01-30T12:00:00.000Z' }
+
+      it 'matches ECMAScript result' do
+        expect([
+                 frontend_relative_month(Time.current, 1),
+                 frontend_relative_month(Time.current, 2),
+                 frontend_relative_month(Time.current, 3),
+                 frontend_relative_month(Time.current, 4),
+                 frontend_relative_month(Time.current, 5),
+                 frontend_relative_month(Time.current, 6),
+                 frontend_relative_month(Time.current, 7),
+                 frontend_relative_month(Time.current, 8),
+                 frontend_relative_month(Time.current, 9),
+                 frontend_relative_month(Time.current, 10),
+                 frontend_relative_month(Time.current, 11),
+                 frontend_relative_month(Time.current, 12),
+               ]).to eq([
+                          '2023-03-02T12:00:00.000Z',
+                          '2023-03-30T11:00:00.000Z',
+                          '2023-04-30T11:00:00.000Z',
+                          '2023-05-30T11:00:00.000Z',
+                          '2023-06-30T11:00:00.000Z',
+                          '2023-07-30T11:00:00.000Z',
+                          '2023-08-30T11:00:00.000Z',
+                          '2023-09-30T11:00:00.000Z',
+                          '2023-10-30T12:00:00.000Z',
+                          '2023-11-30T12:00:00.000Z',
+                          '2023-12-30T12:00:00.000Z',
+                          '2024-01-30T12:00:00.000Z',
+                        ])
+      end
+    end
+
+    context "when it's the 31st day of the month" do
+      let(:datestamp) { '2023-01-31T12:00:00.000Z' }
+
+      it 'matches ECMAScript result' do
+        expect([
+                 frontend_relative_month(Time.current, 1),
+                 frontend_relative_month(Time.current, 2),
+                 frontend_relative_month(Time.current, 3),
+                 frontend_relative_month(Time.current, 4),
+                 frontend_relative_month(Time.current, 5),
+                 frontend_relative_month(Time.current, 6),
+                 frontend_relative_month(Time.current, 7),
+                 frontend_relative_month(Time.current, 8),
+                 frontend_relative_month(Time.current, 9),
+                 frontend_relative_month(Time.current, 10),
+                 frontend_relative_month(Time.current, 11),
+                 frontend_relative_month(Time.current, 12),
+               ]).to eq([
+                          '2023-03-03T12:00:00.000Z',
+                          '2023-03-31T11:00:00.000Z',
+                          '2023-05-01T11:00:00.000Z',
+                          '2023-05-31T11:00:00.000Z',
+                          '2023-07-01T11:00:00.000Z',
+                          '2023-07-31T11:00:00.000Z',
+                          '2023-08-31T11:00:00.000Z',
+                          '2023-10-01T11:00:00.000Z',
+                          '2023-10-31T12:00:00.000Z',
+                          '2023-12-01T12:00:00.000Z',
+                          '2023-12-31T12:00:00.000Z',
+                          '2024-01-31T12:00:00.000Z',
+                        ])
+      end
+    end
+  end
+end

+ 41 - 0
spec/support/time_helper.rb

@@ -15,6 +15,47 @@ module TimeHelperCache
   def browser_travel_to(time)
     execute_script "window.clock = sinon.useFakeTimers({now: new Date(#{time.to_i * 1_000}), toFake: ['Date']})"
   end
+
+  # Reimplementation of `setMonth(month[, date])` from the ECMAScript specification.
+  #   https://tc39.es/ecma262/multipage/numbers-and-dates.html#sec-date.prototype.setmonth
+  def frontend_relative_month(obj, month, date = nil)
+    # 1. Let t be ? thisTimeValue(this value).
+    t = obj
+
+    # 2. Let m be ? ToNumber(month).
+    m = month.to_i
+
+    # 3. If date is present, let dt be ? ToNumber(date).
+    if date.present?
+      dt = date.to_i
+    end
+
+    # 4. If t is NaN, return NaN.
+    raise InvalidDate if !t.is_a?(Time)
+
+    # 5. Set t to LocalTime(t).
+    t = t.in_time_zone
+
+    # 6. If date is not present, let dt be DateFromTime(t).
+    if date.nil?
+      dt = t.day
+    end
+
+    # 7. Let newDate be MakeDate(MakeDay(YearFromTime(t), m, dt), TimeWithinDay(t)).
+    new_year = t.year
+    new_month = t.month + m
+    if new_month > 12
+      new_year += 1
+      new_month -= 12
+    end
+    Time.zone.local(new_year, new_month, dt, t.hour, t.min, t.sec)
+
+    # Ignore the rest, as `Time#local` already handles it correctly:
+    #   8. Let u be TimeClip(UTC(newDate)).
+    #   9. Set the [[DateValue]] internal slot of this Date object to u.
+    #   10. Return u.
+
+  end
 end
 
 RSpec.configure do |config|

+ 5 - 10
spec/system/ticket/create_spec.rb

@@ -1333,16 +1333,11 @@ RSpec.describe 'Ticket Create', type: :system do
         let(:operator)       { 'relative' }
         let(:template_value) { value.to_s }
         let(:date) do
-          # Since front-end uses a JS-specific function to add a single month to the current date,
-          #   calculating the value here with Ruby-specific code may lead to unexpected values.
-          #   E.g.:
-          #   - now = Mon Jan 30 2023 09:43:38 GMT+0000
-          #   - now.setMonth(now.getMonth() + 1) = Thu Mar 02 2023 09:43:38 GMT+0000
-          #   - 1.month.from_now = Tue Feb 28 2023 09:43:38 GMT+0000
-          #   Therefore, we mimic the behavior of `setMonth()` from the ECMAScript specification.
-          #   https://tc39.es/ecma262/multipage/numbers-and-dates.html#sec-date.prototype.setmonth
-          if range == 'month' && value == 1
-            Time.current + (Time.current.end_of_month.day * 24 * 60 * 60)
+          # Since front-end uses a JS-specific function to add a month value to the current date,
+          #   calculating the value here with Ruby code may lead to unexpected values.
+          #   Therefore, we use a reimplementation of the ECMAScript function instead.
+          if range == 'month'
+            frontend_relative_month(Time.current, value)
           else
             value.send(range).from_now
           end