Spaces:
Running
Running
Saurabh Kumar Bajpai commited on
Commit ·
4d64819
1
Parent(s): 457eacc
test: add Playwright e2e coverage
Browse files- .github/workflows/e2e.yml +35 -0
- README.md +1 -0
- frontend/e2e/auth-and-chat.spec.ts +130 -0
- frontend/package-lock.json +63 -0
- frontend/package.json +4 -1
- frontend/playwright.config.ts +30 -0
.github/workflows/e2e.yml
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: Frontend E2E
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
pull_request:
|
| 5 |
+
branches: ["dev"]
|
| 6 |
+
push:
|
| 7 |
+
branches: ["dev"]
|
| 8 |
+
|
| 9 |
+
jobs:
|
| 10 |
+
playwright:
|
| 11 |
+
name: Playwright
|
| 12 |
+
runs-on: ubuntu-latest
|
| 13 |
+
|
| 14 |
+
steps:
|
| 15 |
+
- name: Checkout code
|
| 16 |
+
uses: actions/checkout@v4
|
| 17 |
+
|
| 18 |
+
- name: Set up Node.js
|
| 19 |
+
uses: actions/setup-node@v4
|
| 20 |
+
with:
|
| 21 |
+
node-version: "20"
|
| 22 |
+
cache: "npm"
|
| 23 |
+
cache-dependency-path: frontend/package-lock.json
|
| 24 |
+
|
| 25 |
+
- name: Install frontend dependencies
|
| 26 |
+
working-directory: frontend
|
| 27 |
+
run: npm ci
|
| 28 |
+
|
| 29 |
+
- name: Install Playwright browser
|
| 30 |
+
working-directory: frontend
|
| 31 |
+
run: npx playwright install --with-deps chromium
|
| 32 |
+
|
| 33 |
+
- name: Run Playwright E2E tests
|
| 34 |
+
working-directory: frontend
|
| 35 |
+
run: npm run test:e2e -- --reporter=list
|
README.md
CHANGED
|
@@ -494,6 +494,7 @@ docker compose up --build
|
|
| 494 |
| `npm run dev` | Start **Next.js** dev server |
|
| 495 |
| `npm run build` | Production build → `out/` (static export) |
|
| 496 |
| `npm run lint` | Run ESLint |
|
|
|
|
| 497 |
|
| 498 |
### Docker
|
| 499 |
|
|
|
|
| 494 |
| `npm run dev` | Start **Next.js** dev server |
|
| 495 |
| `npm run build` | Production build → `out/` (static export) |
|
| 496 |
| `npm run lint` | Run ESLint |
|
| 497 |
+
| `npm run test:e2e` | Run Playwright end-to-end tests |
|
| 498 |
|
| 499 |
### Docker
|
| 500 |
|
frontend/e2e/auth-and-chat.spec.ts
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { expect, test, type Page } from "@playwright/test";
|
| 2 |
+
|
| 3 |
+
const user = {
|
| 4 |
+
id: "user-1",
|
| 5 |
+
username: "tester",
|
| 6 |
+
email: "tester@example.com",
|
| 7 |
+
is_admin: false,
|
| 8 |
+
created_at: "2026-05-28T00:00:00Z",
|
| 9 |
+
};
|
| 10 |
+
|
| 11 |
+
const tokenResponse = {
|
| 12 |
+
access_token: "access-token",
|
| 13 |
+
refresh_token: "refresh-token",
|
| 14 |
+
token_type: "bearer",
|
| 15 |
+
user,
|
| 16 |
+
};
|
| 17 |
+
|
| 18 |
+
const uploadedDocument = {
|
| 19 |
+
id: "doc-1",
|
| 20 |
+
original_name: "notes.txt",
|
| 21 |
+
file_size: 11,
|
| 22 |
+
page_count: 1,
|
| 23 |
+
chunk_count: 1,
|
| 24 |
+
status: "ready",
|
| 25 |
+
error_message: null,
|
| 26 |
+
uploaded_at: "2026-05-28T00:00:00Z",
|
| 27 |
+
};
|
| 28 |
+
|
| 29 |
+
async function mockDashboardApis(page: Page, documents: typeof uploadedDocument[] = []) {
|
| 30 |
+
await page.route("**/api/v1/auth/me", async (route) => {
|
| 31 |
+
await route.fulfill({ json: user });
|
| 32 |
+
});
|
| 33 |
+
|
| 34 |
+
await page.route("**/api/v1/documents/", async (route) => {
|
| 35 |
+
await route.fulfill({
|
| 36 |
+
json: {
|
| 37 |
+
items: documents,
|
| 38 |
+
total: documents.length,
|
| 39 |
+
page: 1,
|
| 40 |
+
pages: documents.length > 0 ? 1 : 0,
|
| 41 |
+
},
|
| 42 |
+
});
|
| 43 |
+
});
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
test("logs in with email and password", async ({ page }) => {
|
| 47 |
+
await mockDashboardApis(page);
|
| 48 |
+
await page.route("**/api/v1/auth/login", async (route) => {
|
| 49 |
+
const body = route.request().postDataJSON() as { email: string };
|
| 50 |
+
expect(body.email).toBe(user.email);
|
| 51 |
+
await route.fulfill({ json: tokenResponse });
|
| 52 |
+
});
|
| 53 |
+
|
| 54 |
+
await page.goto("/login");
|
| 55 |
+
await page.locator("#login-email").fill(user.email);
|
| 56 |
+
await page.locator("#login-password").fill("password123");
|
| 57 |
+
await page.getByRole("button", { name: "Sign In" }).click();
|
| 58 |
+
|
| 59 |
+
await expect(page).toHaveURL(/\/dashboard$/);
|
| 60 |
+
await expect(page.getByText("No documents yet")).toBeVisible();
|
| 61 |
+
});
|
| 62 |
+
|
| 63 |
+
test("creates an account from the signup form", async ({ page }) => {
|
| 64 |
+
await mockDashboardApis(page);
|
| 65 |
+
await page.route("**/api/v1/auth/register", async (route) => {
|
| 66 |
+
const body = route.request().postDataJSON() as { username: string; email: string };
|
| 67 |
+
expect(body.username).toBe(user.username);
|
| 68 |
+
expect(body.email).toBe(user.email);
|
| 69 |
+
await route.fulfill({ status: 201, json: tokenResponse });
|
| 70 |
+
});
|
| 71 |
+
|
| 72 |
+
await page.goto("/register");
|
| 73 |
+
await page.locator("#reg-username").fill(user.username);
|
| 74 |
+
await page.locator("#reg-email").fill(user.email);
|
| 75 |
+
await page.locator("#reg-password").fill("password123");
|
| 76 |
+
await page.getByRole("button", { name: "Create Account" }).click();
|
| 77 |
+
|
| 78 |
+
await expect(page).toHaveURL(/\/dashboard$/);
|
| 79 |
+
await expect(page.getByText("No documents yet")).toBeVisible();
|
| 80 |
+
});
|
| 81 |
+
|
| 82 |
+
test("uploads a document and chats with it", async ({ page }) => {
|
| 83 |
+
const documents: typeof uploadedDocument[] = [];
|
| 84 |
+
|
| 85 |
+
await page.addInitScript(() => {
|
| 86 |
+
localStorage.setItem("token", "access-token");
|
| 87 |
+
localStorage.setItem("refresh_token", "refresh-token");
|
| 88 |
+
});
|
| 89 |
+
|
| 90 |
+
await mockDashboardApis(page, documents);
|
| 91 |
+
|
| 92 |
+
await page.route("**/api/v1/documents/upload", async (route) => {
|
| 93 |
+
documents.push(uploadedDocument);
|
| 94 |
+
await route.fulfill({ status: 202, json: uploadedDocument });
|
| 95 |
+
});
|
| 96 |
+
|
| 97 |
+
await page.route("**/api/v1/chat/history/doc-1", async (route) => {
|
| 98 |
+
await route.fulfill({ json: { messages: [], document_id: "doc-1" } });
|
| 99 |
+
});
|
| 100 |
+
|
| 101 |
+
await page.route("**/api/v1/chat/ask/stream", async (route) => {
|
| 102 |
+
await route.fulfill({
|
| 103 |
+
status: 200,
|
| 104 |
+
headers: { "content-type": "text/event-stream" },
|
| 105 |
+
body: [
|
| 106 |
+
'data: {"type":"token","data":"A short"}\n\n',
|
| 107 |
+
'data: {"type":"token","data":" summary."}\n\n',
|
| 108 |
+
'data: {"type":"sources","data":[]}\n\n',
|
| 109 |
+
'data: {"type":"done"}\n\n',
|
| 110 |
+
].join(""),
|
| 111 |
+
});
|
| 112 |
+
});
|
| 113 |
+
|
| 114 |
+
await page.goto("/dashboard");
|
| 115 |
+
await page.locator('input[type="file"]').setInputFiles({
|
| 116 |
+
name: "notes.txt",
|
| 117 |
+
mimeType: "text/plain",
|
| 118 |
+
buffer: Buffer.from("hello world"),
|
| 119 |
+
});
|
| 120 |
+
|
| 121 |
+
await expect(page.getByText("notes.txt")).toBeVisible();
|
| 122 |
+
await page.getByText("notes.txt").click();
|
| 123 |
+
await expect(page.getByText("Ask about your document")).toBeVisible();
|
| 124 |
+
|
| 125 |
+
await page.locator("#chat-input").fill("Summarize this document");
|
| 126 |
+
await page.locator("#send-btn").click();
|
| 127 |
+
|
| 128 |
+
await expect(page.getByText("Summarize this document")).toBeVisible();
|
| 129 |
+
await expect(page.getByText("A short summary.")).toBeVisible();
|
| 130 |
+
});
|
frontend/package-lock.json
CHANGED
|
@@ -25,6 +25,7 @@
|
|
| 25 |
"tw-animate-css": "^1.4.0"
|
| 26 |
},
|
| 27 |
"devDependencies": {
|
|
|
|
| 28 |
"@tailwindcss/postcss": "^4",
|
| 29 |
"@types/node": "^20",
|
| 30 |
"@types/react": "^19",
|
|
@@ -2279,6 +2280,22 @@
|
|
| 2279 |
"integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==",
|
| 2280 |
"license": "MIT"
|
| 2281 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2282 |
"node_modules/@rtsao/scc": {
|
| 2283 |
"version": "1.1.0",
|
| 2284 |
"resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
|
|
@@ -5756,6 +5773,20 @@
|
|
| 5756 |
"node": ">=14.14"
|
| 5757 |
}
|
| 5758 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5759 |
"node_modules/function-bind": {
|
| 5760 |
"version": "1.1.2",
|
| 5761 |
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
|
@@ -9303,6 +9334,38 @@
|
|
| 9303 |
"node": ">=16.20.0"
|
| 9304 |
}
|
| 9305 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9306 |
"node_modules/possible-typed-array-names": {
|
| 9307 |
"version": "1.1.0",
|
| 9308 |
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
|
|
|
|
| 25 |
"tw-animate-css": "^1.4.0"
|
| 26 |
},
|
| 27 |
"devDependencies": {
|
| 28 |
+
"@playwright/test": "^1.60.0",
|
| 29 |
"@tailwindcss/postcss": "^4",
|
| 30 |
"@types/node": "^20",
|
| 31 |
"@types/react": "^19",
|
|
|
|
| 2280 |
"integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==",
|
| 2281 |
"license": "MIT"
|
| 2282 |
},
|
| 2283 |
+
"node_modules/@playwright/test": {
|
| 2284 |
+
"version": "1.60.0",
|
| 2285 |
+
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.60.0.tgz",
|
| 2286 |
+
"integrity": "sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==",
|
| 2287 |
+
"devOptional": true,
|
| 2288 |
+
"license": "Apache-2.0",
|
| 2289 |
+
"dependencies": {
|
| 2290 |
+
"playwright": "1.60.0"
|
| 2291 |
+
},
|
| 2292 |
+
"bin": {
|
| 2293 |
+
"playwright": "cli.js"
|
| 2294 |
+
},
|
| 2295 |
+
"engines": {
|
| 2296 |
+
"node": ">=18"
|
| 2297 |
+
}
|
| 2298 |
+
},
|
| 2299 |
"node_modules/@rtsao/scc": {
|
| 2300 |
"version": "1.1.0",
|
| 2301 |
"resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
|
|
|
|
| 5773 |
"node": ">=14.14"
|
| 5774 |
}
|
| 5775 |
},
|
| 5776 |
+
"node_modules/fsevents": {
|
| 5777 |
+
"version": "2.3.2",
|
| 5778 |
+
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
| 5779 |
+
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
| 5780 |
+
"hasInstallScript": true,
|
| 5781 |
+
"license": "MIT",
|
| 5782 |
+
"optional": true,
|
| 5783 |
+
"os": [
|
| 5784 |
+
"darwin"
|
| 5785 |
+
],
|
| 5786 |
+
"engines": {
|
| 5787 |
+
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
| 5788 |
+
}
|
| 5789 |
+
},
|
| 5790 |
"node_modules/function-bind": {
|
| 5791 |
"version": "1.1.2",
|
| 5792 |
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
|
|
|
| 9334 |
"node": ">=16.20.0"
|
| 9335 |
}
|
| 9336 |
},
|
| 9337 |
+
"node_modules/playwright": {
|
| 9338 |
+
"version": "1.60.0",
|
| 9339 |
+
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz",
|
| 9340 |
+
"integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==",
|
| 9341 |
+
"devOptional": true,
|
| 9342 |
+
"license": "Apache-2.0",
|
| 9343 |
+
"dependencies": {
|
| 9344 |
+
"playwright-core": "1.60.0"
|
| 9345 |
+
},
|
| 9346 |
+
"bin": {
|
| 9347 |
+
"playwright": "cli.js"
|
| 9348 |
+
},
|
| 9349 |
+
"engines": {
|
| 9350 |
+
"node": ">=18"
|
| 9351 |
+
},
|
| 9352 |
+
"optionalDependencies": {
|
| 9353 |
+
"fsevents": "2.3.2"
|
| 9354 |
+
}
|
| 9355 |
+
},
|
| 9356 |
+
"node_modules/playwright-core": {
|
| 9357 |
+
"version": "1.60.0",
|
| 9358 |
+
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz",
|
| 9359 |
+
"integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==",
|
| 9360 |
+
"devOptional": true,
|
| 9361 |
+
"license": "Apache-2.0",
|
| 9362 |
+
"bin": {
|
| 9363 |
+
"playwright-core": "cli.js"
|
| 9364 |
+
},
|
| 9365 |
+
"engines": {
|
| 9366 |
+
"node": ">=18"
|
| 9367 |
+
}
|
| 9368 |
+
},
|
| 9369 |
"node_modules/possible-typed-array-names": {
|
| 9370 |
"version": "1.1.0",
|
| 9371 |
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
|
frontend/package.json
CHANGED
|
@@ -6,7 +6,9 @@
|
|
| 6 |
"dev": "next dev",
|
| 7 |
"build": "next build",
|
| 8 |
"start": "next start",
|
| 9 |
-
"lint": "eslint"
|
|
|
|
|
|
|
| 10 |
},
|
| 11 |
"dependencies": {
|
| 12 |
"@base-ui/react": "^1.4.1",
|
|
@@ -26,6 +28,7 @@
|
|
| 26 |
"tw-animate-css": "^1.4.0"
|
| 27 |
},
|
| 28 |
"devDependencies": {
|
|
|
|
| 29 |
"@tailwindcss/postcss": "^4",
|
| 30 |
"@types/node": "^20",
|
| 31 |
"@types/react": "^19",
|
|
|
|
| 6 |
"dev": "next dev",
|
| 7 |
"build": "next build",
|
| 8 |
"start": "next start",
|
| 9 |
+
"lint": "eslint",
|
| 10 |
+
"test:e2e": "playwright test",
|
| 11 |
+
"test:e2e:ui": "playwright test --ui"
|
| 12 |
},
|
| 13 |
"dependencies": {
|
| 14 |
"@base-ui/react": "^1.4.1",
|
|
|
|
| 28 |
"tw-animate-css": "^1.4.0"
|
| 29 |
},
|
| 30 |
"devDependencies": {
|
| 31 |
+
"@playwright/test": "^1.60.0",
|
| 32 |
"@tailwindcss/postcss": "^4",
|
| 33 |
"@types/node": "^20",
|
| 34 |
"@types/react": "^19",
|
frontend/playwright.config.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { defineConfig, devices } from "@playwright/test";
|
| 2 |
+
|
| 3 |
+
const port = Number(process.env.E2E_PORT ?? 3000);
|
| 4 |
+
const baseURL = process.env.E2E_BASE_URL ?? `http://127.0.0.1:${port}`;
|
| 5 |
+
|
| 6 |
+
export default defineConfig({
|
| 7 |
+
testDir: "./e2e",
|
| 8 |
+
timeout: 30_000,
|
| 9 |
+
expect: {
|
| 10 |
+
timeout: 5_000,
|
| 11 |
+
},
|
| 12 |
+
fullyParallel: true,
|
| 13 |
+
reporter: process.env.CI ? [["github"], ["list"]] : "list",
|
| 14 |
+
use: {
|
| 15 |
+
baseURL,
|
| 16 |
+
trace: "on-first-retry",
|
| 17 |
+
},
|
| 18 |
+
webServer: {
|
| 19 |
+
command: `npm run dev -- --hostname 127.0.0.1 --port ${port}`,
|
| 20 |
+
url: baseURL,
|
| 21 |
+
reuseExistingServer: !process.env.CI,
|
| 22 |
+
timeout: 120_000,
|
| 23 |
+
},
|
| 24 |
+
projects: [
|
| 25 |
+
{
|
| 26 |
+
name: "chromium",
|
| 27 |
+
use: { ...devices["Desktop Chrome"] },
|
| 28 |
+
},
|
| 29 |
+
],
|
| 30 |
+
});
|