From 44f4db5a6db2bbbb8b2be7d4588562e0f3d30246 Mon Sep 17 00:00:00 2001 From: Timothy Date: Wed, 15 Jul 2026 22:51:23 +0200 Subject: [PATCH] test(web): cover extracted shell contracts Refs #247 Co-Authored-By: OpenAI Codex --- web/src/app/routes.test.tsx | 46 ++++++++++++++++++++++++ web/src/screens/SchedulesScreen.test.tsx | 23 +++++++++--- 2 files changed, 65 insertions(+), 4 deletions(-) create mode 100644 web/src/app/routes.test.tsx diff --git a/web/src/app/routes.test.tsx b/web/src/app/routes.test.tsx new file mode 100644 index 000000000..137428f73 --- /dev/null +++ b/web/src/app/routes.test.tsx @@ -0,0 +1,46 @@ +import { afterEach, describe, expect, it } from 'vitest'; +import { + routeById, + routeFromLocation +} from './routes'; + +describe('app route matching', () => { + afterEach(() => { + window.history.replaceState(null, '', '/app'); + }); + + it('returns one stable route object across sibling allowSubPaths locations', () => { + const playoutsRoute = routeById.get('playouts'); + + window.history.replaceState(null, '', '/app/playouts/20/alternate-schedules'); + const alternateSchedules = routeFromLocation(); + + window.history.replaceState(null, '', '/app/playouts/20/templates'); + const templates = routeFromLocation(); + + expect(alternateSchedules).toBe(playoutsRoute); + expect(templates).toBe(playoutsRoute); + expect(templates).toBe(alternateSchedules); + }); + + it('ignores the query string while preserving the route object', () => { + const searchRoute = routeById.get('search'); + + window.history.replaceState(null, '', '/app/search?query=alpha'); + const firstQuery = routeFromLocation(); + + window.history.replaceState(null, '', '/app/search?query=beta'); + const secondQuery = routeFromLocation(); + + expect(firstQuery).toBe(searchRoute); + expect(secondQuery).toBe(searchRoute); + }); + + it('normalizes a trailing slash and rejects an unowned path', () => { + window.history.replaceState(null, '', '/app/channels/'); + expect(routeFromLocation()).toBe(routeById.get('channels')); + + window.history.replaceState(null, '', '/app/channels/42'); + expect(routeFromLocation()).toBeNull(); + }); +}); diff --git a/web/src/screens/SchedulesScreen.test.tsx b/web/src/screens/SchedulesScreen.test.tsx index b595b33f9..4734fde23 100644 --- a/web/src/screens/SchedulesScreen.test.tsx +++ b/web/src/screens/SchedulesScreen.test.tsx @@ -1,5 +1,6 @@ import { cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { PrimaryActionProvider, usePrimaryActionHandler } from '../primaryAction'; import { SchedulesScreen } from './SchedulesScreen'; function jsonResponse(body: unknown, status = 200): Response { @@ -157,6 +158,11 @@ async function renderReady(options: MockOptions = {}) { return handle; } +function SchedulesPrimaryActionButton() { + const handler = usePrimaryActionHandler('schedules'); + return ; +} + describe('SchedulesScreen — load', () => { it('renders the lineup for the active schedule', async () => { await renderReady(); @@ -438,10 +444,19 @@ describe('SchedulesScreen — schedule CRUD', () => { await waitFor(() => expect(handle.requests.some((r) => r.url === '/api/v1/schedules/99/items')).toBe(true)); }); - it('responds to the TopBar primary-action event by opening create', async () => { - await renderReady(); - window.dispatchEvent(new CustomEvent('ctv:primary-action', { detail: 'schedules' })); - await waitFor(() => expect(screen.getByRole('dialog')).toBeTruthy()); + it('registers the TopBar primary action to open create', async () => { + mockApi(); + render( + + + + + ); + await waitFor(() => expect(screen.getByLabelText('Schedule lineup')).toBeTruthy()); + + fireEvent.click(screen.getByRole('button', { name: 'TopBar action' })); + + expect(screen.getByRole('dialog')).toBeTruthy(); expect(within(screen.getByRole('dialog')).getByLabelText('Name')).toBeTruthy(); });