advanced-rag-api / frontend /e2e /graphrag.spec.ts
Ashutosh1975270's picture
feat: complete frontend and backend integration and security configurations
3786a3f
Raw
History Blame Contribute Delete
11.4 kB
/**
* ============================================================================
* GraphRAG β€” End-to-End UI Suite (Playwright)
* ============================================================================
*
* WHAT THIS SUITE COVERS (full critical-journey flow against the live stack):
* Frontend : http://127.0.0.1:3000 (Next.js App Router)
* Backend : http://127.0.0.1:8000/api (Neo4j + JWT auth)
*
* HOW TO RUN
* ----------
* 1. Start the backend (FastAPI on :8000) and Neo4j.
* 2. Start the frontend: cd frontend && npm run dev (serves on :3000)
* 3. Install the runner: npm install (adds @playwright/test)
* npx playwright install chromium
* 4. Run the suite: npm run test:e2e
* Run one file: npx playwright test e2e/graphrag.spec.ts
* Repeat to prove determinism (no sleeps β†’ should be green 10x):
* npx playwright test --repeat-each=10
* Headed / debug: npx playwright test --project=chromium --headed
* npx playwright test --debug
*
* ARTIFACTS ON FAILURE: trace (on-first-retry), screenshot, and video are
* configured in ../playwright.config.ts, so every red run is debuggable from
* CI without a local repro.
*
* ENCODED KNOWN GAPS (visible as soft-assertion failures / comments):
* GAP #1 Register form: backend RegisterSerializer requires `confirm_password`,
* but app/register POSTs only {username,email,password}. UI registration
* therefore 400s and never redirects to '/'. See test "registration".
* GAP #2 GET /graph/ returns 0 edges. The visualization therefore renders
* nodes but no relationships. See test "explore".
* (Also noted inline: several pages fall back to mock data on API error, which
* can mask a failing backend β€” captured with soft assertions on response status.)
* ============================================================================
*/
import { test, expect, type Page } from '@playwright/test';
import { apiUser, seedAuth, API_BASE_URL, type AuthUser } from './fixtures';
const TEST_DOC = '/tmp/opencode/test_doc.md';
/** Number of graph nodes we expect the backend to expose (GAP #2 context). */
async function graphNodeEdgeCounts(page: Page): Promise<{ nodes: number; edges: number }> {
const resp = await page.waitForResponse(
(r) => r.url().match(/\/graph\/?(\?.*)?$/) !== null && r.request().method() === 'GET',
);
const body = (await resp.json()) as { nodes?: any[]; links?: any[]; edges?: any[] };
return {
nodes: (body.nodes || []).length,
edges: (body.links || body.edges || []).length,
};
}
// ---------------------------------------------------------------------------
// 1) REGISTRATION (public route, no seeded auth)
// ---------------------------------------------------------------------------
test.describe('Auth Β· Registration (UI)', () => {
test('fills register form and expects redirect to dashboard', async ({ page }) => {
// KNOWN GAP #1 (encoded as an expected failure):
// The register page DOES render a "Confirm Password" input, but its submit
// handler posts only {username, email, password} to /auth/register/.
// The backend's RegisterSerializer marks `confirm_password` as required=True,
// so the request is rejected with HTTP 400 and the client-side redirect to
// '/' never fires.
// `test.fail()` tells Playwright this test is *expected* to fail while the
// gap is open (keeps CI green); when the form is fixed and the redirect
// succeeds, this flips to "passed unexpectedly" β†’ remove test.fail().
test.fail();
await page.goto('/register');
const username = `uiform_${Date.now()}`;
const email = `${username}@test.local`;
await page.getByPlaceholder('johnsmith').fill(username);
await page.getByPlaceholder('you@example.com').fill(email);
// Two password inputs on the register page: [0]=Password, [1]=Confirm Password.
const passwords = page.locator('input[type="password"]');
await passwords.nth(0).fill('E2ePassw0rd!9');
await passwords.nth(1).fill('E2ePassw0rd!9');
// Capture the registration API call to document the backend contract failure.
const regResp = page.waitForResponse(
(r) => r.url().includes('/auth/register/') && r.request().method() === 'POST',
);
await page.getByRole('button', { name: 'Create Account' }).click();
const resp = await regResp;
// Sanity log so a trace shows the real 400 vs. a different failure mode.
test.info().annotations.push({ type: 'GAP#1', description: `register status=${resp.status()}` });
// Intended (post-fix) behavior:
await expect(page, 'expected redirect to dashboard (/) after successful register').toHaveURL(/\/$/);
});
});
// ---------------------------------------------------------------------------
// 2) DASHBOARD QUERY (authenticated)
// ---------------------------------------------------------------------------
test('dashboard: query returns an answer and the graph canvas renders', async ({ page, apiUser }) => {
await seedAuth(page, apiUser as AuthUser);
await page.goto('/');
const queryInput = page.getByPlaceholder('Ask anything about your documents...');
await expect(queryInput).toBeVisible();
await queryInput.fill('What entities and relationships exist in the knowledge graph?');
const answerResp = page.waitForResponse(
(r) => r.url().includes('/query/') && r.request().method() === 'POST',
);
await page.getByRole('button', { name: /submit/i }).click();
const qr = await answerResp;
// AnswerCard only renders `.prose-invert` when `answer` is non-empty.
const answer = page.locator('.prose-invert').first();
await expect(answer, 'an answer should render in the AnswerCard').toBeVisible();
await expect(answer, 'answer should contain content').not.toBeEmpty();
// KNOWN FRAGILITY: MainQueryView falls back to mockAnswer on any API error,
// so a visible answer can mask a broken backend. Assert the real response.
expect
.soft(qr.status(), 'GAP: /query/ may be masked by mock fallback on error β†’ expect 200')
.toBe(200);
// The graph canvas mounts on the dashboard when graph data is present.
await expect(page.locator('.graph-canvas').first(), 'graph canvas should mount').toBeVisible();
});
// ---------------------------------------------------------------------------
// 3) DOCUMENTS UPLOAD (authenticated)
// ---------------------------------------------------------------------------
test('documents: uploaded markdown appears in the list and reaches COMPLETED', async ({ page, apiUser }) => {
await seedAuth(page, apiUser as AuthUser);
await page.goto('/documents');
const fileInput = page.locator('input[type="file"]');
await expect(fileInput, 'upload input should be present').toBeAttached();
await fileInput.setInputFiles(TEST_DOC);
// The row for our file is upserted client-side on the upload response, so it
// appears regardless of backend β€” but it MUST appear (list visibility).
const fileRow = page.getByText('test_doc.md', { exact: true }).first();
await expect(fileRow, 'uploaded file should appear in the document list').toBeVisible({
timeout: 15000,
});
// KNOWN FRAGILITY: DocumentUpload mocks a PROCESSING status on upload error,
// so a failing backend will NOT surface as a UI error. We assert the genuine
// target state: the row progresses to COMPLETED via the polling hook.
await expect(
page.getByText('COMPLETED', { exact: true }).first(),
'document should reach COMPLETED status',
).toBeVisible({ timeout: 60000 });
});
// ---------------------------------------------------------------------------
// 4) EXPLORE (authenticated) β€” graph canvas + stats sidebar
// ---------------------------------------------------------------------------
test('explore: graph canvas and stats sidebar render', async ({ page, apiUser }) => {
await seedAuth(page, apiUser as AuthUser);
await page.goto('/explore');
// Canvas + the stats sidebar ("Nodes" / "Edges" stat cards from GraphStats).
await expect(page.locator('.graph-canvas').first(), 'graph canvas should render').toBeVisible();
await expect(page.getByText('Nodes', { exact: true }), 'stats: Nodes card').toBeVisible();
await expect(page.getByText('Edges', { exact: true }), 'stats: Edges card').toBeVisible();
// Capture the real backend graph payload to assert observable structure.
const { nodes, edges } = await graphNodeEdgeCounts(page);
// Real assertion: the graph should expose at least one node.
expect(nodes, 'GET /graph/ should return at least one node').toBeGreaterThan(0);
// KNOWN GAP #2: backend GET /graph/ currently returns 0 edges. We assert it
// explicitly (soft) so the missing-relationships defect is always visible.
expect
.soft(edges, 'GAP #2: backend GET /graph/ returns 0 edges (visualization gap)')
.toBe(0);
});
// ---------------------------------------------------------------------------
// 5) COMPARE (authenticated) β€” 3 columns + verdict
// ---------------------------------------------------------------------------
test('compare: running a comparison shows three strategy columns and a verdict', async ({ page, apiUser }) => {
await seedAuth(page, apiUser as AuthUser);
await page.goto('/compare');
const cmpResp = page.waitForResponse(
(r) => r.url().includes('/query/compare/') && r.request().method() === 'POST',
);
await page.getByPlaceholder('Ask anything about your documents...').fill(
'Compare graph, vector, and hybrid retrieval for this corpus',
);
await page.getByRole('button', { name: /submit/i }).click();
await cmpResp;
// ComparisonView renders three columns (Graph Only / Vector Only / Hybrid)
// and a Verdict card. (Falls back to mockComparison on API error, so columns
// appear even if the backend compare endpoint is down β€” see GAP note.)
await expect(page.getByText('Graph Only'), 'column: Graph Only').toBeVisible();
await expect(page.getByText('Vector Only'), 'column: Vector Only').toBeVisible();
await expect(page.getByText('Hybrid'), 'column: Hybrid').toBeVisible();
await expect(page.getByText('Verdict', { exact: true }), 'verdict section').toBeVisible();
});
// ---------------------------------------------------------------------------
// 6) COMMUNITIES (authenticated) β€” community cards
// ---------------------------------------------------------------------------
test('communities: community cards render', async ({ page, apiUser }) => {
await seedAuth(page, apiUser as AuthUser);
const commResp = page.waitForResponse((r) => r.url().includes('/graph/communities/'));
await page.goto('/communities');
await commResp;
// CommunityView defaults to the "Cards" tab and seeds mock communities on
// first render, then tries to hydrate from /graph/communities/. Each card
// exposes an "Expand Graph" action β†’ a reliable card-presence signal.
await expect(page.getByRole('tab', { name: 'Cards' }), 'Cards tab').toBeVisible();
await expect(page.getByRole('tab', { name: 'Graph' }), 'Graph tab').toBeVisible();
const cards = page.getByText('Expand Graph');
await expect(cards, 'at least one community card should render').toHaveCountGreaterThan(0);
});