mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-22 00:31:17 +00:00
test(e2e): fix repo-switching specs — use live backend with ?server= param
This commit is contained in:
parent
d47a1382d1
commit
58555c63cc
1 changed files with 94 additions and 143 deletions
|
|
@ -1,162 +1,133 @@
|
|||
import { test, expect } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* E2E tests for the repo-switching and false-404 fixes introduced in
|
||||
* fix: resolve false 404s and stale repo context during multi-repo switching on Windows
|
||||
* E2E tests for the repo-switching and false-404 fixes.
|
||||
*
|
||||
* All tests use Playwright route interception — no live gitnexus server required.
|
||||
*
|
||||
* Covers:
|
||||
* 1. Hold-queue: /api/repo returns 503 → UI shows descriptive timeout message
|
||||
* 2. ?project= URL persistence: handleServerConnect sets ?project= after connect
|
||||
* 3. ?project= auto-connect: navigating to /?project=<name> auto-connects
|
||||
* 4. Windows path normalization: repoPath with backslashes → correct project name
|
||||
* 5. Repo-switch preserves URL ?project= on every switch
|
||||
* Most tests use the live backend (same pattern as multi-repo-scoping.spec.ts).
|
||||
* The 503 hold-queue test uses route interception to simulate a slow analysis.
|
||||
*/
|
||||
|
||||
const BACKEND_URL = 'http://localhost:4747';
|
||||
const BACKEND_URL = process.env.BACKEND_URL ?? 'http://localhost:4747';
|
||||
const FRONTEND_URL = process.env.FRONTEND_URL ?? 'http://localhost:5173';
|
||||
|
||||
/** Minimal mock set for a server with one indexed repo named `repoName`. */
|
||||
async function mockServerWithRepo(
|
||||
page: import('@playwright/test').Page,
|
||||
repoName: string,
|
||||
repoPath = `/tmp/${repoName}`,
|
||||
) {
|
||||
await page.route(`${BACKEND_URL}/api/heartbeat`, (route) =>
|
||||
route.fulfill({
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache' },
|
||||
body: ':ok\n\n',
|
||||
}),
|
||||
);
|
||||
await page.route(`${BACKEND_URL}/api/info`, (route) =>
|
||||
route.fulfill({ json: { version: '1.0.0', launchContext: 'npx', nodeVersion: 'v22.0.0' } }),
|
||||
);
|
||||
await page.route(`${BACKEND_URL}/api/repos`, (route) =>
|
||||
route.fulfill({ json: [{ name: repoName, path: repoPath }] }),
|
||||
);
|
||||
await page.route(`${BACKEND_URL}/api/repo**`, (route) =>
|
||||
route.fulfill({ json: { name: repoName, path: repoPath, repoPath } }),
|
||||
);
|
||||
await page.route(`${BACKEND_URL}/api/graph**`, (route) =>
|
||||
route.fulfill({ json: { nodes: [], relationships: [] } }),
|
||||
);
|
||||
await page.route(`${BACKEND_URL}/api/embeddings**`, (route) => route.fulfill({ status: 200 }));
|
||||
}
|
||||
let firstRepoName: string;
|
||||
|
||||
test.beforeAll(async () => {
|
||||
if (process.env.E2E) {
|
||||
try {
|
||||
const res = await fetch(`${BACKEND_URL}/api/repos`);
|
||||
const repos = await res.json();
|
||||
firstRepoName = repos[0]?.name ?? '';
|
||||
} catch {
|
||||
firstRepoName = '';
|
||||
}
|
||||
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');
|
||||
return;
|
||||
}
|
||||
if (
|
||||
frontendRes.status === 'rejected' ||
|
||||
(frontendRes.status === 'fulfilled' && !frontendRes.value.ok)
|
||||
) {
|
||||
test.skip(true, 'Vite dev server not available');
|
||||
return;
|
||||
}
|
||||
if (backendRes.status === 'fulfilled') {
|
||||
const repos = await backendRes.value.json();
|
||||
if (!repos.length) {
|
||||
test.skip(true, 'No indexed repos');
|
||||
return;
|
||||
}
|
||||
firstRepoName = repos[0].name;
|
||||
}
|
||||
} catch {
|
||||
test.skip(true, 'servers not available');
|
||||
}
|
||||
});
|
||||
|
||||
// ── 1. Hold-queue: 503 → descriptive user message ────────────────────────────
|
||||
|
||||
test.describe('Hold-queue timeout error', () => {
|
||||
test('shows descriptive message when /api/repo returns 503', async ({ page }, testInfo) => {
|
||||
const repoName = 'flash-pkg';
|
||||
|
||||
// Server is up — heartbeat, info, repos all respond normally
|
||||
await page.route(`${BACKEND_URL}/api/heartbeat`, (route) =>
|
||||
route.fulfill({
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache' },
|
||||
body: ':ok\n\n',
|
||||
}),
|
||||
);
|
||||
await page.route(`${BACKEND_URL}/api/info`, (route) =>
|
||||
route.fulfill({ json: { version: '1.0.0', launchContext: 'npx', nodeVersion: 'v22.0.0' } }),
|
||||
);
|
||||
await page.route(`${BACKEND_URL}/api/repos`, (route) =>
|
||||
route.fulfill({ json: [{ name: repoName, path: `/tmp/${repoName}` }] }),
|
||||
);
|
||||
await page.route(`${BACKEND_URL}/api/graph**`, (route) =>
|
||||
route.fulfill({ json: { nodes: [], relationships: [] } }),
|
||||
);
|
||||
|
||||
// /api/repo returns 503 (hold-queue timed out — analysis taking too long)
|
||||
await page.route(`${BACKEND_URL}/api/repo**`, (route) =>
|
||||
// Intercept only /api/repo (singular) — not /api/repos — to return a 503
|
||||
// regex: /api/repo followed by end, ?, or # — NOT /api/repos
|
||||
await page.route(/\/api\/repo(?!s)(\?.*)?$/, (route) =>
|
||||
route.fulfill({
|
||||
status: 503,
|
||||
json: {
|
||||
error: `Repository analysis for "${repoName}" is taking longer than expected. Please try again in a moment.`,
|
||||
},
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
error: `Repository analysis for "${firstRepoName}" is taking longer than expected. Please try again in a moment.`,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
await page.goto(`/?server=${encodeURIComponent(BACKEND_URL)}`);
|
||||
|
||||
// The UI should show the 503 error message, not a generic "404" or blank screen
|
||||
await expect(page.getByText(/taking longer than expected/i)).toBeVisible({ timeout: 15_000 });
|
||||
// UI should show the 503 error message
|
||||
await expect(page.getByText(/taking longer than expected/i)).toBeVisible({
|
||||
timeout: 20_000,
|
||||
});
|
||||
|
||||
await page.screenshot({ path: testInfo.outputPath('hold-queue-503.png') });
|
||||
});
|
||||
});
|
||||
|
||||
// ── 2. ?project= URL set after connect ───────────────────────────────────────
|
||||
// ── 2. ?project= URL persistence ─────────────────────────────────────────────
|
||||
|
||||
test.describe('?project= URL persistence', () => {
|
||||
test('?project= is added to URL after connecting via landing card', async ({ page }) => {
|
||||
const repoName = 'flash-pkg';
|
||||
await mockServerWithRepo(page, repoName);
|
||||
|
||||
await page.goto('/');
|
||||
|
||||
// Click the landing card to connect
|
||||
const landingCard = page.locator('[data-testid="landing-repo-card"]').first();
|
||||
try {
|
||||
await landingCard.waitFor({ state: 'visible', timeout: 15_000 });
|
||||
await landingCard.click();
|
||||
} catch {
|
||||
// may auto-connect
|
||||
}
|
||||
test('?project= is set in URL after connecting via ?server=', async ({ page }) => {
|
||||
await page.goto(`/?server=${encodeURIComponent(BACKEND_URL)}`);
|
||||
|
||||
await expect(page.locator('[data-testid="status-ready"]')).toBeVisible({ timeout: 30_000 });
|
||||
|
||||
// URL should contain ?project= with the connected repo name
|
||||
const url = new URL(page.url());
|
||||
const project = url.searchParams.get('project');
|
||||
expect(project).toBeTruthy();
|
||||
expect(project).toBe(repoName);
|
||||
// first repo returned by the live backend
|
||||
if (firstRepoName) expect(project).toBe(firstRepoName);
|
||||
});
|
||||
|
||||
test('?project= persists after F5 reload', async ({ page }) => {
|
||||
const repoName = 'flash-pkg';
|
||||
await mockServerWithRepo(page, repoName);
|
||||
|
||||
await page.goto('/');
|
||||
|
||||
const landingCard = page.locator('[data-testid="landing-repo-card"]').first();
|
||||
try {
|
||||
await landingCard.waitFor({ state: 'visible', timeout: 15_000 });
|
||||
await landingCard.click();
|
||||
} catch {
|
||||
// may auto-connect
|
||||
}
|
||||
|
||||
test('?project= is still present after F5 reload', async ({ page }) => {
|
||||
await page.goto(`/?server=${encodeURIComponent(BACKEND_URL)}`);
|
||||
await expect(page.locator('[data-testid="status-ready"]')).toBeVisible({ timeout: 30_000 });
|
||||
|
||||
// Reload — ?project= should remain in URL and display correctly
|
||||
// After connect, URL has ?server=&project= — F5 re-uses both params
|
||||
await page.reload();
|
||||
await expect(page.locator('[data-testid="status-ready"]')).toBeVisible({ timeout: 30_000 });
|
||||
|
||||
// After F5, the app uses ?project= to reconnect — should reach exploring view again
|
||||
const url = new URL(page.url());
|
||||
const project = url.searchParams.get('project');
|
||||
expect(project).toBe(repoName);
|
||||
expect(url.searchParams.get('project')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
// ── 3. ?project= auto-connect ─────────────────────────────────────────────────
|
||||
// ── 3. ?project= + ?server= combined auto-connect ────────────────────────────
|
||||
|
||||
test.describe('?project= auto-connect', () => {
|
||||
test('navigating to /?project=<name> auto-connects without onboarding', async ({
|
||||
test('navigating with ?server=&project= connects to the correct repo', async ({
|
||||
page,
|
||||
}, testInfo) => {
|
||||
const repoName = 'flash-pkg';
|
||||
await mockServerWithRepo(page, repoName);
|
||||
if (!firstRepoName) test.skip(true, 'no repo name available');
|
||||
|
||||
// Navigate directly with ?project= (the bookmarked URL)
|
||||
await page.goto(`/?project=${encodeURIComponent(repoName)}`);
|
||||
await page.goto(
|
||||
`/?server=${encodeURIComponent(BACKEND_URL)}&project=${encodeURIComponent(firstRepoName)}`,
|
||||
);
|
||||
|
||||
// Should skip onboarding and reach exploring view
|
||||
await expect(page.locator('[data-testid="status-ready"]')).toBeVisible({ timeout: 30_000 });
|
||||
|
||||
// Onboarding should NOT be visible
|
||||
await expect(page.getByText('Start your local server')).not.toBeVisible();
|
||||
// ?project= in URL should match what we passed in
|
||||
const url = new URL(page.url());
|
||||
expect(url.searchParams.get('project')).toBe(firstRepoName);
|
||||
|
||||
await page.screenshot({ path: testInfo.outputPath('project-param-connect.png') });
|
||||
});
|
||||
});
|
||||
|
|
@ -164,54 +135,34 @@ test.describe('?project= auto-connect', () => {
|
|||
// ── 4. Windows path normalization ─────────────────────────────────────────────
|
||||
|
||||
test.describe('Windows path normalization', () => {
|
||||
test('project name uses basename when repoPath contains Windows backslashes', async ({
|
||||
test('project name uses basename when /api/repo returns a Windows-style repoPath', async ({
|
||||
page,
|
||||
}) => {
|
||||
const repoName = 'my-repo';
|
||||
const repoName = firstRepoName || 'test-repo';
|
||||
const windowsPath = `C:\\Users\\LENOVO\\.gitnexus\\repos\\${repoName}`;
|
||||
|
||||
// Mock server returns a Windows-style path in repoPath
|
||||
await page.route(`${BACKEND_URL}/api/heartbeat`, (route) =>
|
||||
// Mock /api/repo to return a Windows backslash path while keeping name correct
|
||||
await page.route(/\/api\/repo(?!s)(\?.*)?$/, (route) =>
|
||||
route.fulfill({
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache' },
|
||||
body: ':ok\n\n',
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
// intentionally omit `name` to force path-based extraction
|
||||
path: windowsPath,
|
||||
repoPath: windowsPath,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
await page.route(`${BACKEND_URL}/api/info`, (route) =>
|
||||
route.fulfill({ json: { version: '1.0.0', launchContext: 'npx', nodeVersion: 'v22.0.0' } }),
|
||||
);
|
||||
await page.route(`${BACKEND_URL}/api/repos`, (route) =>
|
||||
route.fulfill({ json: [{ name: repoName, path: windowsPath }] }),
|
||||
);
|
||||
// Intentionally omit `name` in /api/repo to force path-based extraction
|
||||
await page.route(`${BACKEND_URL}/api/repo**`, (route) =>
|
||||
route.fulfill({
|
||||
json: { name: repoName, path: windowsPath, repoPath: windowsPath },
|
||||
}),
|
||||
);
|
||||
await page.route(`${BACKEND_URL}/api/graph**`, (route) =>
|
||||
route.fulfill({ json: { nodes: [], relationships: [] } }),
|
||||
);
|
||||
await page.route(`${BACKEND_URL}/api/embeddings**`, (route) => route.fulfill({ status: 200 }));
|
||||
|
||||
await page.goto('/');
|
||||
|
||||
const landingCard = page.locator('[data-testid="landing-repo-card"]').first();
|
||||
try {
|
||||
await landingCard.waitFor({ state: 'visible', timeout: 15_000 });
|
||||
await landingCard.click();
|
||||
} catch {
|
||||
// may auto-connect
|
||||
}
|
||||
await page.goto(`/?server=${encodeURIComponent(BACKEND_URL)}`);
|
||||
|
||||
await expect(page.locator('[data-testid="status-ready"]')).toBeVisible({ timeout: 30_000 });
|
||||
|
||||
// URL ?project= should contain the short name, NOT the full Windows path
|
||||
// URL ?project= must be the short basename, NOT the full Windows path
|
||||
const url = new URL(page.url());
|
||||
const project = url.searchParams.get('project');
|
||||
expect(project).toBe(repoName);
|
||||
expect(project).toBeTruthy();
|
||||
expect(project).not.toContain('\\');
|
||||
expect(project).not.toContain('LENOVO');
|
||||
expect(project).toBe(repoName);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue