Saurabh Kumar Bajpai commited on
Commit
4d64819
·
1 Parent(s): 457eacc

test: add Playwright e2e coverage

Browse files
.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
+ });