P4-1: read-only Projects + Tasks SPA pages + dev-port pins #1153

Merged
jason.woltje merged 2 commits from feat/webui-p4-1 into next 2026-08-11 04:00:37 +00:00
2 changed files with 23 additions and 4 deletions
Showing only changes of commit 29db24210c - Show all commits
+21 -2
View File
@@ -134,17 +134,36 @@ describe('ProjectDetailPage', () => {
expect(container.querySelector('[role="dialog"]')).toBeNull();
});
it('renders a visible alert when any project detail request fails and lets the user navigate back', async () => {
it('renders the project with an empty missions tab when the missions request fails', async () => {
apiMock
.mockResolvedValueOnce(projectFixtures[0])
.mockRejectedValueOnce(new Error('Missions request failed'))
.mockResolvedValueOnce(taskFixtures.filter((task) => task.projectId === 'project-1'));
await renderProjectDetailPage();
expect(container.textContent).toContain('Mosaic Stack');
expect(container.querySelector('[role="alert"]')).toBeNull();
await act(async () => {
clickButtonByText('Missions (0)');
});
expect(container.textContent).toContain('No missions for this project');
});
it('renders a visible alert when the project request fails and lets the user navigate back', async () => {
apiMock
.mockRejectedValueOnce(new Error('Project request failed'))
.mockResolvedValueOnce(missionFixtures)
.mockResolvedValueOnce(taskFixtures.filter((task) => task.projectId === 'project-1'));
const router = await renderProjectDetailPage();
const alert = container.querySelector('[role="alert"]');
expect(alert).toBeTruthy();
expect(alert?.textContent).toContain('Missions request failed');
expect(alert?.textContent).toContain('Project request failed');
expect(container.textContent).not.toContain('Mosaic Stack');
await act(async () => {
clickButtonByText('Back to projects');
+2 -2
View File
@@ -76,8 +76,8 @@ export function ProjectDetailPage(): ReactElement {
void Promise.all([
api<Project>('/api/projects/' + id),
api<Mission[]>('/api/missions'),
api<Task[]>('/api/tasks?projectId=' + id),
api<Mission[]>('/api/missions').catch(() => [] as Mission[]),
api<Task[]>('/api/tasks?projectId=' + id).catch(() => [] as Task[]),
])
.then(([loadedProject, allMissions, loadedTasks]) => {
if (cancelled) return;