mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-08-28 05:25:25 +00:00
* fix(web): stream large graph responses * fix(server): harden graph streaming * fix(ci): stabilize graph loading coverage --------- Co-authored-by: gfwangjie <gfwangjie@gf.com.cn>
163 lines
5.6 KiB
TypeScript
163 lines
5.6 KiB
TypeScript
import { test, expect } from '@playwright/test';
|
|
|
|
/**
|
|
* E2E tests for the GitNexus web UI — exploring view features.
|
|
*
|
|
* Requires:
|
|
* - gitnexus serve running on localhost:4747 with at least one indexed repo
|
|
* - gitnexus-web dev server running on localhost:5173
|
|
*
|
|
* Skipped when servers aren't available (CI without services, etc.).
|
|
* Set E2E=1 to force-run even without the availability check.
|
|
*/
|
|
|
|
const BACKEND_URL = process.env.BACKEND_URL ?? 'http://localhost:4747';
|
|
const FRONTEND_URL = process.env.FRONTEND_URL ?? 'http://localhost:5173';
|
|
|
|
test.beforeAll(async () => {
|
|
if (process.env.E2E) return;
|
|
try {
|
|
const [backendRes, frontendRes] = await Promise.allSettled([
|
|
fetch(`${BACKEND_URL}/api/repos`),
|
|
fetch(FRONTEND_URL),
|
|
]);
|
|
if (
|
|
backendRes.status === 'rejected' ||
|
|
(backendRes.status === 'fulfilled' && !backendRes.value.ok)
|
|
) {
|
|
test.skip(true, 'gitnexus serve not available on :4747');
|
|
return;
|
|
}
|
|
if (
|
|
frontendRes.status === 'rejected' ||
|
|
(frontendRes.status === 'fulfilled' && !frontendRes.value.ok)
|
|
) {
|
|
test.skip(true, 'Vite dev server not available on :5173');
|
|
return;
|
|
}
|
|
// Check there's at least one indexed repo
|
|
if (backendRes.status === 'fulfilled') {
|
|
const repos = await backendRes.value.json();
|
|
if (!repos.length) {
|
|
test.skip(true, 'No indexed repos — run gitnexus analyze first');
|
|
return;
|
|
}
|
|
}
|
|
} catch {
|
|
test.skip(true, 'servers not available');
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Wait for the server-detection flow to complete.
|
|
*
|
|
* The app auto-detects the server, then either:
|
|
* - shows the landing screen when indexed repos exist, or
|
|
* - goes straight into analyze onboarding when there are zero repos.
|
|
*
|
|
* For these tests we require at least one indexed repo, so pick the first
|
|
* landing card when present and then wait for the exploring view.
|
|
*/
|
|
async function waitForGraphLoaded(page: import('@playwright/test').Page) {
|
|
await page.goto('/');
|
|
|
|
const landingCards = page.locator('[data-testid="landing-repo-card"]');
|
|
const preferredLandingCard = landingCards
|
|
.filter({ hasText: /GitNexus|local-integration/ })
|
|
.first();
|
|
try {
|
|
await landingCards.first().waitFor({ state: 'visible', timeout: 15_000 });
|
|
const landingCard =
|
|
(await preferredLandingCard.count()) > 0 ? preferredLandingCard : landingCards.first();
|
|
await landingCard.click();
|
|
} catch {
|
|
// Landing screen may not appear (e.g. ?server auto-connect)
|
|
}
|
|
|
|
const statusBar = page.getByRole('contentinfo');
|
|
await expect(statusBar.getByText('Ready', { exact: true })).toBeVisible({ timeout: 45_000 });
|
|
await expect(statusBar).toContainText(/nodes/, {
|
|
timeout: 20_000,
|
|
});
|
|
}
|
|
|
|
test.describe('Server Connection & Graph Loading', () => {
|
|
test('selects a repo from landing and loads graph', async ({ page }) => {
|
|
await waitForGraphLoaded(page);
|
|
});
|
|
});
|
|
|
|
test.describe('Nexus AI', () => {
|
|
test('panel opens and agent initializes without error', async ({ page }) => {
|
|
await waitForGraphLoaded(page);
|
|
|
|
await page.getByRole('button', { name: 'Nexus AI' }).click();
|
|
await expect(page.getByText('Ask me anything')).toBeVisible({ timeout: 15_000 });
|
|
|
|
const errorBanner = page.getByText('Database not ready');
|
|
expect(await errorBanner.isVisible().catch(() => false)).toBe(false);
|
|
});
|
|
});
|
|
|
|
test.describe('Processes Panel', () => {
|
|
test('shows process list and View button works', async ({ page }) => {
|
|
await waitForGraphLoaded(page);
|
|
|
|
await page.getByRole('button', { name: 'Nexus AI' }).click();
|
|
await page.getByText('Processes').click();
|
|
|
|
await expect(page.locator('[data-testid="process-list-loaded"]')).toBeVisible({
|
|
timeout: 15_000,
|
|
});
|
|
|
|
const processRow = page.locator('[data-testid="process-row"]').first();
|
|
await expect(processRow).toBeVisible({ timeout: 10_000 });
|
|
await processRow.hover();
|
|
|
|
const viewBtn = processRow.locator('[data-testid="process-view-button"]');
|
|
await viewBtn.waitFor({ state: 'visible', timeout: 5_000 });
|
|
await viewBtn.click();
|
|
await expect(page.locator('[data-testid="process-modal"]')).toBeVisible({ timeout: 5_000 });
|
|
});
|
|
|
|
test('lightbulb highlights nodes in graph', async ({ page }) => {
|
|
await waitForGraphLoaded(page);
|
|
|
|
await page.getByRole('button', { name: 'Nexus AI' }).click();
|
|
await page.getByText('Processes').click();
|
|
await expect(page.locator('[data-testid="process-list-loaded"]')).toBeVisible({
|
|
timeout: 15_000,
|
|
});
|
|
|
|
const processRow = page.locator('[data-testid="process-row"]').first();
|
|
await expect(processRow).toBeVisible({ timeout: 10_000 });
|
|
await processRow.hover();
|
|
|
|
const lightbulb = processRow.locator('[data-testid="process-highlight-button"]');
|
|
await lightbulb.waitFor({ state: 'visible', timeout: 5_000 });
|
|
await lightbulb.click();
|
|
await expect(processRow).toHaveClass(/bg-amber-950/, { timeout: 5_000 });
|
|
});
|
|
});
|
|
|
|
test.describe('Turn Off All Highlights', () => {
|
|
test('selecting a node dims others, button clears it', async ({ page }) => {
|
|
await waitForGraphLoaded(page);
|
|
|
|
await expect(page.locator('canvas').first()).toBeVisible({ timeout: 10_000 });
|
|
|
|
const fileItem = page.getByText('package.json').first();
|
|
await expect(fileItem).toBeVisible({ timeout: 10_000 });
|
|
await fileItem.click();
|
|
|
|
const highlightToggle = page.locator('[data-testid="ai-highlights-toggle"]');
|
|
await expect(highlightToggle).toHaveAttribute('title', 'Turn off all highlights', {
|
|
timeout: 5_000,
|
|
});
|
|
|
|
await highlightToggle.click();
|
|
await expect(highlightToggle).toHaveAttribute('title', 'Turn on AI highlights', {
|
|
timeout: 5_000,
|
|
});
|
|
});
|
|
});
|