tblaisaacliao commited on
Commit
a4062c9
·
1 Parent(s): 5cab93a

remove ui tests, keep api tests only

Browse files
Files changed (38) hide show
  1. CLAUDE.md +11 -69
  2. README.md +6 -14
  3. TODO.md +0 -212
  4. docs/PROMPT_MANAGEMENT_DESIGN.md +2 -2
  5. docs/backend-doc/12-database-integration.md +3 -3
  6. package.json +0 -4
  7. playwright.config.ts +1 -8
  8. tests/e2e/{admin-conversation-export-concurrent.spec.ts → admin-conversation-export-concurrent.api.spec.ts} +0 -0
  9. tests/e2e/admin-conversation-export-ui.spec.ts +0 -365
  10. tests/e2e/{admin-conversation-export.spec.ts → admin-conversation-export.api.spec.ts} +20 -13
  11. tests/e2e/admin-conversation-speaker-display.spec.ts +0 -236
  12. tests/e2e/admin-conversations.ui.spec.ts +0 -336
  13. tests/e2e/admin-create-student-prompt.spec.ts +0 -128
  14. tests/e2e/admin-dashboard.ui.spec.ts +0 -100
  15. tests/e2e/admin-users.ui.spec.ts +0 -175
  16. tests/e2e/coach-ad-prompt.spec.ts +0 -315
  17. tests/e2e/coach-chat-persistence.spec.ts +0 -145
  18. tests/e2e/coach-chat-ui-only.spec.ts +0 -248
  19. tests/e2e/coach-conversation-messaging.spec.ts +0 -124
  20. tests/e2e/coach-conversation-speaker-default.spec.ts +0 -188
  21. tests/e2e/coach-conversation-ui-buttons.spec.ts +0 -244
  22. tests/e2e/coach-guidance-mode.spec.ts +0 -192
  23. tests/e2e/complete-ui-flow.spec.ts +0 -75
  24. tests/e2e/{concurrency-race-conditions.spec.ts → concurrency-race-conditions.api.spec.ts} +0 -0
  25. tests/e2e/conversation-delete.spec.ts +0 -351
  26. tests/e2e/conversation-titles.spec.ts +0 -329
  27. tests/e2e/dashboard-many-conversations.spec.ts +0 -334
  28. tests/e2e/dashboard.spec.ts +0 -219
  29. tests/e2e/message-filter.spec.ts +0 -231
  30. tests/e2e/prompts-in-conversations.spec.ts +0 -343
  31. tests/e2e/quoted-replies.spec.ts +0 -273
  32. tests/e2e/{removed-template-handling.spec.ts → removed-template-handling.api.spec.ts} +0 -0
  33. tests/e2e/{response-id-expiration.spec.ts → response-id-expiration.api.spec.ts} +0 -0
  34. tests/e2e/speaker-auto-detection.spec.ts +0 -151
  35. tests/e2e/speaker-toggle-bug.spec.ts +0 -126
  36. tests/e2e/{title-generation-logic.spec.ts → title-generation-logic.api.spec.ts} +0 -0
  37. tests/e2e/ui-auth-redirect.spec.ts +0 -142
  38. tests/e2e/verify-no-nested-buttons.spec.ts +0 -28
CLAUDE.md CHANGED
@@ -24,11 +24,7 @@ npm run build:ci # Build + integration tests
24
  npm start # Start production server
25
 
26
  # Testing
27
- npm run test:api # Run API tests only (Playwright)
28
- npm run test:ui # Run UI tests only (Playwright)
29
- npm run test:e2e # Run all E2E tests (API + UI)
30
- npm run test:e2e:headed # Run E2E tests with visible browser
31
- npm run test:e2e:ui # Run E2E tests in interactive UI mode
32
 
33
  # Linting
34
  npm run lint # Run ESLint
@@ -237,7 +233,7 @@ TypeScript paths configured in `tsconfig.json`:
237
 
238
  **Fail-fast testing approach:**
239
  When running tests to verify features, use a fail-fast approach to save time:
240
- 1. Start the test suite (`npm run test:e2e`)
241
  2. As soon as the first test failure appears, **stop the test run immediately**
242
  3. Investigate and fix the root cause of that failure
243
  4. Re-run tests to verify the fix
@@ -245,38 +241,6 @@ When running tests to verify features, use a fail-fast approach to save time:
245
 
246
  **Rationale:** If there's a fundamental issue (e.g., server not starting, auth broken, missing dependency), continuing to run all tests just wastes time and produces misleading failure cascades. Failing fast, fixing the root cause, then re-running is far more efficient.
247
 
248
- **Test Environment Cleanup:**
249
- ⚠️ **CRITICAL**: Before running the full E2E test suite, always ensure a clean environment to avoid false test failures:
250
-
251
- 1. **Kill all background processes:**
252
- ```bash
253
- pkill -f "npm run dev" || true
254
- pkill -f "playwright test" || true
255
- lsof -ti:3000 | xargs kill -9 2>/dev/null || true
256
- ```
257
-
258
- 2. **Clean database state:**
259
- ```bash
260
- rm -f ./data/app.db
261
- ```
262
-
263
- 3. **Run tests:**
264
- ```bash
265
- npm run test:e2e
266
- ```
267
-
268
- **Why this matters:**
269
- - Multiple concurrent dev servers or test processes cause port conflicts and resource contention
270
- - Stale database entries from previous test runs can cause validation failures
271
- - Test data accumulation leads to incorrect test counts and failures
272
- - This issue has occurred multiple times and can cause 6+ spurious test failures
273
-
274
- **Symptoms of dirty environment:**
275
- - Database seed verification fails (expected 13 prompts, got 15+)
276
- - Tests pass individually but fail when run in suite
277
- - Random timeouts or port binding errors
278
- - Inconsistent test results between runs
279
-
280
  ### Testing Philosophy: NEVER Ignore Test Failures
281
 
282
  **⚠️ CRITICAL PRINCIPLE: Failed tests are YOUR responsibility until proven otherwise.**
@@ -291,8 +255,8 @@ When tests fail after making changes:
291
  **Bad Practice (What NOT to do):**
292
  ```
293
  ❌ "These look like flaky tests, probably unrelated to my changes"
294
- ❌ "UI tests are timing out, must be a pre-existing issue"
295
- ❌ "Only backend tests matter, UI failures can be ignored"
296
  ```
297
 
298
  **Good Practice (What to do):**
@@ -304,13 +268,6 @@ When tests fail after making changes:
304
  ✅ Only conclude "pre-existing" after finding evidence (e.g., git commit that broke tests)
305
  ```
306
 
307
- **Real Example from This Codebase:**
308
- Database concurrency changes passed all 58 API tests but failed 13 UI tests. Instead of assuming "unrelated flaky tests", investigation revealed:
309
- - Commit `a4c0cfb` (Oct 20) changed dashboard titles "諮詢教練" → "教練回顧"
310
- - Tests weren't updated in that commit
311
- - Failures were pre-existing, BUT required investigation to confirm
312
- - Proper fix: Update tests to match current UI + document investigation process
313
-
314
  **Key Takeaway:** Time spent investigating is NEVER wasted - it either finds bugs you introduced, or reveals technical debt to fix.
315
 
316
  ### Testing Philosophy: Intention is Critical
@@ -378,10 +335,6 @@ Choose proper design over quick fixes. Database abstractions exist for a reason
378
  - Each route.ts exports HTTP method functions (GET, POST, etc.)
379
  - Request/response use Web standard Request/Response objects
380
 
381
- 5. **Model Context Protocol (MCP):**
382
- - Configured in `.mcp.json` for Playwright browser automation
383
- - Enables UI testing capabilities within Claude Code
384
-
385
  ## Development Workflow
386
 
387
  1. **Create new student personality:**
@@ -402,13 +355,13 @@ Choose proper design over quick fixes. Database abstractions exist for a reason
402
 
403
  4. **Remove or deprecate student personality:**
404
  - See "Student Template Management" section for safe removal process
405
- - Run `npm run test:e2e` to verify graceful degradation
406
  - Monitor logs for conversations using removed templates
407
 
408
  5. **Test changes:**
409
  - Run `npm run test:basic` for quick validation
410
  - Run `npm run test` for full test suite
411
- - Run `npm run test:e2e` for end-to-end tests with real LLM
412
 
413
  ## Student Template Management
414
 
@@ -440,7 +393,7 @@ Student personality templates are defined in `src/lib/prompts/student-prompts.ts
440
 
441
  3. **Test Before Removal**: Run the test suite to verify graceful degradation
442
  ```bash
443
- npm run test:e2e # Includes removed-template-handling.spec.ts
444
  ```
445
 
446
  4. **Remove Template**: Delete the entry from `studentPrompts` in `student-prompts.ts`
@@ -475,7 +428,7 @@ Student personality templates are defined in `src/lib/prompts/student-prompts.ts
475
  2. Follow existing template structure (systemPrompt, name, description, tools)
476
  3. Test creation and messaging:
477
  ```bash
478
- npm run test:e2e
479
  ```
480
  4. Document the new personality in README.md
481
 
@@ -486,33 +439,23 @@ Conversations created before the `systemPrompt` storage feature (if any) will:
486
  2. Fail if template is removed
487
  3. **Recommended**: Manually migrate old conversations by regenerating and storing prompts
488
 
489
- ## End-to-End Testing (Playwright + Real LLM)
490
 
491
  **One-time setup:**
492
  ```bash
493
  export CZ_OPENAI_API_KEY=your-key
494
  export MODEL_NAME=gpt-5
495
- npx playwright install --with-deps
496
  ```
497
 
498
  **Run tests:**
499
  ```bash
500
- npm run test:e2e # headless mode (default, fastest) - for verification
501
- npm run test:e2e:headed # headed mode (watch browser) - for debugging
502
- npm run test:e2e:ui # interactive test UI - for development
503
  ```
504
 
505
- **Headless vs Headed:**
506
- - **Always use headless mode** (`npm run test:e2e`) for verification and CI/CD
507
- - Headless is faster (no UI rendering), uses less resources, and is the standard for automated testing
508
- - Only use headed mode (`npm run test:e2e:headed`) when debugging a specific test failure
509
- - Only use UI mode (`npm run test:e2e:ui`) when developing new tests interactively
510
-
511
  **CI setup (summary):**
512
  - Add `CZ_OPENAI_API_KEY` (and optionally `MODEL_NAME`) to job env
513
  - `npm ci`
514
- - `npx playwright install --with-deps`
515
- - `npm run test:e2e`
516
 
517
  **Troubleshooting:**
518
  - **Agents list not empty**: remove local state under `./data` (and optionally `/tmp/ai-sdk-agents.json` if you previously used legacy flows)
@@ -520,4 +463,3 @@ npm run test:e2e:ui # interactive test UI - for development
520
  - **LLM 401/429**: verify key and model access; tests run with `workers: 1` to avoid rate limits
521
  - **Timeouts**: allow 60–90s for LLM responses
522
  - **Port 3000 in use**: stop the other server, or set `reuseExistingServer=false` temporarily
523
- - **Missing browsers**: rerun `npx playwright install --with-deps`
 
24
  npm start # Start production server
25
 
26
  # Testing
27
+ npm run test:api # Run API tests (Playwright)
 
 
 
 
28
 
29
  # Linting
30
  npm run lint # Run ESLint
 
233
 
234
  **Fail-fast testing approach:**
235
  When running tests to verify features, use a fail-fast approach to save time:
236
+ 1. Start the test suite (`npm run test:api`)
237
  2. As soon as the first test failure appears, **stop the test run immediately**
238
  3. Investigate and fix the root cause of that failure
239
  4. Re-run tests to verify the fix
 
241
 
242
  **Rationale:** If there's a fundamental issue (e.g., server not starting, auth broken, missing dependency), continuing to run all tests just wastes time and produces misleading failure cascades. Failing fast, fixing the root cause, then re-running is far more efficient.
243
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
244
  ### Testing Philosophy: NEVER Ignore Test Failures
245
 
246
  **⚠️ CRITICAL PRINCIPLE: Failed tests are YOUR responsibility until proven otherwise.**
 
255
  **Bad Practice (What NOT to do):**
256
  ```
257
  ❌ "These look like flaky tests, probably unrelated to my changes"
258
+ ❌ "Tests are timing out, must be a pre-existing issue"
259
+ ❌ "Only some tests matter, other failures can be ignored"
260
  ```
261
 
262
  **Good Practice (What to do):**
 
268
  ✅ Only conclude "pre-existing" after finding evidence (e.g., git commit that broke tests)
269
  ```
270
 
 
 
 
 
 
 
 
271
  **Key Takeaway:** Time spent investigating is NEVER wasted - it either finds bugs you introduced, or reveals technical debt to fix.
272
 
273
  ### Testing Philosophy: Intention is Critical
 
335
  - Each route.ts exports HTTP method functions (GET, POST, etc.)
336
  - Request/response use Web standard Request/Response objects
337
 
 
 
 
 
338
  ## Development Workflow
339
 
340
  1. **Create new student personality:**
 
355
 
356
  4. **Remove or deprecate student personality:**
357
  - See "Student Template Management" section for safe removal process
358
+ - Run `npm run test:api` to verify graceful degradation
359
  - Monitor logs for conversations using removed templates
360
 
361
  5. **Test changes:**
362
  - Run `npm run test:basic` for quick validation
363
  - Run `npm run test` for full test suite
364
+ - Run `npm run test:api` for API tests with real LLM
365
 
366
  ## Student Template Management
367
 
 
393
 
394
  3. **Test Before Removal**: Run the test suite to verify graceful degradation
395
  ```bash
396
+ npm run test:api # Includes removed-template-handling.api.spec.ts
397
  ```
398
 
399
  4. **Remove Template**: Delete the entry from `studentPrompts` in `student-prompts.ts`
 
428
  2. Follow existing template structure (systemPrompt, name, description, tools)
429
  3. Test creation and messaging:
430
  ```bash
431
+ npm run test:api
432
  ```
433
  4. Document the new personality in README.md
434
 
 
439
  2. Fail if template is removed
440
  3. **Recommended**: Manually migrate old conversations by regenerating and storing prompts
441
 
442
+ ## End-to-End Testing (Playwright API Tests)
443
 
444
  **One-time setup:**
445
  ```bash
446
  export CZ_OPENAI_API_KEY=your-key
447
  export MODEL_NAME=gpt-5
 
448
  ```
449
 
450
  **Run tests:**
451
  ```bash
452
+ npm run test:api # Run API tests
 
 
453
  ```
454
 
 
 
 
 
 
 
455
  **CI setup (summary):**
456
  - Add `CZ_OPENAI_API_KEY` (and optionally `MODEL_NAME`) to job env
457
  - `npm ci`
458
+ - `npm run test:api`
 
459
 
460
  **Troubleshooting:**
461
  - **Agents list not empty**: remove local state under `./data` (and optionally `/tmp/ai-sdk-agents.json` if you previously used legacy flows)
 
463
  - **LLM 401/429**: verify key and model access; tests run with `workers: 1` to avoid rate limits
464
  - **Timeouts**: allow 60–90s for LLM responses
465
  - **Port 3000 in use**: stop the other server, or set `reuseExistingServer=false` temporarily
 
README.md CHANGED
@@ -104,14 +104,8 @@ The project uses Playwright for end-to-end testing with separate test suites for
104
  ### Running Tests
105
 
106
  ```bash
107
- # Run only API tests (fastest - for quick verification)
108
  npm run test:api
109
-
110
- # Run only UI tests (when testing browser interactions)
111
- npm run test:ui
112
-
113
- # Run all tests (API + UI)
114
- npm run test:e2e
115
  ```
116
 
117
  ### Test Files
@@ -133,14 +127,12 @@ Example API tests:
133
  3. Catches most issues without browser overhead
134
 
135
  **Before committing:**
136
- 1. Run `npm run test:e2e` to verify both API and UI
137
  2. Ensures complete system integration
138
- 3. Tests real user flows with browser interactions
139
 
140
- **Speed comparison:**
141
- - API tests only: ~10-20 seconds ⚡
142
- - UI tests only: ~60-90 seconds 🐢
143
- - All tests: ~70-110 seconds
144
 
145
  ### Configuration
146
 
@@ -372,7 +364,7 @@ To change AI behavior, edit prompts in `src/lib/prompts/`:
372
  When removing a student personality template:
373
  - ✅ **Existing conversations continue working** - System prompts are stored with each conversation during creation
374
  - ❌ **New conversations are blocked** - Validation prevents creating conversations with removed templates
375
- - ⚠️ **Test before removing** - Run `npm run test:e2e` to verify graceful degradation
376
  - See `CLAUDE.md` "Student Template Management" section for detailed removal process and migration guidance
377
 
378
  ### Research Foundation
 
104
  ### Running Tests
105
 
106
  ```bash
107
+ # Run API tests
108
  npm run test:api
 
 
 
 
 
 
109
  ```
110
 
111
  ### Test Files
 
127
  3. Catches most issues without browser overhead
128
 
129
  **Before committing:**
130
+ 1. Run `npm run test:api` to verify all API functionality
131
  2. Ensures complete system integration
132
+ 3. Tests real conversation flows with LLM
133
 
134
+ **Speed:**
135
+ - API tests: ~10-20 seconds ⚡
 
 
136
 
137
  ### Configuration
138
 
 
364
  When removing a student personality template:
365
  - ✅ **Existing conversations continue working** - System prompts are stored with each conversation during creation
366
  - ❌ **New conversations are blocked** - Validation prevents creating conversations with removed templates
367
+ - ⚠️ **Test before removing** - Run `npm run test:api` to verify graceful degradation
368
  - See `CLAUDE.md` "Student Template Management" section for detailed removal process and migration guidance
369
 
370
  ### Research Foundation
TODO.md DELETED
@@ -1,212 +0,0 @@
1
- # TODO: Remove studentName and coachName Fields
2
-
3
- This document tracks the progress of removing redundant `studentName` and `coachName` fields from the database and deriving them from prompt templates instead.
4
-
5
- ## Context
6
-
7
- Previously, these fields were stored in the database for quick display, but they can be derived from static template configurations (`student-prompts.ts` and `coach-prompts.ts`). This change:
8
- - Reduces data redundancy
9
- - Ensures names always match the source of truth (prompt templates)
10
- - Simplifies the database schema
11
-
12
- ## Progress
13
-
14
- ### ✅ Completed Tasks
15
-
16
- - [x] **Database Schema** - Removed `student_name` and `coach_name` columns from `src/lib/db/schema.sql`
17
- - [x] **TypeScript Models** - Removed fields from `Conversation` interface in `src/lib/types/models.ts`
18
- - [x] **Repository Methods** - Updated `ConversationRepository.createConversation()` to remove name parameters
19
- - [x] **API Routes** - Updated 6 routes to derive names from templates:
20
- - [x] `src/app/api/conversations/create/route.ts` (lines 51-93)
21
- - [x] `src/app/api/conversations/[conversationId]/branch/route.ts` (lines 52-95)
22
- - [x] `src/app/api/conversations/route.ts` (lines 17-49)
23
- - [x] `src/app/api/conversations/[conversationId]/route.ts` (lines 32-115)
24
- - [x] `src/app/api/stats/route.ts` (lines 39-56)
25
- - [x] `src/app/api/conversations/[conversationId]/generate-coach-summary/route.ts` (lines 55-62)
26
- - [x] **Verify Script** - Updated `scripts/verify-backend.sh` to use `grade_3` instead of deprecated `adhd_inattentive`
27
- - [x] **Database Cleanup** - Deleted old database and cleared Next.js cache
28
-
29
- ### ✅ Completed Verification
30
-
31
- - [x] **Run Backend Verification**
32
- ```bash
33
- ./scripts/verify-backend.sh
34
- ```
35
- - ✅ All API endpoints return 200/201 with correct data
36
- - ✅ Conversation responses include derived `studentName` and `coachName`
37
- - ✅ Backend verification passing 100%
38
-
39
- - [x] **Fixed Database Schema**
40
- - Updated inline schema in `src/lib/db/index.ts` to use `student_prompt_id` and `coach_prompt_id`
41
- - Removed old `student_name`, `coach_name`, `student_personality`, `coach_id` columns
42
- - Database recreated with correct schema
43
-
44
- - [x] **Fixed E2E Test Selectors**
45
- - Updated 11 test files to use current dashboard UI text
46
- - Changed selector from `h3:has-text("新對話")` to `h3:has-text("指定對話演練")`
47
- - Tests now properly find dashboard action buttons
48
-
49
- - [ ] **Run E2E Tests** (Currently running)
50
- ```bash
51
- npm run test:e2e
52
- ```
53
- - Expected: All 105 tests should pass
54
- - Status: Running full suite after fixes
55
-
56
- ### ❓ Potential Issues to Investigate
57
-
58
- - [ ] **Frontend Components** - Verify these components work correctly:
59
- - `src/app/dashboard/page.tsx` - Uses `conv.studentName` and `conv.coachName` (lines 224, 499, 521, 525, 527)
60
- - `src/app/conversation/[id]/page.tsx` - Uses `conversation.studentName` and `conversation.coachName` (lines 318, 321, 500, 670)
61
- - **Status**: Should work fine since API responses include derived names via spread operator
62
- - **Action**: Test manually by creating conversations and checking UI displays correct names
63
-
64
- - [ ] **API Type Definitions** - Check if `src/lib/types/api.ts` needs updates:
65
- - **Action**: Review ConversationResponse types to ensure they match actual API responses
66
- - **Note**: API responses include `studentName` and `coachName` via spread operator, so type definitions might be okay
67
-
68
- - [ ] **E2E Test Fixes** - If tests fail, check for:
69
- - Tests that directly query database for `studentName`/`coachName`
70
- - Tests that create conversations with old API format
71
- - Tests using deprecated `adhd_inattentive` personality ID
72
- - **Action**: Search codebase for any remaining references:
73
- ```bash
74
- grep -r "adhd_inattentive" tests/
75
- grep -r "student_name\|studentName" tests/ --include="*.ts"
76
- grep -r "coach_name\|coachName" tests/ --include="*.ts"
77
- ```
78
-
79
- ### 📝 Documentation Updates Needed
80
-
81
- - [ ] **Update API Documentation** (`docs/backend-doc/`)
82
- - [ ] Review all docs to ensure no references to storing studentName/coachName
83
- - [ ] Add note explaining names are derived from templates
84
- - [ ] Update example responses to show names are included but not stored
85
-
86
- - [ ] **Update README.md**
87
- - [ ] Document the template-based naming system
88
- - [ ] Explain that names come from `student-prompts.ts` and `coach-prompts.ts`
89
-
90
- - [ ] **Update CLAUDE.md**
91
- - [ ] Update "Student Template Management" section if needed
92
- - [ ] Add note about derived names in API responses
93
-
94
- - [ ] **OpenAPI Spec** (`docs/openapi.yaml` and `public/docs/openapi.yaml`)
95
- - [ ] Update Conversation schema to mark studentName/coachName as "derived" in description
96
- - [ ] Clarify they're not stored in database but returned in API responses
97
-
98
- ## Testing Checklist
99
-
100
- ### Backend API Testing
101
-
102
- Run verification script:
103
- ```bash
104
- ./scripts/verify-backend.sh
105
- ```
106
-
107
- Expected results:
108
- - ✅ All endpoints return 200/201/400 as expected
109
- - ✅ Created conversations include `studentName` and `coachName` in response
110
- - ✅ Conversation list shows correct names for each conversation
111
- - ✅ Stats endpoint shows correct student names
112
-
113
- ### Manual UI Testing
114
-
115
- 1. **Create New Conversation**
116
- - Navigate to dashboard
117
- - Click "新對話" (New Conversation)
118
- - Select a student personality (e.g., 小三學生)
119
- - Select a coach (e.g., 陳老師)
120
- - Verify conversation card shows correct student and coach names
121
-
122
- 2. **View Conversation**
123
- - Open a conversation
124
- - Verify header shows correct student and coach names
125
- - Verify messages show correct speaker names/avatars
126
-
127
- 3. **Branch Conversation**
128
- - Open a conversation with messages
129
- - Click branch button on a message
130
- - Verify branched conversation shows correct names
131
-
132
- 4. **Coach Direct Chat**
133
- - Navigate to "諮詢教練" page
134
- - Create coach-direct conversation
135
- - Verify it shows "直接教練對話" as student name
136
- - Verify it shows correct coach name
137
-
138
- ### E2E Test Suite
139
-
140
- Run full test suite:
141
- ```bash
142
- npm run test:e2e
143
- ```
144
-
145
- Expected: All 105 tests pass
146
-
147
- If tests fail:
148
- 1. Check error messages for database schema mismatches
149
- 2. Look for tests expecting studentName/coachName in database
150
- 3. Fix tests to expect derived names in API responses instead
151
-
152
- ## Rollback Plan (If Needed)
153
-
154
- If critical issues are found:
155
-
156
- 1. **Revert Database Schema**
157
- ```sql
158
- ALTER TABLE conversations ADD COLUMN student_name TEXT NOT NULL DEFAULT '';
159
- ALTER TABLE conversations ADD COLUMN coach_name TEXT NOT NULL DEFAULT '';
160
- ```
161
-
162
- 2. **Revert TypeScript Models** - Add fields back to `Conversation` interface
163
-
164
- 3. **Revert Repository Methods** - Add name parameters back to `createConversation()`
165
-
166
- 4. **Revert API Routes** - Remove derivation logic, use stored names
167
-
168
- 5. **Git Revert**
169
- ```bash
170
- git log --oneline # Find commit hash
171
- git revert <commit-hash>
172
- ```
173
-
174
- ## Success Criteria
175
-
176
- The implementation is complete when:
177
-
178
- - ✅ Backend verification script passes 100%
179
- - ✅ All E2E tests pass (105/105)
180
- - ✅ Manual UI testing shows correct names everywhere
181
- - ✅ No database references to student_name or coach_name remain
182
- - ✅ Documentation is updated
183
- - ✅ Code review confirms all API responses include derived names
184
-
185
- ## Notes
186
-
187
- - **Database Migration**: No data migration needed - just delete old database and let it recreate
188
- - **API Compatibility**: API responses still include `studentName` and `coachName`, so frontend doesn't need changes
189
- - **Template Removal**: System gracefully handles removed templates by using stored `systemPrompt` field
190
- - **Performance**: Deriving names on-the-fly has negligible performance impact (simple object lookup)
191
-
192
- ## Related Files
193
-
194
- **Modified Files:**
195
- - `src/lib/db/schema.sql`
196
- - `src/lib/types/models.ts`
197
- - `src/lib/repositories/conversation-repository.ts`
198
- - `src/app/api/conversations/create/route.ts`
199
- - `src/app/api/conversations/[conversationId]/branch/route.ts`
200
- - `src/app/api/conversations/route.ts`
201
- - `src/app/api/conversations/[conversationId]/route.ts`
202
- - `src/app/api/stats/route.ts`
203
- - `src/app/api/conversations/[conversationId]/generate-coach-summary/route.ts`
204
- - `scripts/verify-backend.sh`
205
-
206
- **Files Using Derived Names (Frontend):**
207
- - `src/app/dashboard/page.tsx`
208
- - `src/app/conversation/[id]/page.tsx`
209
-
210
- **Source of Truth:**
211
- - `src/lib/prompts/student-prompts.ts` - Student names and descriptions
212
- - `src/lib/prompts/coach-prompts.ts` - Coach names and descriptions
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
docs/PROMPT_MANAGEMENT_DESIGN.md CHANGED
@@ -959,8 +959,8 @@ If critical issues arise during deployment:
959
  ### Next Steps for Deployment
960
 
961
  1. Restart development server
962
- 2. Run E2E tests: `npm run test:e2e`
963
- 3. Verify all 23 new E2E tests pass
964
  4. Build Docker image: `docker build -t sel-chat-coach .`
965
  5. Test Docker container locally
966
  6. Deploy to staging
 
959
  ### Next Steps for Deployment
960
 
961
  1. Restart development server
962
+ 2. Run API tests: `npm run test:api`
963
+ 3. Verify all tests pass
964
  4. Build Docker image: `docker build -t sel-chat-coach .`
965
  5. Test Docker container locally
966
  6. Deploy to staging
docs/backend-doc/12-database-integration.md CHANGED
@@ -1113,7 +1113,7 @@ rm -f ./data/app.db # SQLite
1113
  # Or truncate tables in Supabase
1114
 
1115
  # Run tests
1116
- npm run test:e2e
1117
  ```
1118
 
1119
  ---
@@ -1317,10 +1317,10 @@ npm run dev
1317
  2. **Test with both providers before deploying:**
1318
  ```bash
1319
  # Test SQLite
1320
- DATABASE_PROVIDER=sqlite npm run test:e2e
1321
 
1322
  # Test Supabase (separate test project)
1323
- DATABASE_PROVIDER=supabase npm run test:e2e
1324
  ```
1325
 
1326
  3. **Use Supabase for production:**
 
1113
  # Or truncate tables in Supabase
1114
 
1115
  # Run tests
1116
+ npm run test:api
1117
  ```
1118
 
1119
  ---
 
1317
  2. **Test with both providers before deploying:**
1318
  ```bash
1319
  # Test SQLite
1320
+ DATABASE_PROVIDER=sqlite npm run test:api
1321
 
1322
  # Test Supabase (separate test project)
1323
+ DATABASE_PROVIDER=supabase npm run test:api
1324
  ```
1325
 
1326
  3. **Use Supabase for production:**
package.json CHANGED
@@ -9,10 +9,6 @@
9
  "lint": "eslint",
10
  "db:seed": "tsx src/scripts/seed-prompts.ts",
11
  "test:api": "playwright test --project=api",
12
- "test:ui": "playwright test --project=ui",
13
- "test:e2e": "playwright test",
14
- "test:e2e:headed": "playwright test --headed",
15
- "test:e2e:ui": "playwright test --ui",
16
  "stress-test": "tsx scripts/stress-test/index.ts"
17
  },
18
  "dependencies": {
 
9
  "lint": "eslint",
10
  "db:seed": "tsx src/scripts/seed-prompts.ts",
11
  "test:api": "playwright test --project=api",
 
 
 
 
12
  "stress-test": "tsx scripts/stress-test/index.ts"
13
  },
14
  "dependencies": {
playwright.config.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { defineConfig, devices } from '@playwright/test';
2
  import * as dotenv from 'dotenv';
3
  import * as path from 'path';
4
 
@@ -27,13 +27,6 @@ export default defineConfig({
27
  // API tests only use request fixture - no browser needed
28
  // This makes them much faster to run
29
  },
30
- {
31
- name: 'ui',
32
- testMatch: /.*\.(spec|test)\.ts$/,
33
- testIgnore: [/.*\.api\.spec\.ts$/, /.*\.setup\.ts$/],
34
- dependencies: ['setup'], // Wait for setup to complete
35
- use: { ...devices['Desktop Chrome'] },
36
- },
37
  ],
38
  webServer: {
39
  command: `PORT=3000 NEXT_TELEMETRY_DISABLED=1 npm run db:seed && PORT=3000 NEXT_TELEMETRY_DISABLED=1 npm run build && PORT=3000 NODE_ENV=production NEXT_TELEMETRY_DISABLED=1 npm start`,
 
1
+ import { defineConfig } from '@playwright/test';
2
  import * as dotenv from 'dotenv';
3
  import * as path from 'path';
4
 
 
27
  // API tests only use request fixture - no browser needed
28
  // This makes them much faster to run
29
  },
 
 
 
 
 
 
 
30
  ],
31
  webServer: {
32
  command: `PORT=3000 NEXT_TELEMETRY_DISABLED=1 npm run db:seed && PORT=3000 NEXT_TELEMETRY_DISABLED=1 npm run build && PORT=3000 NODE_ENV=production NEXT_TELEMETRY_DISABLED=1 npm start`,
tests/e2e/{admin-conversation-export-concurrent.spec.ts → admin-conversation-export-concurrent.api.spec.ts} RENAMED
File without changes
tests/e2e/admin-conversation-export-ui.spec.ts DELETED
@@ -1,365 +0,0 @@
1
- import { test, expect, Page } from '@playwright/test';
2
-
3
- const BASE_URL = 'http://localhost:3000';
4
-
5
- test.describe('Admin Conversation TSV Export UI', () => {
6
- let testConversationId: string;
7
- let testUsername: string;
8
-
9
- test.beforeAll(async ({ request }) => {
10
- // Create test user and conversation
11
- testUsername = `exportuitest_${Date.now()}`;
12
- const registerResponse = await request.post(`${BASE_URL}/api/auth/register`, {
13
- data: { username: testUsername },
14
- });
15
- const userData = await registerResponse.json();
16
-
17
- // Create conversation with messages
18
- const password = process.env.BASIC_AUTH_PASSWORD || 'cz-2025';
19
- const authHeader = `Basic ${Buffer.from(`${testUsername}:${password}`).toString('base64')}`;
20
-
21
- const convResponse = await request.post(`${BASE_URL}/api/conversations/create`, {
22
- headers: { Authorization: authHeader },
23
- data: {
24
- studentPromptId: 'ruirui',
25
- coachPromptId: 'satir',
26
- studentDescription: 'Test conversation for export UI',
27
- },
28
- });
29
- const convData = await convResponse.json();
30
- testConversationId = convData.conversation.id;
31
-
32
- // Send test messages
33
- await request.post(`${BASE_URL}/api/conversations/${testConversationId}/message`, {
34
- headers: { Authorization: authHeader },
35
- data: {
36
- message: 'Test message for export',
37
- },
38
- });
39
- });
40
-
41
- test.describe('Desktop View', () => {
42
- test.beforeEach(async ({ page }) => {
43
- await page.setViewportSize({ width: 1280, height: 720 });
44
- await page.goto('/admin/conversations');
45
- await page.waitForSelector('table', { timeout: 10000 });
46
- });
47
-
48
- test('should show Download link next to View Messages in table', async ({ page }) => {
49
- // Wait for table to load
50
- await page.waitForSelector('a:has-text("View Messages")', { timeout: 10000 });
51
-
52
- // Check that Download links exist in the same row as View Messages
53
- const downloadLinks = page.locator('a:has-text("Download")');
54
- const downloadCount = await downloadLinks.count();
55
-
56
- expect(downloadCount).toBeGreaterThan(0);
57
-
58
- // Verify first Download link is visible and has correct styling
59
- const firstDownloadLink = downloadLinks.first();
60
- await expect(firstDownloadLink).toBeVisible();
61
-
62
- // Check that it has the green color class
63
- const className = await firstDownloadLink.getAttribute('class');
64
- expect(className).toContain('text-green-600');
65
- });
66
-
67
- test('should have Download links with correct href pointing to export API', async ({ page }) => {
68
- await page.waitForSelector('a:has-text("Download")', { timeout: 10000 });
69
-
70
- const downloadLinks = page.locator('a:has-text("Download")');
71
- const firstLink = downloadLinks.first();
72
-
73
- // Verify href points to export TSV endpoint
74
- const href = await firstLink.getAttribute('href');
75
- expect(href).toContain('/api/admin/conversations/');
76
- expect(href).toContain('/export/tsv');
77
- });
78
-
79
- test('should have download attribute on Download links', async ({ page }) => {
80
- await page.waitForSelector('a:has-text("Download")', { timeout: 10000 });
81
-
82
- const downloadLinks = page.locator('a:has-text("Download")');
83
- const firstLink = downloadLinks.first();
84
-
85
- // Verify download attribute exists (triggers download instead of navigation)
86
- const hasDownloadAttr = await firstLink.evaluate(el => el.hasAttribute('download'));
87
- expect(hasDownloadAttr).toBe(true);
88
- });
89
-
90
- test('should show Download and View Messages links side by side', async ({ page }) => {
91
- await page.waitForSelector('a:has-text("View Messages")', { timeout: 10000 });
92
-
93
- // Get first row's action cell
94
- const firstRowActionCell = page.locator('td').filter({ has: page.locator('a:has-text("View Messages")') }).first();
95
-
96
- // Should contain both links
97
- await expect(firstRowActionCell.locator('a:has-text("View Messages")')).toBeVisible();
98
- await expect(firstRowActionCell.locator('a:has-text("Download")')).toBeVisible();
99
-
100
- // Should be in a flex container
101
- const flexContainer = firstRowActionCell.locator('div.flex');
102
- await expect(flexContainer).toBeVisible();
103
- });
104
-
105
- test('should maintain table layout with Download link added', async ({ page }) => {
106
- // Verify table headers are still correct
107
- await expect(page.locator('th:has-text("Title")')).toBeVisible();
108
- await expect(page.locator('th:has-text("User")')).toBeVisible();
109
- await expect(page.locator('th:has-text("Student")')).toBeVisible();
110
- await expect(page.locator('th:has-text("Coach")')).toBeVisible();
111
- await expect(page.locator('th:has-text("Messages")')).toBeVisible();
112
- await expect(page.locator('th:has-text("Last Active")')).toBeVisible();
113
- await expect(page.locator('th:has-text("Actions")')).toBeVisible();
114
-
115
- // Verify each row has both action links
116
- const rows = page.locator('tbody tr');
117
- const rowCount = await rows.count();
118
-
119
- if (rowCount > 0) {
120
- // Check first few rows
121
- for (let i = 0; i < Math.min(rowCount, 3); i++) {
122
- const row = rows.nth(i);
123
- await expect(row.locator('a:has-text("View Messages")')).toBeVisible();
124
- await expect(row.locator('a:has-text("Download")')).toBeVisible();
125
- }
126
- }
127
- });
128
- });
129
-
130
- test.describe('Mobile View', () => {
131
- test.beforeEach(async ({ page }) => {
132
- await page.setViewportSize({ width: 375, height: 667 });
133
- await page.goto('/admin/conversations');
134
- // Wait for mobile-specific content (the mobile card layout has Student/Coach labels that desktop table doesn't)
135
- await page.waitForSelector('text=Student:', { timeout: 10000 });
136
- });
137
-
138
- test('should show Download button in mobile card view', async ({ page }) => {
139
- // Wait for mobile cards to load (specifically looking for mobile layout structure)
140
- await expect(page.locator('text=Student:').first()).toBeVisible();
141
-
142
- // Get the mobile card container specifically (md:hidden means mobile-only)
143
- const mobileContainer = page.locator('div.md\\:hidden');
144
-
145
- // Check that Download buttons exist within mobile cards
146
- const downloadButtons = mobileContainer.locator('a:has-text("Download")');
147
- const downloadCount = await downloadButtons.count();
148
-
149
- expect(downloadCount).toBeGreaterThan(0);
150
-
151
- // Verify first Download button is visible
152
- await expect(downloadButtons.first()).toBeVisible();
153
- });
154
-
155
- test('should have Download button with green styling in mobile view', async ({ page }) => {
156
- await expect(page.locator('text=Student:').first()).toBeVisible();
157
-
158
- // Get mobile container and find Download button within it
159
- const mobileContainer = page.locator('div.md\\:hidden');
160
- const downloadButton = mobileContainer.locator('a:has-text("Download")').first();
161
-
162
- // Wait for it to be visible
163
- await expect(downloadButton).toBeVisible();
164
-
165
- // Check that it has the green background class
166
- const className = await downloadButton.getAttribute('class');
167
- expect(className).toContain('bg-green-600');
168
- expect(className).toContain('text-white');
169
- });
170
-
171
- test('should show Download and View Messages buttons side by side in mobile cards', async ({ page }) => {
172
- await expect(page.locator('text=Student:').first()).toBeVisible();
173
-
174
- // Get mobile container and first card within it
175
- const mobileContainer = page.locator('div.md\\:hidden');
176
- const firstCard = mobileContainer.locator('.bg-white.rounded-lg.shadow').first();
177
-
178
- // Should contain both buttons
179
- await expect(firstCard.locator('a:has-text("View Messages")')).toBeVisible();
180
- await expect(firstCard.locator('a:has-text("Download")')).toBeVisible();
181
-
182
- // Should be in a flex container
183
- const flexContainer = firstCard.locator('div.flex.gap-2').last();
184
- await expect(flexContainer).toBeVisible();
185
- });
186
-
187
- test('should have equal width buttons in mobile view', async ({ page }) => {
188
- await expect(page.locator('text=Student:').first()).toBeVisible();
189
-
190
- // Get mobile container and first card within it
191
- const mobileContainer = page.locator('div.md\\:hidden');
192
- const firstCard = mobileContainer.locator('.bg-white.rounded-lg.shadow').first();
193
- const viewButton = firstCard.locator('a:has-text("View Messages")');
194
- const downloadButton = firstCard.locator('a:has-text("Download")');
195
-
196
- // Both should have flex-1 class for equal width
197
- const viewClass = await viewButton.getAttribute('class');
198
- const downloadClass = await downloadButton.getAttribute('class');
199
-
200
- expect(viewClass).toContain('flex-1');
201
- expect(downloadClass).toContain('flex-1');
202
- });
203
- });
204
-
205
- test.describe('Download Functionality', () => {
206
- test.beforeEach(async ({ page }) => {
207
- await page.goto('/admin/conversations');
208
- await page.waitForSelector('a:has-text("Download")', { timeout: 10000 });
209
- });
210
-
211
- test('should trigger download when Download link is clicked', async ({ page, context }) => {
212
- // Listen for download event
213
- const downloadPromise = page.waitForEvent('download');
214
-
215
- // Find and click the Download link for our test conversation
216
- const downloadLinks = page.locator('a:has-text("Download")');
217
- const linkCount = await downloadLinks.count();
218
-
219
- // Click the first download link (any conversation will do for this test)
220
- if (linkCount > 0) {
221
- await downloadLinks.first().click();
222
-
223
- // Wait for download to start
224
- const download = await downloadPromise;
225
-
226
- // Verify download
227
- expect(download).toBeTruthy();
228
-
229
- // Verify filename format
230
- const filename = download.suggestedFilename();
231
- expect(filename).toMatch(/^conversation_.*\.tsv$/);
232
- expect(filename).toContain('.tsv');
233
- }
234
- });
235
-
236
- test('should download valid TSV file', async ({ page, context }) => {
237
- // Find download link for our specific test conversation
238
- const downloadLink = page.locator(`a[href*="${testConversationId}"][href*="export/tsv"]`).first();
239
-
240
- if (await downloadLink.count() > 0) {
241
- // Wait for download
242
- const downloadPromise = page.waitForEvent('download');
243
- await downloadLink.click();
244
- const download = await downloadPromise;
245
-
246
- // Save the download to verify content
247
- const path = await download.path();
248
- expect(path).toBeTruthy();
249
-
250
- // Read and verify TSV structure (basic check)
251
- if (path) {
252
- const fs = require('fs');
253
- const content = fs.readFileSync(path, 'utf-8');
254
-
255
- // Should have header row
256
- expect(content).toContain('Conversation Title');
257
- expect(content).toContain('Username');
258
- expect(content).toContain('Student Name');
259
- expect(content).toContain('Coach Name');
260
- expect(content).toContain('Message Role');
261
- expect(content).toContain('Speaker');
262
- expect(content).toContain('Content');
263
- expect(content).toContain('Timestamp');
264
-
265
- // Should have data rows (at least one message)
266
- const lines = content.split('\n');
267
- expect(lines.length).toBeGreaterThan(1);
268
- }
269
- } else {
270
- // Test conversation not visible on current page, that's OK
271
- console.log('Test conversation not visible on current page, skipping download verification');
272
- }
273
- });
274
- });
275
-
276
- test.describe('Accessibility', () => {
277
- test.beforeEach(async ({ page }) => {
278
- await page.goto('/admin/conversations');
279
- await page.waitForSelector('a:has-text("Download")', { timeout: 10000 });
280
- });
281
-
282
- test('should have proper link semantics', async ({ page }) => {
283
- const downloadLink = page.locator('a:has-text("Download")').first();
284
-
285
- // Should be an anchor element
286
- const tagName = await downloadLink.evaluate(el => el.tagName);
287
- expect(tagName).toBe('A');
288
-
289
- // Should have href attribute
290
- const href = await downloadLink.getAttribute('href');
291
- expect(href).toBeTruthy();
292
- });
293
-
294
- test('should be keyboard accessible', async ({ page }) => {
295
- // Tab to a Download link
296
- await page.keyboard.press('Tab');
297
-
298
- // Keep tabbing until we find a Download link with focus
299
- let attempts = 0;
300
- let foundDownloadLink = false;
301
-
302
- while (attempts < 50 && !foundDownloadLink) {
303
- const focusedElement = await page.evaluate(() => {
304
- const el = document.activeElement;
305
- return el ? el.textContent : '';
306
- });
307
-
308
- if (focusedElement?.includes('Download')) {
309
- foundDownloadLink = true;
310
- break;
311
- }
312
-
313
- await page.keyboard.press('Tab');
314
- attempts++;
315
- }
316
-
317
- // If we found a Download link, verify it can be activated with Enter
318
- if (foundDownloadLink) {
319
- // Verify focused element is a Download link
320
- const focusedElement = await page.evaluate(() => {
321
- const el = document.activeElement;
322
- return {
323
- text: el?.textContent || '',
324
- href: (el as HTMLAnchorElement)?.href || '',
325
- };
326
- });
327
-
328
- expect(focusedElement.text).toContain('Download');
329
- expect(focusedElement.href).toContain('/export/tsv');
330
- }
331
- });
332
- });
333
-
334
- test.describe('Visual Consistency', () => {
335
- test.beforeEach(async ({ page }) => {
336
- await page.goto('/admin/conversations');
337
- await page.waitForSelector('a:has-text("Download")', { timeout: 10000 });
338
- });
339
-
340
- test('should have consistent color scheme', async ({ page }) => {
341
- const downloadLink = page.locator('a:has-text("Download")').first();
342
- const viewLink = page.locator('a:has-text("View Messages")').first();
343
-
344
- // Download should be green
345
- const downloadClass = await downloadLink.getAttribute('class');
346
- expect(downloadClass).toMatch(/text-green-600|bg-green-600/);
347
-
348
- // View Messages should be blue
349
- const viewClass = await viewLink.getAttribute('class');
350
- expect(viewClass).toMatch(/text-blue-600|bg-blue-600/);
351
- });
352
-
353
- test('should have proper spacing between links', async ({ page }) => {
354
- await page.setViewportSize({ width: 1280, height: 720 });
355
-
356
- // Get action cell with both links
357
- const actionCell = page.locator('td').filter({ has: page.locator('a:has-text("Download")') }).first();
358
- const flexContainer = actionCell.locator('div.flex');
359
-
360
- // Should have gap class
361
- const className = await flexContainer.getAttribute('class');
362
- expect(className).toContain('gap');
363
- });
364
- });
365
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/e2e/{admin-conversation-export.spec.ts → admin-conversation-export.api.spec.ts} RENAMED
@@ -482,19 +482,26 @@ test.describe('Admin Conversation TSV Export API', () => {
482
  };
483
 
484
  // Parse TSV into structured data
485
- const rows = lines.slice(1).map(line => {
486
- const fields = parseTsvLine(line);
487
- return {
488
- conversationTitle: fields[0],
489
- username: fields[1],
490
- studentName: fields[2],
491
- coachName: fields[3],
492
- messageRole: fields[4],
493
- speaker: fields[5],
494
- content: fields[6],
495
- timestamp: fields[7],
496
- };
497
- });
 
 
 
 
 
 
 
498
 
499
  // Expected structure:
500
  // Row 0: System message (role: system, speaker: null or empty)
 
482
  };
483
 
484
  // Parse TSV into structured data
485
+ // Note: Using || '' to prevent undefined errors when split('\n') breaks multi-line quoted fields
486
+ const rows = lines.slice(1)
487
+ .map(line => {
488
+ const fields = parseTsvLine(line);
489
+ // Filter out partial rows from multi-line content (they have < 5 fields)
490
+ if (fields.length < 5) {
491
+ return null;
492
+ }
493
+ return {
494
+ conversationTitle: fields[0] || '',
495
+ username: fields[1] || '',
496
+ studentName: fields[2] || '',
497
+ coachName: fields[3] || '',
498
+ messageRole: fields[4] || '',
499
+ speaker: fields[5] || '',
500
+ content: fields[6] || '',
501
+ timestamp: fields[7] || '',
502
+ };
503
+ })
504
+ .filter(row => row !== null); // Remove null rows (partial lines)
505
 
506
  // Expected structure:
507
  // Row 0: System message (role: system, speaker: null or empty)
tests/e2e/admin-conversation-speaker-display.spec.ts DELETED
@@ -1,236 +0,0 @@
1
- import { test, expect } from '@playwright/test';
2
-
3
- const BASE_URL = 'http://localhost:3000';
4
-
5
- test.describe('Admin Conversation Detail Page - Speaker Display', () => {
6
- let testUserId: string;
7
- let testUsername: string;
8
- let testConversationId: string;
9
- const testPassword = process.env.BASIC_AUTH_PASSWORD || 'cz-2025';
10
-
11
- test.beforeAll(async ({ request }) => {
12
- // Create a test user
13
- const timestamp = Date.now();
14
- testUsername = `speaker_display_test_${timestamp}`;
15
-
16
- const registerResponse = await request.post(`${BASE_URL}/api/auth/register`, {
17
- data: {
18
- username: testUsername,
19
- },
20
- });
21
-
22
- expect(registerResponse.ok()).toBeTruthy();
23
- const userData = await registerResponse.json();
24
- testUserId = userData.user.id;
25
-
26
- // Create Basic Auth header
27
- const authHeader = `Basic ${Buffer.from(`${testUsername}:${testPassword}`).toString('base64')}`;
28
-
29
- // Create a test conversation with both student and coach messages
30
- const createConvResponse = await request.post(`${BASE_URL}/api/conversations/create`, {
31
- headers: {
32
- Authorization: authHeader,
33
- },
34
- data: {
35
- studentPromptId: 'ruirui',
36
- coachPromptId: 'satir',
37
- studentDescription: 'Test student for speaker display verification',
38
- },
39
- });
40
-
41
- expect(createConvResponse.ok()).toBeTruthy();
42
- const convData = await createConvResponse.json();
43
- testConversationId = convData.conversation.id;
44
-
45
- // Send a message to the student (should trigger student response with speaker='student')
46
- const studentMsgResponse = await request.post(`${BASE_URL}/api/conversations/${testConversationId}/message`, {
47
- headers: {
48
- Authorization: authHeader,
49
- },
50
- data: {
51
- messages: [
52
- { id: 'msg1', role: 'user', parts: [{ type: 'text', text: 'Hello student, how are you?' }] }
53
- ]
54
- },
55
- timeout: 90_000,
56
- });
57
- expect(studentMsgResponse.ok()).toBeTruthy();
58
-
59
- // Send a message to the coach (should trigger coach response with speaker='coach')
60
- const coachMsgResponse = await request.post(`${BASE_URL}/api/conversations/${testConversationId}/message`, {
61
- headers: {
62
- Authorization: authHeader,
63
- },
64
- data: {
65
- messages: [
66
- {
67
- id: 'msg2',
68
- role: 'user',
69
- parts: [{ type: 'text', text: '@coach Can you help me understand this student?' }],
70
- metadata: { speaker: 'coach' } // Set speaker in metadata
71
- }
72
- ],
73
- },
74
- timeout: 90_000,
75
- });
76
- expect(coachMsgResponse.ok()).toBeTruthy();
77
- });
78
-
79
- test('Admin API should return messages with speaker field (not in metadata)', async ({ request }) => {
80
- const response = await request.get(
81
- `${BASE_URL}/api/admin/conversations/${testConversationId}`
82
- );
83
-
84
- expect(response.ok()).toBeTruthy();
85
- const data = await response.json();
86
-
87
- // Verify we have messages
88
- expect(data.messages.length).toBeGreaterThan(0);
89
-
90
- // Find student and coach messages
91
- const studentMessages = data.messages.filter((msg: any) => msg.speaker === 'student');
92
- const coachMessages = data.messages.filter((msg: any) => msg.speaker === 'coach');
93
-
94
- // Should have at least one student message (from first exchange)
95
- expect(studentMessages.length).toBeGreaterThan(0);
96
-
97
- // Should have at least one coach message (from second exchange)
98
- expect(coachMessages.length).toBeGreaterThan(0);
99
-
100
- // Verify speaker is a direct field, not in metadata
101
- for (const msg of data.messages) {
102
- if (msg.role === 'assistant') {
103
- // Speaker should be a direct field on the message object
104
- expect(msg).toHaveProperty('speaker');
105
- expect(['student', 'coach']).toContain(msg.speaker);
106
-
107
- // Speaker should NOT be in metadata (that's for UI streaming messages)
108
- expect(msg.metadata).toBeUndefined();
109
- }
110
- }
111
- });
112
-
113
- test('Admin page should display BOTH student and coach names based on speaker field', async ({ page }) => {
114
- // Navigate to admin conversation detail page
115
- await page.goto(`${BASE_URL}/admin/conversations/${testConversationId}`);
116
-
117
- // Wait for messages to load
118
- await page.waitForSelector('[class*="space-y-4"]', { timeout: 10000 });
119
-
120
- // Get all text content from the page
121
- const pageContent = await page.locator('[class*="space-y-4"]').textContent();
122
-
123
- // Should contain BOTH student and coach names
124
- expect(pageContent).toContain('睿睿');
125
- expect(pageContent).toContain('薩提爾');
126
-
127
- // Verify names appear in name labels (outside bubbles)
128
- const nameLabels = await page.locator('[class*="text-xs"][class*="font-semibold"]').allTextContents();
129
-
130
- const hasStudentName = nameLabels.some(label => label.includes('睿睿'));
131
- const hasCoachName = nameLabels.some(label => label.includes('薩提爾'));
132
-
133
- expect(hasStudentName).toBeTruthy();
134
- expect(hasCoachName).toBeTruthy();
135
- });
136
-
137
- test('Admin page should NOT show name labels for user messages (right side)', async ({ page }) => {
138
- // Navigate to admin conversation detail page
139
- await page.goto(`${BASE_URL}/admin/conversations/${testConversationId}`);
140
-
141
- // Wait for messages to load
142
- await page.waitForSelector('[class*="space-y-4"]', { timeout: 10000 });
143
-
144
- // Visual verification: page should have the correct structure
145
- // User messages should exist but not include name labels
146
- const pageContent = await page.textContent('body');
147
-
148
- // Page should contain user messages (we sent them)
149
- expect(pageContent).toContain('Hello student');
150
- expect(pageContent).toContain('@coach');
151
-
152
- // Test passes if we can visually confirm layout is correct
153
- // (This is a smoke test - manual visual confirmation is primary)
154
- expect(pageContent).toBeTruthy();
155
- });
156
-
157
- test('Admin page should show correct emojis for student vs coach messages', async ({ page }) => {
158
- // Navigate to admin conversation detail page
159
- await page.goto(`${BASE_URL}/admin/conversations/${testConversationId}`);
160
-
161
- // Wait for messages to load
162
- await page.waitForSelector('[class*="space-y-4"]', { timeout: 10000 });
163
-
164
- // Get all page content
165
- const pageContent = await page.textContent('body');
166
-
167
- // Should contain both student and coach names
168
- expect(pageContent).toContain('睿睿');
169
- expect(pageContent).toContain('薩提爾');
170
-
171
- // Emojis may not be captured reliably in text content
172
- // Visual confirmation is primary (test passes if page loads correctly)
173
- expect(pageContent).toBeTruthy();
174
- });
175
-
176
- test('Admin page should have avatar circles for assistant messages', async ({ page }) => {
177
- // Navigate to admin conversation detail page
178
- await page.goto(`${BASE_URL}/admin/conversations/${testConversationId}`);
179
-
180
- // Wait for messages to load
181
- await page.waitForSelector('[class*="space-y-4"]', { timeout: 10000 });
182
-
183
- // Visual verification: page should display avatars with modern styling
184
- const pageContent = await page.textContent('body');
185
-
186
- // Should contain conversation participants
187
- expect(pageContent).toContain('睿睿');
188
- expect(pageContent).toContain('薩提爾');
189
-
190
- // Avatar styling verified via manual visual review
191
- // Test passes if page loads correctly with all content
192
- expect(pageContent).toBeTruthy();
193
- });
194
-
195
- test('Admin page should use modern styling for message bubbles', async ({ page }) => {
196
- // Navigate to admin conversation detail page
197
- await page.goto(`${BASE_URL}/admin/conversations/${testConversationId}`);
198
-
199
- // Wait for messages to load
200
- await page.waitForSelector('[class*="space-y-4"]', { timeout: 10000 });
201
-
202
- // Visual verification: page should display messages with modern styling
203
- const pageContent = await page.textContent('body');
204
-
205
- // Should contain conversation content
206
- expect(pageContent).toContain('睿睿');
207
- expect(pageContent).toContain('薩提爾');
208
- expect(pageContent).toContain('Hello student');
209
-
210
- // Smoke test for visual presentation
211
- // Gradients and specific styling verified via manual review
212
- expect(pageContent).toBeTruthy();
213
- });
214
-
215
- test('Admin page should show name labels positioned correctly', async ({ page }) => {
216
- // Navigate to admin conversation detail page
217
- await page.goto(`${BASE_URL}/admin/conversations/${testConversationId}`);
218
-
219
- // Wait for messages to load
220
- await page.waitForSelector('[class*="space-y-4"]', { timeout: 10000 });
221
-
222
- // Verify page contains both student and coach names
223
- const pageContent = await page.textContent('body');
224
-
225
- expect(pageContent).toContain('睿睿');
226
- expect(pageContent).toContain('薩提爾');
227
-
228
- // Verify names appear multiple times (once in header info, multiple times in messages)
229
- const studentNameCount = (pageContent?.match(/睿睿/g) || []).length;
230
- const coachNameCount = (pageContent?.match(/薩提爾/g) || []).length;
231
-
232
- // Should appear at least twice each (header + at least one message)
233
- expect(studentNameCount).toBeGreaterThanOrEqual(2);
234
- expect(coachNameCount).toBeGreaterThanOrEqual(2);
235
- });
236
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/e2e/admin-conversations.ui.spec.ts DELETED
@@ -1,336 +0,0 @@
1
- import { test, expect } from '@playwright/test';
2
-
3
- const BASE_URL = 'http://localhost:3000';
4
-
5
- test.describe('Admin Conversations UI', () => {
6
- let testConversationId: string;
7
- let testUsername: string;
8
-
9
- test.beforeAll(async ({ request }) => {
10
- // Create test user and conversation for UI tests
11
- testUsername = `convuitest_${Date.now()}`;
12
- const registerResponse = await request.post(`${BASE_URL}/api/auth/register`, {
13
- data: { username: testUsername },
14
- });
15
- const userData = await registerResponse.json();
16
-
17
- // Create conversation
18
- const password = process.env.BASIC_AUTH_PASSWORD || 'cz-2025';
19
- const authHeader = `Basic ${Buffer.from(`${testUsername}:${password}`).toString('base64')}`;
20
-
21
- const convResponse = await request.post(`${BASE_URL}/api/conversations/create`, {
22
- headers: { Authorization: authHeader },
23
- data: {
24
- studentPromptId: 'ruirui',
25
- coachPromptId: 'satir',
26
- studentDescription: 'UI test conversation',
27
- },
28
- });
29
- const convData = await convResponse.json();
30
- testConversationId = convData.conversation.id;
31
- });
32
-
33
- test.describe('Conversations List Page', () => {
34
- test.beforeEach(async ({ page }) => {
35
- await page.goto('/admin/conversations');
36
- });
37
-
38
- test('should load conversations list page', async ({ page }) => {
39
- await expect(page.getByRole('heading', { name: 'Conversations' })).toBeVisible();
40
- });
41
-
42
- test('should display conversations table on desktop', async ({ page }) => {
43
- await page.setViewportSize({ width: 1280, height: 720 });
44
- await page.waitForSelector('table, .space-y-4', { timeout: 10000 });
45
-
46
- // Should show table headers
47
- await expect(page.locator('th:has-text("User")')).toBeVisible();
48
- await expect(page.locator('th:has-text("Title")')).toBeVisible();
49
- await expect(page.locator('th:has-text("Student")')).toBeVisible();
50
- await expect(page.locator('th:has-text("Coach")')).toBeVisible();
51
- });
52
-
53
- test('should display conversations cards on mobile', async ({ page }) => {
54
- await page.setViewportSize({ width: 375, height: 667 });
55
- await page.waitForSelector('.bg-white.rounded-lg.shadow', { timeout: 10000 });
56
-
57
- const cards = page.locator('.bg-white.rounded-lg.shadow');
58
- await expect(cards.first()).toBeVisible();
59
- });
60
-
61
- test('should have filter controls', async ({ page }) => {
62
- // Should have filter options (user, student, coach)
63
- await page.waitForSelector('select, input[type="search"]', { timeout: 10000 });
64
- await expect(page.locator('select, input[type="search"]')).toBeVisible();
65
- });
66
-
67
- test('should show results info', async ({ page }) => {
68
- // Check for the results text in the specific div
69
- await page.waitForSelector('text=Showing', { timeout: 10000 });
70
- const resultsDiv = page.locator('div.mt-4.text-sm.text-gray-600');
71
- await expect(resultsDiv).toContainText('Showing');
72
- await expect(resultsDiv).toContainText('conversations');
73
- });
74
-
75
- test('should navigate to conversation detail', async ({ page }) => {
76
- await page.waitForSelector('a[href*="/admin/conversations/"]', { timeout: 10000 });
77
-
78
- // Click first "View Messages" link
79
- const firstConvLink = page.locator('a[href*="/admin/conversations/"]:has-text("View Messages")').first();
80
- await firstConvLink.click();
81
-
82
- // Should navigate to detail page
83
- await page.waitForURL('**/admin/conversations/**');
84
- // Page shows conversation title, just verify a heading exists
85
- await expect(page.getByRole('heading').first()).toBeVisible();
86
- });
87
- });
88
-
89
- test.describe('Conversation Detail Page', () => {
90
- test.beforeEach(async ({ page }) => {
91
- await page.goto(`/admin/conversations/${testConversationId}`);
92
- });
93
-
94
- test('should load conversation detail page', async ({ page }) => {
95
- // Page shows the conversation title as h1, wait for any heading to appear
96
- await expect(page.getByRole('heading').first()).toBeVisible();
97
- });
98
-
99
- test('should display conversation metadata', async ({ page }) => {
100
- // Wait for conversation info to load
101
- await page.waitForSelector('text=User', { timeout: 10000 });
102
-
103
- // Should show student and coach persona labels
104
- await expect(page.locator('text=Student Persona')).toBeVisible();
105
- await expect(page.locator('text=Coach Persona')).toBeVisible();
106
- });
107
-
108
- test('should show conversation title', async ({ page }) => {
109
- // The page shows the conversation title in the h1, not a separate ID field
110
- await expect(page.getByRole('heading')).toBeVisible();
111
- });
112
-
113
- test('should display messages section', async ({ page }) => {
114
- await expect(page.locator('text=Messages')).toBeVisible();
115
- });
116
-
117
- test('should have back to conversations button', async ({ page }) => {
118
- const backLink = page.locator('a[href="/admin/conversations"]:has-text("Back to Conversations")');
119
- await expect(backLink).toBeVisible();
120
-
121
- await backLink.click();
122
- await page.waitForURL('**/admin/conversations');
123
- });
124
-
125
- test('should show system prompt if available', async ({ page }) => {
126
- // System prompt section should be visible or display "No system prompt"
127
- const hasSystemPrompt = await page.locator('text=System Prompt').isVisible();
128
- if (hasSystemPrompt) {
129
- await expect(page.locator('text=System Prompt')).toBeVisible();
130
- }
131
- });
132
-
133
- test('should not render unexpected characters (like "0") in message bubbles', async ({ page }) => {
134
- // Wait for page to load
135
- await page.waitForLoadState('networkidle');
136
-
137
- // Check if Messages section exists
138
- const messagesHeading = page.locator('h2:has-text("Messages")');
139
- await expect(messagesHeading).toBeVisible({ timeout: 10000 });
140
-
141
- // Get all message bubble containers
142
- const messageBubbles = page.locator('.rounded-lg.p-4').filter({
143
- has: page.locator('.text-sm.whitespace-pre-wrap')
144
- });
145
-
146
- const messageCount = await messageBubbles.count();
147
-
148
- // If there are messages, verify none have unexpected "0" characters
149
- if (messageCount > 0) {
150
- for (let i = 0; i < messageCount; i++) {
151
- const bubble = messageBubbles.nth(i);
152
- const bubbleText = await bubble.textContent();
153
-
154
- // Message bubbles should not end with a standalone "0"
155
- // This would indicate the isHistorical field (0) is being rendered
156
- expect(bubbleText?.trim().endsWith('\n0')).toBe(false);
157
- expect(bubbleText?.trim().endsWith(' 0')).toBe(false);
158
-
159
- // Also check that the bubble doesn't contain "0" as the last line
160
- const lines = bubbleText?.split('\n').map(l => l.trim()).filter(l => l.length > 0) || [];
161
- if (lines.length > 0) {
162
- const lastLine = lines[lines.length - 1];
163
- // Last line should not be a standalone "0" (unless it's actually part of the message content)
164
- if (lastLine === '0') {
165
- // This is the bug - isHistorical (0) is being rendered
166
- throw new Error(`Found unexpected "0" character in message bubble ${i}. This likely means isHistorical field is being rendered.`);
167
- }
168
- }
169
- }
170
-
171
- console.log(`✅ Verified ${messageCount} message bubbles have no unexpected "0" characters`);
172
- } else {
173
- // No messages is acceptable - the test conversation might not have messages yet
174
- console.log('ℹ️ No messages found - test passes (conversation has no messages)');
175
- }
176
- });
177
-
178
- test('should handle non-existent conversation', async ({ page }) => {
179
- await page.goto('/admin/conversations/00000000-0000-0000-0000-000000000000');
180
-
181
- // Should show error
182
- await expect(page.locator('text=Error')).toBeVisible();
183
- await expect(page.locator('text=Conversation not found')).toBeVisible();
184
- });
185
- });
186
-
187
- test.describe('Prompts Pages', () => {
188
- test.describe('Prompts List', () => {
189
- test.beforeEach(async ({ page }) => {
190
- await page.goto('/admin/prompts');
191
- });
192
-
193
- test('should load prompts list page', async ({ page }) => {
194
- await expect(page.getByRole('heading', { name: '提示詞管理' })).toBeVisible();
195
- });
196
-
197
- test('should display prompts table', async ({ page }) => {
198
- await page.waitForSelector('table', { timeout: 10000 });
199
-
200
- // Table headers are in Chinese
201
- await expect(page.locator('th:has-text("名稱")')).toBeVisible(); // Name
202
- await expect(page.locator('th:has-text("類型")')).toBeVisible(); // Type
203
- await expect(page.locator('th:has-text("狀態")')).toBeVisible(); // Status
204
- });
205
-
206
- test('should show student prompts', async ({ page }) => {
207
- // Wait for table to load, then check for student prompt IDs
208
- await page.waitForSelector('table', { timeout: 10000 });
209
- await expect(page.locator('td:has-text("ruirui")').first()).toBeVisible();
210
- });
211
-
212
- test('should show coach prompts', async ({ page }) => {
213
- // Wait for table to load, then check for coach prompt IDs
214
- await page.waitForSelector('table', { timeout: 10000 });
215
- await expect(page.locator('td:has-text("satir")').first()).toBeVisible();
216
- });
217
-
218
- test('should have filter dropdowns', async ({ page }) => {
219
- // Check that filter select elements exist
220
- const selects = page.locator('select');
221
- await expect(selects.first()).toBeVisible();
222
- // Verify the dropdowns have the expected options by checking select count
223
- const selectCount = await selects.count();
224
- expect(selectCount).toBeGreaterThanOrEqual(2); // Type and Status filters
225
- });
226
-
227
- test('should filter by student prompts', async ({ page }) => {
228
- // Use first select (type filter) to filter to student prompts
229
- await page.selectOption('select >> nth=0', 'student');
230
-
231
- // Should show student prompts
232
- await expect(page.locator('td:has-text("ruirui")').first()).toBeVisible();
233
- });
234
-
235
- test('should filter by coach prompts', async ({ page }) => {
236
- // Use first select (type filter) to filter to coach prompts
237
- await page.selectOption('select >> nth=0', 'coach');
238
-
239
- // Should show coach prompts
240
- await expect(page.locator('td:has-text("satir")').first()).toBeVisible();
241
- });
242
-
243
- test('should have create new prompt button', async ({ page }) => {
244
- // Button text is "新增提示詞" (Chinese for "Add Prompt")
245
- await expect(page.locator('a[href="/admin/prompts/new"]')).toBeVisible();
246
- await expect(page.locator('text=新增提示詞')).toBeVisible();
247
- });
248
-
249
- test('should navigate to prompt detail', async ({ page }) => {
250
- await page.waitForSelector('a[href*="/admin/prompts/ruirui"]', { timeout: 10000 });
251
-
252
- await page.click('a[href*="/admin/prompts/ruirui"]');
253
- await page.waitForURL('**/admin/prompts/ruirui');
254
-
255
- // Heading is in Chinese: "編輯提示詞"
256
- await expect(page.getByRole('heading', { name: '編輯提示詞' })).toBeVisible();
257
- });
258
- });
259
-
260
- test.describe('Prompt Detail', () => {
261
- test.beforeEach(async ({ page }) => {
262
- await page.goto('/admin/prompts/ruirui');
263
- });
264
-
265
- test('should load prompt edit page', async ({ page }) => {
266
- await expect(page.getByRole('heading', { name: '編輯提示詞' })).toBeVisible();
267
- });
268
-
269
- test('should display prompt form fields', async ({ page }) => {
270
- await page.waitForSelector('input[name="name"], #name', { timeout: 10000 });
271
-
272
- // Should have name field
273
- await expect(page.locator('input[name="name"], #name')).toBeVisible();
274
-
275
- // Should have description field
276
- await expect(page.locator('textarea[name="description"], #description')).toBeVisible();
277
-
278
- // Should have system prompt field
279
- await expect(page.locator('textarea[name="systemPrompt"], #systemPrompt')).toBeVisible();
280
- });
281
-
282
- test('should show prompt type info', async ({ page }) => {
283
- // Type info is displayed but label text is in Chinese, just verify page loaded
284
- await expect(page.getByRole('heading')).toBeVisible();
285
- });
286
-
287
- test('should have save button', async ({ page }) => {
288
- // Button text is "儲存變更" (Save Changes)
289
- await expect(page.locator('button:has-text("儲存變更")')).toBeVisible();
290
- });
291
-
292
- test('should have back button', async ({ page }) => {
293
- // Cancel button text is "取消" (Cancel)
294
- const backLink = page.locator('a[href="/admin/prompts"]:has-text("取消")');
295
- await expect(backLink).toBeVisible();
296
-
297
- await backLink.click();
298
- await page.waitForURL('**/admin/prompts');
299
- });
300
- });
301
-
302
- test.describe('Create Prompt Page', () => {
303
- test.beforeEach(async ({ page }) => {
304
- await page.goto('/admin/prompts/new');
305
- });
306
-
307
- test('should load create prompt page', async ({ page }) => {
308
- // Heading is "新增提示詞" (Create New Prompt)
309
- await expect(page.getByRole('heading', { name: '新增提示詞' })).toBeVisible();
310
- });
311
-
312
- test('should have form fields', async ({ page }) => {
313
- // Wait for form to load
314
- await page.waitForSelector('form', { timeout: 10000 });
315
-
316
- // Should have text input fields (ID, name, etc.)
317
- const inputs = page.locator('input[type="text"]');
318
- await expect(inputs.first()).toBeVisible();
319
-
320
- // Should have radio buttons for type selection (student/coach)
321
- const radios = page.locator('input[type="radio"]');
322
- await expect(radios.first()).toBeVisible();
323
- });
324
-
325
- test('should have create button', async ({ page }) => {
326
- // Button text is "建立提示詞" (Create Prompt)
327
- await expect(page.locator('button:has-text("建立提示詞")')).toBeVisible();
328
- });
329
-
330
- test('should have cancel/back button', async ({ page }) => {
331
- // Cancel button text is "取消"
332
- await expect(page.locator('a[href="/admin/prompts"]:has-text("取消")')).toBeVisible();
333
- });
334
- });
335
- });
336
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/e2e/admin-create-student-prompt.spec.ts DELETED
@@ -1,128 +0,0 @@
1
- import { test, expect } from '@playwright/test';
2
-
3
- /**
4
- * E2E test: Admin creates student prompt → Student appears in new conversation modal
5
- *
6
- * This test verifies the complete user flow:
7
- * 1. Admin creates a new student prompt via admin UI
8
- * 2. Navigate to dashboard
9
- * 3. Open "new conversation" modal
10
- * 4. Verify the newly created student appears in the list
11
- */
12
-
13
- const baseURL = process.env.BASE_URL || 'http://localhost:3000';
14
-
15
- // Helper function to register and login
16
- async function registerAndLogin(page: any, username: string) {
17
- await page.goto(`${baseURL}/login`);
18
- await page.click('text=沒有帳號?建立新帳號');
19
- await page.fill('input[name="username"]', username);
20
- await page.click('button:has-text("建立帳號")');
21
- await expect(page).toHaveURL(`${baseURL}/dashboard`, { timeout: 10_000 });
22
- }
23
-
24
- test.describe('Admin Create Student Prompt UI Flow', () => {
25
- test('should create student prompt and see it in new conversation modal', async ({ page }) => {
26
- // Step 0: Register and login
27
- const testUser = `admin_prompt_test_${Date.now()}`;
28
- await registerAndLogin(page, testUser);
29
-
30
- // Step 1: Navigate to admin new prompt page
31
- await page.goto(`${baseURL}/admin/prompts/new`);
32
- await page.waitForLoadState('networkidle');
33
-
34
- // Step 2: Fill in the form for a new student prompt
35
- const uniqueId = `e2e_test_student_${Date.now()}`;
36
- const studentName = `E2E Test Student`;
37
-
38
- await page.fill('#id', uniqueId);
39
- await page.fill('#name', studentName);
40
- await page.fill('#description', 'E2E test student description');
41
-
42
- // Select student type (radio buttons, not select)
43
- await page.click('input[type="radio"][value="student"]');
44
-
45
- // Fill system prompt (must be at least 50 characters)
46
- await page.fill(
47
- '#systemPrompt',
48
- 'This is an E2E test student prompt with more than 50 characters for testing purposes.'
49
- );
50
-
51
- // Step 3: Submit the form
52
- await page.click('button[type="submit"]');
53
-
54
- // Wait for redirect or success
55
- await page.waitForURL(/\/admin\/prompts/, { timeout: 5000 });
56
-
57
- // Step 4: Navigate to dashboard
58
- await page.goto(`${baseURL}/dashboard`);
59
- await page.waitForLoadState('networkidle');
60
-
61
- // Wait for dashboard to load (wait for "指定對話演練" card which always exists)
62
- await expect(page.locator('h3:has-text("指定對話演練")')).toBeVisible({ timeout: 10000 });
63
-
64
- // Step 5: Click "指定對話演練" card to open modal
65
- await page.click('h3:has-text("指定對話演練")');
66
-
67
- // Wait for student selection modal to appear
68
- await expect(page.locator('h3:has-text("選擇學生")')).toBeVisible({ timeout: 5000 });
69
-
70
- // Wait for student list to load
71
- await expect(page.locator('h3:has-text("選擇學生")')).toBeVisible({ timeout: 5000 });
72
-
73
- // Step 7: Verify the newly created student appears in the list
74
- const newStudentButton = page.locator(`text=${studentName}`).first();
75
- await expect(newStudentButton).toBeVisible({ timeout: 5000 });
76
-
77
- console.log(`✅ Student "${studentName}" successfully appears in new conversation modal`);
78
- });
79
-
80
- test('should immediately reflect newly created student without page refresh', async ({ page }) => {
81
- // Step 0: Register and login
82
- const testUser = `admin_immediate_test_${Date.now()}`;
83
- await registerAndLogin(page, testUser);
84
-
85
- // Navigate to dashboard first
86
- await page.goto(`${baseURL}/dashboard`);
87
- await page.waitForLoadState('networkidle');
88
-
89
- // Open new conversation modal and check initial student count
90
- await page.click('h3:has-text("指定對話演練")');
91
- await expect(page.locator('h3:has-text("選擇學生")')).toBeVisible({ timeout: 5000 });
92
-
93
- const initialStudents = await page.locator('[class*="student"]').count();
94
-
95
- // Close modal by clicking outside (click on backdrop)
96
- await page.click('.fixed.inset-0');
97
- await expect(page.locator('h3:has-text("選擇學生")')).not.toBeVisible({ timeout: 5000 });
98
-
99
- // Create new student via admin page in new tab
100
- const uniqueId = `e2e_immediate_${Date.now()}`;
101
- const studentName = `Immediate Test Student`;
102
-
103
- await page.goto(`${baseURL}/admin/prompts/new`);
104
- await page.fill('#id', uniqueId);
105
- await page.fill('#name', studentName);
106
- await page.fill('#description', 'Immediate test');
107
- await page.click('input[type="radio"][value="student"]');
108
- await page.fill(
109
- '#systemPrompt',
110
- 'Immediate test student prompt with at least 50 characters for validation.'
111
- );
112
- await page.click('button[type="submit"]');
113
- await page.waitForURL(/\/admin\/prompts/, { timeout: 5000 });
114
-
115
- // Go back to dashboard
116
- await page.goto(`${baseURL}/dashboard`);
117
- await page.waitForLoadState('networkidle');
118
-
119
- // Re-open modal - should now show the new student
120
- await page.click('h3:has-text("指定對話演練")');
121
- await expect(page.locator('h3:has-text("選擇學生")')).toBeVisible({ timeout: 5000 });
122
-
123
- // Verify new student appears
124
- await expect(page.locator(`text=${studentName}`).first()).toBeVisible({ timeout: 5000 });
125
-
126
- console.log(`✅ Newly created student appears immediately after creation`);
127
- });
128
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/e2e/admin-dashboard.ui.spec.ts DELETED
@@ -1,100 +0,0 @@
1
- import { test, expect } from '@playwright/test';
2
-
3
- test.describe('Admin Dashboard UI', () => {
4
- test.beforeEach(async ({ page }) => {
5
- await page.goto('/admin');
6
- });
7
-
8
- test('should load dashboard page successfully', async ({ page }) => {
9
- await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
10
- });
11
-
12
- test('should display statistics cards', async ({ page }) => {
13
- // Wait for stats to load
14
- await page.waitForSelector('text=Total Users', { timeout: 10000 });
15
-
16
- // Check for all stat categories (look for h3 headings in the cards)
17
- await expect(page.locator('h3.text-sm:has-text("Total Users")')).toBeVisible();
18
- await expect(page.locator('h3.text-sm:has-text("Conversations")')).toBeVisible();
19
- await expect(page.locator('h3.text-sm:has-text("Total Messages")')).toBeVisible();
20
- await expect(page.locator('h3.text-sm:has-text("Prompts")')).toBeVisible();
21
- });
22
-
23
- test('should display health metrics table', async ({ page }) => {
24
- // Wait for health data to load
25
- await page.waitForSelector('text=Database Health', { timeout: 10000 });
26
-
27
- await expect(page.locator('text=Database Health')).toBeVisible();
28
-
29
- // Check for table headers
30
- await expect(page.locator('text=Table')).toBeVisible();
31
- await expect(page.locator('text=Row Count')).toBeVisible();
32
- await expect(page.locator('text=Last Modified')).toBeVisible();
33
- });
34
-
35
- test('should show expected database tables', async ({ page }) => {
36
- await page.waitForSelector('text=Database Health', { timeout: 10000 });
37
-
38
- // Verify all expected tables are listed - look for td with exact table names
39
- await expect(page.locator('td.px-6.py-4:has-text("users")').first()).toBeVisible();
40
- await expect(page.locator('td.px-6.py-4:has-text("conversations")').first()).toBeVisible();
41
- await expect(page.locator('td.px-6.py-4:has-text("messages")').first()).toBeVisible();
42
- await expect(page.locator('td.px-6.py-4:has-text("sessions")').first()).toBeVisible();
43
- await expect(page.locator('td.px-6.py-4:has-text("prompt_templates")').first()).toBeVisible();
44
- });
45
-
46
- test('should have navigation links', async ({ page }) => {
47
- // Check main navigation links exist (using .first() since there are sidebar + quick links)
48
- await expect(page.locator('a[href="/admin"]').first()).toBeVisible();
49
- await expect(page.locator('a[href="/admin/users"]').first()).toBeVisible();
50
- await expect(page.locator('a[href="/admin/conversations"]').first()).toBeVisible();
51
- await expect(page.locator('a[href="/admin/prompts"]').first()).toBeVisible();
52
- });
53
-
54
- test('should navigate to users page', async ({ page }) => {
55
- await page.click('a[href="/admin/users"]');
56
- await page.waitForURL('**/admin/users');
57
- await expect(page.getByRole('heading', { name: 'Users' })).toBeVisible();
58
- });
59
-
60
- test('should navigate to conversations page', async ({ page }) => {
61
- await page.click('a[href="/admin/conversations"]');
62
- await page.waitForURL('**/admin/conversations');
63
- await expect(page.getByRole('heading', { name: 'Conversations' })).toBeVisible();
64
- });
65
-
66
- test('should navigate to prompts page', async ({ page }) => {
67
- await page.click('a[href="/admin/prompts"]');
68
- await page.waitForURL('**/admin/prompts');
69
- await expect(page.getByRole('heading', { name: '提示詞管理' })).toBeVisible();
70
- });
71
-
72
- test('should display reasonable stat values', async ({ page }) => {
73
- await page.waitForSelector('text=Total Users', { timeout: 10000 });
74
-
75
- // Get stat values from the cards (numbers are in p tags inside stat cards)
76
- const statCards = page.locator('.bg-white.rounded-lg.shadow.p-6');
77
- const firstCardNumber = statCards.first().locator('p.text-3xl');
78
- const value = await firstCardNumber.textContent();
79
-
80
- // Should be a number
81
- expect(value?.trim()).toMatch(/^\d+$/);
82
- });
83
-
84
- test('should handle loading state gracefully', async ({ page }) => {
85
- // Reload to see loading state
86
- const responsePromise = page.waitForResponse(response =>
87
- response.url().includes('/api/admin/stats') && response.status() === 200
88
- );
89
-
90
- await page.reload();
91
-
92
- // Should show some content while loading (skeleton or spinner)
93
- await expect(page.locator('body')).toBeVisible();
94
-
95
- await responsePromise;
96
-
97
- // Stats should appear after loading
98
- await expect(page.locator('text=Total Users')).toBeVisible();
99
- });
100
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/e2e/admin-users.ui.spec.ts DELETED
@@ -1,175 +0,0 @@
1
- import { test, expect } from '@playwright/test';
2
-
3
- const BASE_URL = 'http://localhost:3000';
4
-
5
- test.describe('Admin Users UI', () => {
6
- let testUserId: string;
7
- let testUsername: string;
8
-
9
- test.beforeAll(async ({ request }) => {
10
- // Create a test user for UI tests
11
- testUsername = `uitest_${Date.now()}`;
12
- const response = await request.post(`${BASE_URL}/api/auth/register`, {
13
- data: { username: testUsername },
14
- });
15
-
16
- const data = await response.json();
17
- testUserId = data.user.id;
18
- });
19
-
20
- test.describe('Users List Page', () => {
21
- test.beforeEach(async ({ page }) => {
22
- await page.goto('/admin/users');
23
- });
24
-
25
- test('should load users list page', async ({ page }) => {
26
- await expect(page.getByRole('heading', { name: 'Users Management' })).toBeVisible();
27
- });
28
-
29
- test('should display users table on desktop', async ({ page }) => {
30
- // Wait for data to load
31
- await page.waitForSelector('table, .space-y-4', { timeout: 10000 });
32
-
33
- // On desktop, should show table
34
- await page.setViewportSize({ width: 1280, height: 720 });
35
- await page.waitForSelector('table', { timeout: 5000 });
36
-
37
- // Verify table headers
38
- await expect(page.locator('th:has-text("Username")')).toBeVisible();
39
- await expect(page.locator('th:has-text("Conversations")')).toBeVisible();
40
- await expect(page.locator('th:has-text("Messages")')).toBeVisible();
41
- });
42
-
43
- test('should display users cards on mobile', async ({ page }) => {
44
- // Set mobile viewport
45
- await page.setViewportSize({ width: 375, height: 667 });
46
-
47
- // Wait for cards to appear
48
- await page.waitForSelector('.bg-white.rounded-lg.shadow', { timeout: 10000 });
49
-
50
- // Should show cards instead of table
51
- const cards = page.locator('.bg-white.rounded-lg.shadow');
52
- await expect(cards.first()).toBeVisible();
53
- });
54
-
55
- test('should show search input', async ({ page }) => {
56
- await expect(page.locator('input[placeholder*="Search"]')).toBeVisible();
57
- });
58
-
59
- test('should search for users', async ({ page }) => {
60
- await page.waitForSelector('input[placeholder*="Search"]', { timeout: 10000 });
61
-
62
- const searchInput = page.locator('input[placeholder*="Search"]');
63
- await searchInput.fill(testUsername);
64
-
65
- // Wait for search to complete (expect automatically waits for element)
66
- await expect(page.locator(`td:has-text("${testUsername}"), h3:has-text("${testUsername}")`).first()).toBeVisible({ timeout: 1000 });
67
- });
68
-
69
- test('should show results count', async ({ page }) => {
70
- await page.waitForSelector('text=Showing', { timeout: 10000 });
71
-
72
- // Should display results count "Showing X of Y users"
73
- await expect(page.locator('div.mt-4.text-sm.text-gray-600')).toContainText('Showing');
74
- await expect(page.locator('div.mt-4.text-sm.text-gray-600')).toContainText('users');
75
- });
76
-
77
- test('should have sort controls', async ({ page }) => {
78
- // Should have sort by dropdown
79
- await expect(page.locator('select, button:has-text("Sort")')).toBeVisible();
80
- });
81
-
82
- test('should navigate to user detail when clicking view details', async ({ page }) => {
83
- // Wait for the user's row to appear
84
- await page.waitForSelector(`text=${testUsername}`, { timeout: 10000 });
85
-
86
- // Click on "View Details" link
87
- const viewDetailsLink = page.locator(`a[href="/admin/users/${testUserId}"]`).first();
88
- await viewDetailsLink.click();
89
-
90
- // Should navigate to user detail page
91
- await page.waitForURL(`**/admin/users/${testUserId}`);
92
- await expect(page.getByRole('heading', { name: 'User Details' })).toBeVisible();
93
- });
94
- });
95
-
96
- test.describe('User Detail Page', () => {
97
- test.beforeEach(async ({ page }) => {
98
- await page.goto(`/admin/users/${testUserId}`);
99
- });
100
-
101
- test('should load user detail page', async ({ page }) => {
102
- await expect(page.getByRole('heading', { name: 'User Details' })).toBeVisible();
103
- });
104
-
105
- test('should display user information', async ({ page }) => {
106
- await page.waitForSelector(`text=${testUsername}`, { timeout: 10000 });
107
-
108
- // Should show username
109
- await expect(page.locator(`h2:has-text("${testUsername}")`)).toBeVisible();
110
-
111
- // Should show user ID
112
- await expect(page.locator(`text=User ID:`)).toBeVisible();
113
- });
114
-
115
- test('should show registration date', async ({ page }) => {
116
- await expect(page.locator('text=Registered')).toBeVisible();
117
- });
118
-
119
- test('should show last active timestamp', async ({ page }) => {
120
- await expect(page.locator('text=Last Active')).toBeVisible();
121
- });
122
-
123
- test('should display activity stats', async ({ page }) => {
124
- // In the detail page, stats are shown in the info card
125
- await expect(page.locator('text=Activity')).toBeVisible();
126
- await expect(page.locator('p.text-lg.font-semibold.text-gray-900').filter({ hasText: 'conversations' })).toBeVisible();
127
- await expect(page.locator('p.text-xs.text-gray-500').filter({ hasText: 'messages sent' })).toBeVisible();
128
- });
129
-
130
- test('should show recent conversations section', async ({ page }) => {
131
- await expect(page.locator('text=Recent Conversations')).toBeVisible();
132
- });
133
-
134
- test('should have back to users button', async ({ page }) => {
135
- const backLink = page.locator('a[href="/admin/users"]:has-text("Back")');
136
- await expect(backLink).toBeVisible();
137
-
138
- await backLink.click();
139
- await page.waitForURL('**/admin/users');
140
- await expect(page.getByRole('heading', { name: 'Users' })).toBeVisible();
141
- });
142
-
143
- test('should show no conversations message for new user', async ({ page }) => {
144
- // New user should have no conversations
145
- await page.waitForSelector('text=Recent Conversations', { timeout: 10000 });
146
- await expect(page.locator('text=No conversations yet')).toBeVisible();
147
- });
148
-
149
- test('should handle error state gracefully', async ({ page }) => {
150
- // Navigate to non-existent user
151
- await page.goto('/admin/users/00000000-0000-0000-0000-000000000000');
152
-
153
- // Should show error message
154
- await expect(page.locator('text=Error')).toBeVisible();
155
- await expect(page.locator('text=User not found')).toBeVisible();
156
-
157
- // Should have retry button
158
- await expect(page.locator('button:has-text("Retry")')).toBeVisible();
159
-
160
- // Should have back button
161
- await expect(page.locator('text=Back to Users')).toBeVisible();
162
- });
163
-
164
- test('should show error state with retry functionality', async ({ page }) => {
165
- await page.goto('/admin/users/invalid-id');
166
-
167
- // Should show error
168
- await expect(page.locator('text=Error')).toBeVisible();
169
-
170
- // Back button should work
171
- await page.click('text=Back to Users');
172
- await page.waitForURL('**/admin/users');
173
- });
174
- });
175
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/e2e/coach-ad-prompt.spec.ts DELETED
@@ -1,315 +0,0 @@
1
- import { test, expect } from '@playwright/test';
2
- import { execSync } from 'child_process';
3
-
4
- const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000';
5
- const PASSWORD = process.env.BASIC_AUTH_PASSWORD || 'cz-2025';
6
-
7
- // Helper function to register and login via UI
8
- async function registerAndLogin(page: any, username: string) {
9
- await page.goto(`${API_URL}/login`);
10
- await page.click('text=沒有帳號?建立新帳號');
11
- await page.fill('input[name="username"]', username);
12
- await page.click('button:has-text("建立帳號")');
13
- await expect(page).toHaveURL(`${API_URL}/dashboard`, { timeout: 10_000 });
14
- }
15
-
16
- test.describe('Coach Ad Prompt Feature', () => {
17
- let authHeader: string;
18
- let userId: string;
19
- let username: string;
20
- let conversationId: string;
21
-
22
- test.beforeAll(async ({ request }) => {
23
- // Register a unique user for this test suite
24
- const timestamp = Date.now();
25
- username = `coach-ad-test-${timestamp}`;
26
- authHeader = `Basic ${Buffer.from(`${username}:${PASSWORD}`).toString('base64')}`;
27
-
28
- const registerResponse = await request.post(`${API_URL}/api/auth/register`, {
29
- data: {
30
- username,
31
- password: PASSWORD,
32
- },
33
- });
34
-
35
- expect(registerResponse.ok()).toBeTruthy();
36
- const registerData = await registerResponse.json();
37
- userId = registerData.user.id;
38
-
39
- // Create a conversation with a student (not coach_direct)
40
- const createConvResponse = await request.post(`${API_URL}/api/conversations/create`, {
41
- headers: { Authorization: authHeader },
42
- data: {
43
- studentPromptId: 'ruirui',
44
- coachPromptId: 'satir',
45
- include3ConversationSummary: false,
46
- },
47
- });
48
-
49
- expect(createConvResponse.ok()).toBeTruthy();
50
- const convData = await createConvResponse.json();
51
- conversationId = convData.conversation.id;
52
- });
53
-
54
- test('should show coach ad prompt after 6 user-to-student messages', async ({ page, request }) => {
55
- test.setTimeout(90_000);
56
- const testUser = `coach-ad-ui-${Date.now()}`;
57
-
58
- // Register and login via UI
59
- await registerAndLogin(page, testUser);
60
-
61
- // Create conversation via UI
62
- await page.click('h3:has-text("指定對話演練")');
63
- await expect(page.locator('h3:has-text("選擇學生")')).toBeVisible();
64
- await page.click('text=睿睿');
65
- await expect(page).toHaveURL(/\/conversation\//, { timeout: 10_000 });
66
- await expect(page.locator('input#chat-input')).toBeVisible({ timeout: 5000 });
67
-
68
- // Get conversation ID from URL
69
- const convId = page.url().match(/\/conversation\/([^/?]+)/)?.[1];
70
- expect(convId).toBeTruthy();
71
-
72
- // Set message count to 5 via SQLite (so next message will be #6)
73
- execSync(
74
- `sqlite3 ./data/app.db "UPDATE conversations SET user_to_student_message_count = 5 WHERE id = '${convId}';"`,
75
- { encoding: 'utf-8' }
76
- );
77
-
78
- // Send 6th message (triggers coach ad prompt)
79
- await page.fill('input[type="text"]', 'Help me with homework');
80
- await page.click('button[type="submit"]');
81
-
82
- // Wait for student response to complete (indicates streaming is done)
83
- const studentResponse = page.locator('.bg-white.border.border-blue-100').first();
84
- await expect(studentResponse).toBeVisible({ timeout: 60_000 });
85
-
86
- // Verify coach ad modal appears
87
- await expect(page.locator('text=諮詢教練建議')).toBeVisible({ timeout: 10_000 });
88
- await expect(page.locator('text=需要向教練尋求建議嗎')).toBeVisible();
89
-
90
- console.log('✅ Coach ad prompt appeared after 6 messages');
91
- });
92
-
93
- test('should allow dismissing the coach ad prompt', async ({ page, request }) => {
94
- test.setTimeout(90_000);
95
- const testUser = `coach-ad-dismiss-${Date.now()}`;
96
-
97
- // Register and login via UI
98
- await registerAndLogin(page, testUser);
99
-
100
- // Create conversation via UI
101
- await page.click('h3:has-text("指定對話演練")');
102
- await expect(page.locator('h3:has-text("選擇學生")')).toBeVisible();
103
- await page.click('text=睿睿');
104
- await expect(page).toHaveURL(/\/conversation\//, { timeout: 10_000 });
105
- await expect(page.locator('input#chat-input')).toBeVisible({ timeout: 5000 });
106
-
107
- // Get conversation ID from URL
108
- const convId = page.url().match(/\/conversation\/([^/?]+)/)?.[1];
109
- expect(convId).toBeTruthy();
110
-
111
- // Set message count to 5 via SQLite
112
- execSync(
113
- `sqlite3 ./data/app.db "UPDATE conversations SET user_to_student_message_count = 5 WHERE id = '${convId}';"`,
114
- { encoding: 'utf-8' }
115
- );
116
-
117
- // Send 6th message
118
- await page.fill('input[type="text"]', 'Help me');
119
- await page.click('button[type="submit"]');
120
-
121
- // Wait for student response to complete
122
- const studentResponse = page.locator('.bg-white.border.border-blue-100').first();
123
- await expect(studentResponse).toBeVisible({ timeout: 60_000 });
124
-
125
- // Verify modal appears
126
- await expect(page.locator('text=諮詢教練建議')).toBeVisible({ timeout: 10_000 });
127
-
128
- // Click dismiss button
129
- const dismissButton = page.locator('button:has-text("忽略")');
130
- await expect(dismissButton).toBeVisible();
131
- await dismissButton.click();
132
-
133
- // Verify modal is gone
134
- await expect(page.locator('text=諮詢教練建議')).not.toBeVisible();
135
-
136
- console.log('✅ Coach ad prompt dismissed successfully');
137
- });
138
-
139
- test('should generate and send coach message when clicking OK', async ({ page, request }) => {
140
- test.setTimeout(90_000);
141
- const testUser = `coach-ad-accept-${Date.now()}`;
142
-
143
- // Register and login via UI
144
- await registerAndLogin(page, testUser);
145
-
146
- // Create conversation via UI
147
- await page.click('h3:has-text("指定對話演練")');
148
- await expect(page.locator('h3:has-text("選擇學生")')).toBeVisible();
149
- await page.click('text=睿睿');
150
- await expect(page).toHaveURL(/\/conversation\//, { timeout: 10_000 });
151
- await expect(page.locator('input#chat-input')).toBeVisible({ timeout: 5000 });
152
-
153
- // Get conversation ID from URL
154
- const convId = page.url().match(/\/conversation\/([^/?]+)/)?.[1];
155
- expect(convId).toBeTruthy();
156
-
157
- // Set message count to 5 via SQLite
158
- execSync(
159
- `sqlite3 ./data/app.db "UPDATE conversations SET user_to_student_message_count = 5 WHERE id = '${convId}';"`,
160
- { encoding: 'utf-8' }
161
- );
162
-
163
- // Send 6th message
164
- await page.fill('input[type="text"]', 'I need help with this');
165
- await page.click('button[type="submit"]');
166
-
167
- // Wait for student response to complete
168
- const studentResponse = page.locator('.bg-white.border.border-blue-100').first();
169
- await expect(studentResponse).toBeVisible({ timeout: 60_000 });
170
-
171
- // Verify modal appears
172
- await expect(page.locator('text=諮詢教練建議')).toBeVisible({ timeout: 10_000 });
173
-
174
- // Click OK button
175
- const okButton = page.locator('button:has-text("好的,諮詢教練")');
176
- await expect(okButton).toBeVisible();
177
- await okButton.click();
178
-
179
- // Verify coach message appears (purple gradient background)
180
- const coachMessage = page.locator('.bg-gradient-to-br.from-purple-50');
181
- await expect(coachMessage.first()).toBeVisible({ timeout: 60_000 });
182
-
183
- console.log('✅ Coach message generated after clicking OK');
184
- });
185
-
186
- test('should show "Ask Coach" button when message count >= 6', async ({ page, request }) => {
187
- test.setTimeout(90_000);
188
- const testUser = `coach-button-${Date.now()}`;
189
-
190
- // Register and login via UI
191
- await registerAndLogin(page, testUser);
192
-
193
- // Create conversation via UI
194
- await page.click('h3:has-text("指定對話演練")');
195
- await expect(page.locator('h3:has-text("選擇學生")')).toBeVisible();
196
- await page.click('text=睿睿');
197
- await expect(page).toHaveURL(/\/conversation\//, { timeout: 10_000 });
198
- await expect(page.locator('input#chat-input')).toBeVisible({ timeout: 5000 });
199
-
200
- // Get conversation ID from URL
201
- const convId = page.url().match(/\/conversation\/([^/?]+)/)?.[1];
202
- expect(convId).toBeTruthy();
203
-
204
- // Set message count to 6 via SQLite (threshold reached)
205
- execSync(
206
- `sqlite3 ./data/app.db "UPDATE conversations SET user_to_student_message_count = 6 WHERE id = '${convId}';"`,
207
- { encoding: 'utf-8' }
208
- );
209
-
210
- // Reload page to trigger UI update
211
- await page.reload();
212
- await expect(page.locator('h1')).toBeVisible({ timeout: 10_000 });
213
-
214
- // Verify "Ask Coach" button in header is visible with badge
215
- const askCoachButton = page.locator('button[title="諮詢教練"]');
216
- await expect(askCoachButton).toBeVisible();
217
-
218
- console.log('✅ Ask Coach button visible in header');
219
-
220
- console.log('✅ Ask Coach button visible when count >= 6');
221
- });
222
-
223
- test('should only show "Ask Coach" button when speaker is student (not coach)', async ({ page }) => {
224
- test.setTimeout(90_000);
225
- const testUser = `coach-speaker-button-${Date.now()}`;
226
-
227
- // Register and login via UI
228
- await registerAndLogin(page, testUser);
229
-
230
- // Create conversation via UI
231
- await page.click('h3:has-text("指定對話演練")');
232
- await expect(page.locator('h3:has-text("選擇學生")')).toBeVisible();
233
- await page.click('text=睿睿');
234
- await expect(page).toHaveURL(/\/conversation\//, { timeout: 10_000 });
235
- await expect(page.locator('input#chat-input')).toBeVisible({ timeout: 5000 });
236
-
237
- // Get conversation ID from URL
238
- const convId = page.url().match(/\/conversation\/([^/?]+)/)?.[1];
239
- expect(convId).toBeTruthy();
240
-
241
- // Set message count to 6 to enable the "Ask Coach" button
242
- execSync(
243
- `sqlite3 ./data/app.db "UPDATE conversations SET user_to_student_message_count = 6 WHERE id = '${convId}';"`,
244
- { encoding: 'utf-8' }
245
- );
246
-
247
- // Reload page to update conversation state
248
- await page.reload();
249
- await expect(page.locator('h1')).toBeVisible({ timeout: 10_000 });
250
- await expect(page.locator('input#chat-input')).toBeVisible({ timeout: 5000 });
251
-
252
- // Verify "Ask Coach" button is visible (default speaker is student)
253
- const askCoachButton = page.locator('button[title="諮詢教練"]');
254
- await expect(askCoachButton).toBeVisible();
255
-
256
- console.log('✅ Ask Coach button visible when speaker is student (default)');
257
-
258
- // NOTE: Testing button visibility when speaker is coach would require:
259
- // 1. Either manual speaker toggle UI (which is hidden)
260
- // 2. Or speaker auto-detection from message history (complex to test reliably)
261
- // The feature itself works correctly - button only shows when speaker === 'student'
262
- // This is verified by the conditional rendering logic in the component
263
-
264
- console.log('✅✅✅ Ask Coach button speaker-based visibility test passed!');
265
- });
266
-
267
- test('should not show coach ad features for coach_direct conversations', async ({ request }) => {
268
- // Create a coach_direct conversation
269
- const createConvResponse = await request.post(`${API_URL}/api/conversations/create`, {
270
- headers: { Authorization: authHeader },
271
- data: {
272
- studentPromptId: 'coach_direct',
273
- coachPromptId: 'satir',
274
- include3ConversationSummary: false,
275
- },
276
- });
277
-
278
- expect(createConvResponse.ok()).toBeTruthy();
279
- const convData = await createConvResponse.json();
280
- const coachConvId = convData.conversation.id;
281
-
282
- // Send 20 user messages via API
283
- for (let i = 1; i <= 20; i++) {
284
- const messageResponse = await request.post(`${API_URL}/api/conversations/${coachConvId}/message`, {
285
- headers: {
286
- 'Content-Type': 'application/json',
287
- Authorization: authHeader,
288
- },
289
- data: {
290
- messages: [
291
- {
292
- role: 'user',
293
- parts: [{ type: 'text', text: `測試訊息 ${i}` }],
294
- metadata: { speaker: 'coach' }
295
- }
296
- ]
297
- }
298
- });
299
-
300
- expect(messageResponse.ok()).toBeTruthy();
301
- }
302
-
303
- // Fetch the conversation and verify no coachAdFirstShownAt was set
304
- const convResponse = await request.get(`${API_URL}/api/conversations/${coachConvId}`, {
305
- headers: { Authorization: authHeader }
306
- });
307
-
308
- expect(convResponse.ok()).toBeTruthy();
309
- const { conversation } = await convResponse.json();
310
-
311
- // userToStudentMessageCount should be 0 (because speaker is 'coach')
312
- expect(conversation.userToStudentMessageCount || 0).toBe(0);
313
- expect(conversation.coachAdFirstShownAt).toBeFalsy(); // Should be null/undefined (not set)
314
- });
315
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/e2e/coach-chat-persistence.spec.ts DELETED
@@ -1,145 +0,0 @@
1
- import { test, expect } from '@playwright/test';
2
-
3
- const baseURL = process.env.BASE_URL || 'http://localhost:3000';
4
-
5
- // Helper function to register and login
6
- async function registerAndLogin(page: any, username: string) {
7
- await page.goto(`${baseURL}/login`);
8
- await page.click('text=沒有帳號?建立新帳號');
9
- await page.fill('input[name="username"]', username);
10
- await page.click('button:has-text("建立帳號")');
11
- await expect(page).toHaveURL(`${baseURL}/dashboard`, { timeout: 10_000 });
12
- }
13
-
14
- test.describe('Coach Chat Persistence', () => {
15
- test.use({ workers: 1 }); // Run serially to avoid rate limits
16
-
17
- test('should persist coach chat conversations across page reloads', async ({ page }) => {
18
- test.setTimeout(60_000);
19
- const testUser = `coach-test-${Date.now()}`;
20
-
21
- // Register and login
22
- await registerAndLogin(page, testUser);
23
-
24
- // Wait for dashboard
25
- await expect(page.locator('h1:has-text("SEL Chat Coach")')).toBeVisible();
26
-
27
- // Click "教練回顧" card to open modal, then click "開始諮詢"
28
- await page.click('h3:has-text("教練回顧")');
29
- await expect(page.locator('text=開始諮詢')).toBeVisible({ timeout: 5_000 });
30
- await page.click('button:has-text("開始諮詢")');
31
-
32
- // Wait for conversation page to load (coach conversations now use regular conversation page)
33
- await expect(page).toHaveURL(/\/conversation\//, { timeout: 10_000 });
34
- await expect(page.locator('h1')).toBeVisible({ timeout: 10_000 });
35
-
36
- // Send first message to coach
37
- const firstMessage = 'Hello coach, I need help with teaching ADHD students.';
38
- const inputField = page.locator('input[type="text"]');
39
- await inputField.fill(firstMessage);
40
- await inputField.press('Enter');
41
-
42
- // Wait for assistant response to complete (coach messages have purple bg on conversation page)
43
- const coachMessages = page.locator('.bg-gradient-to-br.from-purple-50 .whitespace-pre-wrap');
44
- await expect(coachMessages.first()).toBeVisible({ timeout: 60000 });
45
- const firstResponse = await coachMessages.first().textContent();
46
-
47
- // Reload the page (message is auto-saved during streaming)
48
- await page.reload();
49
-
50
- // Wait for page to reload
51
- await expect(page.locator('h1')).toBeVisible({ timeout: 10_000 });
52
-
53
- // Verify the conversation history is loaded
54
- await expect(page.getByText(firstMessage)).toBeVisible({ timeout: 10000 });
55
- await expect(page.locator('.whitespace-pre-wrap').filter({ hasText: firstResponse || '' })).toBeVisible({ timeout: 10000 });
56
-
57
- // Send another message
58
- const secondMessage = 'Can you tell me more about that?';
59
- const inputField2 = page.locator('input[type="text"]');
60
- await inputField2.fill(secondMessage);
61
- await inputField2.press('Enter');
62
-
63
- // Wait for second coach response to appear
64
- await expect(coachMessages.nth(1)).toBeVisible({ timeout: 60000 });
65
-
66
- // Verify both messages are still visible
67
- await expect(page.getByText(firstMessage)).toBeVisible();
68
- await expect(page.getByText(secondMessage)).toBeVisible();
69
-
70
- // Navigate away to dashboard
71
- await page.goto(`${baseURL}/dashboard`);
72
- await expect(page.locator('h1:has-text("SEL Chat Coach")')).toBeVisible();
73
-
74
- // Go back to the existing coach conversation from sidebar
75
- await page.click('svg:has(path[d*="M4 6h16"])');
76
- await expect(page.locator('text=對話記錄')).toBeVisible();
77
-
78
- // Click on the coach conversation (has "諮詢" in title or coach emoji)
79
- const coachConv = page.locator('.space-y-1 > div').filter({ hasText: '👨‍🏫' }).first();
80
- await expect(coachConv).toBeVisible();
81
- await coachConv.click();
82
-
83
- // Wait for conversation to load
84
- await expect(page).toHaveURL(/\/conversation\//, { timeout: 10_000 });
85
-
86
- // Verify all messages are still there
87
- await expect(page.getByText(firstMessage)).toBeVisible({ timeout: 10000 });
88
- await expect(page.getByText(secondMessage)).toBeVisible();
89
-
90
- console.log('✅ Coach chat persistence test passed!');
91
- });
92
-
93
- test('should not show coach-direct conversations in regular conversation list', async ({ page }) => {
94
- test.setTimeout(60_000);
95
- const testUser = `sidebar-test-${Date.now()}`;
96
-
97
- // Register and login
98
- await registerAndLogin(page, testUser);
99
-
100
- // Wait for dashboard
101
- await expect(page.locator('h1:has-text("SEL Chat Coach")')).toBeVisible();
102
-
103
- // Click "教練回顧" from dashboard to create a coach conversation
104
- await page.click('h3:has-text("教練回顧")');
105
- await expect(page.locator('text=開始諮詢')).toBeVisible({ timeout: 5_000 });
106
- await page.click('button:has-text("開始諮詢")');
107
-
108
- // Wait for conversation page to load (coach conversations now use regular conversation page)
109
- await expect(page).toHaveURL(/\/conversation\//, { timeout: 10_000 });
110
- await expect(page.locator('h1')).toBeVisible({ timeout: 10_000 });
111
-
112
- // Send a message
113
- const testInput = page.locator('input[type="text"]');
114
- await testInput.fill('Test message');
115
- await testInput.press('Enter');
116
-
117
- // Go back to dashboard
118
- await page.goto(`${baseURL}/dashboard`);
119
- await expect(page.locator('h1:has-text("SEL Chat Coach")')).toBeVisible();
120
-
121
- // Open sidebar
122
- const hamburgerButton = page.locator('button').filter({ has: page.locator('svg') }).first();
123
- await hamburgerButton.click();
124
- await expect(page.getByRole('heading', { name: /對話記錄/i })).toBeVisible({ timeout: 5_000 });
125
-
126
- // Verify coach conversation IS in the conversation list (with coach emoji)
127
- const coachConversations = page.locator('.space-y-1 > div.relative.group').filter({ has: page.locator('text=👨‍🏫') });
128
- await expect(coachConversations.first()).toBeVisible({ timeout: 10000 });
129
-
130
- // Close sidebar by clicking overlay
131
- await page.click('.fixed.inset-0.bg-black\\/50');
132
-
133
- // Verify sidebar is closed (overlay should be gone)
134
- await expect(page.locator('.fixed.inset-0.bg-black\\/50')).not.toBeVisible();
135
-
136
- // Verify we can create a new coach conversation via "教練回顧" button
137
- await page.click('h3:has-text("教練回顧")');
138
- await expect(page.locator('text=開始諮詢')).toBeVisible({ timeout: 5_000 });
139
- await page.click('button:has-text("開始諮詢")');
140
- await expect(page).toHaveURL(/\/conversation\//, { timeout: 10_000 });
141
- await expect(page.locator('h1')).toBeVisible({ timeout: 10_000 });
142
-
143
- console.log('✅ Coach conversation filtering test passed!');
144
- });
145
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/e2e/coach-chat-ui-only.spec.ts DELETED
@@ -1,248 +0,0 @@
1
- import { test, expect } from '@playwright/test';
2
-
3
- const baseURL = process.env.BASE_URL || 'http://localhost:3000';
4
-
5
- // Helper function to register and login
6
- async function registerAndLogin(page: any, username: string) {
7
- await page.goto(`${baseURL}/login`);
8
- await page.click('text=沒有帳號?建立新帳號');
9
- await page.fill('input[name="username"]', username);
10
- await page.click('button:has-text("建立帳號")');
11
- await expect(page).toHaveURL(`${baseURL}/dashboard`, { timeout: 10_000 });
12
- }
13
-
14
- test.describe('Coach Chat UI Implementation', () => {
15
- test('should navigate to coach chat page and show correct UI', async ({ page }) => {
16
- test.setTimeout(60_000);
17
- const testUser = `coach_ui_${Date.now()}`;
18
-
19
- // Register and login
20
- await registerAndLogin(page, testUser);
21
-
22
- // Click coach consultation card to open modal
23
- await page.click('h3:has-text("教練回顧")');
24
-
25
- // Wait for modal to appear
26
- await expect(page.locator('text=開始諮詢')).toBeVisible({ timeout: 5_000 });
27
-
28
- // Click "開始諮詢" button in modal
29
- await page.click('button:has-text("開始諮詢")');
30
-
31
- // Wait for conversation page to load (coach conversations now use regular conversation page)
32
- await expect(page).toHaveURL(/\/conversation\//, { timeout: 10_000 });
33
-
34
- // Verify page loaded with h1 heading
35
- await expect(page.locator('h1')).toBeVisible({ timeout: 10_000 });
36
-
37
- // Verify header has gradient (conversation page uses blue-to-purple gradient)
38
- await expect(page.locator('.bg-gradient-to-r.from-blue-600.to-purple-600')).toBeVisible();
39
-
40
- // Verify input field exists
41
- const inputField = page.locator('input[type="text"]');
42
- await expect(inputField).toBeVisible();
43
-
44
- // Verify submit button exists
45
- const submitButton = page.locator('button[type="submit"]');
46
- await expect(submitButton).toBeVisible();
47
-
48
- console.log('✅ Coach chat page UI structure verified');
49
- });
50
-
51
- test('should create coach-direct conversation on page load', async ({ page }) => {
52
- test.setTimeout(60_000);
53
- const testUser = `coach_conv_${Date.now()}`;
54
-
55
- // Track network requests
56
- const createConvRequests: any[] = [];
57
- const getConvRequests: any[] = [];
58
-
59
- page.on('request', request => {
60
- const url = request.url();
61
- if (url.includes('/api/conversations/create')) {
62
- createConvRequests.push({
63
- url: url,
64
- method: request.method(),
65
- postData: request.postData()
66
- });
67
- }
68
- if (url.includes('/api/conversations') && request.method() === 'GET') {
69
- getConvRequests.push(url);
70
- }
71
- });
72
-
73
- // Register and login
74
- await registerAndLogin(page, testUser);
75
-
76
- // Navigate to coach chat (via modal flow)
77
- await page.click('h3:has-text("教練回顧")');
78
- await expect(page.locator('text=開始諮詢')).toBeVisible({ timeout: 5_000 });
79
- await page.click('button:has-text("開始諮詢")');
80
- await expect(page).toHaveURL(/\/conversation\//, { timeout: 10_000 });
81
-
82
- // Wait for page to load (don't wait for networkidle due to polling)
83
- await page.waitForSelector('input[type="text"]', { timeout: 10_000 });
84
-
85
- // Wait for conversation list to be fetched (poll until at least one request)
86
- for (let i = 0; i < 20 && getConvRequests.length === 0; i++) {
87
- await new Promise(resolve => setTimeout(resolve, 200));
88
- }
89
-
90
- // Verify conversation list was fetched
91
- expect(getConvRequests.length).toBeGreaterThan(0);
92
- console.log(`✅ Conversations fetched ${getConvRequests.length} times`);
93
-
94
- // Verify conversation was created (either new or found existing)
95
- // The page should call GET /api/conversations to find existing coach-direct conversation
96
- // OR call POST /api/conversations/create if none exists
97
- const hasCreateCall = createConvRequests.length > 0;
98
- console.log(`Coach conversation creation calls: ${createConvRequests.length}`);
99
-
100
- if (hasCreateCall) {
101
- const createRequest = createConvRequests[0];
102
- console.log('Create request data:', createRequest.postData);
103
-
104
- // Verify it's creating a coach_direct conversation
105
- if (createRequest.postData) {
106
- const data = JSON.parse(createRequest.postData);
107
- expect(data.studentPromptId).toBe('coach_direct');
108
- expect(data.coachPromptId).toBe('satir');
109
- console.log('✅ Coach-direct conversation created correctly');
110
- }
111
- } else {
112
- console.log('✅ Using existing coach-direct conversation');
113
- }
114
- });
115
-
116
- test('should reload page and maintain conversation', async ({ page }) => {
117
- test.setTimeout(60_000);
118
- const testUser = `coach_reload_${Date.now()}`;
119
-
120
- let conversationId: string | null = null;
121
- const messageRequests: string[] = [];
122
-
123
- // Track API calls
124
- page.on('response', async response => {
125
- const url = response.url();
126
-
127
- // Capture conversation ID from create response
128
- if (url.includes('/api/conversations/create')) {
129
- try {
130
- const data = await response.json();
131
- if (data.conversation?.id) {
132
- conversationId = data.conversation.id;
133
- console.log('Captured conversation ID:', conversationId);
134
- }
135
- } catch (e) {
136
- // Ignore JSON parse errors
137
- }
138
- }
139
-
140
- // Track message endpoint calls
141
- if (url.includes('/messages') && response.request().method() === 'GET') {
142
- messageRequests.push(url);
143
- }
144
- });
145
-
146
- // Register and login
147
- await registerAndLogin(page, testUser);
148
-
149
- // Navigate to coach chat (via modal flow)
150
- await page.click('h3:has-text("教練回顧")');
151
- await expect(page.locator('text=開始諮詢')).toBeVisible({ timeout: 5_000 });
152
- await page.click('button:has-text("開始諮詢")', { force: true });
153
- await expect(page).toHaveURL(/\/conversation\//, { timeout: 10_000 });
154
-
155
- // Wait for conversation creation (don't wait for networkidle due to polling)
156
- await page.waitForSelector('input[type="text"]', { timeout: 10_000 });
157
-
158
- // Wait for initial message requests (poll until at least one)
159
- for (let i = 0; i < 20 && messageRequests.length === 0; i++) {
160
- await new Promise(resolve => setTimeout(resolve, 200));
161
- }
162
-
163
- // Get initial message requests count
164
- const initialMessageCalls = messageRequests.length;
165
- console.log(`Initial message endpoint calls: ${initialMessageCalls}`);
166
-
167
- // Reload the page
168
- await page.reload();
169
- await expect(page).toHaveURL(/\/conversation\//, { timeout: 10_000 });
170
-
171
- // Wait for reload to complete (don't wait for networkidle due to polling)
172
- await page.waitForSelector('input[type="text"]', { timeout: 10_000 });
173
-
174
- // Wait for message requests after reload (poll with timeout)
175
- const beforeCount = initialMessageCalls;
176
- for (let i = 0; i < 20 && messageRequests.length === beforeCount; i++) {
177
- await new Promise(resolve => setTimeout(resolve, 200));
178
- }
179
-
180
- // Verify messages endpoint was called (conversation maintained)
181
- const afterReloadMessageCalls = messageRequests.length;
182
- console.log(`After reload message endpoint calls: ${afterReloadMessageCalls}`);
183
-
184
- // Just verify that messages endpoint was called at least once (conversation persisted)
185
- expect(afterReloadMessageCalls).toBeGreaterThanOrEqual(1);
186
-
187
- // Verify we're still on the same conversation page
188
- await expect(page).toHaveURL(/\/conversation\//);
189
- await expect(page.getByText(/薩提爾教練/i)).toBeVisible();
190
-
191
- console.log('✅ Page reload maintained conversation and loaded messages');
192
- });
193
-
194
- test('should not show coach-direct conversation in sidebar', async ({ page }) => {
195
- test.setTimeout(60_000);
196
- const testUser = `coach_sidebar_${Date.now()}`;
197
-
198
- // Register and login
199
- await registerAndLogin(page, testUser);
200
-
201
- // Navigate to coach chat to create coach-direct conversation (via modal)
202
- await page.click('h3:has-text("教練回顧")');
203
- await expect(page.locator('text=開始諮詢')).toBeVisible({ timeout: 5_000 });
204
- await page.click('button:has-text("開始諮詢")');
205
- await expect(page).toHaveURL(/\/conversation\//, { timeout: 10_000 });
206
- await page.waitForSelector('input[type="text"]', { timeout: 10_000 });
207
-
208
- // Go back to dashboard
209
- await page.goto(`${baseURL}/dashboard`);
210
- await expect(page.locator('h1:has-text("SEL Chat Coach")')).toBeVisible();
211
-
212
- // Open sidebar (click hamburger menu)
213
- const hamburgerButton = page.locator('button').filter({ has: page.locator('svg') }).first();
214
- await hamburgerButton.click();
215
-
216
- // Wait for sidebar to appear
217
- await expect(page.getByRole('heading', { name: /對話記錄/i })).toBeVisible({ timeout: 5_000 });
218
-
219
- // Verify coach conversation IS in the list (with coach emoji)
220
- const coachConversations = page.locator('.space-y-1 > div.relative.group').filter({ has: page.locator('text=👨‍🏫') });
221
- await expect(coachConversations.first()).toBeVisible({ timeout: 10000 });
222
-
223
- console.log('✅ Coach conversation correctly shown in sidebar');
224
- });
225
-
226
- test('should show correct coach avatar and styling', async ({ page }) => {
227
- test.setTimeout(60_000);
228
- const testUser = `coach_style_${Date.now()}`;
229
-
230
- // Register and login
231
- await registerAndLogin(page, testUser);
232
-
233
- // Navigate to coach chat (via modal flow)
234
- await page.click('h3:has-text("教練回顧")');
235
- await expect(page.locator('text=開始諮詢')).toBeVisible({ timeout: 5_000 });
236
- await page.click('button:has-text("開始諮詢")');
237
- await expect(page).toHaveURL(/\/conversation\//, { timeout: 10_000 });
238
-
239
- // Verify header has correct gradient (conversation page uses blue-to-purple)
240
- const header = page.locator('.bg-gradient-to-r.from-blue-600.to-purple-600');
241
- await expect(header).toBeVisible();
242
-
243
- // Verify conversation page loaded correctly
244
- await expect(page.locator('h1')).toBeVisible();
245
-
246
- console.log('✅ Coach conversation page styling correct');
247
- });
248
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/e2e/coach-conversation-messaging.spec.ts DELETED
@@ -1,124 +0,0 @@
1
- import { test, expect } from '@playwright/test';
2
-
3
- const baseURL = process.env.BASE_URL || 'http://localhost:3000';
4
-
5
- // Helper function to register and login
6
- async function registerAndLogin(page: any, username: string) {
7
- await page.goto(`${baseURL}/login`);
8
- await page.click('text=沒有帳號?建立新帳號');
9
- await page.fill('input[name="username"]', username);
10
- await page.click('button:has-text("建立帳號")');
11
- await expect(page).toHaveURL(`${baseURL}/dashboard`, { timeout: 10_000 });
12
- }
13
-
14
- test.describe('Coach Conversation Messaging', () => {
15
- test.use({ workers: 1 }); // Run serially to avoid rate limits
16
-
17
- test('should send messages in coach conversation without 500 errors', async ({ page }) => {
18
- test.setTimeout(90_000); // 90 seconds for LLM responses
19
- const testUser = `coach-msg-test-${Date.now()}`;
20
-
21
- // Step 1: Register and login
22
- await registerAndLogin(page, testUser);
23
- await expect(page.locator('h1:has-text("SEL Chat Coach")')).toBeVisible();
24
-
25
- console.log('✅ Logged in successfully');
26
-
27
- // Step 2: Create a coach conversation via dashboard
28
- await page.click('h3:has-text("教練回顧")');
29
- await expect(page.locator('text=開始諮詢')).toBeVisible({ timeout: 5_000 });
30
- await page.click('button:has-text("開始諮詢")');
31
-
32
- // Wait for conversation page to load
33
- await expect(page).toHaveURL(/\/conversation\//, { timeout: 10_000 });
34
- await expect(page.locator('h1')).toBeVisible({ timeout: 10_000 });
35
-
36
- console.log('✅ Coach conversation created and loaded');
37
-
38
- // Step 3: Send first message
39
- const firstMessage = 'Hello coach, I need help with teaching ADHD students.';
40
- const inputField = page.locator('input[type="text"]');
41
- await expect(inputField).toBeVisible({ timeout: 5000 });
42
- await inputField.fill(firstMessage);
43
-
44
- // Submit via Enter key
45
- await inputField.press('Enter');
46
-
47
- // Wait for coach response (proves message was sent)
48
- const coachResponse = page.locator('.bg-gradient-to-br.from-purple-50 .whitespace-pre-wrap').first();
49
- await expect(coachResponse).toBeVisible({ timeout: 60000 });
50
-
51
- console.log('✅ Received coach response');
52
-
53
- // Step 4: Navigate back to dashboard
54
- await page.goto(`${baseURL}/dashboard`);
55
- await expect(page.locator('h1:has-text("SEL Chat Coach")')).toBeVisible();
56
-
57
- console.log('✅ Navigated back to dashboard');
58
-
59
- // Step 5: Open sidebar and click on the coach conversation
60
- await page.click('svg:has(path[d*="M4 6h16"])');
61
- await expect(page.locator('text=對話記錄')).toBeVisible();
62
-
63
- // Find and click the coach conversation (has coach emoji in sidebar)
64
- const coachConv = page.locator('.space-y-1 > div').filter({ hasText: '👨‍🏫' }).first();
65
- await expect(coachConv).toBeVisible();
66
- await coachConv.click();
67
-
68
- // Wait for conversation page to load
69
- await expect(page).toHaveURL(/\/conversation\//, { timeout: 10_000 });
70
- await expect(page.locator('h1')).toBeVisible({ timeout: 10_000 });
71
-
72
- console.log('✅ Re-opened coach conversation from history');
73
-
74
- // Step 6: Verify first message is still there
75
- await expect(page.getByText(firstMessage)).toBeVisible();
76
-
77
- console.log('✅ Message history persisted');
78
-
79
- // Step 7: Send another message
80
- const secondMessage = 'Can you give me specific strategies?';
81
-
82
- // Close Next.js Dev Tools if open (can block form submission)
83
- const closeDevTools = page.locator('button:has-text("Close Next.js Dev Tools")');
84
- if (await closeDevTools.isVisible()) {
85
- await closeDevTools.click();
86
- }
87
-
88
- // Wait for form to be fully ready (input enabled, not in loading state)
89
- const inputField2 = page.locator('input[type="text"]#chat-input');
90
- await expect(inputField2).toBeVisible({ timeout: 5000 });
91
- await expect(inputField2).toBeEnabled({ timeout: 5000 });
92
-
93
- // Fill and submit second message via Enter key (more reliable than button)
94
- await inputField2.click(); // Focus the input
95
- await inputField2.fill(secondMessage);
96
- await inputField2.press('Enter');
97
-
98
- // Wait for second message to appear first (proves it was sent)
99
- await expect(page.getByText(secondMessage)).toBeVisible({ timeout: 10000 });
100
-
101
- // Wait for second coach response (look for any coach response after second message)
102
- // Since responses might be on same page, just verify we have at least one response
103
- const coachResponses = page.locator('.bg-gradient-to-br.from-purple-50');
104
- await expect(coachResponses.first()).toBeVisible({ timeout: 60000 });
105
-
106
- console.log('✅ Received second coach response');
107
-
108
- console.log('✅ Both messages visible in conversation');
109
-
110
- // Step 8: Verify no network errors (check for 500 errors in console)
111
- const errors = [];
112
- page.on('console', msg => {
113
- if (msg.type() === 'error') {
114
- errors.push(msg.text());
115
- }
116
- });
117
-
118
- // Verify no 500 errors occurred
119
- const has500Error = errors.some(err => err.includes('500'));
120
- expect(has500Error).toBe(false);
121
-
122
- console.log('✅✅✅ Coach conversation messaging test passed!');
123
- });
124
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/e2e/coach-conversation-speaker-default.spec.ts DELETED
@@ -1,188 +0,0 @@
1
- import { test, expect } from '@playwright/test';
2
-
3
- const baseURL = process.env.BASE_URL || 'http://localhost:3000';
4
-
5
- // Helper function to register and login
6
- async function registerAndLogin(page: any, username: string) {
7
- await page.goto(`${baseURL}/login`);
8
- await page.click('text=沒有帳號?建立新帳號');
9
- await page.fill('input[name="username"]', username);
10
- await page.click('button:has-text("建立帳號")');
11
- await expect(page).toHaveURL(`${baseURL}/dashboard`, { timeout: 10_000 });
12
- }
13
-
14
- // Helper to create a conversation via API
15
- async function createConversationViaAPI(
16
- username: string,
17
- studentPromptId: string,
18
- coachPromptId: string = 'satir'
19
- ) {
20
- const authHeader = Buffer.from(`${username}:cz-2025`).toString('base64');
21
- const response = await fetch(`${baseURL}/api/conversations/create`, {
22
- method: 'POST',
23
- headers: {
24
- 'Content-Type': 'application/json',
25
- 'Authorization': `Basic ${authHeader}`,
26
- },
27
- body: JSON.stringify({
28
- studentPromptId,
29
- coachPromptId,
30
- include3ConversationSummary: false,
31
- }),
32
- });
33
-
34
- if (!response.ok) {
35
- throw new Error(`Failed to create conversation: ${response.status}`);
36
- }
37
-
38
- const data = await response.json();
39
- return data.conversation.id;
40
- }
41
-
42
- // SKIPPED: Speaker toggle buttons are hidden in UI
43
- test.describe.skip('Coach Conversation Speaker Default', () => {
44
- test.use({ workers: 1 }); // Run serially to avoid rate limits
45
-
46
- test('should default to coach speaker when opening coach_direct conversation', async ({ page }) =>{
47
- test.setTimeout(60_000);
48
- const testUser = `coach-speaker-test-${Date.now()}`;
49
-
50
- // Step 1: Register and login
51
- await registerAndLogin(page, testUser);
52
- await expect(page.locator('h1:has-text("SEL Chat Coach")')).toBeVisible();
53
-
54
- console.log('✅ Logged in successfully');
55
-
56
- // Step 2: Create coach_direct conversation via API
57
- const coachConvId = await createConversationViaAPI(testUser, 'coach_direct', 'satir');
58
- console.log(`✅ Created coach conversation: ${coachConvId}`);
59
-
60
- // Reload page to get fresh conversation list
61
- await page.reload();
62
- await expect(page.locator('h1:has-text("SEL Chat Coach")')).toBeVisible();
63
-
64
- // Step 3: Navigate to conversation page by clicking from conversation list
65
- await page.click('h3:has-text("自訂對話演練")');
66
- await expect(page.locator('h3:has-text("對話記錄")')).toBeVisible();
67
-
68
- // Find and click the coach conversation (contains "諮詢" in title)
69
- const coachConv = page.locator('.space-y-3 > div').filter({ hasText: '諮詢' }).first();
70
- await expect(coachConv).toBeVisible();
71
- await coachConv.click();
72
-
73
- // Wait for conversation page to load
74
- await expect(page).toHaveURL(/\/conversation\//, { timeout: 10_000 });
75
- await expect(page.locator('h1')).toBeVisible({ timeout: 10_000 });
76
-
77
- console.log('✅ Navigated to coach conversation');
78
-
79
- // Step 4: Verify empty state shows correct text for coach conversation
80
- await expect(page.locator('h3:has-text("開始與教練對話")')).toBeVisible();
81
- await expect(page.locator('text=直接諮詢教練專業建議')).toBeVisible();
82
-
83
- console.log('✅ Empty state shows correct coach text');
84
-
85
- // Step 5: Verify coach button (👨‍🏫) is active by default
86
- const coachButton = page.locator('button[title="詢問教練"]');
87
- await expect(coachButton).toHaveClass(/from-purple-400/); // Should have gradient background
88
-
89
- console.log('✅ Coach button is active by default');
90
-
91
- // Step 6: Verify student button doesn't exist in coach_direct conversation
92
- const studentButton = page.locator('button[title="與學生對話"]');
93
- await expect(studentButton).not.toBeVisible(); // Should not exist
94
-
95
- console.log('✅ Student button is not present (as expected for coach_direct)');
96
-
97
- // Step 7: Verify input placeholder mentions coach
98
- const inputField = page.locator('input[type="text"]#chat-input');
99
- const placeholder = await inputField.getAttribute('placeholder');
100
- expect(placeholder).toContain('陳老師'); // Should mention coach name
101
- expect(placeholder).toContain('建議'); // Should say "建議" not "對話"
102
-
103
- console.log('✅ Input placeholder shows coach context');
104
-
105
- console.log('✅✅✅ Coach conversation speaker default test passed!');
106
- });
107
-
108
- test('should default to student speaker when opening student conversation', async ({ page }) => {
109
- test.setTimeout(60_000);
110
- const testUser = `student-speaker-test-${Date.now()}`;
111
-
112
- // Step 1: Register and login
113
- await registerAndLogin(page, testUser);
114
-
115
- console.log('✅ Logged in');
116
-
117
- // Step 2: Create student conversation via API
118
- const studentConvId = await createConversationViaAPI(testUser, 'ruirui', 'satir');
119
- console.log(`✅ Created student conversation: ${studentConvId}`);
120
-
121
- // Step 3: Navigate directly to conversation page (simulates clicking from list)
122
- await page.goto(`${baseURL}/conversation/${studentConvId}`);
123
- await expect(page).toHaveURL(/\/conversation\//, { timeout: 10_000 });
124
- await expect(page.locator('h1')).toBeVisible({ timeout: 10_000 });
125
-
126
- console.log('✅ Navigated to student conversation');
127
-
128
- // Step 4: Wait for page to fully load
129
- await page.waitForTimeout(1000);
130
-
131
- // Step 5: Verify student button (🎓) is active by default
132
- const studentButton = page.locator('button[title="與學生對話"]');
133
- await expect(studentButton).toHaveClass(/from-blue-400/); // Should have blue gradient
134
-
135
- console.log('✅ Student button is active by default');
136
-
137
- // Step 6: Verify coach button (👨‍🏫) is inactive
138
- const coachButton = page.locator('button[title="詢問教練"]');
139
- await expect(coachButton).toHaveClass(/bg-gray-200/); // Should have gray background
140
-
141
- console.log('✅ Coach button is inactive');
142
-
143
- // Step 7: Verify input placeholder mentions student
144
- const inputField = page.locator('input[type="text"]#chat-input');
145
- const placeholder = await inputField.getAttribute('placeholder');
146
- expect(placeholder).toContain('睿睿'); // Should mention student name
147
- expect(placeholder).toContain('對話'); // Should say "對話" not "建議"
148
-
149
- console.log('✅ Input placeholder shows student context');
150
-
151
- console.log('✅✅✅ Student conversation speaker default test passed!');
152
- });
153
-
154
- test('should send message with correct speaker after navigating from list', async ({ page }) => {
155
- test.setTimeout(90_000);
156
- const testUser = `speaker-message-test-${Date.now()}`;
157
-
158
- // Register and login
159
- await registerAndLogin(page, testUser);
160
-
161
- // Create coach conversation
162
- const coachConvId = await createConversationViaAPI(testUser, 'coach_direct', 'satir');
163
- console.log(`✅ Created coach conversation: ${coachConvId}`);
164
-
165
- // Navigate directly to conversation (simulates clicking from list)
166
- await page.goto(`${baseURL}/conversation/${coachConvId}`);
167
- await expect(page).toHaveURL(/\/conversation\//, { timeout: 10_000 });
168
- await page.waitForTimeout(1000);
169
-
170
- console.log('✅ Opened coach conversation');
171
-
172
- // Send a message (should use coach speaker by default)
173
- const message = 'I need help with classroom management';
174
- const inputField = page.locator('input[type="text"]#chat-input');
175
- await inputField.fill(message);
176
-
177
- // Submit via Enter key (more reliable than clicking button)
178
- await inputField.press('Enter');
179
-
180
- // Wait for coach response (proves message was sent)
181
- const coachResponse = page.locator('.bg-gradient-to-br.from-purple-50').first();
182
- await expect(coachResponse).toBeVisible({ timeout: 60000 });
183
-
184
- console.log('✅ Received coach response');
185
-
186
- console.log('✅✅✅ Message with correct speaker test passed!');
187
- });
188
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/e2e/coach-conversation-ui-buttons.spec.ts DELETED
@@ -1,244 +0,0 @@
1
- import { test, expect } from '@playwright/test';
2
-
3
- const baseURL = process.env.BASE_URL || 'http://localhost:3000';
4
-
5
- // Helper function to register and login
6
- async function registerAndLogin(page: any, username: string) {
7
- await page.goto(`${baseURL}/login`);
8
- await page.click('text=沒有帳號?建立新帳號');
9
- await page.fill('input[name="username"]', username);
10
- await page.click('button:has-text("建立帳號")');
11
- await expect(page).toHaveURL(`${baseURL}/dashboard`, { timeout: 10_000 });
12
- }
13
-
14
- // Helper to create a conversation via API
15
- async function createConversationViaAPI(
16
- username: string,
17
- studentPromptId: string,
18
- coachPromptId: string = 'satir'
19
- ) {
20
- const authHeader = Buffer.from(`${username}:cz-2025`).toString('base64');
21
- const response = await fetch(`${baseURL}/api/conversations/create`, {
22
- method: 'POST',
23
- headers: {
24
- 'Content-Type': 'application/json',
25
- 'Authorization': `Basic ${authHeader}`,
26
- },
27
- body: JSON.stringify({
28
- studentPromptId,
29
- coachPromptId,
30
- include3ConversationSummary: false,
31
- }),
32
- });
33
-
34
- if (!response.ok) {
35
- throw new Error(`Failed to create conversation: ${response.status}`);
36
- }
37
-
38
- const data = await response.json();
39
- return data.conversation.id;
40
- }
41
-
42
- // SKIPPED: Speaker toggle buttons are hidden in UI
43
- test.describe.skip('Coach Conversation UI Buttons', () => {
44
- test.use({ workers: 1 }); // Run serially to avoid rate limits
45
-
46
- test('should hide student button in coach_direct conversation', async ({ page }) => {
47
- test.setTimeout(60_000);
48
- const testUser = `coach-ui-btn-test-${Date.now()}`;
49
-
50
- // Step 1: Register and login
51
- await registerAndLogin(page, testUser);
52
- await expect(page.locator('h1:has-text("SEL Chat Coach")')).toBeVisible();
53
-
54
- console.log('✅ Logged in successfully');
55
-
56
- // Step 2: Create coach_direct conversation via API
57
- const coachConvId = await createConversationViaAPI(testUser, 'coach_direct', 'satir');
58
- console.log(`✅ Created coach conversation: ${coachConvId}`);
59
-
60
- // Step 3: Navigate to conversation page
61
- await page.goto(`${baseURL}/conversation/${coachConvId}`);
62
- await expect(page).toHaveURL(/\/conversation\//, { timeout: 10_000 });
63
- await expect(page.locator('h1')).toBeVisible({ timeout: 10_000 });
64
-
65
- console.log('✅ Navigated to coach conversation');
66
-
67
- // Step 4: Verify coach button (👨‍🏫) is visible and active
68
- const coachButton = page.locator('button[title="詢問教練"]');
69
- await expect(coachButton).toBeVisible();
70
- await expect(coachButton).toHaveClass(/from-purple-400/); // Should have gradient (active)
71
-
72
- console.log('✅ Coach button is visible and active');
73
-
74
- // Step 5: Verify student button (🎓) is NOT in the DOM
75
- const studentButton = page.locator('button[title="與學生對話"]');
76
- await expect(studentButton).toHaveCount(0); // Should not exist in DOM
77
-
78
- console.log('✅ Student button is hidden (not in DOM)');
79
-
80
- // Step 6: Verify only one speaker button exists
81
- const speakerButtons = page.locator('button[title*="對話"], button[title*="教練"]');
82
- const buttonCount = await speakerButtons.count();
83
- expect(buttonCount).toBe(1); // Only coach button
84
-
85
- console.log('✅ Only one speaker button exists (coach)');
86
-
87
- console.log('✅✅✅ Coach conversation UI button test passed!');
88
- });
89
-
90
- test('should show both buttons in student conversation', async ({ page }) => {
91
- test.setTimeout(60_000);
92
- const testUser = `student-ui-btn-test-${Date.now()}`;
93
-
94
- // Step 1: Register and login
95
- await registerAndLogin(page, testUser);
96
-
97
- console.log('✅ Logged in');
98
-
99
- // Step 2: Create student conversation via API
100
- const studentConvId = await createConversationViaAPI(testUser, 'ruirui', 'satir');
101
- console.log(`✅ Created student conversation: ${studentConvId}`);
102
-
103
- // Step 3: Navigate to conversation page
104
- await page.goto(`${baseURL}/conversation/${studentConvId}`);
105
- await expect(page).toHaveURL(/\/conversation\//, { timeout: 10_000 });
106
- await expect(page.locator('h1')).toBeVisible({ timeout: 10_000 });
107
-
108
- console.log('✅ Navigated to student conversation');
109
-
110
- // Step 4: Verify both buttons are visible
111
- const studentButton = page.locator('button[title="與學生對話"]');
112
- const coachButton = page.locator('button[title="詢問教練"]');
113
-
114
- await expect(studentButton).toBeVisible();
115
- await expect(coachButton).toBeVisible();
116
-
117
- console.log('✅ Both student and coach buttons are visible');
118
-
119
- // Step 5: Verify student button is active by default
120
- await expect(studentButton).toHaveClass(/from-blue-400/); // Should have blue gradient (active)
121
-
122
- console.log('✅ Student button is active by default');
123
-
124
- // Step 6: Verify coach button is inactive
125
- await expect(coachButton).toHaveClass(/bg-gray-200/); // Should have gray background (inactive)
126
-
127
- console.log('✅ Coach button is inactive');
128
-
129
- // Step 7: Verify exactly two speaker buttons exist
130
- const speakerButtons = page.locator('button[title*="對話"], button[title*="教練"]');
131
- const buttonCount = await speakerButtons.count();
132
- expect(buttonCount).toBe(2); // Student + Coach buttons
133
-
134
- console.log('✅ Two speaker buttons exist (student + coach)');
135
-
136
- console.log('✅✅✅ Student conversation UI button test passed!');
137
- });
138
-
139
- test('should prevent switching to student speaker in coach conversation', async ({ page }) => {
140
- test.setTimeout(90_000);
141
- const testUser = `coach-speaker-prevent-test-${Date.now()}`;
142
-
143
- // Register and login
144
- await registerAndLogin(page, testUser);
145
-
146
- // Create coach conversation
147
- const coachConvId = await createConversationViaAPI(testUser, 'coach_direct', 'satir');
148
- console.log(`✅ Created coach conversation: ${coachConvId}`);
149
-
150
- // Navigate to conversation
151
- await page.goto(`${baseURL}/conversation/${coachConvId}`);
152
- await expect(page).toHaveURL(/\/conversation\//, { timeout: 10_000 });
153
- await page.waitForTimeout(1000);
154
-
155
- console.log('✅ Opened coach conversation');
156
-
157
- // Verify student button does not exist
158
- const studentButton = page.locator('button[title="與學生對話"]');
159
- await expect(studentButton).toHaveCount(0);
160
-
161
- console.log('✅ No student button available');
162
-
163
- // Send a message (will use coach speaker automatically)
164
- const message = 'Help with classroom behavior management';
165
- const inputField = page.locator('input[type="text"]#chat-input');
166
- await inputField.fill(message);
167
-
168
- // Submit via Enter key (more reliable than clicking button)
169
- await inputField.press('Enter');
170
-
171
- // Wait for coach response (proves message was sent)
172
- const coachResponse = page.locator('.bg-gradient-to-br.from-purple-50').first();
173
- await expect(coachResponse).toBeVisible({ timeout: 60000 });
174
-
175
- console.log('✅ Received coach response (purple gradient)');
176
-
177
- // Verify there's still no way to switch to student
178
- await expect(studentButton).toHaveCount(0);
179
-
180
- console.log('✅ Student button still not available after message');
181
-
182
- console.log('✅✅✅ Coach speaker prevention test passed!');
183
- });
184
-
185
- test('should allow switching speakers in student conversation', async ({ page }) => {
186
- test.setTimeout(90_000);
187
- const testUser = `student-switch-test-${Date.now()}`;
188
-
189
- // Register and login
190
- await registerAndLogin(page, testUser);
191
-
192
- // Create student conversation
193
- const studentConvId = await createConversationViaAPI(testUser, 'ruirui', 'satir');
194
- console.log(`✅ Created student conversation: ${studentConvId}`);
195
-
196
- // Navigate to conversation
197
- await page.goto(`${baseURL}/conversation/${studentConvId}`);
198
- await expect(page).toHaveURL(/\/conversation\//, { timeout: 10_000 });
199
- await page.waitForTimeout(1000);
200
-
201
- console.log('✅ Opened student conversation');
202
-
203
- // Verify both buttons exist
204
- const studentButton = page.locator('button[title="與學生對話"]');
205
- const coachButton = page.locator('button[title="詢問教練"]');
206
-
207
- await expect(studentButton).toBeVisible();
208
- await expect(coachButton).toBeVisible();
209
-
210
- // Student button should be active by default
211
- await expect(studentButton).toHaveClass(/from-blue-400/);
212
-
213
- console.log('✅ Student button active by default');
214
-
215
- // Click coach button to switch
216
- await coachButton.click();
217
- await page.waitForTimeout(500);
218
-
219
- // Verify coach button is now active
220
- await expect(coachButton).toHaveClass(/from-purple-400/);
221
- await expect(studentButton).toHaveClass(/bg-gray-200/);
222
-
223
- console.log('✅ Successfully switched to coach speaker');
224
-
225
- // Remove Next.js dev overlay using JavaScript (it blocks clicks)
226
- await page.evaluate(() => {
227
- const overlay = document.querySelector('nextjs-portal');
228
- if (overlay) overlay.remove();
229
- });
230
- await page.waitForTimeout(500);
231
-
232
- // Click student button to switch back
233
- await studentButton.click();
234
- await page.waitForTimeout(500);
235
-
236
- // Verify student button is active again
237
- await expect(studentButton).toHaveClass(/from-blue-400/);
238
- await expect(coachButton).toHaveClass(/bg-gray-200/);
239
-
240
- console.log('✅ Successfully switched back to student speaker');
241
-
242
- console.log('✅✅✅ Speaker switching test passed!');
243
- });
244
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/e2e/coach-guidance-mode.spec.ts DELETED
@@ -1,192 +0,0 @@
1
- import { test, expect } from '@playwright/test';
2
-
3
- /**
4
- * E2E Test: Coach Guidance Mode
5
- *
6
- * Verifies that when a teacher asks the coach for advice during a student conversation,
7
- * the coach provides meta-advice TO THE TEACHER (using "您") rather than speaking
8
- * directly to the student.
9
- *
10
- * Expected behavior:
11
- * - Coach should address the teacher (user) with "您"
12
- * - Coach should provide guidance about HOW to respond to the student
13
- * - Coach should NOT give advice directly to the student
14
- */
15
-
16
- const API_URL = 'http://localhost:3000';
17
-
18
- test.describe('Coach Guidance Mode', () => {
19
- let userId: string;
20
- let conversationId: string;
21
- let username: string;
22
- let authHeader: string;
23
-
24
- test.beforeAll(async ({ request }) => {
25
- // Use timestamped username to avoid conflicts
26
- username = `test-coach-${Date.now()}`;
27
- authHeader = 'Basic ' + Buffer.from(`${username}:cz-2025`).toString('base64');
28
-
29
- // Register test user
30
- const registerResponse = await request.post(`${API_URL}/api/auth/register`, {
31
- headers: {
32
- 'Content-Type': 'application/json',
33
- },
34
- data: {
35
- username: username,
36
- password: 'cz-2025',
37
- },
38
- });
39
-
40
- expect(registerResponse.ok()).toBeTruthy();
41
- const registerData = await registerResponse.json();
42
- userId = registerData.user.id;
43
- console.log(`[TEST] Created user: ${userId}`);
44
- });
45
-
46
- test('should provide meta-advice to teacher, not direct advice to student', async ({ request }) => {
47
- // Step 1: Create a conversation with a grade 7 student (國一)
48
- const createConvResponse = await request.post(`${API_URL}/api/conversations/create`, {
49
- headers: {
50
- 'Content-Type': 'application/json',
51
- 'Authorization': authHeader,
52
- },
53
- data: {
54
- studentPromptId: 'xiaoxu',
55
- coachPromptId: 'satir',
56
- },
57
- });
58
-
59
- expect(createConvResponse.ok()).toBeTruthy();
60
- const convData = await createConvResponse.json();
61
- conversationId = convData.conversation.id;
62
- console.log(`[TEST] Created conversation: ${conversationId}`);
63
-
64
- // Step 2: Send a message as teacher to student
65
- const studentMessageResponse = await request.post(`${API_URL}/api/conversations/${conversationId}/message`, {
66
- headers: {
67
- 'Content-Type': 'application/json',
68
- 'Authorization': authHeader,
69
- },
70
- data: {
71
- messages: [
72
- {
73
- role: 'user',
74
- parts: [{ type: 'text', text: '有時候有點緊張和害怕,可以怎麼辦?' }],
75
- metadata: { speaker: 'student' },
76
- },
77
- ],
78
- },
79
- });
80
-
81
- expect(studentMessageResponse.ok()).toBeTruthy();
82
-
83
- // Read the streaming response
84
- const studentResponseText = await studentMessageResponse.text();
85
- console.log(`[TEST] Student response received (length: ${studentResponseText.length})`);
86
-
87
- // Step 3: Ask coach for advice (teacher asking coach)
88
- const coachMessageResponse = await request.post(`${API_URL}/api/conversations/${conversationId}/message`, {
89
- headers: {
90
- 'Content-Type': 'application/json',
91
- 'Authorization': authHeader,
92
- },
93
- data: {
94
- messages: [
95
- {
96
- role: 'user',
97
- parts: [{ type: 'text', text: '有時候有點緊張和害怕,可以怎麼辦?' }],
98
- metadata: { speaker: 'student' },
99
- },
100
- {
101
- role: 'assistant',
102
- parts: [{ type: 'text', text: '學生回應內容' }],
103
- metadata: { speaker: 'student' },
104
- },
105
- {
106
- role: 'user',
107
- parts: [{ type: 'text', text: '老師有什麼建議嗎?' }],
108
- metadata: { speaker: 'coach' },
109
- },
110
- ],
111
- },
112
- });
113
-
114
- expect(coachMessageResponse.ok()).toBeTruthy();
115
-
116
- // Read streaming response
117
- const coachResponseText = await coachMessageResponse.text();
118
- console.log(`[TEST] Coach raw response (first 1000 chars): ${coachResponseText.substring(0, 1000)}`);
119
-
120
- // Parse the streamed response to get the actual message content
121
- // The response uses data: prefix for each line
122
- const lines = coachResponseText.split('\n');
123
- let messageContent = '';
124
- const eventTypes = new Set<string>();
125
-
126
- for (const line of lines) {
127
- if (line.startsWith('data: ')) {
128
- try {
129
- const jsonStr = line.substring(6); // Remove 'data: ' prefix
130
- const parsed = JSON.parse(jsonStr);
131
-
132
- eventTypes.add(parsed.type || 'unknown');
133
-
134
- // Look for text-delta events which contain the actual content
135
- if (parsed.type === 'text-delta' && parsed.delta) {
136
- messageContent += parsed.delta;
137
- }
138
- } catch (e) {
139
- // Skip invalid JSON lines
140
- }
141
- }
142
- }
143
-
144
- console.log(`[TEST] Event types found: ${Array.from(eventTypes).join(', ')}`);
145
- console.log(`[TEST] Extracted coach message: "${messageContent}"`);
146
-
147
- // Step 4: Verify coach response addresses TEACHER (not student)
148
-
149
- // Should contain "你" or "您" (addressing teacher)
150
- expect(messageContent).toMatch(/你|您/);
151
- console.log(`[TEST] ✓ Coach addresses teacher directly`);
152
-
153
- // Should contain guidance keywords (建議、可以)
154
- expect(messageContent).toMatch(/建議|可以|試試/);
155
- console.log(`[TEST] ✓ Coach provides guidance/suggestions`);
156
-
157
- // Should NOT directly tell student what to do (like "面對緊張時,可以試試深呼吸")
158
- // Instead should guide teacher with reflective questions or suggestions (Satir style)
159
- // Examples: "建議您可以...", "你可以思考...", "你的感受是什麼", "你希望..."
160
- expect(messageContent).toMatch(/建議您|您可以|你可以|感受|希望|想了解/);
161
- console.log(`[TEST] ✓ Coach provides meta-guidance (reflective questions or advice to teacher)`);
162
-
163
- // Verify the tone is advisory to teacher, not directive to student
164
- // The response should be about "how to respond" not "what student should do"
165
- // Accepts both directive advice ("建議您", "例如可以這樣說") and reflective Satir style ("你的感受", "你希望")
166
- const isMetaAdvice =
167
- messageContent.includes('建議您') ||
168
- messageContent.includes('您可以') ||
169
- messageContent.includes('你可以') ||
170
- messageContent.includes('例如可以這樣說') ||
171
- messageContent.includes('例如:『') ||
172
- messageContent.includes('你的感受') ||
173
- messageContent.includes('你希望') ||
174
- messageContent.includes('你內心') ||
175
- messageContent.includes('是否有想過');
176
-
177
- expect(isMetaAdvice).toBeTruthy();
178
- console.log(`[TEST] ✓ Coach provides meta-guidance (directive advice or reflective questions)`);
179
- });
180
-
181
- test.afterAll(async ({ request }) => {
182
- // Cleanup: delete the conversation
183
- if (conversationId) {
184
- await request.delete(`${API_URL}/api/conversations/${conversationId}`, {
185
- headers: {
186
- 'Authorization': authHeader,
187
- },
188
- });
189
- console.log(`[TEST] Cleaned up conversation: ${conversationId}`);
190
- }
191
- });
192
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/e2e/complete-ui-flow.spec.ts DELETED
@@ -1,75 +0,0 @@
1
- import { test, expect } from '@playwright/test';
2
-
3
- const baseURL = process.env.BASE_URL || 'http://localhost:3000';
4
-
5
- test.describe('Complete UI Flow - No Hydration Errors', () => {
6
- test('should complete full user journey without hydration errors', async ({ page, context }) => {
7
- test.setTimeout(60_000);
8
-
9
- // Monitor console for hydration errors
10
- const consoleErrors: string[] = [];
11
- page.on('console', msg => {
12
- if (msg.type() === 'error' && msg.text().toLowerCase().includes('hydration')) {
13
- consoleErrors.push(msg.text());
14
- }
15
- });
16
-
17
- // 1. Register user
18
- const username = `ui_flow_${Date.now()}`;
19
- await page.goto(`${baseURL}/login`);
20
- await page.click('text=沒有帳號');
21
- await page.fill('input[name="username"]', username);
22
- await page.click('button:has-text("建立帳號")');
23
- await expect(page).toHaveURL(`${baseURL}/dashboard`, { timeout: 10_000 });
24
-
25
- // 2. Create conversation
26
- await page.click('h3:has-text("指定對話演練")');
27
- await page.click('text=睿睿');
28
- await page.waitForURL(/\/conversation\//, { timeout: 15_000 });
29
-
30
- // 3. Send message and wait for LLM response
31
- await page.fill('#chat-input', '你好,我需要幫助');
32
- await page.keyboard.press('Enter');
33
-
34
- // Wait for student response (blue border bubble)
35
- await expect(page.locator('.bg-white.border.border-blue-100').first()).toBeVisible({ timeout: 60_000 });
36
-
37
- // 4. Check for nested buttons before opening sidebar
38
- let nestedButtons = await page.locator('button button').count();
39
- expect(nestedButtons).toBe(0);
40
-
41
- // 5. Go to dashboard and open sidebar
42
- await page.goto(`${baseURL}/dashboard`);
43
- await page.click('svg:has(path[d*="M4 6h16"])');
44
-
45
- // Wait for sidebar to be visible
46
- await expect(page.locator('h3:has-text("對話記錄")')).toBeVisible({ timeout: 5000 });
47
-
48
- // 6. Verify no nested buttons in sidebar
49
- nestedButtons = await page.locator('button button').count();
50
- expect(nestedButtons).toBe(0);
51
-
52
- // 7. Test inline editing
53
- const editButton = page.locator('button[title="編輯標題"]').first();
54
- await expect(editButton).toBeVisible({ timeout: 5000 });
55
- await editButton.click();
56
-
57
- // 8. Verify edit input appears
58
- const titleInput = page.locator('input[type="text"]').first();
59
- await expect(titleInput).toBeVisible();
60
-
61
- // 9. Edit title
62
- await titleInput.fill('我的測試對話');
63
- await titleInput.press('Enter');
64
-
65
- // 10. Verify title updated
66
- await expect(page.locator('text=我的測試對話').first()).toBeVisible();
67
-
68
- // 11. Check for any hydration errors
69
- expect(consoleErrors).toHaveLength(0);
70
-
71
- console.log('✅ Complete UI flow successful without hydration errors');
72
- console.log('✅ Inline editing working correctly');
73
- console.log('✅ No nested buttons detected');
74
- });
75
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/e2e/{concurrency-race-conditions.spec.ts → concurrency-race-conditions.api.spec.ts} RENAMED
File without changes
tests/e2e/conversation-delete.spec.ts DELETED
@@ -1,351 +0,0 @@
1
- import { test, expect } from '@playwright/test';
2
-
3
- const baseURL = process.env.BASE_URL || 'http://localhost:3000';
4
- const PW = 'cz-2025';
5
- const authHeader = (u: string) => 'Basic ' + Buffer.from(`${u}:${PW}`).toString('base64');
6
-
7
- // Helper function to register and login
8
- async function registerAndLogin(page: any, username: string) {
9
- await page.goto(`${baseURL}/login`);
10
- await page.click('text=沒有帳號?建立新帳號');
11
- await page.fill('input[name="username"]', username);
12
- await page.click('button:has-text("建立帳號")');
13
- await expect(page).toHaveURL(`${baseURL}/dashboard`, { timeout: 10_000 });
14
- }
15
-
16
- // Helper to create a conversation
17
- async function createConversation(page: any, studentName: string = '睿睿') {
18
- await page.click('h3:has-text("指定對話演練")');
19
- await expect(page.locator('h3:has-text("選擇學生")')).toBeVisible();
20
- await page.click(`text=${studentName}`);
21
- await page.waitForURL(/\/conversation\//, { timeout: 15_000 });
22
- // Wait for conversation to be fully saved to database
23
- await expect(page.locator('h1')).toBeVisible({ timeout: 10000 });
24
- await expect(page.locator('input#chat-input')).toBeVisible({ timeout: 5000 });
25
- }
26
-
27
- test.describe('Backend API - Delete Conversation', () => {
28
- test('should successfully delete a conversation via API', async ({ request }) => {
29
- const testUser = `delete_api_${Date.now()}`;
30
-
31
- // 1. Register user
32
- const regResp = await request.post(`${baseURL}/api/auth/register`, {
33
- data: { username: testUser },
34
- });
35
- expect(regResp.ok()).toBeTruthy();
36
-
37
- // 2. Create conversation
38
- const convResp = await request.post(`${baseURL}/api/conversations/create`, {
39
- headers: {
40
- Authorization: authHeader(testUser),
41
- 'Content-Type': 'application/json',
42
- },
43
- data: {
44
- studentPromptId: 'ruirui',
45
- coachPromptId: 'satir',
46
- },
47
- });
48
- expect(convResp.ok()).toBeTruthy();
49
- const convData = await convResp.json();
50
- const conversationId = convData.conversation.id;
51
-
52
- // 3. Verify conversation exists
53
- const getResp = await request.get(`${baseURL}/api/conversations/${conversationId}`, {
54
- headers: { Authorization: authHeader(testUser) },
55
- });
56
- expect(getResp.ok()).toBeTruthy();
57
-
58
- // 4. Delete conversation
59
- const deleteResp = await request.delete(`${baseURL}/api/conversations/${conversationId}`, {
60
- headers: { Authorization: authHeader(testUser) },
61
- });
62
- expect(deleteResp.ok()).toBeTruthy();
63
- const deleteData = await deleteResp.json();
64
- expect(deleteData.success).toBe(true);
65
-
66
- // 5. Verify conversation no longer exists
67
- const getAfterDelete = await request.get(`${baseURL}/api/conversations/${conversationId}`, {
68
- headers: { Authorization: authHeader(testUser) },
69
- });
70
- expect(getAfterDelete.status()).toBe(404);
71
- });
72
-
73
- test('should return 404 when deleting non-existent conversation', async ({ request }) => {
74
- const testUser = `delete_404_${Date.now()}`;
75
-
76
- // Register user
77
- const regResp = await request.post(`${baseURL}/api/auth/register`, {
78
- data: { username: testUser },
79
- });
80
- expect(regResp.ok()).toBeTruthy();
81
-
82
- // Try to delete non-existent conversation
83
- const deleteResp = await request.delete(`${baseURL}/api/conversations/nonexistent-id-123`, {
84
- headers: { Authorization: authHeader(testUser) },
85
- });
86
- expect(deleteResp.status()).toBe(404);
87
- });
88
-
89
- test('should return 401 when deleting without authentication', async ({ request }) => {
90
- // Try to delete without auth header
91
- const deleteResp = await request.delete(`${baseURL}/api/conversations/some-id`);
92
- expect(deleteResp.status()).toBe(401);
93
- });
94
-
95
- test('should return 404 when deleting another user\'s conversation', async ({ request }) => {
96
- const user1 = `delete_owner_${Date.now()}`;
97
- const user2 = `delete_thief_${Date.now()}`;
98
-
99
- // Register both users
100
- await request.post(`${baseURL}/api/auth/register`, { data: { username: user1 } });
101
- await request.post(`${baseURL}/api/auth/register`, { data: { username: user2 } });
102
-
103
- // User 1 creates conversation
104
- const convResp = await request.post(`${baseURL}/api/conversations/create`, {
105
- headers: {
106
- Authorization: authHeader(user1),
107
- 'Content-Type': 'application/json',
108
- },
109
- data: {
110
- studentPromptId: 'ruirui',
111
- coachPromptId: 'satir',
112
- },
113
- });
114
- const convData = await convResp.json();
115
- const conversationId = convData.conversation.id;
116
-
117
- // User 2 tries to delete user 1's conversation
118
- const deleteResp = await request.delete(`${baseURL}/api/conversations/${conversationId}`, {
119
- headers: { Authorization: authHeader(user2) },
120
- });
121
- expect(deleteResp.status()).toBe(404); // Should return 404 (not found for this user)
122
-
123
- // Verify conversation still exists for user 1
124
- const getResp = await request.get(`${baseURL}/api/conversations/${conversationId}`, {
125
- headers: { Authorization: authHeader(user1) },
126
- });
127
- expect(getResp.ok()).toBeTruthy();
128
- });
129
- });
130
-
131
- test.describe('UI - Delete Conversation in Sidebar', () => {
132
- test('should show delete button on hover and confirmation modal', async ({ page }) => {
133
- test.setTimeout(60_000);
134
- const testUser = `delete_ui_hover_${Date.now()}`;
135
-
136
- // 1. Register and create conversation
137
- await registerAndLogin(page, testUser);
138
- await createConversation(page);
139
- await page.goto(`${baseURL}/dashboard`);
140
-
141
- // 2. Open sidebar
142
- await page.click('svg:has(path[d*="M4 6h16"])');
143
-
144
- // 3. Hover over conversation to reveal buttons
145
- const conversationItem = page.locator('.space-y-1 > div').first();
146
- await conversationItem.hover();
147
-
148
- // 4. Verify delete button is visible on hover
149
- const deleteButton = conversationItem.locator('button[title="刪除對話"]');
150
- await expect(deleteButton).toBeVisible({ timeout: 2000 });
151
-
152
- // 5. Click delete button
153
- await deleteButton.click();
154
-
155
- // 6. Verify confirmation modal appears
156
- await expect(page.locator('h3:has-text("確認刪除對話")')).toBeVisible();
157
- await expect(page.locator('text=確定要刪除此對話嗎?此操作無法復原。')).toBeVisible();
158
- await expect(page.locator('button:has-text("取消")')).toBeVisible();
159
- await expect(page.locator('button:has-text("確認刪除")')).toBeVisible();
160
- });
161
-
162
- test('should cancel deletion when clicking 取消', async ({ page }) => {
163
- test.setTimeout(60_000);
164
- const testUser = `delete_ui_cancel_${Date.now()}`;
165
-
166
- // Setup
167
- await registerAndLogin(page, testUser);
168
- await createConversation(page);
169
- await page.goto(`${baseURL}/dashboard`);
170
-
171
- // Open sidebar and click delete
172
- await page.click('svg:has(path[d*="M4 6h16"])');
173
- const conversationItem = page.locator('.space-y-1 > div').first();
174
- await expect(conversationItem).toBeVisible({ timeout: 2000 });
175
- const conversationTitle = await conversationItem.locator('p.text-sm.font-medium').textContent();
176
- await conversationItem.hover();
177
- await conversationItem.locator('button[title="刪除對話"]').click();
178
-
179
- // Click 取消
180
- await page.click('button:has-text("取消")');
181
-
182
- // Verify modal is closed
183
- await expect(page.locator('h3:has-text("確認刪除對話")')).not.toBeVisible({ timeout: 2000 });
184
-
185
- // Verify conversation still exists in sidebar
186
- await expect(page.locator(`p.text-sm.font-medium:has-text("${conversationTitle}")`)).toBeVisible();
187
- });
188
-
189
- test('should delete conversation when clicking 確認刪除', async ({ page }) => {
190
- test.setTimeout(60_000);
191
- const testUser = `delete_ui_confirm_${Date.now()}`;
192
-
193
- // Setup
194
- await registerAndLogin(page, testUser);
195
- await createConversation(page);
196
- await page.goto(`${baseURL}/dashboard`);
197
-
198
- // Open sidebar
199
- await page.click('svg:has(path[d*="M4 6h16"])');
200
-
201
- // Get conversation title before deletion
202
- const conversationItem = page.locator('.space-y-1 > div').first();
203
- await expect(conversationItem).toBeVisible({ timeout: 2000 });
204
- const conversationTitle = await conversationItem.locator('p.text-sm.font-medium').textContent();
205
-
206
- // Click delete and confirm
207
- await conversationItem.hover();
208
- await conversationItem.locator('button[title="刪除對話"]').click();
209
- await page.click('button:has-text("確認刪除")');
210
-
211
- // Verify modal is closed
212
- await expect(page.locator('h3:has-text("確認刪除對話")')).not.toBeVisible();
213
-
214
- // Verify conversation is removed from sidebar
215
- await expect(page.locator(`p.text-sm.font-medium:has-text("${conversationTitle}")`)).not.toBeVisible();
216
-
217
- // Verify empty state is shown
218
- await expect(page.locator('text=尚無對話記錄')).toBeVisible({ timeout: 5000 });
219
- });
220
-
221
- test('should delete multiple conversations independently', async ({ page }) => {
222
- test.setTimeout(120_000); // 2 minutes for multiple operations
223
- const testUser = `delete_ui_multiple_${Date.now()}`;
224
-
225
- // Create 3 conversations
226
- await registerAndLogin(page, testUser);
227
- await createConversation(page, '睿睿');
228
- await page.goto(`${baseURL}/dashboard`);
229
- await createConversation(page, '小許');
230
- await page.goto(`${baseURL}/dashboard`);
231
- await createConversation(page, '小恩');
232
- await page.goto(`${baseURL}/dashboard`);
233
-
234
- // Open sidebar
235
- await page.click('svg:has(path[d*="M4 6h16"])');
236
-
237
- // Verify 3 conversations exist
238
- const allConversations = page.locator('.space-y-1 > div');
239
- await expect(allConversations).toHaveCount(3, { timeout: 5000 });
240
-
241
- // Delete the second conversation
242
- const secondConv = allConversations.nth(1);
243
- const secondTitle = await secondConv.locator('p.text-sm.font-medium').textContent();
244
- await secondConv.hover();
245
- await secondConv.locator('button[title="刪除對話"]').click();
246
- await page.click('button:has-text("確認刪除")');
247
-
248
- // Verify only 2 conversations remain
249
- await expect(allConversations).toHaveCount(2, { timeout: 5000 });
250
-
251
- // Verify the deleted conversation is gone
252
- await expect(page.locator(`p.text-sm.font-medium:has-text("${secondTitle}")`)).not.toBeVisible();
253
- });
254
-
255
- test('should show delete button for coach conversations', async ({ page }) => {
256
- test.setTimeout(60_000);
257
- const testUser = `delete_ui_coach_${Date.now()}`;
258
-
259
- // Create a coach conversation via dashboard
260
- await registerAndLogin(page, testUser);
261
- await page.goto(`${baseURL}/dashboard`);
262
-
263
- // Click coach card and confirm
264
- await page.click('h3:has-text("教練回顧")');
265
- await expect(page.locator('text=開始諮詢')).toBeVisible({ timeout: 5_000 });
266
- await page.click('button:has-text("開始諮詢")');
267
-
268
- await page.waitForURL(/\/conversation\//, { timeout: 15_000 });
269
- await page.goto(`${baseURL}/dashboard`);
270
-
271
- // Open sidebar
272
- await page.click('svg:has(path[d*="M4 6h16"])');
273
-
274
- // Verify coach conversation has delete button
275
- const coachConv = page.locator('.space-y-1 > div').filter({ hasText: '👨‍🏫' }).first();
276
- await expect(coachConv).toBeVisible({ timeout: 2000 });
277
- await coachConv.hover();
278
- const deleteButton = coachConv.locator('button[title="刪除對話"]');
279
- await expect(deleteButton).toBeVisible();
280
-
281
- // Delete and verify it works
282
- await deleteButton.click();
283
- await expect(page.locator('h3:has-text("確認刪除對話")')).toBeVisible();
284
- await page.click('button:has-text("確認刪除")');
285
-
286
- // Verify coach conversation is deleted
287
- await expect(page.locator('text=尚無對話記錄')).toBeVisible({ timeout: 5000 });
288
- });
289
- });
290
-
291
- test.describe('Edge Cases', () => {
292
- test('should show only one modal when delete button is clicked', async ({ page }) => {
293
- test.setTimeout(60_000);
294
- const testUser = `delete_edge_modal_${Date.now()}`;
295
-
296
- await registerAndLogin(page, testUser);
297
- await createConversation(page);
298
- await page.goto(`${baseURL}/dashboard`);
299
-
300
- // Open sidebar and hover
301
- await page.click('svg:has(path[d*="M4 6h16"])');
302
- const conversationItem = page.locator('.space-y-1 > div').first();
303
- await expect(conversationItem).toBeVisible({ timeout: 2000 });
304
- await conversationItem.hover();
305
-
306
- // Click delete button
307
- const deleteButton = conversationItem.locator('button[title="刪除對話"]');
308
- await deleteButton.click();
309
-
310
- // Modal should appear exactly once
311
- const modals = page.locator('h3:has-text("確認刪除對話")');
312
- await expect(modals).toHaveCount(1);
313
-
314
- // Cancel the delete
315
- await page.click('button:has-text("取消")');
316
- await expect(page.locator('h3:has-text("確認刪除對話")')).not.toBeVisible({ timeout: 2000 });
317
- });
318
-
319
- test('should successfully delete and update UI state', async ({ page }) => {
320
- test.setTimeout(60_000);
321
- const testUser = `delete_edge_ui_update_${Date.now()}`;
322
-
323
- await registerAndLogin(page, testUser);
324
- await createConversation(page);
325
- await page.goto(`${baseURL}/dashboard`);
326
-
327
- // Open sidebar
328
- await page.click('svg:has(path[d*="M4 6h16"])');
329
-
330
- // Click delete and confirm
331
- const conversationItem = page.locator('.space-y-1 > div').first();
332
- await expect(conversationItem).toBeVisible({ timeout: 2000 });
333
- await conversationItem.hover();
334
- await conversationItem.locator('button[title="刪除對話"]').click();
335
-
336
- // Verify modal appears and confirm deletion
337
- await expect(page.locator('h3:has-text("確認刪除對話")')).toBeVisible();
338
- await page.click('button:has-text("確認刪除")');
339
-
340
- // Verify empty state in sidebar (deletion succeeded)
341
- await expect(page.locator('text=尚無對話記錄')).toBeVisible({ timeout: 5000 });
342
-
343
- // Close sidebar
344
- await page.click('.fixed.inset-0.bg-black\\/50');
345
- await expect(page.locator('.fixed.inset-0.bg-black\\/50')).not.toBeVisible({ timeout: 2000 });
346
-
347
- // Open sidebar again to verify persistence
348
- await page.click('svg:has(path[d*="M4 6h16"])');
349
- await expect(page.locator('text=尚無對話記錄')).toBeVisible({ timeout: 2000 });
350
- });
351
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/e2e/conversation-titles.spec.ts DELETED
@@ -1,329 +0,0 @@
1
- import { test, expect } from '@playwright/test';
2
-
3
- const baseURL = process.env.BASE_URL || 'http://localhost:3000';
4
-
5
- // Helper function to register and login
6
- async function registerAndLogin(page: any, username: string) {
7
- await page.goto(`${baseURL}/login`);
8
- await page.click('text=沒有帳號?建立新帳號');
9
- await page.fill('input[name="username"]', username);
10
- await page.click('button:has-text("建立帳號")');
11
- await expect(page).toHaveURL(`${baseURL}/dashboard`, { timeout: 10_000 });
12
- }
13
-
14
- // Helper to create a conversation
15
- async function createConversation(page: any, studentName: string = '睿睿') {
16
- await page.click('h3:has-text("指定對話演練")');
17
- await expect(page.locator('h3:has-text("選擇學生")')).toBeVisible();
18
- await page.click(`text=${studentName}`);
19
- await page.waitForURL(/\/conversation\//, { timeout: 15_000 });
20
- }
21
-
22
- // Helper to send a message and wait for response
23
- async function sendMessage(page: any, message: string) {
24
- await page.fill('input#chat-input', message);
25
- await page.keyboard.press('Enter');
26
- await expect(page.locator('.whitespace-pre-wrap').first()).toBeVisible({ timeout: 60000 });
27
- }
28
-
29
- test.describe('Conversation Title Auto-Generation', () => {
30
- test('should auto-generate title after first few messages', async ({ page }) => {
31
- test.setTimeout(60_000); // 2 minutes for LLM responses
32
- const testUser = `title_auto_${Date.now()}`;
33
-
34
- // 1. Register and login
35
- await registerAndLogin(page, testUser);
36
-
37
- // 2. Create new conversation
38
- await createConversation(page);
39
-
40
- // 3. Send first message
41
- await sendMessage(page, '你好,我今天心情不好');
42
-
43
- // 4. Open sidebar to check title
44
- await page.click('button[aria-label="menu"], svg:has(path[d*="M4 6h16"])');
45
-
46
- // 6. Verify a title exists (not default "新對話" or "與 XX 的對話")
47
- const conversationItem = page.locator('.space-y-1 > div').first();
48
- const titleText = await conversationItem.locator('p.text-sm.font-medium').textContent();
49
-
50
- // Title should be auto-generated (not empty and not default)
51
- expect(titleText).toBeTruthy();
52
- expect(titleText).not.toBe('新對話');
53
-
54
- console.log('Auto-generated title:', titleText);
55
- });
56
- });
57
-
58
- test.describe('Inline Title Editing - Sidebar', () => {
59
- test('should allow editing conversation title in sidebar', async ({ page }) => {
60
- test.setTimeout(60_000);
61
- const testUser = `title_edit_sidebar_${Date.now()}`;
62
-
63
- // 1. Register and create conversation
64
- await registerAndLogin(page, testUser);
65
- await createConversation(page);
66
- await sendMessage(page, '測試訊息');
67
-
68
- // 2. Open sidebar
69
- await page.click('button[aria-label="menu"], svg:has(path[d*="M4 6h16"])');
70
-
71
- // 3. Hover over conversation to reveal edit button
72
- const conversationItem = page.locator('.space-y-1 > div').first();
73
- await conversationItem.hover();
74
-
75
- // 4. Click edit button (pencil icon)
76
- await conversationItem.locator('button[title="編輯標題"]').click();
77
-
78
- // 5. Edit the title
79
- const titleInput = conversationItem.locator('input[type="text"]');
80
- await expect(titleInput).toBeVisible();
81
- await titleInput.fill('我的自訂標題');
82
-
83
- // 6. Press Enter to save
84
- await titleInput.press('Enter');
85
-
86
- // 7. Verify title is updated
87
- await expect(conversationItem.locator('p.text-sm.font-medium:has-text("我的自訂標題")')).toBeVisible();
88
- });
89
-
90
- test('should cancel edit on Escape key in sidebar', async ({ page }) => {
91
- test.setTimeout(60_000);
92
- const testUser = `title_cancel_sidebar_${Date.now()}`;
93
-
94
- await registerAndLogin(page, testUser);
95
- await createConversation(page);
96
- await sendMessage(page, '測試');
97
-
98
- // Open sidebar and get original title
99
- await page.click('button[aria-label="menu"], svg:has(path[d*="M4 6h16"])');
100
- const conversationItem = page.locator('.space-y-1 > div').first();
101
- const originalTitle = await conversationItem.locator('p.text-sm.font-medium').textContent();
102
-
103
- // Start editing
104
- await conversationItem.hover();
105
- await conversationItem.locator('button[title="編輯標題"]').click();
106
-
107
- // Change text but press Escape
108
- const titleInput = conversationItem.locator('input[type="text"]');
109
- await titleInput.fill('不要儲存這個');
110
- await titleInput.press('Escape');
111
- await expect(titleInput).not.toBeVisible({ timeout: 2000 });
112
-
113
- // Verify original title is preserved
114
- const currentTitle = await conversationItem.locator('p.text-sm.font-medium').textContent();
115
- expect(currentTitle).toBe(originalTitle);
116
- });
117
- });
118
-
119
- test.describe('Inline Title Editing - Dashboard', () => {
120
- test('should allow editing conversation title in dashboard modal', async ({ page }) => {
121
- test.setTimeout(60_000);
122
- const testUser = `title_edit_dashboard_${Date.now()}`;
123
-
124
- // 1. Create conversation
125
- await registerAndLogin(page, testUser);
126
- await createConversation(page);
127
- await sendMessage(page, '測試訊息');
128
-
129
- // 2. Go back to dashboard
130
- await page.goto(`${baseURL}/dashboard`);
131
-
132
- // 3. Open sidebar to see conversation list
133
- await page.click('svg:has(path[d*="M4 6h16"])');
134
- await expect(page.locator('text=對話記錄')).toBeVisible({ timeout: 2000 });
135
-
136
- // 4. Hover over conversation to reveal edit button
137
- const conversationCard = page.locator('.space-y-3 > div').first();
138
- await conversationCard.hover();
139
-
140
- // 5. Click edit button
141
- await conversationCard.locator('button[title="編輯標題"]').click();
142
-
143
- // 6. Edit the title
144
- const titleInput = conversationCard.locator('input[type="text"]');
145
- await expect(titleInput).toBeVisible();
146
- await titleInput.fill('Dashboard編輯標題');
147
-
148
- // 7. Click outside to save (blur)
149
- await page.locator('h3:has-text("對話記錄")').click();
150
- await expect(conversationCard.locator('input[type="text"]')).not.toBeVisible({ timeout: 2000 });
151
-
152
- // 8. Verify title is updated
153
- await expect(conversationCard.locator('h4:has-text("Dashboard編輯標題")')).toBeVisible();
154
- });
155
- });
156
-
157
- test.describe('Title Source Persistence', () => {
158
- test('should not auto-update user-edited titles', async ({ page }) => {
159
- test.setTimeout(60_000); // 3 minutes for multiple LLM responses
160
- const testUser = `title_persist_${Date.now()}`;
161
-
162
- // 1. Create conversation and let auto-title generate
163
- await registerAndLogin(page, testUser);
164
- await createConversation(page);
165
- await sendMessage(page, '我需要幫助');
166
-
167
- // 2. Manually edit the title via sidebar
168
- await page.click('button[aria-label="menu"], svg:has(path[d*="M4 6h16"])');
169
- const conversationItem = page.locator('.space-y-1 > div').first();
170
- await conversationItem.hover();
171
- await conversationItem.locator('button[title="編輯標題"]').click();
172
-
173
- const customTitle = '我的固定標題-不要改';
174
- const titleInput = conversationItem.locator('input[type="text"]');
175
- await titleInput.fill(customTitle);
176
- await titleInput.press('Enter');
177
- await expect(titleInput).not.toBeVisible({ timeout: 2000 });
178
-
179
- // 3. Close sidebar
180
- await page.click('.fixed.inset-0.bg-black\\/50');
181
-
182
- // 4. Send more messages to trigger auto-title logic
183
- await sendMessage(page, '我想談談完全不同的話題');
184
- await sendMessage(page, '關於數學作業的問題');
185
-
186
- // 5. Verify title remains unchanged
187
- await page.click('button[aria-label="menu"], svg:has(path[d*="M4 6h16"])');
188
- const updatedConvItem = page.locator('.space-y-1 > div').first();
189
- const finalTitle = await updatedConvItem.locator('p.text-sm.font-medium').textContent();
190
-
191
- expect(finalTitle).toBe(customTitle);
192
- console.log('User title preserved after additional messages:', finalTitle);
193
- });
194
- });
195
-
196
- test.describe('UI No Hydration Errors', () => {
197
- test('should not have nested buttons in Sidebar - verify DOM structure', async ({ page }) => {
198
- test.setTimeout(60_000);
199
- const testUser = `hydration_test_${Date.now()}`;
200
-
201
- // Register and create conversation
202
- await registerAndLogin(page, testUser);
203
- await createConversation(page);
204
- await page.goto(`${baseURL}/dashboard`);
205
-
206
- // Open sidebar
207
- await page.click('svg:has(path[d*="M4 6h16"])');
208
- await expect(page.locator('button[title="編輯標題"]').first()).toBeVisible({ timeout: 5000 });
209
-
210
- // Check if any button contains another button (which would cause hydration error)
211
- const nestedButtons = await page.locator('button button').count();
212
- expect(nestedButtons).toBe(0);
213
-
214
- // Verify conversation list exists and edit button is accessible
215
- const editButton = await page.locator('button[title="編輯標題"]').first();
216
- await expect(editButton).toBeVisible({ timeout: 5000 });
217
-
218
- console.log('✅ No nested buttons found - hydration error fixed');
219
- });
220
-
221
- test('should allow independent click actions in Sidebar', async ({ page }) => {
222
- test.setTimeout(60_000);
223
- const testUser = `click_test_${Date.now()}`;
224
-
225
- await registerAndLogin(page, testUser);
226
- await createConversation(page);
227
- await sendMessage(page, '測試訊息');
228
-
229
- // Go to dashboard and open sidebar
230
- await page.goto(`${baseURL}/dashboard`);
231
- await page.click('svg:has(path[d*="M4 6h16"])');
232
- await expect(page.locator('text=對話記錄')).toBeVisible({ timeout: 2000 });
233
-
234
- // Click edit button - should NOT navigate
235
- const editButton = await page.locator('button[title="編輯標題"]').first();
236
- await editButton.click();
237
-
238
- // Should show input field
239
- const titleInput = page.locator('input[type="text"]').first();
240
- await expect(titleInput).toBeVisible();
241
-
242
- // Still on dashboard
243
- await expect(page).toHaveURL(`${baseURL}/dashboard`);
244
-
245
- console.log('✅ Edit button works without triggering navigation');
246
- });
247
- });
248
-
249
- test.describe('API Verification', () => {
250
- test('should create conversation with titleSource=auto by default', async ({ page }) => {
251
- test.setTimeout(60_000);
252
-
253
- // Create user via API
254
- const username = `api_test_${Date.now()}`;
255
- const registerResponse = await page.request.post(`${baseURL}/api/auth/register`, {
256
- data: { username, password: 'cz-2025' }
257
- });
258
- expect(registerResponse.ok()).toBeTruthy();
259
-
260
- // Create conversation via API
261
- const basicAuth = Buffer.from(`${username}:cz-2025`).toString('base64');
262
- const convResponse = await page.request.post(`${baseURL}/api/conversations/create`, {
263
- headers: {
264
- 'Authorization': `Basic ${basicAuth}`,
265
- 'Content-Type': 'application/json'
266
- },
267
- data: {
268
- studentPromptId: 'ruirui',
269
- coachPromptId: 'satir'
270
- }
271
- });
272
-
273
- expect(convResponse.ok()).toBeTruthy();
274
- const convData = await convResponse.json();
275
-
276
- // Verify titleSource is 'auto'
277
- expect(convData.conversation.titleSource).toBe('auto');
278
- console.log('API: Conversation created with titleSource=auto');
279
- });
280
-
281
- test('should update titleSource to user when manually editing', async ({ page }) => {
282
- test.setTimeout(60_000);
283
-
284
- // Create user and conversation via API
285
- const username = `api_edit_${Date.now()}`;
286
- const registerResponse = await page.request.post(`${baseURL}/api/auth/register`, {
287
- data: { username, password: 'cz-2025' }
288
- });
289
- const userData = await registerResponse.json();
290
-
291
- const basicAuth = Buffer.from(`${username}:cz-2025`).toString('base64');
292
- const convResponse = await page.request.post(`${baseURL}/api/conversations/create`, {
293
- headers: {
294
- 'Authorization': `Basic ${basicAuth}`,
295
- 'Content-Type': 'application/json'
296
- },
297
- data: {
298
- studentPromptId: 'xiaoxu',
299
- coachPromptId: 'satir'
300
- }
301
- });
302
- const convData = await convResponse.json();
303
- const convId = convData.conversation.id;
304
-
305
- // Update title via API
306
- const updateResponse = await page.request.put(`${baseURL}/api/conversations/${convId}`, {
307
- headers: {
308
- 'Authorization': `Basic ${basicAuth}`,
309
- 'Content-Type': 'application/json'
310
- },
311
- data: {
312
- title: 'API Updated Title',
313
- titleSource: 'user'
314
- }
315
- });
316
-
317
- if (!updateResponse.ok()) {
318
- const errorText = await updateResponse.text();
319
- console.error('Update failed:', updateResponse.status(), errorText);
320
- }
321
- expect(updateResponse.ok()).toBeTruthy();
322
- const updateData = await updateResponse.json();
323
-
324
- // Verify titleSource changed to 'user'
325
- expect(updateData.conversation.titleSource).toBe('user');
326
- expect(updateData.conversation.title).toBe('API Updated Title');
327
- console.log('API: Title source updated to user');
328
- });
329
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/e2e/dashboard-many-conversations.spec.ts DELETED
@@ -1,334 +0,0 @@
1
- import { test, expect } from '@playwright/test';
2
-
3
- const baseURL = process.env.BASE_URL || 'http://localhost:3000';
4
-
5
- // Helper function to register and login
6
- async function registerAndLogin(page: any, username: string) {
7
- await page.goto(`${baseURL}/login`);
8
- await page.click('text=沒有帳號?建立新帳號');
9
- await page.fill('input[name="username"]', username);
10
- await page.click('button:has-text("建立帳號")');
11
- await expect(page).toHaveURL(`${baseURL}/dashboard`, { timeout: 10_000 });
12
- }
13
-
14
- // Helper to create a conversation via API
15
- async function createConversationViaAPI(
16
- username: string,
17
- studentPromptId: string,
18
- coachPromptId: string = 'satir',
19
- title?: string
20
- ) {
21
- const authHeader = Buffer.from(`${username}:cz-2025`).toString('base64');
22
- const response = await fetch(`${baseURL}/api/conversations/create`, {
23
- method: 'POST',
24
- headers: {
25
- 'Content-Type': 'application/json',
26
- 'Authorization': `Basic ${authHeader}`,
27
- },
28
- body: JSON.stringify({
29
- studentPromptId,
30
- coachPromptId,
31
- include3ConversationSummary: false,
32
- ...(title && { title }),
33
- }),
34
- });
35
-
36
- if (!response.ok) {
37
- throw new Error(`Failed to create conversation: ${response.status}`);
38
- }
39
-
40
- const data = await response.json();
41
- return data.conversation.id;
42
- }
43
-
44
- // SKIPPED: "自訂對話演練" card is hidden in UI
45
- test.describe.skip('Dashboard with Many Conversations', () => {
46
- test('should create 15 conversations and verify dashboard accessibility', async ({ page }) => {
47
- test.setTimeout(120_000); // 2 minutes
48
- const testUser = `many_conv_${Date.now()}`;
49
-
50
- // Step 1: Register and login
51
- await registerAndLogin(page, testUser);
52
- await expect(page.locator('h1:has-text("SEL Chat Coach")')).toBeVisible();
53
-
54
- console.log('Creating 15 conversations via API...');
55
-
56
- // Step 2: Create 10 student conversations via API (mix of personality types)
57
- const studentPersonalities = [
58
- 'ruirui',
59
- 'xiaoxu',
60
- 'xiaoen',
61
- 'xiaojie',
62
- ];
63
-
64
- const studentConvIds = [];
65
- for (let i = 0; i < 10; i++) {
66
- const personality = studentPersonalities[i % 4];
67
- const convId = await createConversationViaAPI(testUser, personality);
68
- studentConvIds.push(convId);
69
- console.log(`Created student conversation ${i + 1}/10: ${convId}`);
70
- }
71
-
72
- // Step 3: Create 5 coach-direct conversations via API
73
- const coachConvIds = [];
74
- const coaches = ['satir', 'satir', 'satir'];
75
- for (let i = 0; i < 5; i++) {
76
- const coachPromptId = coaches[i % 3];
77
- const convId = await createConversationViaAPI(testUser, 'coach_direct', coachPromptId, `教練回顧 ${i + 1}`);
78
- coachConvIds.push(convId);
79
- console.log(`Created coach conversation ${i + 1}/5: ${convId}`);
80
- }
81
-
82
- console.log('✅ All 15 conversations created');
83
-
84
- // Step 4: Refresh dashboard to load all conversations
85
- await page.reload();
86
- await page.waitForLoadState('networkidle');
87
-
88
- // Test Case 1: Verify all 3 action cards are visible WITHOUT scrolling
89
- console.log('Test 1: Verifying action cards are accessible without scrolling');
90
-
91
- const newConvCard = page.locator('h3:has-text("指定對話演練")');
92
- const continueConvCard = page.locator('h3:has-text("自訂對話演練")');
93
- const coachCard = page.locator('h3:has-text("教練回顧")');
94
-
95
- await expect(newConvCard).toBeVisible({ timeout: 10000 });
96
- await expect(continueConvCard).toBeVisible({ timeout: 10000 });
97
- await expect(coachCard).toBeVisible({ timeout: 10000 });
98
-
99
- // Verify "自訂對話演練" card shows conversation count badge
100
- // Try multiple selectors as badge format might vary
101
- let conversationBadge = page.locator('text=/\\d+ 個對話/');
102
-
103
- // If not found with exact pattern, try broader selectors
104
- const badgeCount = await conversationBadge.count();
105
- if (badgeCount === 0) {
106
- // Try alternative badge locations
107
- conversationBadge = continueConvCard.locator('text=/\\d+/').first();
108
- }
109
-
110
- await expect(conversationBadge).toBeVisible({ timeout: 5000 });
111
- const badgeText = await conversationBadge.textContent();
112
- console.log(`Dashboard shows: ${badgeText}`);
113
-
114
- console.log('✅ All action cards visible and accessible');
115
-
116
- // Test Case 2: Verify conversation list modal has scrolling with 10+ conversations
117
- console.log('Test 2: Verifying conversation list scrolling');
118
-
119
- await page.click('h3:has-text("自訂對話演練")');
120
- await expect(page.locator('h3:has-text("對話記錄")')).toBeVisible();
121
-
122
- // Count visible conversation items (should filter out coach-direct)
123
- const conversationCards = page.locator('.space-y-3 > div');
124
- const count = await conversationCards.count();
125
-
126
- console.log(`✅ Conversation list shows ${count} conversations (should be 15 total: 10 student + 5 coach)`);
127
-
128
- // Should show all 15 conversations (student + coach)
129
- expect(count).toBe(15);
130
-
131
- // Verify modal is scrollable by checking if last conversation is in view
132
- const lastConvCard = conversationCards.last();
133
-
134
- // Scroll to bottom
135
- await lastConvCard.scrollIntoViewIfNeeded();
136
- await expect(lastConvCard).toBeVisible();
137
-
138
- console.log('✅ Conversation list is scrollable');
139
-
140
- // Verify coach conversations ARE in the list (with coach emoji)
141
- const coachConversations = page.locator('.space-y-3 > div').filter({ hasText: '👨‍🏫 諮詢教練' });
142
- const coachCount = await coachConversations.count();
143
- expect(coachCount).toBe(5); // Should have 5 coach conversations
144
-
145
- console.log(`✅ Coach conversations shown in list: ${coachCount}`);
146
-
147
- // Close modal by clicking backdrop
148
- const modalBackdrop = page.locator('.fixed.inset-0.bg-black\\/50');
149
- await modalBackdrop.click({ position: { x: 10, y: 10 } }); // Click top-left corner of backdrop
150
- await expect(modalBackdrop).toHaveCount(0, { timeout: 5_000 });
151
-
152
- // Test Case 3: Verify can create new conversation without scrolling
153
- console.log('Test 3: Verifying new conversation creation');
154
-
155
- await page.click('h3:has-text("指定對話演練")');
156
- await expect(page.locator('h3:has-text("選擇學生")')).toBeVisible();
157
-
158
- // Select first student
159
- const studentName = page.locator('text=睿睿').first();
160
- await expect(studentName).toBeVisible();
161
- await studentName.click();
162
-
163
- // Should navigate to conversation page
164
- await expect(page).toHaveURL(/\/conversation\//, { timeout: 15_000 });
165
- await expect(page.locator('h1')).toBeVisible({ timeout: 10_000 });
166
-
167
- console.log('✅ New conversation created successfully');
168
-
169
- // Return to dashboard
170
- await page.goto(`${baseURL}/dashboard`);
171
- await expect(page.locator('h1:has-text("SEL Chat Coach")')).toBeVisible({ timeout: 5000 });
172
-
173
- // Test Case 4: Verify can access coach chat without scrolling
174
- console.log('Test 4: Verifying coach chat access');
175
-
176
- await page.click('h3:has-text("教練回顧")');
177
- await expect(page.locator('text=開始諮詢')).toBeVisible({ timeout: 5_000 });
178
- await page.click('button:has-text("開始諮詢")');
179
-
180
- // Should navigate to conversation page (coach conversations now use regular conversation page)
181
- await expect(page).toHaveURL(/\/conversation\//, { timeout: 10_000 });
182
- await expect(page.locator('h1')).toBeVisible({ timeout: 10_000 });
183
-
184
- console.log('✅ Coach conversation created and accessible without scrolling');
185
-
186
- console.log('✅✅✅ All tests passed with 15 conversations!');
187
- });
188
-
189
- test('should verify sidebar with many conversations', async ({ page }) => {
190
- test.setTimeout(120_000);
191
- const testUser = `sidebar_many_${Date.now()}`;
192
-
193
- // Register and login
194
- await registerAndLogin(page, testUser);
195
-
196
- console.log('Creating 12 student conversations...');
197
-
198
- // Create 12 student conversations
199
- for (let i = 0; i < 12; i++) {
200
- const personality = i % 2 === 0 ? 'ruirui' : 'xiaoxu';
201
- await createConversationViaAPI(testUser, personality);
202
- }
203
-
204
- // Create 3 coach-direct conversations
205
- for (let i = 0; i < 3; i++) {
206
- await createConversationViaAPI(testUser, 'coach_direct', 'satir', `教練回顧 ${i + 1}`);
207
- }
208
-
209
- console.log('✅ 15 conversations created');
210
-
211
- // Refresh to load conversations
212
- await page.reload();
213
- await expect(page.locator('h1:has-text("SEL Chat Coach")')).toBeVisible({ timeout: 5000 });
214
-
215
- // Create a conversation and navigate to it
216
- await page.click('h3:has-text("指定對話演練")');
217
- await expect(page.locator('h3:has-text("選擇學生")')).toBeVisible({ timeout: 5000 });
218
- const studentName = page.locator('text=睿睿').first();
219
- await studentName.click();
220
- await expect(page).toHaveURL(/\/conversation\//, { timeout: 15_000 });
221
-
222
- // Wait for conversation page to load (wait for header to be visible)
223
- await expect(page.locator('h1')).toBeVisible({ timeout: 10_000 });
224
- await expect(page.locator('input#chat-input')).toBeVisible({ timeout: 5000 });
225
-
226
- // Open sidebar (look for hamburger button more specifically)
227
- const hamburgerButton = page.locator('button:has(svg)').first();
228
- await hamburgerButton.click();
229
-
230
- // Verify sidebar shows conversations
231
- await expect(page.getByRole('heading', { name: /對話記錄/i })).toBeVisible({ timeout: 3000 });
232
-
233
- // Count conversations in sidebar - they should be in .space-y-1 container
234
- // Each conversation is a div.relative.group within that container
235
- const sidebarConvs = page.locator('.space-y-1 > div.relative.group');
236
-
237
- // Wait for at least one conversation to be visible
238
- await expect(sidebarConvs.first()).toBeVisible({ timeout: 10_000 });
239
-
240
- const sidebarCount = await sidebarConvs.count();
241
-
242
- console.log(`Sidebar shows ${sidebarCount} conversation items`);
243
-
244
- // Should show 15+ conversations (12 student + 3 coach, plus 1 new student = 16)
245
- expect(sidebarCount).toBeGreaterThanOrEqual(15);
246
-
247
- console.log(`✅ Sidebar shows ${sidebarCount} conversations (student + coach)`);
248
-
249
- // Verify sidebar is scrollable (if we have many conversations)
250
- if (sidebarCount > 5) {
251
- console.log('✅ Sidebar is scrollable (has enough conversations)');
252
- }
253
-
254
- // Close sidebar by clicking overlay
255
- await page.click('.fixed.inset-0.bg-black\\/50');
256
- await expect(page.locator('.fixed.inset-0.bg-black\\/50')).not.toBeVisible({ timeout: 2000 });
257
-
258
- console.log('✅✅✅ Sidebar tests passed with many conversations!');
259
- });
260
-
261
- test('should handle navigation between conversations with high count', async ({ page }) => {
262
- test.setTimeout(120_000);
263
- const testUser = `nav_many_${Date.now()}`;
264
-
265
- // Register and login
266
- await registerAndLogin(page, testUser);
267
-
268
- console.log('Creating 8 conversations...');
269
-
270
- // Create 8 student conversations
271
- const convIds = [];
272
- for (let i = 0; i < 8; i++) {
273
- const personality = ['ruirui', 'xiaoxu', 'xiaoen', 'xiaojie'][i % 4];
274
- const convId = await createConversationViaAPI(testUser, personality);
275
- convIds.push(convId);
276
- }
277
-
278
- console.log('✅ 8 conversations created');
279
-
280
- // Refresh dashboard
281
- await page.reload();
282
- await expect(page.locator('h1:has-text("SEL Chat Coach")')).toBeVisible({ timeout: 5000 });
283
-
284
- // Open conversation list
285
- await page.click('h3:has-text("自訂對話演練")');
286
- await expect(page.locator('h3:has-text("對話記錄")')).toBeVisible();
287
-
288
- // Navigate to first conversation
289
- const firstConv = page.locator('.space-y-3 > div').first();
290
- await firstConv.click();
291
- await expect(page).toHaveURL(/\/conversation\//, { timeout: 10_000 });
292
- await expect(page.locator('input#chat-input')).toBeVisible({ timeout: 5000 });
293
-
294
- console.log('✅ Navigated to first conversation');
295
-
296
- // Go back to dashboard
297
- await page.goto(`${baseURL}/dashboard`);
298
- await expect(page.locator('h1:has-text("SEL Chat Coach")')).toBeVisible({ timeout: 5000 });
299
-
300
- // Open conversation list again
301
- await page.click('h3:has-text("自訂對話演練")');
302
- await expect(page.locator('h3:has-text("對話記錄")')).toBeVisible();
303
-
304
- // Scroll to bottom and click last conversation
305
- const lastConv = page.locator('.space-y-3 > div').last();
306
- await lastConv.scrollIntoViewIfNeeded();
307
- await lastConv.click();
308
- await expect(page).toHaveURL(/\/conversation\//, { timeout: 10_000 });
309
-
310
- console.log('✅ Navigated to last conversation after scrolling');
311
-
312
- // Verify all action cards still work from dashboard
313
- await page.goto(`${baseURL}/dashboard`);
314
- await expect(page.locator('h1:has-text("SEL Chat Coach")')).toBeVisible({ timeout: 5000 });
315
-
316
- // Verify new conversation still accessible
317
- await page.click('h3:has-text("指定對話演練")');
318
- await expect(page.locator('h3:has-text("選擇學生")')).toBeVisible();
319
-
320
- // Close modal by clicking backdrop
321
- await page.click('.fixed.inset-0.bg-black\\/50');
322
- await expect(page.locator('h3:has-text("選擇學生")')).not.toBeVisible({ timeout: 2000 });
323
-
324
- // Verify coach chat still accessible
325
- await page.click('h3:has-text("教練回顧")');
326
- await expect(page.locator('text=開始諮詢')).toBeVisible();
327
-
328
- // Close modal
329
- await page.click('button:has-text("取消")');
330
- await expect(page.locator('text=開始諮詢')).not.toBeVisible({ timeout: 2000 });
331
-
332
- console.log('✅✅✅ Navigation tests passed with many conversations!');
333
- });
334
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/e2e/dashboard.spec.ts DELETED
@@ -1,219 +0,0 @@
1
- import { test, expect } from '@playwright/test';
2
-
3
- const baseURL = process.env.BASE_URL || 'http://localhost:3000';
4
-
5
- // Helper function to register and login
6
- async function registerAndLogin(page: any, username: string) {
7
- await page.goto(`${baseURL}/login`);
8
- await page.click('text=沒有帳號?建立新帳號');
9
- await page.fill('input[name="username"]', username);
10
- await page.click('button:has-text("建立帳號")');
11
- await expect(page).toHaveURL(`${baseURL}/dashboard`, { timeout: 10_000 });
12
- }
13
-
14
- test.describe('Dashboard Login Flow', () => {
15
- test('should show dashboard after login with 3 action cards', async ({ page }) => {
16
- test.setTimeout(60_000);
17
- const testUser = `dashboard_${Date.now()}`;
18
-
19
- // 1. Register new user
20
- await registerAndLogin(page, testUser);
21
-
22
- // 2. Verify dashboard header
23
- await expect(page.locator('h1:has-text("SEL Chat Coach")')).toBeVisible();
24
-
25
- // 3. Verify welcome message with username
26
- await expect(page.locator(`text=你好,${testUser}!`)).toBeVisible();
27
-
28
- // 4. Verify 2 action cards are visible (Continue Conversation card is hidden)
29
- await expect(page.locator('h3:has-text("指定對話演練")')).toBeVisible();
30
- await expect(page.locator('h3:has-text("教練回顧")')).toBeVisible();
31
-
32
- // 5. Verify card descriptions
33
- await expect(page.locator('text=選擇一位學生,開始全新的對話練習')).toBeVisible();
34
- await expect(page.locator('text=基於過去 25 天對話,獲取專業建議')).toBeVisible();
35
- });
36
-
37
- test('should open new conversation modal and show student selection', async ({ page }) => {
38
- test.setTimeout(60_000);
39
- const testUser = `modal_test_${Date.now()}`;
40
-
41
- // Register and login
42
- await registerAndLogin(page, testUser);
43
-
44
- // Click "新對話" card
45
- await page.click('h3:has-text("指定對話演練")');
46
-
47
- // Verify modal appears
48
- await expect(page.locator('h3:has-text("選擇學生")')).toBeVisible();
49
- await expect(page.locator('text=點選學生即可開始對話練習')).toBeVisible();
50
-
51
- // Verify students are listed (should have at least 3 with 🎓 emoji)
52
- const studentCards = page.locator('div').filter({ hasText: '🎓' });
53
- await expect(studentCards.first()).toBeVisible();
54
-
55
- // Verify student names are present (Chinese names: 睿睿, 小許, 小恩, 睿睿)
56
- const hasXiaoMing = await page.locator('text=睿睿').isVisible();
57
- const hasXiaoHua = await page.locator('text=小許').isVisible();
58
- const hasXiaoMei = await page.locator('text=小恩').isVisible();
59
- const hasXiaoAn = await page.locator('text=睿睿').isVisible();
60
- expect(hasXiaoMing || hasXiaoHua || hasXiaoMei || hasXiaoAn).toBeTruthy();
61
-
62
- // Close modal by clicking backdrop
63
- await page.click('.fixed.inset-0.bg-black\\/50');
64
- await expect(page.locator('h3:has-text("選擇學生")')).not.toBeVisible({ timeout: 5_000 });
65
- });
66
-
67
- test('should open conversation history modal from sidebar (empty for new user)', async ({ page }) => {
68
- test.setTimeout(60_000);
69
- const testUser = `history_test_${Date.now()}`;
70
-
71
- // Register and login
72
- await registerAndLogin(page, testUser);
73
-
74
- // Open sidebar via hamburger menu
75
- await page.click('svg:has(path[d*="M4 6h16"])');
76
-
77
- // Sidebar should show "尚無對話記錄" for new user
78
- await expect(page.locator('text=尚無對話記錄')).toBeVisible({ timeout: 2000 });
79
-
80
- // Close sidebar
81
- await page.keyboard.press('Escape');
82
- });
83
-
84
- test('should open direct coach modal', async ({ page }) => {
85
- test.setTimeout(60_000);
86
- const testUser = `coach_modal_${Date.now()}`;
87
-
88
- // Register and login
89
- await registerAndLogin(page, testUser);
90
-
91
- // Click "教練回顧" card
92
- await page.click('h3:has-text("教練回顧")');
93
-
94
- // Verify modal appears with more specific selector
95
- await expect(page.locator('text=系統將基於您過去 25 天的所有對話記錄,為您提供綜合建議')).toBeVisible();
96
- await expect(page.locator('button:has-text("開始諮詢")')).toBeVisible();
97
- await expect(page.locator('button:has-text("取消")')).toBeVisible();
98
-
99
- // Close modal using cancel button
100
- await page.click('button:has-text("取消")');
101
- await expect(page.locator('text=開始諮詢')).not.toBeVisible({ timeout: 2000 });
102
- });
103
-
104
- test('should create conversation from dashboard and navigate to chat', async ({ page }) => {
105
- test.setTimeout(60_000);
106
- const testUser = `create_conv_${Date.now()}`;
107
-
108
- // Register and login
109
- await registerAndLogin(page, testUser);
110
-
111
- // Click "新對話" card
112
- await page.click('h3:has-text("指定對話演練")');
113
- await expect(page.locator('h3:has-text("選擇學生")')).toBeVisible();
114
-
115
- // Find student name directly and click on the parent card
116
- const studentName = page.locator('text=睿睿').first();
117
- await expect(studentName).toBeVisible({ timeout: 5000 });
118
-
119
- // Click on the student name's parent container (the actual clickable card)
120
- await studentName.click();
121
-
122
- // Should navigate to conversation page
123
- await expect(page).toHaveURL(/\/conversation\//, { timeout: 15_000 });
124
-
125
- // Verify conversation page loaded
126
- await expect(page.locator('h1')).toBeVisible({ timeout: 10_000 });
127
- await expect(page.locator('input#chat-input')).toBeVisible();
128
- });
129
-
130
- test('should show conversation in history after creation', async ({ page }) => {
131
- test.setTimeout(60_000);
132
- const testUser = `conv_history_${Date.now()}`;
133
-
134
- // Register and login
135
- await registerAndLogin(page, testUser);
136
-
137
- // First create a conversation
138
- await page.click('h3:has-text("指定對話演練")');
139
- await expect(page.locator('h3:has-text("選擇學生")')).toBeVisible();
140
-
141
- // Click on student name to create conversation
142
- const studentName = page.locator('text=睿睿').first();
143
- await expect(studentName).toBeVisible({ timeout: 5000 });
144
- await studentName.click();
145
-
146
- await expect(page).toHaveURL(/\/conversation\//, { timeout: 15_000 });
147
-
148
- // Wait for conversation page to fully load and ensure conversation is saved
149
- await expect(page.locator('h1')).toBeVisible({ timeout: 10_000 });
150
- await expect(page.locator('input#chat-input')).toBeVisible({ timeout: 5000 });
151
-
152
- // Go back to dashboard
153
- await page.goto(`${baseURL}/dashboard`);
154
- await expect(page).toHaveURL(`${baseURL}/dashboard`, { timeout: 10_000 });
155
- await expect(page.locator('h1:has-text("SEL Chat Coach")')).toBeVisible({ timeout: 5000 });
156
-
157
- // Open sidebar to check conversation history
158
- await page.click('svg:has(path[d*="M4 6h16"])');
159
-
160
- // Should now show conversations in sidebar (not empty state)
161
- await expect(page.locator('h3:has-text("對話記錄")')).toBeVisible({ timeout: 2000 });
162
-
163
- // Verify conversation exists by checking for student name in sidebar
164
- const hasConversation = await page.locator('text=睿睿').first().isVisible({ timeout: 5_000 }).catch(() => false);
165
- expect(hasConversation).toBeTruthy();
166
- });
167
-
168
- test('should navigate to coach chat from dashboard', async ({ page }) => {
169
- test.setTimeout(60_000);
170
- const testUser = `coach_nav_${Date.now()}`;
171
-
172
- // Register and login
173
- await registerAndLogin(page, testUser);
174
-
175
- // Click "教練回顧" card
176
- await page.click('h3:has-text("教練回顧")');
177
-
178
- // Wait for modal to appear
179
- await expect(page.locator('text=系統將基於您過去 25 天的所有對話記錄,為您提供綜合建議')).toBeVisible();
180
-
181
- // Click "開始諮詢" button
182
- await page.click('button:has-text("開始諮詢")');
183
-
184
- // Should navigate to conversation page (coach conversations now use regular conversation page)
185
- await expect(page).toHaveURL(/\/conversation\//, { timeout: 10_000 });
186
- });
187
-
188
- test('should navigate back to dashboard using sidebar home button', async ({ page }) => {
189
- test.setTimeout(60_000);
190
- const testUser = `sidebar_home_${Date.now()}`;
191
-
192
- // Register and login
193
- await registerAndLogin(page, testUser);
194
-
195
- // Create a conversation and navigate to it
196
- await page.click('h3:has-text("指定對話演練")');
197
- await expect(page.locator('h3:has-text("選擇學生")')).toBeVisible({ timeout: 5000 });
198
- await page.click('text=睿睿');
199
- await expect(page).toHaveURL(/\/conversation\//, { timeout: 15_000 });
200
- await expect(page.locator('input#chat-input')).toBeVisible();
201
-
202
- // Open sidebar
203
- await page.click('svg:has(path[d*="M4 6h16"])');
204
- await expect(page.locator('h3:has-text("對話記錄")')).toBeVisible({ timeout: 2000 });
205
-
206
- // Verify home button exists
207
- const homeButton = page.locator('button:has-text("回到首頁")');
208
- await expect(homeButton).toBeVisible();
209
-
210
- // Click home button
211
- await homeButton.click();
212
-
213
- // Should navigate back to dashboard
214
- await expect(page).toHaveURL(`${baseURL}/dashboard`, { timeout: 10_000 });
215
-
216
- // Sidebar should be closed
217
- await expect(page.locator('text=選單')).not.toBeVisible();
218
- });
219
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/e2e/message-filter.spec.ts DELETED
@@ -1,231 +0,0 @@
1
- import { test, expect } from '@playwright/test';
2
-
3
- const baseURL = process.env.BASE_URL || 'http://localhost:3000';
4
-
5
- // SKIPPED: Filter button is hidden in UI
6
- test.describe.skip('Message Filter Feature', () => {
7
- let sharedUser: string;
8
- let sharedConversationCreated = false;
9
-
10
- test('should filter messages between student and coach', async ({ page }) => {
11
- sharedUser = `filter_test_${Date.now()}`;
12
- test.setTimeout(60_000); // 2 minutes for LLM calls
13
-
14
- // 1. Register and login
15
- await page.goto(`${baseURL}/login`);
16
- await page.click('text=沒有帳號?建立新帳號');
17
- await page.fill('input[name="username"]', sharedUser);
18
- await page.click('button:has-text("建立帳號")');
19
- await expect(page).toHaveURL(`${baseURL}/dashboard`, { timeout: 10_000 });
20
-
21
- // 2. Create a new conversation
22
- await page.click('h3:has-text("指定對話演練")');
23
- await expect(page.locator('h3:has-text("選擇學生")')).toBeVisible();
24
- await page.waitForTimeout(1000);
25
- const studentName = page.locator('text=睿睿').first();
26
- await expect(studentName).toBeVisible();
27
- await studentName.click();
28
- await expect(page).toHaveURL(/\/conversation\//, { timeout: 15_000 });
29
-
30
- // 3. Wait for conversation to load
31
- await expect(page.locator('h1')).toBeVisible({ timeout: 10_000 });
32
- await expect(page.locator('input#chat-input')).toBeVisible();
33
-
34
- // 4. Verify filter button exists in header
35
- const filterButton = page.locator('button[title="篩選訊息"]');
36
- await expect(filterButton).toBeVisible();
37
-
38
- // 5. Send a student message
39
- const studentButton = page.locator('button').filter({ hasText: '🎓' }).first();
40
- await studentButton.click();
41
- const input = page.locator('input#chat-input');
42
- await input.fill('你好睿睿');
43
- await input.press('Enter');
44
-
45
- // Wait for student response
46
- await page.waitForTimeout(2000);
47
- await expect(page.locator('div').filter({ hasText: '🎓' }).first()).toBeVisible({ timeout: 60_000 });
48
-
49
- // 6. Send a coach message (asking for feedback on teaching)
50
- const coachButton = page.locator('button').filter({ hasText: '👨‍🏫' }).first();
51
- await coachButton.click();
52
- await page.waitForTimeout(500);
53
- await input.fill('請給我教學建議');
54
- await input.press('Enter');
55
-
56
- // Wait for coach response
57
- await page.waitForTimeout(2000);
58
- await expect(page.locator('div').filter({ hasText: '👨‍🏫' }).first()).toBeVisible({ timeout: 60_000 });
59
-
60
- // Wait a bit to ensure all messages are rendered
61
- await page.waitForTimeout(2000);
62
-
63
- // 7. Test filter button click (use JavaScript click to bypass overlay)
64
- await filterButton.evaluate((el) => (el as HTMLElement).click());
65
- await page.waitForTimeout(500); // Wait for menu to appear
66
- await expect(page.locator('text=📋 所有訊息')).toBeVisible();
67
- await expect(page.locator('text=🎓 學生訊息')).toBeVisible();
68
- await expect(page.locator('text=👨‍🏫 老師訊息')).toBeVisible();
69
-
70
- // 8. Test filtering to student messages only
71
- await page.click('text=🎓 學生訊息');
72
- await page.waitForTimeout(500);
73
-
74
- // Verify filter is active (yellow indicator dot should be visible)
75
- const filterIndicator = page.locator('button[title="篩選訊息"] .bg-yellow-400');
76
- await expect(filterIndicator).toBeVisible();
77
-
78
- // Count message bubbles - should only show student messages
79
- const studentBubbles = page.locator('div').filter({ hasText: '🎓' });
80
- const coachBubbles = page.locator('div').filter({ hasText: '👨‍🏫' });
81
-
82
- // Student messages should be visible
83
- const studentCount = await studentBubbles.count();
84
- expect(studentCount).toBeGreaterThan(0);
85
-
86
- // 9. Test filtering to coach messages only
87
- await filterButton.evaluate((el) => (el as HTMLElement).click());
88
- await page.waitForTimeout(300);
89
- await page.click('text=👨‍🏫 老師訊息');
90
- await page.waitForTimeout(500);
91
-
92
- // Verify filter indicator still visible
93
- await expect(filterIndicator).toBeVisible();
94
-
95
- // 10. Test filtering back to all messages
96
- await filterButton.evaluate((el) => (el as HTMLElement).click());
97
- await page.waitForTimeout(300);
98
- await page.click('text=📋 所有訊息');
99
- await page.waitForTimeout(500);
100
-
101
- // Filter indicator should not be visible when showing all
102
- await expect(filterIndicator).not.toBeVisible();
103
-
104
- // Both student and coach messages should be visible
105
- const allStudentBubbles = await page.locator('div').filter({ hasText: '🎓' }).count();
106
- const allCoachBubbles = await page.locator('div').filter({ hasText: '👨‍🏫' }).count();
107
-
108
- expect(allStudentBubbles).toBeGreaterThan(0);
109
- expect(allCoachBubbles).toBeGreaterThan(0);
110
- });
111
-
112
- test('should close filter menu when clicking outside', async ({ page }) => {
113
- test.setTimeout(60_000); // 1 minute
114
-
115
- // Login with shared user
116
- await page.goto(`${baseURL}/login`);
117
- await page.fill('input[name="username"]', sharedUser);
118
- await page.fill('input[name="password"]', 'cz-2025');
119
- await page.click('button:has-text("登入")');
120
- await expect(page).toHaveURL(`${baseURL}/dashboard`, { timeout: 10_000 });
121
-
122
- // Navigate to existing conversation
123
- await page.waitForTimeout(1000);
124
- await page.click('h3:has-text("自訂對話演練")');
125
- await page.waitForTimeout(500);
126
- const firstConv = page.locator('text=睿睿').first();
127
- await firstConv.click();
128
- await expect(page).toHaveURL(/\/conversation\//, { timeout: 10_000 });
129
- await expect(page.locator('input#chat-input')).toBeVisible();
130
-
131
- // Open filter menu
132
- const filterButton = page.locator('button[title="篩選訊息"]');
133
- await filterButton.evaluate((el) => (el as HTMLElement).click());
134
- await page.waitForTimeout(300);
135
- await expect(page.locator('text=📋 所有訊息')).toBeVisible();
136
-
137
- // Click outside the menu (click on the header area)
138
- await page.locator('h1').click();
139
- await page.waitForTimeout(300);
140
-
141
- // Menu should be closed
142
- await expect(page.locator('text=📋 所有訊息')).not.toBeVisible();
143
- });
144
-
145
- test('should show empty state when no messages match filter', async ({ page }) => {
146
- test.setTimeout(60_000); // 2 minutes for LLM call
147
-
148
- // Login with shared user
149
- await page.goto(`${baseURL}/login`);
150
- await page.fill('input[name="username"]', sharedUser);
151
- await page.fill('input[name="password"]', 'cz-2025');
152
- await page.click('button:has-text("登入")');
153
- await expect(page).toHaveURL(`${baseURL}/dashboard`, { timeout: 10_000 });
154
-
155
- // Create a new conversation
156
- await page.waitForTimeout(1000);
157
- await page.click('h3:has-text("指定對話演練")');
158
- await page.waitForTimeout(1000);
159
- const studentName = page.locator('text=小許').first();
160
- await expect(studentName).toBeVisible();
161
- await studentName.click();
162
- await expect(page).toHaveURL(/\/conversation\//, { timeout: 15_000 });
163
- await expect(page.locator('input#chat-input')).toBeVisible();
164
-
165
- // Send one coach message first (so we have messages but can filter for empty student messages)
166
- const coachButton = page.locator('button').filter({ hasText: '👨‍🏫' }).first();
167
- await coachButton.click();
168
- await page.waitForTimeout(500);
169
- const input = page.locator('input#chat-input');
170
- await input.fill('測試訊息');
171
- await input.press('Enter');
172
- await page.waitForTimeout(2000);
173
-
174
- // Open filter menu and select student messages (should be empty)
175
- const filterButton = page.locator('button[title="篩選訊息"]');
176
- await filterButton.evaluate((el) => (el as HTMLElement).click());
177
- await page.waitForTimeout(300);
178
- await page.click('text=🎓 學生訊息');
179
- await page.waitForTimeout(500);
180
-
181
- // Should show empty state for student messages (since we only sent coach message)
182
- await page.waitForTimeout(500); // Wait for empty state to render
183
- await expect(page.locator('text=沒有學生訊息')).toBeVisible();
184
- await expect(page.locator('text=🔍')).toBeVisible();
185
-
186
- // Switch to coach filter (should show the coach message)
187
- await filterButton.evaluate((el) => (el as HTMLElement).click());
188
- await page.waitForTimeout(300);
189
- await page.click('text=👨‍🏫 老師訊息');
190
- await page.waitForTimeout(500);
191
-
192
- // Should show coach message (not empty)
193
- const coachMessages = await page.locator('div').filter({ hasText: '👨‍🏫' }).count();
194
- expect(coachMessages).toBeGreaterThan(0);
195
- });
196
-
197
- test('should maintain filter state during conversation', async ({ page }) => {
198
- test.setTimeout(60_000); // 1 minute
199
-
200
- // Login with shared user
201
- await page.goto(`${baseURL}/login`);
202
- await page.fill('input[name="username"]', sharedUser);
203
- await page.fill('input[name="password"]', 'cz-2025');
204
- await page.click('button:has-text("登入")');
205
- await expect(page).toHaveURL(`${baseURL}/dashboard`, { timeout: 10_000 });
206
-
207
- // Navigate to conversation with messages
208
- await page.waitForTimeout(1000);
209
- await page.click('h3:has-text("自訂對話演練")');
210
- await page.waitForTimeout(500);
211
- const firstConv = page.locator('text=睿睿').first();
212
- await firstConv.click();
213
- await expect(page).toHaveURL(/\/conversation\//, { timeout: 10_000 });
214
- await expect(page.locator('input#chat-input')).toBeVisible();
215
-
216
- // Apply student filter
217
- const filterButton = page.locator('button[title="篩選訊息"]');
218
- await filterButton.evaluate((el) => (el as HTMLElement).click());
219
- await page.waitForTimeout(300);
220
- await page.click('text=🎓 學生訊息');
221
- await page.waitForTimeout(500);
222
-
223
- // Verify filter indicator is visible
224
- const filterIndicator = page.locator('button[title="篩選訊息"] .bg-yellow-400');
225
- await expect(filterIndicator).toBeVisible();
226
-
227
- // Verify only student messages are shown (should see student avatar)
228
- const studentBubbles = await page.locator('div').filter({ hasText: '🎓' }).count();
229
- expect(studentBubbles).toBeGreaterThan(0);
230
- });
231
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/e2e/prompts-in-conversations.spec.ts DELETED
@@ -1,343 +0,0 @@
1
- import { test, expect } from '@playwright/test';
2
-
3
- const baseURL = process.env.BASE_URL || 'http://localhost:3000';
4
- const PW = 'cz-2025';
5
- const authHeader = (u: string) => 'Basic ' + Buffer.from(`${u}:${PW}`).toString('base64');
6
-
7
- test.describe('Database-Backed Prompts in Conversations', () => {
8
- test('should create conversation with DB-backed student prompt', async ({ request }) => {
9
- test.setTimeout(90_000);
10
-
11
- const testUser = `prompt_test_${Date.now()}`;
12
-
13
- // Register user
14
- const reg = await request.post(`${baseURL}/api/auth/register`, {
15
- data: { username: testUser },
16
- });
17
- expect(reg.ok()).toBeTruthy();
18
-
19
- // Create conversation with student prompt from database
20
- const convResp = await request.post(`${baseURL}/api/conversations/create`, {
21
- headers: {
22
- Authorization: authHeader(testUser),
23
- 'Content-Type': 'application/json',
24
- },
25
- data: {
26
- studentPromptId: 'ruirui',
27
- coachPromptId: 'satir',
28
- include3ConversationSummary: false,
29
- },
30
- });
31
-
32
- expect(convResp.ok()).toBeTruthy();
33
- const convData = await convResp.json();
34
-
35
- // Verify conversation was created
36
- expect(convData.conversation).toBeTruthy();
37
- expect(convData.conversation.id).toBeTruthy();
38
- expect(convData.conversation.studentPromptId).toBe('ruirui');
39
- expect(convData.conversation.coachPromptId).toBe('satir');
40
-
41
- // Verify systemPrompt is stored (from database)
42
- expect(convData.conversation.systemPrompt).toBeTruthy();
43
- expect(convData.conversation.systemPrompt.length).toBeGreaterThan(50);
44
- });
45
-
46
- test('should use DB prompt when sending messages in conversation', async ({ request }) => {
47
- test.setTimeout(90_000);
48
-
49
- const testUser = `prompt_msg_${Date.now()}`;
50
-
51
- // Register user
52
- await request.post(`${baseURL}/api/auth/register`, { data: { username: testUser } });
53
-
54
- // Create conversation
55
- const convResp = await request.post(`${baseURL}/api/conversations/create`, {
56
- headers: { Authorization: authHeader(testUser), 'Content-Type': 'application/json' },
57
- data: { studentPromptId: 'ruirui', coachPromptId: 'satir' },
58
- });
59
- const convData = await convResp.json();
60
- const conversationId = convData.conversation.id;
61
-
62
- // Send message using the conversation endpoint
63
- const msgResp = await request.post(`${baseURL}/api/conversations/${conversationId}/message`, {
64
- headers: { Authorization: authHeader(testUser), 'Content-Type': 'application/json' },
65
- data: {
66
- messages: [
67
- { id: '1', role: 'user', parts: [{ type: 'text', text: '你好,我是小明' }] },
68
- ],
69
- },
70
- timeout: 60_000,
71
- });
72
-
73
- // Streaming endpoint returns 200
74
- expect(msgResp.status()).toBe(200);
75
- expect(msgResp.headers()['content-type']).toContain('text/event-stream');
76
-
77
- // Verify response has data
78
- const msgBody = await msgResp.text();
79
- expect(msgBody.length).toBeGreaterThan(0);
80
- expect(msgBody).toContain('data:');
81
-
82
- // The student should respond (indicating the prompt is working)
83
- // Response should be in Traditional Chinese as specified in ruirui prompt
84
- // We can't check the exact content since it's streaming, but we verified the format
85
- });
86
-
87
- test('should create conversation and verify prompt fields are derived', async ({ request }) => {
88
- test.setTimeout(60_000);
89
-
90
- const testUser = `prompt_derived_${Date.now()}`;
91
-
92
- await request.post(`${baseURL}/api/auth/register`, { data: { username: testUser } });
93
-
94
- // Create conversation
95
- const convResp = await request.post(`${baseURL}/api/conversations/create`, {
96
- headers: { Authorization: authHeader(testUser), 'Content-Type': 'application/json' },
97
- data: { studentPromptId: 'xiaoxu', coachPromptId: 'satir' },
98
- });
99
-
100
- expect(convResp.ok()).toBeTruthy();
101
- const convData = await convResp.json();
102
-
103
- // Verify derived fields (studentName, coachName) are present
104
- // These are NOT stored in DB, but derived from PromptService
105
- expect(convData.conversation.studentName).toBeTruthy();
106
- expect(convData.conversation.coachName).toBeTruthy();
107
- expect(typeof convData.conversation.studentName).toBe('string');
108
- expect(typeof convData.conversation.coachName).toBe('string');
109
- });
110
-
111
- test('should list conversations with derived prompt names', async ({ request }) => {
112
- test.setTimeout(60_000);
113
-
114
- const testUser = `prompt_list_${Date.now()}`;
115
-
116
- await request.post(`${baseURL}/api/auth/register`, { data: { username: testUser } });
117
-
118
- // Create a conversation
119
- await request.post(`${baseURL}/api/conversations/create`, {
120
- headers: { Authorization: authHeader(testUser), 'Content-Type': 'application/json' },
121
- data: { studentPromptId: 'xiaoen', coachPromptId: 'satir' },
122
- });
123
-
124
- // List conversations
125
- const listResp = await request.get(`${baseURL}/api/conversations`, {
126
- headers: { Authorization: authHeader(testUser) },
127
- });
128
-
129
- expect(listResp.ok()).toBeTruthy();
130
- const listData = await listResp.json();
131
-
132
- expect(listData.conversations).toBeTruthy();
133
- expect(listData.conversations.length).toBeGreaterThan(0);
134
-
135
- // Verify first conversation has derived fields
136
- const conversation = listData.conversations[0];
137
- expect(conversation.studentName).toBeTruthy();
138
- expect(conversation.coachName).toBeTruthy();
139
- });
140
-
141
- test('should handle invalid student prompt ID gracefully', async ({ request }) => {
142
- test.setTimeout(60_000);
143
-
144
- const testUser = `prompt_invalid_${Date.now()}`;
145
-
146
- await request.post(`${baseURL}/api/auth/register`, { data: { username: testUser } });
147
-
148
- // Try to create conversation with invalid student prompt ID
149
- const convResp = await request.post(`${baseURL}/api/conversations/create`, {
150
- headers: { Authorization: authHeader(testUser), 'Content-Type': 'application/json' },
151
- data: { studentPromptId: 'invalid_student_999', coachPromptId: 'satir' },
152
- });
153
-
154
- // Should return error
155
- expect(convResp.status()).toBe(400);
156
- const convData = await convResp.json();
157
- expect(convData.error).toBeTruthy();
158
- });
159
-
160
- test('should handle invalid coach prompt ID gracefully', async ({ request }) => {
161
- test.setTimeout(60_000);
162
-
163
- const testUser = `prompt_invalid_coach_${Date.now()}`;
164
-
165
- await request.post(`${baseURL}/api/auth/register`, { data: { username: testUser } });
166
-
167
- // Try to create conversation with invalid coach prompt ID
168
- const convResp = await request.post(`${baseURL}/api/conversations/create`, {
169
- headers: { Authorization: authHeader(testUser), 'Content-Type': 'application/json' },
170
- data: { studentPromptId: 'ruirui', coachPromptId: 'invalid_coach_999' },
171
- });
172
-
173
- // Should return error
174
- expect(convResp.status()).toBe(400);
175
- const convData = await convResp.json();
176
- expect(convData.error).toBeTruthy();
177
- });
178
-
179
- test('should create new conversation after editing prompt in admin', async ({ request, page }) => {
180
- test.setTimeout(120_000);
181
-
182
- const testUser = `prompt_edit_test_${Date.now()}`;
183
- const testPromptId = `test_edit_prompt_${Date.now()}`;
184
-
185
- await request.post(`${baseURL}/api/auth/register`, { data: { username: testUser } });
186
-
187
- // 1. Create a test prompt via API
188
- const createPromptResp = await request.post(`${baseURL}/api/prompts`, {
189
- headers: { 'Content-Type': 'application/json' },
190
- data: {
191
- id: testPromptId,
192
- type: 'student',
193
- name: `測試學生 Original`,
194
- description: '原始描述',
195
- systemPrompt:
196
- 'IMPORTANT: You MUST respond in Traditional Chinese. You are a test student. Original version. Keep responses SHORT (5-7 words). This is for testing prompt management system.',
197
- tools: ['calculate'],
198
- },
199
- });
200
- expect(createPromptResp.ok()).toBeTruthy();
201
-
202
- // 2. Create conversation with this prompt
203
- const conv1Resp = await request.post(`${baseURL}/api/conversations/create`, {
204
- headers: { Authorization: authHeader(testUser), 'Content-Type': 'application/json' },
205
- data: { studentPromptId: testPromptId, coachPromptId: 'satir' },
206
- });
207
- expect(conv1Resp.ok()).toBeTruthy();
208
- const conv1Data = await conv1Resp.json();
209
- const originalPrompt = conv1Data.conversation.systemPrompt;
210
- expect(originalPrompt).toContain('Original version');
211
-
212
- // 3. Edit the prompt via admin UI
213
- await page.goto(`${baseURL}/admin/prompts/${testPromptId}`);
214
-
215
- const nameInput = page.locator('input#name');
216
- await expect(nameInput).toBeVisible({ timeout: 5000 });
217
- await nameInput.fill(`測試學生 Updated`);
218
-
219
- const systemPromptTextarea = page.locator('textarea#systemPrompt');
220
- await systemPromptTextarea.fill(
221
- 'IMPORTANT: You MUST respond in Traditional Chinese. You are a test student. UPDATED version. Keep responses SHORT (5-7 words). This is for testing prompt management system updates.'
222
- );
223
-
224
- await page.click('button:has-text("儲存")');
225
- await expect(page).toHaveURL(`${baseURL}/admin/prompts`, { timeout: 10_000 });
226
-
227
- // Note: Cache has 5 min TTL, but new conversations will fetch fresh data from DB
228
-
229
- // 4. Create NEW conversation with the updated prompt
230
- const conv2Resp = await request.post(`${baseURL}/api/conversations/create`, {
231
- headers: { Authorization: authHeader(testUser), 'Content-Type': 'application/json' },
232
- data: { studentPromptId: testPromptId, coachPromptId: 'satir' },
233
- });
234
- expect(conv2Resp.ok()).toBeTruthy();
235
- const conv2Data = await conv2Resp.json();
236
- const updatedPrompt = conv2Data.conversation.systemPrompt;
237
- expect(updatedPrompt).toContain('UPDATED version');
238
- expect(updatedPrompt).not.toContain('Original version');
239
-
240
- // 5. Verify old conversation still has original prompt (stored in DB)
241
- const getConv1Resp = await request.get(
242
- `${baseURL}/api/conversations/${conv1Data.conversation.id}`,
243
- {
244
- headers: { Authorization: authHeader(testUser) },
245
- }
246
- );
247
- expect(getConv1Resp.ok()).toBeTruthy();
248
- const getConv1Data = await getConv1Resp.json();
249
- expect(getConv1Data.conversation.systemPrompt).toContain('Original version');
250
-
251
- // 6. Clean up: Delete the test prompt
252
- const deleteResp = await request.delete(`${baseURL}/api/prompts/${testPromptId}`);
253
- expect(deleteResp.ok()).toBeTruthy();
254
- });
255
-
256
- test('should continue working with deactivated prompt (existing conversations)', async ({ request }) => {
257
- test.setTimeout(120_000);
258
-
259
- const testUser = `prompt_deactivate_${Date.now()}`;
260
- const testPromptId = `test_deactivate_${Date.now()}`;
261
-
262
- await request.post(`${baseURL}/api/auth/register`, { data: { username: testUser } });
263
-
264
- // 1. Create a test prompt
265
- await request.post(`${baseURL}/api/prompts`, {
266
- headers: { 'Content-Type': 'application/json' },
267
- data: {
268
- id: testPromptId,
269
- type: 'student',
270
- name: `測試停用學生`,
271
- systemPrompt:
272
- 'IMPORTANT: You MUST respond in Traditional Chinese. You are a test student for deactivation testing. Keep responses SHORT (5-7 words).',
273
- tools: ['calculate'],
274
- },
275
- });
276
-
277
- // 2. Create conversation with this prompt
278
- const convResp = await request.post(`${baseURL}/api/conversations/create`, {
279
- headers: { Authorization: authHeader(testUser), 'Content-Type': 'application/json' },
280
- data: { studentPromptId: testPromptId, coachPromptId: 'satir' },
281
- });
282
- expect(convResp.ok()).toBeTruthy();
283
- const convData = await convResp.json();
284
- const conversationId = convData.conversation.id;
285
-
286
- // 3. Deactivate (soft delete) the prompt
287
- const deleteResp = await request.delete(`${baseURL}/api/prompts/${testPromptId}`);
288
- expect(deleteResp.ok()).toBeTruthy();
289
-
290
- // 4. Verify existing conversation still works (uses stored systemPrompt)
291
- const msgResp = await request.post(`${baseURL}/api/conversations/${conversationId}/message`, {
292
- headers: { Authorization: authHeader(testUser), 'Content-Type': 'application/json' },
293
- data: {
294
- messages: [{ id: '1', role: 'user', parts: [{ type: 'text', text: '你好' }] }],
295
- },
296
- timeout: 60_000,
297
- });
298
-
299
- // Should still work because systemPrompt is stored
300
- expect(msgResp.status()).toBe(200);
301
- const msgBody = await msgResp.text();
302
- expect(msgBody).toContain('data:');
303
-
304
- // 5. Try to create NEW conversation with deactivated prompt
305
- const newConvResp = await request.post(`${baseURL}/api/conversations/create`, {
306
- headers: { Authorization: authHeader(testUser), 'Content-Type': 'application/json' },
307
- data: { studentPromptId: testPromptId, coachPromptId: 'satir' },
308
- });
309
-
310
- // Should fail with 400 error
311
- expect(newConvResp.status()).toBe(400);
312
- const newConvData = await newConvResp.json();
313
- expect(newConvData.error).toBeTruthy();
314
- });
315
-
316
- test('should use correct tools from database prompt', async ({ request }) => {
317
- test.setTimeout(60_000);
318
-
319
- const testUser = `prompt_tools_${Date.now()}`;
320
-
321
- await request.post(`${baseURL}/api/auth/register`, { data: { username: testUser } });
322
-
323
- // Create conversation with ruirui which has tools defined
324
- const convResp = await request.post(`${baseURL}/api/conversations/create`, {
325
- headers: { Authorization: authHeader(testUser), 'Content-Type': 'application/json' },
326
- data: { studentPromptId: 'ruirui', coachPromptId: 'satir' },
327
- });
328
-
329
- expect(convResp.ok()).toBeTruthy();
330
- const convData = await convResp.json();
331
-
332
- // Verify systemPrompt contains tools ({{TOOLS}} placeholder should be interpolated)
333
- const systemPrompt = convData.conversation.systemPrompt;
334
- expect(systemPrompt).toBeTruthy();
335
-
336
- // Should NOT contain the placeholder anymore (should be interpolated)
337
- expect(systemPrompt).not.toContain('{{TOOLS}}');
338
-
339
- // Should contain tool descriptions (if tools are defined in ruirui)
340
- // This is indirect verification - we're checking that interpolation happened
341
- expect(systemPrompt.length).toBeGreaterThan(100);
342
- });
343
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/e2e/quoted-replies.spec.ts DELETED
@@ -1,273 +0,0 @@
1
- import { test, expect } from '@playwright/test';
2
-
3
- const baseURL = process.env.BASE_URL || 'http://localhost:3000';
4
-
5
- // Helper function to register and login
6
- async function registerAndLogin(page: any, username: string) {
7
- await page.goto(`${baseURL}/login`);
8
- await page.click('text=沒有帳號?建立新帳號');
9
- await page.fill('input[name="username"]', username);
10
- await page.click('button:has-text("建立帳號")');
11
- await expect(page).toHaveURL(`${baseURL}/dashboard`, { timeout: 10_000 });
12
- }
13
-
14
- test.describe('Quoted Replies Feature', () => {
15
- test.use({ workers: 1 }); // Run serially to avoid rate limits
16
-
17
- test('should select text from student message and send quoted reply', async ({ page }) => {
18
- test.setTimeout(90_000); // 90 seconds for LLM responses
19
- const testUser = `quote-test-${Date.now()}`;
20
-
21
- // Step 1: Register and create conversation with student
22
- await registerAndLogin(page, testUser);
23
- await expect(page.locator('h1:has-text("SEL Chat Coach")')).toBeVisible();
24
-
25
- // Create student conversation
26
- await page.click('h3:has-text("指定對話演練")');
27
- await expect(page.locator('h3:has-text("選擇學生")')).toBeVisible();
28
-
29
- // Select 睿睿 (ADHD Inattentive)
30
- await page.click('text=睿睿');
31
- await expect(page).toHaveURL(/\/conversation\//, { timeout: 10_000 });
32
- await expect(page.locator('h1')).toBeVisible();
33
-
34
- console.log('✅ Student conversation created');
35
-
36
- // Step 2: Send first message as student
37
- await page.fill('input[type="text"]', 'I need help with my math homework. The problem is 25 + 37 and I am confused.');
38
- await page.click('button[type="submit"]');
39
-
40
- // Wait for student response
41
- const assistantMessages = page.locator('.whitespace-pre-wrap');
42
- await expect(assistantMessages.first()).toBeVisible({ timeout: 60000 });
43
-
44
- console.log('✅ Student responded');
45
-
46
- // Step 3: Select text from student's response using triple-click (more reliable in tests)
47
- // Find the student's message bubble (white background with blue border)
48
- const studentMessageBubble = page.locator('.bg-white.border.border-blue-100').first();
49
- await expect(studentMessageBubble).toBeVisible();
50
-
51
- // Get the text content
52
- const messageText = await studentMessageBubble.textContent();
53
- console.log('Student message:', messageText);
54
-
55
- // Select text by triple-clicking (selects entire text, more reliable than drag)
56
- const textElement = studentMessageBubble.locator('.whitespace-pre-wrap').first();
57
- await textElement.click({ clickCount: 3 });
58
- await expect(page.locator('.bg-gray-50.border-l-4.border-blue-500')).toBeVisible({ timeout: 2000 });
59
-
60
- // Verify selection was created
61
- const selectedText = await page.evaluate(() => {
62
- return window.getSelection()?.toString() || '';
63
- });
64
- console.log('Selected text:', selectedText);
65
- expect(selectedText.length).toBeGreaterThan(0);
66
-
67
- // Step 4: Verify quote preview appears
68
- const quotePreview = page.locator('.bg-gray-50.border-l-4.border-blue-500');
69
- await expect(quotePreview).toBeVisible({ timeout: 5000 });
70
- await expect(quotePreview.locator('text=回覆:')).toBeVisible();
71
-
72
- console.log('✅ Quote preview displayed');
73
-
74
- // Step 5: Switch to coach and send reply with quote
75
- await page.click('button[title="詢問教練"]');
76
- await page.fill('input[type="text"]', 'Can you help explain this to the student?');
77
- await page.click('button[type="submit"]');
78
-
79
- // Verify the quoted message appears in the chat
80
- const quotedMessage = page.locator('[data-message-id]').filter({ has: page.locator('text=回覆:') }).first();
81
- await expect(quotedMessage).toBeVisible({ timeout: 60000 });
82
-
83
- console.log('✅ Message with quote sent successfully');
84
-
85
- // Step 6: Verify quote is saved and displayed
86
- const quotedTextDisplay = quotedMessage.locator('.border-l-2');
87
- await expect(quotedTextDisplay).toBeVisible();
88
-
89
- console.log('✅ Quoted text displayed in message bubble');
90
-
91
- console.log('✅✅✅ Quoted reply test passed!');
92
- });
93
-
94
- test('should clear quote before sending', async ({ page }) => {
95
- test.setTimeout(90_000);
96
- const testUser = `quote-clear-${Date.now()}`;
97
-
98
- // Register and create conversation
99
- await registerAndLogin(page, testUser);
100
- await page.click('h3:has-text("指定對話演練")');
101
- await expect(page.locator('h3:has-text("選擇學生")')).toBeVisible();
102
- await page.click('text=睿睿');
103
- await expect(page).toHaveURL(/\/conversation\//, { timeout: 10_000 });
104
-
105
- console.log('✅ Conversation created');
106
-
107
- // Send message and get response
108
- await page.fill('input[type="text"]', 'Tell me about addition.');
109
- await page.click('button[type="submit"]');
110
-
111
- // Wait for response
112
- const assistantMessages = page.locator('.whitespace-pre-wrap');
113
- await expect(assistantMessages.first()).toBeVisible({ timeout: 60000 });
114
-
115
- console.log('✅ Got student response');
116
-
117
- // Select text using triple-click (more reliable than drag)
118
- const studentMessageBubble = page.locator('.bg-white.border.border-blue-100').first();
119
- const textElement = studentMessageBubble.locator('.whitespace-pre-wrap').first();
120
- await textElement.click({ clickCount: 3 });
121
- await expect(page.locator('.bg-gray-50.border-l-4.border-blue-500')).toBeVisible({ timeout: 2000 });
122
-
123
- // Verify quote preview appears
124
- const quotePreview = page.locator('.bg-gray-50.border-l-4.border-blue-500');
125
- await expect(quotePreview).toBeVisible();
126
-
127
- console.log('✅ Quote preview displayed');
128
-
129
- // Click clear button
130
- const clearButton = quotePreview.locator('button[aria-label="清除引用"]');
131
- await expect(clearButton).toBeVisible();
132
- await clearButton.click();
133
-
134
- // Verify quote preview is gone
135
- await expect(quotePreview).not.toBeVisible();
136
-
137
- console.log('✅ Quote cleared successfully');
138
-
139
- // Send message without quote
140
- await page.fill('input[type="text"]', 'This message should have no quote.');
141
- await page.click('button[type="submit"]');
142
- await expect(page.locator('.bg-gradient-to-br.from-blue-500.to-blue-600').last()).toBeVisible({ timeout: 5000 });
143
-
144
- // Verify message was sent without quoted text indicator
145
- const lastUserMessage = page.locator('.bg-gradient-to-br.from-blue-500.to-blue-600').last();
146
- await expect(lastUserMessage).toBeVisible();
147
-
148
- // Should NOT have the "回覆:" label
149
- const hasQuoteLabel = await lastUserMessage.locator('text=回覆:').count();
150
- expect(hasQuoteLabel).toBe(0);
151
-
152
- console.log('✅✅✅ Clear quote test passed!');
153
- });
154
-
155
- test('should persist quoted messages after page reload', async ({ page }) => {
156
- test.setTimeout(90_000);
157
- const testUser = `quote-persist-${Date.now()}`;
158
-
159
- // Create conversation and send quoted reply
160
- await registerAndLogin(page, testUser);
161
- await page.click('h3:has-text("指定對話演練")');
162
- await expect(page.locator('h3:has-text("選擇學生")')).toBeVisible();
163
- await page.click('text=小許');
164
- await expect(page).toHaveURL(/\/conversation\//, { timeout: 10_000 });
165
-
166
- console.log('✅ Conversation created');
167
-
168
- // Send message
169
- await page.fill('input[type="text"]', 'What is your favorite subject?');
170
- await page.click('button[type="submit"]');
171
-
172
- // Wait for response
173
- await expect(page.locator('.whitespace-pre-wrap').first()).toBeVisible({ timeout: 60000 });
174
-
175
- // Create a quote using triple-click (more reliable than drag)
176
- const studentMessageBubble = page.locator('.bg-white.border.border-blue-100').first();
177
- const textElement = studentMessageBubble.locator('.whitespace-pre-wrap').first();
178
- await textElement.click({ clickCount: 3 });
179
- await expect(page.locator('.bg-gray-50.border-l-4.border-blue-500')).toBeVisible({ timeout: 2000 });
180
-
181
- // Send quoted reply
182
- await page.click('button[title="詢問教練"]');
183
- await page.fill('input[type="text"]', 'That is interesting!');
184
- await page.click('button[type="submit"]');
185
-
186
- // Verify quote is shown
187
- const quotedMessage = page.locator('[data-message-id]').filter({ has: page.locator('text=回覆:') }).first();
188
- await expect(quotedMessage).toBeVisible({ timeout: 10000 });
189
-
190
- console.log('✅ Quoted message sent');
191
-
192
- // Get current URL (conversation ID)
193
- const currentUrl = page.url();
194
-
195
- // Reload page
196
- await page.reload();
197
- await page.waitForLoadState('networkidle');
198
- await expect(page).toHaveURL(currentUrl);
199
-
200
- console.log('✅ Page reloaded');
201
-
202
- // Verify quoted message is still displayed
203
- const quotedMessageAfterReload = page.locator('[data-message-id]').filter({ has: page.locator('text=回覆:') }).first();
204
- await expect(quotedMessageAfterReload).toBeVisible({ timeout: 10000 });
205
-
206
- // Verify the quoted text display is still there
207
- const quotedTextDisplay = quotedMessageAfterReload.locator('.border-l-2');
208
- await expect(quotedTextDisplay).toBeVisible();
209
-
210
- console.log('✅✅✅ Quote persistence test passed!');
211
- });
212
-
213
- test('should handle multiple quoted replies in same conversation', async ({ page }) => {
214
- test.setTimeout(120_000); // 2 minutes
215
- const testUser = `quote-multi-${Date.now()}`;
216
-
217
- // Create conversation
218
- await registerAndLogin(page, testUser);
219
- await page.click('h3:has-text("指定對話演練")');
220
- await expect(page.locator('h3:has-text("選擇學生")')).toBeVisible();
221
- await page.click('text=小恩');
222
- await expect(page).toHaveURL(/\/conversation\//, { timeout: 10_000 });
223
-
224
- console.log('✅ Conversation created');
225
-
226
- // Send first message
227
- await page.fill('input[type="text"]', 'Can you help me with science?');
228
- await page.click('button[type="submit"]');
229
- await expect(page.locator('.whitespace-pre-wrap').first()).toBeVisible({ timeout: 60000 });
230
-
231
- console.log('✅ First response received');
232
-
233
- // Create first quote using triple-click (more reliable than drag)
234
- const firstStudentBubble = page.locator('.bg-white.border.border-blue-100').first();
235
- const textElement1 = firstStudentBubble.locator('.whitespace-pre-wrap').first();
236
- await textElement1.click({ clickCount: 3 });
237
- await expect(page.locator('.bg-gray-50.border-l-4.border-blue-500')).toBeVisible({ timeout: 2000 });
238
-
239
- await page.click('button[title="詢問教練"]');
240
- await page.fill('input[type="text"]', 'First quoted reply');
241
- await page.click('button[type="submit"]');
242
-
243
- console.log('✅ First quoted reply sent');
244
-
245
- // Send another student message
246
- await page.click('button[title="與學生對話"]');
247
- await page.fill('input[type="text"]', 'What about math?');
248
- await page.click('button[type="submit"]');
249
- await expect(page.locator('.bg-white.border.border-blue-100').nth(1)).toBeVisible({ timeout: 60000 });
250
-
251
- console.log('✅ Second response received');
252
-
253
- // Create second quote using triple-click (more reliable than drag)
254
- const secondStudentBubble = page.locator('.bg-white.border.border-blue-100').nth(1);
255
- const textElement2 = secondStudentBubble.locator('.whitespace-pre-wrap').first();
256
- await textElement2.click({ clickCount: 3 });
257
- await expect(page.locator('.bg-gray-50.border-l-4.border-blue-500')).toBeVisible({ timeout: 2000 });
258
-
259
- await page.click('button[title="詢問教練"]');
260
- await page.fill('input[type="text"]', 'Second quoted reply');
261
- await page.click('button[type="submit"]');
262
-
263
- console.log('✅ Second quoted reply sent');
264
-
265
- // Verify both quoted messages exist
266
- const quotedMessages = page.locator('[data-message-id]').filter({ has: page.locator('text=回覆:') });
267
- const count = await quotedMessages.count();
268
- expect(count).toBeGreaterThanOrEqual(2);
269
-
270
- console.log(`✅ Found ${count} quoted messages`);
271
- console.log('✅✅✅ Multiple quotes test passed!');
272
- });
273
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/e2e/{removed-template-handling.spec.ts → removed-template-handling.api.spec.ts} RENAMED
File without changes
tests/e2e/{response-id-expiration.spec.ts → response-id-expiration.api.spec.ts} RENAMED
File without changes
tests/e2e/speaker-auto-detection.spec.ts DELETED
@@ -1,151 +0,0 @@
1
- import { test, expect } from '@playwright/test';
2
-
3
- const baseURL = process.env.BASE_URL || 'http://localhost:3000';
4
-
5
- // Helper function to register and login
6
- async function registerAndLogin(page: any, username: string) {
7
- await page.goto(`${baseURL}/login`);
8
- await page.click('text=沒有帳號?建立新帳號');
9
- await page.fill('input[name="username"]', username);
10
- await page.click('button:has-text("建立帳號")');
11
- await expect(page).toHaveURL(`${baseURL}/dashboard`, { timeout: 10_000 });
12
- }
13
-
14
- test.describe('Speaker Auto-Detection on Page Load', () => {
15
- test.use({ workers: 1 }); // Run serially to avoid rate limits
16
-
17
- test('should auto-detect student speaker after student responded last', async ({ page }) => {
18
- test.setTimeout(90_000); // 90 seconds for LLM responses
19
- const testUser = `speaker_detect_student_${Date.now()}`;
20
-
21
- // Step 1: Register and login
22
- await registerAndLogin(page, testUser);
23
- console.log('✅ Logged in successfully');
24
-
25
- // Step 2: Create student conversation
26
- await page.click('h3:has-text("指定對話演練")');
27
- await expect(page.locator('h3:has-text("選擇學生")')).toBeVisible();
28
- await page.click('text=睿睿');
29
- await expect(page).toHaveURL(/\/conversation\//, { timeout: 10_000 });
30
- await expect(page.locator('input#chat-input')).toBeVisible();
31
-
32
- console.log('✅ Student conversation created');
33
-
34
- // Step 3: Send message to student
35
- await page.fill('input[type="text"]', 'Hello, can you help me?');
36
- await page.click('button[type="submit"]');
37
-
38
- // Wait for student response
39
- const studentResponse = page.locator('.bg-white.border.border-blue-100').first();
40
- await expect(studentResponse).toBeVisible({ timeout: 60000 });
41
-
42
- console.log('✅ Student responded');
43
-
44
- // Step 4: Navigate away to dashboard
45
- await page.goto(`${baseURL}/dashboard`);
46
- await expect(page.locator('h1:has-text("SEL Chat Coach")')).toBeVisible();
47
-
48
- console.log('✅ Navigated to dashboard');
49
-
50
- // Step 5: Return to conversation and check speaker auto-detection
51
- await page.click('svg:has(path[d*="M4 6h16"])');
52
- await expect(page.locator('h3:has-text("對話記錄")')).toBeVisible();
53
-
54
- const conversation = page.locator('.space-y-1 > div').filter({ hasText: '睿睿' }).first();
55
- await expect(conversation).toBeVisible();
56
- await conversation.click();
57
-
58
- await expect(page).toHaveURL(/\/conversation\//, { timeout: 10_000 });
59
- await expect(page.locator('input#chat-input')).toBeVisible();
60
-
61
- console.log('✅ Returned to conversation');
62
-
63
- // Step 6: Verify speaker was auto-detected to continue with student
64
- // Since last message was from student (assistant), user should continue talking to student
65
- // We can verify this by checking console logs or by trying to send a message
66
- // For now, let's just verify the page loaded and we can interact
67
-
68
- // Check console for our debug log
69
- const logs: string[] = [];
70
- page.on('console', msg => {
71
- if (msg.text().includes('[FRONTEND]')) {
72
- logs.push(msg.text());
73
- }
74
- });
75
-
76
- // Trigger a reload to see the logs
77
- await page.reload();
78
- await expect(page.locator('input#chat-input')).toBeVisible();
79
-
80
- console.log('Console logs:', logs);
81
- console.log('✅✅✅ Speaker auto-detection test passed!');
82
- });
83
-
84
- test('should auto-detect coach speaker after coach responded last', async ({ page }) => {
85
- test.setTimeout(90_000);
86
- const testUser = `speaker_detect_coach_${Date.now()}`;
87
-
88
- // Step 1: Register and login
89
- await registerAndLogin(page, testUser);
90
-
91
- // Step 2: Create student conversation
92
- await page.click('h3:has-text("指定對話演練")');
93
- await expect(page.locator('h3:has-text("選擇學生")')).toBeVisible();
94
- await page.click('text=睿睿');
95
- await expect(page).toHaveURL(/\/conversation\//, { timeout: 10_000 });
96
- await expect(page.locator('input#chat-input')).toBeVisible();
97
-
98
- console.log('✅ Student conversation created');
99
-
100
- // Step 3: Send message to student (triggers student response)
101
- await page.fill('input[type="text"]', 'Hello');
102
- await page.click('button[type="submit"]');
103
-
104
- // Wait for student response
105
- await expect(page.locator('.bg-white.border.border-blue-100').first()).toBeVisible({ timeout: 60000 });
106
-
107
- console.log('✅ Student responded');
108
-
109
- // Step 4: Manually switch to coach and send message
110
- // Note: Speaker toggle is hidden, so we need to check if we can still switch programmatically
111
- // Since UI is hidden, we'll skip this test for now and just verify basic auto-detection works
112
-
113
- console.log('⚠️ Skipping coach speaker test - speaker toggle UI is hidden');
114
- });
115
-
116
- test('should not auto-detect speaker for coach_direct conversations', async ({ page }) => {
117
- test.setTimeout(90_000);
118
- const testUser = `speaker_detect_coach_direct_${Date.now()}`;
119
-
120
- // Step 1: Register and login
121
- await registerAndLogin(page, testUser);
122
-
123
- // Step 2: Create coach-direct conversation
124
- await page.click('h3:has-text("教練回顧")');
125
- await expect(page.locator('text=開始諮詢')).toBeVisible({ timeout: 5_000 });
126
- await page.click('button:has-text("開始諮詢")');
127
-
128
- await expect(page).toHaveURL(/\/conversation\//, { timeout: 10_000 });
129
- await expect(page.locator('h1')).toBeVisible({ timeout: 10_000 });
130
-
131
- console.log('✅ Coach-direct conversation created');
132
-
133
- // Step 3: Send message
134
- await page.fill('input[type="text"]', 'I need coaching advice');
135
- await page.click('button[type="submit"]');
136
-
137
- // Wait for coach response
138
- const coachResponse = page.locator('.bg-gradient-to-br.from-purple-50').first();
139
- await expect(coachResponse).toBeVisible({ timeout: 60000 });
140
-
141
- console.log('✅ Coach responded');
142
-
143
- // Step 4: Reload page
144
- await page.reload();
145
- await expect(page.locator('input#chat-input')).toBeVisible();
146
-
147
- // Verify no speaker auto-detection happened (coach_direct doesn't need it)
148
- // This is indicated by studentPromptId === 'coach_direct'
149
- console.log('✅✅✅ Coach-direct conversation correctly skipped auto-detection!');
150
- });
151
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/e2e/speaker-toggle-bug.spec.ts DELETED
@@ -1,126 +0,0 @@
1
- import { test, expect } from '@playwright/test';
2
- import { execSync } from 'child_process';
3
-
4
- const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000';
5
-
6
- // Helper function to register and login via UI
7
- async function registerAndLogin(page: any, username: string) {
8
- await page.goto(`${API_URL}/login`);
9
- await page.click('text=沒有帳號?建立新帳號');
10
- await page.fill('input[name="username"]', username);
11
- await page.click('button:has-text("建立帳號")');
12
- await expect(page).toHaveURL(`${API_URL}/dashboard`, { timeout: 10_000 });
13
- }
14
-
15
- // SKIPPED: Speaker toggle buttons are hidden in UI
16
- test.describe.skip('Speaker Toggle Bug After Coach Ad', () => {
17
- test('should allow switching back to student after accepting coach ad prompt', async ({ page }) => {
18
- test.setTimeout(120_000); // 2 minutes for LLM responses
19
- const testUser = `speaker-toggle-bug-${Date.now()}`;
20
-
21
- console.log('🧪 Starting speaker toggle bug reproduction test...');
22
-
23
- // Step 1: Register and login via UI
24
- console.log('📝 Step 1: Registering and logging in...');
25
- await registerAndLogin(page, testUser);
26
- console.log('✅ Logged in successfully');
27
-
28
- // Step 2: Create conversation via UI
29
- console.log('📝 Step 2: Creating student conversation...');
30
- await page.click('h3:has-text("指定對話演練")');
31
- await expect(page.locator('h3:has-text("選擇學生")')).toBeVisible();
32
- await page.click('text=睿睿');
33
- await expect(page).toHaveURL(/\/conversation\//, { timeout: 10_000 });
34
- await page.waitForTimeout(1000);
35
-
36
- // Get conversation ID from URL
37
- const convId = page.url().match(/\/conversation\/([^/?]+)/)?.[1];
38
- expect(convId).toBeTruthy();
39
- console.log(`✅ Created conversation: ${convId}`);
40
-
41
- // Step 3: Verify initial state - student button should be active
42
- console.log('📝 Step 3: Verifying initial speaker state...');
43
- const studentButton = page.locator('button[title="與學生對話"]');
44
- const coachButton = page.locator('button[title="詢問教練"]');
45
-
46
- await expect(studentButton).toBeVisible();
47
- await expect(coachButton).toBeVisible();
48
- await expect(studentButton).toHaveClass(/from-blue-400/);
49
- console.log('✅ Initial state: student button active');
50
-
51
- // Step 4: Set message count to 14 via SQLite (so next message will be #15)
52
- console.log('📝 Step 4: Setting message count to 14 via SQLite...');
53
- execSync(
54
- `sqlite3 ./data/app.db "UPDATE conversations SET user_to_student_message_count = 14 WHERE id = '${convId}';"`,
55
- { encoding: 'utf-8' }
56
- );
57
- console.log('✅ Message count set to 14');
58
-
59
- // Step 5: Send 15th message (triggers coach ad prompt)
60
- console.log('📝 Step 5: Sending 15th message to trigger coach ad...');
61
- await page.fill('input[type="text"]', 'Help me with my homework please');
62
- await page.click('button[type="submit"]');
63
-
64
- // Wait for student response to complete (indicates streaming is done)
65
- const studentResponse = page.locator('.bg-white.border.border-blue-100').first();
66
- await expect(studentResponse).toBeVisible({ timeout: 60_000 });
67
- console.log('✅ Student response received');
68
-
69
- // Wait for modal to appear
70
- await page.waitForTimeout(2000);
71
-
72
- // Step 6: Verify coach ad modal appears
73
- console.log('📝 Step 6: Verifying coach ad modal appeared...');
74
- await expect(page.locator('text=諮詢教練建議')).toBeVisible({ timeout: 10_000 });
75
- await expect(page.locator('text=需要向教練尋求建議嗎')).toBeVisible();
76
- console.log('✅ Coach ad modal appeared');
77
-
78
- // Step 7: Accept the coach ad prompt
79
- console.log('📝 Step 7: Accepting coach ad prompt...');
80
- const okButton = page.locator('button:has-text("好的,諮詢教練")');
81
- await expect(okButton).toBeVisible();
82
- await okButton.click();
83
-
84
- // Wait for coach message generation and response
85
- await page.waitForTimeout(5000);
86
-
87
- // Wait for coach response (purple gradient background)
88
- const coachMessage = page.locator('.bg-gradient-to-br.from-purple-50');
89
- await expect(coachMessage.first()).toBeVisible({ timeout: 60_000 });
90
- console.log('✅ Coach message received');
91
-
92
- // Step 8: Verify speaker automatically switched to coach
93
- console.log('📝 Step 8: Verifying speaker switched to coach...');
94
- await page.waitForTimeout(1000);
95
- await expect(coachButton).toHaveClass(/from-purple-400/);
96
- await expect(studentButton).toHaveClass(/bg-gray-200/);
97
- console.log('✅ Speaker automatically switched to coach mode');
98
-
99
- // Step 9: THE BUG TEST - Try to manually click student button to switch back
100
- console.log('📝 Step 9: Testing manual switch back to student (BUG REPRODUCTION)...');
101
- await studentButton.click();
102
- await page.waitForTimeout(1000);
103
-
104
- // Step 10: Verify student button is now active (THIS SHOULD PASS AFTER FIX)
105
- console.log('📝 Step 10: Verifying student button is active...');
106
- await expect(studentButton).toHaveClass(/from-blue-400/, {
107
- timeout: 5_000
108
- });
109
- await expect(coachButton).toHaveClass(/bg-gray-200/);
110
- console.log('✅ Successfully switched back to student mode!');
111
-
112
- // Step 11: Send a message as student to verify it works
113
- console.log('📝 Step 11: Sending message as student to verify...');
114
- await page.fill('input[type="text"]', 'Testing student mode');
115
- await page.click('button[type="submit"]');
116
-
117
- // Wait for student response
118
- await page.waitForTimeout(3000);
119
- const finalStudentResponse = page.locator('.bg-white.border.border-blue-100').last();
120
- await expect(finalStudentResponse).toBeVisible({ timeout: 60_000 });
121
- console.log('✅ Student message sent and response received!');
122
-
123
- console.log('');
124
- console.log('🎉🎉🎉 SPEAKER TOGGLE BUG TEST PASSED! 🎉🎉🎉');
125
- });
126
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/e2e/{title-generation-logic.spec.ts → title-generation-logic.api.spec.ts} RENAMED
File without changes
tests/e2e/ui-auth-redirect.spec.ts DELETED
@@ -1,142 +0,0 @@
1
- import { test, expect } from '@playwright/test';
2
-
3
- const baseURL = process.env.BASE_URL || 'http://localhost:3000';
4
-
5
- test.describe('UI Authentication & Auto-Redirect Tests', () => {
6
- test.describe('Auto-redirect to /login on 401', () => {
7
- test('Should redirect to /login when accessing dashboard without auth', async ({ page }) => {
8
- test.setTimeout(30_000);
9
-
10
- // 1. Go directly to dashboard without logging in
11
- await page.goto(`${baseURL}/dashboard`);
12
-
13
- // 2. Should be redirected to /login via useEffect
14
- await expect(page).toHaveURL(`${baseURL}/login`, { timeout: 10_000 });
15
-
16
- // Verify login form is shown
17
- await expect(page.locator('h1:has-text("SEL 對話教練")')).toBeVisible();
18
- await expect(page.locator('input[name="username"]')).toBeVisible();
19
- });
20
-
21
- test('Should redirect to /login when accessing conversation page without auth', async ({ page }) => {
22
- test.setTimeout(30_000);
23
-
24
- // 1. Try to access a conversation page directly
25
- const fakeConvId = '00000000-0000-0000-0000-000000000000';
26
- await page.goto(`${baseURL}/conversation/${fakeConvId}`);
27
-
28
- // 2. Should be redirected to /login via useEffect
29
- await expect(page).toHaveURL(`${baseURL}/login`, { timeout: 10_000 });
30
-
31
- // Verify login form is shown
32
- await expect(page.locator('h1:has-text("SEL 對話教練")')).toBeVisible();
33
- await expect(page.locator('input[name="username"]')).toBeVisible();
34
- });
35
- });
36
-
37
- test.describe('Auth state persistence', () => {
38
- test('Auth state persists across page reloads', async ({ page }) => {
39
- test.setTimeout(60_000);
40
- const testUser = `persist_test_${Date.now()}`;
41
-
42
- // 1. Register and login
43
- await page.goto(`${baseURL}/login`);
44
- await page.click('text=沒有帳號?建立新帳號');
45
- await page.fill('input[name="username"]', testUser);
46
- await page.click('button:has-text("建立帳號")');
47
- await expect(page).toHaveURL(`${baseURL}/dashboard`, { timeout: 10_000 });
48
-
49
- // 2. Verify login state
50
- await expect(page.locator(`text=你好,${testUser}!`)).toBeVisible();
51
-
52
- // 3. Reload the page
53
- await page.reload();
54
-
55
- // 4. Should still be logged in and on dashboard
56
- await expect(page).toHaveURL(`${baseURL}/dashboard`, { timeout: 10_000 });
57
- await expect(page.locator(`text=你好,${testUser}!`)).toBeVisible();
58
-
59
- // 5. Navigate to home page
60
- await page.goto(`${baseURL}/`);
61
-
62
- // 6. Should redirect to dashboard (since user is logged in)
63
- await expect(page).toHaveURL(`${baseURL}/dashboard`, { timeout: 10_000 });
64
- });
65
-
66
- test('Logout clears auth state and redirects to login', async ({ page }) => {
67
- test.setTimeout(60_000);
68
- const testUser = `logout_test_${Date.now()}`;
69
-
70
- // 1. Register and login
71
- await page.goto(`${baseURL}/login`);
72
- await page.click('text=沒有帳號?建立新帳號');
73
- await page.fill('input[name="username"]', testUser);
74
- await page.click('button:has-text("建立帳號")');
75
- await expect(page).toHaveURL(`${baseURL}/dashboard`, { timeout: 10_000 });
76
-
77
- // 2. Open sidebar
78
- const menuButton = page.locator('button').filter({ has: page.locator('svg path[d*="M4 6h16"]') });
79
- await menuButton.click();
80
-
81
- // 3. Wait for sidebar to appear
82
- await expect(page.locator('h2:has-text("選單")')).toBeVisible();
83
-
84
- // 4. Click logout
85
- await page.click('button:has-text("登出")');
86
-
87
- // 5. Should be redirected to /login
88
- await expect(page).toHaveURL(`${baseURL}/login`, { timeout: 10_000 });
89
-
90
- // 6. Verify auth credentials are cleared
91
- const hasCredentials = await page.evaluate(() => {
92
- return localStorage.getItem('auth_credentials') !== null;
93
- });
94
- expect(hasCredentials).toBeFalsy();
95
-
96
- // 7. Trying to access dashboard should redirect back to login
97
- await page.goto(`${baseURL}/dashboard`);
98
- await expect(page).toHaveURL(`${baseURL}/login`, { timeout: 10_000 });
99
- });
100
- });
101
-
102
- test.describe('useAuthFetch hook behavior', () => {
103
- test('useAuthFetch automatically adds auth headers', async ({ page, request }) => {
104
- test.setTimeout(90_000);
105
- const testUser = `fetch_test_${Date.now()}`;
106
-
107
- // 1. Register via API
108
- const PW = 'cz-2025';
109
- await request.post(`${baseURL}/api/auth/register`, {
110
- data: { username: testUser },
111
- });
112
-
113
- // 2. Login via UI
114
- await page.goto(`${baseURL}/login`);
115
- await page.fill('input[name="username"]', testUser);
116
- await page.fill('input[name="password"]', PW);
117
- await page.click('button:has-text("登入")');
118
- await expect(page).toHaveURL(/\/(dashboard)?$/, { timeout: 10_000 });
119
-
120
- // Navigate to dashboard if needed
121
- if (!(await page.url().includes('dashboard'))) {
122
- await page.goto(`${baseURL}/dashboard`);
123
- }
124
-
125
- // 3. Trigger a fetch operation (create conversation)
126
- await page.click('h3:has-text("指定對話演練")');
127
- await expect(page.locator('h3:has-text("選擇學生")')).toBeVisible();
128
-
129
- // Click on the clickable student card (not just any div with emoji)
130
- // Use a selector that targets the outer card with cursor-pointer class
131
- const firstStudentCard = page.locator('.cursor-pointer').filter({ hasText: '🎓' }).first();
132
- await firstStudentCard.click();
133
-
134
- // 4. Should succeed and navigate to conversation
135
- await expect(page).toHaveURL(/\/conversation\/[a-f0-9-]+/, { timeout: 15_000 });
136
-
137
- // 5. Verify conversation loaded successfully (auth header was added)
138
- // The placeholder contains "對話" (conversation) not "輸入" (input)
139
- await expect(page.locator('input[placeholder*="對話"]')).toBeVisible({ timeout: 10_000 });
140
- });
141
- });
142
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/e2e/verify-no-nested-buttons.spec.ts DELETED
@@ -1,28 +0,0 @@
1
- import { test, expect } from '@playwright/test';
2
-
3
- const baseURL = process.env.BASE_URL || 'http://localhost:3000';
4
-
5
- test.describe('Hydration Error Fix Verification', () => {
6
- test('should verify no nested buttons exist in entire app', async ({ page }) => {
7
- test.setTimeout(60_000);
8
-
9
- // Just visit the login page and check for nested buttons
10
- await page.goto(`${baseURL}/login`);
11
-
12
- // Check entire page for any nested buttons
13
- const nestedButtonsCount = await page.locator('button button').count();
14
-
15
- console.log(`Found ${nestedButtonsCount} nested button(s) on login page`);
16
- expect(nestedButtonsCount).toBe(0);
17
-
18
- // Visit dashboard (will redirect to login but that's fine for structure check)
19
- await page.goto(`${baseURL}/dashboard`);
20
- await page.waitForLoadState('domcontentloaded');
21
-
22
- const nestedOnDashboard = await page.locator('button button').count();
23
- console.log(`Found ${nestedOnDashboard} nested button(s) on dashboard page`);
24
- expect(nestedOnDashboard).toBe(0);
25
-
26
- console.log('✅ No nested buttons found in app - hydration error fixed!');
27
- });
28
- });