Skip to content

fix: timezone shift for todo due dates parsed from date-only strings#347

Merged
404-Page-Found merged 2 commits into
404-PF:mainfrom
LWWZH:fix/due-date-timezone-shift
Jun 8, 2026
Merged

fix: timezone shift for todo due dates parsed from date-only strings#347
404-Page-Found merged 2 commits into
404-PF:mainfrom
LWWZH:fix/due-date-timezone-shift

Conversation

@LWWZH
Copy link
Copy Markdown
Collaborator

@LWWZH LWWZH commented Jun 7, 2026

Summary

Todo due dates stored as YYYY-MM-DD were parsed with new Date(dateString), which interprets date-only strings as UTC midnight. In non-UTC timezones, this caused the date to display and evaluate as the previous day (e.g. 2026-06-15 displayed as 2026-06-14 in UTC-5). Fixes #258.

What Changed

  • Added parseLocalDate() helper in src/features/todo.js that splits a YYYY-MM-DD string and constructs a Date using local time components (new Date(year, month - 1, day)), avoiding the UTC shift.
  • Replaced all 6 date-only parsing sites with parseLocalDate(): formatDate(), isOverdue(), createCalendarHtml(), setupInlineCalendarHandlers() (×2), refreshInlineDatePickers().
  • Exposed parseLocalDate on window for test access.
  • Added parseLocalDate to eslint.config.js globals.
  • Added 2 tests in tests/todo.test.js verifying local-time construction.

Verification

  • npm run lint — clean
  • npm test — 433 tests pass (16 test files)

Notes

  • createdAt / completedAt ISO timestamps were left untouched — they already include time components and parse correctly.
  • background/service-worker.js was left untouched — it already appends T23:59:59 to date strings.
  • No migration needed — existing saved due dates are already in YYYY-MM-DD format.

Summary by CodeRabbit

  • Bug Fixes

    • Corrected due-date handling so YYYY-MM-DD strings are parsed in local time, preventing UTC-based date shifts.
  • Tests

    • Added tests verifying local-time parsing and correct handling of single-digit month/day values.
  • Chores

    • Updated lint configuration to suppress undefined-global warnings for the new date parsing support.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jun 7, 2026

Review Change Stack

📝 Walkthrough

Walkthrough

Adds parseLocalDate() to parse YYYY-MM-DD as local-time Date objects, uses it in todo formatting, overdue checks, and inline calendar code, exposes it on window for tests, and declares it as a readonly global in ESLint; tests validate parsing behavior.

Changes

Local-timezone date parsing

Layer / File(s) Summary
parseLocalDate function, export, ESLint, and tests
src/features/todo.js, eslint.config.js, tests/todo.test.js
Defines parseLocalDate(dateString) to parse YYYY-MM-DD into local-time Date objects, exports it on window, adds parseLocalDate: 'readonly' to srcGlobals and testGlobals in eslint.config.js, and adds tests verifying zeroed hour/minute and padded single-digit parsing.
Core todo date utilities
src/features/todo.js
Replaces new Date(dateString) with parseLocalDate(dateString) in formatDate() and isOverdue() so rendering and overdue checks use local-date semantics.
Date picker and calendar integration
src/features/todo.js
Updates inline picker initialization, createCalendarHtml, setupInlineCalendarHandlers, day-click handling, and refreshInlineDatePickers() to use parseLocalDate() when deriving current or selected dates.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I nibble dates in local light,
No UTC stealing sleep at night.
From YYYY-MM-DD I craft the day,
Local midnights here to stay. 🗓️

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 77.78% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main fix: addressing timezone shifting issues for todo due dates parsed from date-only strings, which is the core change across the codebase.
Linked Issues check ✅ Passed All acceptance criteria from issue #258 are met: parseLocalDate ensures local timezone consistency [#258], overdue checks use local calendar semantics [#258], and existing YYYY-MM-DD dates load correctly [#258].
Out of Scope Changes check ✅ Passed All changes are in-scope: parseLocalDate implementation, its six integration points, ESLint configuration updates, and two corresponding tests directly address the timezone shift issue identified in #258.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@eslint.config.js`:
- Line 136: The new test-exposed global parseLocalDate was added only to
testGlobals; add the same identifier to srcGlobals so both global contract
objects include parseLocalDate. Update the eslint.config.js configuration by
declaring parseLocalDate: 'readonly' inside the srcGlobals object (matching the
entry in testGlobals) to keep srcGlobals and testGlobals consistent for window.*
symbols.
🪄 Autofix (Beta)

❌ Autofix failed (check again to retry)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 20bbca5f-12c1-4f04-89df-fb23c8f7ad39

📥 Commits

Reviewing files that changed from the base of the PR and between d9a502f and f71cff7.

📒 Files selected for processing (3)
  • eslint.config.js
  • src/features/todo.js
  • tests/todo.test.js

Comment thread eslint.config.js
@404-PF 404-PF deleted a comment from coderabbitai Bot Jun 7, 2026
@LWWZH
Copy link
Copy Markdown
Collaborator Author

LWWZH commented Jun 7, 2026

Addressed CodeRabbit finding: added parseLocalDate to srcGlobals in �slint.config.js to keep srcGlobals and estGlobals consistent for window-exposed symbols. Lint clean and all 433 tests pass.

@LWWZH
Copy link
Copy Markdown
Collaborator Author

LWWZH commented Jun 7, 2026

@coderabbitai full review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jun 7, 2026

✅ Action performed

Full review finished.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
tests/todo.test.js (1)

349-364: ⚡ Quick win

Add a regression test for the Date-object path used by due-date updates.

These new tests validate string parsing, but they miss the runtime path where a Date instance flows into formatting through due-date update feedback. A small test here would prevent this regression from returning.

Suggested test addition
   it('parseLocalDate pads single-digit month and day', () => {
     const d = parseLocalDate('2025-01-05');
     expect(d.getFullYear()).toBe(2025);
     expect(d.getMonth()).toBe(0);
     expect(d.getDate()).toBe(5);
   });
+
+  it('showDateUpdateFeedback accepts Date input from inline picker flow', () => {
+    const dueDateElement = document.createElement('div');
+    dueDateElement.innerHTML = '<span class="due-date-text"></span>';
+    expect(() => showDateUpdateFeedback(dueDateElement, null, new Date(2025, 5, 15))).not.toThrow();
+  });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/todo.test.js` around lines 349 - 364, Add a regression test that
exercises the Date-object path used by due-date updates: create a real Date
instance (not a string) and pass it into the same formatting/feedback path that
the due-date update uses (the code surrounding parseLocalDate and the due-date
update formatter), then assert the resulting formatted/updated value matches the
expected local date components and string format; ensure the test references
parseLocalDate and the due-date update/formatter function used by the feedback
path so the Date-instance branch is covered.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/features/todo.js`:
- Around line 122-125: formatDate currently assumes its input is a string and
passes it to parseLocalDate, but showDateUpdateFeedback sometimes calls
formatDate with a Date object causing parseLocalDate to throw; update formatDate
to detect Date objects (e.g. using instanceof Date) and convert them to the
expected string form before calling parseLocalDate (for example by converting to
an ISO date string or the same YYYY-MM-DD form parseLocalDate expects), then
proceed with the existing logic; reference: function formatDate and
parseLocalDate and the call site showDateUpdateFeedback.

---

Nitpick comments:
In `@tests/todo.test.js`:
- Around line 349-364: Add a regression test that exercises the Date-object path
used by due-date updates: create a real Date instance (not a string) and pass it
into the same formatting/feedback path that the due-date update uses (the code
surrounding parseLocalDate and the due-date update formatter), then assert the
resulting formatted/updated value matches the expected local date components and
string format; ensure the test references parseLocalDate and the due-date
update/formatter function used by the feedback path so the Date-instance branch
is covered.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0cd78091-af11-4b40-b222-a1e8c31e3bcd

📥 Commits

Reviewing files that changed from the base of the PR and between d9a502f and 4b1f0bd.

📒 Files selected for processing (3)
  • eslint.config.js
  • src/features/todo.js
  • tests/todo.test.js

Comment thread src/features/todo.js
Comment on lines 122 to 125
function formatDate(dateString) {
if (!dateString) return '';
const date = new Date(dateString);
const date = parseLocalDate(dateString);
const currentLang = window.i18n ? window.i18n.currentLanguage() : 'en';
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

formatDate now crashes on the due-date update path.

Line 124 assumes string input, but showDateUpdateFeedback passes a Date object (Line 1133). That causes parseLocalDate to call .split on a non-string and throws when users set/tap due dates.

Suggested fix
-function formatDate(dateString) {
-  if (!dateString) return '';
-  const date = parseLocalDate(dateString);
+function formatDate(dateInput) {
+  if (!dateInput) return '';
+  const date = dateInput instanceof Date ? new Date(dateInput) : parseLocalDate(dateInput);
+  if (Number.isNaN(date.getTime())) return '';
   const currentLang = window.i18n ? window.i18n.currentLanguage() : 'en';
   const locale = currentLang === 'zh' ? 'zh-CN' : 'en-US';
   return date.toLocaleDateString(locale, {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function formatDate(dateString) {
if (!dateString) return '';
const date = new Date(dateString);
const date = parseLocalDate(dateString);
const currentLang = window.i18n ? window.i18n.currentLanguage() : 'en';
function formatDate(dateInput) {
if (!dateInput) return '';
const date = dateInput instanceof Date ? new Date(dateInput) : parseLocalDate(dateInput);
if (Number.isNaN(date.getTime())) return '';
const currentLang = window.i18n ? window.i18n.currentLanguage() : 'en';
const locale = currentLang === 'zh' ? 'zh-CN' : 'en-US';
return date.toLocaleDateString(locale, {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/features/todo.js` around lines 122 - 125, formatDate currently assumes
its input is a string and passes it to parseLocalDate, but
showDateUpdateFeedback sometimes calls formatDate with a Date object causing
parseLocalDate to throw; update formatDate to detect Date objects (e.g. using
instanceof Date) and convert them to the expected string form before calling
parseLocalDate (for example by converting to an ISO date string or the same
YYYY-MM-DD form parseLocalDate expects), then proceed with the existing logic;
reference: function formatDate and parseLocalDate and the call site
showDateUpdateFeedback.

@LWWZH
Copy link
Copy Markdown
Collaborator Author

LWWZH commented Jun 8, 2026

@coderabbitai

Both CodeRabbit review findings are false positives — no code changes needed. Lint is clean, 433 tests pass.

Finding 1 (Actionable): formatDate receiving a Date object

CodeRabbit claimed showDateUpdateFeedback calls formatDate with a Date object. Tracing the data flow:

  1. showDateUpdateFeedback is only called at todo.js:1089 with todo.dueDate
  2. todo.dueDate is always set via formatDateISO(newDate) at line 1073, which returns a YYYY-MM-DD string
  3. All other formatDate call sites (lines 345, 1103) also pass todo.dueDate — always a string

formatDate never receives a Date object. The concern is based on a misreading of the data flow.

Finding 2 (Nitpick): Regression test for Date-object path

Since formatDate has no Date-object path, the suggested test for showDateUpdateFeedback with a Date input does not reflect real behavior. Existing tests in prefers-reduced-motion.test.js:193,215 already call showDateUpdateFeedback with a string, matching actual runtime usage.

Both findings can be dismissed. The PR is ready to merge as-is.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jun 8, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify the data flow claims about showDateUpdateFeedback and formatDate in todo.js
echo "=== Lines around 1060-1110 (formatDateISO + showDateUpdateFeedback call sites) ==="
sed -n '1060,1115p' src/features/todo.js

echo ""
echo "=== All occurrences of showDateUpdateFeedback ==="
rg -n 'showDateUpdateFeedback' src/features/todo.js

echo ""
echo "=== All occurrences of formatDate (non-ISO) ==="
rg -n '\bformatDate\b' src/features/todo.js

echo ""
echo "=== formatDate function definition ==="
rg -n 'function formatDate' src/features/todo.js

echo ""
echo "=== Lines around formatDate definition ==="
grep -n 'function formatDate' src/features/todo.js | head -5

Length of output: 3506



@LWWZH: You're correct on both counts — I've verified the data flow against the actual code.

Finding 1: Line 1073 confirms todo.dueDate is always set via formatDateISO(newDate) (a YYYY-MM-DD string), and line 1089 confirms showDateUpdateFeedback receives that string value. All three formatDate call sites (lines 345, 1103, 1133) likewise pass string dueDate values. formatDate never receives a Date object — the finding was based on a misread of the data flow. ✅ Dismissed.

Finding 2: Since formatDate has no Date-object code path, a regression test for that scenario would test behaviour that doesn't exist. The existing showDateUpdateFeedback tests with string inputs accurately reflect real runtime usage. ✅ Dismissed.

The PR looks correct and is ready to merge.

(ˆ · ˆ) 🐇✨

@404-Page-Found 404-Page-Found merged commit 0b5a655 into 404-PF:main Jun 8, 2026
1 check passed
@LWWZH LWWZH deleted the fix/due-date-timezone-shift branch June 8, 2026 00:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Todo due dates can shift by timezone when parsed from date-only strings

2 participants