Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,28 @@ const SOURCE_TYPE_BY_VISUAL_TYPE = {
note: 'note'
} as const

/**
* The instant these tests pretend it is, on today's real local date.
*
* `use-today` snapshots the local date into module scope at import and re-reads the wall clock
* for its first subscriber. A clock faked onto any other date therefore arrives as a midnight
* rollover, which moves `todayCalendarRange`, moves the query key with it, and makes the widget
* fetch a second day on mount. Only the time of day is pinned, and from local fields rather than
* a UTC instant, which far enough from UTC would name a different day.
*/
const NOW = new Date()
NOW.setHours(9, 30, 0, 0)

function todayAtHour(hour: number): string {
const at = new Date(NOW)
at.setHours(hour, 0, 0, 0)
return at.toISOString()
}

function projectionItem(
id: string,
title: string,
hourUtc: number,
hour: number,
visualType: keyof typeof SOURCE_TYPE_BY_VISUAL_TYPE
): CalendarProjectionItem {
return {
Expand All @@ -43,8 +61,8 @@ function projectionItem(
sourceId: id,
title,
descriptionPreview: null,
startAt: `2026-08-31T${String(hourUtc).padStart(2, '0')}:00:00.000Z`,
endAt: `2026-08-31T${String(hourUtc + 1).padStart(2, '0')}:00:00.000Z`,
startAt: todayAtHour(hour),
endAt: todayAtHour(hour + 1),
isAllDay: false,
timezone: 'UTC',
visualType,
Expand Down Expand Up @@ -101,7 +119,7 @@ function renderApp(): { showBoard: (visible: boolean) => void } {

describe('home calendar widget stays current', () => {
beforeEach(() => {
vi.setSystemTime(new Date('2026-08-31T09:30:00.000Z'))
vi.setSystemTime(NOW)
listeners.clear()
server.items = [projectionItem('e1', 'Standup', 10, 'event')]
mockGetRange.mockReset()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

import React from 'react'
import { describe, expect, it, vi, beforeEach } from 'vitest'
import { screen, waitFor } from '@testing-library/react'
import { fireEvent, screen, waitFor } from '@testing-library/react'
import { renderWithProviders, getMockApi, userEvent } from '@tests/utils/render'
import { JournalReminderButton } from './journal-reminder-button'

Expand Down Expand Up @@ -39,14 +39,45 @@ vi.mock('@/components/ui/picker', async () => {
return createPickerStub()
})

function reminderApi(): Record<string, ReturnType<typeof vi.fn>> {
return (getMockApi() as unknown as { reminders: Record<string, ReturnType<typeof vi.fn>> })
.reminders
}

/**
* `useNoteReminders` asks for both the note's own reminders and its highlight
* reminders, so the seed has to answer per target type. Returning the same row
* to both queries would list it twice and make the remove button ambiguous.
*/
function seedActiveReminder(targetType: 'journal' | 'note', targetId: string): void {
reminderApi().getForTarget.mockImplementation((input: { targetType: string }) =>
Promise.resolve(
input.targetType === targetType
? [
{
id: `rem-${targetType}-1`,
targetType,
targetId,
remindAt: '2026-05-17T09:00:00.000Z',
status: 'pending',
note: null
}
]
: []
)
)
}

describe('JournalReminderButton', () => {
beforeEach(() => {
const api = getMockApi() as unknown as {
reminders: Record<string, ReturnType<typeof vi.fn>>
}
api.reminders.getForTarget = vi.fn().mockResolvedValue([])
api.reminders.create.mockClear()
api.reminders.create.mockResolvedValue({ success: true })
const api = reminderApi()
api.getForTarget = vi.fn().mockResolvedValue([])
api.create.mockClear()
api.create.mockResolvedValue({ success: true })
api.update.mockClear()
api.update.mockResolvedValue({ success: true, reminder: null })
api.delete.mockClear()
api.delete.mockResolvedValue({ success: true })
})

it('sends the note typed in the picker to reminders.create', async () => {
Expand All @@ -72,4 +103,62 @@ describe('JournalReminderButton', () => {
)
})
})

it('moves the reminder the entry already has instead of creating a second', async () => {
const user = userEvent.setup()
seedActiveReminder('journal', '2026-05-10')
renderWithProviders(<JournalReminderButton journalDate="2026-05-10" />)

await screen.findByRole('button', {
name: /phaseF.componentsReminderReminderPicker.deleteReminder/
})
await user.click(screen.getByTestId('preset-in-one-week'))

await waitFor(() => {
expect(reminderApi().update).toHaveBeenCalledWith(
expect.objectContaining({ id: 'rem-journal-1' })
)
})
expect(reminderApi().create).not.toHaveBeenCalled()
})

it('removes the reminder from the picker list', async () => {
const user = userEvent.setup()
seedActiveReminder('journal', '2026-05-10')
renderWithProviders(<JournalReminderButton journalDate="2026-05-10" />)

await user.click(
await screen.findByRole('button', {
name: /phaseF.componentsReminderReminderPicker.deleteReminder/
})
)

await waitFor(() => {
expect(reminderApi().delete).toHaveBeenCalledWith('rem-journal-1')
})
})

it('changes the time on the reminder already set', async () => {
const user = userEvent.setup()
seedActiveReminder('journal', '2026-05-10')
renderWithProviders(<JournalReminderButton journalDate="2026-05-10" />)

await user.click(
await screen.findByRole('button', {
name: /phaseF.componentsReminderReminderPicker.editReminder/
})
)
fireEvent.change(screen.getByLabelText(/phaseF.componentsReminderReminderPicker.time/), {
target: { value: '07:15' }
})
await user.click(
screen.getByRole('button', { name: 'phaseF.componentsReminderReminderPicker.save' })
)

await waitFor(() => expect(reminderApi().update).toHaveBeenCalledTimes(1))
const [payload] = reminderApi().update.mock.calls[0] as [{ id: string; remindAt: string }]
expect(payload.id).toBe('rem-journal-1')
const moved = new Date(payload.remindAt)
expect([moved.getHours(), moved.getMinutes()]).toEqual([7, 15])
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export function JournalReminderButton({
const {
settings: { clockFormat }
} = useGeneralSettings()
const { hasActiveReminder, nextReminder, activeReminderCount, actions } =
const { activeReminders, hasActiveReminder, nextReminder, activeReminderCount, actions } =
useJournalReminders(journalDate)

const handleSetReminder = async (date: Date, note?: string): Promise<void> => {
Expand Down Expand Up @@ -74,6 +74,9 @@ export function JournalReminderButton({
telemetrySurface="journal"
showNote
disabled={disabled}
reminders={activeReminders}
onEdit={(id, date, note) => void actions.editReminder(id, date, note)}
onDelete={(id) => void actions.deleteReminder(id)}
trigger={
<Button
variant="ghost"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,13 @@ vi.mock('@/hooks/use-general-settings', () => ({

vi.mock('@/hooks/use-journal-reminders', () => ({
useJournalReminders: () => ({
activeReminders: [],
...mocks.reminderState,
actions: { setReminder: mocks.setReminder }
actions: {
setReminder: mocks.setReminder,
editReminder: vi.fn(),
deleteReminder: vi.fn()
}
})
}))

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

import React from 'react'
import { describe, expect, it, vi, beforeEach } from 'vitest'
import { screen, waitFor } from '@testing-library/react'
import { fireEvent, screen, waitFor } from '@testing-library/react'
import { renderWithProviders, getMockApi, userEvent } from '@tests/utils/render'
import { NoteReminderButton } from './note-reminder-button'

Expand Down Expand Up @@ -39,14 +39,45 @@ vi.mock('@/components/ui/picker', async () => {
return createPickerStub()
})

function reminderApi(): Record<string, ReturnType<typeof vi.fn>> {
return (getMockApi() as unknown as { reminders: Record<string, ReturnType<typeof vi.fn>> })
.reminders
}

/**
* `useNoteReminders` asks for both the note's own reminders and its highlight
* reminders, so the seed has to answer per target type. Returning the same row
* to both queries would list it twice and make the remove button ambiguous.
*/
function seedActiveReminder(targetType: 'journal' | 'note', targetId: string): void {
reminderApi().getForTarget.mockImplementation((input: { targetType: string }) =>
Promise.resolve(
input.targetType === targetType
? [
{
id: `rem-${targetType}-1`,
targetType,
targetId,
remindAt: '2026-05-17T09:00:00.000Z',
status: 'pending',
note: null
}
]
: []
)
)
}

describe('NoteReminderButton', () => {
beforeEach(() => {
const api = getMockApi() as unknown as {
reminders: Record<string, ReturnType<typeof vi.fn>>
}
api.reminders.getForTarget = vi.fn().mockResolvedValue([])
api.reminders.create.mockClear()
api.reminders.create.mockResolvedValue({ success: true })
const api = reminderApi()
api.getForTarget = vi.fn().mockResolvedValue([])
api.create.mockClear()
api.create.mockResolvedValue({ success: true })
api.update.mockClear()
api.update.mockResolvedValue({ success: true, reminder: null })
api.delete.mockClear()
api.delete.mockResolvedValue({ success: true })
})

it('sends the note typed in the picker to reminders.create', async () => {
Expand All @@ -72,4 +103,62 @@ describe('NoteReminderButton', () => {
)
})
})

it('moves the reminder the note already has instead of creating a second', async () => {
const user = userEvent.setup()
seedActiveReminder('note', 'note-1')
renderWithProviders(<NoteReminderButton noteId="note-1" />)

await screen.findByRole('button', {
name: /phaseF.componentsReminderReminderPicker.deleteReminder/
})
await user.click(screen.getByTestId('preset-tomorrow'))

await waitFor(() => {
expect(reminderApi().update).toHaveBeenCalledWith(
expect.objectContaining({ id: 'rem-note-1' })
)
})
expect(reminderApi().create).not.toHaveBeenCalled()
})

it('removes the reminder from the picker list', async () => {
const user = userEvent.setup()
seedActiveReminder('note', 'note-1')
renderWithProviders(<NoteReminderButton noteId="note-1" />)

await user.click(
await screen.findByRole('button', {
name: /phaseF.componentsReminderReminderPicker.deleteReminder/
})
)

await waitFor(() => {
expect(reminderApi().delete).toHaveBeenCalledWith('rem-note-1')
})
})

it('changes the time on the reminder already set', async () => {
const user = userEvent.setup()
seedActiveReminder('note', 'note-1')
renderWithProviders(<NoteReminderButton noteId="note-1" />)

await user.click(
await screen.findByRole('button', {
name: /phaseF.componentsReminderReminderPicker.editReminder/
})
)
fireEvent.change(screen.getByLabelText(/phaseF.componentsReminderReminderPicker.time/), {
target: { value: '07:15' }
})
await user.click(
screen.getByRole('button', { name: 'phaseF.componentsReminderReminderPicker.save' })
)

await waitFor(() => expect(reminderApi().update).toHaveBeenCalledTimes(1))
const [payload] = reminderApi().update.mock.calls[0] as [{ id: string; remindAt: string }]
expect(payload.id).toBe('rem-note-1')
const moved = new Date(payload.remindAt)
expect([moved.getHours(), moved.getMinutes()]).toEqual([7, 15])
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ export function NoteReminderButton({
const {
settings: { clockFormat }
} = useGeneralSettings()
const { hasActiveReminder, nextReminder, activeReminderCount, actions } = useNoteReminders(noteId)
const { activeReminders, hasActiveReminder, nextReminder, activeReminderCount, actions } =
useNoteReminders(noteId)

const handleSetReminder = async (date: Date, note?: string): Promise<void> => {
await actions.setReminder(date, note)
Expand Down Expand Up @@ -71,6 +72,9 @@ export function NoteReminderButton({
telemetrySurface="notes"
showNote
disabled={disabled}
reminders={activeReminders}
onEdit={(id, date, note) => void actions.editReminder(id, date, note)}
onDelete={(id) => void actions.deleteReminder(id)}
trigger={
<Button
variant="ghost"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,15 @@ vi.mock('@/hooks/use-general-settings', () => ({

vi.mock('@/hooks/use-note-reminders', () => ({
useNoteReminders: () => ({
activeReminders: [],
hasActiveReminder: true,
nextReminder: { remindAt: '2026-05-10T10:00:00.000Z' },
activeReminderCount: 12,
actions: { setReminder: mocks.setReminder }
actions: {
setReminder: mocks.setReminder,
editReminder: vi.fn(),
deleteReminder: vi.fn()
}
})
}))

Expand Down
Loading
Loading