feat: permanent calendar with editable day modal and per-subject bar chart
- Calendar moved out of List/Group tab toggle — always visible below cross- subject analytics (ExperimentAnalysisCharts) - Day modal shows all subjects: existing data as read-only + Edit button, missing data as + Add button; both open DailyStatusForm inline - Status changes/creates propagate up to parent via onStatusChange/onStatusCreate and calendar counts recompute locally (no extra API round-trip) - Bar chart (recharts) at bottom: Total/Success/Failure bars per subject plus % complete text, grouped by subject name - Tests updated: 17 ExperimentCalendar tests, 76 total passing Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -12,11 +12,11 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.7.2",
|
"axios": "^1.7.2",
|
||||||
"recharts": "^2.12.7",
|
|
||||||
"date-fns": "^3.6.0",
|
"date-fns": "^3.6.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-router-dom": "^6.24.0"
|
"react-router-dom": "^6.24.0",
|
||||||
|
"recharts": "^2.15.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.24.7",
|
"@babel/core": "^7.24.7",
|
||||||
@@ -38,8 +38,12 @@
|
|||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
"testEnvironment": "jsdom",
|
"testEnvironment": "jsdom",
|
||||||
"setupFilesAfterEnv": ["<rootDir>/tests/setup.js"],
|
"setupFilesAfterEnv": [
|
||||||
"testMatch": ["**/tests/**/*.test.{js,jsx}"],
|
"<rootDir>/tests/setup.js"
|
||||||
|
],
|
||||||
|
"testMatch": [
|
||||||
|
"**/tests/**/*.test.{js,jsx}"
|
||||||
|
],
|
||||||
"transform": {
|
"transform": {
|
||||||
"^.+\\.[jt]sx?$": "babel-jest"
|
"^.+\\.[jt]sx?$": "babel-jest"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { render, screen, fireEvent, waitFor, act } from '@testing-library/react';
|
import { render, screen, fireEvent, waitFor, within } from '@testing-library/react';
|
||||||
import ExperimentCalendar from '../src/components/ExperimentCalendar';
|
import ExperimentCalendar from '../src/components/ExperimentCalendar';
|
||||||
import { experimentsApi } from '../src/api/client';
|
import { experimentsApi } from '../src/api/client';
|
||||||
|
|
||||||
@@ -9,6 +9,17 @@ jest.mock('../src/api/client', () => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
jest.mock('recharts', () => ({
|
||||||
|
ResponsiveContainer: ({ children }) => <div>{children}</div>,
|
||||||
|
BarChart: ({ children }) => <div data-testid="bar-chart">{children}</div>,
|
||||||
|
Bar: () => null,
|
||||||
|
XAxis: () => null,
|
||||||
|
YAxis: () => null,
|
||||||
|
CartesianGrid: () => null,
|
||||||
|
Tooltip: () => null,
|
||||||
|
Legend: () => null,
|
||||||
|
}));
|
||||||
|
|
||||||
const EXP_ID = 'aaaaaaaa-0000-0000-0000-000000000001';
|
const EXP_ID = 'aaaaaaaa-0000-0000-0000-000000000001';
|
||||||
|
|
||||||
const TEMPLATE = [
|
const TEMPLATE = [
|
||||||
@@ -22,18 +33,18 @@ const ANIMALS = [
|
|||||||
{ id: 'a2000000-0000-0000-0000-000000000002', animal_name: 'Rat B', animal_id_string: 'R002' },
|
{ id: 'a2000000-0000-0000-0000-000000000002', animal_name: 'Rat B', animal_id_string: 'R002' },
|
||||||
];
|
];
|
||||||
|
|
||||||
function todayYYYYMM() {
|
function todayStr() {
|
||||||
const d = new Date();
|
const d = new Date();
|
||||||
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`;
|
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeStatus(animalId, date, customFields = {}) {
|
function makeStatus(animalId, date, customFields = {}, vitals = null) {
|
||||||
return {
|
return {
|
||||||
id: `s-${animalId}-${date}`,
|
id: `s-${animalId}-${date}`,
|
||||||
animal_id: animalId,
|
animal_id: animalId,
|
||||||
date: `${date}T00:00:00.000Z`,
|
date: `${date}T00:00:00.000Z`,
|
||||||
experiment_description: null,
|
experiment_description: null,
|
||||||
vitals: null,
|
vitals,
|
||||||
treatment: null,
|
treatment: null,
|
||||||
notes: null,
|
notes: null,
|
||||||
custom_fields: customFields,
|
custom_fields: customFields,
|
||||||
@@ -42,29 +53,21 @@ function makeStatus(animalId, date, customFields = {}) {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
experimentsApi.getCalendar.mockResolvedValue({ field: 'ww000000-0000-0000-0000-000000000099', days: {} });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Rendering ──────────────────────────────────────────────────────────────────
|
// ── Rendering ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
describe('ExperimentCalendar rendering', () => {
|
describe('ExperimentCalendar rendering', () => {
|
||||||
it('renders the current month and year', async () => {
|
it('renders the current month and year', () => {
|
||||||
render(
|
render(
|
||||||
<ExperimentCalendar
|
<ExperimentCalendar experimentId={EXP_ID} template={TEMPLATE} allStatuses={[]} animals={ANIMALS} />,
|
||||||
experimentId={EXP_ID}
|
|
||||||
template={TEMPLATE}
|
|
||||||
allStatuses={[]}
|
|
||||||
animals={ANIMALS}
|
|
||||||
/>,
|
|
||||||
);
|
);
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const monthLabel = now.toLocaleString('en-US', { month: 'long', year: 'numeric' });
|
const monthLabel = now.toLocaleString('en-US', { month: 'long', year: 'numeric' });
|
||||||
await waitFor(() => {
|
expect(screen.getByText(monthLabel)).toBeInTheDocument();
|
||||||
expect(screen.getByText(monthLabel)).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders day-of-week headers', async () => {
|
it('renders day-of-week headers', () => {
|
||||||
render(
|
render(
|
||||||
<ExperimentCalendar experimentId={EXP_ID} template={TEMPLATE} allStatuses={[]} animals={ANIMALS} />,
|
<ExperimentCalendar experimentId={EXP_ID} template={TEMPLATE} allStatuses={[]} animals={ANIMALS} />,
|
||||||
);
|
);
|
||||||
@@ -73,11 +76,11 @@ describe('ExperimentCalendar rendering', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders the field selector with active template fields only', async () => {
|
it('renders the field selector with active template fields only', () => {
|
||||||
render(
|
render(
|
||||||
<ExperimentCalendar experimentId={EXP_ID} template={TEMPLATE} allStatuses={[]} animals={ANIMALS} />,
|
<ExperimentCalendar experimentId={EXP_ID} template={TEMPLATE} allStatuses={[]} animals={ANIMALS} />,
|
||||||
);
|
);
|
||||||
await waitFor(() => expect(screen.getByTestId('field-select')).toBeInTheDocument());
|
expect(screen.getByTestId('field-select')).toBeInTheDocument();
|
||||||
const options = screen.getAllByRole('option');
|
const options = screen.getAllByRole('option');
|
||||||
const labels = options.map((o) => o.textContent);
|
const labels = options.map((o) => o.textContent);
|
||||||
expect(labels).toContain('Vitals');
|
expect(labels).toContain('Vitals');
|
||||||
@@ -89,19 +92,14 @@ describe('ExperimentCalendar rendering', () => {
|
|||||||
// ── Default field selection ────────────────────────────────────────────────────
|
// ── Default field selection ────────────────────────────────────────────────────
|
||||||
|
|
||||||
describe('default field selection', () => {
|
describe('default field selection', () => {
|
||||||
it('defaults to the field whose label contains "weight"', async () => {
|
it('defaults to the field whose label contains "weight"', () => {
|
||||||
render(
|
render(
|
||||||
<ExperimentCalendar experimentId={EXP_ID} template={TEMPLATE} allStatuses={[]} animals={ANIMALS} />,
|
<ExperimentCalendar experimentId={EXP_ID} template={TEMPLATE} allStatuses={[]} animals={ANIMALS} />,
|
||||||
);
|
);
|
||||||
await waitFor(() => {
|
expect(screen.getByTestId('field-select')).toHaveValue('ww000000-0000-0000-0000-000000000099');
|
||||||
expect(experimentsApi.getCalendar).toHaveBeenCalledWith(
|
|
||||||
EXP_ID,
|
|
||||||
'ww000000-0000-0000-0000-000000000099',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('defaults to first field if none contains "weight"', async () => {
|
it('defaults to first field if none contains "weight"', () => {
|
||||||
const noWeightTemplate = [
|
const noWeightTemplate = [
|
||||||
{ fieldId: 'ff000000-0000-0000-0000-000000000001', key: 'treatment', label: 'Treatment', active: true, builtin: true },
|
{ 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 },
|
{ fieldId: 'ff000000-0000-0000-0000-000000000002', key: 'vitals', label: 'Vitals', active: true, builtin: true },
|
||||||
@@ -109,189 +107,161 @@ describe('default field selection', () => {
|
|||||||
render(
|
render(
|
||||||
<ExperimentCalendar experimentId={EXP_ID} template={noWeightTemplate} allStatuses={[]} animals={ANIMALS} />,
|
<ExperimentCalendar experimentId={EXP_ID} template={noWeightTemplate} allStatuses={[]} animals={ANIMALS} />,
|
||||||
);
|
);
|
||||||
await waitFor(() => {
|
expect(screen.getByTestId('field-select')).toHaveValue('treatment');
|
||||||
expect(experimentsApi.getCalendar).toHaveBeenCalledWith(EXP_ID, 'treatment');
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Count badges ──────────────────────────────────────────────────────────────
|
// ── Count badges (computed from allStatuses) ───────────────────────────────────
|
||||||
|
|
||||||
describe('count badges', () => {
|
describe('count badges', () => {
|
||||||
it('shows a count badge on days returned by the API', async () => {
|
it('shows a count badge for a day where the selected field is non-empty', () => {
|
||||||
const today = new Date();
|
const dateStr = todayStr();
|
||||||
const year = today.getFullYear();
|
const statuses = [
|
||||||
const month = String(today.getMonth() + 1).padStart(2, '0');
|
makeStatus('a1000000-0000-0000-0000-000000000001', dateStr, { 'ww000000-0000-0000-0000-000000000099': '325' }),
|
||||||
const day = String(today.getDate()).padStart(2, '0');
|
makeStatus('a2000000-0000-0000-0000-000000000002', dateStr, { 'ww000000-0000-0000-0000-000000000099': '310' }),
|
||||||
const dateStr = `${year}-${month}-${day}`;
|
];
|
||||||
|
|
||||||
experimentsApi.getCalendar.mockResolvedValue({
|
|
||||||
field: 'ww000000-0000-0000-0000-000000000099',
|
|
||||||
days: { [dateStr]: 3 },
|
|
||||||
});
|
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<ExperimentCalendar experimentId={EXP_ID} template={TEMPLATE} allStatuses={[]} animals={ANIMALS} />,
|
<ExperimentCalendar experimentId={EXP_ID} template={TEMPLATE} allStatuses={statuses} animals={ANIMALS} />,
|
||||||
);
|
);
|
||||||
|
expect(screen.getByTestId(`count-${dateStr}`)).toHaveTextContent('2');
|
||||||
await waitFor(() => {
|
|
||||||
expect(screen.getByTestId(`count-${dateStr}`)).toHaveTextContent('3');
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not show count badge on days with no data', async () => {
|
it('does not show count badge on days with no data', () => {
|
||||||
const today = new Date();
|
const dateStr = todayStr();
|
||||||
const year = today.getFullYear();
|
|
||||||
const month = String(today.getMonth() + 1).padStart(2, '0');
|
|
||||||
const day = String(today.getDate()).padStart(2, '0');
|
|
||||||
const dateStr = `${year}-${month}-${day}`;
|
|
||||||
|
|
||||||
experimentsApi.getCalendar.mockResolvedValue({ field: 'vitals', days: {} });
|
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<ExperimentCalendar experimentId={EXP_ID} template={TEMPLATE} allStatuses={[]} animals={ANIMALS} />,
|
<ExperimentCalendar experimentId={EXP_ID} template={TEMPLATE} allStatuses={[]} animals={ANIMALS} />,
|
||||||
);
|
);
|
||||||
|
|
||||||
await waitFor(() => expect(experimentsApi.getCalendar).toHaveBeenCalled());
|
|
||||||
expect(screen.queryByTestId(`count-${dateStr}`)).not.toBeInTheDocument();
|
expect(screen.queryByTestId(`count-${dateStr}`)).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('shows a count badge for builtin field (vitals)', () => {
|
||||||
|
const dateStr = todayStr();
|
||||||
|
const statuses = [makeStatus('a1000000-0000-0000-0000-000000000001', dateStr, {}, 'HR 72')];
|
||||||
|
render(
|
||||||
|
<ExperimentCalendar experimentId={EXP_ID} template={TEMPLATE} allStatuses={statuses} animals={ANIMALS} />,
|
||||||
|
);
|
||||||
|
// Switch to vitals field
|
||||||
|
fireEvent.change(screen.getByTestId('field-select'), { target: { value: 'vitals' } });
|
||||||
|
expect(screen.getByTestId(`count-${dateStr}`)).toHaveTextContent('1');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Month navigation ──────────────────────────────────────────────────────────
|
// ── Month navigation ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
describe('month navigation', () => {
|
describe('month navigation', () => {
|
||||||
it('moves to the previous month on ‹ click', async () => {
|
it('moves to the previous month on ‹ click', () => {
|
||||||
render(
|
render(
|
||||||
<ExperimentCalendar experimentId={EXP_ID} template={TEMPLATE} allStatuses={[]} animals={ANIMALS} />,
|
<ExperimentCalendar experimentId={EXP_ID} template={TEMPLATE} allStatuses={[]} animals={ANIMALS} />,
|
||||||
);
|
);
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const prev = new Date(now.getFullYear(), now.getMonth() - 1, 1);
|
const prev = new Date(now.getFullYear(), now.getMonth() - 1, 1);
|
||||||
const prevLabel = prev.toLocaleString('en-US', { month: 'long', year: 'numeric' });
|
const prevLabel = prev.toLocaleString('en-US', { month: 'long', year: 'numeric' });
|
||||||
|
|
||||||
fireEvent.click(screen.getByLabelText('Previous month'));
|
fireEvent.click(screen.getByLabelText('Previous month'));
|
||||||
await waitFor(() => expect(screen.getByText(prevLabel)).toBeInTheDocument());
|
expect(screen.getByText(prevLabel)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('moves to the next month on › click', async () => {
|
it('moves to the next month on › click', () => {
|
||||||
render(
|
render(
|
||||||
<ExperimentCalendar experimentId={EXP_ID} template={TEMPLATE} allStatuses={[]} animals={ANIMALS} />,
|
<ExperimentCalendar experimentId={EXP_ID} template={TEMPLATE} allStatuses={[]} animals={ANIMALS} />,
|
||||||
);
|
);
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const next = new Date(now.getFullYear(), now.getMonth() + 1, 1);
|
const next = new Date(now.getFullYear(), now.getMonth() + 1, 1);
|
||||||
const nextLabel = next.toLocaleString('en-US', { month: 'long', year: 'numeric' });
|
const nextLabel = next.toLocaleString('en-US', { month: 'long', year: 'numeric' });
|
||||||
|
|
||||||
fireEvent.click(screen.getByLabelText('Next month'));
|
fireEvent.click(screen.getByLabelText('Next month'));
|
||||||
await waitFor(() => expect(screen.getByText(nextLabel)).toBeInTheDocument());
|
expect(screen.getByText(nextLabel)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('re-fetches calendar data after field change', async () => {
|
it('recomputes counts after field change', () => {
|
||||||
|
const dateStr = todayStr();
|
||||||
|
// Only has vitals, not weights
|
||||||
|
const statuses = [makeStatus('a1000000-0000-0000-0000-000000000001', dateStr, {}, 'HR 72')];
|
||||||
render(
|
render(
|
||||||
<ExperimentCalendar experimentId={EXP_ID} template={TEMPLATE} allStatuses={[]} animals={ANIMALS} />,
|
<ExperimentCalendar experimentId={EXP_ID} template={TEMPLATE} allStatuses={statuses} animals={ANIMALS} />,
|
||||||
);
|
);
|
||||||
await waitFor(() => expect(experimentsApi.getCalendar).toHaveBeenCalledTimes(1));
|
// 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' } });
|
fireEvent.change(screen.getByTestId('field-select'), { target: { value: 'vitals' } });
|
||||||
|
expect(screen.getByTestId(`count-${dateStr}`)).toHaveTextContent('1');
|
||||||
await waitFor(() => expect(experimentsApi.getCalendar).toHaveBeenCalledTimes(2));
|
|
||||||
expect(experimentsApi.getCalendar).toHaveBeenLastCalledWith(EXP_ID, 'vitals');
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Day click → modal ─────────────────────────────────────────────────────────
|
// ── Day click → modal ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
describe('day click modal', () => {
|
describe('day click modal', () => {
|
||||||
it('opens a modal showing subject data when a day with data is clicked', async () => {
|
it('opens a modal when a day with data is clicked', async () => {
|
||||||
const today = new Date();
|
const dateStr = todayStr();
|
||||||
const year = today.getFullYear();
|
|
||||||
const month = String(today.getMonth() + 1).padStart(2, '0');
|
|
||||||
const day = String(today.getDate()).padStart(2, '0');
|
|
||||||
const dateStr = `${year}-${month}-${day}`;
|
|
||||||
|
|
||||||
experimentsApi.getCalendar.mockResolvedValue({
|
|
||||||
field: 'ww000000-0000-0000-0000-000000000099',
|
|
||||||
days: { [dateStr]: 1 },
|
|
||||||
});
|
|
||||||
|
|
||||||
const statuses = [
|
const statuses = [
|
||||||
makeStatus('a1000000-0000-0000-0000-000000000001', dateStr, {
|
makeStatus('a1000000-0000-0000-0000-000000000001', dateStr, { 'ww000000-0000-0000-0000-000000000099': '325' }),
|
||||||
'ww000000-0000-0000-0000-000000000099': '325',
|
|
||||||
}),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<ExperimentCalendar
|
<ExperimentCalendar experimentId={EXP_ID} template={TEMPLATE} allStatuses={statuses} animals={ANIMALS} />,
|
||||||
experimentId={EXP_ID}
|
|
||||||
template={TEMPLATE}
|
|
||||||
allStatuses={statuses}
|
|
||||||
animals={ANIMALS}
|
|
||||||
/>,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
await waitFor(() => expect(screen.getByTestId(`day-${dateStr}`)).not.toBeDisabled());
|
|
||||||
|
|
||||||
fireEvent.click(screen.getByTestId(`day-${dateStr}`));
|
fireEvent.click(screen.getByTestId(`day-${dateStr}`));
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByText('Rat A')).toBeInTheDocument();
|
const dialog = screen.getByRole('dialog');
|
||||||
expect(screen.getByText('325')).toBeInTheDocument();
|
expect(within(dialog).getByText('Rat A')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not open a modal when clicking a day without data', async () => {
|
it('shows subject value in read-only view after clicking day', async () => {
|
||||||
const today = new Date();
|
const dateStr = todayStr();
|
||||||
const year = today.getFullYear();
|
const statuses = [
|
||||||
const month = String(today.getMonth() + 1).padStart(2, '0');
|
makeStatus('a1000000-0000-0000-0000-000000000001', dateStr, { 'ww000000-0000-0000-0000-000000000099': '325' }),
|
||||||
const day = String(today.getDate()).padStart(2, '0');
|
];
|
||||||
const dateStr = `${year}-${month}-${day}`;
|
render(
|
||||||
|
<ExperimentCalendar experimentId={EXP_ID} template={TEMPLATE} allStatuses={statuses} animals={ANIMALS} />,
|
||||||
experimentsApi.getCalendar.mockResolvedValue({ field: 'vitals', days: {} });
|
);
|
||||||
|
fireEvent.click(screen.getByTestId(`day-${dateStr}`));
|
||||||
|
await waitFor(() => expect(screen.getByText('325')).toBeInTheDocument());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not open a modal when clicking a day without data', () => {
|
||||||
|
const dateStr = todayStr();
|
||||||
render(
|
render(
|
||||||
<ExperimentCalendar experimentId={EXP_ID} template={TEMPLATE} allStatuses={[]} animals={ANIMALS} />,
|
<ExperimentCalendar experimentId={EXP_ID} template={TEMPLATE} allStatuses={[]} animals={ANIMALS} />,
|
||||||
);
|
);
|
||||||
|
|
||||||
await waitFor(() => expect(experimentsApi.getCalendar).toHaveBeenCalled());
|
|
||||||
|
|
||||||
// Day button is disabled — click should not open modal
|
|
||||||
const dayBtn = screen.getByTestId(`day-${dateStr}`);
|
const dayBtn = screen.getByTestId(`day-${dateStr}`);
|
||||||
expect(dayBtn).toBeDisabled();
|
expect(dayBtn).toBeDisabled();
|
||||||
fireEvent.click(dayBtn);
|
fireEvent.click(dayBtn);
|
||||||
|
|
||||||
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows all subjects with data on the selected day', async () => {
|
it('shows all animals in the modal (with and without data)', async () => {
|
||||||
const today = new Date();
|
const dateStr = todayStr();
|
||||||
const year = today.getFullYear();
|
|
||||||
const month = String(today.getMonth() + 1).padStart(2, '0');
|
|
||||||
const day = String(today.getDate()).padStart(2, '0');
|
|
||||||
const dateStr = `${year}-${month}-${day}`;
|
|
||||||
|
|
||||||
experimentsApi.getCalendar.mockResolvedValue({
|
|
||||||
field: 'ww000000-0000-0000-0000-000000000099',
|
|
||||||
days: { [dateStr]: 2 },
|
|
||||||
});
|
|
||||||
|
|
||||||
const statuses = [
|
const statuses = [
|
||||||
makeStatus('a1000000-0000-0000-0000-000000000001', dateStr, { 'ww000000-0000-0000-0000-000000000099': '320' }),
|
makeStatus('a1000000-0000-0000-0000-000000000001', dateStr, { 'ww000000-0000-0000-0000-000000000099': '320' }),
|
||||||
makeStatus('a2000000-0000-0000-0000-000000000002', dateStr, { 'ww000000-0000-0000-0000-000000000099': '310' }),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<ExperimentCalendar
|
<ExperimentCalendar experimentId={EXP_ID} template={TEMPLATE} allStatuses={statuses} animals={ANIMALS} />,
|
||||||
experimentId={EXP_ID}
|
|
||||||
template={TEMPLATE}
|
|
||||||
allStatuses={statuses}
|
|
||||||
animals={ANIMALS}
|
|
||||||
/>,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
await waitFor(() => expect(screen.getByTestId(`day-${dateStr}`)).not.toBeDisabled());
|
|
||||||
fireEvent.click(screen.getByTestId(`day-${dateStr}`));
|
fireEvent.click(screen.getByTestId(`day-${dateStr}`));
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByText('Rat A')).toBeInTheDocument();
|
const dialog = screen.getByRole('dialog');
|
||||||
expect(screen.getByText('Rat B')).toBeInTheDocument();
|
expect(within(dialog).getByText('Rat A')).toBeInTheDocument();
|
||||||
|
expect(within(dialog).getByText('Rat B')).toBeInTheDocument(); // shown even without data
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── Bar chart ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe('bar chart', () => {
|
||||||
|
it('renders bar chart when animals have statuses', () => {
|
||||||
|
const dateStr = todayStr();
|
||||||
|
const statuses = [
|
||||||
|
makeStatus('a1000000-0000-0000-0000-000000000001', dateStr, { 'ww000000-0000-0000-0000-000000000099': '325' }),
|
||||||
|
];
|
||||||
|
render(
|
||||||
|
<ExperimentCalendar experimentId={EXP_ID} template={TEMPLATE} allStatuses={statuses} animals={ANIMALS} />,
|
||||||
|
);
|
||||||
|
expect(screen.getByTestId('bar-chart')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not render bar chart when no animals have statuses', () => {
|
||||||
|
render(
|
||||||
|
<ExperimentCalendar experimentId={EXP_ID} template={TEMPLATE} allStatuses={[]} animals={ANIMALS} />,
|
||||||
|
);
|
||||||
|
expect(screen.queryByTestId('bar-chart')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user