Files
experiments-database/frontend/tests/ExperimentCalendar.test.jsx
T
Experiments DB Dev c8f0c5f298 feat: replace calendar day modal with dedicated day-view page
Calendar day clicks now navigate to /experiments/:id/day/:date
(with ?field= param). ExperimentDayView shows a sticky-column table
of all subjects with data that day, inline edit via modal, and a
session-metrics bar chart from analysis_summary. Subject name links
to /daily-statuses/:id. Backend adds ?date= filtering to
GET /experiments/:id/daily-statuses.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-23 12:46:35 -04:00

187 lines
8.6 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import ExperimentCalendar from '../src/components/ExperimentCalendar';
const mockNavigate = jest.fn();
jest.mock('react-router-dom', () => ({
useNavigate: () => mockNavigate,
}));
const EXP_ID = 'aaaaaaaa-0000-0000-0000-000000000001';
const TEMPLATE = [
{ fieldId: '00000000-0000-0000-0000-000000000002', key: 'vitals', label: 'Vitals', active: true, builtin: true },
{ fieldId: 'ww000000-0000-0000-0000-000000000099', key: 'weights', label: 'Weights', active: true, builtin: false },
{ fieldId: 'zz000000-0000-0000-0000-000000000001', key: 'notes', label: 'Notes', active: false, builtin: true },
];
function todayStr() {
const d = new Date();
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
}
function makeStatus(animalId, date, customFields = {}, vitals = null) {
return {
id: `s-${animalId}-${date}`,
animal_id: animalId,
date: `${date}T00:00:00.000Z`,
experiment_description: null,
vitals,
treatment: null,
notes: null,
custom_fields: customFields,
analysis_summary: null,
};
}
beforeEach(() => {
jest.clearAllMocks();
});
// ── Rendering ──────────────────────────────────────────────────────────────────
describe('ExperimentCalendar rendering', () => {
it('renders the current month and year', () => {
render(<ExperimentCalendar experimentId={EXP_ID} template={TEMPLATE} allStatuses={[]} />);
const now = new Date();
const monthLabel = now.toLocaleString('en-US', { month: 'long', year: 'numeric' });
expect(screen.getByText(monthLabel)).toBeInTheDocument();
});
it('renders day-of-week headers', () => {
render(<ExperimentCalendar experimentId={EXP_ID} template={TEMPLATE} allStatuses={[]} />);
for (const d of ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']) {
expect(screen.getByText(d)).toBeInTheDocument();
}
});
it('renders the field selector with active template fields only', () => {
render(<ExperimentCalendar experimentId={EXP_ID} template={TEMPLATE} allStatuses={[]} />);
const options = screen.getAllByRole('option').map((o) => o.textContent);
expect(options).toContain('Vitals');
expect(options).toContain('Weights');
expect(options).not.toContain('Notes'); // inactive
});
});
// ── Default field selection ────────────────────────────────────────────────────
describe('default field selection', () => {
it('defaults to the field whose label contains "weight"', () => {
render(<ExperimentCalendar experimentId={EXP_ID} template={TEMPLATE} allStatuses={[]} />);
expect(screen.getByTestId('field-select')).toHaveValue('ww000000-0000-0000-0000-000000000099');
});
it('defaults to first field if none contains "weight"', () => {
const noWeightTemplate = [
{ fieldId: 'ff000000-0000-0000-0000-000000000001', key: 'treatment', label: 'Treatment', active: true, builtin: true },
{ fieldId: 'ff000000-0000-0000-0000-000000000002', key: 'vitals', label: 'Vitals', active: true, builtin: true },
];
render(<ExperimentCalendar experimentId={EXP_ID} template={noWeightTemplate} allStatuses={[]} />);
expect(screen.getByTestId('field-select')).toHaveValue('treatment');
});
});
// ── Count badges (computed from allStatuses) ───────────────────────────────────
describe('count badges', () => {
it('shows a count badge for a day where the selected field is non-empty', () => {
const dateStr = todayStr();
const statuses = [
makeStatus('a1', dateStr, { 'ww000000-0000-0000-0000-000000000099': '325' }),
makeStatus('a2', dateStr, { 'ww000000-0000-0000-0000-000000000099': '310' }),
];
render(<ExperimentCalendar experimentId={EXP_ID} template={TEMPLATE} allStatuses={statuses} />);
expect(screen.getByTestId(`count-${dateStr}`)).toHaveTextContent('2');
});
it('does not show count badge on days with no data', () => {
render(<ExperimentCalendar experimentId={EXP_ID} template={TEMPLATE} allStatuses={[]} />);
expect(screen.queryByTestId(`count-${todayStr()}`)).not.toBeInTheDocument();
});
it('shows a count badge for builtin field (vitals)', () => {
const dateStr = todayStr();
const statuses = [makeStatus('a1', dateStr, {}, 'HR 72')];
render(<ExperimentCalendar experimentId={EXP_ID} template={TEMPLATE} allStatuses={statuses} />);
fireEvent.change(screen.getByTestId('field-select'), { target: { value: 'vitals' } });
expect(screen.getByTestId(`count-${dateStr}`)).toHaveTextContent('1');
});
it('recomputes counts after field change', () => {
const dateStr = todayStr();
const statuses = [makeStatus('a1', dateStr, {}, 'HR 72')];
render(<ExperimentCalendar experimentId={EXP_ID} template={TEMPLATE} allStatuses={statuses} />);
// Default is weights — no badge
expect(screen.queryByTestId(`count-${dateStr}`)).not.toBeInTheDocument();
// Switch to vitals — badge appears
fireEvent.change(screen.getByTestId('field-select'), { target: { value: 'vitals' } });
expect(screen.getByTestId(`count-${dateStr}`)).toHaveTextContent('1');
});
});
// ── Month navigation ──────────────────────────────────────────────────────────
describe('month navigation', () => {
it('moves to the previous month on click', () => {
render(<ExperimentCalendar experimentId={EXP_ID} template={TEMPLATE} allStatuses={[]} />);
const now = new Date();
const prev = new Date(now.getFullYear(), now.getMonth() - 1, 1);
const prevLabel = prev.toLocaleString('en-US', { month: 'long', year: 'numeric' });
fireEvent.click(screen.getByLabelText('Previous month'));
expect(screen.getByText(prevLabel)).toBeInTheDocument();
});
it('moves to the next month on click', () => {
render(<ExperimentCalendar experimentId={EXP_ID} template={TEMPLATE} allStatuses={[]} />);
const now = new Date();
const next = new Date(now.getFullYear(), now.getMonth() + 1, 1);
const nextLabel = next.toLocaleString('en-US', { month: 'long', year: 'numeric' });
fireEvent.click(screen.getByLabelText('Next month'));
expect(screen.getByText(nextLabel)).toBeInTheDocument();
});
});
// ── Day click navigation ──────────────────────────────────────────────────────
describe('day click navigation', () => {
it('navigates to the day view with field param when a day with data is clicked', () => {
const dateStr = todayStr();
const statuses = [
makeStatus('a1', dateStr, { 'ww000000-0000-0000-0000-000000000099': '325' }),
];
render(<ExperimentCalendar experimentId={EXP_ID} template={TEMPLATE} allStatuses={statuses} />);
fireEvent.click(screen.getByTestId(`day-${dateStr}`));
expect(mockNavigate).toHaveBeenCalledWith(
`/experiments/${EXP_ID}/day/${dateStr}?field=ww000000-0000-0000-0000-000000000099`,
);
});
it('does not navigate when clicking a day without data (button is disabled)', () => {
render(<ExperimentCalendar experimentId={EXP_ID} template={TEMPLATE} allStatuses={[]} />);
const dayBtn = screen.getByTestId(`day-${todayStr()}`);
expect(dayBtn).toBeDisabled();
fireEvent.click(dayBtn);
expect(mockNavigate).not.toHaveBeenCalled();
});
it('includes the currently selected field in the navigation URL', () => {
const dateStr = todayStr();
const statuses = [makeStatus('a1', dateStr, {}, 'HR 72')];
render(<ExperimentCalendar experimentId={EXP_ID} template={TEMPLATE} allStatuses={statuses} />);
// Switch to vitals field first
fireEvent.change(screen.getByTestId('field-select'), { target: { value: 'vitals' } });
fireEvent.click(screen.getByTestId(`day-${dateStr}`));
expect(mockNavigate).toHaveBeenCalledWith(
`/experiments/${EXP_ID}/day/${dateStr}?field=vitals`,
);
});
it('navigates with no field param when selectedField is null (no active fields)', () => {
const emptyTemplate = [];
render(<ExperimentCalendar experimentId={EXP_ID} template={emptyTemplate} allStatuses={[]} />);
// No days have data with empty template, so nothing to click — just verify no crash
expect(screen.queryByTestId(`count-${todayStr()}`)).not.toBeInTheDocument();
});
});