mirror of
https://github.com/aljazceru/claude-code-viewer.git
synced 2025-12-24 08:44:21 +01:00
refactor: replace e2e tests with screenshot capture system
- Delete all Playwright test files from e2e/tests/
- Create new screenshot capture system in e2e/captureSnapshot/
- Add unified execution script (index.ts) to run all captures
- Implement state-based snapshot organization:
- snapshots/{page}/{state}/{device}.png structure
- Multiple UI states per page (default, filters-expanded, sidebar-open, etc.)
- Support real session UUIDs and comprehensive page coverage
- Enable single-command execution: npx tsx e2e/captureSnapshot/index.ts
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
211
e2e/README.md
211
e2e/README.md
@@ -1,211 +0,0 @@
|
||||
# End-to-End Visual Regression Testing
|
||||
|
||||
This directory contains Playwright-based visual regression tests for the Claude Code Viewer application.
|
||||
|
||||
## Overview
|
||||
|
||||
The VRT setup uses Playwright to capture screenshots of key pages and components, comparing them against baseline images to detect visual regressions. Tests run against mock data to ensure consistent results.
|
||||
|
||||
## Test Structure
|
||||
|
||||
### Test Files
|
||||
|
||||
- `projects-page.spec.ts` - Tests the main projects listing page
|
||||
- `project-detail-page.spec.ts` - Tests individual project pages with session lists
|
||||
- `session-detail-page.spec.ts` - Tests conversation view pages
|
||||
- `modal-components.spec.ts` - Tests modal dialogs and overlays
|
||||
- `error-states.spec.ts` - Tests error pages and loading states
|
||||
|
||||
### Mock Data
|
||||
|
||||
Tests use the `mock-global-claude-dir` directory which contains sample projects and conversation data. This ensures consistent test results by avoiding dependency on real user data.
|
||||
|
||||
## Running Tests
|
||||
|
||||
### Prerequisites
|
||||
|
||||
```bash
|
||||
# Install dependencies including Playwright
|
||||
pnpm install
|
||||
|
||||
# Install Playwright browsers (first time only)
|
||||
npx playwright install
|
||||
|
||||
# Start the development server on localhost:4000
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
### Test Commands
|
||||
|
||||
```bash
|
||||
# Run all VRT tests (requires server running on localhost:4000)
|
||||
pnpm test:e2e
|
||||
|
||||
# Run only the main working tests on Chrome
|
||||
pnpm test:e2e:new
|
||||
|
||||
# Run tests with UI (interactive mode)
|
||||
npx playwright test --ui
|
||||
|
||||
# Run tests with browser visible (headed mode)
|
||||
npx playwright test --headed
|
||||
|
||||
# Update screenshot baselines
|
||||
npx playwright test --update-snapshots
|
||||
|
||||
# Run only on Chrome (fastest for development)
|
||||
pnpm test:e2e:chrome
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
|
||||
Tests run against a server on `localhost:4000`. The server should be configured to use mock data from `mock-global-claude-dir` for consistent test results.
|
||||
|
||||
## Test Coverage
|
||||
|
||||
### Pages Tested
|
||||
|
||||
1. **Projects Page** (`/projects`)
|
||||
- Empty state
|
||||
- Projects list with data
|
||||
- Responsive layouts (mobile, tablet, desktop)
|
||||
|
||||
2. **Project Detail Page** (`/projects/:projectId`)
|
||||
- Session cards display
|
||||
- Filter panel (collapsed/expanded)
|
||||
- Navigation elements
|
||||
- Empty sessions state
|
||||
|
||||
3. **Session Detail Page** (`/projects/:projectId/sessions/:sessionId`)
|
||||
- Conversation messages
|
||||
- Sidebar (desktop) and mobile menu
|
||||
- Task states (running, paused)
|
||||
- Resume chat interface
|
||||
|
||||
4. **Modal Components**
|
||||
- New chat modal
|
||||
- Diff comparison modal
|
||||
- Sidechain conversation modal
|
||||
- Mobile responsiveness
|
||||
|
||||
5. **Error States**
|
||||
- 404 pages
|
||||
- Invalid project/session IDs
|
||||
- Network error handling
|
||||
- Loading states
|
||||
|
||||
### Responsive Testing
|
||||
|
||||
Tests include coverage for:
|
||||
- Mobile (375x667) - iPhone SE
|
||||
- Tablet (768x1024) - iPad
|
||||
- Desktop (1920x1080) - Standard desktop
|
||||
|
||||
## Configuration
|
||||
|
||||
### Playwright Config
|
||||
|
||||
The `playwright.config.ts` file includes:
|
||||
- Multi-browser testing (Chrome, Firefox, Safari, Mobile)
|
||||
- Local dev server startup with mock data
|
||||
- Screenshot comparison settings
|
||||
- Retry and parallel execution settings
|
||||
|
||||
### Test Utilities
|
||||
|
||||
The `utils/test-utils.ts` file provides:
|
||||
- `waitForAppLoad()` - Ensures full app hydration
|
||||
- `takeFullPageScreenshot()` - Captures consistent full-page images
|
||||
- `testViewport()` - Tests responsive layouts
|
||||
- Animation disabling for consistent screenshots
|
||||
|
||||
## Screenshot Baseline Management
|
||||
|
||||
### Initial Setup
|
||||
|
||||
When running tests for the first time:
|
||||
|
||||
```bash
|
||||
# Generate initial baselines
|
||||
pnpm test:e2e:update
|
||||
```
|
||||
|
||||
### Updating Baselines
|
||||
|
||||
After intentional UI changes:
|
||||
|
||||
```bash
|
||||
# Update all screenshots
|
||||
pnpm test:e2e:update
|
||||
|
||||
# Update specific test screenshots
|
||||
npx playwright test projects-page.spec.ts --update-snapshots
|
||||
```
|
||||
|
||||
### Reviewing Changes
|
||||
|
||||
Use Playwright's test results viewer:
|
||||
|
||||
```bash
|
||||
# Open test results with visual diffs
|
||||
npx playwright show-report
|
||||
```
|
||||
|
||||
|
||||
## CI/CD Integration
|
||||
|
||||
The tests are configured for CI environments:
|
||||
- Retry failed tests up to 2 times
|
||||
- Use single worker in CI for consistency
|
||||
- Generate HTML reports for failure analysis
|
||||
- Screenshots are automatically uploaded as artifacts
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Flaky screenshots due to animations**
|
||||
- Tests disable animations via CSS injection
|
||||
- Use `waitForTimeout()` for custom timing
|
||||
|
||||
2. **Inconsistent timestamps**
|
||||
- Timestamps are hidden via CSS in test utilities
|
||||
- Use `data-testid` attributes for reliable element selection
|
||||
|
||||
3. **Mock data changes**
|
||||
- Tests use fixed mock data in `mock-global-claude-dir`
|
||||
- Ensure mock data remains stable between test runs
|
||||
|
||||
4. **Browser differences**
|
||||
- Tests run on multiple browsers
|
||||
- Use Playwright's built-in element waiting
|
||||
- Avoid browser-specific CSS or JavaScript
|
||||
|
||||
### Debug Mode
|
||||
|
||||
For troubleshooting test failures:
|
||||
|
||||
```bash
|
||||
# Run with browser visible and debug mode
|
||||
npx playwright test --debug
|
||||
|
||||
# Run specific test in headed mode
|
||||
npx playwright test projects-page.spec.ts --headed
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Stable Selectors**: Use `data-testid` attributes for reliable element selection
|
||||
2. **Wait Strategies**: Always wait for content to load before screenshots
|
||||
3. **Animation Handling**: Disable animations for consistent visual comparisons
|
||||
4. **Viewport Testing**: Test all responsive breakpoints
|
||||
5. **Error Coverage**: Include error states and edge cases
|
||||
6. **Mock Data**: Use consistent, fixed data for reproducible results
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- Add performance testing metrics
|
||||
- Include accessibility testing
|
||||
- Add cross-platform testing (Windows, Linux)
|
||||
- Integrate with visual testing services
|
||||
- Add automated baseline updates on approved changes
|
||||
40
e2e/captureSnapshot/error-pages.ts
Normal file
40
e2e/captureSnapshot/error-pages.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { resolve } from "node:path";
|
||||
import { withPlaywright } from "../utils/withPlaywright";
|
||||
import { testDevices } from "../testDevices";
|
||||
|
||||
// Different error scenarios to capture
|
||||
const errorScenarios = [
|
||||
{
|
||||
name: "404",
|
||||
url: "http://localhost:3400/non-existent-page"
|
||||
},
|
||||
{
|
||||
name: "invalid-project",
|
||||
url: "http://localhost:3400/projects/non-existent-project"
|
||||
},
|
||||
{
|
||||
name: "invalid-session",
|
||||
url: "http://localhost:3400/projects/sample-project/sessions/non-existent-session"
|
||||
}
|
||||
];
|
||||
|
||||
for (const scenario of errorScenarios) {
|
||||
for (const { device, name } of testDevices) {
|
||||
await withPlaywright(
|
||||
async ({ context, cleanUp }) => {
|
||||
const page = await context.newPage();
|
||||
await page.goto(scenario.url);
|
||||
await page.screenshot({
|
||||
path: resolve("e2e", "snapshots", "errors", `${scenario.name}_${name}.png`),
|
||||
fullPage: true,
|
||||
});
|
||||
await cleanUp();
|
||||
},
|
||||
{
|
||||
contextOptions: {
|
||||
...device,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
23
e2e/captureSnapshot/home.ts
Normal file
23
e2e/captureSnapshot/home.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { resolve } from "node:path";
|
||||
import { withPlaywright } from "../utils/withPlaywright";
|
||||
import { testDevices } from "../testDevices";
|
||||
|
||||
for (const { device, name } of testDevices) {
|
||||
await withPlaywright(
|
||||
async ({ context, cleanUp }) => {
|
||||
const page = await context.newPage();
|
||||
await page.goto("http://localhost:3400/");
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.screenshot({
|
||||
path: resolve("e2e", "snapshots", "root", `${name}.png`),
|
||||
fullPage: true,
|
||||
});
|
||||
await cleanUp();
|
||||
},
|
||||
{
|
||||
contextOptions: {
|
||||
...device,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
38
e2e/captureSnapshot/index.ts
Normal file
38
e2e/captureSnapshot/index.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { execSync } from "node:child_process";
|
||||
import { resolve, dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
const scripts = [
|
||||
"home.ts",
|
||||
"projects.ts",
|
||||
"project-detail.ts",
|
||||
"session-detail.ts",
|
||||
"error-pages.ts"
|
||||
];
|
||||
|
||||
async function captureAllSnapshots() {
|
||||
console.log("🚀 Starting screenshot capture for all pages...\n");
|
||||
|
||||
for (const script of scripts) {
|
||||
const scriptPath = resolve(__dirname, script);
|
||||
console.log(`📸 Capturing: ${script.replace('.ts', '')}...`);
|
||||
|
||||
try {
|
||||
// Execute each script using tsx
|
||||
execSync(`npx tsx "${scriptPath}"`, {
|
||||
stdio: 'inherit',
|
||||
cwd: resolve(__dirname, "..", "..")
|
||||
});
|
||||
console.log(`✅ Completed: ${script.replace('.ts', '')}\n`);
|
||||
} catch (error) {
|
||||
console.error(`❌ Failed: ${script.replace('.ts', '')} - ${error.message}\n`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log("🎉 All screenshot captures completed!");
|
||||
}
|
||||
|
||||
captureAllSnapshots().catch(console.error);
|
||||
49
e2e/captureSnapshot/project-detail.ts
Normal file
49
e2e/captureSnapshot/project-detail.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { resolve } from "node:path";
|
||||
import { withPlaywright } from "../utils/withPlaywright";
|
||||
import { testDevices } from "../testDevices";
|
||||
|
||||
// Test different UI states on project detail page
|
||||
const testStates = [
|
||||
{ name: 'default', action: null },
|
||||
{ name: 'filters-expanded', action: async (page) => {
|
||||
const filterToggle = page.locator('[data-testid="filter-toggle"], button:has-text("Filters"), .filter-toggle');
|
||||
if (await filterToggle.isVisible()) {
|
||||
await filterToggle.click();
|
||||
await page.waitForTimeout(300);
|
||||
}
|
||||
}},
|
||||
{ name: 'new-chat-modal', action: async (page) => {
|
||||
const newChatButton = page.locator('button:has-text("New Chat"), [data-testid="new-chat"], .new-chat-button');
|
||||
if (await newChatButton.isVisible()) {
|
||||
await newChatButton.click();
|
||||
await page.waitForTimeout(300);
|
||||
}
|
||||
}}
|
||||
];
|
||||
|
||||
for (const state of testStates) {
|
||||
for (const { device, name } of testDevices) {
|
||||
await withPlaywright(
|
||||
async ({ context, cleanUp }) => {
|
||||
const page = await context.newPage();
|
||||
await page.goto("http://localhost:3400/projects/sample-project");
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
if (state.action) {
|
||||
await state.action(page);
|
||||
}
|
||||
|
||||
await page.screenshot({
|
||||
path: resolve("e2e", "snapshots", "project-detail", state.name, `${name}.png`),
|
||||
fullPage: true,
|
||||
});
|
||||
await cleanUp();
|
||||
},
|
||||
{
|
||||
contextOptions: {
|
||||
...device,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
39
e2e/captureSnapshot/projects.ts
Normal file
39
e2e/captureSnapshot/projects.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { resolve } from "node:path";
|
||||
import { withPlaywright } from "../utils/withPlaywright";
|
||||
import { testDevices } from "../testDevices";
|
||||
|
||||
// Test different states on projects page
|
||||
const testStates = [
|
||||
{ name: 'default', action: null },
|
||||
{ name: 'empty', action: async (page) => {
|
||||
// Check for empty state (this will capture whatever state exists)
|
||||
await page.waitForTimeout(500);
|
||||
}}
|
||||
];
|
||||
|
||||
for (const state of testStates) {
|
||||
for (const { device, name } of testDevices) {
|
||||
await withPlaywright(
|
||||
async ({ context, cleanUp }) => {
|
||||
const page = await context.newPage();
|
||||
await page.goto("http://localhost:3400/projects");
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
if (state.action) {
|
||||
await state.action(page);
|
||||
}
|
||||
|
||||
await page.screenshot({
|
||||
path: resolve("e2e", "snapshots", "projects", state.name, `${name}.png`),
|
||||
fullPage: true,
|
||||
});
|
||||
await cleanUp();
|
||||
},
|
||||
{
|
||||
contextOptions: {
|
||||
...device,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
52
e2e/captureSnapshot/session-detail.ts
Normal file
52
e2e/captureSnapshot/session-detail.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { resolve } from "node:path";
|
||||
import { withPlaywright } from "../utils/withPlaywright";
|
||||
import { testDevices } from "../testDevices";
|
||||
|
||||
// Multiple session IDs to capture different session detail pages
|
||||
const sessionIds = [
|
||||
"1af7fc5e-8455-4414-9ccd-011d40f70b2a",
|
||||
"5c0375b4-57a5-4f26-b12d-d022ee4e51b7",
|
||||
"fe5e1c67-53e7-4862-81ae-d0e013e3270b"
|
||||
];
|
||||
|
||||
// Test different sidebar states
|
||||
const testStates = [
|
||||
{ name: 'default', action: null },
|
||||
{ name: 'sidebar-open', action: async (page) => {
|
||||
const menuButton = page.locator('[data-testid="menu-button"], button:has-text("Menu"), .menu-toggle, .hamburger');
|
||||
if (await menuButton.isVisible()) {
|
||||
await menuButton.click();
|
||||
await page.waitForTimeout(300);
|
||||
}
|
||||
}}
|
||||
];
|
||||
|
||||
for (const sessionId of sessionIds) {
|
||||
for (const state of testStates) {
|
||||
for (const { device, name } of testDevices) {
|
||||
await withPlaywright(
|
||||
async ({ context, cleanUp }) => {
|
||||
const page = await context.newPage();
|
||||
await page.goto(`http://localhost:3400/projects/sample-project/sessions/${sessionId}`);
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
if (state.action) {
|
||||
await state.action(page);
|
||||
}
|
||||
|
||||
// Create separate directories for each session
|
||||
await page.screenshot({
|
||||
path: resolve("e2e", "snapshots", "session-detail", state.name, `${sessionId}_${name}.png`),
|
||||
fullPage: true,
|
||||
});
|
||||
await cleanUp();
|
||||
},
|
||||
{
|
||||
contextOptions: {
|
||||
...device,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import { takeScreenshots } from "../utils/snapshot-utils.js";
|
||||
|
||||
const BASE_URL = "http://localhost:3400";
|
||||
|
||||
async function main() {
|
||||
// Error state screenshots
|
||||
await takeScreenshots(`${BASE_URL}/non-existent-page`, "error-404-page");
|
||||
await takeScreenshots(`${BASE_URL}/projects/non-existent-project`, "error-invalid-project");
|
||||
await takeScreenshots(`${BASE_URL}/projects/sample-project/sessions/non-existent-session`, "error-invalid-session");
|
||||
|
||||
console.log("✅ Error states screenshots completed");
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -1,49 +0,0 @@
|
||||
import { resolve } from "node:path";
|
||||
import { withPlaywright } from "../utils/withPlaywright.js";
|
||||
import { testDevices } from "../testDevices.js";
|
||||
|
||||
const BASE_URL = "http://localhost:3400";
|
||||
const projectId = "sample-project";
|
||||
const sessionId = "1af7fc5e-8455-4414-9ccd-011d40f70b2a";
|
||||
|
||||
async function main() {
|
||||
// Modal components screenshots - requires interaction
|
||||
for (const { device, name } of testDevices) {
|
||||
await withPlaywright(
|
||||
async ({ context, cleanUp }) => {
|
||||
const page = await context.newPage();
|
||||
|
||||
try {
|
||||
// Test new chat modal
|
||||
await page.goto(`${BASE_URL}/projects/${projectId}`);
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Try to click new chat button
|
||||
const newChatButton = page.locator('button:has-text("New Chat"), button:has-text("Start"), [data-testid="new-chat-button"]').first();
|
||||
if (await newChatButton.isVisible()) {
|
||||
await newChatButton.click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
await page.screenshot({
|
||||
path: resolve("e2e", "snapshots", "new-chat-modal", `${name}.png`),
|
||||
fullPage: true,
|
||||
});
|
||||
}
|
||||
|
||||
} finally {
|
||||
await cleanUp();
|
||||
}
|
||||
},
|
||||
{
|
||||
contextOptions: {
|
||||
...device,
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
console.log("✅ Modal components screenshots completed");
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -1,12 +0,0 @@
|
||||
import { takeScreenshots } from "../utils/snapshot-utils.js";
|
||||
|
||||
const BASE_URL = "http://localhost:3400";
|
||||
const projectId = "sample-project";
|
||||
|
||||
async function main() {
|
||||
// Project detail page screenshots
|
||||
await takeScreenshots(`${BASE_URL}/projects/${projectId}`, "project-detail-page");
|
||||
console.log("✅ Project detail page screenshots completed");
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -1,102 +0,0 @@
|
||||
import { test } from "@playwright/test";
|
||||
import { navigateAndWait, takeFullPageScreenshot } from "../utils/test-utils";
|
||||
|
||||
test.describe("Project Detail Page Visual Regression", () => {
|
||||
const projectId = "sample-project"; // Using the mock project ID
|
||||
|
||||
test("should render project detail page correctly on desktop", async ({
|
||||
page,
|
||||
}) => {
|
||||
await navigateAndWait(page, `/projects/${projectId}`);
|
||||
await takeFullPageScreenshot(page, "project-detail-page-desktop");
|
||||
});
|
||||
|
||||
test("should render with filter panel collapsed", async ({ page }) => {
|
||||
await navigateAndWait(page, `/projects/${projectId}`);
|
||||
|
||||
// Ensure filter panel is collapsed
|
||||
const filterToggle = page
|
||||
.locator('[data-testid="filter-toggle"], button:has-text("Filter")')
|
||||
.first();
|
||||
if (await filterToggle.isVisible()) {
|
||||
const isExpanded = await filterToggle.getAttribute("aria-expanded");
|
||||
if (isExpanded === "true") {
|
||||
await filterToggle.click();
|
||||
await page.waitForTimeout(300); // Wait for animation
|
||||
}
|
||||
}
|
||||
|
||||
await takeFullPageScreenshot(page, "project-detail-filter-collapsed");
|
||||
});
|
||||
|
||||
test("should render with filter panel expanded", async ({ page }) => {
|
||||
await navigateAndWait(page, `/projects/${projectId}`);
|
||||
|
||||
// Try to expand filter panel
|
||||
const filterToggle = page
|
||||
.locator('[data-testid="filter-toggle"], button:has-text("Filter")')
|
||||
.first();
|
||||
if (await filterToggle.isVisible()) {
|
||||
const isExpanded = await filterToggle.getAttribute("aria-expanded");
|
||||
if (isExpanded !== "true") {
|
||||
await filterToggle.click();
|
||||
await page.waitForTimeout(300); // Wait for animation
|
||||
}
|
||||
await takeFullPageScreenshot(page, "project-detail-filter-expanded");
|
||||
}
|
||||
});
|
||||
|
||||
test("should render session cards correctly", async ({ page }) => {
|
||||
await navigateAndWait(page, `/projects/${projectId}`);
|
||||
|
||||
// Wait for session cards to load
|
||||
const sessionCards = page.locator('[data-testid="session-card"]').first();
|
||||
await sessionCards
|
||||
.waitFor({ state: "visible", timeout: 5000 })
|
||||
.catch(() => {
|
||||
// If no session cards, that's fine - we'll test empty state
|
||||
});
|
||||
|
||||
await takeFullPageScreenshot(page, "project-detail-sessions");
|
||||
});
|
||||
|
||||
test("should render back navigation", async ({ page }) => {
|
||||
await navigateAndWait(page, `/projects/${projectId}`);
|
||||
|
||||
// Check for back button
|
||||
const backButton = page
|
||||
.locator(
|
||||
'button:has-text("Back"), a:has-text("Back"), [data-testid="back-button"]',
|
||||
)
|
||||
.first();
|
||||
if (await backButton.isVisible()) {
|
||||
await takeFullPageScreenshot(page, "project-detail-navigation");
|
||||
}
|
||||
});
|
||||
|
||||
test("should render new chat button", async ({ page }) => {
|
||||
await navigateAndWait(page, `/projects/${projectId}`);
|
||||
|
||||
// Look for new chat button
|
||||
const newChatButton = page
|
||||
.locator(
|
||||
'button:has-text("New Chat"), button:has-text("Start"), [data-testid="new-chat-button"]',
|
||||
)
|
||||
.first();
|
||||
if (await newChatButton.isVisible()) {
|
||||
await takeFullPageScreenshot(page, "project-detail-new-chat");
|
||||
}
|
||||
});
|
||||
|
||||
test("should handle empty sessions state", async ({ page }) => {
|
||||
// Test with a project that might have no sessions
|
||||
await navigateAndWait(page, `/projects/${projectId}`);
|
||||
|
||||
const sessionCount = await page
|
||||
.locator('[data-testid="session-card"]')
|
||||
.count();
|
||||
if (sessionCount === 0) {
|
||||
await takeFullPageScreenshot(page, "project-detail-empty-sessions");
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,11 +0,0 @@
|
||||
import { takeScreenshots } from "../utils/snapshot-utils.js";
|
||||
|
||||
const BASE_URL = "http://localhost:3400";
|
||||
|
||||
async function main() {
|
||||
// Projects page screenshots
|
||||
await takeScreenshots(`${BASE_URL}/projects`, "projects-page");
|
||||
console.log("✅ Projects page screenshots completed");
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -1,47 +0,0 @@
|
||||
import { test } from "@playwright/test";
|
||||
import { navigateAndWait, takeFullPageScreenshot } from "../utils/test-utils";
|
||||
|
||||
test.describe("Projects Page Visual Regression", () => {
|
||||
test("should render projects page correctly on desktop", async ({ page }) => {
|
||||
await navigateAndWait(page, "/projects");
|
||||
await takeFullPageScreenshot(page, "projects-page-desktop");
|
||||
});
|
||||
|
||||
test("should render empty state correctly", async ({ page }) => {
|
||||
// This test assumes the mock data might have empty projects
|
||||
// or we could add a query parameter to simulate empty state
|
||||
await navigateAndWait(page, "/projects");
|
||||
|
||||
// Check if we have the empty state or projects
|
||||
const hasProjects =
|
||||
(await page.locator('[data-testid="project-card"]').count()) > 0;
|
||||
|
||||
if (!hasProjects) {
|
||||
await takeFullPageScreenshot(page, "projects-page-empty-state");
|
||||
}
|
||||
});
|
||||
|
||||
test("should render projects list with data", async ({ page }) => {
|
||||
await navigateAndWait(page, "/projects");
|
||||
|
||||
// Wait for any project cards to appear
|
||||
const projectCards = page.locator('[data-testid="project-card"]').first();
|
||||
await projectCards
|
||||
.waitFor({ state: "visible", timeout: 5000 })
|
||||
.catch(() => {
|
||||
// If no project cards, that's fine - we'll test empty state
|
||||
});
|
||||
|
||||
await takeFullPageScreenshot(page, "projects-page-with-data");
|
||||
});
|
||||
|
||||
test("should render header correctly", async ({ page }) => {
|
||||
await navigateAndWait(page, "/projects");
|
||||
|
||||
// Focus on the header area
|
||||
const header = page.locator("header, .header, h1").first();
|
||||
if (await header.isVisible()) {
|
||||
await takeFullPageScreenshot(page, "projects-page-header");
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,28 +0,0 @@
|
||||
import { takeScreenshots } from "../utils/snapshot-utils.js";
|
||||
|
||||
const BASE_URL = "http://localhost:3400";
|
||||
const projectId = "sample-project";
|
||||
|
||||
async function main() {
|
||||
const sessionId = "1af7fc5e-8455-4414-9ccd-011d40f70b2a";
|
||||
|
||||
// Session detail page screenshots
|
||||
await takeScreenshots(`${BASE_URL}/projects/${projectId}/sessions/${sessionId}`, "session-detail-full");
|
||||
|
||||
// Test other session IDs as well
|
||||
const sessionIds = [
|
||||
"5c0375b4-57a5-4f26-b12d-d022ee4e51b7",
|
||||
"fe5e1c67-53e7-4862-81ae-d0e013e3270b"
|
||||
];
|
||||
|
||||
for (const sessionId of sessionIds) {
|
||||
await takeScreenshots(
|
||||
`${BASE_URL}/projects/${projectId}/sessions/${sessionId}`,
|
||||
`session-detail-${sessionId.substring(0, 8)}`
|
||||
);
|
||||
}
|
||||
|
||||
console.log("✅ Session detail page screenshots completed");
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -1,142 +0,0 @@
|
||||
import { test } from "@playwright/test";
|
||||
import { navigateAndWait, takeFullPageScreenshot } from "../utils/test-utils";
|
||||
|
||||
test.describe("Session Detail Page Visual Regression", () => {
|
||||
const projectId = "sample-project";
|
||||
const sessionId = "1af7fc5e-8455-4414-9ccd-011d40f70b2a"; // Using one of the mock session IDs
|
||||
|
||||
test("should render session detail page correctly on desktop", async ({
|
||||
page,
|
||||
}) => {
|
||||
await navigateAndWait(page, `/projects/${projectId}/sessions/${sessionId}`);
|
||||
await takeFullPageScreenshot(page, "session-detail-page-desktop");
|
||||
});
|
||||
|
||||
test("should render sidebar correctly on desktop", async ({ page }) => {
|
||||
await page.setViewportSize({ width: 1920, height: 1080 });
|
||||
await navigateAndWait(page, `/projects/${projectId}/sessions/${sessionId}`);
|
||||
|
||||
// Wait for sidebar to be visible
|
||||
const sidebar = page
|
||||
.locator('[data-testid="session-sidebar"], .sidebar, aside')
|
||||
.first();
|
||||
await sidebar.waitFor({ state: "visible", timeout: 5000 }).catch(() => {
|
||||
// Sidebar might not be present on this design
|
||||
});
|
||||
|
||||
await takeFullPageScreenshot(page, "session-detail-with-sidebar");
|
||||
});
|
||||
|
||||
test("should render mobile sidebar overlay", async ({ page }) => {
|
||||
await page.setViewportSize({ width: 375, height: 667 });
|
||||
await navigateAndWait(page, `/projects/${projectId}/sessions/${sessionId}`);
|
||||
|
||||
// Try to open mobile sidebar
|
||||
const menuButton = page
|
||||
.locator(
|
||||
'[data-testid="mobile-menu"], button[aria-label*="menu"], .hamburger',
|
||||
)
|
||||
.first();
|
||||
if (await menuButton.isVisible()) {
|
||||
await menuButton.click();
|
||||
await page.waitForTimeout(300); // Wait for animation
|
||||
await takeFullPageScreenshot(page, "session-detail-mobile-sidebar-open");
|
||||
}
|
||||
});
|
||||
|
||||
test("should render conversation messages", async ({ page }) => {
|
||||
await navigateAndWait(page, `/projects/${projectId}/sessions/${sessionId}`);
|
||||
|
||||
// Wait for messages to load
|
||||
const messages = page
|
||||
.locator('[data-testid="message"], .message, .conversation-entry')
|
||||
.first();
|
||||
await messages.waitFor({ state: "visible", timeout: 5000 }).catch(() => {
|
||||
// No messages might be expected for some sessions
|
||||
});
|
||||
|
||||
await takeFullPageScreenshot(page, "session-detail-conversation");
|
||||
});
|
||||
|
||||
test("should render session header", async ({ page }) => {
|
||||
await navigateAndWait(page, `/projects/${projectId}/sessions/${sessionId}`);
|
||||
|
||||
// Focus on the header area
|
||||
const header = page
|
||||
.locator('[data-testid="session-header"], .session-header, header')
|
||||
.first();
|
||||
if (await header.isVisible()) {
|
||||
await takeFullPageScreenshot(page, "session-detail-header");
|
||||
}
|
||||
});
|
||||
|
||||
test("should render running task state", async ({ page }) => {
|
||||
await navigateAndWait(page, `/projects/${projectId}/sessions/${sessionId}`);
|
||||
|
||||
// Check if there's a running task indicator
|
||||
const runningTask = page
|
||||
.locator('[data-testid="running-task"], .running-task, .loading')
|
||||
.first();
|
||||
if (await runningTask.isVisible()) {
|
||||
await takeFullPageScreenshot(page, "session-detail-running-task");
|
||||
}
|
||||
});
|
||||
|
||||
test("should render paused task state", async ({ page }) => {
|
||||
await navigateAndWait(page, `/projects/${projectId}/sessions/${sessionId}`);
|
||||
|
||||
// Check if there's a paused task indicator
|
||||
const pausedTask = page
|
||||
.locator('[data-testid="paused-task"], .paused-task')
|
||||
.first();
|
||||
if (await pausedTask.isVisible()) {
|
||||
await takeFullPageScreenshot(page, "session-detail-paused-task");
|
||||
}
|
||||
});
|
||||
|
||||
test("should render diff button", async ({ page }) => {
|
||||
await navigateAndWait(page, `/projects/${projectId}/sessions/${sessionId}`);
|
||||
|
||||
// Look for diff button (usually floating)
|
||||
const diffButton = page
|
||||
.locator(
|
||||
'[data-testid="diff-button"], button:has-text("Diff"), .diff-button',
|
||||
)
|
||||
.first();
|
||||
if (await diffButton.isVisible()) {
|
||||
await takeFullPageScreenshot(page, "session-detail-with-diff-button");
|
||||
}
|
||||
});
|
||||
|
||||
test("should render resume chat interface", async ({ page }) => {
|
||||
await navigateAndWait(page, `/projects/${projectId}/sessions/${sessionId}`);
|
||||
|
||||
// Look for resume chat elements
|
||||
const resumeChat = page
|
||||
.locator(
|
||||
'[data-testid="resume-chat"], .resume-chat, textarea, input[type="text"]',
|
||||
)
|
||||
.first();
|
||||
if (await resumeChat.isVisible()) {
|
||||
await takeFullPageScreenshot(page, "session-detail-resume-chat");
|
||||
}
|
||||
});
|
||||
|
||||
test("should test with different session IDs", async ({ page }) => {
|
||||
const sessionIds = [
|
||||
"5c0375b4-57a5-4f26-b12d-d022ee4e51b7",
|
||||
"fe5e1c67-53e7-4862-81ae-d0e013e3270b",
|
||||
];
|
||||
|
||||
for (const sessionId of sessionIds) {
|
||||
await navigateAndWait(
|
||||
page,
|
||||
`/projects/${projectId}/sessions/${sessionId}`,
|
||||
);
|
||||
await takeFullPageScreenshot(
|
||||
page,
|
||||
`session-detail-${sessionId.substring(0, 8)}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,30 +0,0 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
test.describe("Smoke Tests", () => {
|
||||
test("should load the application", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
// Check that the page loads without errors
|
||||
await expect(page).toHaveTitle(/Claude Code Viewer/);
|
||||
|
||||
// Take a simple screenshot to verify VRT setup
|
||||
await page.waitForLoadState("networkidle");
|
||||
await expect(page).toHaveScreenshot("smoke-test.png");
|
||||
});
|
||||
|
||||
test("should navigate to projects page", async ({ page }) => {
|
||||
await page.goto("/projects");
|
||||
|
||||
// Wait for the page to load
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
// Check that we're on the projects page
|
||||
await expect(page.locator("h1, h2, .title"))
|
||||
.toContainText(/project/i)
|
||||
.catch(() => {
|
||||
// If no specific title found, just check page loads
|
||||
});
|
||||
|
||||
await expect(page).toHaveScreenshot("projects-smoke.png");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user