Files
ultra-todo/src/components/CardDetailModal.test.jsx
Hermes (Agent) 81b37530e3 feat(redesign): [P7] integrate P3+P4+P5+P6 components + new App.jsx
- src/components/Board.jsx: prop-driven, owns single DnD context
  (PointerSensor 5px activation distance, KeyboardSensor, closestCorners).
  Wires all sortable lists + cards. Drops P3's store-coupled Board in
  favour of P4's prop-driven List pattern + P2's canonical store.
- src/components/List.jsx: prop-driven, wraps cards in SortableContext
  via SortableCard helper for vertical DnD.
- src/components/ListHeader.jsx: rewritten as prop-driven.
- src/components/Card.jsx, CardDetailModal.jsx: P4 verbatim.
- src/lib/boardDnd.js: P5 reducer + ID helpers.
- src/store/boardStore.js: extended addBoard/addList/addCard to return
  new ids (computed from post-action state) so callers (Sidebar) can
  select the new entity immediately.
- src/components/AppShell.jsx, Sidebar.jsx, TopBar.jsx, EmptyState.jsx:
  P6 verbatim (stateless, prop-driven).
- src/App.jsx: new shell per spec — Sidebar + board area with
  FirstRunEmptyState / SelectBoardEmptyState / TopBar+Board.
- src/test/testUtils.jsx: renderWithStore helper that pre-seeds
  localStorage so useBoardStore's initializer picks up test state.
- src/index.css: fresh global resets.
- Deleted: TodoForm/TodoList/TodoItem (legacy flat-todo), useLocalStorage
  (replaced by useBoardStore), old App.css.
- Added: __fixtures__/DnDFixtureBoard.jsx (P5 DnD fixture for tests).
2026-06-24 05:26:59 +00:00

158 lines
5.4 KiB
JavaScript

import { screen, within } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { render } from '@testing-library/react'
import CardDetailModal from './CardDetailModal.jsx'
function makeCard(overrides = {}) {
return {
id: 'c1',
boardId: 'b1',
listId: 'l1',
title: 'Buy milk',
description: 'From the corner store',
dueDate: '2099-12-31',
createdAt: 1,
...overrides,
}
}
const LISTS = [
{ id: 'l1', boardId: 'b1', name: 'Todo' },
{ id: 'l2', boardId: 'b1', name: 'Doing' },
{ id: 'l3', boardId: 'b1', name: 'Done' },
]
function setup(props = {}, cardOverrides = {}) {
const onSave = jest.fn()
const onDelete = jest.fn()
const onMove = jest.fn()
const onClose = jest.fn()
const card = makeCard(cardOverrides)
const utils = render(
<CardDetailModal
card={card}
lists={LISTS}
onSave={onSave}
onDelete={onDelete}
onMove={onMove}
onClose={onClose}
{...props}
/>
)
return { ...utils, onSave, onDelete, onMove, onClose, card }
}
beforeEach(() => {
// jsdom may have lingering confirm mocks between tests.
jest.restoreAllMocks()
})
describe('<CardDetailModal />', () => {
test('renders the card title, description, and due date in editable fields', () => {
setup()
expect(screen.getByDisplayValue('Buy milk')).toBeInTheDocument()
expect(screen.getByDisplayValue('From the corner store')).toBeInTheDocument()
expect(screen.getByDisplayValue('2099-12-31')).toBeInTheDocument()
})
test('focuses the title input on open', () => {
setup()
const titleInput = screen.getByDisplayValue('Buy milk')
expect(titleInput).toHaveFocus()
})
test('Save button commits edits via onSave with all updated fields', async () => {
const user = userEvent.setup()
const { onSave, onClose } = setup()
const titleInput = screen.getByDisplayValue('Buy milk')
await user.clear(titleInput)
await user.type(titleInput, 'Buy oat milk')
const descInput = screen.getByDisplayValue('From the corner store')
await user.clear(descInput)
await user.type(descInput, 'From the corner store, organic')
const dueInput = screen.getByDisplayValue('2099-12-31')
await user.clear(dueInput)
await user.type(dueInput, '2030-01-15')
await user.click(screen.getByRole('button', { name: /^save$/i }))
expect(onSave).toHaveBeenCalledWith('c1', {
title: 'Buy oat milk',
description: 'From the corner store, organic',
dueDate: '2030-01-15',
})
expect(onClose).toHaveBeenCalled()
})
test('Save with whitespace-only title is a no-op (no onSave call)', async () => {
const user = userEvent.setup()
const { onSave, onClose } = setup()
const titleInput = screen.getByDisplayValue('Buy milk')
await user.clear(titleInput)
await user.type(titleInput, ' ')
await user.click(screen.getByRole('button', { name: /^save$/i }))
expect(onSave).not.toHaveBeenCalled()
expect(onClose).not.toHaveBeenCalled()
})
test('Delete button confirms via window.confirm, then calls onDelete, then closes', async () => {
const user = userEvent.setup()
const confirmSpy = jest.spyOn(window, 'confirm').mockReturnValue(true)
const { onDelete, onClose } = setup()
await user.click(screen.getByRole('button', { name: /^delete$/i }))
expect(confirmSpy).toHaveBeenCalled()
expect(onDelete).toHaveBeenCalledWith('c1')
expect(onClose).toHaveBeenCalled()
})
test('Delete with confirm=false does NOT call onDelete', async () => {
const user = userEvent.setup()
const confirmSpy = jest.spyOn(window, 'confirm').mockReturnValue(false)
const { onDelete, onClose } = setup()
await user.click(screen.getByRole('button', { name: /^delete$/i }))
expect(confirmSpy).toHaveBeenCalled()
expect(onDelete).not.toHaveBeenCalled()
expect(onClose).not.toHaveBeenCalled()
})
test('Changing the "Move to list" select calls onMove with the new list id', async () => {
const user = userEvent.setup()
const { onMove } = setup()
const select = screen.getByLabelText(/move to list/i)
await user.selectOptions(select, 'l2')
expect(onMove).toHaveBeenCalledWith('c1', 'l2', expect.any(Number))
})
test('Escape closes the modal without saving', async () => {
const user = userEvent.setup()
const { onClose, onSave } = setup()
await user.keyboard('{Escape}')
expect(onClose).toHaveBeenCalled()
expect(onSave).not.toHaveBeenCalled()
})
test('Clicking the overlay closes the modal', async () => {
const user = userEvent.setup()
const { onClose } = setup()
// The overlay is the parent of the modal box
const overlay = screen.getByTestId('card-modal-overlay')
await user.click(overlay)
expect(onClose).toHaveBeenCalled()
})
test('Clicking inside the modal box does NOT close', async () => {
const user = userEvent.setup()
const { onClose } = setup()
const titleInput = screen.getByDisplayValue('Buy milk')
await user.click(titleInput)
expect(onClose).not.toHaveBeenCalled()
})
test('Clearing the due date field sends null to onSave', async () => {
const user = userEvent.setup()
const { onSave } = setup()
const dueInput = screen.getByDisplayValue('2099-12-31')
await user.clear(dueInput)
await user.click(screen.getByRole('button', { name: /^save$/i }))
expect(onSave).toHaveBeenCalledWith('c1', expect.objectContaining({ dueDate: null }))
})
})