mirror of
https://github.com/callumalpass/tasknotes.git
synced 2026-07-22 12:50:26 +00:00
fix: mini calendar off-by-one date display in negative UTC timezones (#822)
Fixes issue where mini calendar tooltips and month displays showed incorrect dates for users in negative UTC offset timezones (e.g., Pacific, Eastern US). Root cause: UTC-anchored dates were being formatted directly with date-fns format(), which interprets dates in local timezone. For UTC-7, 2025-10-02T00:00:00.000Z displays as Oct 1 at 5:00 PM. Solution: Use convertUTCToLocalCalendarDate() helper before formatting to ensure UTC components are preserved. Changes: - Import convertUTCToLocalCalendarDate in MiniCalendarView.ts - Wrap all format() calls with helper at 8 locations: - updateSelectedDate aria-label comparison - updateMonthDisplay month text - createCalendarControls month display - Calendar grid aria-label - All day aria-labels (previous/current/next month) - Remove test file incompatible with Jest date-fns mocking Closes #822
This commit is contained in:
parent
c4a8ce72a9
commit
8f40f4d174
3 changed files with 14 additions and 283 deletions
|
|
@ -97,3 +97,9 @@ Example:
|
|||
- Ensures consistent overdue detection logic between recurring and non-recurring tasks
|
||||
- Thanks to @skyrunner15 for reporting
|
||||
|
||||
- (#822) Fixed mini calendar showing incorrect dates in negative UTC offset timezones
|
||||
- Calendar tooltips and month displays now show correct dates for users in Americas/Pacific timezones
|
||||
- UTC-anchored dates are now converted to local calendar dates before formatting
|
||||
- Fixes off-by-one day error that affected date selection and display
|
||||
- Thanks to @kenhsmith for reporting
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import {
|
|||
formatDateForStorage,
|
||||
getTodayLocal,
|
||||
createUTCDateFromLocalCalendarDate,
|
||||
convertUTCToLocalCalendarDate,
|
||||
} from "../utils/dateUtils";
|
||||
import TaskNotesPlugin from "../main";
|
||||
import { getAllDailyNotes, getDailyNote } from "obsidian-daily-notes-interface";
|
||||
|
|
@ -358,7 +359,7 @@ export class MiniCalendarView extends ItemView {
|
|||
const dayEl = day as HTMLElement;
|
||||
const ariaLabel = dayEl.getAttribute("aria-label") || "";
|
||||
// Check if this element represents the new date
|
||||
if (ariaLabel.includes(format(newDate, "EEEE, MMMM d, yyyy"))) {
|
||||
if (ariaLabel.includes(format(convertUTCToLocalCalendarDate(newDate), "EEEE, MMMM d, yyyy"))) {
|
||||
dayEl.classList.add("mini-calendar-view__day--selected");
|
||||
dayEl.setAttribute("aria-selected", "true");
|
||||
dayEl.setAttribute("tabindex", "0");
|
||||
|
|
@ -375,7 +376,7 @@ export class MiniCalendarView extends ItemView {
|
|||
private updateMonthDisplay() {
|
||||
const monthDisplay = this.contentEl.querySelector(".mini-calendar-view__month-display");
|
||||
if (monthDisplay) {
|
||||
monthDisplay.textContent = format(this.plugin.selectedDate, "MMMM yyyy");
|
||||
monthDisplay.textContent = format(convertUTCToLocalCalendarDate(this.plugin.selectedDate), "MMMM yyyy");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -464,7 +465,7 @@ export class MiniCalendarView extends ItemView {
|
|||
// Current Month Display
|
||||
navSection.createDiv({
|
||||
cls: "mini-calendar-view__month-display",
|
||||
text: format(this.plugin.selectedDate, "MMMM yyyy"),
|
||||
text: format(convertUTCToLocalCalendarDate(this.plugin.selectedDate), "MMMM yyyy"),
|
||||
});
|
||||
|
||||
// Next Month Button
|
||||
|
|
@ -539,7 +540,7 @@ export class MiniCalendarView extends ItemView {
|
|||
cls: "mini-calendar-view__grid",
|
||||
attr: {
|
||||
role: "grid",
|
||||
"aria-label": `Calendar for ${format(this.plugin.selectedDate, "MMMM yyyy")}`,
|
||||
"aria-label": `Calendar for ${format(convertUTCToLocalCalendarDate(this.plugin.selectedDate), "MMMM yyyy")}`,
|
||||
id: "calendar-grid",
|
||||
},
|
||||
});
|
||||
|
|
@ -601,7 +602,7 @@ export class MiniCalendarView extends ItemView {
|
|||
attr: {
|
||||
role: "gridcell",
|
||||
tabindex: isSelected ? "0" : "-1",
|
||||
"aria-label": format(dayDate, "EEEE, MMMM d, yyyy"),
|
||||
"aria-label": format(convertUTCToLocalCalendarDate(dayDate), "EEEE, MMMM d, yyyy"),
|
||||
"aria-selected": isSelected ? "true" : "false",
|
||||
},
|
||||
});
|
||||
|
|
@ -654,7 +655,7 @@ export class MiniCalendarView extends ItemView {
|
|||
role: "gridcell",
|
||||
tabindex: isSelected ? "0" : "-1",
|
||||
"aria-label":
|
||||
format(dayDate, "EEEE, MMMM d, yyyy") + (isToday ? " (Today)" : ""),
|
||||
format(convertUTCToLocalCalendarDate(dayDate), "EEEE, MMMM d, yyyy") + (isToday ? " (Today)" : ""),
|
||||
"aria-selected": isSelected ? "true" : "false",
|
||||
"aria-current": isToday ? "date" : null,
|
||||
},
|
||||
|
|
@ -699,7 +700,7 @@ export class MiniCalendarView extends ItemView {
|
|||
attr: {
|
||||
role: "gridcell",
|
||||
tabindex: isSelected ? "0" : "-1",
|
||||
"aria-label": format(dayDate, "EEEE, MMMM d, yyyy"),
|
||||
"aria-label": format(convertUTCToLocalCalendarDate(dayDate), "EEEE, MMMM d, yyyy"),
|
||||
"aria-selected": isSelected ? "true" : "false",
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,276 +0,0 @@
|
|||
/**
|
||||
* Test for GitHub Issue #822: Mini Calendar offset by one day
|
||||
*
|
||||
* Bug Description:
|
||||
* When selecting October 2nd in the mini calendar, the tooltip shows October 1st.
|
||||
* This also links over to the Notes view - when October 2nd is selected,
|
||||
* the Notes view shows October 1st.
|
||||
*
|
||||
* Root Cause Analysis:
|
||||
* The mini calendar uses UTC-anchored dates (e.g., 2025-10-02T00:00:00.000Z)
|
||||
* but formats them using date-fns `format()` function which interprets the date
|
||||
* in the user's local timezone. In timezones behind UTC (e.g., UTC-5, UTC-7),
|
||||
* this causes the date to appear as the previous day.
|
||||
*
|
||||
* For example:
|
||||
* - UTC date: 2025-10-02T00:00:00.000Z (midnight UTC on Oct 2)
|
||||
* - In UTC-7 timezone: This is Oct 1 at 5:00 PM
|
||||
* - format(date, "EEEE, MMMM d, yyyy") returns "Wednesday, October 1, 2025"
|
||||
*
|
||||
* The fix requires using UTC-aware formatting for UTC-anchored dates.
|
||||
*/
|
||||
|
||||
import { format } from 'date-fns';
|
||||
|
||||
describe('Issue #822: Mini Calendar Tooltip Off-by-One Bug', () => {
|
||||
// Store original timezone
|
||||
const originalTZ = process.env.TZ;
|
||||
|
||||
afterEach(() => {
|
||||
// Restore original timezone
|
||||
if (originalTZ) {
|
||||
process.env.TZ = originalTZ;
|
||||
} else {
|
||||
delete process.env.TZ;
|
||||
}
|
||||
});
|
||||
|
||||
describe('Bug Reproduction', () => {
|
||||
it('should demonstrate the tooltip off-by-one bug in UTC-7 timezone (Pacific)', () => {
|
||||
// Simulate Pacific timezone (UTC-7 during PDT)
|
||||
process.env.TZ = 'America/Los_Angeles';
|
||||
|
||||
// Create a UTC-anchored date for October 2nd (as used in mini calendar)
|
||||
const utcDate = new Date(Date.UTC(2025, 9, 2)); // October 2, 2025 00:00 UTC
|
||||
|
||||
console.log('UTC Date:', utcDate.toISOString());
|
||||
console.log('Expected: October 2, 2025');
|
||||
|
||||
// This is what the current buggy code does
|
||||
const buggyTooltip = format(utcDate, 'EEEE, MMMM d, yyyy');
|
||||
console.log('Buggy tooltip:', buggyTooltip);
|
||||
|
||||
// In Pacific timezone, UTC midnight becomes the previous day
|
||||
expect(buggyTooltip).toBe('Wednesday, October 1, 2025'); // BUG!
|
||||
expect(buggyTooltip).not.toBe('Thursday, October 2, 2025'); // What it should be
|
||||
});
|
||||
|
||||
it('should demonstrate the bug in UTC-5 timezone (Eastern)', () => {
|
||||
// Simulate Eastern timezone (UTC-5 during EDT)
|
||||
process.env.TZ = 'America/New_York';
|
||||
|
||||
const utcDate = new Date(Date.UTC(2025, 9, 2)); // October 2, 2025 00:00 UTC
|
||||
|
||||
console.log('UTC Date:', utcDate.toISOString());
|
||||
console.log('Expected: October 2, 2025');
|
||||
|
||||
const buggyTooltip = format(utcDate, 'EEEE, MMMM d, yyyy');
|
||||
console.log('Buggy tooltip:', buggyTooltip);
|
||||
|
||||
// In Eastern timezone, UTC midnight is 8 PM previous day (EDT) or 7 PM (EST)
|
||||
expect(buggyTooltip).toBe('Wednesday, October 1, 2025'); // BUG!
|
||||
});
|
||||
|
||||
it('should NOT have the bug in UTC timezone', () => {
|
||||
// In UTC timezone, the bug doesn't occur
|
||||
process.env.TZ = 'UTC';
|
||||
|
||||
const utcDate = new Date(Date.UTC(2025, 9, 2));
|
||||
|
||||
const tooltip = format(utcDate, 'EEEE, MMMM d, yyyy');
|
||||
console.log('Tooltip in UTC:', tooltip);
|
||||
|
||||
// Works correctly in UTC
|
||||
expect(tooltip).toBe('Thursday, October 2, 2025');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Bug Impact on Different Calendar Days', () => {
|
||||
it('should show off-by-one error for all days in UTC-7', () => {
|
||||
process.env.TZ = 'America/Los_Angeles';
|
||||
|
||||
const testDays = [
|
||||
{ date: new Date(Date.UTC(2025, 9, 1)), expected: 'October 1', buggy: 'September 30' },
|
||||
{ date: new Date(Date.UTC(2025, 9, 2)), expected: 'October 2', buggy: 'October 1' },
|
||||
{ date: new Date(Date.UTC(2025, 9, 15)), expected: 'October 15', buggy: 'October 14' },
|
||||
{ date: new Date(Date.UTC(2025, 9, 31)), expected: 'October 31', buggy: 'October 30' },
|
||||
];
|
||||
|
||||
testDays.forEach(({ date, expected, buggy }) => {
|
||||
const buggyFormat = format(date, 'MMMM d');
|
||||
console.log(`UTC: ${date.toISOString()} | Expected: ${expected} | Buggy: ${buggyFormat}`);
|
||||
expect(buggyFormat).toBe(buggy);
|
||||
expect(buggyFormat).not.toBe(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Solution Approaches', () => {
|
||||
it('Solution 1: Convert UTC components to local date for formatting', () => {
|
||||
process.env.TZ = 'America/Los_Angeles';
|
||||
|
||||
const utcDate = new Date(Date.UTC(2025, 9, 2));
|
||||
|
||||
// Extract UTC components and create local date for formatting
|
||||
const year = utcDate.getUTCFullYear();
|
||||
const month = utcDate.getUTCMonth();
|
||||
const day = utcDate.getUTCDate();
|
||||
|
||||
// Create a local date with these components for formatting
|
||||
const localDate = new Date(year, month, day);
|
||||
const correctTooltip = format(localDate, 'EEEE, MMMM d, yyyy');
|
||||
console.log('Correct tooltip using local date with UTC components:', correctTooltip);
|
||||
|
||||
expect(correctTooltip).toBe('Thursday, October 2, 2025');
|
||||
});
|
||||
|
||||
it('Solution 2: Use helper function convertUTCToLocalCalendarDate', () => {
|
||||
process.env.TZ = 'America/Los_Angeles';
|
||||
|
||||
const utcDate = new Date(Date.UTC(2025, 9, 2));
|
||||
|
||||
// This is the recommended approach - use the existing helper from dateUtils
|
||||
function convertUTCToLocalCalendarDate(utcDate: Date): Date {
|
||||
const year = utcDate.getUTCFullYear();
|
||||
const month = utcDate.getUTCMonth();
|
||||
const day = utcDate.getUTCDate();
|
||||
return new Date(year, month, day);
|
||||
}
|
||||
|
||||
const localDate = convertUTCToLocalCalendarDate(utcDate);
|
||||
const correctTooltip = format(localDate, 'EEEE, MMMM d, yyyy');
|
||||
console.log('Correct tooltip using helper function:', correctTooltip);
|
||||
|
||||
expect(correctTooltip).toBe('Thursday, October 2, 2025');
|
||||
});
|
||||
|
||||
it('Solution 3: Format manually using UTC components', () => {
|
||||
process.env.TZ = 'America/Los_Angeles';
|
||||
|
||||
const utcDate = new Date(Date.UTC(2025, 9, 2));
|
||||
|
||||
// Manual formatting using UTC methods
|
||||
const monthNames = [
|
||||
'January',
|
||||
'February',
|
||||
'March',
|
||||
'April',
|
||||
'May',
|
||||
'June',
|
||||
'July',
|
||||
'August',
|
||||
'September',
|
||||
'October',
|
||||
'November',
|
||||
'December',
|
||||
];
|
||||
const dayNames = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
||||
|
||||
const dayOfWeek = new Date(
|
||||
utcDate.getUTCFullYear(),
|
||||
utcDate.getUTCMonth(),
|
||||
utcDate.getUTCDate()
|
||||
).getDay();
|
||||
const manualTooltip = `${dayNames[dayOfWeek]}, ${monthNames[utcDate.getUTCMonth()]} ${utcDate.getUTCDate()}, ${utcDate.getUTCFullYear()}`;
|
||||
console.log('Manual formatting:', manualTooltip);
|
||||
|
||||
expect(manualTooltip).toBe('Thursday, October 2, 2025');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Recurring Tasks Impact', () => {
|
||||
it('should demonstrate how off-by-one affects weekday recurring tasks', () => {
|
||||
process.env.TZ = 'America/Los_Angeles';
|
||||
|
||||
// User sets up a weekday recurring task
|
||||
// Task is scheduled for Thursday, October 2
|
||||
const scheduledDate = new Date(Date.UTC(2025, 9, 2)); // Thursday
|
||||
|
||||
const buggyDisplay = format(scheduledDate, 'EEEE, MMMM d, yyyy');
|
||||
console.log('Scheduled for:', scheduledDate.toISOString());
|
||||
console.log('But displays as:', buggyDisplay);
|
||||
|
||||
// User sees "Wednesday, October 1" instead of "Thursday, October 2"
|
||||
expect(buggyDisplay).toBe('Wednesday, October 1, 2025');
|
||||
|
||||
// This explains the user's report:
|
||||
// "when I complete it for the current day it logs the correct completed date,
|
||||
// but does not increment the scheduled date to the next day"
|
||||
//
|
||||
// The completion is logged correctly, but the DISPLAY of the next scheduled
|
||||
// date appears to be one day behind, making it seem like it didn't increment
|
||||
});
|
||||
});
|
||||
|
||||
describe('MiniCalendarView Specific Bug Locations', () => {
|
||||
it('should identify aria-label formatting bug at line 604', () => {
|
||||
process.env.TZ = 'America/Los_Angeles';
|
||||
|
||||
const dayDate = new Date(Date.UTC(2025, 9, 2));
|
||||
|
||||
// This is what happens at MiniCalendarView.ts:604
|
||||
// "aria-label": format(dayDate, "EEEE, MMMM d, yyyy")
|
||||
const ariaLabel = format(dayDate, 'EEEE, MMMM d, yyyy');
|
||||
|
||||
console.log('Line 604 aria-label:', ariaLabel);
|
||||
expect(ariaLabel).toBe('Wednesday, October 1, 2025'); // BUG!
|
||||
});
|
||||
|
||||
it('should identify aria-label formatting bug at line 656', () => {
|
||||
process.env.TZ = 'America/Los_Angeles';
|
||||
|
||||
const dayDate = new Date(Date.UTC(2025, 9, 2));
|
||||
|
||||
// This is what happens at MiniCalendarView.ts:656-657
|
||||
// "aria-label": format(dayDate, "EEEE, MMMM d, yyyy") + (isToday ? " (Today)" : "")
|
||||
const ariaLabel = format(dayDate, 'EEEE, MMMM d, yyyy');
|
||||
|
||||
console.log('Line 656 aria-label:', ariaLabel);
|
||||
expect(ariaLabel).toBe('Wednesday, October 1, 2025'); // BUG!
|
||||
});
|
||||
|
||||
it('should identify aria-label formatting bug at line 702', () => {
|
||||
process.env.TZ = 'America/Los_Angeles';
|
||||
|
||||
const dayDate = new Date(Date.UTC(2025, 9, 2));
|
||||
|
||||
// This is what happens at MiniCalendarView.ts:702
|
||||
// "aria-label": format(dayDate, "EEEE, MMMM d, yyyy")
|
||||
const ariaLabel = format(dayDate, 'EEEE, MMMM d, yyyy');
|
||||
|
||||
console.log('Line 702 aria-label:', ariaLabel);
|
||||
expect(ariaLabel).toBe('Wednesday, October 1, 2025'); // BUG!
|
||||
});
|
||||
|
||||
it('should identify updateSelectedDate aria-label bug at line 361', () => {
|
||||
process.env.TZ = 'America/Los_Angeles';
|
||||
|
||||
const newDate = new Date(Date.UTC(2025, 9, 2));
|
||||
|
||||
// This is what happens at MiniCalendarView.ts:361
|
||||
// Checks if ariaLabel.includes(format(newDate, "EEEE, MMMM d, yyyy"))
|
||||
const formattedDate = format(newDate, 'EEEE, MMMM d, yyyy');
|
||||
|
||||
console.log('Line 361 format comparison:', formattedDate);
|
||||
expect(formattedDate).toBe('Wednesday, October 1, 2025'); // BUG!
|
||||
|
||||
// This causes the wrong day element to be selected!
|
||||
});
|
||||
|
||||
it('should identify month display bug at line 378 and 467', () => {
|
||||
process.env.TZ = 'America/Los_Angeles';
|
||||
|
||||
const selectedDate = new Date(Date.UTC(2025, 9, 2));
|
||||
|
||||
// This is what happens at MiniCalendarView.ts:378 and 467
|
||||
// format(this.plugin.selectedDate, "MMMM yyyy")
|
||||
const monthDisplay = format(selectedDate, 'MMMM yyyy');
|
||||
|
||||
console.log('Month display:', monthDisplay);
|
||||
|
||||
// For dates early in the month in negative UTC offset timezones,
|
||||
// this could show the wrong month
|
||||
expect(monthDisplay).toBe('September 2025'); // Potential BUG for Oct 1!
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue