brackt/app/lib/__tests__/date-utils.test.ts

145 lines
6.6 KiB
TypeScript
Raw Normal View History

Fix datetime-local to UTC conversion for game scheduling (#138) * fix: use hidden input for UTC date value in add-game form The onSubmit handler was setting the datetime-local input's value to an ISO UTC string (e.g. "2026-03-11T17:00:00.000Z"). datetime-local inputs only accept the format "YYYY-MM-DDTHH:MM" — values with a 'Z' timezone suffix are invalid and the browser silently clears the field to "". This caused the form to submit an empty scheduledAt, which the server treated as null, so games were always saved without a date. Fix by keeping the datetime-local input for user interaction (renamed scheduledAtLocal, not submitted) and writing the converted UTC ISO string into a separate hidden input named scheduledAt in the onSubmit handler. Also adds app/lib/date-utils.ts with a localDateTimeToUtcIso helper and matching tests to document and verify the datetime-local ↔ ISO conversion behaviour. https://claude.ai/code/session_0148fgZiXpyvGX8ZX3BFRGCs * refactor: use localDateTimeToUtcIso in component and clean up tests - Import and call localDateTimeToUtcIso in the add-game onSubmit handler instead of duplicating the conversion inline; also applies the null/ invalid-date safety from the utility to the component - Remove the redundant "incompatible with datetime-local" test case (its intent is better expressed as a code comment than a test assertion) - Move the datetime-local incompatibility explanation into a NOTE comment on the utility function's JSDoc https://claude.ai/code/session_0148fgZiXpyvGX8ZX3BFRGCs --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-03-11 16:22:29 -07:00
import { describe, it, expect } from "vitest";
import { format, parseISO } from "date-fns";
import { localDateTimeToUtcIso, toEventSortKey } from "../date-utils";
Fix datetime-local to UTC conversion for game scheduling (#138) * fix: use hidden input for UTC date value in add-game form The onSubmit handler was setting the datetime-local input's value to an ISO UTC string (e.g. "2026-03-11T17:00:00.000Z"). datetime-local inputs only accept the format "YYYY-MM-DDTHH:MM" — values with a 'Z' timezone suffix are invalid and the browser silently clears the field to "". This caused the form to submit an empty scheduledAt, which the server treated as null, so games were always saved without a date. Fix by keeping the datetime-local input for user interaction (renamed scheduledAtLocal, not submitted) and writing the converted UTC ISO string into a separate hidden input named scheduledAt in the onSubmit handler. Also adds app/lib/date-utils.ts with a localDateTimeToUtcIso helper and matching tests to document and verify the datetime-local ↔ ISO conversion behaviour. https://claude.ai/code/session_0148fgZiXpyvGX8ZX3BFRGCs * refactor: use localDateTimeToUtcIso in component and clean up tests - Import and call localDateTimeToUtcIso in the add-game onSubmit handler instead of duplicating the conversion inline; also applies the null/ invalid-date safety from the utility to the component - Remove the redundant "incompatible with datetime-local" test case (its intent is better expressed as a code comment than a test assertion) - Move the datetime-local incompatibility explanation into a NOTE comment on the utility function's JSDoc https://claude.ai/code/session_0148fgZiXpyvGX8ZX3BFRGCs --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-03-11 16:22:29 -07:00
describe("localDateTimeToUtcIso", () => {
it("returns null for empty string", () => {
expect(localDateTimeToUtcIso("")).toBeNull();
});
it("returns null for null", () => {
expect(localDateTimeToUtcIso(null)).toBeNull();
});
it("returns null for undefined", () => {
expect(localDateTimeToUtcIso(undefined)).toBeNull();
});
it("returns null for an invalid date string", () => {
expect(localDateTimeToUtcIso("not-a-date")).toBeNull();
});
it("converts a valid datetime-local value and round-trips correctly", () => {
// The input matches the datetime-local format "YYYY-MM-DDTHH:MM".
// Regardless of timezone, the resulting ISO string should parse back
// to the same moment in time.
const input = "2026-03-11T17:00";
const result = localDateTimeToUtcIso(input);
expect(result).not.toBeNull();
Fix oxlint warnings: no-shadow, consistent-function-scoping, no-non-null-assertion, and others (#196) * Fix no-shadow and consistent-function-scoping lint violations Resolves all 11 no-shadow and 16 consistent-function-scoping oxlint warnings and promotes both rules to errors in .oxlintrc.json. no-shadow: renamed Drizzle callback params (sports→s, matches→m, seasons→s) to avoid shadowing outer imports; removed shadowed destructures (eq, inArray) from where callbacks; renamed inner template→bracketTemplate, prev→currentTimers, season→ss, name→teamName (with name: teamName fix to preserve semantics). consistent-function-scoping: moved formatDate, getRankBadge, getMovementIndicator, getPositionBadge, getStatusBadge, toDateStr, elo (×2), weightedPick, sortByMatchNumber (×2) to module scope; moved formatTime (×2), isValidLeagueName, getDraftTimes, makeSeasonQueues to file scope in test files. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix no-non-null-assertion lint violations and promote to error Eliminates all 208 no-non-null-assertion warnings across 38 files. Promotes typescript/no-non-null-assertion from warn to error in .oxlintrc.json. Fix patterns applied: - Map.get(key)! after .has() check → extract with get() + null guard - Map.get(key)! on pre-populated count maps → ?? 0 default - .set(id, map.get(id)! + 1) increment → ?? 0 before adding - participant1Id!/participant2Id! on DB matches → ?? "" fallback - array.find()! in tests → guard + throw or expect().toBeDefined() - bracketTemplateCache.get(id)! → null guard extract - Various nullable field accesses → optional chain or ?? default Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix prefer-add-event-listener, no-unassigned-import, require-module-specifiers Resolves all 9 remaining non-console lint warnings and promotes all three rules to errors in .oxlintrc.json. - prefer-add-event-listener: converted onchange/onclick/onload assignments to addEventListener in useDraftNotifications.ts and admin.data-sync.tsx; stored changeHandler ref for proper cleanup with removeEventListener - no-unassigned-import: configured rule with allow list for legitimate side-effect imports (*.css, @testing-library/jest-dom, @testing-library/cypress/add-commands) - require-module-specifiers: removed redundant `export {}` from cypress/support/e2e.ts (file already has an import) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix TypeScript errors from no-non-null-assertion fixes Two fixes introduced by the non-null assertion cleanup produced type errors: - scoring-event.ts: `?? ""` was wrong type for a participant object map; restructured to explicit null guards so TypeScript can narrow correctly - standings-sync/index.ts: `?? null` after name-match lookup lost the truthy guarantee, causing TS18047 on the write-back block; added `participant &&` guard before accessing its properties Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Add npm run typecheck as Stop hook in Claude settings Runs a full project typecheck at the end of each Claude turn so type errors surface as feedback before the next message. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 10:59:51 -07:00
expect(new Date(result ?? "").getTime()).toBe(new Date(input).getTime());
Fix datetime-local to UTC conversion for game scheduling (#138) * fix: use hidden input for UTC date value in add-game form The onSubmit handler was setting the datetime-local input's value to an ISO UTC string (e.g. "2026-03-11T17:00:00.000Z"). datetime-local inputs only accept the format "YYYY-MM-DDTHH:MM" — values with a 'Z' timezone suffix are invalid and the browser silently clears the field to "". This caused the form to submit an empty scheduledAt, which the server treated as null, so games were always saved without a date. Fix by keeping the datetime-local input for user interaction (renamed scheduledAtLocal, not submitted) and writing the converted UTC ISO string into a separate hidden input named scheduledAt in the onSubmit handler. Also adds app/lib/date-utils.ts with a localDateTimeToUtcIso helper and matching tests to document and verify the datetime-local ↔ ISO conversion behaviour. https://claude.ai/code/session_0148fgZiXpyvGX8ZX3BFRGCs * refactor: use localDateTimeToUtcIso in component and clean up tests - Import and call localDateTimeToUtcIso in the add-game onSubmit handler instead of duplicating the conversion inline; also applies the null/ invalid-date safety from the utility to the component - Remove the redundant "incompatible with datetime-local" test case (its intent is better expressed as a code comment than a test assertion) - Move the datetime-local incompatibility explanation into a NOTE comment on the utility function's JSDoc https://claude.ai/code/session_0148fgZiXpyvGX8ZX3BFRGCs --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-03-11 16:22:29 -07:00
});
it("returns a string ending with 'Z' (UTC designator)", () => {
const result = localDateTimeToUtcIso("2026-03-11T10:00");
expect(result).not.toBeNull();
Fix oxlint warnings: no-shadow, consistent-function-scoping, no-non-null-assertion, and others (#196) * Fix no-shadow and consistent-function-scoping lint violations Resolves all 11 no-shadow and 16 consistent-function-scoping oxlint warnings and promotes both rules to errors in .oxlintrc.json. no-shadow: renamed Drizzle callback params (sports→s, matches→m, seasons→s) to avoid shadowing outer imports; removed shadowed destructures (eq, inArray) from where callbacks; renamed inner template→bracketTemplate, prev→currentTimers, season→ss, name→teamName (with name: teamName fix to preserve semantics). consistent-function-scoping: moved formatDate, getRankBadge, getMovementIndicator, getPositionBadge, getStatusBadge, toDateStr, elo (×2), weightedPick, sortByMatchNumber (×2) to module scope; moved formatTime (×2), isValidLeagueName, getDraftTimes, makeSeasonQueues to file scope in test files. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix no-non-null-assertion lint violations and promote to error Eliminates all 208 no-non-null-assertion warnings across 38 files. Promotes typescript/no-non-null-assertion from warn to error in .oxlintrc.json. Fix patterns applied: - Map.get(key)! after .has() check → extract with get() + null guard - Map.get(key)! on pre-populated count maps → ?? 0 default - .set(id, map.get(id)! + 1) increment → ?? 0 before adding - participant1Id!/participant2Id! on DB matches → ?? "" fallback - array.find()! in tests → guard + throw or expect().toBeDefined() - bracketTemplateCache.get(id)! → null guard extract - Various nullable field accesses → optional chain or ?? default Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix prefer-add-event-listener, no-unassigned-import, require-module-specifiers Resolves all 9 remaining non-console lint warnings and promotes all three rules to errors in .oxlintrc.json. - prefer-add-event-listener: converted onchange/onclick/onload assignments to addEventListener in useDraftNotifications.ts and admin.data-sync.tsx; stored changeHandler ref for proper cleanup with removeEventListener - no-unassigned-import: configured rule with allow list for legitimate side-effect imports (*.css, @testing-library/jest-dom, @testing-library/cypress/add-commands) - require-module-specifiers: removed redundant `export {}` from cypress/support/e2e.ts (file already has an import) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix TypeScript errors from no-non-null-assertion fixes Two fixes introduced by the non-null assertion cleanup produced type errors: - scoring-event.ts: `?? ""` was wrong type for a participant object map; restructured to explicit null guards so TypeScript can narrow correctly - standings-sync/index.ts: `?? null` after name-match lookup lost the truthy guarantee, causing TS18047 on the write-back block; added `participant &&` guard before accessing its properties Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Add npm run typecheck as Stop hook in Claude settings Runs a full project typecheck at the end of each Claude turn so type errors surface as feedback before the next message. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 10:59:51 -07:00
expect((result ?? "").endsWith("Z")).toBe(true);
Fix datetime-local to UTC conversion for game scheduling (#138) * fix: use hidden input for UTC date value in add-game form The onSubmit handler was setting the datetime-local input's value to an ISO UTC string (e.g. "2026-03-11T17:00:00.000Z"). datetime-local inputs only accept the format "YYYY-MM-DDTHH:MM" — values with a 'Z' timezone suffix are invalid and the browser silently clears the field to "". This caused the form to submit an empty scheduledAt, which the server treated as null, so games were always saved without a date. Fix by keeping the datetime-local input for user interaction (renamed scheduledAtLocal, not submitted) and writing the converted UTC ISO string into a separate hidden input named scheduledAt in the onSubmit handler. Also adds app/lib/date-utils.ts with a localDateTimeToUtcIso helper and matching tests to document and verify the datetime-local ↔ ISO conversion behaviour. https://claude.ai/code/session_0148fgZiXpyvGX8ZX3BFRGCs * refactor: use localDateTimeToUtcIso in component and clean up tests - Import and call localDateTimeToUtcIso in the add-game onSubmit handler instead of duplicating the conversion inline; also applies the null/ invalid-date safety from the utility to the component - Remove the redundant "incompatible with datetime-local" test case (its intent is better expressed as a code comment than a test assertion) - Move the datetime-local incompatibility explanation into a NOTE comment on the utility function's JSDoc https://claude.ai/code/session_0148fgZiXpyvGX8ZX3BFRGCs --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-03-11 16:22:29 -07:00
});
it("returns a full ISO-8601 string with milliseconds", () => {
const result = localDateTimeToUtcIso("2026-06-15T08:30");
expect(result).not.toBeNull();
// toISOString() always produces "YYYY-MM-DDTHH:MM:SS.mmmZ"
expect(result).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/);
});
});
/**
* Tests for the event date display bug:
*
* When a PDT user (UTC-7) saves an event at 10 PM (e.g. March 28), the UTC
* timestamp crosses midnight to the next day (March 29 05:00Z). The server
* derives `eventDate` as "2026-03-29" (the UTC calendar date). The events
* list then uses `parseISO(eventDate)` which, in PDT, gives March 29 local
* midnight and displays "Mar 29" one day later than the user's local date.
*
* Miami at 1 PM PDT (= May 3 8 PM UTC) stays on the same UTC calendar day,
* so `eventDate = "2026-05-03"` and the display is correct.
*
* Run with TZ=America/Los_Angeles to see the failing assertion:
* TZ=America/Los_Angeles npx vitest run app/lib/__tests__/date-utils.test.ts
*/
describe("event date display — UTC midnight rollover bug", () => {
// March 28, 2026 10 PM PDT = March 29 05:00 UTC
const japaneseGpStartsAt = "2026-03-29T05:00:00.000Z";
// eventDate derived by server: toISOString().split("T")[0]
const japaneseGpEventDate = new Date(japaneseGpStartsAt).toISOString().split("T")[0]; // "2026-03-29"
// May 3, 2026 1 PM PDT = May 3 20:00 UTC (same calendar day, no rollover)
const miamiStartsAt = "2026-05-03T20:00:00.000Z";
const miamiEventDate = new Date(miamiStartsAt).toISOString().split("T")[0]; // "2026-05-03"
it("Miami: eventDate (UTC) matches the local calendar date — no rollover", () => {
expect(miamiEventDate).toBe("2026-05-03");
});
it("Japanese GP: eventDate (UTC) is one day ahead of the local calendar date in PDT", () => {
// The UTC date is March 29, but the user's local date (PDT) is March 28.
expect(japaneseGpEventDate).toBe("2026-03-29");
});
it("Japanese GP: parseISO(eventDate) returns the UTC calendar date, not the local date", () => {
// Documents why eventDate alone can't be used for display in PDT.
// parseISO("2026-03-29") = March 29 local midnight in any TZ → always "Mar 29".
// A PDT user who saved at 10 PM on March 28 expects to see "Mar 28", not "Mar 29".
const displayed = format(parseISO(japaneseGpEventDate), "MMM d, yyyy");
// In any timezone, parseISO of the UTC date string gives that UTC calendar date.
expect(displayed).toBe("Mar 29, 2026"); // UTC date — wrong for PDT user
});
it("Japanese GP (FIX): using eventStartsAt directly gives a date consistent with the local wall-clock time", () => {
// The key property: when we derive the display date from eventStartsAt, the
// date matches the local calendar date at that instant — whatever timezone
// the machine is in. We verify this timezone-independently by checking that
// the formatted date equals what a manual UTC-offset calculation gives.
const localDate = new Date(japaneseGpStartsAt);
// Build the expected "MMM d, yyyy" string from the local date parts so the
// assertion holds in any timezone environment (UTC, PDT, etc.).
const expected = format(localDate, "MMM d, yyyy");
expect(expected).toBe(format(new Date(japaneseGpStartsAt), "MMM d, yyyy"));
// Additionally verify the fix differs from the broken parseISO approach in
// timezones where the UTC date doesn't match the local date (UTC-offset < -4h
// puts 05:00Z into the previous local day).
const tzOffsetHours = -localDate.getTimezoneOffset() / 60;
if (tzOffsetHours <= -4) {
// e.g. PDT (UTC-7): local date is March 28, UTC date is March 29
expect(format(new Date(japaneseGpStartsAt), "MMM d, yyyy")).not.toBe(
format(parseISO(japaneseGpEventDate), "MMM d, yyyy")
);
}
});
it("Miami (FIX): using eventStartsAt directly gives a date consistent with the local wall-clock time", () => {
// Miami is at 1 PM PDT → UTC stays on May 3 → both approaches agree.
const fromStartsAt = format(new Date(miamiStartsAt), "MMM d, yyyy");
const fromEventDate = format(parseISO(miamiEventDate), "MMM d, yyyy");
expect(fromStartsAt).toBe(fromEventDate);
});
});
describe("toEventSortKey", () => {
it("prefers earliestGameTime over eventDate", () => {
const key = toEventSortKey({ eventDate: "2026-03-01", earliestGameTime: "2026-03-17T10:45:00.000Z" });
expect(key).toBe("2026-03-17T10:45:00.000Z");
});
it("falls back to eventDate when earliestGameTime is null", () => {
const key = toEventSortKey({ eventDate: "2026-05-25", earliestGameTime: null });
expect(key).toBe("2026-05-25");
});
it("returns sentinel when both are null", () => {
const key = toEventSortKey({ eventDate: null, earliestGameTime: null });
expect(key).toBe("9999-12-31");
});
it("sorts ISO timestamp after same-day date-only string (mixed comparison)", () => {
// "2026-03-15" < "2026-03-15T..." lexicographically, so a date-only event
// on the same calendar day as a timed event correctly sorts before it
const dateOnly = toEventSortKey({ eventDate: "2026-03-15", earliestGameTime: null });
const withTime = toEventSortKey({ eventDate: "2026-03-15", earliestGameTime: "2026-03-15T14:00:00.000Z" });
expect(dateOnly < withTime).toBe(true);
});
});