Skip to content

Commit 43a7e2d

Browse files
committed
test: edge cases
1 parent 4701b91 commit 43a7e2d

File tree

1 file changed

+341
-0
lines changed

1 file changed

+341
-0
lines changed

tests/SvelteTimeEdgeCases.test.ts

Lines changed: 341 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,341 @@
1+
import dayjs from "dayjs";
2+
import { flushSync, mount, tick, unmount } from "svelte";
3+
import Time from "svelte-time";
4+
5+
describe("svelte-time-edge-cases", () => {
6+
let instance: null | ReturnType<typeof mount> = null;
7+
const FIXED_DATE = new Date("2024-01-01T00:00:00.000Z");
8+
9+
beforeEach(() => {
10+
vi.useFakeTimers();
11+
vi.setSystemTime(FIXED_DATE);
12+
});
13+
14+
afterEach(() => {
15+
vi.restoreAllMocks();
16+
if (instance) {
17+
unmount(instance);
18+
}
19+
instance = null;
20+
document.body.innerHTML = "";
21+
});
22+
23+
const getElement = (selector: string) => {
24+
return document.querySelector(selector) as HTMLElement;
25+
};
26+
27+
describe("live interval edge cases", () => {
28+
test("live with negative interval should use absolute value", async () => {
29+
const target = document.body;
30+
31+
instance = mount(Time, {
32+
target,
33+
props: {
34+
"data-test": "live-negative",
35+
relative: true,
36+
live: -5000,
37+
},
38+
});
39+
flushSync();
40+
41+
const element = getElement('[data-test="live-negative"]');
42+
expect(element.innerHTML).toEqual("a few seconds ago");
43+
44+
vi.advanceTimersByTime(5000);
45+
await tick();
46+
expect(element.innerHTML).toEqual("a few seconds ago");
47+
});
48+
49+
test("live with very large interval", async () => {
50+
const target = document.body;
51+
const setIntervalSpy = vi.spyOn(global, "setInterval");
52+
53+
instance = mount(Time, {
54+
target,
55+
props: {
56+
"data-test": "live-large",
57+
relative: true,
58+
live: 1000000,
59+
},
60+
});
61+
flushSync();
62+
63+
const element = getElement('[data-test="live-large"]');
64+
expect(element.innerHTML).toEqual("a few seconds ago");
65+
66+
expect(setIntervalSpy).toHaveBeenCalledWith(
67+
expect.any(Function),
68+
1000000,
69+
);
70+
setIntervalSpy.mockRestore();
71+
});
72+
73+
test("live without relative should not create interval", async () => {
74+
const setIntervalSpy = vi.spyOn(global, "setInterval");
75+
76+
const target = document.body;
77+
78+
instance = mount(Time, {
79+
target,
80+
props: {
81+
"data-test": "live-no-relative",
82+
relative: false,
83+
live: true,
84+
},
85+
});
86+
flushSync();
87+
expect(setIntervalSpy).not.toHaveBeenCalled();
88+
setIntervalSpy.mockRestore();
89+
});
90+
});
91+
92+
describe("memory leak / interval cleanup", () => {
93+
test("should clear interval on unmount", async () => {
94+
const clearIntervalSpy = vi.spyOn(global, "clearInterval");
95+
96+
const target = document.body;
97+
98+
instance = mount(Time, {
99+
target,
100+
props: {
101+
"data-test": "cleanup",
102+
relative: true,
103+
live: true,
104+
},
105+
});
106+
flushSync();
107+
108+
const element = getElement('[data-test="cleanup"]');
109+
expect(element.innerHTML).toEqual("a few seconds ago");
110+
111+
unmount(instance);
112+
instance = null;
113+
114+
expect(clearIntervalSpy).toHaveBeenCalled();
115+
clearIntervalSpy.mockRestore();
116+
});
117+
118+
test("multiple components should each manage their own intervals", async () => {
119+
const target = document.body;
120+
121+
const instance1 = mount(Time, {
122+
target,
123+
props: {
124+
"data-test": "multi-1",
125+
relative: true,
126+
live: 30000,
127+
},
128+
});
129+
flushSync();
130+
131+
const instance2 = mount(Time, {
132+
target,
133+
props: {
134+
"data-test": "multi-2",
135+
relative: true,
136+
live: 15000,
137+
},
138+
});
139+
flushSync();
140+
141+
const element1 = getElement('[data-test="multi-1"]');
142+
const element2 = getElement('[data-test="multi-2"]');
143+
144+
expect(element1.innerHTML).toEqual("a few seconds ago");
145+
expect(element2.innerHTML).toEqual("a few seconds ago");
146+
147+
vi.advanceTimersByTime(15000);
148+
await tick();
149+
150+
expect(element1.innerHTML).toEqual("a few seconds ago");
151+
expect(element2.innerHTML).toEqual("a few seconds ago");
152+
153+
vi.advanceTimersByTime(15000);
154+
await tick();
155+
156+
expect(element1.innerHTML).toEqual("a few seconds ago");
157+
expect(element2.innerHTML).toEqual("a few seconds ago");
158+
159+
unmount(instance1);
160+
unmount(instance2);
161+
});
162+
163+
test("no memory leak with many component creates and destroys", async () => {
164+
const target = document.body;
165+
const clearIntervalSpy = vi.spyOn(global, "clearInterval");
166+
167+
for (let i = 0; i < 100; i++) {
168+
const inst = mount(Time, {
169+
target,
170+
props: {
171+
"data-test": `leak-test-${i}`,
172+
relative: true,
173+
live: true,
174+
},
175+
});
176+
flushSync();
177+
unmount(inst);
178+
document.body.innerHTML = "";
179+
}
180+
181+
expect(clearIntervalSpy).toHaveBeenCalledTimes(100);
182+
clearIntervalSpy.mockRestore();
183+
});
184+
});
185+
186+
describe("timestamp edge cases", () => {
187+
test("very far future date", async () => {
188+
const target = document.body;
189+
const futureDate = dayjs().add(100, "years").toISOString();
190+
191+
instance = mount(Time, {
192+
target,
193+
props: {
194+
"data-test": "far-future",
195+
timestamp: futureDate,
196+
relative: true,
197+
},
198+
});
199+
flushSync();
200+
201+
const element = getElement('[data-test="far-future"]');
202+
expect(/in/.test(element.innerHTML)).toEqual(true);
203+
});
204+
205+
test("very far past date", async () => {
206+
const target = document.body;
207+
const pastDate = dayjs().subtract(1000, "years").toISOString();
208+
209+
instance = mount(Time, {
210+
target,
211+
props: {
212+
"data-test": "far-past",
213+
timestamp: pastDate,
214+
relative: true,
215+
},
216+
});
217+
flushSync();
218+
219+
const element = getElement('[data-test="far-past"]');
220+
expect(/ago/.test(element.innerHTML)).toEqual(true);
221+
});
222+
223+
test("date near epoch (1970)", async () => {
224+
const target = document.body;
225+
226+
instance = mount(Time, {
227+
target,
228+
props: {
229+
"data-test": "epoch",
230+
timestamp: 0,
231+
format: "YYYY-MM-DD",
232+
},
233+
});
234+
flushSync();
235+
236+
const element = getElement('[data-test="epoch"]');
237+
const formattedDate = dayjs(0).format("YYYY-MM-DD");
238+
expect(element.innerHTML).toEqual(formattedDate);
239+
expect(element.getAttribute("datetime")).toEqual("0");
240+
});
241+
242+
test("undefined timestamp should use default (current date)", async () => {
243+
const target = document.body;
244+
245+
instance = mount(Time, {
246+
target,
247+
props: {
248+
"data-test": "undefined-timestamp",
249+
},
250+
});
251+
flushSync();
252+
253+
const element = getElement('[data-test="undefined-timestamp"]');
254+
expect(element.innerHTML).toEqual(
255+
dayjs(FIXED_DATE).format("MMM DD, YYYY"),
256+
);
257+
});
258+
});
259+
260+
describe("format edge cases", () => {
261+
test("empty format string", async () => {
262+
const target = document.body;
263+
264+
instance = mount(Time, {
265+
target,
266+
props: {
267+
"data-test": "empty-format",
268+
timestamp: "2020-02-01",
269+
format: "",
270+
},
271+
});
272+
flushSync();
273+
274+
const element = getElement('[data-test="empty-format"]');
275+
expect(element.innerHTML).toBeTruthy();
276+
});
277+
278+
test("complex format with escaped characters", async () => {
279+
const target = document.body;
280+
281+
instance = mount(Time, {
282+
target,
283+
props: {
284+
"data-test": "escaped-format",
285+
timestamp: "2020-02-01",
286+
format: "[Today is] YYYY-MM-DD [at] HH:mm",
287+
},
288+
});
289+
flushSync();
290+
291+
const element = getElement('[data-test="escaped-format"]');
292+
expect(element.innerHTML).toContain("Today is");
293+
expect(element.innerHTML).toContain("2020-02-01");
294+
expect(element.innerHTML).toContain("at");
295+
});
296+
});
297+
298+
describe("custom HTML attributes", () => {
299+
test("should pass through custom HTML attributes", async () => {
300+
const target = document.body;
301+
302+
instance = mount(Time, {
303+
target,
304+
props: {
305+
"data-test": "custom-attrs",
306+
"data-custom": "custom-value",
307+
id: "time-element",
308+
class: "time-class",
309+
timestamp: "2020-02-01",
310+
},
311+
});
312+
flushSync();
313+
314+
const element = getElement('[data-test="custom-attrs"]');
315+
expect(element.getAttribute("data-custom")).toEqual("custom-value");
316+
expect(element.id).toEqual("time-element");
317+
expect(element.className).toEqual("time-class");
318+
});
319+
320+
test("should pass through aria attributes", async () => {
321+
const target = document.body;
322+
323+
instance = mount(Time, {
324+
target,
325+
props: {
326+
"data-test": "aria-attrs",
327+
"aria-label": "Event time",
328+
"aria-describedby": "time-description",
329+
timestamp: "2020-02-01",
330+
},
331+
});
332+
flushSync();
333+
334+
const element = getElement('[data-test="aria-attrs"]');
335+
expect(element.getAttribute("aria-label")).toEqual("Event time");
336+
expect(element.getAttribute("aria-describedby")).toEqual(
337+
"time-description",
338+
);
339+
});
340+
});
341+
});

0 commit comments

Comments
 (0)