Files
automaker/apps/ui/tests/projects/overview-dashboard.spec.ts
gsxdsm 0330c70261 Feature: worktree view customization and stability fixes (#805)
* Changes from feature/worktree-view-customization

* Feature: Git sync, set-tracking, and push divergence handling (#796)

* Add quick-add feature with improved workflows (#802)

* Changes from feature/quick-add

* feat: Clarify system prompt and improve error handling across services. Address PR Feedback

* feat: Improve PR description parsing and refactor event handling

* feat: Add context options to pipeline orchestrator initialization

* fix: Deduplicate React and handle CJS interop for use-sync-external-store

Resolve "Cannot read properties of null (reading 'useState')" errors by
deduplicating React/react-dom and ensuring use-sync-external-store is
bundled together with React to prevent CJS packages from resolving to
different React instances.

* Changes from feature/worktree-view-customization

* refactor: Remove unused worktree swap and highlight props

* refactor: Consolidate feature completion logic and improve thinking level defaults

* feat: Increase max turn limit to 10000

- Update DEFAULT_MAX_TURNS from 1000 to 10000 in settings-helpers.ts and agent-executor.ts
- Update MAX_ALLOWED_TURNS from 2000 to 10000 in settings-helpers.ts
- Update UI clamping logic from 2000 to 10000 in app-store.ts
- Update fallback values from 1000 to 10000 in use-settings-sync.ts
- Update default value from 1000 to 10000 in DEFAULT_GLOBAL_SETTINGS
- Update documentation to reflect new range: 1-10000

Allows agents to perform up to 10000 turns for complex feature execution.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>

* feat: Add model resolution, improve session handling, and enhance UI stability

* refactor: Remove unused sync and tracking branch props from worktree components

* feat: Add PR number update functionality to worktrees. Address pr feedback

* feat: Optimize Gemini CLI startup and add tool result tracking

* refactor: Improve error handling and simplify worktree task cleanup

---------

Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
2026-02-23 20:31:25 -08:00

410 lines
14 KiB
TypeScript

/**
* Projects Overview Dashboard End-to-End Test
*
* Tests the multi-project overview dashboard that shows status across all projects.
* This verifies that:
* 1. The overview view can be accessed via the sidebar
* 2. The overview displays aggregate statistics
* 3. Navigation back to dashboard works correctly
* 4. The UI responds to API data correctly
*/
import { test, expect } from '@playwright/test';
import {
setupMockMultipleProjects,
authenticateForTests,
handleLoginScreenIfPresent,
} from '../utils';
/**
* Helper to build overview API response bodies.
* Each test sets `overviewMock` before navigating so the single
* route handler registered in `beforeEach` returns the right data.
*/
function makeOverviewResponse(
overrides: {
projects?: unknown[];
aggregate?: Record<string, unknown>;
recentActivity?: unknown[];
status?: number;
error?: string;
} = {}
) {
const { projects = [], aggregate, recentActivity = [], status = 200, error } = overrides;
const defaultAggregate = {
projectCounts: { total: 0, active: 0, idle: 0, waiting: 0, withErrors: 0, allCompleted: 0 },
featureCounts: { total: 0, pending: 0, running: 0, completed: 0, failed: 0, verified: 0 },
totalUnreadNotifications: 0,
projectsWithAutoModeRunning: 0,
computedAt: new Date().toISOString(),
};
return {
status,
body: error
? JSON.stringify({ error })
: JSON.stringify({
success: true,
projects,
aggregate: aggregate ?? defaultAggregate,
recentActivity,
generatedAt: new Date().toISOString(),
}),
};
}
test.describe('Projects Overview Dashboard', () => {
// Mutable mock response - tests set this before navigating.
// The single route handler in beforeEach reads it on every request.
let overviewMock: { status: number; body: string };
test.beforeEach(async ({ page }) => {
// Start with an empty default
overviewMock = makeOverviewResponse();
// Set up mock projects state
await setupMockMultipleProjects(page, 3);
// Intercept settings API to preserve mock project data and prevent
// the server's settings from overriding our test setup.
await page.route('**/api/settings/global', async (route) => {
const method = route.request().method();
if (method === 'PUT') {
return route.continue();
}
try {
const response = await route.fetch();
const json = await response.json();
if (json.settings) {
json.settings.projects = [
{
id: 'test-project-1',
name: 'Test Project 1',
path: '/mock/test-project-1',
lastOpened: new Date().toISOString(),
},
{
id: 'test-project-2',
name: 'Test Project 2',
path: '/mock/test-project-2',
lastOpened: new Date(Date.now() - 86400000).toISOString(),
},
{
id: 'test-project-3',
name: 'Test Project 3',
path: '/mock/test-project-3',
lastOpened: new Date(Date.now() - 172800000).toISOString(),
},
];
json.settings.currentProjectId = 'test-project-1';
json.settings.setupComplete = true;
json.settings.isFirstRun = false;
}
await route.fulfill({ response, json });
} catch {
// Route may be called after test ends; swallow errors from closed context
}
});
// Mock the initialize-project endpoint for mock paths that don't exist on disk.
await page.route('**/api/project/initialize', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ success: true }),
});
});
// Mock features list for mock project paths (they don't exist on disk)
await page.route('**/api/features/list**', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ success: true, features: [] }),
});
});
// Single overview route handler that reads from the mutable `overviewMock`.
// Tests update `overviewMock` before navigating to control the response.
await page.route('**/api/projects/overview', async (route) => {
await route.fulfill({
status: overviewMock.status,
contentType: 'application/json',
body: overviewMock.body,
});
});
await authenticateForTests(page);
});
test('should navigate to overview from sidebar and display overview UI', async ({ page }) => {
// Use default empty overview mock (set in beforeEach)
// Go to the app
await page.goto('/board');
await page.waitForLoadState('load');
await handleLoginScreenIfPresent(page);
// Wait for the board view to load
await expect(page.locator('[data-testid="board-view"]')).toBeVisible({ timeout: 15000 });
// Expand sidebar if collapsed
const expandSidebarButton = page.locator('button:has-text("Expand sidebar")');
if (await expandSidebarButton.isVisible()) {
await expandSidebarButton.click();
await page.waitForTimeout(300);
}
// Click on the Dashboard link in the sidebar (navigates to /overview)
const overviewLink = page.getByRole('button', { name: 'Dashboard' });
await expect(overviewLink).toBeVisible({ timeout: 5000 });
await overviewLink.click();
// Wait for the overview view to appear
await expect(page.locator('[data-testid="overview-view"]')).toBeVisible({ timeout: 15000 });
// Verify the header is visible with title
await expect(page.getByText('Automaker Dashboard')).toBeVisible({ timeout: 5000 });
// Verify the refresh button is present
await expect(page.getByRole('button', { name: /Refresh/i })).toBeVisible();
// Verify the Open Project and New Project buttons are present in the overview header
const overviewHeader = page.locator('[data-testid="overview-view"] header');
await expect(overviewHeader.getByRole('button', { name: /Open Project/i })).toBeVisible();
await expect(overviewHeader.getByRole('button', { name: /New Project/i })).toBeVisible();
});
test('should display aggregate statistics cards', async ({ page }) => {
overviewMock = makeOverviewResponse({
projects: [
{
projectId: 'test-project-1',
projectName: 'Test Project 1',
projectPath: '/mock/test-project-1',
healthStatus: 'active',
featureCounts: { pending: 2, running: 1, completed: 3, failed: 0, verified: 2 },
totalFeatures: 8,
isAutoModeRunning: true,
unreadNotificationCount: 1,
},
{
projectId: 'test-project-2',
projectName: 'Test Project 2',
projectPath: '/mock/test-project-2',
healthStatus: 'idle',
featureCounts: { pending: 5, running: 0, completed: 10, failed: 1, verified: 8 },
totalFeatures: 24,
isAutoModeRunning: false,
unreadNotificationCount: 0,
},
],
aggregate: {
projectCounts: {
total: 2,
active: 1,
idle: 1,
waiting: 0,
withErrors: 1,
allCompleted: 0,
},
featureCounts: {
total: 32,
pending: 7,
running: 1,
completed: 13,
failed: 1,
verified: 10,
},
totalUnreadNotifications: 1,
projectsWithAutoModeRunning: 1,
computedAt: new Date().toISOString(),
},
recentActivity: [
{
id: 'activity-1',
projectId: 'test-project-1',
projectName: 'Test Project 1',
type: 'feature_completed',
description: 'Feature completed: Add login form',
severity: 'success',
timestamp: new Date().toISOString(),
featureId: 'feature-1',
featureTitle: 'Add login form',
},
],
});
// Navigate directly to overview
await page.goto('/overview');
await page.waitForLoadState('load');
await handleLoginScreenIfPresent(page);
// Wait for the overview view to appear
await expect(page.locator('[data-testid="overview-view"]')).toBeVisible({ timeout: 15000 });
// Verify aggregate stat cards are displayed
// Projects count card
await expect(page.getByText('Projects').first()).toBeVisible({ timeout: 10000 });
// Running features card
await expect(page.getByText('Running').first()).toBeVisible();
// Pending features card
await expect(page.getByText('Pending').first()).toBeVisible();
// Completed features card
await expect(page.getByText('Completed').first()).toBeVisible();
// Auto-mode card
await expect(page.getByText('Auto-mode').first()).toBeVisible();
});
test('should display project status cards', async ({ page }) => {
overviewMock = makeOverviewResponse({
projects: [
{
projectId: 'test-project-1',
projectName: 'Test Project 1',
projectPath: '/mock/test-project-1',
healthStatus: 'active',
featureCounts: { pending: 2, running: 1, completed: 3, failed: 0, verified: 2 },
totalFeatures: 8,
isAutoModeRunning: true,
unreadNotificationCount: 1,
},
],
aggregate: {
projectCounts: {
total: 1,
active: 1,
idle: 0,
waiting: 0,
withErrors: 0,
allCompleted: 0,
},
featureCounts: {
total: 8,
pending: 2,
running: 1,
completed: 3,
failed: 0,
verified: 2,
},
totalUnreadNotifications: 1,
projectsWithAutoModeRunning: 1,
computedAt: new Date().toISOString(),
},
});
// Navigate directly to overview
await page.goto('/overview');
await page.waitForLoadState('load');
await handleLoginScreenIfPresent(page);
// Wait for the overview view to appear
await expect(page.locator('[data-testid="overview-view"]')).toBeVisible({ timeout: 15000 });
// Verify project status card is displayed
const projectCard = page.locator('[data-testid="project-status-card-test-project-1"]');
await expect(projectCard).toBeVisible({ timeout: 10000 });
// Verify project name is displayed
await expect(projectCard.getByText('Test Project 1')).toBeVisible();
// Verify the Active status badge (use .first() to avoid strict mode violation due to "Auto-mode active" also containing "active")
await expect(projectCard.getByText('Active').first()).toBeVisible();
// Verify auto-mode indicator is shown
await expect(projectCard.getByText('Auto-mode active')).toBeVisible();
});
test('should navigate to board when clicking on a project card', async ({ page }) => {
overviewMock = makeOverviewResponse({
projects: [
{
projectId: 'test-project-1',
projectName: 'Test Project 1',
projectPath: '/mock/test-project-1',
healthStatus: 'idle',
featureCounts: { pending: 0, running: 0, completed: 0, failed: 0, verified: 0 },
totalFeatures: 0,
isAutoModeRunning: false,
unreadNotificationCount: 0,
},
],
aggregate: {
projectCounts: {
total: 1,
active: 0,
idle: 1,
waiting: 0,
withErrors: 0,
allCompleted: 0,
},
featureCounts: {
total: 0,
pending: 0,
running: 0,
completed: 0,
failed: 0,
verified: 0,
},
totalUnreadNotifications: 0,
projectsWithAutoModeRunning: 0,
computedAt: new Date().toISOString(),
},
});
// Navigate directly to overview
await page.goto('/overview');
await page.waitForLoadState('load');
await handleLoginScreenIfPresent(page);
// Wait for the overview view to appear
await expect(page.locator('[data-testid="overview-view"]')).toBeVisible({ timeout: 15000 });
// Verify project card is displayed (clicking it would navigate to board, but requires more mocking)
const projectCard = page.locator('[data-testid="project-status-card-test-project-1"]');
await expect(projectCard).toBeVisible({ timeout: 10000 });
});
test('should display empty state when no projects exist', async ({ page }) => {
// Default overviewMock already returns empty projects - no change needed
// Navigate directly to overview
await page.goto('/overview');
await page.waitForLoadState('load');
await handleLoginScreenIfPresent(page);
// Wait for the overview view to appear
await expect(page.locator('[data-testid="overview-view"]')).toBeVisible({ timeout: 15000 });
// Verify empty state message
await expect(page.getByText('No projects yet')).toBeVisible({ timeout: 10000 });
await expect(page.getByText('Create or open a project to get started')).toBeVisible();
});
test('should show error state when API fails', async ({ page }) => {
overviewMock = makeOverviewResponse({
status: 500,
error: 'Internal server error',
});
// Navigate directly to overview
await page.goto('/overview');
await page.waitForLoadState('load');
await handleLoginScreenIfPresent(page);
// Wait for the overview view to appear
await expect(page.locator('[data-testid="overview-view"]')).toBeVisible({ timeout: 15000 });
// Verify error state message
await expect(page.getByText('Failed to load overview')).toBeVisible({ timeout: 10000 });
// Verify the "Try again" button is visible
await expect(page.getByRole('button', { name: /Try again/i })).toBeVisible();
});
});