import { page } from 'vitest/browser'; import { describe, it, expect, vi } from 'vitest'; import { render } from 'vitest-browser-svelte'; import WeekSchedule from './WeekSchedule.svelte'; const WEEK_DAYS = [ { key: 'mon', label: 'Monday' }, { key: 'tue', label: 'Tuesday' }, { key: 'wed', label: 'Wednesday' }, { key: 'thu', label: 'Thursday' }, { key: 'fri', label: 'Friday' }, { key: 'sat', label: 'Saturday' }, { key: 'sun', label: 'Sunday' } ]; const block = (overrides: Record = {}) => ({ id: 'b1', day: 'mon', startMinutes: 6 * 60, endMinutes: 8 * 60, dynamicAudio: true, proclamations: [{ id: 'p1', name: 'Peace' }], ...overrides }); function mount(props: Record = {}) { return render(WeekSchedule, { days: WEEK_DAYS, blocks: [], ...props }); } // Click at a fraction of the way down a day column. async function clickColumn(label: string, fraction: number) { const column = document.querySelector( `[aria-label="Schedule grid ${label}"]` ) as HTMLElement | null; if (!column) throw new Error(`no column ${label}`); const rect = column.getBoundingClientRect(); const x = rect.left + rect.width / 2; const y = rect.top + rect.height * fraction; column.dispatchEvent( new PointerEvent('pointerdown', { clientX: x, clientY: y, bubbles: true, pointerId: 1 }) ); column.dispatchEvent( new PointerEvent('pointerup', { clientX: x, clientY: y, bubbles: true, pointerId: 1 }) ); } describe('WeekSchedule', () => { it('renders all seven day columns side by side', async () => { mount(); for (const day of WEEK_DAYS) { await expect.element(page.getByText(day.label)).toBeInTheDocument(); } }); it('renders a single column for a daily schedule', async () => { mount({ days: [{ key: 'daily', label: 'Every day' }] }); await expect.element(page.getByText('Every day')).toBeInTheDocument(); expect(document.querySelectorAll('[aria-label^="Schedule grid"]')).toHaveLength(1); }); it('is horizontally scrollable with a pinned time gutter (mobile-friendly)', async () => { mount(); // The day columns live in a horizontally scrollable row. const scroller = document.querySelector('.overflow-x-auto') as HTMLElement | null; expect(scroller).not.toBeNull(); // The time gutter is sticky so it stays visible while days scroll. const gutter = scroller?.querySelector('[class*="sticky"][class*="left-0"]'); expect(gutter).not.toBeNull(); // Each day column has a minimum basis width so it stays legible on a phone. const columns = scroller?.querySelectorAll('[aria-label^="Schedule grid"]') ?? []; expect(columns.length).toBe(7); }); it('shows short day labels on small screens', async () => { mount({ days: [ { key: 'mon', label: 'Monday', short: 'Mon' }, { key: 'tue', label: 'Tuesday', short: 'Tue' } ] }); // Both the short (small-screen) and long (large-screen) labels exist in // the DOM; CSS decides which is visible at a given width. const text = document.body.textContent ?? ''; expect(text).toContain('Mon'); expect(text).toContain('Monday'); }); it('shows blocks as events', async () => { mount({ blocks: [block()] }); await expect.element(page.getByText('Peace')).toBeInTheDocument(); await expect.element(page.getByLabelText('Block 6:00 AM to 8:00 AM')).toBeInTheDocument(); }); it('opens create on the clicked day', async () => { const onCreate = vi.fn(); mount({ onCreate }); await clickColumn('Monday', 0.25); expect(onCreate).toHaveBeenCalledTimes(1); const [day, start, end] = onCreate.mock.calls[0] as [string, number, number]; expect(day).toBe('mon'); // The handler always yields a sensible minute range (a default-length // block when the click distance is below the snap threshold). expect(Number.isFinite(start)).toBe(true); expect(Number.isFinite(end)).toBe(true); expect(end - start).toBeGreaterThan(0); }); it('opens edit when a block is clicked', async () => { const onEdit = vi.fn(); mount({ blocks: [block()], onEdit }); await page.getByLabelText('Block 6:00 AM to 8:00 AM').click(); expect(onEdit).toHaveBeenCalledTimes(1); expect(onEdit.mock.calls[0][0]).toMatchObject({ id: 'b1', day: 'mon' }); }); });