fix(theme): make dark mode actually apply to the public pages, and test the modals

Tailwind was configured with darkMode: "class" while next-themes writes
data-theme="dark" on the public pages. Every dark: utility on the public
site — 111 of them — compiled to a selector that never matched, which is why
the booking flow stayed white in dark mode. The variant strategy now accepts
both .dark (the panel) and [data-theme="dark"] (the public pages), so neither
provider had to change and 66 dark rules now compile against the real
attribute.

The cancel and reschedule modals get tests, the first component tests in this
repo. They pin the things that would be silently wrong: the penalty comes from
the server before anything is cancelled, the free window says "no penalty"
rather than showing a zero, confirming actually sends the request (the old
dialog's confirm button only closed it), a failed preview does not block the
cancellation, slots are requested with exclude_appointment_uuid so the
patient's own hour is not shown as taken, and the reschedule sends only the
start time because the server owns the duration.

The amount assertion deliberately checks the number and unit rather than the
digit shape — numberToArStyle uses the ar-AE locale and its output depends on
the ICU data in the environment.

lib/getStateInfo.test.js had been failing since before this work: an unknown
host falls through to the representation API, so the test made a real network
call and timed out after five seconds. It mocks lib/req now. The suite is
fully green for the first time: 158 tests.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
hamed
2026-08-01 16:06:49 +03:30
co-authored by Claude Opus 5
parent 4c912dc6c1
commit 519de26839
4 changed files with 214 additions and 1 deletions
@@ -0,0 +1,105 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
vi.mock('@/services/response', () => ({
request: {
getServiceSlotsForReschedule: vi.fn(),
serviceReschedule: vi.fn(),
},
}));
import { request } from '@/services/response';
import RescheduleModal from './RescheduleModal';
const appointment = {
uuid: 'a-1',
doctor: { uuid: 'd-1' },
service_items: [{ uuid: 's-1' }, { uuid: 's-2' }],
};
const slots = {
data: {
total_duration_minutes: 30,
start_times: [
{ start: 1_900_000_000, end: 1_900_001_800, start_time: '09:00', end_time: '09:30' },
{ start: 1_900_003_600, end: 1_900_005_400, start_time: '10:00', end_time: '10:30' },
],
},
};
async function openAndPickFirstDay(user) {
render(<RescheduleModal appointment={appointment} />);
await user.click(screen.getByRole('button', { name: 'جابه‌جایی نوبت' }));
const days = screen.getAllByRole('button').filter((b) => /\d|[۰-۹]/.test(b.textContent));
await user.click(days[0]);
}
describe('مودال جابه‌جایی نوبت سایت', () => {
beforeEach(() => {
request.getServiceSlotsForReschedule.mockResolvedValue(slots);
request.serviceReschedule.mockResolvedValue({ data: {} });
});
/**
* ⭐ بدون `exclude_appointment_uuid`، بیمار ساعت خودش را «پر» می‌بیند و نمی‌تواند
* حتی به همان روز جابه‌جا شود.
*/
it('وقت‌ها را با کنارگذاشتن نوبت فعلی می‌گیرد', async () => {
const user = userEvent.setup();
await openAndPickFirstDay(user);
await waitFor(() => expect(request.getServiceSlotsForReschedule).toHaveBeenCalled());
const [doctorUuid, , appointmentUuid, serviceUuids] =
request.getServiceSlotsForReschedule.mock.calls[0];
expect(doctorUuid).toBe('d-1');
expect(appointmentUuid).toBe('a-1');
expect(serviceUuids).toEqual(['s-1', 's-2']);
});
/** مدت را سرور حساب می‌کند؛ فرانت فقط زمان شروع می‌فرستد. */
it('فقط زمان شروع را می‌فرستد، نه مدت', async () => {
const onDone = vi.fn();
const user = userEvent.setup();
render(<RescheduleModal appointment={appointment} onDone={onDone} />);
await user.click(screen.getByRole('button', { name: 'جابه‌جایی نوبت' }));
const days = screen.getAllByRole('button').filter((b) => /\d|[۰-۹]/.test(b.textContent));
await user.click(days[0]);
const slot = await screen.findByRole('button', { name: '09:00' });
await user.click(slot);
await user.click(screen.getByRole('button', { name: 'جابه‌جا کن' }));
await waitFor(() =>
expect(request.serviceReschedule).toHaveBeenCalledWith('a-1', { start: 1_900_000_000 }),
);
await waitFor(() => expect(onDone).toHaveBeenCalled());
});
it('روزِ بدون وقت آزاد را صریح می‌گوید', async () => {
request.getServiceSlotsForReschedule.mockResolvedValue({ data: { start_times: [] } });
const user = userEvent.setup();
await openAndPickFirstDay(user);
expect(await screen.findByText(/در این روز وقت آزادی نیست/)).toBeInTheDocument();
});
/** ۴۰۹ یعنی همین لحظه کس دیگری گرفت — پیام سرور باید دیده شود. */
it('پیام خطای سرور را نشان می‌دهد', async () => {
request.serviceReschedule.mockRejectedValue(new Error('این بازه زمانی قبلاً رزرو شده است'));
const user = userEvent.setup();
await openAndPickFirstDay(user);
await user.click(await screen.findByRole('button', { name: '09:00' }));
await user.click(screen.getByRole('button', { name: 'جابه‌جا کن' }));
expect(await screen.findByText('این بازه زمانی قبلاً رزرو شده است')).toBeInTheDocument();
});
});