Skip to content

Commit a7d05e5

Browse files
fix: refactor mt-datepicker (#942)
* fix: timezone issue test: add additional tests for timezones fix: timezone issue fix: incorrect timezone value fix: range value fix: range value time fix: time range fix: hide time hint in time mode refactor remove unused timeToIso method fix: time case fix: type error test: add additional test feat: rename methods docs: add dateType select to story fix: improve type guards refactor: allow date objects as props test add default datepicker visual test feat: add hour and minute increment prop to datepicker test: add tests for increment props style: code format update snapshots feat: implement helpText style: code format update snapshots test: simplify timezone fix test add non timezone test style: code format refactor: simplify timezone fix feat: implement text-input test: add test for text input fix: exact match prop naming fix: remove console.log * chore: add changeset
1 parent 502e096 commit a7d05e5

8 files changed

+501
-105
lines changed

.changeset/slimy-rules-nail.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@shopware-ag/meteor-component-library": minor
3+
---
4+
5+
Fix timezone issue in `mt-datepicker`
6.47 KB
Loading
20 Bytes
Loading
8.77 KB
Loading

packages/component-library/src/components/form/mt-datepicker/mt-datepicker.interactive.stories.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,13 @@ export default {
99
title: "Interaction Tests/Form/mt-datepicker",
1010
} as MtDatepickerMeta;
1111

12+
export const VisualTestDatepickerDefault: MtDatepickerStory = {
13+
name: "Render datepicker",
14+
args: {
15+
label: "Datepicker",
16+
},
17+
};
18+
1219
export const TestDatepickerShouldOpen: MtDatepickerStory = {
1320
name: "Should open datepicker",
1421
args: {
@@ -417,3 +424,17 @@ export const VisualTestMinDateDisabledDays: MtDatepickerStory = {
417424
expect(day15).toBeDefined();
418425
},
419426
};
427+
428+
export const VisualTestHelpText: MtDatepickerStory = {
429+
name: "Should display help text",
430+
args: {
431+
label: "Datepicker",
432+
helpText: "This is a help text",
433+
},
434+
play: async () => {
435+
const canvas = within(document.body);
436+
await userEvent.tab();
437+
438+
expect(canvas.getByRole("tooltip")).toBeInTheDocument();
439+
},
440+
};

packages/component-library/src/components/form/mt-datepicker/mt-datepicker.spec.ts

Lines changed: 317 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
1-
import { describe, it, expect } from "vitest";
1+
import { describe, it, expect, vi, beforeEach } from "vitest";
22
import { render, screen } from "@testing-library/vue";
33
import MtDatepicker from "./mt-datepicker.vue";
44
import userEvent from "@testing-library/user-event";
5+
import { waitUntil } from "@/_internal/test-helper";
6+
import { getByRole } from "@storybook/test";
57

68
describe("mt-datepicker", () => {
9+
beforeEach(() => {
10+
// Set system time to ensure consistent test results
11+
vi.setSystemTime(new Date("2024-07-15T09:00:00Z"));
12+
});
713
it("is enabled by default", () => {
814
// ARRANGE
915
render(MtDatepicker);
@@ -158,4 +164,314 @@ describe("mt-datepicker", () => {
158164
// ASSERT - Check that the component renders without errors when minDate is provided
159165
expect(screen.getByRole("textbox")).toBeInTheDocument();
160166
});
167+
168+
it("emits date when value when typed into input", async () => {
169+
// ARRANGE
170+
const handler = vi.fn();
171+
render(MtDatepicker, {
172+
props: {
173+
dateType: "date",
174+
textInput: true,
175+
timeZone: "UTC",
176+
"onUpdate:modelValue": handler,
177+
},
178+
});
179+
180+
const input = screen.getByRole("textbox");
181+
182+
// ACT
183+
await userEvent.clear(input);
184+
await userEvent.type(input, "2025/01/01");
185+
await userEvent.keyboard("{enter}");
186+
187+
// ASSERT
188+
expect(handler).toHaveBeenLastCalledWith("2025-01-01T00:00:00.000Z");
189+
});
190+
191+
it("should accept an ISO string as modelValue", async () => {
192+
// ARRANGE
193+
const handler = vi.fn();
194+
await render(MtDatepicker, {
195+
props: {
196+
dateType: "datetime",
197+
modelValue: "2024-03-20T14:30:00+01:00",
198+
timeZone: "UTC",
199+
"onUpdate:modelValue": handler,
200+
},
201+
});
202+
203+
// ASSERT
204+
expect(screen.getByRole("textbox")).toHaveValue("2024/03/20, 13:30");
205+
});
206+
207+
it("should accept a date object as modelValue", async () => {
208+
const date = new Date("2024-03-20T14:30:00+01:00");
209+
210+
// ARRANGE
211+
await render(MtDatepicker, {
212+
props: {
213+
dateType: "datetime",
214+
modelValue: date,
215+
},
216+
});
217+
218+
// ASSERT
219+
expect(screen.getByRole("textbox")).toHaveValue("2024/03/20, 13:30");
220+
});
221+
222+
it("should accept ISO string array as modelValue", async () => {
223+
// ARRANGE
224+
const handler = vi.fn();
225+
await render(MtDatepicker, {
226+
props: {
227+
dateType: "datetime",
228+
range: true,
229+
modelValue: ["2024-03-20T14:30:00+01:00", "2024-03-21T14:30:00+01:00"],
230+
timeZone: "UTC",
231+
"onUpdate:modelValue": handler,
232+
},
233+
});
234+
235+
// ASSERT
236+
expect(screen.getByRole("textbox")).toHaveValue("2024/03/20, 13:30 - 2024/03/21, 13:30");
237+
});
238+
239+
it("converts time correctly when timezone changes", async () => {
240+
// ARRANGE - Set a specific UTC time
241+
const { rerender } = await render(MtDatepicker, {
242+
props: {
243+
dateType: "datetime",
244+
timeZone: "UTC",
245+
modelValue: "2024-12-31T13:00:00Z",
246+
},
247+
});
248+
249+
// ACT - Change timezone to America/New_York (UTC-5 in November)
250+
await rerender({
251+
dateType: "datetime",
252+
timeZone: "Australia/Sydney",
253+
});
254+
255+
// ASSERT - 10:30 UTC should become 05:30 EST
256+
const input = document.querySelector(".dp__input") as HTMLInputElement;
257+
expect(input.value).toBe("2025/01/01, 00:00");
258+
});
259+
260+
it("should emit an iso string with the correct converted time", async () => {
261+
const handler = vi.fn();
262+
// ARRANGE
263+
render(MtDatepicker, {
264+
props: {
265+
dateType: "datetime",
266+
timeZone: "America/New_York",
267+
modelValue: "2023-07-18T09:15:00.000Z",
268+
"onUpdate:modelValue": handler,
269+
},
270+
});
271+
272+
// ACT - select a datetime
273+
await userEvent.click(screen.getByRole("textbox"));
274+
await waitUntil(() => document.getElementsByClassName("dp__menu").length > 0);
275+
await waitUntil(() => document.getElementsByClassName("dp__time_input") !== null);
276+
277+
// Set the hours
278+
await userEvent.click(
279+
document.querySelector('[data-test-id="hours-toggle-overlay-btn-0"]') as HTMLElement,
280+
);
281+
await waitUntil(() => document.querySelector(".dp__overlay") !== null);
282+
await userEvent.click(document.querySelector('[data-test-id="00"]') as HTMLElement);
283+
await waitUntil(() => !document.querySelector(".dp__overlay"));
284+
await waitUntil(() =>
285+
expect(
286+
document.querySelector('[data-test-id="hours-toggle-overlay-btn-0"]'),
287+
).toHaveTextContent("00"),
288+
);
289+
290+
// Set the minutes
291+
await userEvent.click(
292+
document.querySelector('[data-test-id="minutes-toggle-overlay-btn-0"]') as HTMLElement,
293+
);
294+
await waitUntil(() => document.querySelector(".dp__overlay") !== null);
295+
await userEvent.click(document.querySelector('[data-test-id="00"]') as HTMLElement);
296+
await waitUntil(() => !document.querySelector(".dp__overlay"));
297+
expect(
298+
document.querySelector('[data-test-id="minutes-toggle-overlay-btn-0"]'),
299+
).toHaveTextContent("00");
300+
301+
// Set the month
302+
await userEvent.click(
303+
document.querySelector('[data-test-id="month-toggle-overlay-0"]') as HTMLElement,
304+
);
305+
await waitUntil(() => document.querySelector(".dp__overlay") !== null);
306+
await userEvent.click(document.querySelector('[data-test-id="Mär"]') as HTMLElement);
307+
await waitUntil(() => !document.querySelector(".dp__overlay"));
308+
expect(document.querySelector('[data-test-id="month-toggle-overlay-0"]')).toHaveTextContent(
309+
"Mär",
310+
);
311+
312+
// Set the year
313+
await userEvent.click(
314+
document.querySelector('[data-test-id="year-toggle-overlay-0"]') as HTMLElement,
315+
);
316+
await waitUntil(() => document.querySelector(".dp__overlay") !== null);
317+
await userEvent.click(document.querySelector('[data-test-id="2025"]') as HTMLElement);
318+
await waitUntil(() => !document.querySelector(".dp__overlay"));
319+
320+
// Set the day
321+
await waitUntil(() => document.getElementById("2025-03-15") !== null);
322+
const dayElement = document.getElementById("2025-03-15") as HTMLElement;
323+
await userEvent.click(dayElement);
324+
325+
// Wait and check if menu closed
326+
await new Promise((resolve) => setTimeout(resolve, 100));
327+
await waitUntil(() => !document.querySelector(".dp__menu"));
328+
329+
// ASSERT - The handler was called with the correct date "2025-03-15T04:00:00Z"
330+
// The input is "2025-03-15, 00:00" and timeZone is "America/New_York" so the utcOffset is "+04:00"
331+
expect(handler).toHaveBeenLastCalledWith("2025-03-15T04:00:00.000Z");
332+
});
333+
334+
it("should emit iso string with the correct time", async () => {
335+
const handler = vi.fn();
336+
// ARRANGE
337+
render(MtDatepicker, {
338+
props: {
339+
dateType: "datetime",
340+
"onUpdate:modelValue": handler,
341+
},
342+
});
343+
344+
// ACT - select a datetime
345+
await userEvent.click(screen.getByRole("textbox"));
346+
await waitUntil(() => document.getElementsByClassName("dp__menu").length > 0);
347+
await waitUntil(() => document.getElementsByClassName("dp__time_input") !== null);
348+
349+
// Set the hours
350+
await userEvent.click(
351+
document.querySelector('[data-test-id="hours-toggle-overlay-btn-0"]') as HTMLElement,
352+
);
353+
await waitUntil(() => document.querySelector(".dp__overlay") !== null);
354+
await userEvent.click(document.querySelector('[data-test-id="08"]') as HTMLElement);
355+
await waitUntil(() => !document.querySelector(".dp__overlay"));
356+
await waitUntil(() =>
357+
expect(
358+
document.querySelector('[data-test-id="hours-toggle-overlay-btn-0"]'),
359+
).toHaveTextContent("08"),
360+
);
361+
362+
// Set the minutes
363+
await userEvent.click(
364+
document.querySelector('[data-test-id="minutes-toggle-overlay-btn-0"]') as HTMLElement,
365+
);
366+
await waitUntil(() => document.querySelector(".dp__overlay") !== null);
367+
await userEvent.click(document.querySelector('[data-test-id="05"]') as HTMLElement);
368+
await waitUntil(() => !document.querySelector(".dp__overlay"));
369+
expect(
370+
document.querySelector('[data-test-id="minutes-toggle-overlay-btn-0"]'),
371+
).toHaveTextContent("05");
372+
373+
// Set the month
374+
await userEvent.click(
375+
document.querySelector('[data-test-id="month-toggle-overlay-0"]') as HTMLElement,
376+
);
377+
await waitUntil(() => document.querySelector(".dp__overlay") !== null);
378+
await userEvent.click(document.querySelector('[data-test-id="Jan"]') as HTMLElement);
379+
await waitUntil(() => !document.querySelector(".dp__overlay"));
380+
expect(document.querySelector('[data-test-id="month-toggle-overlay-0"]')).toHaveTextContent(
381+
"Jan",
382+
);
383+
384+
// Set the year
385+
await userEvent.click(
386+
document.querySelector('[data-test-id="year-toggle-overlay-0"]') as HTMLElement,
387+
);
388+
await waitUntil(() => document.querySelector(".dp__overlay") !== null);
389+
await userEvent.click(document.querySelector('[data-test-id="2025"]') as HTMLElement);
390+
await waitUntil(() => !document.querySelector(".dp__overlay"));
391+
392+
// Set the day
393+
await waitUntil(() => document.getElementById("2025-01-01") !== null);
394+
const dayElement = document.getElementById("2025-01-01") as HTMLElement;
395+
await userEvent.click(dayElement);
396+
397+
// Wait and check if menu closed
398+
await new Promise((resolve) => setTimeout(resolve, 100));
399+
await waitUntil(() => !document.querySelector(".dp__menu"));
400+
401+
// ASSERT - The handler was called with the correct date
402+
// The input is "2025-01-01, 08:05" so the output should be "2025-01-01T08:05:00.000Z"
403+
expect(handler).toHaveBeenLastCalledWith("2025-01-01T08:05:00.000Z");
404+
});
405+
406+
it("should increment the hours overlay by the prop value", async () => {
407+
const incrementValue = 2;
408+
// ARRANGE
409+
render(MtDatepicker, {
410+
props: {
411+
dateType: "datetime",
412+
modelValue: "2023-07-18T09:15:00.000Z",
413+
hourIncrement: incrementValue,
414+
},
415+
});
416+
417+
// ACT - select a datetime
418+
await userEvent.click(screen.getByRole("textbox"));
419+
await waitUntil(() => document.getElementsByClassName("dp__menu").length > 0);
420+
await waitUntil(() => document.getElementsByClassName("dp__time_input") !== null);
421+
422+
// Set the hours
423+
await userEvent.click(
424+
document.querySelector('[data-test-id="hours-toggle-overlay-btn-0"]') as HTMLElement,
425+
);
426+
await waitUntil(() => document.querySelector(".dp__overlay") !== null);
427+
428+
// Get an array of the overlay cell values
429+
const overlayCells = Array.from(document.getElementsByClassName("dp__overlay_cell")).map((el) =>
430+
parseInt(el.textContent || "0"),
431+
);
432+
433+
// Check each value difference
434+
const allDifferencesMatch = overlayCells
435+
.slice(1)
436+
.every((value, index) => value - overlayCells[index] === incrementValue);
437+
438+
// ASSERT - The differences between each consecutive pair should match the increment value
439+
expect(allDifferencesMatch).toBe(true);
440+
});
441+
442+
it("should increment the minutes overlay by the prop value", async () => {
443+
const incrementValue = 4;
444+
// ARRANGE
445+
render(MtDatepicker, {
446+
props: {
447+
dateType: "datetime",
448+
modelValue: "2023-07-18T09:15:00.000Z",
449+
minuteIncrement: incrementValue,
450+
},
451+
});
452+
453+
// ACT - select a datetime
454+
await userEvent.click(screen.getByRole("textbox"));
455+
await waitUntil(() => document.getElementsByClassName("dp__menu").length > 0);
456+
await waitUntil(() => document.getElementsByClassName("dp__time_input") !== null);
457+
458+
// Set the minutes
459+
await userEvent.click(
460+
document.querySelector('[data-test-id="minutes-toggle-overlay-btn-0"]') as HTMLElement,
461+
);
462+
await waitUntil(() => document.querySelector(".dp__overlay") !== null);
463+
464+
// Get an array of the overlay cell values
465+
const overlayCells = Array.from(document.getElementsByClassName("dp__overlay_cell")).map((el) =>
466+
parseInt(el.textContent || "0"),
467+
);
468+
469+
// Check each value difference
470+
const allDifferencesMatch = overlayCells
471+
.slice(1)
472+
.every((value, index) => value - overlayCells[index] === incrementValue);
473+
474+
// ASSERT - The differences between each consecutive pair should match the increment value
475+
expect(allDifferencesMatch).toBe(true);
476+
});
161477
});

0 commit comments

Comments
 (0)