|
| 1 | +/** |
| 2 | + * SPDX-FileCopyrightText: 2026 LibreSign contributors |
| 3 | + * SPDX-License-Identifier: AGPL-3.0-or-later |
| 4 | + */ |
| 5 | + |
| 6 | +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' |
| 7 | + |
| 8 | +import { getTimePresetRange, getTimePresets } from '../../utils/timePresets.js' |
| 9 | + |
| 10 | +vi.mock('@nextcloud/l10n', () => ({ |
| 11 | + t: (_app: string, text: string) => text, |
| 12 | +})) |
| 13 | + |
| 14 | +describe('getTimePresets', () => { |
| 15 | + describe('business rule: should return all 5 presets with correct structure', () => { |
| 16 | + it('returns exactly 5 presets', () => { |
| 17 | + expect(getTimePresets()).toHaveLength(5) |
| 18 | + }) |
| 19 | + |
| 20 | + it('each preset has id, label, start, end', () => { |
| 21 | + getTimePresets().forEach(preset => { |
| 22 | + expect(preset).toHaveProperty('id') |
| 23 | + expect(preset).toHaveProperty('label') |
| 24 | + expect(preset).toHaveProperty('start') |
| 25 | + expect(preset).toHaveProperty('end') |
| 26 | + }) |
| 27 | + }) |
| 28 | + |
| 29 | + it('returns presets with the expected ids in order', () => { |
| 30 | + const ids = getTimePresets().map(p => p.id) |
| 31 | + expect(ids).toEqual(['today', 'last-7', 'last-30', 'this-year', 'last-year']) |
| 32 | + }) |
| 33 | + |
| 34 | + it('each preset start is before its end', () => { |
| 35 | + getTimePresets().forEach(preset => { |
| 36 | + expect(preset.start).toBeLessThan(preset.end) |
| 37 | + }) |
| 38 | + }) |
| 39 | + }) |
| 40 | + |
| 41 | + describe('business rule: today preset should span the current day', () => { |
| 42 | + it('start is midnight of today', () => { |
| 43 | + const preset = getTimePresets().find(p => p.id === 'today')! |
| 44 | + const d = new Date(preset.start) |
| 45 | + expect(d.getHours()).toBe(0) |
| 46 | + expect(d.getMinutes()).toBe(0) |
| 47 | + expect(d.getSeconds()).toBe(0) |
| 48 | + }) |
| 49 | + |
| 50 | + it('end is 23:59:59.999 of today', () => { |
| 51 | + const preset = getTimePresets().find(p => p.id === 'today')! |
| 52 | + const d = new Date(preset.end) |
| 53 | + expect(d.getHours()).toBe(23) |
| 54 | + expect(d.getMinutes()).toBe(59) |
| 55 | + expect(d.getSeconds()).toBe(59) |
| 56 | + }) |
| 57 | + }) |
| 58 | + |
| 59 | + describe('business rule: range widths should match their names', () => { |
| 60 | + const MS_PER_DAY = 24 * 60 * 60 * 1000 |
| 61 | + |
| 62 | + it('last-7 spans approximately 7 days', () => { |
| 63 | + const preset = getTimePresets().find(p => p.id === 'last-7')! |
| 64 | + const days = (preset.end - preset.start) / MS_PER_DAY |
| 65 | + expect(days).toBeGreaterThanOrEqual(7) |
| 66 | + expect(days).toBeLessThan(8) |
| 67 | + }) |
| 68 | + |
| 69 | + it('last-30 spans approximately 30 days', () => { |
| 70 | + const preset = getTimePresets().find(p => p.id === 'last-30')! |
| 71 | + const days = (preset.end - preset.start) / MS_PER_DAY |
| 72 | + expect(days).toBeGreaterThanOrEqual(30) |
| 73 | + expect(days).toBeLessThan(31) |
| 74 | + }) |
| 75 | + |
| 76 | + it('this-year starts on January 1st', () => { |
| 77 | + const preset = getTimePresets().find(p => p.id === 'this-year')! |
| 78 | + const d = new Date(preset.start) |
| 79 | + expect(d.getMonth()).toBe(0) |
| 80 | + expect(d.getDate()).toBe(1) |
| 81 | + expect(d.getFullYear()).toBe(new Date().getFullYear()) |
| 82 | + }) |
| 83 | + |
| 84 | + it('last-year starts on January 1st of the previous year', () => { |
| 85 | + const preset = getTimePresets().find(p => p.id === 'last-year')! |
| 86 | + const d = new Date(preset.start) |
| 87 | + expect(d.getMonth()).toBe(0) |
| 88 | + expect(d.getDate()).toBe(1) |
| 89 | + expect(d.getFullYear()).toBe(new Date().getFullYear() - 1) |
| 90 | + }) |
| 91 | + }) |
| 92 | + |
| 93 | + describe('business rule: dates are computed fresh on each call', () => { |
| 94 | + beforeEach(() => { |
| 95 | + vi.useFakeTimers() |
| 96 | + }) |
| 97 | + |
| 98 | + afterEach(() => { |
| 99 | + vi.useRealTimers() |
| 100 | + }) |
| 101 | + |
| 102 | + it('returns different start for today when the date changes', () => { |
| 103 | + vi.setSystemTime(new Date('2026-01-01T00:00:00')) |
| 104 | + const presets1 = getTimePresets() |
| 105 | + |
| 106 | + vi.setSystemTime(new Date('2026-02-15T00:00:00')) |
| 107 | + const presets2 = getTimePresets() |
| 108 | + |
| 109 | + expect(presets1[0].start).not.toBe(presets2[0].start) |
| 110 | + }) |
| 111 | + |
| 112 | + it('always uses the fake current date, not a cached value', () => { |
| 113 | + vi.setSystemTime(new Date('2026-06-15T12:00:00')) |
| 114 | + const preset = getTimePresets().find(p => p.id === 'this-year')! |
| 115 | + expect(new Date(preset.start).getFullYear()).toBe(2026) |
| 116 | + }) |
| 117 | + }) |
| 118 | +}) |
| 119 | + |
| 120 | +describe('getTimePresetRange', () => { |
| 121 | + describe('business rule: should return null for missing or unknown ids', () => { |
| 122 | + it('returns null when presetId is empty string', () => { |
| 123 | + expect(getTimePresetRange('')).toBeNull() |
| 124 | + }) |
| 125 | + |
| 126 | + it('returns null when presetId is null/undefined', () => { |
| 127 | + expect(getTimePresetRange(null as unknown as string)).toBeNull() |
| 128 | + expect(getTimePresetRange(undefined as unknown as string)).toBeNull() |
| 129 | + }) |
| 130 | + |
| 131 | + it('returns null for unknown preset id', () => { |
| 132 | + expect(getTimePresetRange('unknown')).toBeNull() |
| 133 | + expect(getTimePresetRange('weekly')).toBeNull() |
| 134 | + }) |
| 135 | + }) |
| 136 | + |
| 137 | + describe('business rule: should return { start, end } for known ids', () => { |
| 138 | + it.each(['today', 'last-7', 'last-30', 'this-year', 'last-year'])('returns range for "%s"', (id) => { |
| 139 | + const range = getTimePresetRange(id) |
| 140 | + expect(range).not.toBeNull() |
| 141 | + expect(range!.start).toBeTypeOf('number') |
| 142 | + expect(range!.end).toBeTypeOf('number') |
| 143 | + expect(range!.start).toBeLessThan(range!.end) |
| 144 | + }) |
| 145 | + |
| 146 | + it('range values match getTimePresets output', () => { |
| 147 | + const preset = getTimePresets().find(p => p.id === 'last-7')! |
| 148 | + const range = getTimePresetRange('last-7')! |
| 149 | + expect(range.start).toBe(preset.start) |
| 150 | + expect(range.end).toBe(preset.end) |
| 151 | + }) |
| 152 | + }) |
| 153 | +}) |
0 commit comments